First step to adapt to Android

This commit is contained in:
Zhiyuan Zheng 2021-01-13 01:03:46 +01:00
parent 2df172d026
commit 49715bba0d
No known key found for this signature in database
GPG Key ID: 078A93AB607D85E0
39 changed files with 417 additions and 324 deletions

32
App.tsx
View File

@ -1,4 +1,11 @@
import { ActionSheetProvider } from '@expo/react-native-action-sheet'
import Index from '@root/Index' import Index from '@root/Index'
import dev from '@root/startup/dev'
import sentry from '@root/startup/sentry'
import log from '@root/startup/log'
import audio from '@root/startup/audio'
import onlineStatus from '@root/startup/onlineStatus'
import netInfo from '@root/startup/netInfo'
import { persistor, store } from '@root/store' import { persistor, store } from '@root/store'
import ThemeManager from '@utils/styles/ThemeManager' import ThemeManager from '@utils/styles/ThemeManager'
import * as SplashScreen from 'expo-splash-screen' import * as SplashScreen from 'expo-splash-screen'
@ -7,17 +14,22 @@ import { enableScreens } from 'react-native-screens'
import { QueryClient, QueryClientProvider } from 'react-query' import { QueryClient, QueryClientProvider } from 'react-query'
import { Provider } from 'react-redux' import { Provider } from 'react-redux'
import { PersistGate } from 'redux-persist/integration/react' import { PersistGate } from 'redux-persist/integration/react'
import dev from '@root/startup/dev' import { LogBox, Platform } from 'react-native'
import sentry from '@root/startup/sentry' import Moment from 'react-moment'
import log from '@root/startup/log'
import audio from '@root/startup/audio' import moment from 'moment'
import onlineStatus from '@root/startup/onlineStatus' import 'moment/min/locales'
import netInfo from '@root/startup/netInfo'
if (Platform.OS === 'android') {
LogBox.ignoreLogs(['Setting a timer for a long period of time'])
}
dev() dev()
sentry() sentry()
audio() audio()
onlineStatus() onlineStatus()
Moment.globalMoment = moment
Moment.startPooledTimer(1000)
log('log', 'react-query', 'initializing') log('log', 'react-query', 'initializing')
const queryClient = new QueryClient() const queryClient = new QueryClient()
@ -68,9 +80,11 @@ const App: React.FC = () => {
log('log', 'App', 'loading actual app :)') log('log', 'App', 'loading actual app :)')
require('@root/i18n/i18n') require('@root/i18n/i18n')
return ( return (
<ThemeManager> <ActionSheetProvider>
<Index localCorrupt={localCorrupt} /> <ThemeManager>
</ThemeManager> <Index localCorrupt={localCorrupt} />
</ThemeManager>
</ActionSheetProvider>
) )
} else { } else {
return null return null

View File

@ -24,6 +24,10 @@ export default (): ExpoConfig => ({
}, },
googleServicesFile: './configs/GoogleService-Info.plist' googleServicesFile: './configs/GoogleService-Info.plist'
}, },
android: {
package: 'com.xmflsct.app.mastodon',
googleServicesFile: './configs/google-services.json'
},
// locales: { // locales: {
// zh: { // zh: {
// CFBundleDisplayName: '我的嘟嘟' // CFBundleDisplayName: '我的嘟嘟'
@ -57,8 +61,5 @@ export default (): ExpoConfig => ({
measurementId: 'G-3J0FS8WV5J' measurementId: 'G-3J0FS8WV5J'
} }
} }
},
experiments: {
turboModules: true
} }
}) })

View File

@ -0,0 +1,46 @@
{
"project_info": {
"project_number": "661638997772",
"project_id": "xmflsct-mastodon-app",
"storage_bucket": "xmflsct-mastodon-app.appspot.com"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:661638997772:android:0f929e1c9ebd7f969f8b29",
"android_client_info": {
"package_name": "com.xmflsct.app.mastodon"
}
},
"oauth_client": [
{
"client_id": "661638997772-6aiqk97aema0rt280i7nfar3ha2mlgno.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyDUw4s-mhQsHvs4hdIsldsi68ZIygM5MC4"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "661638997772-6aiqk97aema0rt280i7nfar3ha2mlgno.apps.googleusercontent.com",
"client_type": 3
},
{
"client_id": "661638997772-sqa4raeghhrieqt9guljhcul9b51dvna.apps.googleusercontent.com",
"client_type": 2,
"ios_info": {
"bundle_id": "com.xmflsct.app.mastodon"
}
}
]
}
}
}
],
"configuration_version": "1"
}

View File

@ -9,6 +9,7 @@
"test": "jest --watchAll" "test": "jest --watchAll"
}, },
"dependencies": { "dependencies": {
"@expo/react-native-action-sheet": "^3.8.0",
"@react-native-async-storage/async-storage": "^1.13.2", "@react-native-async-storage/async-storage": "^1.13.2",
"@react-native-community/masked-view": "0.1.10", "@react-native-community/masked-view": "0.1.10",
"@react-native-community/netinfo": "^5.9.7", "@react-native-community/netinfo": "^5.9.7",
@ -43,10 +44,12 @@
"gl-react-expo": "^4.0.1", "gl-react-expo": "^4.0.1",
"i18next": "^19.8.4", "i18next": "^19.8.4",
"lodash": "^4.17.20", "lodash": "^4.17.20",
"moment": "^2.29.1",
"pretty-bytes": "^5.5.0", "pretty-bytes": "^5.5.0",
"react": "16.13.1", "react": "16.13.1",
"react-dom": "16.13.1", "react-dom": "16.13.1",
"react-i18next": "^11.8.5", "react-i18next": "^11.8.5",
"react-moment": "^1.1.1",
"react-native": "https://github.com/expo/react-native/archive/sdk-40.0.0.tar.gz", "react-native": "https://github.com/expo/react-native/archive/sdk-40.0.0.tar.gz",
"react-native-animated-spinkit": "^1.4.2", "react-native-animated-spinkit": "^1.4.2",
"react-native-expo-image-cache": "^4.1.0", "react-native-expo-image-cache": "^4.1.0",
@ -100,4 +103,4 @@
"typescript": "~4.1.3" "typescript": "~4.1.3"
}, },
"private": true "private": true
} }

View File

