1
0
mirror of https://github.com/tooot-app/app synced 2025-04-06 06:31:08 +02:00

Highlighted toot

This commit is contained in:
Zhiyuan Zheng 2020-12-12 22:19:18 +01:00
parent dcb37e91c9
commit 70743ec82d
No known key found for this signature in database
GPG Key ID: 078A93AB607D85E0
9 changed files with 163 additions and 82 deletions

View File

@ -1,5 +1,5 @@
import React, { useCallback, useState } from 'react' import React, { useCallback, useState } from 'react'
import { Pressable, StyleSheet, Text } from 'react-native' import { Pressable, Text } from 'react-native'
import HTMLView from 'react-native-htmlview' import HTMLView from 'react-native-htmlview'
import { useNavigation } from '@react-navigation/native' import { useNavigation } from '@react-navigation/native'
@ -22,7 +22,7 @@ const renderNode = ({
theme: any theme: any
node: any node: any
index: number index: number
size: number size: 'M' | 'L'
navigation: any navigation: any
mentions?: Mastodon.Mention[] mentions?: Mastodon.Mention[]
showFullLink: boolean showFullLink: boolean
@ -35,7 +35,10 @@ const renderNode = ({
return ( return (
<Text <Text
key={index} key={index}
style={{ color: theme.link, fontSize: size }} style={{
color: theme.link,
fontSize: StyleConstants.Font.Size[size]
}}
onPress={() => { onPress={() => {
const tag = href.split(new RegExp(/\/tag\/(.*)|\/tags\/(.*)/)) const tag = href.split(new RegExp(/\/tag\/(.*)|\/tags\/(.*)/))
navigation.push('Screen-Shared-Hashtag', { navigation.push('Screen-Shared-Hashtag', {
@ -51,7 +54,10 @@ const renderNode = ({
return ( return (
<Text <Text
key={index} key={index}
style={{ color: theme.link, fontSize: size }} style={{
color: theme.link,
fontSize: StyleConstants.Font.Size[size]
}}
onPress={() => { onPress={() => {
const username = href.split(new RegExp(/@(.*)/)) const username = href.split(new RegExp(/@(.*)/))
const usernameIndex = mentions.findIndex( const usernameIndex = mentions.findIndex(
@ -72,7 +78,10 @@ const renderNode = ({
return ( return (
<Text <Text
key={index} key={index}
style={{ color: theme.link, fontSize: size }} style={{
color: theme.link,
fontSize: StyleConstants.Font.Size[size]
}}
onPress={() => { onPress={() => {
navigation.navigate('Screen-Shared-Webview', { navigation.navigate('Screen-Shared-Webview', {
uri: href, uri: href,
@ -80,7 +89,11 @@ const renderNode = ({
}) })
}} }}
> >
<Feather name='external-link' size={size} color={theme.link} />{' '} <Feather
name='external-link'
size={StyleConstants.Font.Size[size]}
color={theme.link}
/>{' '}
{showFullLink ? href : domain[1]} {showFullLink ? href : domain[1]}
</Text> </Text>
) )
@ -90,7 +103,7 @@ const renderNode = ({
export interface Props { export interface Props {
content: string content: string
size: number size: 'M' | 'L'
emojis?: Mastodon.Emoji[] emojis?: Mastodon.Emoji[]
mentions?: Mastodon.Mention[] mentions?: Mastodon.Mention[]
showFullLink?: boolean showFullLink?: boolean
@ -124,7 +137,11 @@ const ParseContent: React.FC<Props> = ({
const textComponent = useCallback( const textComponent = useCallback(
({ children }) => ({ children }) =>
emojis && children ? ( emojis && children ? (
<Emojis content={children.toString()} emojis={emojis} size={size} /> <Emojis
content={children.toString()}
emojis={emojis}
size={StyleConstants.Font.Size[size]}
/>
) : ( ) : (
<Text>{children}</Text> <Text>{children}</Text>
), ),
@ -143,7 +160,7 @@ const ParseContent: React.FC<Props> = ({
numberOfLines={ numberOfLines={
totalLines && totalLines > numberOfLines ? shownLines : totalLines totalLines && totalLines > numberOfLines ? shownLines : totalLines
} }
style={styles.root} style={{ lineHeight: StyleConstants.Font.LineHeight[size] }}
onTextLayout={({ nativeEvent }) => { onTextLayout={({ nativeEvent }) => {
if (!textLoaded) { if (!textLoaded) {
setTextLoaded(true) setTextLoaded(true)
@ -200,10 +217,4 @@ const ParseContent: React.FC<Props> = ({
) )
} }
const styles = StyleSheet.create({
root: {
lineHeight: StyleConstants.Font.LineHeight.M
}
})
export default ParseContent export default ParseContent

View File

@ -1,5 +1,5 @@
import React, { useCallback, useEffect, useRef } from 'react' import React, { useCallback, useEffect, useMemo, useRef } from 'react'
import { ActivityIndicator, AppState, FlatList, StyleSheet } from 'react-native' import { AppState, FlatList, StyleSheet } from 'react-native'
import { setFocusHandler, useInfiniteQuery } from 'react-query' import { setFocusHandler, useInfiniteQuery } from 'react-query'
import TimelineNotifications from 'src/components/Timelines/Timeline/Notifications' import TimelineNotifications from 'src/components/Timelines/Timeline/Notifications'
@ -75,10 +75,33 @@ const Timeline: React.FC<Props> = ({
case 'Notifications': case 'Notifications':
return <TimelineNotifications notification={item} queryKey={queryKey} /> return <TimelineNotifications notification={item} queryKey={queryKey} />
default: default:
return <TimelineDefault item={item} queryKey={queryKey} /> return (
<TimelineDefault
item={item}
queryKey={queryKey}
{...(toot && toot.id === item.id && { highlighted: true })}
/>
)
} }
}, []) }, [])
const flItemSeparatorComponent = useCallback(() => <TimelineSeparator />, []) const flItemSeparatorComponent = useCallback(
({ leadingItem }) => (
<TimelineSeparator
{...(toot && toot.id === leadingItem.id && { highlighted: true })}
/>
),
[]
)
const flItemEmptyComponent = useMemo(
() => (
<TimelineEmpty
isLoading={isLoading}
isError={isError}
refetch={refetch}
/>
),
[isLoading, isError]
)
const flOnRefresh = useCallback( const flOnRefresh = useCallback(
() => () =>
!disableRefresh && !disableRefresh &&
@ -102,11 +125,12 @@ const Timeline: React.FC<Props> = ({
[flattenData] [flattenData]
) )
const flFooter = useCallback(() => { const flFooter = useCallback(() => {
if (isFetchingMore) { return <TimelineEnd isFetchingMore={isFetchingMore} />
return <ActivityIndicator /> // if (isFetchingMore) {
} else { // return <ActivityIndicator />
return <TimelineEnd /> // } else {
} // return <TimelineEnd />
// }
}, [isFetchingMore]) }, [isFetchingMore])
const onScrollToIndexFailed = useCallback(error => { const onScrollToIndexFailed = useCallback(error => {
const offset = error.averageItemLength * error.index const offset = error.averageItemLength * error.index
@ -127,18 +151,12 @@ const Timeline: React.FC<Props> = ({
renderItem={flRenderItem} renderItem={flRenderItem}
onEndReached={flOnEndReach} onEndReached={flOnEndReach}
keyExtractor={flKeyExtrator} keyExtractor={flKeyExtrator}
ListFooterComponent={flFooter}
scrollEnabled={scrollEnabled} // For timeline in Account view scrollEnabled={scrollEnabled} // For timeline in Account view
ListFooterComponent={flFooter}
ListEmptyComponent={flItemEmptyComponent}
ItemSeparatorComponent={flItemSeparatorComponent} ItemSeparatorComponent={flItemSeparatorComponent}
onEndReachedThreshold={!disableRefresh ? 0.75 : null} onEndReachedThreshold={!disableRefresh ? 0.75 : null}
refreshing={!disableRefresh && isLoading && flattenData.length > 0} refreshing={!disableRefresh && isLoading && flattenData.length > 0}
ListEmptyComponent={
<TimelineEmpty
isLoading={isLoading}
isError={isError}
refetch={refetch}
/>
}
{...(toot && isSuccess && { onScrollToIndexFailed })} {...(toot && isSuccess && { onScrollToIndexFailed })}
/> />
) )

View File

@ -16,15 +16,22 @@ import { StyleConstants } from 'src/utils/styles/constants'
export interface Props { export interface Props {
item: Mastodon.Status item: Mastodon.Status
queryKey: App.QueryKey queryKey: App.QueryKey
highlighted?: boolean
} }
// When the poll is long // When the poll is long
const TimelineDefault: React.FC<Props> = ({ item, queryKey }) => { const TimelineDefault: React.FC<Props> = ({
item,
queryKey,
highlighted = false
}) => {
const navigation = useNavigation() const navigation = useNavigation()
let actualStatus = item.reblog ? item.reblog : item let actualStatus = item.reblog ? item.reblog : item
const contentWidth = const contentWidth = highlighted
Dimensions.get('window').width - ? Dimensions.get('window').width -
StyleConstants.Spacing.Global.PagePadding * 2 // Global page padding on both sides
: Dimensions.get('window').width -
StyleConstants.Spacing.Global.PagePadding * 2 - // Global page padding on both sides StyleConstants.Spacing.Global.PagePadding * 2 - // Global page padding on both sides
StyleConstants.Avatar.S - // Avatar width StyleConstants.Avatar.S - // Avatar width
StyleConstants.Spacing.S // Avatar margin to the right StyleConstants.Spacing.S // Avatar margin to the right
@ -38,9 +45,15 @@ const TimelineDefault: React.FC<Props> = ({ item, queryKey }) => {
) )
const tootChildren = useMemo( const tootChildren = useMemo(
() => ( () => (
<> <View
style={{
paddingLeft: highlighted
? 0
: StyleConstants.Avatar.S + StyleConstants.Spacing.S
}}
>
{actualStatus.content.length > 0 && ( {actualStatus.content.length > 0 && (
<TimelineContent status={actualStatus} /> <TimelineContent status={actualStatus} highlighted={highlighted} />
)} )}
{actualStatus.poll && ( {actualStatus.poll && (
<TimelinePoll queryKey={queryKey} status={actualStatus} /> <TimelinePoll queryKey={queryKey} status={actualStatus} />
@ -49,7 +62,7 @@ const TimelineDefault: React.FC<Props> = ({ item, queryKey }) => {
<TimelineAttachment status={actualStatus} width={contentWidth} /> <TimelineAttachment status={actualStatus} width={contentWidth} />
)} )}
{actualStatus.card && <TimelineCard card={actualStatus.card} />} {actualStatus.card && <TimelineCard card={actualStatus.card} />}
</> </View>
), ),
[actualStatus.poll?.voted] [actualStatus.poll?.voted]
) )
@ -59,13 +72,19 @@ const TimelineDefault: React.FC<Props> = ({ item, queryKey }) => {
{item.reblog && ( {item.reblog && (
<TimelineActioned action='reblog' account={item.account} /> <TimelineActioned action='reblog' account={item.account} />
)} )}
<View style={styles.status}> <View style={styles.header}>
<TimelineAvatar account={actualStatus.account} /> <TimelineAvatar account={actualStatus.account} />
<View style={styles.details}>
<TimelineHeaderDefault queryKey={queryKey} status={actualStatus} /> <TimelineHeaderDefault queryKey={queryKey} status={actualStatus} />
<Pressable onPress={tootOnPress} children={tootChildren} />
<TimelineActions queryKey={queryKey} status={actualStatus} />
</View> </View>
<Pressable onPress={tootOnPress} children={tootChildren} />
<View
style={{
paddingLeft: highlighted
? 0
: StyleConstants.Avatar.S + StyleConstants.Spacing.S
}}
>
<TimelineActions queryKey={queryKey} status={actualStatus} />
</View> </View>
</View> </View>
) )
@ -73,17 +92,17 @@ const TimelineDefault: React.FC<Props> = ({ item, queryKey }) => {
const styles = StyleSheet.create({ const styles = StyleSheet.create({
statusView: { statusView: {
flex: 1,
flexDirection: 'column',
padding: StyleConstants.Spacing.Global.PagePadding, padding: StyleConstants.Spacing.Global.PagePadding,
paddingBottom: StyleConstants.Spacing.M paddingBottom: StyleConstants.Spacing.M
}, },
status: { header: {
flex: 1, flex: 1,
flexDirection: 'row' width: '100%',
flexDirection: 'row',
marginBottom: StyleConstants.Spacing.S
}, },
details: { content: {
flex: 1 paddingLeft: StyleConstants.Avatar.S + StyleConstants.Spacing.S
} }
}) })

View File

@ -4,20 +4,34 @@ import { StyleSheet, View } from 'react-native'
import { useTheme } from 'src/utils/styles/ThemeManager' import { useTheme } from 'src/utils/styles/ThemeManager'
import { StyleConstants } from 'src/utils/styles/constants' import { StyleConstants } from 'src/utils/styles/constants'
const TimelineSeparator = () => { export interface Props {
highlighted?: boolean
}
const TimelineSeparator: React.FC<Props> = ({ highlighted = false }) => {
const { theme } = useTheme() const { theme } = useTheme()
return <View style={[styles.base, { borderTopColor: theme.separator }]} /> return (
<View
style={[
styles.base,
{
borderTopColor: theme.separator,
marginLeft: highlighted
? StyleConstants.Spacing.Global.PagePadding
: StyleConstants.Spacing.Global.PagePadding +
StyleConstants.Avatar.S +
StyleConstants.Spacing.S
}
]}
/>
)
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
base: { base: {
borderTopWidth: 1, borderTopWidth: 1,
marginLeft: marginRight: StyleConstants.Spacing.Global.PagePadding
StyleConstants.Spacing.M +
StyleConstants.Avatar.S +
StyleConstants.Spacing.S,
marginRight: StyleConstants.Spacing.M
} }
}) })

View File

@ -25,7 +25,7 @@ const TimelineAvatar: React.FC<Props> = ({ account }) => {
const styles = StyleSheet.create({ const styles = StyleSheet.create({
avatar: { avatar: {
width: StyleConstants.Avatar.S, flexBasis: StyleConstants.Avatar.S,
height: StyleConstants.Avatar.S, height: StyleConstants.Avatar.S,
marginRight: StyleConstants.Spacing.S marginRight: StyleConstants.Spacing.S
}, },

View File

@ -11,9 +11,14 @@ import { LinearGradient } from 'expo-linear-gradient'
export interface Props { export interface Props {
status: Mastodon.Status status: Mastodon.Status
numberOfLines?: number numberOfLines?: number
highlighted?: boolean
} }
const TimelineContent: React.FC<Props> = ({ status, numberOfLines }) => { const TimelineContent: React.FC<Props> = ({
status,
numberOfLines,
highlighted = false
}) => {
const { theme } = useTheme() const { theme } = useTheme()
const [spoilerCollapsed, setSpoilerCollapsed] = useState(true) const [spoilerCollapsed, setSpoilerCollapsed] = useState(true)
const lineHeight = 28 const lineHeight = 28
@ -24,13 +29,13 @@ const TimelineContent: React.FC<Props> = ({ status, numberOfLines }) => {
<> <>
<ParseContent <ParseContent
content={status.spoiler_text} content={status.spoiler_text}
size={StyleConstants.Font.Size.M} size={highlighted ? 'L' : 'M'}
emojis={status.emojis} emojis={status.emojis}
/> />
<Collapsible collapsed={spoilerCollapsed} collapsedHeight={20}> <Collapsible collapsed={spoilerCollapsed} collapsedHeight={20}>
<ParseContent <ParseContent
content={status.content} content={status.content}
size={StyleConstants.Font.Size.M} size={highlighted ? 'L' : 'M'}
emojis={status.emojis} emojis={status.emojis}
mentions={status.mentions} mentions={status.mentions}
{...(numberOfLines && { numberOfLines: numberOfLines })} {...(numberOfLines && { numberOfLines: numberOfLines })}
@ -68,7 +73,7 @@ const TimelineContent: React.FC<Props> = ({ status, numberOfLines }) => {
) : ( ) : (
<ParseContent <ParseContent
content={status.content} content={status.content}
size={StyleConstants.Font.Size.M} size={highlighted ? 'L' : 'M'}
emojis={status.emojis} emojis={status.emojis}
mentions={status.mentions} mentions={status.mentions}
{...(numberOfLines && { numberOfLines: numberOfLines })} {...(numberOfLines && { numberOfLines: numberOfLines })}

View File

@ -1,14 +1,21 @@
import { Feather } from '@expo/vector-icons' import { Feather } from '@expo/vector-icons'
import React from 'react' import React from 'react'
import { StyleSheet, Text, View } from 'react-native' import { ActivityIndicator, StyleSheet, Text, View } from 'react-native'
import { StyleConstants } from 'src/utils/styles/constants' import { StyleConstants } from 'src/utils/styles/constants'
import { useTheme } from 'src/utils/styles/ThemeManager' import { useTheme } from 'src/utils/styles/ThemeManager'
const TimelineEnd: React.FC = () => { export interface Props {
isFetchingMore: false | 'previous' | 'next' | undefined
}
const TimelineEnd: React.FC<Props> = ({ isFetchingMore }) => {
const { theme } = useTheme() const { theme } = useTheme()
return ( return (
<View style={styles.base}> <View style={styles.base}>
{isFetchingMore ? (
<ActivityIndicator />
) : (
<Text style={[styles.text, { color: theme.secondary }]}> <Text style={[styles.text, { color: theme.secondary }]}>
{' '} {' '}
<Feather <Feather
@ -18,6 +25,7 @@ const TimelineEnd: React.FC = () => {
/>{' '} />{' '}
</Text> </Text>
)}
</View> </View>
) )
} }

View File

@ -60,7 +60,7 @@ const TimelineHeaderDefault: React.FC<Props> = ({ queryKey, status }) => {
) )
return ( return (
<View> <View style={styles.base}>
<View style={styles.nameAndAction}> <View style={styles.nameAndAction}>
<View style={styles.name}> <View style={styles.name}>
{emojis?.length ? ( {emojis?.length ? (
@ -156,13 +156,17 @@ const TimelineHeaderDefault: React.FC<Props> = ({ queryKey, status }) => {
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
base: {
flex: 1
},
nameAndAction: { nameAndAction: {
width: '100%', flex: 1,
flexBasis: '100%',
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between' alignItems: 'flex-start'
}, },
name: { name: {
flexBasis: '90%', flexBasis: '85%',
flexDirection: 'row' flexDirection: 'row'
}, },
nameWithoutEmoji: { nameWithoutEmoji: {
@ -170,7 +174,9 @@ const styles = StyleSheet.create({
fontWeight: StyleConstants.Font.Weight.Bold fontWeight: StyleConstants.Font.Weight.Bold
}, },
action: { action: {
alignItems: 'flex-end' flex: 1,
flexDirection: 'row',
justifyContent: 'center'
}, },
account: { account: {
flexShrink: 1, flexShrink: 1,

View File

@ -7,7 +7,7 @@ export const StyleConstants = {
M: 16, M: 16,
L: 18 L: 18
}, },
LineHeight: { M: 20 }, LineHeight: { M: 20, L: 24 },
Weight: { Weight: {
Bold: '600' as '600' Bold: '600' as '600'
} }