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 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

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

View File

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

View File

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

View File

@ -13,7 +13,6 @@ import BottomSheet from '@root/components/BottomSheet'
import { useSelector } from 'react-redux'
import { getLocalAccountId } from '@root/utils/slices/instancesSlice'
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
@ -113,12 +112,12 @@ const ScreenSharedAccount: React.FC<Props> = ({
/>
) : null}
<ScrollView
bounces={false}
scrollEventThrottle={16}
showsVerticalScrollIndicator={false}
onScroll={Animated.event(
[{ nativeEvent: { contentOffset: { y: scrollY } } }],
{ useNativeDriver: false }
)}
scrollEventThrottle={8}
>
<AccountHeader
accountState={accountState}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -19,6 +19,9 @@ const AccountToots: React.FC<Props> = ({
accountDispatch,
id
}) => {
const headerHeight = useSafeAreaInsets().top + 44
const footerHeight = useSafeAreaInsets().bottom + useBottomTabBarHeight()
const routes: { key: App.Pages }[] = [
{ key: 'Account_Default' },
{ key: 'Account_All' },
@ -37,20 +40,11 @@ const AccountToots: React.FC<Props> = ({
},
[]
)
const headerHeight = useSafeAreaInsets().top + 44
const footerHeight = useSafeAreaInsets().bottom + useBottomTabBarHeight()
return (
<TabView
lazy
swipeEnabled
style={[
styles.base,
{
height:
Dimensions.get('window').height - headerHeight - footerHeight - 33
}
]}
renderScene={renderScene}
renderTabBar={() => null}
initialLayout={{ width: Dimensions.get('window').width }}
@ -58,6 +52,16 @@ const AccountToots: React.FC<Props> = ({
onIndexChange={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 client from '@root/api/client'
import Button from '@root/components/Button'
import haptics from '@root/components/haptics'
import ParseContent from '@root/components/ParseContent'
import { announcementFetch } from '@root/utils/fetches/announcementsFetch'
import relativeTime from '@root/utils/relativeTime'
import { StyleConstants } from '@root/utils/styles/constants'
import { useTheme } from '@root/utils/styles/ThemeManager'
import Button from '@components/Button'
import haptics from '@components/haptics'
import { ParseHTML } from '@components/Parse'
import relativeTime from '@components/relativeTime'
import { announcementFetch } from '@utils/fetches/announcementsFetch'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useCallback, useEffect, useState } from 'react'
import {
Dimensions,
@ -103,7 +103,7 @@ const ScreenSharedAnnouncements: React.FC = ({
{relativeTime(item.published_at)}
</Text>
<ScrollView style={styles.scrollView} showsVerticalScrollIndicator>
<ParseContent
<ParseHTML
content={item.content}
size='M'
emojis={item.emojis}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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