@ -108,7 +108,6 @@ const Index: React.FC<Props> = ({ localCorrupt }) => {
refetchIntervalInBackground: true refetchIntervalInBackground: true
} }
}) })
const prevNotification = useSelector(getLocalNotification) const prevNotification = useSelector(getLocalNotification)
useEffect(() => { useEffect(() => {
if (queryNotification.data?.pages) { if (queryNotification.data?.pages) {
@ -244,7 +243,7 @@ const Index: React.FC<Props> = ({ localCorrupt }) => {
) )
return ( return (
<> <>
<StatusBar barStyle={barStyle[mode]} /> <StatusBar barStyle={barStyle[mode]} backgroundColor={theme.background} />
<NavigationContainer <NavigationContainer
ref={navigationRef} ref={navigationRef}
theme={themes[mode]} theme={themes[mode]}
@ -293,7 +292,7 @@ const Index: React.FC<Props> = ({ localCorrupt }) => {
<Tab.Screen name='Screen-Me' component={ScreenMe} /> <Tab.Screen name='Screen-Me' component={ScreenMe} />
</Tab.Navigator> </Tab.Navigator>
<Toast ref={Toast.setRef} config={toastConfig} /> {/* <Toast ref={Toast.setRef} config={toastConfig} /> */}
</NavigationContainer> </NavigationContainer>
</> </>
) )

View File

@ -1,61 +0,0 @@
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import React, { useRef } from 'react'
import { RefreshControl } from 'react-native'
import { InfiniteData, useQueryClient } from 'react-query'
export interface Props {
queryKey: QueryKeyTimeline
isFetchingPreviousPage: boolean
isFetching: boolean
fetchPreviousPage: () => void
refetch: () => void
}
const CustomRefreshControl = React.memo(
({
queryKey,
isFetchingPreviousPage,
isFetching,
fetchPreviousPage,
refetch
}: Props) => {
const queryClient = useQueryClient()
const refreshCount = useRef(0)
return (
<RefreshControl
refreshing={
refreshCount.current < 2 ? isFetchingPreviousPage : isFetching
}
onRefresh={async () => {
if (refreshCount.current < 2) {
await fetchPreviousPage()
refreshCount.current++
} else {
queryClient.setQueryData<InfiniteData<any> | undefined>(
queryKey,
data => {
if (data) {
return {
pages: data.pages.slice(1),
pageParams: data.pageParams.slice(1)
}
}
}
)
await refetch()
refreshCount.current = 0
}
}}
/>
)
},
(prev, next) => {
let skipUpdate = true
skipUpdate = prev.isFetchingPreviousPage === next.isFetchingPreviousPage
skipUpdate = prev.isFetching === next.isFetching
return skipUpdate
}
)
export default CustomRefreshControl

View File

@ -65,7 +65,7 @@ const MenuRow: React.FC<Props> = ({
{iconFront && ( {iconFront && (
<Icon <Icon
name={iconFront} name={iconFront}
size={StyleConstants.Font.Size.M + 2} size={StyleConstants.Font.Size.L}
color={theme[iconFrontColor]} color={theme[iconFrontColor]}
style={styles.iconFront} style={styles.iconFront}
/> />
@ -118,7 +118,7 @@ const MenuRow: React.FC<Props> = ({
<> <>
<Icon <Icon
name={iconBack} name={iconBack}
size={StyleConstants.Font.Size.M + 2} size={StyleConstants.Font.Size.L}
color={theme[iconBackColor]} color={theme[iconBackColor]}
style={[styles.iconBack, { opacity: loading ? 0 : 1 }]} style={[styles.iconBack, { opacity: loading ? 0 : 1 }]}
/> />
@ -139,6 +139,7 @@ const styles = StyleSheet.create({
core: { core: {
flex: 1, flex: 1,
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center',
paddingLeft: StyleConstants.Spacing.Global.PagePadding, paddingLeft: StyleConstants.Spacing.Global.PagePadding,
paddingRight: StyleConstants.Spacing.Global.PagePadding paddingRight: StyleConstants.Spacing.Global.PagePadding
}, },

View File

@ -45,12 +45,9 @@ const renderNode = ({
? routeParams.hashtag !== tag[1] && routeParams.hashtag !== tag[2] ? routeParams.hashtag !== tag[1] && routeParams.hashtag !== tag[2]
: true : true
return ( return (
<Text <Pressable
key={index} key={index}
style={{ hitSlop={StyleConstants.Font.Size[size] / 2}
color: theme.blue,
...StyleConstants.FontStyle[size]
}}
onPress={() => { onPress={() => {
!disableDetails && !disableDetails &&
differentTag && differentTag &&
@ -59,9 +56,16 @@ const renderNode = ({
}) })
}} }}
> >
{node.children[0].data} <Text
{node.children[1]?.children[0].data} style={{
</Text> color: theme.blue,
...StyleConstants.FontStyle[size]
}}
>
{node.children[0].data}
{node.children[1]?.children[0].data}
</Text>
</Pressable>
) )
} else if (classes.includes('mention') && mentions) { } else if (classes.includes('mention') && mentions) {
const accountIndex = mentions.findIndex( const accountIndex = mentions.findIndex(
@ -71,12 +75,9 @@ const renderNode = ({
? routeParams.account.id !== mentions[accountIndex].id ? routeParams.account.id !== mentions[accountIndex].id
: true : true
return ( return (
<Text <Pressable
key={index} key={index}
style={{ hitSlop={StyleConstants.Font.Size[size] / 2}
color: accountIndex !== -1 ? theme.blue : undefined,
...StyleConstants.FontStyle[size]
}}
onPress={() => { onPress={() => {
accountIndex !== -1 && accountIndex !== -1 &&
!disableDetails && !disableDetails &&
@ -86,9 +87,16 @@ const renderNode = ({
}) })
}} }}
> >
{node.children[0].data} <Text
{node.children[1]?.children[0].data} style={{
</Text> color: accountIndex !== -1 ? theme.blue : undefined,
...StyleConstants.FontStyle[size]
}}
>
{node.children[0].data}
{node.children[1]?.children[0].data}
</Text>
</Pressable>
) )
} }
} else { } else {
@ -99,12 +107,9 @@ const renderNode = ({
const shouldBeTag = const shouldBeTag =
tags && tags.filter(tag => `#${tag.name}` === content).length > 0 tags && tags.filter(tag => `#${tag.name}` === content).length > 0
return ( return (
<Text <Pressable
key={index} key={index}
style={{ hitSlop={StyleConstants.Font.Size[size] / 2}
color: theme.blue,
...StyleConstants.FontStyle[size]
}}
onPress={async () => onPress={async () =>
!disableDetails && !shouldBeTag !disableDetails && !shouldBeTag
? await openLink(href) ? await openLink(href)
@ -113,15 +118,22 @@ const renderNode = ({
}) })
} }
> >
{!shouldBeTag ? ( <Text
<Icon style={{
color={theme.blue} color: theme.blue,
name='ExternalLink' ...StyleConstants.FontStyle[size]
size={StyleConstants.Font.Size[size]} }}
/> >
) : null} {!shouldBeTag ? (
{content || (showFullLink ? href : domain[1])} <Icon
</Text> color={theme.blue}
name='ExternalLink'
size={StyleConstants.Font.Size[size]}
/>
) : null}
{content || (showFullLink ? href : domain[1])}
</Text>
</Pressable>
) )
} }
break break
@ -206,7 +218,7 @@ const ParseHTML: React.FC<Props> = ({
}, []) }, [])
return ( return (
<View> <View style={{ overflow: 'hidden' }}>
<Text <Text
children={children} children={children}
onTextLayout={onTextLayout} onTextLayout={onTextLayout}
@ -222,14 +234,21 @@ const ParseHTML: React.FC<Props> = ({
layoutAnimation() layoutAnimation()
setExpanded(!expanded) setExpanded(!expanded)
}} }}
style={{ marginTop: expanded ? 0 : -lineHeight * 2.25 }} style={{
marginTop: expanded
? 0
: -lineHeight * (numberOfLines === 0 ? 1 : 2)
}}
> >
<LinearGradient <LinearGradient
colors={[ colors={[
theme.backgroundGradientStart, theme.backgroundGradientStart,
theme.backgroundGradientEnd theme.backgroundGradientEnd
]} ]}
locations={[0, lineHeight / (StyleConstants.Font.Size.S * 4)]} locations={[
0,
lineHeight / (StyleConstants.Font.Size[size] * 5)
]}
style={{ style={{
paddingTop: StyleConstants.Font.Size.S * 2, paddingTop: StyleConstants.Font.Size.S * 2,
paddingBottom: StyleConstants.Font.Size.S paddingBottom: StyleConstants.Font.Size.S

View File

@ -69,7 +69,9 @@ const Timelines: React.FC<Props> = ({ name, content }) => {
) )
return ( return (
<Stack.Navigator screenOptions={{ headerHideShadow: true }}> <Stack.Navigator
screenOptions={{ headerHideShadow: true, headerTopInsetEnabled: false }}
>
<Stack.Screen <Stack.Screen
// @ts-ignore // @ts-ignore
name={`Screen-${name}-Root`} name={`Screen-${name}-Root`}

View File

@ -9,12 +9,13 @@ import { useScrollToTop } from '@react-navigation/native'
import { localUpdateNotification } from '@utils/slices/instancesSlice' import { localUpdateNotification } from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import React, { useCallback, useEffect, useMemo, useRef } from 'react' import React, { useCallback, useEffect, useMemo, useRef } from 'react'
import { StyleSheet } from 'react-native' import { RefreshControl, StyleSheet } from 'react-native'
import { FlatList } from 'react-native-gesture-handler' import { FlatList } from 'react-native-gesture-handler'
import { useDispatch } from 'react-redux' import { useDispatch } from 'react-redux'
import { QueryKeyTimeline, useTimelineQuery } from '@utils/queryHooks/timeline' import { QueryKeyTimeline, useTimelineQuery } from '@utils/queryHooks/timeline'
import { findIndex } from 'lodash' import { findIndex } from 'lodash'
import CustomRefreshControl from '@components/CustomRefreshControl' import CustomRefreshControl from '@components/CustomRefreshControl'
import { InfiniteData, useQueryClient } from 'react-query'
export interface Props { export interface Props {
page: App.Pages page: App.Pages
@ -156,14 +157,35 @@ const Timeline: React.FC<Props> = ({
() => <TimelineEnd hasNextPage={!disableInfinity ? hasNextPage : false} />, () => <TimelineEnd hasNextPage={!disableInfinity ? hasNextPage : false} />,
[hasNextPage] [hasNextPage]
) )
const queryClient = useQueryClient()
const refreshCount = useRef(0)
const refreshControl = useMemo( const refreshControl = useMemo(
() => ( () => (
<CustomRefreshControl <RefreshControl
queryKey={queryKey} refreshing={
isFetchingPreviousPage={isFetchingNextPage} refreshCount.current < 2 ? isFetchingPreviousPage : isFetching
isFetching={isFetching} }
fetchPreviousPage={fetchPreviousPage} onRefresh={async () => {
refetch={refetch} if (refreshCount.current < 2) {
await fetchPreviousPage()
refreshCount.current++
} else {
queryClient.setQueryData<InfiniteData<any> | undefined>(
queryKey,
data => {
if (data) {
return {
pages: data.pages.slice(1),
pageParams: data.pageParams.slice(1)
}
}
}
)
await refetch()
refreshCount.current = 0
}
}}
/> />
), ),
[isFetchingPreviousPage, isFetching] [isFetchingPreviousPage, isFetching]
@ -199,10 +221,10 @@ const Timeline: React.FC<Props> = ({
{...(queryKey && {...(queryKey &&
queryKey[1].page === 'RemotePublic' && { ListHeaderComponent })} queryKey[1].page === 'RemotePublic' && { ListHeaderComponent })}
{...(toot && isSuccess && { onScrollToIndexFailed })} {...(toot && isSuccess && { onScrollToIndexFailed })}
maintainVisibleContentPosition={{ // maintainVisibleContentPosition={{
minIndexForVisible: 0, // minIndexForVisible: 0,
autoscrollToTopThreshold: 2 // autoscrollToTopThreshold: 2
}} // }}
/> />
) )
} }

View File

@ -20,7 +20,7 @@ const TimelineEnd: React.FC<Props> = ({ hasNextPage }) => {
) : ( ) : (
<Text style={[styles.text, { color: theme.secondary }]}> <Text style={[styles.text, { color: theme.secondary }]}>
<Trans <Trans
i18nKey='timeline:shared.end.message' // optional -> fallbacks to defaults if not provided i18nKey='timeline:shared.end.message'
components={[ components={[
<Icon <Icon
name='Coffee' name='Coffee'

View File

@ -11,7 +11,14 @@ import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React, { useCallback, useMemo } from 'react' import React, { useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { ActionSheetIOS, Pressable, StyleSheet, Text, View } from 'react-native' import {
Platform,
Pressable,
Share,
StyleSheet,
Text,
View
} from 'react-native'
import { useQueryClient } from 'react-query' import { useQueryClient } from 'react-query'
export interface Props { export interface Props {
@ -35,12 +42,39 @@ const TimelineActions: React.FC<Props> = ({ queryKey, status, reblog }) => {
onSuccess: (_, params) => { onSuccess: (_, params) => {
const theParams = params as MutationVarsTimelineUpdateStatusProperty const theParams = params as MutationVarsTimelineUpdateStatusProperty
if ( if (
// Un-bookmark from bookmarks page
(queryKey[1].page === 'Bookmarks' && (queryKey[1].page === 'Bookmarks' &&
theParams.payload.property === 'bookmarked') || theParams.payload.property === 'bookmarked') ||
// Un-favourite from favourites page
(queryKey[1].page === 'Favourites' && (queryKey[1].page === 'Favourites' &&
theParams.payload.property === 'favourited') theParams.payload.property === 'favourited') ||
// Un-reblog from following page
(queryKey[1].page === 'Following' &&
theParams.payload.property === 'reblogged' &&
theParams.payload.currentValue === true)
) { ) {
queryClient.invalidateQueries(queryKey) queryClient.invalidateQueries(queryKey)
} else if (theParams.payload.property === 'reblogged') {
// When reblogged, update cache of following page
const tempQueryKey: QueryKeyTimeline = [
'Timeline',
{ page: 'Following' }
]
queryClient.invalidateQueries(tempQueryKey)
} else if (theParams.payload.property === 'favourited') {
// When favourited, update favourited page
const tempQueryKey: QueryKeyTimeline = [
'Timeline',
{ page: 'Favourites' }
]
queryClient.invalidateQueries(tempQueryKey)
} else if (theParams.payload.property === 'bookmarked') {
// When bookmarked, update bookmark page
const tempQueryKey: QueryKeyTimeline = [
'Timeline',
{ page: 'Bookmarks' }
]
queryClient.invalidateQueries(tempQueryKey)
} }
}, },
onError: (err: any, params, oldData) => { onError: (err: any, params, oldData) => {
@ -115,23 +149,18 @@ const TimelineActions: React.FC<Props> = ({ queryKey, status, reblog }) => {
}), }),
[status.bookmarked] [status.bookmarked]
) )
const onPressShare = useCallback( const onPressShare = useCallback(() => {
() => switch (Platform.OS) {
ActionSheetIOS.showShareActionSheetWithOptions( case 'ios':
{ return Share.share({
url: status.uri, url: status.uri
excludedActivityTypes: [ })
'com.apple.UIKit.activity.Mail', case 'android':
'com.apple.UIKit.activity.Print', return Share.share({
'com.apple.UIKit.activity.SaveToCameraRoll', message: status.uri
'com.apple.UIKit.activity.OpenInIBooks' })
] }
}, }, [])
() => haptics('Error'),
() => haptics('Success')
),
[]
)
const childrenReply = useMemo( const childrenReply = useMemo(
() => ( () => (
@ -139,7 +168,7 @@ const TimelineActions: React.FC<Props> = ({ queryKey, status, reblog }) => {
<Icon <Icon
name='MessageCircle' name='MessageCircle'
color={iconColor} color={iconColor}
size={StyleConstants.Font.Size.M + 2} size={StyleConstants.Font.Size.L}
/> />
{status.replies_count > 0 && ( {status.replies_count > 0 && (
<Text <Text
@ -165,7 +194,7 @@ const TimelineActions: React.FC<Props> = ({ queryKey, status, reblog }) => {
? theme.disabled ? theme.disabled
: iconColorAction(status.reblogged) : iconColorAction(status.reblogged)
} }
size={StyleConstants.Font.Size.M + 2} size={StyleConstants.Font.Size.L}
/> />
), ),
[status.reblogged] [status.reblogged]
@ -175,7 +204,7 @@ const TimelineActions: React.FC<Props> = ({ queryKey, status, reblog }) => {
<Icon <Icon
name='Heart' name='Heart'
color={iconColorAction(status.favourited)} color={iconColorAction(status.favourited)}
size={StyleConstants.Font.Size.M + 2} size={StyleConstants.Font.Size.L}
/> />
), ),
[status.favourited] [status.favourited]
@ -185,18 +214,14 @@ const TimelineActions: React.FC<Props> = ({ queryKey, status, reblog }) => {
<Icon <Icon
name='Bookmark' name='Bookmark'
color={iconColorAction(status.bookmarked)} color={iconColorAction(status.bookmarked)}
size={StyleConstants.Font.Size.M + 2} size={StyleConstants.Font.Size.L}
/> />
), ),
[status.bookmarked] [status.bookmarked]
) )
const childrenShare = useMemo( const childrenShare = useMemo(
() => ( () => (
<Icon <Icon name='Share2' color={iconColor} size={StyleConstants.Font.Size.L} />
name='Share2'
color={iconColor}
size={StyleConstants.Font.Size.M + 2}
/>
), ),
[] []
) )
@ -252,6 +277,7 @@ const styles = StyleSheet.create({
width: '20%', width: '20%',
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center',
paddingVertical: StyleConstants.Spacing.S paddingVertical: StyleConstants.Spacing.S
} }
}) })

View File

@ -36,7 +36,7 @@ const HeaderActions = React.memo(
<Icon <Icon
name='MoreHorizontal' name='MoreHorizontal'
color={theme.secondary} color={theme.secondary}
size={StyleConstants.Font.Size.M + 2} size={StyleConstants.Font.Size.L}
/> />
), ),
[] []

View File

@ -65,7 +65,7 @@ const HeaderConversation: React.FC<Props> = ({ queryKey, conversation }) => {
<Icon <Icon
name='Trash' name='Trash'
color={theme.secondary} color={theme.secondary}
size={StyleConstants.Font.Size.M + 2} size={StyleConstants.Font.Size.L}
/> />
), ),
[] []

View File

@ -1,8 +1,8 @@
import relativeTime from '@components/relativeTime'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React, { useEffect, useState } from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Moment from 'react-moment'
import { StyleSheet, Text } from 'react-native' import { StyleSheet, Text } from 'react-native'
export interface Props { export interface Props {
@ -13,16 +13,10 @@ const HeaderSharedCreated: React.FC<Props> = ({ created_at }) => {
const { theme } = useTheme() const { theme } = useTheme()
const { i18n } = useTranslation() const { i18n } = useTranslation()
const [since, setSince] = useState(relativeTime(created_at, i18n.language))
useEffect(() => {
const timer = setTimeout(() => {
setSince(relativeTime(created_at, i18n.language))
}, 1000)
return () => clearTimeout(timer)
}, [since])
return ( return (
<Text style={[styles.created_at, { color: theme.secondary }]}>{since}</Text> <Text style={[styles.created_at, { color: theme.secondary }]}>
<Moment date={created_at} locale={i18n.language} element={Text} fromNow />
</Text>
) )
} }

View File

@ -10,8 +10,10 @@ import {
} from '@utils/queryHooks/timeline' } from '@utils/queryHooks/timeline'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import { maxBy } from 'lodash'
import React, { useCallback, useMemo, useState } from 'react' import React, { useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { Trans, useTranslation } from 'react-i18next'
import Moment from 'react-moment'
import { Pressable, StyleSheet, Text, View } from 'react-native' import { Pressable, StyleSheet, Text, View } from 'react-native'
import { useQueryClient } from 'react-query' import { useQueryClient } from 'react-query'
@ -123,16 +125,24 @@ const TimelinePoll: React.FC<Props> = ({
} else { } else {
return ( return (
<Text style={[styles.expiration, { color: theme.secondary }]}> <Text style={[styles.expiration, { color: theme.secondary }]}>
{t('shared.poll.meta.expiration.until', { <Trans
at: relativeTime(poll.expires_at, i18n.language) i18nKey='timeline:shared.poll.meta.expiration.until'
})} components={[
<Moment
date={poll.expires_at}
locale={i18n.language}
element={Text}
fromNow
/>
]}
/>
</Text> </Text>
) )
} }
}, [mode, poll.expired, poll.expires_at]) }, [mode, poll.expired, poll.expires_at])
const isSelected = useCallback( const isSelected = useCallback(
(index: number): any => (index: number): string =>
allOptions[index] allOptions[index]
? `Check${poll.multiple ? 'Square' : 'Circle'}` ? `Check${poll.multiple ? 'Square' : 'Circle'}`
: `${poll.multiple ? 'Square' : 'Circle'}`, : `${poll.multiple ? 'Square' : 'Circle'}`,
@ -140,6 +150,8 @@ const TimelinePoll: React.FC<Props> = ({
) )
const pollBodyDisallow = useMemo(() => { const pollBodyDisallow = useMemo(() => {
const maxValue = maxBy(poll.options, option => option.votes_count)
?.votes_count
return poll.options.map((option, index) => ( return poll.options.map((option, index) => (
<View key={index} style={styles.optionContainer}> <View key={index} style={styles.optionContainer}>
<View style={styles.optionContent}> <View style={styles.optionContent}>
@ -152,7 +164,7 @@ const TimelinePoll: React.FC<Props> = ({
} }
size={StyleConstants.Font.Size.M} size={StyleConstants.Font.Size.M}
color={ color={
poll.own_votes?.includes(index) ? theme.primary : theme.disabled poll.own_votes?.includes(index) ? theme.blue : theme.disabled
} }
/> />
<Text style={styles.optionText}> <Text style={styles.optionText}>
@ -160,7 +172,11 @@ const TimelinePoll: React.FC<Props> = ({
</Text> </Text>
<Text style={[styles.optionPercentage, { color: theme.primary }]}> <Text style={[styles.optionPercentage, { color: theme.primary }]}>
{poll.votes_count {poll.votes_count
? Math.round((option.votes_count / poll.voters_count) * 100) ? Math.round(
(option.votes_count /
(poll.voters_count || poll.votes_count)) *
100
)
: 0} : 0}
% %
</Text> </Text>
@ -171,9 +187,11 @@ const TimelinePoll: React.FC<Props> = ({
styles.background, styles.background,
{ {
width: `${Math.round( width: `${Math.round(
(option.votes_count / poll.voters_count) * 100 (option.votes_count / (poll.voters_count || poll.votes_count)) *
100
)}%`, )}%`,
backgroundColor: theme.disabled backgroundColor:
option.votes_count === maxValue ? theme.blue : theme.disabled
} }
]} ]}
/> />
@ -221,14 +239,28 @@ const TimelinePoll: React.FC<Props> = ({
)) ))
}, [mode, allOptions]) }, [mode, allOptions])
const pollVoteCounts = useMemo(() => {
if (poll.voters_count !== null) {
return (
<Text style={[styles.votes, { color: theme.secondary }]}>
{t('shared.poll.meta.count.voters', { count: poll.voters_count })}
</Text>
)
} else if (poll.votes_count !== null) {
return (
<Text style={[styles.votes, { color: theme.secondary }]}>
{t('shared.poll.meta.count.votes', { count: poll.votes_count })}
</Text>
)
}
}, [poll.voters_count, poll.votes_count])
return ( return (
<View style={styles.base}> <View style={styles.base}>
{poll.expired || poll.voted ? pollBodyDisallow : pollBodyAllow} {poll.expired || poll.voted ? pollBodyDisallow : pollBodyAllow}
<View style={styles.meta}> <View style={styles.meta}>
{pollButton} {pollButton}
<Text style={[styles.votes, { color: theme.secondary }]}> {pollVoteCounts}
{t('shared.poll.meta.voted', { count: poll.voters_count })}
</Text>
{pollExpiration} {pollExpiration}
</View> </View>
</View> </View>

View File

@ -1,27 +0,0 @@
const relativeTime = (date: string, language: string) => {
const units = {
year: 24 * 60 * 60 * 1000 * 365,
month: (24 * 60 * 60 * 1000 * 365) / 12,
day: 24 * 60 * 60 * 1000,
hour: 60 * 60 * 1000,
minute: 60 * 1000,
second: 1000
}
const rtf = new Intl.RelativeTimeFormat(language, {
numeric: 'auto'
})
const elapsed = +new Date(date) - +new Date()
// "Math.abs" accounts for both "past" & "future" scenarios
for (const u in units) {
// @ts-ignore
if (Math.abs(elapsed) > units[u] || u == 'second') {
// @ts-ignore
return rtf.format(Math.round(elapsed / units[u]), u)
}
}
}
export default relativeTime

View File

@ -13,29 +13,24 @@ import { store } from '@root/store'
if (!getSettingsLanguage(store.getState())) { if (!getSettingsLanguage(store.getState())) {
const deviceLocal = Localization.locale const deviceLocal = Localization.locale
if (deviceLocal.startsWith('zh')) { if (deviceLocal.startsWith('zh')) {
store.dispatch(changeLanguage('zh')) store.dispatch(changeLanguage('zh-CN'))
} else { } else {
store.dispatch(changeLanguage('en')) store.dispatch(changeLanguage('en-US'))
} }
} }
i18next.use(initReactI18next).init({ i18next.use(initReactI18next).init({
lng: getSettingsLanguage(store.getState()), lng: 'zh-CN',
fallbackLng: 'en', fallbackLng: 'en-US',
supportedLngs: ['zh', 'en'], supportedLngs: ['zh-CN', 'en-US'],
nonExplicitSupportedLngs: true,
ns: ['common'], ns: ['common'],
defaultNS: 'common', defaultNS: 'common',
resources: { resources: { 'zh-CN': zh, 'en-US': en },
zh: zh,
en: en
},
saveMissing: true, saveMissing: true,
missingKeyHandler: (lng, ns, key, fallbackValue) => { missingKeyHandler: (lng, ns, key, fallbackValue) => {
console.warn('i18n missing: ' + ns + ' : ' + key) console.log('i18n missing: ' + lng + ' - ' + ns + ' : ' + key)
}, },
// react options // react options

View File

@ -123,11 +123,14 @@ export default {
vote: '投票', vote: '投票',
refresh: '刷新' refresh: '刷新'
}, },
count: {
voters: '已投{{count}}人 • ',
votes: '{{count}}票 • '
},
expiration: { expiration: {
expired: '投票已结束', expired: '投票已结束',
until: '{{at}}截止' until: '<0 />截止'
}, }
voted: '已投{{count}}人 • '
} }
} }
} }

View File

@ -19,7 +19,7 @@ const ScreenMe: React.FC = () => {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<Stack.Navigator screenOptions={{ headerHideShadow: true }}> <Stack.Navigator screenOptions={{ headerHideShadow: true, headerTopInsetEnabled: false }}>
<Stack.Screen <Stack.Screen
name='Screen-Me-Root' name='Screen-Me-Root'
component={ScreenMeRoot} component={ScreenMeRoot}

View File

@ -20,17 +20,13 @@ import { StackScreenProps } from '@react-navigation/stack'
const ScreenMeRoot: React.FC<StackScreenProps< const ScreenMeRoot: React.FC<StackScreenProps<
Nav.MeStackParamList, Nav.MeStackParamList,
'Screen-Me-Root' 'Screen-Me-Root'
>> = ({ >> = ({ route: { params }, navigation }) => {
route: {
params: { navigateAway }
},
navigation
}) => {
useEffect(() => { useEffect(() => {
if (navigateAway) { if (params && params.navigateAway) {
navigation.navigate(navigateAway) console.log('oops')
navigation.navigate(params.navigateAway)
} }
}, [navigateAway]) }, [params])
const localActiveIndex = useSelector(getLocalActiveIndex) const localActiveIndex = useSelector(getLocalActiveIndex)
const scrollRef = useRef<Animated.ScrollView>(null) const scrollRef = useRef<Animated.ScrollView>(null)

View File

@ -1,5 +1,6 @@
import Button from '@components/Button' import Button from '@components/Button'
import { MenuContainer, MenuRow } from '@components/Menu' import { MenuContainer, MenuRow } from '@components/Menu'
import { useActionSheet } from '@expo/react-native-action-sheet'
import { useNavigation } from '@react-navigation/native' import { useNavigation } from '@react-navigation/native'
import haptics from '@root/components/haptics' import haptics from '@root/components/haptics'
import { persistor } from '@root/store' import { persistor } from '@root/store'
@ -23,11 +24,13 @@ import { useTheme } from '@utils/styles/ThemeManager'
import prettyBytes from 'pretty-bytes' import prettyBytes from 'pretty-bytes'
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { ActionSheetIOS, StyleSheet, Text } from 'react-native' import { StyleSheet, Text } from 'react-native'
import { CacheManager } from 'react-native-expo-image-cache' import { CacheManager } from 'react-native-expo-image-cache'
import { ScrollView } from 'react-native-gesture-handler'
import { useDispatch, useSelector } from 'react-redux' import { useDispatch, useSelector } from 'react-redux'
const DevDebug: React.FC = () => { const DevDebug: React.FC = () => {
const { showActionSheetWithOptions } = useActionSheet()
const localActiveIndex = useSelector(getLocalActiveIndex) const localActiveIndex = useSelector(getLocalActiveIndex)
const localInstances = useSelector(getLocalInstances) const localInstances = useSelector(getLocalInstances)
@ -43,7 +46,7 @@ const DevDebug: React.FC = () => {
content={localInstances.length.toString()} content={localInstances.length.toString()}
iconBack='ChevronRight' iconBack='ChevronRight'
onPress={() => onPress={() =>
ActionSheetIOS.showActionSheetWithOptions( showActionSheetWithOptions(
{ {
options: localInstances options: localInstances
.map(instance => { .map(instance => {
@ -71,6 +74,7 @@ const DevDebug: React.FC = () => {
} }
const ScreenMeSettings: React.FC = () => { const ScreenMeSettings: React.FC = () => {
const { showActionSheetWithOptions } = useActionSheet()
const navigation = useNavigation() const navigation = useNavigation()
const { t, i18n } = useTranslation('meSettings') const { t, i18n } = useTranslation('meSettings')
const { setTheme, theme } = useTheme() const { setTheme, theme } = useTheme()
@ -87,15 +91,16 @@ const ScreenMeSettings: React.FC = () => {
}, []) }, [])
return ( return (
<> <ScrollView>
<MenuContainer> <MenuContainer>
<MenuRow <MenuRow
title={t('content.language.heading')} title={t('content.language.heading')}
content={t(`content.language.options.${settingsLanguage}`)} content={t(`content.language.options.${settingsLanguage}`)}
iconBack='ChevronRight' iconBack='ChevronRight'
onPress={() => onPress={() =>
ActionSheetIOS.showActionSheetWithOptions( showActionSheetWithOptions(
{ {
title: t('content.language.heading'),
options: [ options: [
t('content.language.options.zh'), t('content.language.options.zh'),
t('content.language.options.en'), t('content.language.options.en'),
@ -107,13 +112,13 @@ const ScreenMeSettings: React.FC = () => {
switch (buttonIndex) { switch (buttonIndex) {
case 0: case 0:
haptics('Success') haptics('Success')
dispatch(changeLanguage('zh')) dispatch(changeLanguage('zh-CN'))
i18n.changeLanguage('zh') i18n.changeLanguage('zh-CN')
break break
case 1: case 1:
haptics('Success') haptics('Success')
dispatch(changeLanguage('en')) dispatch(changeLanguage('en-US'))
i18n.changeLanguage('en') i18n.changeLanguage('en-US')
break break
} }
} }
@ -125,8 +130,9 @@ const ScreenMeSettings: React.FC = () => {
content={t(`content.theme.options.${settingsTheme}`)} content={t(`content.theme.options.${settingsTheme}`)}
iconBack='ChevronRight' iconBack='ChevronRight'
onPress={() => onPress={() =>
ActionSheetIOS.showActionSheetWithOptions( showActionSheetWithOptions(
{ {
title: t('content.theme.heading'),
options: [ options: [
t('content.theme.options.auto'), t('content.theme.options.auto'),
t('content.theme.options.light'), t('content.theme.options.light'),
@ -161,8 +167,9 @@ const ScreenMeSettings: React.FC = () => {
content={t(`content.browser.options.${settingsBrowser}`)} content={t(`content.browser.options.${settingsBrowser}`)}
iconBack='ChevronRight' iconBack='ChevronRight'
onPress={() => onPress={() =>
ActionSheetIOS.showActionSheetWithOptions( showActionSheetWithOptions(
{ {
title: t('content.browser.heading'),
options: [ options: [
t('content.browser.options.internal'), t('content.browser.options.internal'),
t('content.browser.options.external'), t('content.browser.options.external'),
@ -226,7 +233,7 @@ const ScreenMeSettings: React.FC = () => {
</MenuContainer> </MenuContainer>
{__DEV__ ? <DevDebug /> : null} {__DEV__ ? <DevDebug /> : null}
</> </ScrollView>
) )
} }

View File

@ -8,7 +8,7 @@ const Stack = createNativeStackNavigator()
const ScreenMeSwitch: React.FC = ({ navigation }) => { const ScreenMeSwitch: React.FC = ({ navigation }) => {
return ( return (
<Stack.Navigator screenOptions={{ headerHideShadow: true }}> <Stack.Navigator screenOptions={{ headerHideShadow: true, headerTopInsetEnabled: false }}>
<Stack.Screen <Stack.Screen
name='Screen-Me-Switch-Root' name='Screen-Me-Switch-Root'
component={ScreenMeSwitchRoot} component={ScreenMeSwitchRoot}

View File

@ -16,7 +16,8 @@ const ScreenNotifications: React.FC = () => {
<Stack.Navigator <Stack.Navigator
screenOptions={{ screenOptions={{
headerTitle: t('notifications:heading'), headerTitle: t('notifications:heading'),
headerHideShadow: true headerHideShadow: true,
headerTopInsetEnabled: false
}} }}
> >
<Stack.Screen name='Screen-Notifications-Root'> <Stack.Screen name='Screen-Notifications-Root'>

View File

@ -1,5 +1,4 @@
import Timelines from '@components/Timelines' import Timelines from '@components/Timelines'
import { getRemoteUrl } from '@utils/slices/instancesSlice'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'

View File

@ -73,19 +73,24 @@ const AccountInformation: React.FC<Props> = ({
<AccountInformationAccount ref={shimmerAccountRef} account={account} /> <AccountInformationAccount ref={shimmerAccountRef} account={account} />
{account?.fields && account.fields.length > 0 ? ( {!ownAccount ? (
<AccountInformationFields account={account} /> <>
{account?.fields && account.fields.length > 0 ? (
<AccountInformationFields account={account} />
) : null}
{account?.note &&
account.note.length > 0 &&
account.note !== '<p></p>' ? (
// Empty notes might generate empty p tag
<AccountInformationNotes account={account} />
) : null}
<AccountInformationCreated
ref={shimmerCreatedRef}
account={account}
/>
</>
) : null} ) : null}
{account?.note &&
account.note.length > 0 &&
account.note !== '<p></p>' ? (
// Empty notes might generate empty p tag
<AccountInformationNotes account={account} />
) : null}
<AccountInformationCreated ref={shimmerCreatedRef} account={account} />
<AccountInformationStats ref={shimmerStatsRef} account={account} /> <AccountInformationStats ref={shimmerStatsRef} account={account} />
</View> </View>
) )

View File

@ -1,8 +1,6 @@
import client from '@api/client'
import Button from '@components/Button' import Button from '@components/Button'
import haptics from '@components/haptics' import haptics from '@components/haptics'
import { ParseHTML } from '@components/Parse' import { ParseHTML } from '@components/Parse'
import relativeTime from '@components/relativeTime'
import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs' import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs'
import { import {
useAnnouncementMutation, useAnnouncementMutation,
@ -12,6 +10,7 @@ import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React, { useCallback, useEffect, useState } from 'react' import React, { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Moment from 'react-moment'
import { import {
Dimensions, Dimensions,
Image, Image,
@ -25,33 +24,6 @@ import { FlatList, ScrollView } from 'react-native-gesture-handler'
import { SafeAreaView } from 'react-native-safe-area-context' import { SafeAreaView } from 'react-native-safe-area-context'
import { SharedAnnouncementsProp } from './sharedScreens' import { SharedAnnouncementsProp } from './sharedScreens'
const fireMutation = async ({
announcementId,
type,
name,
me
}: {
announcementId: Mastodon.Announcement['id']
type: 'reaction' | 'dismiss'
name?: Mastodon.AnnouncementReaction['name']
me?: boolean
}) => {
switch (type) {
case 'reaction':
return client<{}>({
method: me ? 'delete' : 'put',
instance: 'local',
url: `announcements/${announcementId}/reactions/${name}`
})
case 'dismiss':
return client<{}>({
method: 'post',
instance: 'local',
url: `announcements/${announcementId}/dismiss`
})
}
}
const ScreenSharedAnnouncements: React.FC<SharedAnnouncementsProp> = ({ const ScreenSharedAnnouncements: React.FC<SharedAnnouncementsProp> = ({
route: { route: {
params: { showAll = false } params: { showAll = false }
@ -108,7 +80,13 @@ const ScreenSharedAnnouncements: React.FC<SharedAnnouncementsProp> = ({
]} ]}
> >
<Text style={[styles.published, { color: theme.secondary }]}> <Text style={[styles.published, { color: theme.secondary }]}>
{relativeTime(item.published_at, i18n.language)} {' '}
<Moment
date={item.published_at}
locale={i18n.language}
element={Text}
fromNow
/>
</Text> </Text>
<ScrollView style={styles.scrollView} showsVerticalScrollIndicator> <ScrollView style={styles.scrollView} showsVerticalScrollIndicator>
<ParseHTML <ParseHTML

View File

@ -214,7 +214,7 @@ const Compose: React.FC<SharedComposeProp> = ({
edges={hasKeyboard ? ['left', 'right'] : ['left', 'right', 'bottom']} edges={hasKeyboard ? ['left', 'right'] : ['left', 'right', 'bottom']}
> >
<ComposeContext.Provider value={{ composeState, composeDispatch }}> <ComposeContext.Provider value={{ composeState, composeDispatch }}>
<Stack.Navigator> <Stack.Navigator screenOptions={{ headerTopInsetEnabled: false }}>
<Stack.Screen <Stack.Screen
name='Screen-Shared-Compose-Root' name='Screen-Shared-Compose-Root'
component={ComposeRoot} component={ComposeRoot}

View File

@ -1,13 +1,15 @@
import Icon from '@components/Icon' import Icon from '@components/Icon'
import { useActionSheet } from '@expo/react-native-action-sheet'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import layoutAnimation from '@utils/styles/layoutAnimation' import layoutAnimation from '@utils/styles/layoutAnimation'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React, { useCallback, useContext, useMemo } from 'react' import React, { useCallback, useContext, useMemo } from 'react'
import { ActionSheetIOS, Pressable, StyleSheet, View } from 'react-native' import { Pressable, StyleSheet, View } from 'react-native'
import addAttachment from './/addAttachment' import addAttachment from './/addAttachment'
import ComposeContext from './utils/createContext' import ComposeContext from './utils/createContext'
const ComposeActions: React.FC = () => { const ComposeActions: React.FC = () => {
const { showActionSheetWithOptions } = useActionSheet()
const { composeState, composeDispatch } = useContext(ComposeContext) const { composeState, composeDispatch } = useContext(ComposeContext)
const { theme } = useTheme() const { theme } = useTheme()
@ -24,7 +26,10 @@ const ComposeActions: React.FC = () => {
if (composeState.poll.active) return if (composeState.poll.active) return
if (composeState.attachments.uploads.length < 4) { if (composeState.attachments.uploads.length < 4) {
return await addAttachment({ composeDispatch }) return await addAttachment({
composeDispatch,
showActionSheetWithOptions
})
} }
}, [composeState.poll.active, composeState.attachments.uploads]) }, [composeState.poll.active, composeState.attachments.uploads])
@ -64,7 +69,7 @@ const ComposeActions: React.FC = () => {
}, [composeState.visibility]) }, [composeState.visibility])
const visibilityOnPress = useCallback(() => { const visibilityOnPress = useCallback(() => {
if (!composeState.visibilityLock) { if (!composeState.visibilityLock) {
ActionSheetIOS.showActionSheetWithOptions( showActionSheetWithOptions(
{ {
options: ['公开', '不公开', '仅关注着', '私信', '取消'], options: ['公开', '不公开', '仅关注着', '私信', '取消'],
cancelButtonIndex: 4 cancelButtonIndex: 4

View File

@ -1,6 +1,7 @@
import Button from '@components/Button' import Button from '@components/Button'
import haptics from '@components/haptics' import haptics from '@components/haptics'
import Icon from '@components/Icon' import Icon from '@components/Icon'
import { useActionSheet } from '@expo/react-native-action-sheet'
import { useNavigation } from '@react-navigation/native' import { useNavigation } from '@react-navigation/native'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import layoutAnimation from '@utils/styles/layoutAnimation' import layoutAnimation from '@utils/styles/layoutAnimation'
@ -28,6 +29,7 @@ import { ExtendedAttachment } from './utils/types'
const DEFAULT_HEIGHT = 200 const DEFAULT_HEIGHT = 200
const ComposeAttachments: React.FC = () => { const ComposeAttachments: React.FC = () => {
const { showActionSheetWithOptions } = useActionSheet()
const { composeState, composeDispatch } = useContext(ComposeContext) const { composeState, composeDispatch } = useContext(ComposeContext)
const { theme } = useTheme() const { theme } = useTheme()
const navigation = useNavigation() const navigation = useNavigation()
@ -192,7 +194,9 @@ const ComposeAttachments: React.FC = () => {
backgroundColor: theme.backgroundOverlay backgroundColor: theme.backgroundOverlay
} }
]} ]}
onPress={async () => await addAttachment({ composeDispatch })} onPress={async () =>
await addAttachment({ composeDispatch, showActionSheetWithOptions })
}
> >
<Button <Button
type='icon' type='icon'
@ -200,7 +204,9 @@ const ComposeAttachments: React.FC = () => {
spacing='M' spacing='M'
round round
overlay overlay
onPress={async () => await addAttachment({ composeDispatch })} onPress={async () =>
await addAttachment({ composeDispatch, showActionSheetWithOptions })
}
style={{ style={{
position: 'absolute', position: 'absolute',
top: top:

View File

@ -146,7 +146,7 @@ const ComposeEditAttachment: React.FC<Props> = ({
return ( return (
<KeyboardAvoidingView behavior='padding' style={{ flex: 1 }}> <KeyboardAvoidingView behavior='padding' style={{ flex: 1 }}>
<SafeAreaView style={{ flex: 1 }} edges={['left', 'right', 'bottom']}> <SafeAreaView style={{ flex: 1 }} edges={['left', 'right', 'bottom']}>
<Stack.Navigator> <Stack.Navigator screenOptions={{ headerTopInsetEnabled: false }}>
<Stack.Screen <Stack.Screen
name='Screen-Shared-Compose-EditAttachment-Root' name='Screen-Shared-Compose-EditAttachment-Root'
children={children} children={children}

View File

@ -65,7 +65,10 @@ const ComposeEditAttachmentRoot: React.FC<Props> = ({
</Text> </Text>
<TextInput <TextInput
style={[styles.altTextInput, { borderColor: theme.border }]} style={[
styles.altTextInput,
{ borderColor: theme.border, color: theme.primary }
]}
autoCapitalize='none' autoCapitalize='none'
autoCorrect={false} autoCorrect={false}
maxLength={1500} maxLength={1500}

View File

@ -1,13 +1,15 @@
import Button from '@components/Button' import Button from '@components/Button'
import Icon from '@components/Icon' import Icon from '@components/Icon'
import { MenuRow } from '@components/Menu' import { MenuRow } from '@components/Menu'
import { useActionSheet } from '@expo/react-native-action-sheet'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React, { useContext, useEffect, useState } from 'react' import React, { useContext, useEffect, useState } from 'react'
import { ActionSheetIOS, StyleSheet, TextInput, View } from 'react-native' import { StyleSheet, TextInput, View } from 'react-native'
import ComposeContext from './utils/createContext' import ComposeContext from './utils/createContext'
const ComposePoll: React.FC = () => { const ComposePoll: React.FC = () => {
const { showActionSheetWithOptions } = useActionSheet()
const { const {
composeState: { composeState: {
poll: { total, options, multiple, expire } poll: { total, options, multiple, expire }
@ -111,7 +113,7 @@ const ComposePoll: React.FC = () => {
title='可选项' title='可选项'
content={multiple ? '多选' : '单选'} content={multiple ? '多选' : '单选'}
onPress={() => onPress={() =>
ActionSheetIOS.showActionSheetWithOptions( showActionSheetWithOptions(
{ {
options: ['单选', '多选', '取消'], options: ['单选', '多选', '取消'],
cancelButtonIndex: 2 cancelButtonIndex: 2
@ -130,7 +132,7 @@ const ComposePoll: React.FC = () => {
title='有效期' title='有效期'
content={expireMapping[expire]} content={expireMapping[expire]}
onPress={() => onPress={() =>
ActionSheetIOS.showActionSheetWithOptions( showActionSheetWithOptions(
{ {
options: [...Object.values(expireMapping), '取消'], options: [...Object.values(expireMapping), '取消'],
cancelButtonIndex: 7 cancelButtonIndex: 7

View File

@ -4,14 +4,16 @@ import * as Crypto from 'expo-crypto'
import { ImageInfo } from 'expo-image-picker/build/ImagePicker.types' import { ImageInfo } from 'expo-image-picker/build/ImagePicker.types'
import * as VideoThumbnails from 'expo-video-thumbnails' import * as VideoThumbnails from 'expo-video-thumbnails'
import { Dispatch } from 'react' import { Dispatch } from 'react'
import { ActionSheetIOS, Alert, Linking } from 'react-native' import { Alert, Linking } from 'react-native'
import { ComposeAction } from './utils/types' import { ComposeAction } from './utils/types'
import { ActionSheetOptions } from '@expo/react-native-action-sheet'
export interface Props { export interface Props {
composeDispatch: Dispatch<ComposeAction> composeDispatch: Dispatch<ComposeAction>
showActionSheetWithOptions: (options: ActionSheetOptions, callback: (i: number) => void) => void
} }
const addAttachment = async ({ composeDispatch }: Props): Promise<any> => { const addAttachment = async ({ composeDispatch, showActionSheetWithOptions }: Props): Promise<any> => {
const uploadAttachment = async (result: ImageInfo) => { const uploadAttachment = async (result: ImageInfo) => {
const hash = await Crypto.digestStringAsync( const hash = await Crypto.digestStringAsync(
Crypto.CryptoDigestAlgorithm.SHA256, Crypto.CryptoDigestAlgorithm.SHA256,
@ -106,7 +108,7 @@ const addAttachment = async ({ composeDispatch }: Props): Promise<any> => {
}) })
} }
ActionSheetIOS.showActionSheetWithOptions( showActionSheetWithOptions(
{ {
options: ['从相册选取', '现照', '取消'], options: ['从相册选取', '现照', '取消'],
cancelButtonIndex: 2 cancelButtonIndex: 2

View File

@ -1,9 +1,14 @@
import haptics from '@components/haptics'
import { HeaderLeft, HeaderRight } from '@components/Header' import { HeaderLeft, HeaderRight } from '@components/Header'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { findIndex } from 'lodash' import { findIndex } from 'lodash'
import React, { useCallback, useState } from 'react' import React, { useCallback, useState } from 'react'
import { ActionSheetIOS, Image, StyleSheet, Text } from 'react-native' import {
Image,
Platform,
Share,
StyleSheet,
Text
} from 'react-native'
import ImageViewer from 'react-native-image-zoom-viewer' import ImageViewer from 'react-native-image-zoom-viewer'
import { IImageInfo } from 'react-native-image-zoom-viewer/built/image-viewer.type' import { IImageInfo } from 'react-native-image-zoom-viewer/built/image-viewer.type'
import { useSafeAreaInsets } from 'react-native-safe-area-context' import { useSafeAreaInsets } from 'react-native-safe-area-context'
@ -69,8 +74,18 @@ const ScreenSharedImagesViewer: React.FC<SharedImagesViewerProp> = ({
), ),
[] []
) )
const onPress = useCallback(() => {
switch (Platform.OS) {
case 'ios':
return Share.share({ url: imageUrls[currentIndex].url })
case 'android':
return Share.share({ message: imageUrls[currentIndex].url })
}
}, [currentIndex])
return ( return (
<Stack.Navigator screenOptions={{ headerHideShadow: true }}> <Stack.Navigator screenOptions={{ headerHideShadow: true, headerTopInsetEnabled: false }}>
<Stack.Screen <Stack.Screen
name='Screen-Shared-ImagesViewer-Root' name='Screen-Shared-ImagesViewer-Root'
component={component} component={component}
@ -85,20 +100,7 @@ const ScreenSharedImagesViewer: React.FC<SharedImagesViewerProp> = ({
{currentIndex + 1} / {imageUrls.length} {currentIndex + 1} / {imageUrls.length}
</Text> </Text>
), ),
headerRight: () => ( headerRight: () => <HeaderRight content='Share' onPress={onPress} />
<HeaderRight
content='Share'
onPress={() =>
ActionSheetIOS.showShareActionSheetWithOptions(
{
url: imageUrls[currentIndex].url
},
() => haptics('Error'),
() => haptics('Success')
)
}
/>
)
}} }}
/> />
</Stack.Navigator> </Stack.Navigator>

View File

@ -6,12 +6,12 @@ const dev = () => {
if (__DEV__) { if (__DEV__) {
Analytics.setDebugModeEnabled(true) Analytics.setDebugModeEnabled(true)
log('log', 'devs', 'initializing wdyr') // log('log', 'devs', 'initializing wdyr')
const whyDidYouRender = require('@welldone-software/why-did-you-render') // const whyDidYouRender = require('@welldone-software/why-did-you-render')
whyDidYouRender(React, { // whyDidYouRender(React, {
trackHooks: true, // trackHooks: true,
hotReloadBufferMs: 1000 // hotReloadBufferMs: 1000
}) // })
} }
} }

View File

@ -3,7 +3,7 @@ import { RootState } from '@root/store'
import * as Analytics from 'expo-firebase-analytics' import * as Analytics from 'expo-firebase-analytics'
export type SettingsState = { export type SettingsState = {
language: 'zh' | 'en' | undefined language: 'zh-CN' | 'en-US' | undefined
theme: 'light' | 'dark' | 'auto' theme: 'light' | 'dark' | 'auto'
browser: 'internal' | 'external' browser: 'internal' | 'external'
analytics: boolean analytics: boolean

View File

@ -1115,6 +1115,14 @@
"@expo/config" "3.3.22" "@expo/config" "3.3.22"
metro-react-native-babel-transformer "^0.58.0" metro-react-native-babel-transformer "^0.58.0"
"@expo/react-native-action-sheet@^3.8.0":
version "3.8.0"
resolved "https://registry.yarnpkg.com/@expo/react-native-action-sheet/-/react-native-action-sheet-3.8.0.tgz#0db8b70ea8550ceb2983abda8584efa3a61d7389"
integrity sha512-tCfwysuqy0sfaN+aA98IKUrwCLKsbDHSYLcnHrx9wNbawOHNez8rSeFtieAS48/HyrPI75yg/ZGvxe6UsJRS8Q==
dependencies:
"@types/hoist-non-react-statics" "^3.3.1"
hoist-non-react-statics "^3.3.0"
"@expo/spawn-async@^1.2.8": "@expo/spawn-async@^1.2.8":
version "1.5.0" version "1.5.0"
resolved "https://registry.yarnpkg.com/@expo/spawn-async/-/spawn-async-1.5.0.tgz#799827edd8c10ef07eb1a2ff9dcfe081d596a395" resolved "https://registry.yarnpkg.com/@expo/spawn-async/-/spawn-async-1.5.0.tgz#799827edd8c10ef07eb1a2ff9dcfe081d596a395"
@ -2091,7 +2099,7 @@
resolved "https://registry.yarnpkg.com/@types/hammerjs/-/hammerjs-2.0.36.tgz#17ce0a235e9ffbcdcdf5095646b374c2bf615a4c" resolved "https://registry.yarnpkg.com/@types/hammerjs/-/hammerjs-2.0.36.tgz#17ce0a235e9ffbcdcdf5095646b374c2bf615a4c"
integrity sha512-7TUK/k2/QGpEAv/BCwSHlYu3NXZhQ9ZwBYpzr9tjlPIL2C5BeGhH3DmVavRx3ZNyELX5TLC91JTz/cen6AAtIQ== integrity sha512-7TUK/k2/QGpEAv/BCwSHlYu3NXZhQ9ZwBYpzr9tjlPIL2C5BeGhH3DmVavRx3ZNyELX5TLC91JTz/cen6AAtIQ==
"@types/hoist-non-react-statics@^3.3.0": "@types/hoist-non-react-statics@^3.3.0", "@types/hoist-non-react-statics@^3.3.1":
version "3.3.1" version "3.3.1"
resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f" resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f"
integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA== integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==
@ -7307,6 +7315,11 @@ mkdirp@^1.0.3:
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
moment@^2.29.1:
version "2.29.1"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3"
integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==
ms@2.0.0: ms@2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
@ -8173,6 +8186,11 @@ react-is@^17.0.1:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.1.tgz#5b3531bd76a645a4c9fb6e693ed36419e3301339" resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.1.tgz#5b3531bd76a645a4c9fb6e693ed36419e3301339"
integrity sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA== integrity sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA==
react-moment@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/react-moment/-/react-moment-1.1.1.tgz#5fe9fb257039590c804e2b3aedfc3ceb0a6ffb16"
integrity sha512-WjwvxBSnmLMRcU33do0KixDB+9vP3e84eCse+rd+HNklAMNWyRgZTDEQlay/qK6lcXFPRuEIASJTpEt6pyK7Ww==
react-native-animated-spinkit@^1.4.2: react-native-animated-spinkit@^1.4.2:
version "1.4.2" version "1.4.2"
resolved "https://registry.yarnpkg.com/react-native-animated-spinkit/-/react-native-animated-spinkit-1.4.2.tgz#cb60ff8bcc2bb848409d9aa85ed528646ad1f953" resolved "https://registry.yarnpkg.com/react-native-animated-spinkit/-/react-native-animated-spinkit-1.4.2.tgz#cb60ff8bcc2bb848409d9aa85ed528646ad1f953"