1
0
mirror of https://github.com/tooot-app/app synced 2025-06-05 22:19:13 +02:00
This commit is contained in:
Zhiyuan Zheng
2021-01-01 16:48:16 +01:00
parent 5473fcb770
commit 6aed76ac65
38 changed files with 632 additions and 637 deletions

View File

@ -1,5 +1,5 @@
import { Feather } from '@expo/vector-icons'
import React, { useLayoutEffect, useMemo } from 'react'
import React, { useEffect, useMemo } from 'react'
import {
Pressable,
StyleProp,
@ -46,7 +46,7 @@ const Button: React.FC<Props> = ({
}) => {
const { theme } = useTheme()
useLayoutEffect(() => layoutAnimation(), [content, loading, disabled])
useEffect(() => layoutAnimation(), [content, loading, disabled])
const loadingSpinkit = useMemo(
() => (

4
src/components/Parse.tsx Normal file
View File

@ -0,0 +1,4 @@
import ParseEmojis from './Parse/Emojis'
import ParseHTML from './Parse/HTML'
export { ParseEmojis, ParseHTML }

View File

@ -0,0 +1,70 @@
import React from 'react'
import { Image, StyleSheet, Text } from 'react-native'
import { useTheme } from '@utils/styles/ThemeManager'
import { StyleConstants } from '@utils/styles/constants'
const regexEmoji = new RegExp(/(:[A-Za-z0-9_]+:)/)
export interface Props {
content: string
emojis?: Mastodon.Emoji[]
size?: 'S' | 'M' | 'L'
fontBold?: boolean
}
const ParseEmojis: React.FC<Props> = ({
content,
emojis,
size = 'M',
fontBold = false
}) => {
const { theme } = useTheme()
const styles = StyleSheet.create({
text: {
color: theme.primary,
...StyleConstants.FontStyle[size],
...(fontBold && { fontWeight: StyleConstants.Font.Weight.Bold })
},
image: {
width: StyleConstants.Font.Size[size],
height: StyleConstants.Font.Size[size]
}
})
return (
<Text style={styles.text}>
{emojis ? (
content
.split(regexEmoji)
.filter(f => f)
.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}>{emojiShortcode}</Text>
) : (
<Text key={i}>
{/* When emoji starts a paragraph, lineHeight will break */}
{i === 0 ? <Text> </Text> : null}
<Image
resizeMode='contain'
source={{ uri: emojis[emojiIndex].url }}
style={[styles.image]}
/>
</Text>
)
} else {
return <Text key={i}>{str}</Text>
}
})
) : (
<Text>{content}</Text>
)}
</Text>
)
}
export default React.memo(ParseEmojis, () => true)

View File

