This commit is contained in:
Zhiyuan Zheng 2021-01-01 16:48:16 +01:00
parent 5473fcb770
commit 6aed76ac65
No known key found for this signature in database
GPG Key ID: 078A93AB607D85E0
38 changed files with 632 additions and 637 deletions

View File

@ -1,5 +1,5 @@
import { Feather } from '@expo/vector-icons' import { Feather } from '@expo/vector-icons'
import React, { useLayoutEffect, useMemo } from 'react' import React, { useEffect, useMemo } from 'react'
import { import {
Pressable, Pressable,
StyleProp, StyleProp,
@ -46,7 +46,7 @@ const Button: React.FC<Props> = ({
}) => { }) => {
const { theme } = useTheme() const { theme } = useTheme()
useLayoutEffect(() => layoutAnimation(), [content, loading, disabled]) useEffect(() => layoutAnimation(), [content, loading, disabled])
const loadingSpinkit = useMemo( 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 { LinearGradient } from 'expo-linear-gradient'
import React, { useCallback, useMemo, useState } from 'react' import React, { useCallback, useMemo, useState } from 'react'
import { Pressable, Text, View } from 'react-native' import { Pressable, Text, View } from 'react-native'
import HTMLView from 'react-native-htmlview' 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 // Prevent going to the same hashtag multiple times
const renderNode = ({ const renderNode = ({
@ -126,7 +126,7 @@ export interface Props {
expandHint?: string expandHint?: string
} }
const ParseContent: React.FC<Props> = ({ const ParseHTML: React.FC<Props> = ({
content, content,
size = 'M', size = 'M',
emojis, emojis,
@ -155,11 +155,7 @@ const ParseContent: React.FC<Props> = ({
) )
const textComponent = useCallback(({ children }) => { const textComponent = useCallback(({ children }) => {
if (children) { if (children) {
return emojis ? ( return <ParseEmojis content={children.toString()} emojis={emojis} />
<Emojis content={children.toString()} emojis={emojis} size={size} />
) : (
<Text style={{ ...StyleConstants.FontStyle[size] }}>{children}</Text>
)
} else { } else {
return null return null
} }
@ -170,7 +166,9 @@ const ParseContent: React.FC<Props> = ({
const [heightOriginal, setHeightOriginal] = useState<number>() const [heightOriginal, setHeightOriginal] = useState<number>()
const [heightTruncated, setHeightTruncated] = 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 [showAllText, setShowAllText] = useState(false)
const calNumberOfLines = useMemo(() => { const calNumberOfLines = useMemo(() => {
@ -189,13 +187,8 @@ const ParseContent: React.FC<Props> = ({
} }
}, [heightOriginal, heightTruncated, allowExpand, showAllText]) }, [heightOriginal, heightTruncated, allowExpand, showAllText])
return ( const onLayout = useCallback(
<View> ({ nativeEvent }) => {
<Text
style={{ color: theme.primary, overflow: 'hidden' }}
children={children}
numberOfLines={calNumberOfLines}
onLayout={({ nativeEvent }) => {
if (!heightOriginal) { if (!heightOriginal) {
setHeightOriginal(nativeEvent.layout.height) setHeightOriginal(nativeEvent.layout.height)
} else { } else {
@ -207,9 +200,23 @@ const ParseContent: React.FC<Props> = ({
} }
} }
} }
},
[heightOriginal, heightTruncated]
)
return (
<View>
<Text
style={{
...StyleConstants.FontStyle[size],
color: theme.primary,
overflow: 'hidden'
}} }}
children={children}
numberOfLines={calNumberOfLines}
onLayout={allowExpand === undefined ? onLayout : undefined}
/> />
{allowExpand && ( {allowExpand ? (
<Pressable <Pressable
onPress={() => { onPress={() => {
layoutAnimation() layoutAnimation()
@ -239,7 +246,7 @@ const ParseContent: React.FC<Props> = ({
</Text> </Text>
</LinearGradient> </LinearGradient>
</Pressable> </Pressable>
)} ) : null}
</View> </View>
) )
}, },
@ -256,4 +263,4 @@ const ParseContent: React.FC<Props> = ({
) )
} }
export default ParseContent export default ParseHTML

View File

@ -34,7 +34,8 @@ const Timelines: React.FC<Props> = ({ name, content }) => {
.filter(p => (localRegistered ? true : p.page === 'RemotePublic')) .filter(p => (localRegistered ? true : p.page === 'RemotePublic'))
.map(p => ({ key: p.page })) .map(p => ({ key: p.page }))
const renderScene = ({ const renderScene = useCallback(
({
route route
}: { }: {
route: { route: {
@ -46,7 +47,9 @@ const Timelines: React.FC<Props> = ({ name, content }) => {
<Timeline page={route.key} /> <Timeline page={route.key} />
) )
) )
} },
[localRegistered]
)
const screenComponent = useCallback( const screenComponent = useCallback(
() => ( () => (

View File

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

View File

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

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 React, { useCallback, useMemo } from 'react'
import { ActionSheetIOS, Pressable, StyleSheet, Text, View } from 'react-native' import { ActionSheetIOS, Pressable, StyleSheet, Text, View } from 'react-native'
import { useMutation, useQueryClient } from 'react-query' 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 ({ const fireMutation = async ({
id, id,
@ -35,10 +34,8 @@ const fireMutation = async ({
}) // bug in response from Mastodon }) // bug in response from Mastodon
if (!res.body[stateKey] === prevState) { if (!res.body[stateKey] === prevState) {
toast({ type: 'success', content: '功能成功' })
return Promise.resolve(res.body) return Promise.resolve(res.body)
} else { } else {
toast({ type: 'error', content: '功能错误' })
return Promise.reject() return Promise.reject()
} }
break break
@ -64,6 +61,7 @@ const TimelineActions: React.FC<Props> = ({ queryKey, status, reblog }) => {
queryClient.cancelQueries(queryKey) queryClient.cancelQueries(queryKey)
const oldData = queryClient.getQueryData(queryKey) const oldData = queryClient.getQueryData(queryKey)
haptics('Success')
switch (type) { switch (type) {
case 'favourite': case 'favourite':
case 'reblog': case 'reblog':
@ -111,7 +109,6 @@ const TimelineActions: React.FC<Props> = ({ queryKey, status, reblog }) => {
return old return old
}) })
haptics('Success')
break break
} }
@ -175,8 +172,8 @@ const TimelineActions: React.FC<Props> = ({ queryKey, status, reblog }) => {
'com.apple.UIKit.activity.OpenInIBooks' 'com.apple.UIKit.activity.OpenInIBooks'
] ]
}, },
() => haptics('Success'), () => haptics('Error'),
() => haptics('Error') () => haptics('Success')
), ),
[] []
) )
@ -294,12 +291,13 @@ const styles = StyleSheet.create({
width: '100%', width: '100%',
flex: 1, flex: 1,
flexDirection: 'row', flexDirection: 'row',
marginTop: StyleConstants.Spacing.M marginTop: StyleConstants.Spacing.S
}, },
action: { action: {
width: '20%', width: '20%',
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'center' justifyContent: 'center',
paddingVertical: StyleConstants.Spacing.S
} }
}) })

View File

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

View File

@ -1,8 +1,8 @@
import React, { useEffect, useMemo, useState } from 'react' import React, { useEffect, useMemo, useState } from 'react'
import { Image, Pressable, StyleSheet, Text, View } from 'react-native' import { Image, Pressable, StyleSheet, Text, View } from 'react-native'
import openLink from '@components/openLink'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import openLink from '@root/utils/openLink'
import { Surface } from 'gl-react-expo' import { Surface } from 'gl-react-expo'
import { Blurhash } from 'gl-react-blurhash' 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 React from 'react'
import { View } from 'react-native' import { View } from 'react-native'
import ParseContent from '@components/ParseContent'
import { StyleConstants } from '@utils/styles/constants'
export interface Props { export interface Props {
status: Mastodon.Status status: Mastodon.Status
@ -18,7 +18,8 @@ const TimelineContent: React.FC<Props> = ({
<> <>
{status.spoiler_text ? ( {status.spoiler_text ? (
<> <>
<ParseContent <View style={{ marginBottom: StyleConstants.Font.Size.M }}>
<ParseHTML
content={status.spoiler_text} content={status.spoiler_text}
size={highlighted ? 'L' : 'M'} size={highlighted ? 'L' : 'M'}
emojis={status.emojis} emojis={status.emojis}
@ -26,20 +27,19 @@ const TimelineContent: React.FC<Props> = ({
tags={status.tags} tags={status.tags}
numberOfLines={999} numberOfLines={999}
/> />
<View style={{ marginTop: StyleConstants.Font.Size.M }}> </View>
<ParseContent <ParseHTML
content={status.content} content={status.content}
size={highlighted ? 'L' : 'M'} size={highlighted ? 'L' : 'M'}
emojis={status.emojis} emojis={status.emojis}
mentions={status.mentions} mentions={status.mentions}
tags={status.tags} tags={status.tags}
numberOfLines={1} numberOfLines={0}
expandHint='隐藏内容' expandHint='隐藏内容'
/> />
</View>
</> </>
) : ( ) : (
<ParseContent <ParseHTML
content={status.content} content={status.content}
size={highlighted ? 'L' : 'M'} size={highlighted ? 'L' : 'M'}
emojis={status.emojis} 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 { Feather } from '@expo/vector-icons'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useCallback, useMemo } from 'react' import React, { useCallback, useMemo } from 'react'
import { Pressable, StyleSheet, Text, View } from 'react-native' import { Pressable, StyleSheet, Text, View } from 'react-native'
import { useMutation, useQueryClient } from 'react-query' 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 { export interface Props {
queryKey: QueryKey.Timeline queryKey: QueryKey.Timeline
@ -43,6 +42,7 @@ const HeaderConversation: React.FC<Props> = ({ queryKey, conversation }) => {
queryClient.cancelQueries(queryKey) queryClient.cancelQueries(queryKey)
const oldData = queryClient.getQueryData(queryKey) const oldData = queryClient.getQueryData(queryKey)
haptics('Success')
queryClient.setQueryData(queryKey, (old: any) => queryClient.setQueryData(queryKey, (old: any) =>
old.pages.map((paging: any) => ({ old.pages.map((paging: any) => ({
toots: paging.toots.filter( toots: paging.toots.filter(
@ -51,7 +51,6 @@ const HeaderConversation: React.FC<Props> = ({ queryKey, conversation }) => {
pointer: paging.pointer pointer: paging.pointer
})) }))
) )
haptics('Success')
return oldData return oldData
}, },
@ -80,26 +79,17 @@ const HeaderConversation: React.FC<Props> = ({ queryKey, conversation }) => {
return ( return (
<View style={styles.base}> <View style={styles.base}>
<View style={styles.nameAndDate}> <View style={styles.nameAndDate}>
<View style={styles.name}> <View style={styles.namdAndAccount}>
{conversation.accounts[0].emojis ? ( <Text numberOfLines={1}>
<Emojis <ParseEmojis
content={ content={
conversation.accounts[0].display_name || conversation.accounts[0].display_name ||
conversation.accounts[0].username conversation.accounts[0].username
} }
emojis={conversation.accounts[0].emojis} emojis={conversation.accounts[0].emojis}
size='M' fontBold
fontBold={true}
/> />
) : (
<Text
numberOfLines={1}
style={[styles.nameWithoutEmoji, { color: theme.primary }]}
>
{conversation.accounts[0].display_name ||
conversation.accounts[0].username}
</Text> </Text>
)}
<Text <Text
style={[styles.account, { color: theme.secondary }]} style={[styles.account, { color: theme.secondary }]}
numberOfLines={1} numberOfLines={1}
@ -136,7 +126,7 @@ const styles = StyleSheet.create({
nameAndDate: { nameAndDate: {
width: '80%' width: '80%'
}, },
name: { namdAndAccount: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center' alignItems: 'center'
}, },
@ -144,10 +134,6 @@ const styles = StyleSheet.create({
flexShrink: 1, flexShrink: 1,
marginLeft: StyleConstants.Spacing.XS marginLeft: StyleConstants.Spacing.XS
}, },
nameWithoutEmoji: {
...StyleConstants.FontStyle.M,
fontWeight: StyleConstants.Font.Weight.Bold
},
meta: { meta: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', 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 React, { useCallback, useEffect, useMemo, useState } from 'react'
import { Pressable, StyleSheet, Text, View } from 'react-native' 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 { 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 { export interface Props {
queryKey?: QueryKey.Timeline queryKey?: QueryKey.Timeline
@ -69,21 +69,9 @@ const TimelineHeaderDefault: React.FC<Props> = ({
<View style={styles.base}> <View style={styles.base}>
<View style={queryKey ? { flexBasis: '80%' } : { flexBasis: '100%' }}> <View style={queryKey ? { flexBasis: '80%' } : { flexBasis: '100%' }}>
<View style={styles.nameAndAccount}> <View style={styles.nameAndAccount}>
{emojis?.length ? ( <Text numberOfLines={1}>
<Emojis <ParseEmojis content={name} emojis={emojis} fontBold />
content={name}
emojis={emojis}
size='M'
fontBold={true}
/>
) : (
<Text
numberOfLines={1}
style={[styles.nameWithoutEmoji, { color: theme.primary }]}
>
{name}
</Text> </Text>
)}
<Text <Text
style={[styles.account, { color: theme.secondary }]} style={[styles.account, { color: theme.secondary }]}
numberOfLines={1} numberOfLines={1}
@ -163,7 +151,8 @@ const TimelineHeaderDefault: React.FC<Props> = ({
const styles = StyleSheet.create({ const styles = StyleSheet.create({
base: { base: {
flex: 1, flex: 1,
flexDirection: 'row' flexDirection: 'row',
alignItems: 'baseline'
}, },
nameAndMeta: { nameAndMeta: {
flexBasis: '80%' flexBasis: '80%'
@ -172,10 +161,6 @@ const styles = StyleSheet.create({
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center' alignItems: 'center'
}, },
nameWithoutEmoji: {
...StyleConstants.FontStyle.M,
fontWeight: StyleConstants.Font.Weight.Bold
},
account: { account: {
flex: 1, flex: 1,
marginLeft: StyleConstants.Spacing.XS marginLeft: StyleConstants.Spacing.XS
@ -199,7 +184,8 @@ const styles = StyleSheet.create({
action: { action: {
flexBasis: '20%', flexBasis: '20%',
flexDirection: 'row', 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 React, { useCallback, useEffect, useMemo, useState } from 'react'
import { Pressable, StyleSheet, Text, View } from 'react-native' import { Pressable, StyleSheet, Text, View } from 'react-native'
import { Chase } from 'react-native-animated-spinkit' import { Chase } from 'react-native-animated-spinkit'
import { useQuery } from 'react-query' 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 { export interface Props {
notification: Mastodon.Notification notification: Mastodon.Notification
@ -129,16 +129,9 @@ const TimelineHeaderNotification: React.FC<Props> = ({ notification }) => {
<View style={styles.base}> <View style={styles.base}>
<View style={styles.nameAndMeta}> <View style={styles.nameAndMeta}>
<View style={styles.nameAndAccount}> <View style={styles.nameAndAccount}>
{emojis?.length ? ( <Text numberOfLines={1}>
<Emojis content={name} emojis={emojis} size='M' fontBold={true} /> <ParseEmojis content={name} emojis={emojis} fontBold />
) : (
<Text
numberOfLines={1}
style={[styles.nameWithoutEmoji, { color: theme.primary }]}
>
{name}
</Text> </Text>
)}
<Text <Text
style={[styles.account, { color: theme.secondary }]} style={[styles.account, { color: theme.secondary }]}
numberOfLines={1} numberOfLines={1}
@ -194,10 +187,6 @@ const styles = StyleSheet.create({
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center' alignItems: 'center'
}, },
nameWithoutEmoji: {
...StyleConstants.FontStyle.M,
fontWeight: StyleConstants.Font.Weight.Bold
},
account: { account: {
flexShrink: 1, flexShrink: 1,
marginLeft: StyleConstants.Spacing.XS 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 { 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 React, { useCallback, useMemo, useState } from 'react'
import { Pressable, StyleSheet, Text, View } from 'react-native' import { Pressable, StyleSheet, Text, View } from 'react-native'
import { useMutation, useQueryClient } from 'react-query' 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 ({ const fireMutation = async ({
id, id,
@ -39,11 +37,6 @@ const fireMutation = async ({
if (res.body.id === id) { if (res.body.id === id) {
return Promise.resolve(res.body as Mastodon.Poll) return Promise.resolve(res.body as Mastodon.Poll)
} else { } else {
toast({
type: 'error',
content: '投票失败,请重试',
autoHide: false
})
return Promise.reject() return Promise.reject()
} }
} }
@ -61,7 +54,7 @@ const TimelinePoll: React.FC<Props> = ({
reblog, reblog,
sameAccount sameAccount
}) => { }) => {
const { theme } = useTheme() const { mode, theme } = useTheme()
const queryClient = useQueryClient() const queryClient = useQueryClient()
const [allOptions, setAllOptions] = useState( const [allOptions, setAllOptions] = useState(
@ -98,10 +91,13 @@ const TimelinePoll: React.FC<Props> = ({
}) })
haptics('Success') haptics('Success')
},
onError: () => {
haptics('Error')
} }
}) })
const pollButton = () => { const pollButton = useMemo(() => {
if (!poll.expired) { if (!poll.expired) {
if (!sameAccount && !poll.voted) { if (!sameAccount && !poll.voted) {
return ( return (
@ -131,7 +127,7 @@ const TimelinePoll: React.FC<Props> = ({
) )
} }
} }
} }, [poll.expired, poll.voted, allOptions, mutation.isLoading])
const pollExpiration = useMemo(() => { const pollExpiration = useMemo(() => {
if (poll.expired) { if (poll.expired) {
@ -147,7 +143,7 @@ const TimelinePoll: React.FC<Props> = ({
</Text> </Text>
) )
} }
}, []) }, [mode])
const isSelected = useCallback( const isSelected = useCallback(
(index: number): any => (index: number): any =>
@ -157,35 +153,26 @@ const TimelinePoll: React.FC<Props> = ({
[allOptions] [allOptions]
) )
return ( const pollBodyDisallow = useMemo(() => {
<View style={styles.base}> return poll.options.map((option, index) => (
{poll.options.map((option, index) => <View key={index} style={styles.optionContainer}>
poll.voted ? ( <View style={styles.optionContent}>
<View key={index} style={styles.poll}>
<View style={styles.optionSelected}>
<Feather <Feather
style={styles.voted} style={styles.optionSelection}
name={ name={
`${poll.own_votes!.includes(index) ? 'check-' : ''}${ `${poll.own_votes?.includes(index) ? 'check-' : ''}${
poll.multiple ? 'square' : 'circle' poll.multiple ? 'square' : 'circle'
}` as any }` as any
} }
size={StyleConstants.Font.Size.M} size={StyleConstants.Font.Size.M}
color={ color={
poll.own_votes!.includes(index) poll.own_votes?.includes(index) ? theme.primary : theme.disabled
? theme.primary
: theme.disabled
} }
/> />
<View style={styles.contentSelected}> <Text style={styles.optionText}>
<Emojis <ParseEmojis content={option.title} emojis={poll.emojis} />
content={option.title} </Text>
emojis={poll.emojis} <Text style={[styles.optionPercentage, { color: theme.primary }]}>
size='M'
numberOfLines={2}
/>
</View>
<Text style={[styles.percentage, { color: theme.primary }]}>
{poll.votes_count {poll.votes_count
? Math.round((option.votes_count / poll.voters_count) * 100) ? Math.round((option.votes_count / poll.voters_count) * 100)
: 0} : 0}
@ -205,16 +192,17 @@ const TimelinePoll: React.FC<Props> = ({
]} ]}
/> />
</View> </View>
) : ( ))
<View key={index} style={styles.poll}> }, [mode, poll.options])
const pollBodyAllow = useMemo(() => {
return poll.options.map((option, index) => (
<Pressable <Pressable
style={[styles.optionUnselected]} key={index}
style={styles.optionContainer}
onPress={() => { onPress={() => {
haptics('Light') haptics('Light')
if (poll.multiple) { if (poll.multiple) {
setAllOptions( setAllOptions(allOptions.map((o, i) => (i === index ? !o : o)))
allOptions.map((o, i) => (i === index ? !o : o))
)
} else { } else {
{ {
const otherOptions = const otherOptions =
@ -232,26 +220,26 @@ const TimelinePoll: React.FC<Props> = ({
} }
}} }}
> >
<View style={[styles.optionContent]}>
<Feather <Feather
style={styles.votedNot} style={styles.optionSelection}
name={isSelected(index)} name={isSelected(index)}
size={StyleConstants.Font.Size.L} size={StyleConstants.Font.Size.L}
color={theme.primary} color={theme.primary}
/> />
<View style={styles.contentUnselected}> <Text style={styles.optionText}>
<Emojis <ParseEmojis content={option.title} emojis={poll.emojis} />
content={option.title} </Text>
emojis={poll.emojis}
size='M'
numberOfLines={2}
/>
</View> </View>
</Pressable> </Pressable>
</View> ))
) }, [mode, allOptions])
)}
return (
<View style={styles.base}>
{poll.expired || poll.voted ? pollBodyDisallow : pollBodyAllow}
<View style={styles.meta}> <View style={styles.meta}>
{pollButton()} {pollButton}
<Text style={[styles.votes, { color: theme.secondary }]}> <Text style={[styles.votes, { color: theme.secondary }]}>
{poll.voters_count || 0}{' • '} {poll.voters_count || 0}{' • '}
</Text> </Text>
@ -265,39 +253,24 @@ const styles = StyleSheet.create({
base: { base: {
marginTop: StyleConstants.Spacing.M marginTop: StyleConstants.Spacing.M
}, },
poll: { optionContainer: {
flex: 1, flex: 1,
minHeight: StyleConstants.Font.LineHeight.M * 2, paddingVertical: StyleConstants.Spacing.S
paddingVertical: StyleConstants.Spacing.XS
}, },
optionSelected: { optionContent: {
flex: 1,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingRight: StyleConstants.Spacing.M
},
optionUnselected: {
flex: 1, flex: 1,
flexDirection: 'row' flexDirection: 'row'
}, },
contentSelected: { optionText: {
flex: 1, flex: 1
flexDirection: 'row',
alignItems: 'center',
paddingRight: StyleConstants.Spacing.S
}, },
contentUnselected: { optionSelection: {
flexShrink: 1
},
voted: {
marginRight: StyleConstants.Spacing.S marginRight: StyleConstants.Spacing.S
}, },
votedNot: { optionPercentage: {
paddingRight: StyleConstants.Spacing.S ...StyleConstants.FontStyle.M,
}, alignSelf: 'center',
percentage: { marginLeft: StyleConstants.Spacing.S
...StyleConstants.FontStyle.M
}, },
background: { background: {
height: StyleConstants.Spacing.XS, height: StyleConstants.Spacing.XS,
@ -314,7 +287,7 @@ const styles = StyleSheet.create({
marginTop: StyleConstants.Spacing.XS marginTop: StyleConstants.Spacing.XS
}, },
button: { button: {
marginRight: StyleConstants.Spacing.M marginRight: StyleConstants.Spacing.S
}, },
votes: { votes: {
...StyleConstants.FontStyle.S ...StyleConstants.FontStyle.S

View File

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

View File

@ -1,7 +1,7 @@
import { store } from '@root/store' import { store } from '@root/store'
import { getSettingsBrowser } from '@utils/slices/settingsSlice'
import * as Linking from 'expo-linking' import * as Linking from 'expo-linking'
import * as WebBrowser from 'expo-web-browser' import * as WebBrowser from 'expo-web-browser'
import { getSettingsBrowser } from './slices/settingsSlice'
const openLink = async (url: string) => { const openLink = async (url: string) => {
switch (getSettingsBrowser(store.getState())) { switch (getSettingsBrowser(store.getState())) {

View File

@ -12,7 +12,6 @@ import Logout from '@screens/Me/Root/Logout'
import { useScrollToTop } from '@react-navigation/native' import { useScrollToTop } from '@react-navigation/native'
import { AccountState } from '../Shared/Account' import { AccountState } from '../Shared/Account'
import AccountNav from '../Shared/Account/Nav' import AccountNav from '../Shared/Account/Nav'
import layoutAnimation from '@root/utils/styles/layoutAnimation'
const ScreenMeRoot: React.FC = () => { const ScreenMeRoot: React.FC = () => {
const localRegistered = useSelector(getLocalUrl) const localRegistered = useSelector(getLocalUrl)

View File

@ -1,9 +1,9 @@
import analytics from '@components/analytics'
import Button from '@components/Button' import Button from '@components/Button'
import ParseContent from '@components/ParseContent' import haptics from '@components/haptics'
import { ParseHTML } from '@components/Parse'
import { Feather } from '@expo/vector-icons' import { Feather } from '@expo/vector-icons'
import { useNavigation } from '@react-navigation/native' import { useNavigation } from '@react-navigation/native'
import analytics from '@root/components/analytics'
import haptics from '@root/components/haptics'
import { applicationFetch } from '@utils/fetches/applicationFetch' import { applicationFetch } from '@utils/fetches/applicationFetch'
import { instanceFetch } from '@utils/fetches/instanceFetch' import { instanceFetch } from '@utils/fetches/instanceFetch'
import { loginLocal } from '@utils/slices/instancesSlice' import { loginLocal } from '@utils/slices/instancesSlice'
@ -144,7 +144,12 @@ const Login: React.FC = () => {
height={StyleConstants.Font.Size.M} height={StyleConstants.Font.Size.M}
shimmerColors={theme.shimmer} shimmerColors={theme.shimmer}
> >
<ParseContent content={content!} size={'M'} numberOfLines={5} /> <ParseHTML
content={content!}
size={'M'}
numberOfLines={5}
expandHint='介绍'
/>
</ShimmerPlaceholder> </ShimmerPlaceholder>
</View> </View>
) )

View File

@ -13,7 +13,6 @@ import BottomSheet from '@root/components/BottomSheet'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import { getLocalAccountId } from '@root/utils/slices/instancesSlice' import { getLocalAccountId } from '@root/utils/slices/instancesSlice'
import HeaderDefaultActionsAccount from '@root/components/Timelines/Timeline/Shared/HeaderDefault/ActionsAccount' import HeaderDefaultActionsAccount from '@root/components/Timelines/Timeline/Shared/HeaderDefault/ActionsAccount'
import layoutAnimation from '@root/utils/styles/layoutAnimation'
// Moved account example: https://m.cmx.im/web/accounts/27812 // Moved account example: https://m.cmx.im/web/accounts/27812
@ -113,12 +112,12 @@ const ScreenSharedAccount: React.FC<Props> = ({
/> />
) : null} ) : null}
<ScrollView <ScrollView
bounces={false} scrollEventThrottle={16}
showsVerticalScrollIndicator={false}
onScroll={Animated.event( onScroll={Animated.event(
[{ nativeEvent: { contentOffset: { y: scrollY } } }], [{ nativeEvent: { contentOffset: { y: scrollY } } }],
{ useNativeDriver: false } { useNativeDriver: false }
)} )}
scrollEventThrottle={8}
> >
<AccountHeader <AccountHeader
accountState={accountState} accountState={accountState}

View File

@ -1,6 +1,7 @@
import { useNavigation } from '@react-navigation/native' import { useNavigation } from '@react-navigation/native'
import client from '@root/api/client' import client from '@root/api/client'
import Button from '@root/components/Button' import Button from '@root/components/Button'
import haptics from '@root/components/haptics'
import { toast } from '@root/components/toast' import { toast } from '@root/components/toast'
import { relationshipFetch } from '@root/utils/fetches/relationshipFetch' import { relationshipFetch } from '@root/utils/fetches/relationshipFetch'
import { StyleConstants } from '@root/utils/styles/constants' import { StyleConstants } from '@root/utils/styles/constants'
@ -55,8 +56,14 @@ const AccountInformationActions: React.FC<Props> = ({ account }) => {
}, [account]) }, [account])
const queryClient = useQueryClient() const queryClient = useQueryClient()
const mutation = useMutation(fireMutation, { const mutation = useMutation(fireMutation, {
onSuccess: data => queryClient.setQueryData(relationshipQueryKey, data), onSuccess: data => {
onError: () => toast({ type: 'error', content: '关注失败,请重试' }) haptics('Success')
queryClient.setQueryData(relationshipQueryKey, data)
},
onError: () => {
haptics('Error')
toast({ type: 'error', content: '关注失败,请重试' })
}
}) })
const mainAction = useMemo(() => { const mainAction = useMemo(() => {

View File

@ -1,7 +1,7 @@
import { ParseHTML } from '@components/Parse'
import { Feather } from '@expo/vector-icons' import { Feather } from '@expo/vector-icons'
import ParseContent from '@root/components/ParseContent' import { StyleConstants } from '@utils/styles/constants'
import { StyleConstants } from '@root/utils/styles/constants' import { useTheme } from '@utils/styles/ThemeManager'
import { useTheme } from '@root/utils/styles/ThemeManager'
import React from 'react' import React from 'react'
import { StyleSheet, View } from 'react-native' import { StyleSheet, View } from 'react-native'
@ -20,7 +20,7 @@ const AccountInformationFields: React.FC<Props> = ({ account }) => {
style={[styles.field, { borderBottomColor: theme.border }]} style={[styles.field, { borderBottomColor: theme.border }]}
> >
<View style={[styles.fieldLeft, { borderRightColor: theme.border }]}> <View style={[styles.fieldLeft, { borderRightColor: theme.border }]}>
<ParseContent <ParseHTML
content={field.name} content={field.name}
size={'M'} size={'M'}
emojis={account.emojis} emojis={account.emojis}
@ -36,7 +36,7 @@ const AccountInformationFields: React.FC<Props> = ({ account }) => {
) : null} ) : null}
</View> </View>
<View style={styles.fieldRight}> <View style={styles.fieldRight}>
<ParseContent <ParseHTML
content={field.value} content={field.value}
size={'M'} size={'M'}
emojis={account.emojis} emojis={account.emojis}

View File

@ -1,9 +1,9 @@
import Emojis from '@root/components/Timelines/Timeline/Shared/Emojis' import { ParseEmojis } from '@components/Parse'
import { StyleConstants } from '@root/utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@root/utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import { LinearGradient } from 'expo-linear-gradient' import { LinearGradient } from 'expo-linear-gradient'
import React, { forwardRef } from 'react' import React, { forwardRef } from 'react'
import { StyleSheet, Text, View } from 'react-native' import { StyleSheet } from 'react-native'
import ShimmerPlaceholder, { import ShimmerPlaceholder, {
createShimmerPlaceholder createShimmerPlaceholder
} from 'react-native-shimmer-placeholder' } from 'react-native-shimmer-placeholder'
@ -28,26 +28,14 @@ const AccountInformationName = forwardRef<ShimmerPlaceholder, Props>(
style={styles.name} style={styles.name}
shimmerColors={theme.shimmer} shimmerColors={theme.shimmer}
> >
<View> {account ? (
{account?.emojis ? ( <ParseEmojis
<Emojis content={account.display_name || account.username}
content={account?.display_name || account?.username}
emojis={account.emojis} emojis={account.emojis}
size='L' size='L'
fontBold={true} fontBold
/> />
) : ( ) : null}
<Text
style={{
color: theme.primary,
...StyleConstants.FontStyle.L,
fontWeight: StyleConstants.Font.Weight.Bold
}}
>
{account?.display_name || account?.username}
</Text>
)}
</View>
</ShimmerPlaceholder> </ShimmerPlaceholder>
) )
} }

View File

@ -1,5 +1,5 @@
import ParseContent from '@root/components/ParseContent' import { ParseHTML } from '@components/Parse'
import { StyleConstants } from '@root/utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import React from 'react' import React from 'react'
import { StyleSheet, View } from 'react-native' import { StyleSheet, View } from 'react-native'
@ -10,11 +10,7 @@ export interface Props {
const AccountInformationNotes: React.FC<Props> = ({ account }) => { const AccountInformationNotes: React.FC<Props> = ({ account }) => {
return ( return (
<View style={styles.note}> <View style={styles.note}>
<ParseContent <ParseHTML content={account.note!} size={'M'} emojis={account.emojis} />
content={account.note!}
size={'M'}
emojis={account.emojis}
/>
</View> </View>
) )
} }

View File

@ -1,10 +1,10 @@
import Emojis from '@root/components/Timelines/Timeline/Shared/Emojis' import { ParseEmojis } from '@components/Parse'
import { StyleConstants } from '@root/utils/styles/constants' import { AccountState } from '@screens/Shared/Account'
import { useTheme } from '@root/utils/styles/ThemeManager' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React from 'react' import React from 'react'
import { Animated, Dimensions, StyleSheet, Text, View } from 'react-native' import { Animated, Dimensions, StyleSheet, Text, View } from 'react-native'
import { useSafeAreaInsets } from 'react-native-safe-area-context' import { useSafeAreaInsets } from 'react-native-safe-area-context'
import { AccountState } from '../Account'
export interface Props { export interface Props {
accountState: AccountState accountState: AccountState
@ -59,24 +59,15 @@ const AccountNav: React.FC<Props> = ({ accountState, scrollY, account }) => {
} }
]} ]}
> >
{account?.emojis ? ( {account ? (
<Emojis <Text numberOfLines={1}>
content={account?.display_name || account?.username} <ParseEmojis
content={account.display_name || account.username}
emojis={account.emojis} emojis={account.emojis}
size='M' fontBold
fontBold={true}
/> />
) : (
<Text
style={{
color: theme.primary,
...StyleConstants.FontStyle.M,
fontWeight: StyleConstants.Font.Weight.Bold
}}
>
{account?.display_name || account?.username}
</Text> </Text>
)} ) : null}
</Animated.View> </Animated.View>
</View> </View>
</Animated.View> </Animated.View>

View File

@ -19,6 +19,9 @@ const AccountToots: React.FC<Props> = ({
accountDispatch, accountDispatch,
id id
}) => { }) => {
const headerHeight = useSafeAreaInsets().top + 44
const footerHeight = useSafeAreaInsets().bottom + useBottomTabBarHeight()
const routes: { key: App.Pages }[] = [ const routes: { key: App.Pages }[] = [
{ key: 'Account_Default' }, { key: 'Account_Default' },
{ key: 'Account_All' }, { key: 'Account_All' },
@ -37,20 +40,11 @@ const AccountToots: React.FC<Props> = ({
}, },
[] []
) )
const headerHeight = useSafeAreaInsets().top + 44
const footerHeight = useSafeAreaInsets().bottom + useBottomTabBarHeight()
return ( return (
<TabView <TabView
lazy lazy
swipeEnabled swipeEnabled
style={[
styles.base,
{
height:
Dimensions.get('window').height - headerHeight - footerHeight - 33
}
]}
renderScene={renderScene} renderScene={renderScene}
renderTabBar={() => null} renderTabBar={() => null}
initialLayout={{ width: Dimensions.get('window').width }} initialLayout={{ width: Dimensions.get('window').width }}
@ -58,6 +52,16 @@ const AccountToots: React.FC<Props> = ({
onIndexChange={index => onIndexChange={index =>
accountDispatch({ type: 'segmentedIndex', payload: index }) accountDispatch({ type: 'segmentedIndex', payload: index })
} }
style={[
styles.base,
{
height:
Dimensions.get('window').height -
headerHeight -
footerHeight -
(33 + StyleConstants.Spacing.Global.PagePadding * 2)
}
]}
/> />
) )
} }
@ -68,4 +72,4 @@ const styles = StyleSheet.create({
} }
}) })
export default AccountToots export default React.memo(AccountToots, () => true)

View File

@ -1,12 +1,12 @@
import client from '@api/client'
import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs' import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs'
import client from '@root/api/client' import Button from '@components/Button'
import Button from '@root/components/Button' import haptics from '@components/haptics'
import haptics from '@root/components/haptics' import { ParseHTML } from '@components/Parse'
import ParseContent from '@root/components/ParseContent' import relativeTime from '@components/relativeTime'
import { announcementFetch } from '@root/utils/fetches/announcementsFetch' import { announcementFetch } from '@utils/fetches/announcementsFetch'
import relativeTime from '@root/utils/relativeTime' import { StyleConstants } from '@utils/styles/constants'
import { StyleConstants } from '@root/utils/styles/constants' import { useTheme } from '@utils/styles/ThemeManager'
import { useTheme } from '@root/utils/styles/ThemeManager'
import React, { useCallback, useEffect, useState } from 'react' import React, { useCallback, useEffect, useState } from 'react'
import { import {
Dimensions, Dimensions,
@ -103,7 +103,7 @@ const ScreenSharedAnnouncements: React.FC = ({
{relativeTime(item.published_at)} {relativeTime(item.published_at)}
</Text> </Text>
<ScrollView style={styles.scrollView} showsVerticalScrollIndicator> <ScrollView style={styles.scrollView} showsVerticalScrollIndicator>
<ParseContent <ParseHTML
content={item.content} content={item.content}
size='M' size='M'
emojis={item.emojis} emojis={item.emojis}

View File

@ -1,6 +1,5 @@
import { HeaderLeft, HeaderRight } from '@components/Header' import { HeaderLeft, HeaderRight } from '@components/Header'
import haptics from '@root/components/haptics' import haptics from '@root/components/haptics'
import { toast } from '@root/components/toast'
import { store } from '@root/store' import { store } from '@root/store'
import layoutAnimation from '@root/utils/styles/layoutAnimation' import layoutAnimation from '@root/utils/styles/layoutAnimation'
import formatText from '@screens/Shared/Compose/formatText' import formatText from '@screens/Shared/Compose/formatText'
@ -27,7 +26,6 @@ import { SafeAreaView } from 'react-native-safe-area-context'
import { createNativeStackNavigator } from 'react-native-screens/native-stack' import { createNativeStackNavigator } from 'react-native-screens/native-stack'
import { useQueryClient } from 'react-query' import { useQueryClient } from 'react-query'
import ComposeEditAttachment from './Compose/EditAttachment' import ComposeEditAttachment from './Compose/EditAttachment'
import ComposeEditAttachmentRoot from './Compose/EditAttachment/Root'
import composeInitialState from './Compose/utils/initialState' import composeInitialState from './Compose/utils/initialState'
import composeParseState from './Compose/utils/parseState' import composeParseState from './Compose/utils/parseState'
import composeSend from './Compose/utils/post' import composeSend from './Compose/utils/post'
@ -190,7 +188,6 @@ const Compose: React.FC<Props> = ({ route: { params }, navigation }) => {
haptics('Success') haptics('Success')
queryClient.invalidateQueries(['Following']) queryClient.invalidateQueries(['Following'])
navigation.goBack() navigation.goBack()
toast({ type: 'success', content: '发布成功' })
}) })
.catch(() => { .catch(() => {
haptics('Error') haptics('Error')

View File

@ -6,7 +6,13 @@ import addAttachment from '@screens/Shared/Compose/addAttachment'
import { ExtendedAttachment } from '@screens/Shared/Compose/utils/types' import { ExtendedAttachment } from '@screens/Shared/Compose/utils/types'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React, { useCallback, useContext, useMemo } from 'react' import React, {
useCallback,
useContext,
useEffect,
useMemo,
useRef
} from 'react'
import { import {
FlatList, FlatList,
Image, Image,
@ -26,6 +32,9 @@ const ComposeAttachments: React.FC = () => {
const { theme } = useTheme() const { theme } = useTheme()
const navigation = useNavigation() const navigation = useNavigation()
const flatListRef = useRef<FlatList>(null)
let prevOffsets = useRef<number[]>()
const sensitiveOnPress = useCallback( const sensitiveOnPress = useCallback(
() => () =>
composeDispatch({ composeDispatch({
@ -35,35 +44,75 @@ const ComposeAttachments: React.FC = () => {
[composeState.attachments.sensitive] [composeState.attachments.sensitive]
) )
const renderAttachment = useCallback( const calculateWidth = useCallback(item => {
({ item, index }: { item: ExtendedAttachment; index: number }) => {
let calculatedWidth: number
if (item.local) { if (item.local) {
calculatedWidth = return (item.local.width / item.local.height) * DEFAULT_HEIGHT
(item.local.width / item.local.height) * DEFAULT_HEIGHT
} else { } else {
if (item.remote) { if (item.remote) {
if (item.remote.meta.original.aspect) { if (item.remote.meta.original.aspect) {
calculatedWidth = item.remote.meta.original.aspect * DEFAULT_HEIGHT return item.remote.meta.original.aspect * DEFAULT_HEIGHT
} else if ( } else if (
item.remote.meta.original.width && item.remote.meta.original.width &&
item.remote.meta.original.height item.remote.meta.original.height
) { ) {
calculatedWidth = return (
(item.remote.meta.original.width / (item.remote.meta.original.width /
item.remote.meta.original.height) * item.remote.meta.original.height) *
DEFAULT_HEIGHT DEFAULT_HEIGHT
)
} else { } else {
calculatedWidth = DEFAULT_HEIGHT return DEFAULT_HEIGHT
} }
} else { } else {
calculatedWidth = DEFAULT_HEIGHT return DEFAULT_HEIGHT
} }
} }
}, [])
const snapToOffsets = useMemo(() => {
const attachmentsOffsets = composeState.attachments.uploads.map(
(_, index) => {
let currentOffset = 0
Array.from(Array(index).keys()).map(
i =>
(currentOffset =
currentOffset +
calculateWidth(composeState.attachments.uploads[i]) +
StyleConstants.Spacing.Global.PagePadding)
)
return currentOffset
}
)
return attachmentsOffsets.length < 4
? [
...attachmentsOffsets,
attachmentsOffsets.reduce((a, b) => a + b, 0) +
DEFAULT_HEIGHT +
StyleConstants.Spacing.Global.PagePadding
]
: attachmentsOffsets
}, [composeState.attachments.uploads.length])
useEffect(() => {
if (
snapToOffsets.length >
(prevOffsets.current ? prevOffsets.current?.length : 0)
) {
flatListRef.current?.scrollToOffset({
offset:
snapToOffsets[snapToOffsets.length - 2] +
snapToOffsets[snapToOffsets.length - 1]
})
}
prevOffsets.current = snapToOffsets
}, [snapToOffsets, prevOffsets])
const renderAttachment = useCallback(
({ item, index }: { item: ExtendedAttachment; index: number }) => {
return ( return (
<View <View
key={index} key={index}
style={[styles.container, { width: calculatedWidth }]} style={[styles.container, { width: calculateWidth(item) }]}
> >
<Image <Image
style={styles.image} style={styles.image}
@ -172,7 +221,6 @@ const ComposeAttachments: React.FC = () => {
), ),
[] []
) )
return ( return (
<View style={styles.base}> <View style={styles.base}>
<Pressable style={styles.sensitive} onPress={sensitiveOnPress}> <Pressable style={styles.sensitive} onPress={sensitiveOnPress}>
@ -187,14 +235,19 @@ const ComposeAttachments: React.FC = () => {
</Pressable> </Pressable>
<FlatList <FlatList
horizontal horizontal
keyExtractor={item => item.local!.uri || item.remote!.url} ref={flatListRef}
data={composeState.attachments.uploads} decelerationRate={0}
pagingEnabled={false}
snapToAlignment='center'
renderItem={renderAttachment} renderItem={renderAttachment}
snapToOffsets={snapToOffsets}
keyboardShouldPersistTaps='handled'
showsHorizontalScrollIndicator={false}
data={composeState.attachments.uploads}
keyExtractor={item => item.local!.uri || item.remote!.url}
ListFooterComponent={ ListFooterComponent={
composeState.attachments.uploads.length < 4 ? listFooter : null composeState.attachments.uploads.length < 4 ? listFooter : null
} }
showsHorizontalScrollIndicator={false}
keyboardShouldPersistTaps='handled'
/> />
</View> </View>
) )

View File

@ -1,5 +1,5 @@
import Emojis from '@components/Timelines/Timeline/Shared/Emojis' import haptics from '@components/haptics'
import haptics from '@root/components/haptics' import { ParseEmojis } from '@components/Parse'
import { ComposeContext } from '@screens/Shared/Compose' import { ComposeContext } from '@screens/Shared/Compose'
import ComposeActions from '@screens/Shared/Compose/Actions' import ComposeActions from '@screens/Shared/Compose/Actions'
import ComposeRootFooter from '@screens/Shared/Compose/Root/Footer' import ComposeRootFooter from '@screens/Shared/Compose/Root/Footer'
@ -9,7 +9,6 @@ import { emojisFetch } from '@utils/fetches/emojisFetch'
import { searchFetch } from '@utils/fetches/searchFetch' import { searchFetch } from '@utils/fetches/searchFetch'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import * as Permissions from 'expo-permissions'
import { forEach, groupBy, sortBy } from 'lodash' import { forEach, groupBy, sortBy } from 'lodash'
import React, { import React, {
Dispatch, Dispatch,
@ -66,18 +65,20 @@ const ListItem = React.memo(
<View style={[styles.account, { borderBottomColor: theme.border }]}> <View style={[styles.account, { borderBottomColor: theme.border }]}>
<Image source={{ uri: item.avatar }} style={styles.accountAvatar} /> <Image source={{ uri: item.avatar }} style={styles.accountAvatar} />
<View> <View>
<Text style={[styles.accountName, { color: theme.primary }]}> <Text
{item.emojis?.length ? ( style={[styles.accountName, { color: theme.primary }]}
<Emojis numberOfLines={1}
>
<ParseEmojis
content={item.display_name || item.username} content={item.display_name || item.username}
emojis={item.emojis} emojis={item.emojis}
size='S' size='S'
/> />
) : (
item.display_name || item.username
)}
</Text> </Text>
<Text style={[styles.accountAccount, { color: theme.primary }]}> <Text
style={[styles.accountAccount, { color: theme.primary }]}
numberOfLines={1}
>
@{item.acct} @{item.acct}
</Text> </Text>
</View> </View>

View File

@ -94,8 +94,12 @@ const formatText = ({
contentLength = contentLength + 23 contentLength = contentLength + 23
break break
case 'accounts': case 'accounts':
if (main.match(/@/g)!.length > 1) {
contentLength = contentLength =
contentLength + main.split(new RegExp('(@.*)@?'))[1].length contentLength + main.split(new RegExp('(@.*?)@'))[1].length
} else {
contentLength = contentLength + main.length
}
break break
case 'hashtags': case 'hashtags':
contentLength = contentLength + main.length contentLength = contentLength + main.length

View File

@ -16,6 +16,9 @@ const composeParseState = ({
case 'edit': case 'edit':
return { return {
...composeInitialState, ...composeInitialState,
...(incomingStatus.spoiler_text && {
spoiler: { ...composeInitialState.spoiler, active: true }
}),
...(incomingStatus.poll && { ...(incomingStatus.poll && {
poll: { poll: {
active: true, active: true,

View File

@ -109,8 +109,8 @@ const ScreenSharedImagesViewer: React.FC<Props> = ({
{ {
url: imageUrls[currentIndex].url url: imageUrls[currentIndex].url
}, },
() => haptics('Success'), () => haptics('Error'),
() => haptics('Error') () => haptics('Success')
) )
} }
/> />

View File

@ -1,11 +1,10 @@
import { HeaderRight } from '@components/Header'
import { ParseEmojis, ParseHTML } from '@components/Parse'
import { Feather } from '@expo/vector-icons' import { Feather } from '@expo/vector-icons'
import { useNavigation } from '@react-navigation/native' import { useNavigation } from '@react-navigation/native'
import { HeaderRight } from '@root/components/Header' import { searchFetch } from '@utils/fetches/searchFetch'
import ParseContent from '@root/components/ParseContent' import { StyleConstants } from '@utils/styles/constants'
import Emojis from '@root/components/Timelines/Timeline/Shared/Emojis' import { useTheme } from '@utils/styles/ThemeManager'
import { searchFetch } from '@root/utils/fetches/searchFetch'
import { StyleConstants } from '@root/utils/styles/constants'
import { useTheme } from '@root/utils/styles/ThemeManager'
import { debounce } from 'lodash' import { debounce } from 'lodash'
import React, { useCallback, useEffect, useMemo, useState } from 'react' import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { import {
@ -173,21 +172,16 @@ const ScreenSharedSearch: React.FC = () => {
style={styles.itemAccountAvatar} style={styles.itemAccountAvatar}
/> />
<View> <View>
{item.emojis?.length ? ( <Text numberOfLines={1}>
<Emojis <ParseEmojis
content={item.display_name || item.username} content={item.display_name || item.username}
emojis={item.emojis} emojis={item.emojis}
size='S' size='S'
fontBold={true} fontBold
/> />
) : (
<Text
style={[styles.nameWithoutEmoji, { color: theme.primary }]}
>
{item.display_name || item.username}
</Text> </Text>
)}
<Text <Text
numberOfLines={1}
style={[styles.itemAccountAcct, { color: theme.secondary }]} style={[styles.itemAccountAcct, { color: theme.secondary }]}
> >
@{item.acct} @{item.acct}
@ -229,28 +223,23 @@ const ScreenSharedSearch: React.FC = () => {
style={styles.itemAccountAvatar} style={styles.itemAccountAvatar}
/> />
<View> <View>
{item.account.emojis?.length ? ( <Text numberOfLines={1}>
<Emojis <ParseEmojis
content={item.account.display_name || item.account.username} content={item.account.display_name || item.account.username}
emojis={item.account.emojis} emojis={item.account.emojis}
size='S' size='S'
fontBold={true} fontBold
/> />
) : (
<Text
style={[styles.nameWithoutEmoji, { color: theme.primary }]}
>
{item.account.display_name || item.account.username}
</Text> </Text>
)}
<Text <Text
numberOfLines={1}
style={[styles.itemAccountAcct, { color: theme.secondary }]} style={[styles.itemAccountAcct, { color: theme.secondary }]}
> >
@{item.account.acct} @{item.account.acct}
</Text> </Text>
{item.content && ( {item.content && (
<View style={styles.itemStatus}> <View style={styles.itemStatus}>
<ParseContent <ParseHTML
content={item.content} content={item.content}
size='M' size='M'
emojis={item.emojis} emojis={item.emojis}
@ -405,10 +394,6 @@ const styles = StyleSheet.create({
borderRadius: 6, borderRadius: 6,
marginRight: StyleConstants.Spacing.S marginRight: StyleConstants.Spacing.S
}, },
nameWithoutEmoji: {
...StyleConstants.FontStyle.S,
fontWeight: StyleConstants.Font.Weight.Bold
},
itemAccountAcct: { marginTop: StyleConstants.Spacing.XS }, itemAccountAcct: { marginTop: StyleConstants.Spacing.XS },
itemHashtag: { itemHashtag: {
...StyleConstants.FontStyle.M ...StyleConstants.FontStyle.M

View File

@ -15,7 +15,7 @@ const ScreenSharedToot: React.FC<Props> = ({
params: { toot } params: { toot }
} }
}) => { }) => {
return <Timeline page='Toot' toot={toot} disableRefresh /> return <Timeline page='Toot' toot={toot} disableRefresh disableInfinity />
} }
export default ScreenSharedToot export default ScreenSharedToot