mirror of
https://github.com/tooot-app/app
synced 2025-06-05 22:19:13 +02:00
Some basic styling
This commit is contained in:
@ -2,6 +2,7 @@ import React from 'react'
|
||||
import { Pressable, StyleSheet, Text, View } from 'react-native'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import { Feather } from '@expo/vector-icons'
|
||||
import { useTheme } from 'src/utils/styles/ThemeManager'
|
||||
|
||||
export interface Props {
|
||||
icon?: string
|
||||
@ -11,6 +12,8 @@ export interface Props {
|
||||
}
|
||||
|
||||
const Core: React.FC<Props> = ({ icon, title, navigateTo }) => {
|
||||
const { theme } = useTheme()
|
||||
|
||||
return (
|
||||
<View style={styles.core}>
|
||||
{icon && <Feather name={icon} size={24} style={styles.iconLeading} />}
|
||||
@ -19,7 +22,7 @@ const Core: React.FC<Props> = ({ icon, title, navigateTo }) => {
|
||||
<Feather
|
||||
name='chevron-right'
|
||||
size={24}
|
||||
color='lightgray'
|
||||
color={theme.secondary}
|
||||
style={styles.iconNavigation}
|
||||
/>
|
||||
)}
|
||||
|
@ -1,18 +1,22 @@
|
||||
import React from 'react'
|
||||
import { StyleSheet, Text } from 'react-native'
|
||||
import { Text } from 'react-native'
|
||||
import HTMLView, { HTMLViewNode } from 'react-native-htmlview'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
|
||||
import Emojis from 'src/components/Status/Emojis'
|
||||
import Emojis from 'src/components/Timelines/Timeline/Shared/Emojis'
|
||||
import { useTheme } from 'src/utils/styles/ThemeManager'
|
||||
|
||||
// Prevent going to the same hashtag multiple times
|
||||
const renderNode = ({
|
||||
theme,
|
||||
node,
|
||||
index,
|
||||
navigation,
|
||||
mentions,
|
||||
showFullLink
|
||||
}: {
|
||||
node: HTMLViewNode
|
||||
theme: any
|
||||
node: any
|
||||
index: number
|
||||
navigation: any
|
||||
mentions?: Mastodon.Mention[]
|
||||
@ -26,10 +30,10 @@ const renderNode = ({
|
||||
return (
|
||||
<Text
|
||||
key={index}
|
||||
style={styles.a}
|
||||
style={{ color: theme.link }}
|
||||
onPress={() => {
|
||||
const tag = href.split(new RegExp(/\/tag\/(.*)|\/tags\/(.*)/))
|
||||
navigation.push('Hashtag', {
|
||||
navigation.push('Screen-Shared-Hashtag', {
|
||||
hashtag: tag[1] || tag[2]
|
||||
})
|
||||
}}
|
||||
@ -42,13 +46,13 @@ const renderNode = ({
|
||||
return (
|
||||
<Text
|
||||
key={index}
|
||||
style={styles.a}
|
||||
style={{ color: theme.link }}
|
||||
onPress={() => {
|
||||
const username = href.split(new RegExp(/@(.*)/))
|
||||
const usernameIndex = mentions.findIndex(
|
||||
m => m.username === username[1]
|
||||
)
|
||||
navigation.push('Account', {
|
||||
navigation.push('Screen-Shared-Account', {
|
||||
id: mentions[usernameIndex].id
|
||||
})
|
||||
}}
|
||||
@ -63,9 +67,9 @@ const renderNode = ({
|
||||
return (
|
||||
<Text
|
||||
key={index}
|
||||
style={styles.a}
|
||||
style={{ color: theme.link }}
|
||||
onPress={() => {
|
||||
navigation.navigate('Webview', {
|
||||
navigation.navigate('Screen-Shared-Webview', {
|
||||
uri: href,
|
||||
domain: domain[1]
|
||||
})
|
||||
@ -80,8 +84,8 @@ const renderNode = ({
|
||||
|
||||
export interface Props {
|
||||
content: string
|
||||
size: number
|
||||
emojis?: Mastodon.Emoji[]
|
||||
emojiSize?: number
|
||||
mentions?: Mastodon.Mention[]
|
||||
showFullLink?: boolean
|
||||
linesTruncated?: number
|
||||
@ -89,29 +93,24 @@ export interface Props {
|
||||
|
||||
const ParseContent: React.FC<Props> = ({
|
||||
content,
|
||||
size,
|
||||
emojis,
|
||||
emojiSize = 14,
|
||||
mentions,
|
||||
showFullLink = false,
|
||||
linesTruncated = 10
|
||||
}) => {
|
||||
const navigation = useNavigation()
|
||||
const { theme } = useTheme()
|
||||
|
||||
return (
|
||||
<HTMLView
|
||||
value={content}
|
||||
stylesheet={HTMLstyles}
|
||||
paragraphBreak=''
|
||||
renderNode={(node, index) =>
|
||||
renderNode({ node, index, navigation, mentions, showFullLink })
|
||||
renderNode({ theme, node, index, navigation, mentions, showFullLink })
|
||||
}
|
||||
TextComponent={({ children }) =>
|
||||
emojis && children ? (
|
||||
<Emojis
|
||||
content={children.toString()}
|
||||
emojis={emojis}
|
||||
dimension={emojiSize}
|
||||
/>
|
||||
<Emojis content={children.toString()} emojis={emojis} size={size} />
|
||||
) : (
|
||||
<Text>{children}</Text>
|
||||
)
|
||||
@ -123,16 +122,4 @@ const ParseContent: React.FC<Props> = ({
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
a: {
|
||||
color: 'blue'
|
||||
}
|
||||
})
|
||||
|
||||
const HTMLstyles = StyleSheet.create({
|
||||
p: {
|
||||
marginBottom: 12
|
||||
}
|
||||
})
|
||||
|
||||
export default ParseContent
|
||||
|
121
src/components/Timelines.tsx
Normal file
121
src/components/Timelines.tsx
Normal file
@ -0,0 +1,121 @@
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import { Dimensions, FlatList, Text, View } from 'react-native'
|
||||
import SegmentedControl from '@react-native-community/segmented-control'
|
||||
import { createNativeStackNavigator } from 'react-native-screens/native-stack'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { Feather } from '@expo/vector-icons'
|
||||
|
||||
import Timeline from './Timelines/Timeline'
|
||||
import sharedScreens from 'src/screens/Shared/sharedScreens'
|
||||
import { InstancesState } from 'src/utils/slices/instancesSlice'
|
||||
import { RootState } from 'src/store'
|
||||
import { useTheme } from 'src/utils/styles/ThemeManager'
|
||||
|
||||
const Stack = createNativeStackNavigator()
|
||||
|
||||
const Page = ({
|
||||
item: { page },
|
||||
localRegistered
|
||||
}: {
|
||||
item: { page: App.Pages }
|
||||
localRegistered: InstancesState['local']['url'] | undefined
|
||||
}) => {
|
||||
return (
|
||||
<View style={{ width: Dimensions.get('window').width }}>
|
||||
{localRegistered || page === 'RemotePublic' ? (
|
||||
<Timeline page={page} />
|
||||
) : (
|
||||
<Text>请先登录</Text>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export interface Props {
|
||||
name: string
|
||||
content: { title: string; page: App.Pages }[]
|
||||
}
|
||||
|
||||
const Timelines: React.FC<Props> = ({ name, content }) => {
|
||||
const { theme } = useTheme()
|
||||
const localRegistered = useSelector(
|
||||
(state: RootState) => state.instances.local.url
|
||||
)
|
||||
const [segment, setSegment] = useState(0)
|
||||
const [renderHeader, setRenderHeader] = useState(false)
|
||||
const [segmentManuallyTriggered, setSegmentManuallyTriggered] = useState(
|
||||
false
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const nbr = setTimeout(() => setRenderHeader(true), 50)
|
||||
return
|
||||
}, [])
|
||||
|
||||
const horizontalPaging = useRef<FlatList>(null!)
|
||||
|
||||
return (
|
||||
<Stack.Navigator>
|
||||
<Stack.Screen
|
||||
name={name}
|
||||
options={{
|
||||
headerRight: () =>
|
||||
renderHeader ? (
|
||||
<Feather name='search' size={24} color={theme.secondary} />
|
||||
) : null,
|
||||
headerCenter: () =>
|
||||
renderHeader ? (
|
||||
<SegmentedControl
|
||||
values={[content[0].title, content[1].title]}
|
||||
selectedIndex={segment}
|
||||
onChange={({ nativeEvent }) => {
|
||||
setSegmentManuallyTriggered(true)
|
||||
setSegment(nativeEvent.selectedSegmentIndex)
|
||||
horizontalPaging.current.scrollToIndex({
|
||||
index: nativeEvent.selectedSegmentIndex
|
||||
})
|
||||
}}
|
||||
style={{ width: 150, height: 30 }}
|
||||
/>
|
||||
) : null
|
||||
}}
|
||||
>
|
||||
{() => (
|
||||
<FlatList
|
||||
style={{ width: Dimensions.get('window').width, height: '100%' }}
|
||||
data={content}
|
||||
extraData={localRegistered}
|
||||
keyExtractor={({ page }) => page}
|
||||
renderItem={({ item, index }) => (
|
||||
<Page key={index} item={item} localRegistered={localRegistered} />
|
||||
)}
|
||||
ref={horizontalPaging}
|
||||
bounces={false}
|
||||
getItemLayout={(data, index) => ({
|
||||
length: Dimensions.get('window').width,
|
||||
offset: Dimensions.get('window').width * index,
|
||||
index
|
||||
})}
|
||||
horizontal
|
||||
onMomentumScrollEnd={() => setSegmentManuallyTriggered(false)}
|
||||
onScroll={({ nativeEvent }) =>
|
||||
!segmentManuallyTriggered &&
|
||||
setSegment(
|
||||
nativeEvent.contentOffset.x <=
|
||||
Dimensions.get('window').width / 2
|
||||
? 0
|
||||
: 1
|
||||
)
|
||||
}
|
||||
pagingEnabled
|
||||
showsHorizontalScrollIndicator={false}
|
||||
/>
|
||||
)}
|
||||
</Stack.Screen>
|
||||
|
||||
{sharedScreens(Stack)}
|
||||
</Stack.Navigator>
|
||||
)
|
||||
}
|
||||
|
||||
export default Timelines
|
119
src/components/Timelines/Timeline.tsx
Normal file
119
src/components/Timelines/Timeline.tsx
Normal file
@ -0,0 +1,119 @@
|
||||
import React from 'react'
|
||||
import { ActivityIndicator, AppState, FlatList, Text, View } from 'react-native'
|
||||
import { setFocusHandler, useInfiniteQuery } from 'react-query'
|
||||
|
||||
import TimelineNotifications from 'src/components/Timelines/Timeline/Notifications'
|
||||
import TimelineDefault from 'src/components/Timelines/Timeline/Default'
|
||||
import TimelineConversation from 'src/components/Timelines/Timeline/Conversation'
|
||||
import { timelineFetch } from 'src/utils/fetches/timelineFetch'
|
||||
import TimelineSeparator from './Timeline/Separator'
|
||||
|
||||
// Opening nesting hashtag pages
|
||||
|
||||
export interface Props {
|
||||
page: App.Pages
|
||||
hashtag?: string
|
||||
list?: string
|
||||
toot?: string
|
||||
account?: string
|
||||
disableRefresh?: boolean
|
||||
scrollEnabled?: boolean
|
||||
}
|
||||
|
||||
const Timeline: React.FC<Props> = ({
|
||||
page,
|
||||
hashtag,
|
||||
list,
|
||||
toot,
|
||||
account,
|
||||
disableRefresh = false,
|
||||
scrollEnabled = true
|
||||
}) => {
|
||||
setFocusHandler(handleFocus => {
|
||||
const handleAppStateChange = (appState: string) => {
|
||||
if (appState === 'active') {
|
||||
handleFocus()
|
||||
}
|
||||
}
|
||||
AppState.addEventListener('change', handleAppStateChange)
|
||||
return () => AppState.removeEventListener('change', handleAppStateChange)
|
||||
})
|
||||
|
||||
const queryKey: App.QueryKey = [page, { page, hashtag, list, toot, account }]
|
||||
const {
|
||||
isLoading,
|
||||
isFetchingMore,
|
||||
isError,
|
||||
isSuccess,
|
||||
data,
|
||||
fetchMore
|
||||
} = useInfiniteQuery(queryKey, timelineFetch)
|
||||
const flattenData = data ? data.flatMap(d => [...d?.toots]) : []
|
||||
// const flattenPointer = data ? data.flatMap(d => [d?.pointer]) : []
|
||||
|
||||
let content
|
||||
if (!isSuccess) {
|
||||
content = <ActivityIndicator />
|
||||
} else if (isError) {
|
||||
content = <Text>Error message</Text>
|
||||
} else {
|
||||
content = (
|
||||
<>
|
||||
<FlatList
|
||||
style={{ minHeight: '100%' }}
|
||||
scrollEnabled={scrollEnabled} // For timeline in Account view
|
||||
data={flattenData}
|
||||
keyExtractor={({ id }) => id}
|
||||
renderItem={({ item, index, separators }) => {
|
||||
switch (page) {
|
||||
case 'Conversations':
|
||||
return <TimelineConversation key={index} item={item} />
|
||||
case 'Notifications':
|
||||
return (
|
||||
<TimelineNotifications
|
||||
key={index}
|
||||
notification={item}
|
||||
queryKey={queryKey}
|
||||
/>
|
||||
)
|
||||
default:
|
||||
return (
|
||||
<TimelineDefault
|
||||
key={index}
|
||||
item={item}
|
||||
queryKey={queryKey}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}}
|
||||
ItemSeparatorComponent={() => <TimelineSeparator />}
|
||||
// require getItemLayout
|
||||
// {...(flattenPointer[0] && { initialScrollIndex: flattenPointer[0] })}
|
||||
{...(!disableRefresh && {
|
||||
onRefresh: () =>
|
||||
fetchMore(
|
||||
{
|
||||
direction: 'prev',
|
||||
id: flattenData[0].id
|
||||
},
|
||||
{ previous: true }
|
||||
),
|
||||
refreshing: isLoading,
|
||||
onEndReached: () => {
|
||||
fetchMore({
|
||||
direction: 'next',
|
||||
id: flattenData[flattenData.length - 1].id
|
||||
})
|
||||
},
|
||||
onEndReachedThreshold: 0.5
|
||||
})}
|
||||
/>
|
||||
{isFetchingMore && <ActivityIndicator />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return <View>{content}</View>
|
||||
}
|
||||
|
||||
export default Timeline
|
@ -2,9 +2,11 @@ import React, { useMemo } from 'react'
|
||||
import { Pressable, StyleSheet, View } from 'react-native'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
|
||||
import Avatar from './Status/Avatar'
|
||||
import HeaderConversation from './Status/HeaderConversation'
|
||||
import Content from './Status/Content'
|
||||
import Avatar from './Shared/Avatar'
|
||||
import HeaderConversation from './Shared/HeaderConversation'
|
||||
import Content from './Shared/Content'
|
||||
|
||||
import constants from 'src/utils/styles/constants'
|
||||
|
||||
export interface Props {
|
||||
item: Mastodon.Conversation
|
||||
@ -58,7 +60,7 @@ const styles = StyleSheet.create({
|
||||
statusView: {
|
||||
flex: 1,
|
||||
flexDirection: 'column',
|
||||
padding: 12
|
||||
padding: constants.GLOBAL_PAGE_PADDING
|
||||
},
|
||||
status: {
|
||||
flex: 1,
|
@ -2,20 +2,23 @@ import React, { useMemo } from 'react'
|
||||
import { Dimensions, Pressable, StyleSheet, View } from 'react-native'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
|
||||
import Actioned from './Status/Actioned'
|
||||
import Avatar from './Status/Avatar'
|
||||
import Header from './Status/Header'
|
||||
import Content from './Status/Content'
|
||||
import Poll from './Status/Poll'
|
||||
import Attachment from './Status/Attachment'
|
||||
import Card from './Status/Card'
|
||||
import ActionsStatus from './Status/ActionsStatus'
|
||||
import Actioned from './Shared/Actioned'
|
||||
import Avatar from './Shared/Avatar'
|
||||
import HeaderDefault from './Shared/HeaderDefault'
|
||||
import Content from './Shared/Content'
|
||||
import Poll from './Shared/Poll'
|
||||
import Attachment from './Shared/Attachment'
|
||||
import Card from './Shared/Card'
|
||||
import ActionsStatus from './Shared/ActionsStatus'
|
||||
|
||||
import constants from 'src/utils/styles/constants'
|
||||
|
||||
export interface Props {
|
||||
item: Mastodon.Status
|
||||
queryKey: App.QueryKey
|
||||
}
|
||||
|
||||
// When the poll is long
|
||||
const TimelineDefault: React.FC<Props> = ({ item, queryKey }) => {
|
||||
const navigation = useNavigation()
|
||||
|
||||
@ -37,7 +40,7 @@ const TimelineDefault: React.FC<Props> = ({ item, queryKey }) => {
|
||||
id={actualStatus.account.id}
|
||||
/>
|
||||
<View style={styles.details}>
|
||||
<Header
|
||||
<HeaderDefault
|
||||
queryKey={queryKey}
|
||||
accountId={actualStatus.account.id}
|
||||
domain={actualStatus.uri.split(new RegExp(/\/\/(.*?)\//))[1]}
|
||||
@ -75,7 +78,12 @@ const TimelineDefault: React.FC<Props> = ({ item, queryKey }) => {
|
||||
<Attachment
|
||||
media_attachments={actualStatus.media_attachments}
|
||||
sensitive={actualStatus.sensitive}
|
||||
width={Dimensions.get('window').width - 24 - 50 - 8}
|
||||
width={
|
||||
Dimensions.get('window').width -
|
||||
constants.SPACING_M * 2 -
|
||||
50 -
|
||||
8
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{actualStatus.card && <Card card={actualStatus.card} />}
|
||||
@ -94,7 +102,7 @@ const styles = StyleSheet.create({
|
||||
statusView: {
|
||||
flex: 1,
|
||||
flexDirection: 'column',
|
||||
padding: 12
|
||||
padding: constants.GLOBAL_PAGE_PADDING
|
||||
},
|
||||
status: {
|
||||
flex: 1,
|
@ -2,21 +2,23 @@ import React, { useMemo } from 'react'
|
||||
import { Dimensions, Pressable, StyleSheet, View } from 'react-native'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
|
||||
import Actioned from './Status/Actioned'
|
||||
import Avatar from './Status/Avatar'
|
||||
import Header from './Status/Header'
|
||||
import Content from './Status/Content'
|
||||
import Poll from './Status/Poll'
|
||||
import Attachment from './Status/Attachment'
|
||||
import Card from './Status/Card'
|
||||
import ActionsStatus from './Status/ActionsStatus'
|
||||
import Actioned from './Shared/Actioned'
|
||||
import Avatar from './Shared/Avatar'
|
||||
import HeaderDefault from './Shared/HeaderDefault'
|
||||
import Content from './Shared/Content'
|
||||
import Poll from './Shared/Poll'
|
||||
import Attachment from './Shared/Attachment'
|
||||
import Card from './Shared/Card'
|
||||
import ActionsStatus from './Shared/ActionsStatus'
|
||||
|
||||
import constants from 'src/utils/styles/constants'
|
||||
|
||||
export interface Props {
|
||||
notification: Mastodon.Notification
|
||||
queryKey: App.QueryKey
|
||||
}
|
||||
|
||||
const TootNotification: React.FC<Props> = ({ notification, queryKey }) => {
|
||||
const TimelineNotifications: React.FC<Props> = ({ notification, queryKey }) => {
|
||||
const navigation = useNavigation()
|
||||
const actualAccount = notification.status
|
||||
? notification.status.account
|
||||
@ -37,7 +39,7 @@ const TootNotification: React.FC<Props> = ({ notification, queryKey }) => {
|
||||
<View style={styles.notification}>
|
||||
<Avatar uri={actualAccount.avatar} id={actualAccount.id} />
|
||||
<View style={styles.details}>
|
||||
<Header
|
||||
<HeaderDefault
|
||||
name={actualAccount.display_name || actualAccount.username}
|
||||
emojis={actualAccount.emojis}
|
||||
account={actualAccount.acct}
|
||||
@ -96,7 +98,7 @@ const styles = StyleSheet.create({
|
||||
notificationView: {
|
||||
flex: 1,
|
||||
flexDirection: 'column',
|
||||
padding: 12
|
||||
padding: constants.GLOBAL_PAGE_PADDING
|
||||
},
|
||||
notification: {
|
||||
flex: 1,
|
||||
@ -108,4 +110,4 @@ const styles = StyleSheet.create({
|
||||
}
|
||||
})
|
||||
|
||||
export default TootNotification
|
||||
export default TimelineNotifications
|
20
src/components/Timelines/Timeline/Separator.tsx
Normal file
20
src/components/Timelines/Timeline/Separator.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import React from 'react'
|
||||
import { StyleSheet, View } from 'react-native'
|
||||
|
||||
import constants from 'src/utils/styles/constants'
|
||||
import { useTheme } from 'src/utils/styles/ThemeManager'
|
||||
|
||||
const TimelineSeparator = () => {
|
||||
const { theme } = useTheme()
|
||||
|
||||
return <View style={[styles.base, { borderTopColor: theme.separator }]} />
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
base: {
|
||||
borderTopWidth: 1,
|
||||
marginLeft: constants.SPACING_M + constants.AVATAR_S + constants.SPACING_S
|
||||
}
|
||||
})
|
||||
|
||||
export default TimelineSeparator
|
@ -3,6 +3,9 @@ import { StyleSheet, Text, View } from 'react-native'
|
||||
import { Feather } from '@expo/vector-icons'
|
||||
|
||||
import Emojis from './Emojis'
|
||||
import { useTheme } from 'src/utils/styles/ThemeManager'
|
||||
|
||||
import constants from 'src/utils/styles/constants'
|
||||
|
||||
export interface Props {
|
||||
action: 'favourite' | 'follow' | 'mention' | 'poll' | 'reblog'
|
||||
@ -17,18 +20,31 @@ const Actioned: React.FC<Props> = ({
|
||||
emojis,
|
||||
notification = false
|
||||
}) => {
|
||||
const { theme } = useTheme()
|
||||
const iconColor = theme.primary
|
||||
|
||||
let icon
|
||||
let content
|
||||
switch (action) {
|
||||
case 'favourite':
|
||||
icon = (
|
||||
<Feather name='heart' size={12} color='black' style={styles.icon} />
|
||||
<Feather
|
||||
name='heart'
|
||||
size={constants.FONT_SIZE_S}
|
||||
color={iconColor}
|
||||
style={styles.icon}
|
||||
/>
|
||||
)
|
||||
content = `${name} 喜欢了你的嘟嘟`
|
||||
break
|
||||
case 'follow':
|
||||
icon = (
|
||||
<Feather name='user-plus' size={12} color='black' style={styles.icon} />
|
||||
<Feather
|
||||
name='user-plus'
|
||||
size={constants.FONT_SIZE_S}
|
||||
color={iconColor}
|
||||
style={styles.icon}
|
||||
/>
|
||||
)
|
||||
content = `${name} 开始关注你`
|
||||
break
|
||||
@ -36,7 +52,7 @@ const Actioned: React.FC<Props> = ({
|
||||
icon = (
|
||||
<Feather
|
||||
name='bar-chart-2'
|
||||
size={12}
|
||||
size={constants.FONT_SIZE_S}
|
||||
color='black'
|
||||
style={styles.icon}
|
||||
/>
|
||||
@ -45,7 +61,12 @@ const Actioned: React.FC<Props> = ({
|
||||
break
|
||||
case 'reblog':
|
||||
icon = (
|
||||
<Feather name='repeat' size={12} color='black' style={styles.icon} />
|
||||
<Feather
|
||||
name='repeat'
|
||||
size={constants.FONT_SIZE_S}
|
||||
color={iconColor}
|
||||
style={styles.icon}
|
||||
/>
|
||||
)
|
||||
content = `${name} 转嘟了${notification ? '你的嘟嘟' : ''}`
|
||||
break
|
||||
@ -57,7 +78,11 @@ const Actioned: React.FC<Props> = ({
|
||||
{content ? (
|
||||
<View style={styles.content}>
|
||||
{emojis ? (
|
||||
<Emojis content={content} emojis={emojis} dimension={12} />
|
||||
<Emojis
|
||||
content={content}
|
||||
emojis={emojis}
|
||||
size={constants.FONT_SIZE_S}
|
||||
/>
|
||||
) : (
|
||||
<Text>{content}</Text>
|
||||
)}
|
||||
@ -72,11 +97,11 @@ const Actioned: React.FC<Props> = ({
|
||||
const styles = StyleSheet.create({
|
||||
actioned: {
|
||||
flexDirection: 'row',
|
||||
marginBottom: 8
|
||||
marginBottom: constants.SPACING_S
|
||||
},
|
||||
icon: {
|
||||
marginLeft: 50 - 12,
|
||||
marginRight: 8
|
||||
marginLeft: constants.AVATAR_S - constants.FONT_SIZE_S,
|
||||
marginRight: constants.SPACING_S
|
||||
},
|
||||
content: {
|
||||
flexDirection: 'row'
|
@ -14,7 +14,9 @@ import { Feather } from '@expo/vector-icons'
|
||||
|
||||
import client from 'src/api/client'
|
||||
import { getLocalAccountId } from 'src/utils/slices/instancesSlice'
|
||||
import {store} from 'src/store'
|
||||
import { store } from 'src/store'
|
||||
import { useTheme } from 'src/utils/styles/ThemeManager'
|
||||
import constants from 'src/utils/styles/constants'
|
||||
|
||||
const fireMutation = async ({
|
||||
id,
|
||||
@ -112,6 +114,11 @@ export interface Props {
|
||||
}
|
||||
|
||||
const ActionsStatus: React.FC<Props> = ({ queryKey, status }) => {
|
||||
const { theme } = useTheme()
|
||||
const iconColor = theme.secondary
|
||||
const iconColorAction = (state: boolean) =>
|
||||
state ? theme.primary : theme.secondary
|
||||
|
||||
const localAccountId = getLocalAccountId(store.getState())
|
||||
const [modalVisible, setModalVisible] = useState(false)
|
||||
|
||||
@ -153,8 +160,22 @@ const ActionsStatus: React.FC<Props> = ({ queryKey, status }) => {
|
||||
<>
|
||||
<View style={styles.actions}>
|
||||
<Pressable style={styles.action}>
|
||||
<Feather name='message-circle' color='gray' />
|
||||
{status.replies_count > 0 && <Text>{status.replies_count}</Text>}
|
||||
<Feather
|
||||
name='message-circle'
|
||||
color={iconColor}
|
||||
size={constants.FONT_SIZE_M + 2}
|
||||
/>
|
||||
{status.replies_count > 0 && (
|
||||
<Text
|
||||
style={{
|
||||
color: theme.secondary,
|
||||
fontSize: constants.FONT_SIZE_M,
|
||||
marginLeft: constants.SPACING_XS
|
||||
}}
|
||||
>
|
||||
{status.replies_count}
|
||||
</Text>
|
||||
)}
|
||||
</Pressable>
|
||||
|
||||
<Pressable
|
||||
@ -168,7 +189,11 @@ const ActionsStatus: React.FC<Props> = ({ queryKey, status }) => {
|
||||
})
|
||||
}
|
||||
>
|
||||
<Feather name='repeat' color={status.reblogged ? 'black' : 'gray'} />
|
||||
<Feather
|
||||
name='repeat'
|
||||
color={iconColorAction(status.reblogged)}
|
||||
size={constants.FONT_SIZE_M + 2}
|
||||
/>
|
||||
</Pressable>
|
||||
|
||||
<Pressable
|
||||
@ -182,7 +207,11 @@ const ActionsStatus: React.FC<Props> = ({ queryKey, status }) => {
|
||||
})
|
||||
}
|
||||
>
|
||||
<Feather name='heart' color={status.favourited ? 'black' : 'gray'} />
|
||||
<Feather
|
||||
name='heart'
|
||||
color={iconColorAction(status.favourited)}
|
||||
size={constants.FONT_SIZE_M + 2}
|
||||
/>
|
||||
</Pressable>
|
||||
|
||||
<Pressable
|
||||
@ -198,12 +227,17 @@ const ActionsStatus: React.FC<Props> = ({ queryKey, status }) => {
|
||||
>
|
||||
<Feather
|
||||
name='bookmark'
|
||||
color={status.bookmarked ? 'black' : 'gray'}
|
||||
color={iconColorAction(status.bookmarked)}
|
||||
size={constants.FONT_SIZE_M + 2}
|
||||
/>
|
||||
</Pressable>
|
||||
|
||||
<Pressable style={styles.action} onPress={() => setModalVisible(true)}>
|
||||
<Feather name='more-horizontal' color='gray' />
|
||||
<Feather
|
||||
name='share-2'
|
||||
color={iconColor}
|
||||
size={constants.FONT_SIZE_M + 2}
|
||||
/>
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
@ -319,14 +353,12 @@ const styles = StyleSheet.create({
|
||||
width: '100%',
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
marginTop: 8
|
||||
marginTop: constants.SPACING_M
|
||||
},
|
||||
action: {
|
||||
width: '20%',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
paddingTop: 8,
|
||||
paddingBottom: 8
|
||||
justifyContent: 'center'
|
||||
},
|
||||
modalBackground: {
|
||||
width: '100%',
|
@ -2,6 +2,8 @@ import React from 'react'
|
||||
import { Image, Pressable, StyleSheet } from 'react-native'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
|
||||
import constants from 'src/utils/styles/constants'
|
||||
|
||||
export interface Props {
|
||||
uri: string
|
||||
id: string
|
||||
@ -26,13 +28,14 @@ const Avatar: React.FC<Props> = ({ uri, id }) => {
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
avatar: {
|
||||
width: 50,
|
||||
height: 50,
|
||||
marginRight: 8
|
||||
width: constants.AVATAR_S,
|
||||
height: constants.AVATAR_S,
|
||||
marginRight: constants.SPACING_S
|
||||
},
|
||||
image: {
|
||||
width: '100%',
|
||||
height: '100%'
|
||||
height: '100%',
|
||||
borderRadius: 8
|
||||
}
|
||||
})
|
||||
|
@ -4,6 +4,9 @@ import Collapsible from 'react-native-collapsible'
|
||||
|
||||
import ParseContent from 'src/components/ParseContent'
|
||||
|
||||
import constants from 'src/utils/styles/constants'
|
||||
import { useTheme } from 'src/utils/styles/ThemeManager'
|
||||
|
||||
export interface Props {
|
||||
content: string
|
||||
emojis: Mastodon.Emoji[]
|
||||
@ -17,6 +20,7 @@ const Content: React.FC<Props> = ({
|
||||
mentions,
|
||||
spoiler_text
|
||||
}) => {
|
||||
const { theme } = useTheme()
|
||||
const [spoilerCollapsed, setSpoilerCollapsed] = useState(true)
|
||||
|
||||
return (
|
||||
@ -26,15 +30,18 @@ const Content: React.FC<Props> = ({
|
||||
<>
|
||||
<Text>
|
||||
{spoiler_text}{' '}
|
||||
<Text onPress={() => setSpoilerCollapsed(!spoilerCollapsed)}>
|
||||
点击展开
|
||||
<Text
|
||||
onPress={() => setSpoilerCollapsed(!spoilerCollapsed)}
|
||||
style={{ color: theme.link }}
|
||||
>
|
||||
{spoilerCollapsed ? '点击展开' : '点击收起'}
|
||||
</Text>
|
||||
</Text>
|
||||
<Collapsible collapsed={spoilerCollapsed}>
|
||||
<ParseContent
|
||||
content={content}
|
||||
size={constants.FONT_SIZE_M}
|
||||
emojis={emojis}
|
||||
emojiSize={14}
|
||||
mentions={mentions}
|
||||
/>
|
||||
</Collapsible>
|
||||
@ -42,8 +49,8 @@ const Content: React.FC<Props> = ({
|
||||
) : (
|
||||
<ParseContent
|
||||
content={content}
|
||||
size={constants.FONT_SIZE_M}
|
||||
emojis={emojis}
|
||||
emojiSize={14}
|
||||
mentions={mentions}
|
||||
/>
|
||||
))}
|
@ -1,16 +1,37 @@
|
||||
import React from 'react'
|
||||
import { Image, Text } from 'react-native'
|
||||
import { Image, StyleSheet, Text } from 'react-native'
|
||||
import { useTheme } from 'src/utils/styles/ThemeManager'
|
||||
|
||||
const regexEmoji = new RegExp(/(:[a-z0-9_]+:)/)
|
||||
|
||||
export interface Props {
|
||||
content: string
|
||||
emojis: Mastodon.Emoji[]
|
||||
dimension: number
|
||||
size: number
|
||||
fontBold?: boolean
|
||||
}
|
||||
|
||||
const Emojis: React.FC<Props> = ({ content, emojis, dimension }) => {
|
||||
const Emojis: React.FC<Props> = ({
|
||||
content,
|
||||
emojis,
|
||||
size,
|
||||
fontBold = false
|
||||
}) => {
|
||||
const { theme } = useTheme()
|
||||
const styles = StyleSheet.create({
|
||||
text: {
|
||||
fontSize: size,
|
||||
lineHeight: size + 2,
|
||||
color: theme.primary,
|
||||
...(fontBold && { fontWeight: 'bold' })
|
||||
},
|
||||
image: {
|
||||
width: size,
|
||||
height: size
|
||||
}
|
||||
})
|
||||
const hasEmojis = content.match(regexEmoji)
|
||||
|
||||
return hasEmojis ? (
|
||||
<>
|
||||
{content.split(regexEmoji).map((str, i) => {
|
||||
@ -20,20 +41,19 @@ const Emojis: React.FC<Props> = ({ content, emojis, dimension }) => {
|
||||
return emojiShortcode === `:${emoji.shortcode}:`
|
||||
})
|
||||
return emojiIndex === -1 ? (
|
||||
<Text key={i}>{emojiShortcode}</Text>
|
||||
<Text key={i} style={styles.text}>
|
||||
{emojiShortcode}
|
||||
</Text>
|
||||
) : (
|
||||
<Image
|
||||
key={i}
|
||||
source={{ uri: emojis[emojiIndex].url }}
|
||||
style={{ width: dimension, height: dimension }}
|
||||
style={styles.image}
|
||||
/>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<Text
|
||||
key={i}
|
||||
style={{ fontSize: dimension, lineHeight: dimension + 1 }}
|
||||
>
|
||||
<Text key={i} style={styles.text}>
|
||||
{str}
|
||||
</Text>
|
||||
)
|
||||
@ -41,9 +61,7 @@ const Emojis: React.FC<Props> = ({ content, emojis, dimension }) => {
|
||||
})}
|
||||
</>
|
||||
) : (
|
||||
<Text style={{ fontSize: dimension, lineHeight: dimension + 1 }}>
|
||||
{content}
|
||||
</Text>
|
||||
<Text style={styles.text}>{content}</Text>
|
||||
)
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ const HeaderConversation: React.FC<Props> = ({ account, created_at }) => {
|
||||
<Emojis
|
||||
content={account.display_name || account.username}
|
||||
emojis={account.emojis}
|
||||
dimension={14}
|
||||
size={14}
|
||||
/>
|
||||
) : (
|
||||
<Text numberOfLines={1}>
|
@ -8,12 +8,10 @@ import { useMutation, useQueryCache } from 'react-query'
|
||||
import Emojis from './Emojis'
|
||||
import relativeTime from 'src/utils/relativeTime'
|
||||
import client from 'src/api/client'
|
||||
import { useSelector } from 'react-redux'
|
||||
import {
|
||||
getLocalAccountId,
|
||||
getLocalUrl
|
||||
} from 'src/utils/slices/instancesSlice'
|
||||
import {store} from 'src/store'
|
||||
import { getLocalAccountId, getLocalUrl } from 'src/utils/slices/instancesSlice'
|
||||
import { store } from 'src/store'
|
||||
import { useTheme } from 'src/utils/styles/ThemeManager'
|
||||
import constants from 'src/utils/styles/constants'
|
||||
|
||||
const fireMutation = async ({
|
||||
id,
|
||||
@ -131,7 +129,7 @@ export interface Props {
|
||||
application?: Mastodon.Application
|
||||
}
|
||||
|
||||
const Header: React.FC<Props> = ({
|
||||
const HeaderDefault: React.FC<Props> = ({
|
||||
queryKey,
|
||||
accountId,
|
||||
domain,
|
||||
@ -141,6 +139,8 @@ const Header: React.FC<Props> = ({
|
||||
created_at,
|
||||
application
|
||||
}) => {
|
||||
const { theme } = useTheme()
|
||||
|
||||
const navigation = useNavigation()
|
||||
const localAccountId = getLocalAccountId(store.getState())
|
||||
const localDomain = getLocalUrl(store.getState())
|
||||
@ -194,26 +194,42 @@ const Header: React.FC<Props> = ({
|
||||
<View style={styles.nameAndAction}>
|
||||
<View style={styles.name}>
|
||||
{emojis ? (
|
||||
<Emojis content={name} emojis={emojis} dimension={14} />
|
||||
<Emojis
|
||||
content={name}
|
||||
emojis={emojis}
|
||||
size={constants.FONT_SIZE_M}
|
||||
fontBold={true}
|
||||
/>
|
||||
) : (
|
||||
<Text numberOfLines={1}>{name}</Text>
|
||||
<Text numberOfLines={1} style={{ color: theme.primary }}>
|
||||
{name}
|
||||
</Text>
|
||||
)}
|
||||
<Text
|
||||
style={[styles.account, { color: theme.secondary }]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
@{account}
|
||||
</Text>
|
||||
</View>
|
||||
{accountId !== localAccountId && domain !== localDomain && (
|
||||
<Pressable
|
||||
style={styles.action}
|
||||
onPress={() => setModalVisible(true)}
|
||||
>
|
||||
<Feather name='more-horizontal' color='gray' />
|
||||
<Feather
|
||||
name='more-horizontal'
|
||||
color={theme.secondary}
|
||||
size={constants.FONT_SIZE_M + 2}
|
||||
/>
|
||||
</Pressable>
|
||||
)}
|
||||
</View>
|
||||
<Text style={styles.account} numberOfLines={1}>
|
||||
@{account}
|
||||
</Text>
|
||||
<View style={styles.meta}>
|
||||
<View>
|
||||
<Text style={styles.created_at}>{since}</Text>
|
||||
<Text style={[styles.created_at, { color: theme.secondary }]}>
|
||||
{since}
|
||||
</Text>
|
||||
</View>
|
||||
{application && application.name !== 'Web' && (
|
||||
<View>
|
||||
@ -223,9 +239,9 @@ const Header: React.FC<Props> = ({
|
||||
uri: application.website
|
||||
})
|
||||
}}
|
||||
style={styles.application}
|
||||
style={[styles.application, { color: theme.secondary }]}
|
||||
>
|
||||
{application.name}
|
||||
发自于 - {application.name}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
@ -310,32 +326,29 @@ const styles = StyleSheet.create({
|
||||
justifyContent: 'space-between'
|
||||
},
|
||||
name: {
|
||||
flexDirection: 'row',
|
||||
marginRight: 8,
|
||||
fontWeight: '900'
|
||||
},
|
||||
action: {
|
||||
width: 14,
|
||||
height: 14,
|
||||
marginLeft: 8
|
||||
},
|
||||
account: {
|
||||
lineHeight: 14,
|
||||
flexShrink: 1
|
||||
},
|
||||
meta: {
|
||||
flexBasis: '80%',
|
||||
flexDirection: 'row'
|
||||
},
|
||||
action: {
|
||||
flexBasis: '20%',
|
||||
alignItems: 'center'
|
||||
},
|
||||
account: {
|
||||
flexShrink: 1,
|
||||
marginLeft: constants.SPACING_XS,
|
||||
lineHeight: constants.FONT_SIZE_M + 2
|
||||
},
|
||||
meta: {
|
||||
flexDirection: 'row',
|
||||
marginTop: constants.SPACING_XS,
|
||||
marginBottom: constants.SPACING_S
|
||||
},
|
||||
created_at: {
|
||||
fontSize: 12,
|
||||
lineHeight: 12,
|
||||
marginTop: 8,
|
||||
marginBottom: 8,
|
||||
marginRight: 8
|
||||
fontSize: constants.FONT_SIZE_S
|
||||
},
|
||||
application: {
|
||||
fontSize: 12,
|
||||
lineHeight: 11
|
||||
fontSize: constants.FONT_SIZE_S,
|
||||
marginLeft: constants.SPACING_S
|
||||
},
|
||||
modalBackground: {
|
||||
width: '100%',
|
||||
@ -354,4 +367,4 @@ const styles = StyleSheet.create({
|
||||
}
|
||||
})
|
||||
|
||||
export default Header
|
||||
export default HeaderDefault
|
@ -19,7 +19,7 @@ const Poll: React.FC<Props> = ({ poll }) => {
|
||||
<Emojis
|
||||
content={option.title}
|
||||
emojis={poll.emojis}
|
||||
dimension={14}
|
||||
size={14}
|
||||
/>
|
||||
</View>
|
||||
<View
|
Reference in New Issue
Block a user