This commit is contained in:
Zhiyuan Zheng 2021-01-17 22:37:05 +01:00
parent 813f6b57c4
commit f977fdfa8b
No known key found for this signature in database
GPG Key ID: 078A93AB607D85E0
44 changed files with 252 additions and 158 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 69 KiB

View File

@ -154,7 +154,7 @@ const GracefullyImage: React.FC<Props> = ({
children={children}
style={[style, dimension && { ...dimension }]}
{...(onPress
? !imageVisible
? hidden
? { disabled: true }
: { onPress }
: { disabled: true })}

View File

@ -244,6 +244,7 @@ const ComponentInstance: React.FC<Props> = ({
{type === 'local' && appData ? (
<InstanceAuth
instanceDomain={instanceDomain!}
instanceUri={instanceQuery.data!.uri}
appData={appData}
goBack={goBack}
/>

View File

@ -8,12 +8,14 @@ import { useDispatch } from 'react-redux'
export interface Props {
instanceDomain: string
// Domain can be different than uri
instanceUri: Mastodon.Instance['uri']
appData: InstanceLocal['appData']
goBack?: boolean
}
const InstanceAuth = React.memo(
({ instanceDomain, appData, goBack }: Props) => {
({ instanceDomain, instanceUri, appData, goBack }: Props) => {
let redirectUri: string
switch (Constants.manifest.releaseChannel) {
case 'production':
@ -70,6 +72,7 @@ const InstanceAuth = React.memo(
localAddInstance({
url: instanceDomain,
token: accessToken,
uri: instanceUri,
appData
})
)

View File

@ -1,5 +1,6 @@
import React from 'react'
import { Image, StyleSheet, Text } from 'react-native'
import { StyleSheet, Text } from 'react-native'
import { Image } from 'react-native-expo-image-cache'
import { useTheme } from '@utils/styles/ThemeManager'
import { StyleConstants } from '@utils/styles/constants'
@ -49,10 +50,7 @@ const ParseEmojis: React.FC<Props> = ({
<Text key={i}>
{/* When emoji starts a paragraph, lineHeight will break */}
{i === 0 ? <Text> </Text> : null}
<Image
source={{ uri: emojis[emojiIndex].url }}
style={[styles.image]}
/>
<Image uri={emojis[emojiIndex].url} style={[styles.image]} />
</Text>
)
} else {

View File

@ -68,7 +68,7 @@ const renderNode = ({
mention => mention.url === href
)
const differentAccount = routeParams?.account
? routeParams.account.id !== mentions[accountIndex].id
? routeParams.account.id !== mentions[accountIndex]?.id
: true
return (
<Text

View File

@ -5,22 +5,20 @@ import TimeAgo from 'react-timeago'
// @ts-ignore
import buildFormatter from 'react-timeago/lib/formatters/buildFormatter'
import zh from '@root/i18n/zh/components/relativeTime'
import en from '@root/i18n/en/components/relativeTime'
export interface Props {
date: string
}
const RelativeTime: React.FC<Props> = ({ date }) => {
const { i18n } = useTranslation()
const mapLanguageToTranslation: { [key: string]: Object } = {
'zh-CN': zh,
'en-US': en
}
const formatter = buildFormatter(mapLanguageToTranslation[i18n.language])
const { t } = useTranslation('relativeTime')
return <TimeAgo date={date} formatter={formatter} component={Text} />
return (
<TimeAgo
date={date}
component={Text}
formatter={buildFormatter(t('strings', { returnObjects: true }))}
/>
)
}
export default RelativeTime

View File

@ -56,6 +56,7 @@ const Timeline: React.FC<Props> = ({
refetch,
isSuccess,
isFetching,
isLoading,
hasPreviousPage,
fetchPreviousPage,
isFetchingPreviousPage,
@ -170,7 +171,8 @@ const Timeline: React.FC<Props> = ({
<RefreshControl
{...(Platform.OS === 'android' && { enabled: true })}
refreshing={
isFetchingPreviousPage || (isFetching && !isFetchingNextPage)
isFetchingPreviousPage ||
(isFetching && !isFetchingNextPage && !isLoading)
}
onRefresh={() => {
if (hasPreviousPage) {
@ -192,7 +194,13 @@ const Timeline: React.FC<Props> = ({
}}
/>
),
[hasPreviousPage, isFetchingPreviousPage, isFetching, isFetchingNextPage]
[
hasPreviousPage,
isFetchingPreviousPage,
isFetching,
isFetchingNextPage,
isLoading
]
)
const onScrollToIndexFailed = useCallback(error => {
const offset = error.averageItemLength * error.index
@ -209,10 +217,10 @@ const Timeline: React.FC<Props> = ({
return (
<FlatList
ref={flRef}
windowSize={11}
windowSize={8}
data={flattenData}
initialNumToRender={5}
maxToRenderPerBatch={5}
initialNumToRender={3}
maxToRenderPerBatch={3}
style={styles.flatList}
renderItem={renderItem}
onEndReached={onEndReached}

View File

@ -1,9 +1,9 @@
import Button from '@components/Button'
import haptics from '@components/haptics'
import Icon from '@components/Icon'
import { ParseEmojis } from '@components/Parse'
import RelativeTime from '@components/RelativeTime'
import { ParseEmojis } from '@root/components/Parse'
import { toast } from '@root/components/toast'
import { toast } from '@components/toast'
import {
QueryKeyTimeline,
useTimelineMutation
@ -32,7 +32,7 @@ const TimelinePoll: React.FC<Props> = ({
sameAccount
}) => {
const { mode, theme } = useTheme()
const { t, i18n } = useTranslation('timeline')
const { t } = useTranslation('timeline')
const [allOptions, setAllOptions] = useState(
new Array(poll.options.length).fill(false)
@ -220,7 +220,7 @@ const TimelinePoll: React.FC<Props> = ({
<Icon
style={styles.optionSelection}
name={isSelected(index)}
size={StyleConstants.Font.Size.L}
size={StyleConstants.Font.Size.M}
color={theme.primary}
/>
<Text style={styles.optionText}>
@ -275,6 +275,7 @@ const styles = StyleSheet.create({
flex: 1
},
optionSelection: {
paddingTop: StyleConstants.Font.LineHeight.M - StyleConstants.Font.Size.M,
marginRight: StyleConstants.Spacing.S
},
optionPercentage: {

View File

@ -1,3 +1,5 @@
export default {
common: require('./common').default
common: require('./common').default,
relativeTime: require('./components/relativeTime').default
}

View File

@ -1,20 +1,20 @@
const strings = {
prefixAgo: null,
prefixFromNow: null,
suffixAgo: 'ago',
suffixFromNow: 'from now',
seconds: '%d seconds',
minute: 'about a minute',
minutes: '%d minutes',
hour: 'about an hour',
hours: 'about %d hours',
day: 'a day',
days: '%d days',
month: 'about a month',
months: '%d months',
year: 'about a year',
years: '%d years',
wordSeparator: ' '
export default {
strings: {
prefixAgo: null,
prefixFromNow: null,
suffixAgo: 'ago',
suffixFromNow: 'from now',
seconds: '%d seconds',
minute: 'about a minute',
minutes: '%d minutes',
hour: 'about an hour',
hours: 'about %d hours',
day: 'a day',
days: '%d days',
month: 'about a month',
months: '%d months',
year: 'about a year',
years: '%d years',
wordSeparator: ' '
}
}
export default strings

View File

@ -1,32 +1,20 @@
import { store } from '@root/store'
import { getSettingsLanguage, supportedLngs } from '@utils/slices/settingsSlice'
import i18next from 'i18next'
import { initReactI18next } from 'react-i18next'
import * as Localization from 'expo-localization'
import zh from '@root/i18n/zh/_all'
import en from '@root/i18n/en/_all'
import {
changeLanguage,
getSettingsLanguage
} from '@utils/slices/settingsSlice'
import { store } from '@root/store'
import zh_Hans from '@root/i18n/zh-Hans/_all'
if (!getSettingsLanguage(store.getState())) {
const deviceLocal = Localization.locale
if (deviceLocal.startsWith('zh')) {
store.dispatch(changeLanguage('zh-CN'))
} else {
store.dispatch(changeLanguage('en-US'))
}
}
i18next.use(initReactI18next).init({
lng: 'zh-CN',
fallbackLng: 'en-US',
supportedLngs: ['zh-CN', 'en-US'],
lng: getSettingsLanguage(store.getState()),
fallbackLng: 'en',
supportedLngs: supportedLngs,
ns: ['common'],
defaultNS: 'common',
resources: { 'zh-CN': zh, 'en-US': en },
resources: { 'zh-Hans': zh_Hans, en },
saveMissing: true,
missingKeyHandler: (lng, ns, key, fallbackValue) => {

View File

@ -21,5 +21,6 @@ export default {
sharedAnnouncements: require('./screens/sharedAnnouncements').default,
relationship: require('./components/relationship').default,
relativeTime: require('./components/relativeTime').default,
timeline: require('./components/timeline').default
}

View File

@ -0,0 +1,21 @@
export default {
strings: {
prefixAgo: null,
prefixFromNow: null,
suffixAgo: '之前',
suffixFromNow: '之后',
seconds: '%d秒',
minute: '1分钟',
minutes: '%d分钟',
hour: '1小时',
hours: '%d小时',
day: '1天',
days: '%d天',
month: '1个月',
months: '%d月',
year: '大约1年',
years: '%d年',
wordSeparator: ''
}
}

View File

@ -4,8 +4,8 @@ export default {
language: {
heading: '切换语言',
options: {
zh: '简体中文',
en: 'English',
'en': 'English',
'zh-Hans': '简体中文',
cancel: '$t(common:buttons.cancel)'
}
},

View File

@ -1,21 +0,0 @@
const strings = {
prefixAgo: null,
prefixFromNow: null,
suffixAgo: '之前',
suffixFromNow: '之后',
seconds: '%d秒',
minute: '1分钟',
minutes: '%d分钟',
hour: '1小时',
hours: '%d小时',
day: '1天',
days: '%d天',
month: '1个月',
months: '%d月',
year: '大约1年',
years: '%d年',
wordSeparator: ''
}
export default strings

View File

@ -98,33 +98,28 @@ const ScreenMeSettings: React.FC = () => {
title={t('content.language.heading')}
content={t(`content.language.options.${settingsLanguage}`)}
iconBack='ChevronRight'
onPress={() =>
onPress={() => {
const availableLanguages = Object.keys(
i18n.services.resourceStore.data
)
showActionSheetWithOptions(
{
title: t('content.language.heading'),
options: [
t('content.language.options.zh'),
t('content.language.options.en'),
...availableLanguages.map(language =>
t(`content.language.options.${language}`)
),
t('content.language.options.cancel')
],
cancelButtonIndex: 2
cancelButtonIndex: i18n.languages.length
},
buttonIndex => {
switch (buttonIndex) {
case 0:
haptics('Success')
dispatch(changeLanguage('zh-CN'))
i18n.changeLanguage('zh-CN')
break
case 1:
haptics('Success')
dispatch(changeLanguage('en-US'))
i18n.changeLanguage('en-US')
break
}
haptics('Success')
dispatch(changeLanguage(availableLanguages[buttonIndex]))
i18n.changeLanguage(availableLanguages[buttonIndex])
}
)
}
}}
/>
<MenuRow
title={t('content.theme.heading')}
@ -229,7 +224,7 @@ const ScreenMeSettings: React.FC = () => {
iconBack='ChevronRight'
/>
<Text style={[styles.version, { color: theme.secondary }]}>
{t('content.version', { version: '1.0.0' })}
{t('content.version', { version: Constants.manifest.version })}
</Text>
</MenuContainer>

View File

@ -15,6 +15,7 @@ export interface Props {
const AccountInformationCreated = forwardRef<ShimmerPlaceholder, Props>(
({ account }, ref) => {
const { i18n } = useTranslation()
const { theme } = useTheme()
const { t } = useTranslation('sharedAccount')
const ShimmerPlaceholder = createShimmerPlaceholder(LinearGradient)
@ -26,7 +27,11 @@ const AccountInformationCreated = forwardRef<ShimmerPlaceholder, Props>(
width={StyleConstants.Font.Size.S * 8}
height={StyleConstants.Font.LineHeight.S}
style={{ marginBottom: StyleConstants.Spacing.M }}
shimmerColors={[theme.shimmerDefault, theme.shimmerHighlight, theme.shimmerDefault]}
shimmerColors={[
theme.shimmerDefault,
theme.shimmerHighlight,
theme.shimmerDefault
]}
>
<View style={styles.created}>
<Icon
@ -42,11 +47,14 @@ const AccountInformationCreated = forwardRef<ShimmerPlaceholder, Props>(
}}
>
{t('content.created_at', {
date: new Date(account?.created_at!).toLocaleDateString('zh-CN', {
year: 'numeric',
month: 'long',
day: 'numeric'
})
date: new Date(account?.created_at!).toLocaleDateString(
i18n.language,
{
year: 'numeric',
month: 'long',
day: 'numeric'
}
)
})}
</Text>
</View>

View File

@ -28,7 +28,7 @@ const AccountInformationFields = React.memo(
size={'M'}
emojis={account.emojis}
showFullLink
numberOfLines={3}
numberOfLines={5}
/>
{field.verified_at ? (
<Icon
@ -45,7 +45,7 @@ const AccountInformationFields = React.memo(
size={'M'}
emojis={account.emojis}
showFullLink
numberOfLines={3}
numberOfLines={5}
/>
</View>
</View>

View File

@ -123,19 +123,28 @@ const Compose: React.FC<SharedComposeProp> = ({
<HeaderLeft
type='text'
content='退出编辑'
onPress={() =>
Alert.alert('确认取消编辑?', '', [
{
text: '退出编辑',
style: 'destructive',
onPress: () => navigation.goBack()
},
{ text: '继续编辑', style: 'cancel' }
])
}
onPress={() => {
if (
totalTextCount === 0 &&
composeState.attachments.uploads.length === 0 &&
composeState.poll.active === false
) {
navigation.goBack()
return
} else {
Alert.alert('确认取消编辑?', '', [
{
text: '退出编辑',
style: 'destructive',
onPress: () => navigation.goBack()
},
{ text: '继续编辑', style: 'cancel' }
])
}
}}
/>
),
[]
[totalTextCount]
)
const headerCenter = useCallback(
() => (

View File

@ -1,17 +1,77 @@
import { useAccountQuery } from '@utils/queryHooks/account'
import {
getLocalActiveIndex,
getLocalInstances,
InstanceLocal
} from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useContext } from 'react'
import ComposeSpoilerInput from '@screens/Shared/Compose/SpoilerInput'
import ComposeTextInput from '@screens/Shared/Compose/TextInput'
import ComposeContext from '@screens/Shared/Compose//utils/createContext'
import { StyleSheet, Text, View } from 'react-native'
import { Chase } from 'react-native-animated-spinkit'
import { useSelector } from 'react-redux'
import ComposeSpoilerInput from '../SpoilerInput'
import ComposeTextInput from '../TextInput'
import ComposeContext from '../utils/createContext'
const PostingAs: React.FC<{
id: Mastodon.Account['id']
domain: InstanceLocal['url']
}> = ({ id, domain }) => {
const { theme } = useTheme()
const { data, status } = useAccountQuery({ id })
switch (status) {
case 'loading':
return (
<Chase
size={StyleConstants.Font.LineHeight.M - 2}
color={theme.secondary}
/>
)
case 'success':
return (
<Text style={[styles.postingAsText, { color: theme.secondary }]}>
@{data?.acct}@{domain}
</Text>
)
default:
return null
}
}
const ComposeRootHeader: React.FC = () => {
const { composeState } = useContext(ComposeContext)
const localActiveIndex = useSelector(getLocalActiveIndex)
const localInstances = useSelector(getLocalInstances)
return (
<>
{localActiveIndex !== null &&
localInstances.length &&
localInstances.length > 1 && (
<View style={styles.postingAs}>
<PostingAs
id={localInstances[localActiveIndex].account.id}
domain={localInstances[localActiveIndex].uri}
/>
</View>
)}
{composeState.spoiler.active ? <ComposeSpoilerInput /> : null}
<ComposeTextInput />
</>
)
}
const styles = StyleSheet.create({
postingAs: {
marginHorizontal: StyleConstants.Spacing.Global.PagePadding,
marginTop: StyleConstants.Spacing.S
},
postingAsText: {
...StyleConstants.FontStyle.S
}
})
export default ComposeRootHeader

View File

@ -6,6 +6,7 @@ import {
Image,
Platform,
Share,
StatusBar,
StyleSheet,
Text
} from 'react-native'
@ -57,20 +58,23 @@ const ScreenSharedImagesViewer: React.FC<SharedImagesViewerProp> = ({
const component = useCallback(
() => (
<ImageViewer
index={initialIndex}
imageUrls={imageUrls}
pageAnimateTime={250}
enableSwipeDown={true}
useNativeDriver={true}
swipeDownThreshold={100}
renderIndicator={() => <></>}
saveToLocalByLongPress={false}
onSwipeDown={() => navigation.goBack()}
style={{ flex: 1, marginBottom: 44 + safeAreaInsets.bottom }}
onChange={index => index !== undefined && setCurrentIndex(index)}
renderImage={props => <TheImage {...props} imageUrls={imageUrls} />}
/>
<>
<StatusBar barStyle='light-content' />
<ImageViewer
index={initialIndex}
imageUrls={imageUrls}
pageAnimateTime={250}
enableSwipeDown={true}
useNativeDriver={true}
swipeDownThreshold={100}
renderIndicator={() => <></>}
saveToLocalByLongPress={false}
onSwipeDown={() => navigation.goBack()}
style={{ flex: 1, marginBottom: 44 + safeAreaInsets.bottom }}
onChange={index => index !== undefined && setCurrentIndex(index)}
renderImage={props => <TheImage {...props} imageUrls={imageUrls} />}
/>
</>
),
[]
)
@ -85,7 +89,9 @@ const ScreenSharedImagesViewer: React.FC<SharedImagesViewerProp> = ({
}, [currentIndex])
return (
<Stack.Navigator screenOptions={{ headerHideShadow: true, headerTopInsetEnabled: false }}>
<Stack.Navigator
screenOptions={{ headerHideShadow: true, headerTopInsetEnabled: false }}
>
<Stack.Screen
name='Screen-Shared-ImagesViewer-Root'
component={component}

View File

@ -150,7 +150,7 @@ const ScreenSharedSearch: React.FC<Props> = ({ searchTerm }) => {
<ComponentAccount
account={item}
onPress={() => {
navigation.push('Screen-Shared-Account', { item })
navigation.push('Screen-Shared-Account', { account: item })
}}
/>
)

View File

@ -201,16 +201,27 @@ const sharedScreens = (
// https://github.com/react-navigation/react-navigation/issues/6746#issuecomment-583897436
headerCenter: () => (
<View style={styles.searchBar}>
<Text
style={{ ...StyleConstants.FontStyle.M, color: theme.primary }}
>
</Text>
<TextInput
editable={false}
children={
<Text
style={[
styles.textInput,
{
color: theme.primary
}
]}
children='搜索'
/>
}
/>
<TextInput
style={[
styles.textInput,
{
color: theme.primary
flex: 1,
color: theme.primary,
paddingLeft: StyleConstants.Spacing.XS
}
]}
autoFocus
@ -251,10 +262,7 @@ const styles = StyleSheet.create({
alignItems: 'center'
},
textInput: {
...StyleConstants.FontStyle.M,
paddingLeft: StyleConstants.Spacing.XS,
marginBottom:
(StyleConstants.Font.LineHeight.M - StyleConstants.Font.Size.M) / 2
fontSize: StyleConstants.Font.Size.M
}
})

View File

@ -3,6 +3,7 @@ import analytics from '@components/analytics'
import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit'
import { RootState } from '@root/store'
import * as AuthSession from 'expo-auth-session'
import * as Localization from 'expo-localization'
export type InstanceLocal = {
appData: {
@ -11,6 +12,7 @@ export type InstanceLocal = {
}
url: string
token: string
uri: Mastodon.Instance['uri']
account: {
id: Mastodon.Account['id']
preferences: Mastodon.Preferences
@ -50,10 +52,12 @@ export const localAddInstance = createAsyncThunk(
async ({
url,
token,
uri,
appData
}: {
url: InstanceLocal['url']
token: InstanceLocal['token']
uri: Mastodon.Instance['uri']
appData: InstanceLocal['appData']
}): Promise<{ type: 'add' | 'overwrite'; data: InstanceLocal }> => {
const { store } = require('@root/store')
@ -101,6 +105,7 @@ export const localAddInstance = createAsyncThunk(
appData,
url,
token,
uri,
account: {
id,
preferences
@ -159,7 +164,7 @@ export const instancesInitialState: InstancesState = {
instances: []
},
remote: {
url: 'm.cmx.im'
url: Localization.locale.includes('zh') ? 'm.cmx.im' : 'mastodon.social'
}
}

View File

@ -1,16 +1,19 @@
import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit'
import { RootState } from '@root/store'
import * as Analytics from 'expo-firebase-analytics'
import * as Localization from 'expo-localization'
export const supportedLngs = ['zh-Hans', 'en']
export type SettingsState = {
language: 'zh-CN' | 'en-US' | undefined
language: 'zh-Hans' | 'en'
theme: 'light' | 'dark' | 'auto'
browser: 'internal' | 'external'
analytics: boolean
}
export const settingsInitialState = {
language: undefined,
language: Localization.locale,
theme: 'auto',
browser: 'internal',
analytics: true