@ -1,14 +1,14 @@
import openLink from '@components/openLink'
import { ParseEmojis } from '@components/Parse'
import { Feather } from '@expo/vector-icons'
import { useNavigation } from '@react-navigation/native'
import { StyleConstants } from '@utils/styles/constants'
import layoutAnimation from '@utils/styles/layoutAnimation'
import { useTheme } from '@utils/styles/ThemeManager'
import { LinearGradient } from 'expo-linear-gradient'
import React, { useCallback, useMemo, useState } from 'react'
import { Pressable, Text, View } from 'react-native'
import HTMLView from 'react-native-htmlview'
import { useNavigation } from '@react-navigation/native'
import Emojis from '@components/Timelines/Timeline/Shared/Emojis'
import { useTheme } from '@utils/styles/ThemeManager'
import { Feather } from '@expo/vector-icons'
import { StyleConstants } from '@utils/styles/constants'
import openLink from '@root/utils/openLink'
import layoutAnimation from '@root/utils/styles/layoutAnimation'
// Prevent going to the same hashtag multiple times
const renderNode = ({
@ -126,7 +126,7 @@ export interface Props {
expandHint?: string
}
const ParseContent: React.FC<Props> = ({
const ParseHTML: React.FC<Props> = ({
content,
size = 'M',
emojis,
@ -155,11 +155,7 @@ const ParseContent: React.FC<Props> = ({
)
const textComponent = useCallback(({ children }) => {
if (children) {
return emojis ? (
<Emojis content={children.toString()} emojis={emojis} size={size} />
) : (
<Text style={{ ...StyleConstants.FontStyle[size] }}>{children}</Text>
)
return <ParseEmojis content={children.toString()} emojis={emojis} />
} else {
return null
}
@ -170,7 +166,9 @@ const ParseContent: React.FC<Props> = ({
const [heightOriginal, setHeightOriginal] = useState<number>()
const [heightTruncated, setHeightTruncated] = useState<number>()
const [allowExpand, setAllowExpand] = useState(false)
const [allowExpand, setAllowExpand] = useState(
numberOfLines === 0 ? true : undefined
)
const [showAllText, setShowAllText] = useState(false)
const calNumberOfLines = useMemo(() => {
@ -189,27 +187,36 @@ const ParseContent: React.FC<Props> = ({
}
}, [heightOriginal, heightTruncated, allowExpand, showAllText])
const onLayout = useCallback(
({ nativeEvent }) => {
if (!heightOriginal) {
setHeightOriginal(nativeEvent.layout.height)
} else {
if (!heightTruncated) {
setHeightTruncated(nativeEvent.layout.height)
} else {
if (heightOriginal > heightTruncated) {
setAllowExpand(true)
}
}
}
},
[heightOriginal, heightTruncated]
)
return (
<View>
<Text
style={{ color: theme.primary, overflow: 'hidden' }}
style={{
...StyleConstants.FontStyle[size],
color: theme.primary,
overflow: 'hidden'
}}
children={children}
numberOfLines={calNumberOfLines}
onLayout={({ nativeEvent }) => {
if (!heightOriginal) {
setHeightOriginal(nativeEvent.layout.height)
} else {
if (!heightTruncated) {
setHeightTruncated(nativeEvent.layout.height)
} else {
if (heightOriginal > heightTruncated) {
setAllowExpand(true)
}
}
}
}}
onLayout={allowExpand === undefined ? onLayout : undefined}
/>
{allowExpand && (
{allowExpand ? (
<Pressable
onPress={() => {
layoutAnimation()
@ -239,7 +246,7 @@ const ParseContent: React.FC<Props> = ({
</Text>
</LinearGradient>
</Pressable>
)}
) : null}
</View>
)
},
@ -256,4 +263,4 @@ const ParseContent: React.FC<Props> = ({
)
}
export default ParseContent
export default ParseHTML

View File

@ -34,19 +34,22 @@ const Timelines: React.FC<Props> = ({ name, content }) => {
.filter(p => (localRegistered ? true : p.page === 'RemotePublic'))
.map(p => ({ key: p.page }))
const renderScene = ({
route
}: {
route: {
key: App.Pages
}
}) => {
return (
(localRegistered || route.key === 'RemotePublic') && (
<Timeline page={route.key} />
const renderScene = useCallback(
({
route
}: {
route: {
key: App.Pages
}
}) => {
return (
(localRegistered || route.key === 'RemotePublic') && (
<Timeline page={route.key} />
)
)
)
}
},
[localRegistered]
)
const screenComponent = useCallback(
() => (

View File

@ -29,6 +29,7 @@ export interface Props {
toot?: Mastodon.Status
account?: string
disableRefresh?: boolean
disableInfinity?: boolean
}
const Timeline: React.FC<Props> = ({
@ -37,7 +38,8 @@ const Timeline: React.FC<Props> = ({
list,
toot,
account,
disableRefresh = false
disableRefresh = false,
disableInfinity = false
}) => {
const queryKey: QueryKey.Timeline = [
page,
@ -152,11 +154,11 @@ const Timeline: React.FC<Props> = ({
[status]
)
const onEndReached = useCallback(
() => !disableRefresh && !isFetchingNextPage && fetchNextPage(),
() => !disableInfinity && !isFetchingNextPage && fetchNextPage(),
[isFetchingNextPage]
)
const ListFooterComponent = useCallback(
() => <TimelineEnd hasNextPage={!disableRefresh ? hasNextPage : false} />,
() => <TimelineEnd hasNextPage={!disableInfinity ? hasNextPage : false} />,
[hasNextPage]
)
const refreshControl = useMemo(
@ -197,6 +199,10 @@ const Timeline: React.FC<Props> = ({
{...(!disableRefresh && { refreshControl })}
ItemSeparatorComponent={ItemSeparatorComponent}
{...(toot && isSuccess && { onScrollToIndexFailed })}
maintainVisibleContentPosition={{
minIndexForVisible: 0,
autoscrollToTopThreshold: 2
}}
/>
)
}
@ -207,4 +213,6 @@ const styles = StyleSheet.create({
}
})
// Timeline.whyDidYouRender = true
export default Timeline

View File

@ -111,7 +111,7 @@ const TimelineDefault: React.FC<Props> = ({
const styles = StyleSheet.create({
statusView: {
padding: StyleConstants.Spacing.Global.PagePadding,
paddingBottom: StyleConstants.Spacing.M
paddingBottom: StyleConstants.Spacing.S
},
header: {
flex: 1,

View File

@ -1,14 +1,14 @@
import React from 'react'
import { StyleSheet, Text, View } from 'react-native'
import { Feather } from '@expo/vector-icons'
import Emojis from '@components/Timelines/Timeline/Shared/Emojis'
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'
export interface Props {
account: Mastodon.Account
action: 'favourite' | 'follow' | 'mention' | 'poll' | 'reblog' | 'pinned'
action: 'favourite' | 'follow' | 'poll' | 'reblog' | 'pinned' | 'mention'
notification?: boolean
}
@ -18,96 +18,106 @@ const TimelineActioned: React.FC<Props> = ({
notification = false
}) => {
const { theme } = useTheme()
const navigation = useNavigation()
const name = account.display_name || account.username
const iconColor = theme.primary
let icon
let content
switch (action) {
case 'pinned':
icon = (
<Feather
name='anchor'
size={StyleConstants.Font.Size.S}
color={iconColor}
style={styles.icon}
/>
)
content = `置顶`
break
case 'favourite':
icon = (
<Feather
name='heart'
size={StyleConstants.Font.Size.S}
color={iconColor}
style={styles.icon}
/>
)
content = `${name} 喜欢了你的嘟嘟`
break
case 'follow':
icon = (
<Feather
name='user-plus'
size={StyleConstants.Font.Size.S}
color={iconColor}
style={styles.icon}
/>
)
content = `${name} 开始关注你`
break
case 'poll':
icon = (
<Feather
name='bar-chart-2'
size={StyleConstants.Font.Size.S}
color={iconColor}
style={styles.icon}
/>
)
content = `你参与的投票已结束`
break
case 'reblog':
icon = (
<Feather
name='repeat'
size={StyleConstants.Font.Size.S}
color={iconColor}
style={styles.icon}
/>
)
content = `${name} 转嘟了${notification ? '你的嘟嘟' : ''}`
break
}
return (
<View style={styles.actioned}>
{icon}
{content && (
<View style={styles.content}>
{account.emojis ? (
<Emojis content={content} emojis={account.emojis} size='S' />
) : (
<Text>{content}</Text>
)}
</View>
)}
</View>
const content = (content: string) => (
<ParseEmojis content={content} emojis={account.emojis} size='S' />
)
const onPress = useCallback(() => {
navigation.push('Screen-Shared-Account', { account })
}, [])
const children = useMemo(() => {
switch (action) {
case 'pinned':
return (
<>
<Feather
name='anchor'
size={StyleConstants.Font.Size.S}
color={iconColor}
style={styles.icon}
/>
{content('置顶')}
</>
)
break
case 'favourite':
return (
<>
<Feather
name='heart'
size={StyleConstants.Font.Size.S}
color={iconColor}
style={styles.icon}
/>
<Pressable onPress={onPress}>
{content(`${name} 喜欢了你的嘟嘟`)}
</Pressable>
</>
)
break
case 'follow':
return (
<>
<Feather
name='user-plus'
size={StyleConstants.Font.Size.S}
color={iconColor}
style={styles.icon}
/>
<Pressable onPress={onPress}>
{content(`${name} 开始关注你`)}
</Pressable>
</>
)
break
case 'poll':
return (
<>
<Feather
name='bar-chart-2'
size={StyleConstants.Font.Size.S}
color={iconColor}
style={styles.icon}
/>
{content('你参与的投票已结束')}
</>
)
break
case 'reblog':
return (
<>
<Feather
name='repeat'
size={StyleConstants.Font.Size.S}
color={iconColor}
style={styles.icon}
/>
<Pressable onPress={onPress}>
{content(`${name} 转嘟了${notification ? '你的嘟嘟' : ''}`)}
</Pressable>
</>
)
break
}
}, [])
return <View style={styles.actioned} children={children} />
}
const styles = StyleSheet.create({
actioned: {
flexDirection: 'row',
marginBottom: StyleConstants.Spacing.S
marginBottom: StyleConstants.Spacing.S,
paddingLeft: StyleConstants.Avatar.M - StyleConstants.Font.Size.S,
paddingRight: StyleConstants.Spacing.Global.PagePadding
},
icon: {
marginLeft: StyleConstants.Avatar.M - StyleConstants.Font.Size.S,
marginRight: StyleConstants.Spacing.S
},
content: {
flexDirection: 'row'
paddingRight: StyleConstants.Spacing.S
}
})

View File

@ -1,16 +1,15 @@
import client from '@api/client'
import haptics from '@components/haptics'
import { TimelineData } from '@components/Timelines/Timeline'
import { toast } from '@components/toast'
import { Feather } from '@expo/vector-icons'
import { useNavigation } from '@react-navigation/native'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import { findIndex } from 'lodash'
import React, { useCallback, useMemo } from 'react'
import { ActionSheetIOS, Pressable, StyleSheet, Text, View } from 'react-native'
import { useMutation, useQueryClient } from 'react-query'
import { Feather } from '@expo/vector-icons'
import client from '@api/client'
import { useTheme } from '@utils/styles/ThemeManager'
import { toast } from '@components/toast'
import { StyleConstants } from '@utils/styles/constants'
import { useNavigation } from '@react-navigation/native'
import { findIndex } from 'lodash'
import { TimelineData } from '../../Timeline'
import haptics from '@root/components/haptics'
const fireMutation = async ({
id,
@ -35,10 +34,8 @@ const fireMutation = async ({
}) // 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
@ -64,6 +61,7 @@ const TimelineActions: React.FC<Props> = ({ queryKey, status, reblog }) => {
queryClient.cancelQueries(queryKey)
const oldData = queryClient.getQueryData(queryKey)
haptics('Success')
switch (type) {
case 'favourite':
case 'reblog':
@ -111,7 +109,6 @@ const TimelineActions: React.FC<Props> = ({ queryKey, status, reblog }) => {
return old
})
haptics('Success')
break
}
@ -175,8 +172,8 @@ const TimelineActions: React.FC<Props> = ({ queryKey, status, reblog }) => {
'com.apple.UIKit.activity.OpenInIBooks'
]
},
() => haptics('Success'),
() => haptics('Error')
() => haptics('Error'),
() => haptics('Success')
),
[]
)
@ -294,12 +291,13 @@ const styles = StyleSheet.create({
width: '100%',
flex: 1,
flexDirection: 'row',
marginTop: StyleConstants.Spacing.M
marginTop: StyleConstants.Spacing.S
},
action: {
width: '20%',
flexDirection: 'row',
justifyContent: 'center'
justifyContent: 'center',
paddingVertical: StyleConstants.Spacing.S
}
})

View File

@ -1,7 +1,7 @@
import Button from '@components/Button'
import openLink from '@root/utils/openLink'
import { StyleConstants } from '@root/utils/styles/constants'
import { useTheme } from '@root/utils/styles/ThemeManager'
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 React from 'react'
@ -38,7 +38,7 @@ const AttachmentUnsupported: React.FC<Props> = ({
{ color: attachment.blurhash ? theme.background : theme.primary }
]}
>
</Text>
{attachment.remote_url ? (
<Button

View File

@ -1,8 +1,8 @@
import React, { useEffect, useMemo, useState } from 'react'
import { Image, Pressable, StyleSheet, Text, View } from 'react-native'
import openLink from '@components/openLink'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import openLink from '@root/utils/openLink'
import { Surface } from 'gl-react-expo'
import { Blurhash } from 'gl-react-blurhash'

View File

@ -1,7 +1,7 @@
import { ParseHTML } from '@components/Parse'
import { StyleConstants } from '@utils/styles/constants'
import React from 'react'
import { View } from 'react-native'
import ParseContent from '@components/ParseContent'
import { StyleConstants } from '@utils/styles/constants'
export interface Props {
status: Mastodon.Status
@ -18,28 +18,28 @@ const TimelineContent: React.FC<Props> = ({
<>
{status.spoiler_text ? (
<>
<ParseContent
content={status.spoiler_text}
size={highlighted ? 'L' : 'M'}
emojis={status.emojis}
mentions={status.mentions}
tags={status.tags}
numberOfLines={999}
/>
<View style={{ marginTop: StyleConstants.Font.Size.M }}>
<ParseContent
content={status.content}
<View style={{ marginBottom: StyleConstants.Font.Size.M }}>
<ParseHTML
content={status.spoiler_text}
size={highlighted ? 'L' : 'M'}
emojis={status.emojis}
mentions={status.mentions}
tags={status.tags}
numberOfLines={1}
expandHint='隐藏内容'
numberOfLines={999}
/>
</View>
<ParseHTML
content={status.content}
size={highlighted ? 'L' : 'M'}
emojis={status.emojis}
mentions={status.mentions}
tags={status.tags}
numberOfLines={0}
expandHint='隐藏内容'
/>
</>
) : (
<ParseContent
<ParseHTML
content={status.content}
size={highlighted ? 'L' : 'M'}
emojis={status.emojis}

View File

@ -1,72 +0,0 @@
import React from 'react'
import { Image, StyleSheet, Text } from 'react-native'
import { useTheme } from '@utils/styles/ThemeManager'
import { StyleConstants } from '@utils/styles/constants'
const regexEmoji = new RegExp(/(:[A-Za-z0-9_]+:)/)
export interface Props {
content: string
emojis: Mastodon.Emoji[]
size?: 'S' | 'M' | 'L'
fontBold?: boolean
numberOfLines?: number
}
const Emojis: React.FC<Props> = ({
content,
emojis,
size = 'M',
fontBold = false,
numberOfLines
}) => {
const { theme } = useTheme()
const styles = StyleSheet.create({
text: {
color: theme.primary,
...StyleConstants.FontStyle[size],
...(fontBold && { fontWeight: StyleConstants.Font.Weight.Bold })
},
image: {
width: StyleConstants.Font.Size[size],
height: StyleConstants.Font.Size[size],
marginBottom: -2 // hacking
}
})
return (
<Text numberOfLines={numberOfLines || undefined}>
{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}
resizeMode='contain'
source={{ uri: emojis[emojiIndex].url }}
style={[styles.image]}
/>
)
} else {
return str ? (
<Text key={i} style={styles.text}>
{str}
</Text>
) : (
undefined
)
}
})}
</Text>
)
}
// export default React.memo(Emojis, () => true)
export default Emojis

View File

@ -1,15 +1,14 @@
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 { useMutation, useQueryClient } from 'react-query'
import client from '@api/client'
import { toast } from '@components/toast'
import relativeTime from '@utils/relativeTime'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import Emojis from '@components/Timelines/Timeline/Shared/Emojis'
import haptics from '@root/components/haptics'
export interface Props {
queryKey: QueryKey.Timeline
@ -43,6 +42,7 @@ const HeaderConversation: React.FC<Props> = ({ queryKey, conversation }) => {
queryClient.cancelQueries(queryKey)
const oldData = queryClient.getQueryData(queryKey)
haptics('Success')
queryClient.setQueryData(queryKey, (old: any) =>
old.pages.map((paging: any) => ({
toots: paging.toots.filter(
@ -51,7 +51,6 @@ const HeaderConversation: React.FC<Props> = ({ queryKey, conversation }) => {
pointer: paging.pointer
}))
)
haptics('Success')
return oldData
},
@ -80,26 +79,17 @@ const HeaderConversation: React.FC<Props> = ({ queryKey, conversation }) => {
return (
<View style={styles.base}>
<View style={styles.nameAndDate}>
<View style={styles.name}>
{conversation.accounts[0].emojis ? (
<Emojis
<View style={styles.namdAndAccount}>
<Text numberOfLines={1}>
<ParseEmojis
content={
conversation.accounts[0].display_name ||
conversation.accounts[0].username
}
emojis={conversation.accounts[0].emojis}
size='M'
fontBold={true}
fontBold
/>
) : (
<Text
numberOfLines={1}
style={[styles.nameWithoutEmoji, { color: theme.primary }]}
>
{conversation.accounts[0].display_name ||
conversation.accounts[0].username}
</Text>
)}
</Text>
<Text
style={[styles.account, { color: theme.secondary }]}
numberOfLines={1}
@ -136,7 +126,7 @@ const styles = StyleSheet.create({
nameAndDate: {
width: '80%'
},
name: {
namdAndAccount: {
flexDirection: 'row',
alignItems: 'center'
},
@ -144,10 +134,6 @@ const styles = StyleSheet.create({
flexShrink: 1,
marginLeft: StyleConstants.Spacing.XS
},
nameWithoutEmoji: {
...StyleConstants.FontStyle.M,
fontWeight: StyleConstants.Font.Weight.Bold
},
meta: {
flexDirection: 'row',
alignItems: 'center',

View File

@ -1,17 +1,17 @@
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'
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 { Feather } from '@expo/vector-icons'
import Emojis from '@components/Timelines/Timeline/Shared/Emojis'
import relativeTime from '@utils/relativeTime'
import { getLocalUrl } from '@utils/slices/instancesSlice'
import { useTheme } from '@utils/styles/ThemeManager'
import BottomSheet from '@components/BottomSheet'
import { useSelector } from 'react-redux'
import { StyleConstants } from '@utils/styles/constants'
import HeaderDefaultActionsAccount from '@components/Timelines/Timeline/Shared/HeaderDefault/ActionsAccount'
import HeaderDefaultActionsStatus from '@components/Timelines/Timeline/Shared/HeaderDefault/ActionsStatus'
import HeaderDefaultActionsDomain from '@components/Timelines/Timeline/Shared/HeaderDefault/ActionsDomain'
import openLink from '@root/utils/openLink'
export interface Props {
queryKey?: QueryKey.Timeline
@ -69,21 +69,9 @@ const TimelineHeaderDefault: React.FC<Props> = ({
<View style={styles.base}>
<View style={queryKey ? { flexBasis: '80%' } : { flexBasis: '100%' }}>
<View style={styles.nameAndAccount}>
{emojis?.length ? (
<Emojis
content={name}
emojis={emojis}
size='M'
fontBold={true}
/>
) : (
<Text
numberOfLines={1}
style={[styles.nameWithoutEmoji, { color: theme.primary }]}
>
{name}
</Text>
)}
<Text numberOfLines={1}>
<ParseEmojis content={name} emojis={emojis} fontBold />
</Text>
<Text
style={[styles.account, { color: theme.secondary }]}
numberOfLines={1}
@ -163,7 +151,8 @@ const TimelineHeaderDefault: React.FC<Props> = ({
const styles = StyleSheet.create({
base: {
flex: 1,
flexDirection: 'row'
flexDirection: 'row',
alignItems: 'baseline'
},
nameAndMeta: {
flexBasis: '80%'
@ -172,10 +161,6 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center'
},
nameWithoutEmoji: {
...StyleConstants.FontStyle.M,
fontWeight: StyleConstants.Font.Weight.Bold
},
account: {
flex: 1,
marginLeft: StyleConstants.Spacing.XS
@ -199,7 +184,8 @@ const styles = StyleSheet.create({
action: {
flexBasis: '20%',
flexDirection: 'row',
justifyContent: 'center'
justifyContent: 'center',
paddingBottom: StyleConstants.Spacing.S
}
})

View File

@ -1,17 +1,17 @@
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 { Chase } from 'react-native-animated-spinkit'
import { useQuery } from 'react-query'
import client from '@api/client'
import { Feather } from '@expo/vector-icons'
import Emojis from '@components/Timelines/Timeline/Shared/Emojis'
import { toast } from '@components/toast'
import openLink from '@root/utils/openLink'
import relativeTime from '@utils/relativeTime'
import { StyleConstants } from '@utils/styles/constants'
import { relationshipFetch } from '@utils/fetches/relationshipFetch'
import { useTheme } from '@utils/styles/ThemeManager'
import haptics from '@root/components/haptics'
export interface Props {
notification: Mastodon.Notification
@ -129,16 +129,9 @@ const TimelineHeaderNotification: React.FC<Props> = ({ notification }) => {
<View style={styles.base}>
<View style={styles.nameAndMeta}>
<View style={styles.nameAndAccount}>
{emojis?.length ? (
<Emojis content={name} emojis={emojis} size='M' fontBold={true} />
) : (
<Text
numberOfLines={1}
style={[styles.nameWithoutEmoji, { color: theme.primary }]}
>
{name}
</Text>
)}
<Text numberOfLines={1}>
<ParseEmojis content={name} emojis={emojis} fontBold />
</Text>
<Text
style={[styles.account, { color: theme.secondary }]}
numberOfLines={1}
@ -194,10 +187,6 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center'
},
nameWithoutEmoji: {
...StyleConstants.FontStyle.M,
fontWeight: StyleConstants.Font.Weight.Bold
},
account: {
flexShrink: 1,
marginLeft: StyleConstants.Spacing.XS

View File

@ -1,18 +1,16 @@
import client from '@api/client'
import Button from '@components/Button'
import haptics from '@components/haptics'
import relativeTime from '@components/relativeTime'
import { TimelineData } from '@components/Timelines/Timeline'
import { Feather } from '@expo/vector-icons'
import { ParseEmojis } from '@root/components/Parse'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import { findIndex } from 'lodash'
import React, { useCallback, useMemo, useState } from 'react'
import { Pressable, StyleSheet, Text, View } from 'react-native'
import { useMutation, useQueryClient } from 'react-query'
import client from '@api/client'
import Button from '@components/Button'
import { toast } from '@components/toast'
import relativeTime from '@utils/relativeTime'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import Emojis from './Emojis'
import { TimelineData } from '../../Timeline'
import { findIndex } from 'lodash'
import haptics from '@root/components/haptics'
const fireMutation = async ({
id,
@ -39,11 +37,6 @@ const fireMutation = async ({
if (res.body.id === id) {
return Promise.resolve(res.body as Mastodon.Poll)
} else {
toast({
type: 'error',
content: '投票失败,请重试',
autoHide: false
})
return Promise.reject()
}
}
@ -61,7 +54,7 @@ const TimelinePoll: React.FC<Props> = ({
reblog,
sameAccount
}) => {
const { theme } = useTheme()
const { mode, theme } = useTheme()
const queryClient = useQueryClient()
const [allOptions, setAllOptions] = useState(
@ -98,10 +91,13 @@ const TimelinePoll: React.FC<Props> = ({
})
haptics('Success')
},
onError: () => {
haptics('Error')
}
})
const pollButton = () => {
const pollButton = useMemo(() => {
if (!poll.expired) {
if (!sameAccount && !poll.voted) {
return (
@ -131,7 +127,7 @@ const TimelinePoll: React.FC<Props> = ({
)
}
}
}
}, [poll.expired, poll.voted, allOptions, mutation.isLoading])
const pollExpiration = useMemo(() => {
if (poll.expired) {
@ -147,7 +143,7 @@ const TimelinePoll: React.FC<Props> = ({
</Text>
)
}
}, [])
}, [mode])
const isSelected = useCallback(
(index: number): any =>
@ -157,101 +153,93 @@ const TimelinePoll: React.FC<Props> = ({
[allOptions]
)
const pollBodyDisallow = useMemo(() => {
return poll.options.map((option, index) => (
<View key={index} style={styles.optionContainer}>
<View style={styles.optionContent}>
<Feather
style={styles.optionSelection}
name={
`${poll.own_votes?.includes(index) ? 'check-' : ''}${
poll.multiple ? 'square' : 'circle'
}` as any
}
size={StyleConstants.Font.Size.M}
color={
poll.own_votes?.includes(index) ? theme.primary : theme.disabled
}
/>
<Text style={styles.optionText}>
<ParseEmojis content={option.title} emojis={poll.emojis} />
</Text>
<Text style={[styles.optionPercentage, { color: theme.primary }]}>
{poll.votes_count
? Math.round((option.votes_count / poll.voters_count) * 100)
: 0}
%
</Text>
</View>
<View
style={[
styles.background,
{
width: `${Math.round(
(option.votes_count / poll.voters_count) * 100
)}%`,
backgroundColor: theme.disabled
}
]}
/>
</View>
))
}, [mode, poll.options])
const pollBodyAllow = useMemo(() => {
return poll.options.map((option, index) => (
<Pressable
key={index}
style={styles.optionContainer}
onPress={() => {
haptics('Light')
if (poll.multiple) {
setAllOptions(allOptions.map((o, i) => (i === index ? !o : o)))
} else {
{
const otherOptions =
allOptions[index] === false ? false : undefined
setAllOptions(
allOptions.map((o, i) =>
i === index
? !o
: otherOptions !== undefined
? otherOptions
: o
)
)
}
}
}}
>
<View style={[styles.optionContent]}>
<Feather
style={styles.optionSelection}
name={isSelected(index)}
size={StyleConstants.Font.Size.L}
color={theme.primary}
/>
<Text style={styles.optionText}>
<ParseEmojis content={option.title} emojis={poll.emojis} />
</Text>
</View>
</Pressable>
))
}, [mode, allOptions])
return (
<View style={styles.base}>
{poll.options.map((option, index) =>
poll.voted ? (
<View key={index} style={styles.poll}>
<View style={styles.optionSelected}>
<Feather
style={styles.voted}
name={
`${poll.own_votes!.includes(index) ? 'check-' : ''}${
poll.multiple ? 'square' : 'circle'
}` as any
}
size={StyleConstants.Font.Size.M}
color={
poll.own_votes!.includes(index)
? theme.primary
: theme.disabled
}
/>
<View style={styles.contentSelected}>
<Emojis
content={option.title}
emojis={poll.emojis}
size='M'
numberOfLines={2}
/>
</View>
<Text style={[styles.percentage, { color: theme.primary }]}>
{poll.votes_count
? Math.round((option.votes_count / poll.voters_count) * 100)
: 0}
%
</Text>
</View>
<View
style={[
styles.background,
{
width: `${Math.round(
(option.votes_count / poll.voters_count) * 100
)}%`,
backgroundColor: theme.disabled
}
]}
/>
</View>
) : (
<View key={index} style={styles.poll}>
<Pressable
style={[styles.optionUnselected]}
onPress={() => {
haptics('Light')
if (poll.multiple) {
setAllOptions(
allOptions.map((o, i) => (i === index ? !o : o))
)
} else {
{
const otherOptions =
allOptions[index] === false ? false : undefined
setAllOptions(
allOptions.map((o, i) =>
i === index
? !o
: otherOptions !== undefined
? otherOptions
: o
)
)
}
}
}}
>
<Feather
style={styles.votedNot}
name={isSelected(index)}
size={StyleConstants.Font.Size.L}
color={theme.primary}
/>
<View style={styles.contentUnselected}>
<Emojis
content={option.title}
emojis={poll.emojis}
size='M'
numberOfLines={2}
/>
</View>
</Pressable>
</View>
)
)}
{poll.expired || poll.voted ? pollBodyDisallow : pollBodyAllow}
<View style={styles.meta}>
{pollButton()}
{pollButton}
<Text style={[styles.votes, { color: theme.secondary }]}>
{poll.voters_count || 0}{' • '}
</Text>
@ -265,39 +253,24 @@ const styles = StyleSheet.create({
base: {
marginTop: StyleConstants.Spacing.M
},
poll: {
optionContainer: {
flex: 1,
minHeight: StyleConstants.Font.LineHeight.M * 2,
paddingVertical: StyleConstants.Spacing.XS
paddingVertical: StyleConstants.Spacing.S
},
optionSelected: {
flex: 1,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingRight: StyleConstants.Spacing.M
},
optionUnselected: {
optionContent: {
flex: 1,
flexDirection: 'row'
},
contentSelected: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
paddingRight: StyleConstants.Spacing.S
optionText: {
flex: 1
},
contentUnselected: {
flexShrink: 1
},
voted: {
optionSelection: {
marginRight: StyleConstants.Spacing.S
},
votedNot: {
paddingRight: StyleConstants.Spacing.S
},
percentage: {
...StyleConstants.FontStyle.M
optionPercentage: {
...StyleConstants.FontStyle.M,
alignSelf: 'center',
marginLeft: StyleConstants.Spacing.S
},
background: {
height: StyleConstants.Spacing.XS,
@ -314,7 +287,7 @@ const styles = StyleSheet.create({
marginTop: StyleConstants.Spacing.XS
},
button: {
marginRight: StyleConstants.Spacing.M
marginRight: StyleConstants.Spacing.S
},
votes: {
...StyleConstants.FontStyle.S

View File

@ -2,8 +2,9 @@ import * as Analytics from 'expo-firebase-analytics'
import * as Sentry from 'sentry-expo'
const analytics = (event: string, params?: { [key: string]: string }) => {
Analytics.logEvent(event, params).catch(error =>
Sentry.Native.captureException(error)
Analytics.logEvent(event, params).catch(
error => {}
// Sentry.Native.captureException(error)
)
}

View File

@ -0,0 +1,17 @@
import { store } from '@root/store'
import { getSettingsBrowser } from '@utils/slices/settingsSlice'
import * as Linking from 'expo-linking'
import * as WebBrowser from 'expo-web-browser'
const openLink = async (url: string) => {
switch (getSettingsBrowser(store.getState())) {
case 'internal':
await WebBrowser.openBrowserAsync(url)
break
case 'external':
await Linking.openURL(url)
break
}
}
export default openLink

View File

@ -0,0 +1,29 @@
import { store } from '@root/store'
const relativeTime = (date: string) => {
const units = {
year: 24 * 60 * 60 * 1000 * 365,
month: (24 * 60 * 60 * 1000 * 365) / 12,
day: 24 * 60 * 60 * 1000,
hour: 60 * 60 * 1000,
minute: 60 * 1000,
second: 1000
}
const rtf = new Intl.RelativeTimeFormat(store.getState().settings.language, {
numeric: 'auto'
})
const elapsed = +new Date(date) - +new Date()
// "Math.abs" accounts for both "past" & "future" scenarios
for (const u in units) {
// @ts-ignore
if (Math.abs(elapsed) > units[u] || u == 'second') {
// @ts-ignore
return rtf.format(Math.round(elapsed / units[u]), u)
}
}
}
export default relativeTime