Some basic styling

This commit is contained in:
Zhiyuan Zheng 2020-11-23 00:07:32 +01:00
parent 6d6b808af2
commit fba1d0d531
No known key found for this signature in database
GPG Key ID: 078A93AB607D85E0
40 changed files with 1381 additions and 270 deletions

22
App.tsx
View File

@ -1,7 +1,9 @@
import React from 'react'
import { AppearanceProvider } from 'react-native-appearance'
import { QueryCache, ReactQueryCacheProvider, setConsole } from 'react-query'
import { Provider } from 'react-redux'
import ThemeManager from 'src/utils/styles/ThemeManager'
import { Index } from 'src/Index'
import { store } from 'src/store'
@ -18,12 +20,18 @@ if (__DEV__) {
// whyDidYouRender(React)
}
const App: React.FC = () => (
<ReactQueryCacheProvider queryCache={queryCache}>
<Provider store={store}>
<Index />
</Provider>
</ReactQueryCacheProvider>
)
const App: React.FC = () => {
return (
<AppearanceProvider>
<ThemeManager>
<ReactQueryCacheProvider queryCache={queryCache}>
<Provider store={store}>
<Index />
</Provider>
</ReactQueryCacheProvider>
</ThemeManager>
</AppearanceProvider>
)
}
export default App

23
app.config.ts Normal file
View File

@ -0,0 +1,23 @@
import { ExpoConfig } from '@expo/config'
export default (): ExpoConfig => ({
name: 'mastodon-app',
description: 'This is a description',
slug: 'mastodon-app',
privacy: 'hidden',
version: '1.0.0',
platforms: ['ios'],
orientation: 'portrait',
userInterfaceStyle: 'automatic',
icon: './assets/icon.png',
developmentClient: { silentLaunch: true },
scheme: 'mastodonct',
ios: {
splash: {
image: './assets/splash.png',
resizeMode: 'contain',
backgroundColor: '#ffffff'
}
},
assetBundlePatterns: ['**/*']
})

View File

@ -1,24 +0,0 @@
{
"expo": {
"name": "mastodon-app",
"slug": "mastodon-app",
"scheme": "mastodonct",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/icon.png",
"splash": {
"image": "./assets/splash.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
"updates": {
"fallbackToCacheTimeout": 0
},
"assetBundlePatterns": [
"**/*"
],
"ios": {
"supportsTablet": false
}
}
}

View File

@ -9,6 +9,7 @@
},
"dependencies": {
"@expo/vector-icons": "^10.0.0",
"@react-native-community/masked-view": "0.1.10",
"@react-native-community/segmented-control": "2.1.1",
"@react-navigation/bottom-tabs": "^5.10.6",
"@react-navigation/native": "^5.8.6",
@ -25,6 +26,7 @@
"react": "16.13.1",
"react-dom": "16.13.1",
"react-native": "https://github.com/expo/react-native/archive/sdk-39.0.3.tar.gz",
"react-native-appearance": "~0.3.3",
"react-native-collapsible": "^1.5.3",
"react-native-gesture-handler": "~1.7.0",
"react-native-htmlview": "^0.16.0",
@ -45,6 +47,7 @@
"devDependencies": {
"@babel/core": "~7.12.3",
"@babel/plugin-proposal-optional-chaining": "^7.12.1",
"@expo/config": "^3.3.15",
"@types/lodash": "^4.14.164",
"@types/node": "^14.14.7",
"@types/react": "~16.9.35",
@ -58,4 +61,4 @@
"typescript": "~3.9.2"
},
"private": true
}
}

View File

@ -14,12 +14,17 @@ import ScreenPublic from 'src/screens/Public'
import ScreenNotifications from 'src/screens/Notifications'
import ScreenMe from 'src/screens/Me'
import { themes } from 'src/utils/styles/themes'
import { useTheme } from 'src/utils/styles/ThemeManager'
enableScreens()
const Tab = createBottomTabNavigator()
export const Index: React.FC = () => {
const { mode, theme } = useTheme()
return (
<NavigationContainer>
<NavigationContainer theme={themes[mode]}>
<Tab.Navigator
screenOptions={({ route }) => ({
tabBarIcon: ({ focused, color, size }) => {
@ -48,8 +53,8 @@ export const Index: React.FC = () => {
}
})}
tabBarOptions={{
activeTintColor: 'black',
inactiveTintColor: 'gray',
activeTintColor: theme.primary,
inactiveTintColor: theme.secondary,
showLabel: false
}}
>

View File

@ -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}
/>
)}

View File

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

View File

