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 React from 'react'
import { AppearanceProvider } from 'react-native-appearance'
import { QueryCache, ReactQueryCacheProvider, setConsole } from 'react-query' import { QueryCache, ReactQueryCacheProvider, setConsole } from 'react-query'
import { Provider } from 'react-redux' import { Provider } from 'react-redux'
import ThemeManager from 'src/utils/styles/ThemeManager'
import { Index } from 'src/Index' import { Index } from 'src/Index'
import { store } from 'src/store' import { store } from 'src/store'
@ -18,12 +20,18 @@ if (__DEV__) {
// whyDidYouRender(React) // whyDidYouRender(React)
} }
const App: React.FC = () => ( const App: React.FC = () => {
<ReactQueryCacheProvider queryCache={queryCache}> return (
<Provider store={store}> <AppearanceProvider>
<Index /> <ThemeManager>
</Provider> <ReactQueryCacheProvider queryCache={queryCache}>
</ReactQueryCacheProvider> <Provider store={store}>
) <Index />
</Provider>
</ReactQueryCacheProvider>
</ThemeManager>
</AppearanceProvider>
)
}
export default App 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": { "dependencies": {
"@expo/vector-icons": "^10.0.0", "@expo/vector-icons": "^10.0.0",
"@react-native-community/masked-view": "0.1.10",
"@react-native-community/segmented-control": "2.1.1", "@react-native-community/segmented-control": "2.1.1",
"@react-navigation/bottom-tabs": "^5.10.6", "@react-navigation/bottom-tabs": "^5.10.6",
"@react-navigation/native": "^5.8.6", "@react-navigation/native": "^5.8.6",
@ -25,6 +26,7 @@
"react": "16.13.1", "react": "16.13.1",
"react-dom": "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": "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-collapsible": "^1.5.3",
"react-native-gesture-handler": "~1.7.0", "react-native-gesture-handler": "~1.7.0",
"react-native-htmlview": "^0.16.0", "react-native-htmlview": "^0.16.0",
@ -45,6 +47,7 @@
"devDependencies": { "devDependencies": {
"@babel/core": "~7.12.3", "@babel/core": "~7.12.3",
"@babel/plugin-proposal-optional-chaining": "^7.12.1", "@babel/plugin-proposal-optional-chaining": "^7.12.1",
"@expo/config": "^3.3.15",
"@types/lodash": "^4.14.164", "@types/lodash": "^4.14.164",
"@types/node": "^14.14.7", "@types/node": "^14.14.7",
"@types/react": "~16.9.35", "@types/react": "~16.9.35",
@ -58,4 +61,4 @@
"typescript": "~3.9.2" "typescript": "~3.9.2"
}, },
"private": true "private": true
} }

View File

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

View File