@ -5,10 +5,11 @@ import { createNativeStackNavigator } from 'react-native-screens/native-stack'
import { useSelector } from 'react-redux'
import { Feather } from '@expo/vector-icons'
import Timeline from './Timeline'
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()
@ -36,6 +37,7 @@ export interface Props {
}
const Timelines: React.FC<Props> = ({ name, content }) => {
const { theme } = useTheme()
const localRegistered = useSelector(
(state: RootState) => state.instances.local.url
)
@ -59,7 +61,7 @@ const Timelines: React.FC<Props> = ({ name, content }) => {
options={{
headerRight: () =>
renderHeader ? (
<Feather name='search' size={24} color='black' />
<Feather name='search' size={24} color={theme.secondary} />
) : null,
headerCenter: () =>
renderHeader ? (

View File

@ -2,10 +2,11 @@ import React from 'react'
import { ActivityIndicator, AppState, FlatList, Text, View } from 'react-native'
import { setFocusHandler, useInfiniteQuery } from 'react-query'
import StatusInNotifications from 'src/components/StatusInNotifications'
import TimelineDefault from 'src/components/TimelineDefault'
import TimelineConversation from 'src/components/TimelineConversation'
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
@ -69,7 +70,7 @@ const Timeline: React.FC<Props> = ({
return <TimelineConversation key={index} item={item} />
case 'Notifications':
return (
<StatusInNotifications
<TimelineNotifications
key={index}
notification={item}
queryKey={queryKey}
@ -85,6 +86,7 @@ const Timeline: React.FC<Props> = ({
)
}
}}
ItemSeparatorComponent={() => <TimelineSeparator />}
// require getItemLayout
// {...(flattenPointer[0] && { initialScrollIndex: flattenPointer[0] })}
{...(!disableRefresh && {

View File

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

View File

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

View File

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

View 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

View File

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

View File

@ -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%',

View File

@ -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
}
})

View File

@ -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}
/>
))}

View File

@ -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>
)
}

View File

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

View File

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

View File

@ -19,7 +19,7 @@ const Poll: React.FC<Props> = ({ poll }) => {
<Emojis
content={option.title}
emojis={poll.emojis}
dimension={14}
size={14}
/>
</View>
<View

View File

@ -1,6 +1,6 @@
import React from 'react'
import Timelines from 'src/screens/Timelines/Timelines'
import Timelines from 'src/components/Timelines'
const ScreenLocal: React.FC = () => {
return (

View File

@ -1,6 +1,6 @@
import React from 'react'
import Timeline from 'src/screens/Timelines/Timeline'
import Timeline from 'src/components/Timelines/Timeline'
const ScreenMeBookmarks: React.FC = () => {
return <Timeline page='Bookmarks' />

View File

@ -1,6 +1,6 @@
import React from 'react'
import Timeline from 'src/screens/Timelines/Timeline'
import Timeline from 'src/components/Timelines/Timeline'
const ScreenMeConversations: React.FC = () => {
return <Timeline page='Conversations' />

View File

@ -1,6 +1,6 @@
import React from 'react'
import Timeline from 'src/screens/Timelines/Timeline'
import Timeline from 'src/components/Timelines/Timeline'
const ScreenMeFavourites: React.FC = () => {
return <Timeline page='Favourites' />

View File

@ -1,6 +1,6 @@
import React from 'react'
import Timeline from 'src/screens/Timelines/Timeline'
import Timeline from 'src/components/Timelines/Timeline'
// Show remote hashtag? Only when private, show local version?

View File

@ -2,12 +2,15 @@ import React, { useEffect, useState } from 'react'
import { createNativeStackNavigator } from 'react-native-screens/native-stack'
import { Feather } from '@expo/vector-icons'
import Timeline from 'src/screens/Timelines/Timeline'
import Timeline from 'src/components/Timelines/Timeline'
import sharedScreens from 'src/screens/Shared/sharedScreens'
import { useTheme } from 'src/utils/styles/ThemeManager'
const Stack = createNativeStackNavigator()
const ScreenNotifications: React.FC = () => {
const { theme } = useTheme()
const [renderHeader, setRenderHeader] = useState(false)
useEffect(() => {
@ -20,9 +23,10 @@ const ScreenNotifications: React.FC = () => {
screenOptions={{
headerRight: () =>
renderHeader ? (
<Feather name='search' size={24} color='black' />
<Feather name='search' size={24} color={theme.secondary} />
) : null,
headerTitle: '通知'
headerTitle: '通知',
headerLargeTitle: true
}}
>
<Stack.Screen name='Notifications'>

View File

@ -1,6 +1,6 @@
import React from 'react'
import Timelines from 'src/screens/Timelines/Timelines'
import Timelines from 'src/components/Timelines'
const ScreenPublic: React.FC = () => {
return (

View File

@ -4,12 +4,16 @@ import ShimmerPlaceholder from 'react-native-shimmer-placeholder'
import { Feather } from '@expo/vector-icons'
import ParseContent from 'src/components/ParseContent'
import { useTheme } from 'src/utils/styles/ThemeManager'
import constants from 'src/utils/styles/constants'
export interface Props {
account: Mastodon.Account | undefined
}
const AccountInformation: React.FC<Props> = ({ account }) => {
const { theme } = useTheme()
const [avatarLoaded, setAvatarLoaded] = useState(false)
// add emoji support
@ -24,71 +28,128 @@ const AccountInformation: React.FC<Props> = ({ account }) => {
/>
</ShimmerPlaceholder>
<Text style={styles.display_name}>
<Text style={[styles.display_name, { color: theme.primary }]}>
{account?.display_name || account?.username}
{account?.bot && (
<Feather name='hard-drive' style={styles.display_name} />
)}
</Text>
<Text style={styles.account}>
{account?.acct}
<Text style={[styles.account, { color: theme.secondary }]}>
@{account?.acct}
{account?.locked && <Feather name='lock' />}
</Text>
{account?.fields &&
account.fields.map((field, index) => (
<View key={index} style={{ flex: 1, flexDirection: 'row' }}>
<Text style={{ width: '30%', alignSelf: 'center' }}>
<ParseContent
content={field.name}
emojis={account.emojis}
showFullLink
/>{' '}
{field.verified_at && <Feather name='check-circle' />}
</Text>
<Text style={{ width: '70%' }}>
<ParseContent
content={field.value}
emojis={account.emojis}
showFullLink
/>
</Text>
</View>
))}
{account?.note && (
<ParseContent content={account.note} emojis={account.emojis} />
)}
{account?.created_at && (
<Text>
{' '}
{new Date(account.created_at).toLocaleDateString('zh-CN', {
year: 'numeric',
month: 'long',
day: 'numeric'
})}
</Text>
{account?.fields && (
<View style={styles.fields}>
{account.fields.map((field, index) => (
<View key={index} style={{ flex: 1, flexDirection: 'row' }}>
<Text
style={{
width: '30%',
alignSelf: 'center',
color: theme.primary
}}
>
<ParseContent
content={field.name}
size={constants.FONT_SIZE_M}
emojis={account.emojis}
showFullLink
/>{' '}
{field.verified_at && <Feather name='check-circle' />}
</Text>
<Text style={{ width: '70%', color: theme.primary }}>
<ParseContent
content={field.value}
size={constants.FONT_SIZE_M}
emojis={account.emojis}
showFullLink
/>
</Text>
</View>
))}
</View>
)}
<Text>Toots: {account?.statuses_count}</Text>
<Text>Followers: {account?.followers_count}</Text>
<Text>Following: {account?.following_count}</Text>
{account?.note && (
<View style={styles.note}>
<ParseContent
content={account.note}
size={constants.FONT_SIZE_M}
emojis={account.emojis}
/>
</View>
)}
{account?.created_at && (
<View style={styles.created_at}>
<Feather name='calendar' size={constants.FONT_SIZE_M + 2} />
<Text
style={{
color: theme.primary,
fontSize: constants.FONT_SIZE_M,
marginLeft: constants.SPACING_XS
}}
>
{new Date(account.created_at).toLocaleDateString('zh-CN', {
year: 'numeric',
month: 'long',
day: 'numeric'
})}
</Text>
</View>
)}
<View style={styles.summary}>
<Text style={{ color: theme.primary }}>
{account?.statuses_count}
</Text>
<Text style={{ color: theme.primary }}>
{account?.followers_count}
</Text>
<Text style={{ color: theme.primary }}>
{account?.following_count}
</Text>
</View>
</View>
)
}
const styles = StyleSheet.create({
information: { marginTop: -30, paddingLeft: 12, paddingRight: 12 },
information: {
marginTop: -30 - constants.GLOBAL_PAGE_PADDING,
padding: constants.GLOBAL_PAGE_PADDING
},
avatar: {
width: 90,
height: 90
width: constants.AVATAR_L,
height: constants.AVATAR_L,
borderRadius: 8
},
display_name: {
fontSize: 18,
fontSize: constants.FONT_SIZE_L,
fontWeight: 'bold',
marginTop: 12
marginTop: constants.SPACING_M,
marginBottom: constants.SPACING_XS
},
account: {
marginTop: 4
fontSize: constants.FONT_SIZE_M,
marginBottom: constants.SPACING_S
},
fields: {
marginBottom: constants.SPACING_S
},
note: {
marginBottom: constants.SPACING_M
},
created_at: {
flexDirection: 'row',
marginBottom: constants.SPACING_M
},
summary: {
flexDirection: 'row',
justifyContent: 'space-between'
}
})

View File

@ -2,7 +2,7 @@ import React, { useRef, useState } from 'react'
import { Dimensions, FlatList, View } from 'react-native'
import SegmentedControl from '@react-native-community/segmented-control'
import Timeline from 'src/screens/Timelines/Timeline'
import Timeline from 'src/components/Timelines/Timeline'
export interface Props {
id: Mastodon.Account['id']

View File

@ -1,6 +1,6 @@
import React from 'react'
import Timeline from 'src/screens/Timelines/Timeline'
import Timeline from 'src/components/Timelines/Timeline'
// Show remote hashtag? Only when private, show local version?

View File

@ -1,6 +1,6 @@
import React from 'react'
import Timeline from 'src/screens/Timelines/Timeline'
import Timeline from 'src/components/Timelines/Timeline'
// Show remote hashtag? Only when private, show local version?

View File

@ -0,0 +1,44 @@
import React, { createContext, useContext, useEffect, useState } from 'react'
import { Appearance, useColorScheme } from 'react-native-appearance'
import { ColorDefinitions, getTheme } from 'src/utils/styles/themes'
const osTheme = Appearance.getColorScheme() as 'light' | 'dark'
export const ManageThemeContext: React.Context<{
mode: 'light' | 'dark'
theme: { [key in ColorDefinitions]: string }
toggle: () => void
}> = createContext({
mode: osTheme,
theme: getTheme(osTheme),
toggle: () => {}
})
export const useTheme = () => useContext(ManageThemeContext)
const ThemeManager: React.FC = ({ children }) => {
const [mode, setMode] = useState(osTheme)
const systemTheme = useColorScheme()
const toggleTheme = () => {
mode === 'light' ? setMode('dark') : setMode('light')
}
useEffect(() => {
setMode(systemTheme === 'no-preference' ? 'light' : systemTheme)
}, [systemTheme])
return (
<ManageThemeContext.Provider
value={{
mode: mode,
theme: getTheme(mode),
toggle: toggleTheme
}}
>
{children}
</ManageThemeContext.Provider>
)
}
export default ThemeManager

View File

@ -0,0 +1,16 @@
export default {
FONT_SIZE_S: 12,
FONT_SIZE_M: 14,
FONT_SIZE_L: 18,
SPACING_XS: 4,
SPACING_S: 8,
SPACING_M: 16,
SPACING_L: 24,
SPACING_XL: 40,
GLOBAL_PAGE_PADDING: 16, // SPACING_M
AVATAR_S: 52,
AVATAR_L: 104
}

View File

@ -0,0 +1,80 @@
import { DefaultTheme, DarkTheme } from '@react-navigation/native'
export type ColorDefinitions =
| 'primary'
| 'secondary'
| 'background'
| 'link'
| 'border'
| 'separator'
const themeColors: {
[key in ColorDefinitions]: {
light: string
dark: string
}
} = {
primary: {
light: 'rgb(0, 0, 0)',
dark: 'rgb(255, 255, 255)'
},
secondary: {
light: 'rgb(153, 153, 153)',
dark: 'rgb(117, 117, 117)'
},
background: {
light: 'rgb(255, 255, 255)',
dark: 'rgb(0, 0, 0)'
},
link: {
light: 'rgb(0, 122, 255)',
dark: 'rgb(10, 132, 255)'
},
border: {
light: 'rgba(0, 0, 0, 0.3)',
dark: 'rgba(255, 255, 255, 0.16)'
},
separator: {
light: 'rgba(0, 0, 0, 0.1)',
dark: 'rgba(255, 255, 255, 0.1)'
}
}
const getTheme = (mode: 'light' | 'dark') => {
let Theme = {} as {
[key in ColorDefinitions]: string
}
const keys = Object.keys(themeColors) as ColorDefinitions[]
keys.forEach(key => (Theme[key] = themeColors[key][mode]))
return Theme
}
const themes = {
light: {
...DefaultTheme,
colors: {
...DefaultTheme.colors,
primary: themeColors.primary.light,
background: themeColors.background.light,
card: themeColors.background.light || 'rgba(249, 249, 249, 0.94)',
text: themeColors.primary.light,
border: themeColors.border.light,
notification: 'rgb(255, 59, 48)'
}
},
dark: {
...DarkTheme,
colors: {
...DarkTheme.colors,
primary: themeColors.primary.dark,
background: themeColors.background.dark,
card: themeColors.background.dark || 'rgba(22, 22, 22, 0.94)',
text: themeColors.primary.dark,
border: themeColors.border.dark,
notification: 'rgb(255, 69, 58)'
}
}
}
export { themeColors, getTheme, themes }

837
yarn.lock

File diff suppressed because it is too large Load Diff