@ -2,6 +2,7 @@ import React from 'react'
import { Pressable, StyleSheet, Text, View } from 'react-native' import { Pressable, StyleSheet, Text, View } from 'react-native'
import { useNavigation } from '@react-navigation/native' import { useNavigation } from '@react-navigation/native'
import { Feather } from '@expo/vector-icons' import { Feather } from '@expo/vector-icons'
import { useTheme } from 'src/utils/styles/ThemeManager'
export interface Props { export interface Props {
icon?: string icon?: string
@ -11,6 +12,8 @@ export interface Props {
} }
const Core: React.FC<Props> = ({ icon, title, navigateTo }) => { const Core: React.FC<Props> = ({ icon, title, navigateTo }) => {
const { theme } = useTheme()
return ( return (
<View style={styles.core}> <View style={styles.core}>
{icon && <Feather name={icon} size={24} style={styles.iconLeading} />} {icon && <Feather name={icon} size={24} style={styles.iconLeading} />}
@ -19,7 +22,7 @@ const Core: React.FC<Props> = ({ icon, title, navigateTo }) => {
<Feather <Feather
name='chevron-right' name='chevron-right'
size={24} size={24}
color='lightgray' color={theme.secondary}
style={styles.iconNavigation} style={styles.iconNavigation}
/> />
)} )}

View File

@ -1,18 +1,22 @@
import React from 'react' import React from 'react'
import { StyleSheet, Text } from 'react-native' import { Text } from 'react-native'
import HTMLView, { HTMLViewNode } from 'react-native-htmlview' import HTMLView, { HTMLViewNode } from 'react-native-htmlview'
import { useNavigation } from '@react-navigation/native' 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 // Prevent going to the same hashtag multiple times
const renderNode = ({ const renderNode = ({
theme,
node, node,
index, index,
navigation, navigation,
mentions, mentions,
showFullLink showFullLink
}: { }: {
node: HTMLViewNode theme: any
node: any
index: number index: number
navigation: any navigation: any
mentions?: Mastodon.Mention[] mentions?: Mastodon.Mention[]
@ -26,10 +30,10 @@ const renderNode = ({
return ( return (
<Text <Text
key={index} key={index}
style={styles.a} style={{ color: theme.link }}
onPress={() => { onPress={() => {
const tag = href.split(new RegExp(/\/tag\/(.*)|\/tags\/(.*)/)) const tag = href.split(new RegExp(/\/tag\/(.*)|\/tags\/(.*)/))
navigation.push('Hashtag', { navigation.push('Screen-Shared-Hashtag', {
hashtag: tag[1] || tag[2] hashtag: tag[1] || tag[2]
}) })
}} }}
@ -42,13 +46,13 @@ const renderNode = ({
return ( return (
<Text <Text
key={index} key={index}
style={styles.a} style={{ color: theme.link }}
onPress={() => { onPress={() => {
const username = href.split(new RegExp(/@(.*)/)) const username = href.split(new RegExp(/@(.*)/))
const usernameIndex = mentions.findIndex( const usernameIndex = mentions.findIndex(
m => m.username === username[1] m => m.username === username[1]
) )
navigation.push('Account', { navigation.push('Screen-Shared-Account', {
id: mentions[usernameIndex].id id: mentions[usernameIndex].id
}) })
}} }}
@ -63,9 +67,9 @@ const renderNode = ({
return ( return (
<Text <Text
key={index} key={index}
style={styles.a} style={{ color: theme.link }}
onPress={() => { onPress={() => {
navigation.navigate('Webview', { navigation.navigate('Screen-Shared-Webview', {
uri: href, uri: href,
domain: domain[1] domain: domain[1]
}) })
@ -80,8 +84,8 @@ const renderNode = ({
export interface Props { export interface Props {
content: string content: string
size: number
emojis?: Mastodon.Emoji[] emojis?: Mastodon.Emoji[]
emojiSize?: number
mentions?: Mastodon.Mention[] mentions?: Mastodon.Mention[]
showFullLink?: boolean showFullLink?: boolean
linesTruncated?: number linesTruncated?: number
@ -89,29 +93,24 @@ export interface Props {
const ParseContent: React.FC<Props> = ({ const ParseContent: React.FC<Props> = ({
content, content,
size,
emojis, emojis,
emojiSize = 14,
mentions, mentions,
showFullLink = false, showFullLink = false,
linesTruncated = 10 linesTruncated = 10
}) => { }) => {
const navigation = useNavigation() const navigation = useNavigation()
const { theme } = useTheme()
return ( return (
<HTMLView <HTMLView
value={content} value={content}
stylesheet={HTMLstyles}
paragraphBreak=''
renderNode={(node, index) => renderNode={(node, index) =>
renderNode({ node, index, navigation, mentions, showFullLink }) renderNode({ theme, node, index, navigation, mentions, showFullLink })
} }
TextComponent={({ children }) => TextComponent={({ children }) =>
emojis && children ? ( emojis && children ? (
<Emojis <Emojis content={children.toString()} emojis={emojis} size={size} />
content={children.toString()}
emojis={emojis}
dimension={emojiSize}
/>
) : ( ) : (
<Text>{children}</Text> <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 export default ParseContent

View File

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

View File

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

View File

@ -2,9 +2,11 @@ import React, { useMemo } from 'react'
import { Pressable, StyleSheet, View } from 'react-native' import { Pressable, StyleSheet, View } from 'react-native'
import { useNavigation } from '@react-navigation/native' import { useNavigation } from '@react-navigation/native'
import Avatar from './Status/Avatar' import Avatar from './Shared/Avatar'
import HeaderConversation from './Status/HeaderConversation' import HeaderConversation from './Shared/HeaderConversation'
import Content from './Status/Content' import Content from './Shared/Content'
import constants from 'src/utils/styles/constants'
export interface Props { export interface Props {
item: Mastodon.Conversation item: Mastodon.Conversation
@ -58,7 +60,7 @@ const styles = StyleSheet.create({
statusView: { statusView: {
flex: 1, flex: 1,
flexDirection: 'column', flexDirection: 'column',
padding: 12 padding: constants.GLOBAL_PAGE_PADDING
}, },
status: { status: {
flex: 1, flex: 1,

View File

@ -2,20 +2,23 @@ import React, { useMemo } from 'react'
import { Dimensions, Pressable, StyleSheet, View } from 'react-native' import { Dimensions, Pressable, StyleSheet, View } from 'react-native'
import { useNavigation } from '@react-navigation/native' import { useNavigation } from '@react-navigation/native'
import Actioned from './Status/Actioned' import Actioned from './Shared/Actioned'
import Avatar from './Status/Avatar' import Avatar from './Shared/Avatar'
import Header from './Status/Header' import HeaderDefault from './Shared/HeaderDefault'
import Content from './Status/Content' import Content from './Shared/Content'
import Poll from './Status/Poll' import Poll from './Shared/Poll'
import Attachment from './Status/Attachment' import Attachment from './Shared/Attachment'
import Card from './Status/Card' import Card from './Shared/Card'
import ActionsStatus from './Status/ActionsStatus' import ActionsStatus from './Shared/ActionsStatus'
import constants from 'src/utils/styles/constants'
export interface Props { export interface Props {
item: Mastodon.Status item: Mastodon.Status
queryKey: App.QueryKey queryKey: App.QueryKey
} }
// When the poll is long
const TimelineDefault: React.FC<Props> = ({ item, queryKey }) => { const TimelineDefault: React.FC<Props> = ({ item, queryKey }) => {
const navigation = useNavigation() const navigation = useNavigation()
@ -37,7 +40,7 @@ const TimelineDefault: React.FC<Props> = ({ item, queryKey }) => {
id={actualStatus.account.id} id={actualStatus.account.id}
/> />
<View style={styles.details}> <View style={styles.details}>
<Header <HeaderDefault
queryKey={queryKey} queryKey={queryKey}
accountId={actualStatus.account.id} accountId={actualStatus.account.id}
domain={actualStatus.uri.split(new RegExp(/\/\/(.*?)\//))[1]} domain={actualStatus.uri.split(new RegExp(/\/\/(.*?)\//))[1]}
@ -75,7 +78,12 @@ const TimelineDefault: React.FC<Props> = ({ item, queryKey }) => {
<Attachment <Attachment
media_attachments={actualStatus.media_attachments} media_attachments={actualStatus.media_attachments}
sensitive={actualStatus.sensitive} 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} />} {actualStatus.card && <Card card={actualStatus.card} />}
@ -94,7 +102,7 @@ const styles = StyleSheet.create({
statusView: { statusView: {
flex: 1, flex: 1,
flexDirection: 'column', flexDirection: 'column',
padding: 12 padding: constants.GLOBAL_PAGE_PADDING
}, },
status: { status: {
flex: 1, flex: 1,

View File

@ -2,21 +2,23 @@ import React, { useMemo } from 'react'
import { Dimensions, Pressable, StyleSheet, View } from 'react-native' import { Dimensions, Pressable, StyleSheet, View } from 'react-native'
import { useNavigation } from '@react-navigation/native' import { useNavigation } from '@react-navigation/native'
import Actioned from './Status/Actioned' import Actioned from './Shared/Actioned'
import Avatar from './Status/Avatar' import Avatar from './Shared/Avatar'
import Header from './Status/Header' import HeaderDefault from './Shared/HeaderDefault'
import Content from './Status/Content' import Content from './Shared/Content'
import Poll from './Status/Poll' import Poll from './Shared/Poll'
import Attachment from './Status/Attachment' import Attachment from './Shared/Attachment'
import Card from './Status/Card' import Card from './Shared/Card'
import ActionsStatus from './Status/ActionsStatus' import ActionsStatus from './Shared/ActionsStatus'
import constants from 'src/utils/styles/constants'
export interface Props { export interface Props {
notification: Mastodon.Notification notification: Mastodon.Notification
queryKey: App.QueryKey queryKey: App.QueryKey
} }
const TootNotification: React.FC<Props> = ({ notification, queryKey }) => { const TimelineNotifications: React.FC<Props> = ({ notification, queryKey }) => {
const navigation = useNavigation() const navigation = useNavigation()
const actualAccount = notification.status const actualAccount = notification.status
? notification.status.account ? notification.status.account
@ -37,7 +39,7 @@ const TootNotification: React.FC<Props> = ({ notification, queryKey }) => {
<View style={styles.notification}> <View style={styles.notification}>
<Avatar uri={actualAccount.avatar} id={actualAccount.id} /> <Avatar uri={actualAccount.avatar} id={actualAccount.id} />
<View style={styles.details}> <View style={styles.details}>
<Header <HeaderDefault
name={actualAccount.display_name || actualAccount.username} name={actualAccount.display_name || actualAccount.username}
emojis={actualAccount.emojis} emojis={actualAccount.emojis}
account={actualAccount.acct} account={actualAccount.acct}
@ -96,7 +98,7 @@ const styles = StyleSheet.create({
notificationView: { notificationView: {
flex: 1, flex: 1,
flexDirection: 'column', flexDirection: 'column',
padding: 12 padding: constants.GLOBAL_PAGE_PADDING
}, },
notification: { notification: {
flex: 1, 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 { Feather } from '@expo/vector-icons'
import Emojis from './Emojis' import Emojis from './Emojis'
import { useTheme } from 'src/utils/styles/ThemeManager'
import constants from 'src/utils/styles/constants'
export interface Props { export interface Props {
action: 'favourite' | 'follow' | 'mention' | 'poll' | 'reblog' action: 'favourite' | 'follow' | 'mention' | 'poll' | 'reblog'
@ -17,18 +20,31 @@ const Actioned: React.FC<Props> = ({
emojis, emojis,
notification = false notification = false
}) => { }) => {
const { theme } = useTheme()
const iconColor = theme.primary
let icon let icon
let content let content
switch (action) { switch (action) {
case 'favourite': case 'favourite':
icon = ( 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} 喜欢了你的嘟嘟` content = `${name} 喜欢了你的嘟嘟`
break break
case 'follow': case 'follow':
icon = ( 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} 开始关注你` content = `${name} 开始关注你`
break break
@ -36,7 +52,7 @@ const Actioned: React.FC<Props> = ({
icon = ( icon = (
<Feather <Feather
name='bar-chart-2' name='bar-chart-2'
size={12} size={constants.FONT_SIZE_S}
color='black' color='black'
style={styles.icon} style={styles.icon}
/> />
@ -45,7 +61,12 @@ const Actioned: React.FC<Props> = ({
break break
case 'reblog': case 'reblog':
icon = ( 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 ? '你的嘟嘟' : ''}` content = `${name} 转嘟了${notification ? '你的嘟嘟' : ''}`
break break
@ -57,7 +78,11 @@ const Actioned: React.FC<Props> = ({
{content ? ( {content ? (
<View style={styles.content}> <View style={styles.content}>
{emojis ? ( {emojis ? (
<Emojis content={content} emojis={emojis} dimension={12} /> <Emojis
content={content}
emojis={emojis}
size={constants.FONT_SIZE_S}
/>
) : ( ) : (
<Text>{content}</Text> <Text>{content}</Text>
)} )}
@ -72,11 +97,11 @@ const Actioned: React.FC<Props> = ({
const styles = StyleSheet.create({ const styles = StyleSheet.create({
actioned: { actioned: {
flexDirection: 'row', flexDirection: 'row',
marginBottom: 8 marginBottom: constants.SPACING_S
}, },
icon: { icon: {
marginLeft: 50 - 12, marginLeft: constants.AVATAR_S - constants.FONT_SIZE_S,
marginRight: 8 marginRight: constants.SPACING_S
}, },
content: { content: {
flexDirection: 'row' flexDirection: 'row'

View File

@ -14,7 +14,9 @@ import { Feather } from '@expo/vector-icons'
import client from 'src/api/client' import client from 'src/api/client'
import { getLocalAccountId } from 'src/utils/slices/instancesSlice' 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 ({ const fireMutation = async ({
id, id,
@ -112,6 +114,11 @@ export interface Props {
} }
const ActionsStatus: React.FC<Props> = ({ queryKey, status }) => { 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 localAccountId = getLocalAccountId(store.getState())
const [modalVisible, setModalVisible] = useState(false) const [modalVisible, setModalVisible] = useState(false)
@ -153,8 +160,22 @@ const ActionsStatus: React.FC<Props> = ({ queryKey, status }) => {
<> <>
<View style={styles.actions}> <View style={styles.actions}>
<Pressable style={styles.action}> <Pressable style={styles.action}>
<Feather name='message-circle' color='gray' /> <Feather
{status.replies_count > 0 && <Text>{status.replies_count}</Text>} 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>
<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>
<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>
<Pressable <Pressable
@ -198,12 +227,17 @@ const ActionsStatus: React.FC<Props> = ({ queryKey, status }) => {
> >
<Feather <Feather
name='bookmark' name='bookmark'
color={status.bookmarked ? 'black' : 'gray'} color={iconColorAction(status.bookmarked)}
size={constants.FONT_SIZE_M + 2}
/> />
</Pressable> </Pressable>
<Pressable style={styles.action} onPress={() => setModalVisible(true)}> <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> </Pressable>
</View> </View>
@ -319,14 +353,12 @@ const styles = StyleSheet.create({
width: '100%', width: '100%',
flex: 1, flex: 1,
flexDirection: 'row', flexDirection: 'row',
marginTop: 8 marginTop: constants.SPACING_M
}, },
action: { action: {
width: '20%', width: '20%',
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'center', justifyContent: 'center'
paddingTop: 8,
paddingBottom: 8
}, },
modalBackground: { modalBackground: {
width: '100%', width: '100%',

View File

@ -2,6 +2,8 @@ import React from 'react'
import { Image, Pressable, StyleSheet } from 'react-native' import { Image, Pressable, StyleSheet } from 'react-native'
import { useNavigation } from '@react-navigation/native' import { useNavigation } from '@react-navigation/native'
import constants from 'src/utils/styles/constants'
export interface Props { export interface Props {
uri: string uri: string
id: string id: string
@ -26,13 +28,14 @@ const Avatar: React.FC<Props> = ({ uri, id }) => {
const styles = StyleSheet.create({ const styles = StyleSheet.create({
avatar: { avatar: {
width: 50, width: constants.AVATAR_S,
height: 50, height: constants.AVATAR_S,
marginRight: 8 marginRight: constants.SPACING_S
}, },
image: { image: {
width: '100%', 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 ParseContent from 'src/components/ParseContent'
import constants from 'src/utils/styles/constants'
import { useTheme } from 'src/utils/styles/ThemeManager'
export interface Props { export interface Props {
content: string content: string
emojis: Mastodon.Emoji[] emojis: Mastodon.Emoji[]
@ -17,6 +20,7 @@ const Content: React.FC<Props> = ({
mentions, mentions,
spoiler_text spoiler_text
}) => { }) => {
const { theme } = useTheme()
const [spoilerCollapsed, setSpoilerCollapsed] = useState(true) const [spoilerCollapsed, setSpoilerCollapsed] = useState(true)
return ( return (
@ -26,15 +30,18 @@ const Content: React.FC<Props> = ({
<> <>
<Text> <Text>
{spoiler_text}{' '} {spoiler_text}{' '}
<Text onPress={() => setSpoilerCollapsed(!spoilerCollapsed)}> <Text
onPress={() => setSpoilerCollapsed(!spoilerCollapsed)}
style={{ color: theme.link }}
>
{spoilerCollapsed ? '点击展开' : '点击收起'}
</Text> </Text>
</Text> </Text>
<Collapsible collapsed={spoilerCollapsed}> <Collapsible collapsed={spoilerCollapsed}>
<ParseContent <ParseContent
content={content} content={content}
size={constants.FONT_SIZE_M}
emojis={emojis} emojis={emojis}
emojiSize={14}
mentions={mentions} mentions={mentions}
/> />
</Collapsible> </Collapsible>
@ -42,8 +49,8 @@ const Content: React.FC<Props> = ({
) : ( ) : (
<ParseContent <ParseContent
content={content} content={content}
size={constants.FONT_SIZE_M}
emojis={emojis} emojis={emojis}
emojiSize={14}
mentions={mentions} mentions={mentions}
/> />
))} ))}

View File

@ -1,16 +1,37 @@
import React from 'react' 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_]+:)/) const regexEmoji = new RegExp(/(:[a-z0-9_]+:)/)
export interface Props { export interface Props {
content: string content: string
emojis: Mastodon.Emoji[] 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) const hasEmojis = content.match(regexEmoji)
return hasEmojis ? ( return hasEmojis ? (
<> <>
{content.split(regexEmoji).map((str, i) => { {content.split(regexEmoji).map((str, i) => {
@ -20,20 +41,19 @@ const Emojis: React.FC<Props> = ({ content, emojis, dimension }) => {
return emojiShortcode === `:${emoji.shortcode}:` return emojiShortcode === `:${emoji.shortcode}:`
}) })
return emojiIndex === -1 ? ( return emojiIndex === -1 ? (
<Text key={i}>{emojiShortcode}</Text> <Text key={i} style={styles.text}>
{emojiShortcode}
</Text>
) : ( ) : (
<Image <Image
key={i} key={i}
source={{ uri: emojis[emojiIndex].url }} source={{ uri: emojis[emojiIndex].url }}
style={{ width: dimension, height: dimension }} style={styles.image}
/> />
) )
} else { } else {
return ( return (
<Text <Text key={i} style={styles.text}>
key={i}
style={{ fontSize: dimension, lineHeight: dimension + 1 }}
>
{str} {str}
</Text> </Text>
) )
@ -41,9 +61,7 @@ const Emojis: React.FC<Props> = ({ content, emojis, dimension }) => {
})} })}
</> </>
) : ( ) : (
<Text style={{ fontSize: dimension, lineHeight: dimension + 1 }}> <Text style={styles.text}>{content}</Text>
{content}
</Text>
) )
} }

View File

@ -18,7 +18,7 @@ const HeaderConversation: React.FC<Props> = ({ account, created_at }) => {
<Emojis <Emojis
content={account.display_name || account.username} content={account.display_name || account.username}
emojis={account.emojis} emojis={account.emojis}
dimension={14} size={14}
/> />
) : ( ) : (
<Text numberOfLines={1}> <Text numberOfLines={1}>

View File

@ -8,12 +8,10 @@ import { useMutation, useQueryCache } from 'react-query'
import Emojis from './Emojis' import Emojis from './Emojis'
import relativeTime from 'src/utils/relativeTime' import relativeTime from 'src/utils/relativeTime'
import client from 'src/api/client' import client from 'src/api/client'
import { useSelector } from 'react-redux' import { getLocalAccountId, getLocalUrl } from 'src/utils/slices/instancesSlice'
import { import { store } from 'src/store'
getLocalAccountId, import { useTheme } from 'src/utils/styles/ThemeManager'
getLocalUrl import constants from 'src/utils/styles/constants'
} from 'src/utils/slices/instancesSlice'
import {store} from 'src/store'
const fireMutation = async ({ const fireMutation = async ({
id, id,
@ -131,7 +129,7 @@ export interface Props {
application?: Mastodon.Application application?: Mastodon.Application
} }
const Header: React.FC<Props> = ({ const HeaderDefault: React.FC<Props> = ({
queryKey, queryKey,
accountId, accountId,
domain, domain,
@ -141,6 +139,8 @@ const Header: React.FC<Props> = ({
created_at, created_at,
application application
}) => { }) => {
const { theme } = useTheme()
const navigation = useNavigation() const navigation = useNavigation()
const localAccountId = getLocalAccountId(store.getState()) const localAccountId = getLocalAccountId(store.getState())
const localDomain = getLocalUrl(store.getState()) const localDomain = getLocalUrl(store.getState())
@ -194,26 +194,42 @@ const Header: React.FC<Props> = ({
<View style={styles.nameAndAction}> <View style={styles.nameAndAction}>
<View style={styles.name}> <View style={styles.name}>
{emojis ? ( {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> </View>
{accountId !== localAccountId && domain !== localDomain && ( {accountId !== localAccountId && domain !== localDomain && (
<Pressable <Pressable
style={styles.action} style={styles.action}
onPress={() => setModalVisible(true)} onPress={() => setModalVisible(true)}
> >
<Feather name='more-horizontal' color='gray' /> <Feather
name='more-horizontal'
color={theme.secondary}
size={constants.FONT_SIZE_M + 2}
/>
</Pressable> </Pressable>
)} )}
</View> </View>
<Text style={styles.account} numberOfLines={1}>
@{account}
</Text>
<View style={styles.meta}> <View style={styles.meta}>
<View> <View>
<Text style={styles.created_at}>{since}</Text> <Text style={[styles.created_at, { color: theme.secondary }]}>
{since}
</Text>
</View> </View>
{application && application.name !== 'Web' && ( {application && application.name !== 'Web' && (
<View> <View>
@ -223,9 +239,9 @@ const Header: React.FC<Props> = ({
uri: application.website uri: application.website
}) })
}} }}
style={styles.application} style={[styles.application, { color: theme.secondary }]}
> >
{application.name} - {application.name}
</Text> </Text>
</View> </View>
)} )}
@ -310,32 +326,29 @@ const styles = StyleSheet.create({
justifyContent: 'space-between' justifyContent: 'space-between'
}, },
name: { name: {
flexDirection: 'row', flexBasis: '80%',
marginRight: 8,
fontWeight: '900'
},
action: {
width: 14,
height: 14,
marginLeft: 8
},
account: {
lineHeight: 14,
flexShrink: 1
},
meta: {
flexDirection: 'row' 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: { created_at: {
fontSize: 12, fontSize: constants.FONT_SIZE_S
lineHeight: 12,
marginTop: 8,
marginBottom: 8,
marginRight: 8
}, },
application: { application: {
fontSize: 12, fontSize: constants.FONT_SIZE_S,
lineHeight: 11 marginLeft: constants.SPACING_S
}, },
modalBackground: { modalBackground: {
width: '100%', 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 <Emojis
content={option.title} content={option.title}
emojis={poll.emojis} emojis={poll.emojis}
dimension={14} size={14}
/> />
</View> </View>
<View <View

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import React from 'react' 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? // Show remote hashtag? Only when private, show local version?

View File

@ -1,6 +1,6 @@
import React from 'react' 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? // 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