mirror of
https://github.com/tooot-app/app
synced 2025-06-05 22:19:13 +02:00
More adaption to Android
This commit is contained in:
@ -1,4 +1,5 @@
|
||||
import HeaderLeft from '@components/Header/Left'
|
||||
import HeaderCenter from '@components/Header/Center'
|
||||
import HeaderRight from '@components/Header/Right'
|
||||
|
||||
export { HeaderLeft, HeaderRight }
|
||||
export { HeaderLeft, HeaderCenter, HeaderRight }
|
||||
|
31
src/components/Header/Center.tsx
Normal file
31
src/components/Header/Center.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React from 'react'
|
||||
import { StyleSheet, Text } from 'react-native'
|
||||
|
||||
export interface Props {
|
||||
content: string
|
||||
}
|
||||
|
||||
// Used for Android mostly
|
||||
const HeaderCenter = React.memo(
|
||||
({ content }: Props) => {
|
||||
const { theme } = useTheme()
|
||||
|
||||
return (
|
||||
<Text
|
||||
style={[styles.text, { color: theme.primary }]}
|
||||
children={content}
|
||||
/>
|
||||
)
|
||||
},
|
||||
() => true
|
||||
)
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
text: {
|
||||
...StyleConstants.FontStyle.M
|
||||
}
|
||||
})
|
||||
|
||||
export default HeaderCenter
|
@ -45,9 +45,12 @@ const renderNode = ({
|
||||
? routeParams.hashtag !== tag[1] && routeParams.hashtag !== tag[2]
|
||||
: true
|
||||
return (
|
||||
<Pressable
|
||||
<Text
|
||||
key={index}
|
||||
hitSlop={StyleConstants.Font.Size[size] / 2}
|
||||
style={{
|
||||
color: theme.blue,
|
||||
...StyleConstants.FontStyle[size]
|
||||
}}
|
||||
onPress={() => {
|
||||
!disableDetails &&
|
||||
differentTag &&
|
||||
@ -56,16 +59,9 @@ const renderNode = ({
|
||||
})
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.blue,
|
||||
...StyleConstants.FontStyle[size]
|
||||
}}
|
||||
>
|
||||
{node.children[0].data}
|
||||
{node.children[1]?.children[0].data}
|
||||
</Text>
|
||||
</Pressable>
|
||||
{node.children[0].data}
|
||||
{node.children[1]?.children[0].data}
|
||||
</Text>
|
||||
)
|
||||
} else if (classes.includes('mention') && mentions) {
|
||||
const accountIndex = mentions.findIndex(
|
||||
@ -75,9 +71,12 @@ const renderNode = ({
|
||||
? routeParams.account.id !== mentions[accountIndex].id
|
||||
: true
|
||||
return (
|
||||
<Pressable
|
||||
<Text
|
||||
key={index}
|
||||
hitSlop={StyleConstants.Font.Size[size] / 2}
|
||||
style={{
|
||||
color: accountIndex !== -1 ? theme.blue : undefined,
|
||||
...StyleConstants.FontStyle[size]
|
||||
}}
|
||||
onPress={() => {
|
||||
accountIndex !== -1 &&
|
||||
!disableDetails &&
|
||||
@ -87,16 +86,9 @@ const renderNode = ({
|
||||
})
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color: accountIndex !== -1 ? theme.blue : undefined,
|
||||
...StyleConstants.FontStyle[size]
|
||||
}}
|
||||
>
|
||||
{node.children[0].data}
|
||||
{node.children[1]?.children[0].data}
|
||||
</Text>
|
||||
</Pressable>
|
||||
{node.children[0].data}
|
||||
{node.children[1]?.children[0].data}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
} else {
|
||||
@ -107,9 +99,12 @@ const renderNode = ({
|
||||
const shouldBeTag =
|
||||
tags && tags.filter(tag => `#${tag.name}` === content).length > 0
|
||||
return (
|
||||
<Pressable
|
||||
<Text
|
||||
key={index}
|
||||
hitSlop={StyleConstants.Font.Size[size] / 2}
|
||||
style={{
|
||||
color: theme.blue,
|
||||
...StyleConstants.FontStyle[size]
|
||||
}}
|
||||
onPress={async () =>
|
||||
!disableDetails && !shouldBeTag
|
||||
? await openLink(href)
|
||||
@ -118,22 +113,15 @@ const renderNode = ({
|
||||
})
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.blue,
|
||||
...StyleConstants.FontStyle[size]
|
||||
}}
|
||||
>
|
||||
{!shouldBeTag ? (
|
||||
<Icon
|
||||
color={theme.blue}
|
||||
name='ExternalLink'
|
||||
size={StyleConstants.Font.Size[size]}
|
||||
/>
|
||||
) : null}
|
||||
{content || (showFullLink ? href : domain[1])}
|
||||
</Text>
|
||||
</Pressable>
|
||||
{!shouldBeTag ? (
|
||||
<Icon
|
||||
color={theme.blue}
|
||||
name='ExternalLink'
|
||||
size={StyleConstants.Font.Size[size]}
|
||||
/>
|
||||
) : null}
|
||||
{content || (showFullLink ? href : domain[1])}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
break
|
||||
|
26
src/components/RelativeTime.tsx
Normal file
26
src/components/RelativeTime.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Text } from 'react-native'
|
||||
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])
|
||||
|
||||
return <TimeAgo date={date} formatter={formatter} component={Text} />
|
||||
}
|
||||
|
||||
export default RelativeTime
|
@ -1,12 +1,12 @@
|
||||
import { HeaderRight } from '@components/Header'
|
||||
import { HeaderCenter, HeaderRight } from '@components/Header'
|
||||
import Timeline from '@components/Timelines/Timeline'
|
||||
import SegmentedControl from '@react-native-community/segmented-control'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import sharedScreens from '@screens/Shared/sharedScreens'
|
||||
import { getLocalActiveIndex, getRemoteUrl } from '@utils/slices/instancesSlice'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { Dimensions, StyleSheet, View } from 'react-native'
|
||||
import React, { useCallback, useMemo, useState } from 'react'
|
||||
import { Dimensions, Platform, StyleSheet, View } from 'react-native'
|
||||
import { createNativeStackNavigator } from 'react-native-screens/native-stack'
|
||||
import { TabView } from 'react-native-tab-view'
|
||||
import { useSelector } from 'react-redux'
|
||||
@ -68,6 +68,40 @@ const Timelines: React.FC<Props> = ({ name, content }) => {
|
||||
[segment, localActiveIndex]
|
||||
)
|
||||
|
||||
const screenOptions = useMemo(() => {
|
||||
if (localActiveIndex === null) {
|
||||
if (name === 'Public') {
|
||||
return {
|
||||
headerTitle: publicDomain,
|
||||
...(Platform.OS === 'android' && {
|
||||
headerCenter: () => <HeaderCenter content={publicDomain} />
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
headerCenter: () => (
|
||||
<View style={styles.segmentsContainer}>
|
||||
<SegmentedControl
|
||||
appearance={mode}
|
||||
values={[
|
||||
content[0].title,
|
||||
content[1].remote ? remoteUrl : content[1].title
|
||||
]}
|
||||
selectedIndex={segment}
|
||||
onChange={({ nativeEvent }) =>
|
||||
setSegment(nativeEvent.selectedSegmentIndex)
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
),
|
||||
headerRight: () => (
|
||||
<HeaderRight content='Search' onPress={onPressSearch} />
|
||||
)
|
||||
}
|
||||
}
|
||||
}, [localActiveIndex, mode, segment])
|
||||
|
||||
return (
|
||||
<Stack.Navigator
|
||||
screenOptions={{ headerHideShadow: true, headerTopInsetEnabled: false }}
|
||||
@ -76,29 +110,7 @@ const Timelines: React.FC<Props> = ({ name, content }) => {
|
||||
// @ts-ignore
|
||||
name={`Screen-${name}-Root`}
|
||||
component={screenComponent}
|
||||
options={{
|
||||
headerTitle: name === 'Public' ? publicDomain : '',
|
||||
...(localActiveIndex !== null && {
|
||||
headerCenter: () => (
|
||||
<View style={styles.segmentsContainer}>
|
||||
<SegmentedControl
|
||||
appearance={mode}
|
||||
values={[
|
||||
content[0].title,
|
||||
content[1].remote ? remoteUrl : content[1].title
|
||||
]}
|
||||
selectedIndex={segment}
|
||||
onChange={({ nativeEvent }) =>
|
||||
setSegment(nativeEvent.selectedSegmentIndex)
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
),
|
||||
headerRight: () => (
|
||||
<HeaderRight content='Search' onPress={onPressSearch} />
|
||||
)
|
||||
})
|
||||
}}
|
||||
options={screenOptions}
|
||||
/>
|
||||
|
||||
{sharedScreens(Stack)}
|
||||
|
@ -9,13 +9,14 @@ import { useScrollToTop } from '@react-navigation/native'
|
||||
import { localUpdateNotification } from '@utils/slices/instancesSlice'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import React, { useCallback, useEffect, useMemo, useRef } from 'react'
|
||||
import { RefreshControl, StyleSheet } from 'react-native'
|
||||
import { Platform, RefreshControl, StyleSheet } from 'react-native'
|
||||
import { FlatList } from 'react-native-gesture-handler'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import { QueryKeyTimeline, useTimelineQuery } from '@utils/queryHooks/timeline'
|
||||
import { findIndex } from 'lodash'
|
||||
import CustomRefreshControl from '@components/CustomRefreshControl'
|
||||
import { InfiniteData, useQueryClient } from 'react-query'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
|
||||
export interface Props {
|
||||
page: App.Pages
|
||||
@ -36,6 +37,8 @@ const Timeline: React.FC<Props> = ({
|
||||
disableRefresh = false,
|
||||
disableInfinity = false
|
||||
}) => {
|
||||
const { theme } = useTheme()
|
||||
|
||||
const queryKeyParams = {
|
||||
page,
|
||||
...(hashtag && { hashtag }),
|
||||
@ -163,8 +166,13 @@ const Timeline: React.FC<Props> = ({
|
||||
const refreshControl = useMemo(
|
||||
() => (
|
||||
<RefreshControl
|
||||
{...(Platform.OS === 'android' && { enabled: true })}
|
||||
refreshing={
|
||||
refreshCount.current < 2 ? isFetchingPreviousPage : isFetching
|
||||
refreshCount.current < 2
|
||||
? Platform.OS === 'ios'
|
||||
? isFetchingPreviousPage
|
||||
: isFetchingPreviousPage || isFetching
|
||||
: isFetching
|
||||
}
|
||||
onRefresh={async () => {
|
||||
if (refreshCount.current < 2) {
|
||||
|
@ -1,8 +1,8 @@
|
||||
import RelativeTime from '@components/RelativeTime'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Moment from 'react-moment'
|
||||
import { StyleSheet, Text } from 'react-native'
|
||||
|
||||
export interface Props {
|
||||
@ -15,7 +15,7 @@ const HeaderSharedCreated: React.FC<Props> = ({ created_at }) => {
|
||||
|
||||
return (
|
||||
<Text style={[styles.created_at, { color: theme.secondary }]}>
|
||||
<Moment date={created_at} locale={i18n.language} element={Text} fromNow />
|
||||
<RelativeTime date={created_at} />
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import Button from '@components/Button'
|
||||
import haptics from '@components/haptics'
|
||||
import Icon from '@components/Icon'
|
||||
import relativeTime from '@components/relativeTime'
|
||||
import RelativeTime from '@components/RelativeTime'
|
||||
import { ParseEmojis } from '@root/components/Parse'
|
||||
import { toast } from '@root/components/toast'
|
||||
import {
|
||||
@ -13,7 +13,6 @@ import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import { maxBy } from 'lodash'
|
||||
import React, { useCallback, useMemo, useState } from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import Moment from 'react-moment'
|
||||
import { Pressable, StyleSheet, Text, View } from 'react-native'
|
||||
import { useQueryClient } from 'react-query'
|
||||
|
||||
@ -127,14 +126,7 @@ const TimelinePoll: React.FC<Props> = ({
|
||||
<Text style={[styles.expiration, { color: theme.secondary }]}>
|
||||
<Trans
|
||||
i18nKey='timeline:shared.poll.meta.expiration.until'
|
||||
components={[
|
||||
<Moment
|
||||
date={poll.expires_at}
|
||||
locale={i18n.language}
|
||||
element={Text}
|
||||
fromNow
|
||||
/>
|
||||
]}
|
||||
components={[<RelativeTime date={poll.expires_at} />]}
|
||||
/>
|
||||
</Text>
|
||||
)
|
||||
|
@ -1,9 +1,17 @@
|
||||
import * as Haptics from 'expo-haptics'
|
||||
import { Platform } from 'react-native'
|
||||
import * as Sentry from 'sentry-expo'
|
||||
|
||||
const haptics = (
|
||||
type: 'Success' | 'Warning' | 'Error' | 'Light' | 'Medium' | 'Heavy'
|
||||
) => {
|
||||
if (Platform.OS === 'android') {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle['Light']).catch(error =>
|
||||
Sentry.Native.captureException(error)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case 'Success':
|
||||
case 'Warning':
|
||||
|
@ -2,7 +2,7 @@ import Icon from '@components/Icon'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React from 'react'
|
||||
import { StyleSheet, Text, View } from 'react-native'
|
||||
import { Platform, StyleSheet, Text, ToastAndroid, View } from 'react-native'
|
||||
import { SafeAreaView } from 'react-native-safe-area-context'
|
||||
import Toast from 'react-native-toast-message'
|
||||
import * as Sentry from 'sentry-expo'
|
||||
@ -33,18 +33,23 @@ const toast = ({
|
||||
onShow,
|
||||
onHide
|
||||
}: Params) => {
|
||||
Toast.show({
|
||||
type,
|
||||
position,
|
||||
text1: message,
|
||||
text2: description,
|
||||
visibilityTime: 1500,
|
||||
autoHide,
|
||||
topOffset: 0,
|
||||
bottomOffset: 0,
|
||||
onShow: onShow,
|
||||
onHide: onHide
|
||||
})
|
||||
switch (Platform.OS) {
|
||||
case 'ios':
|
||||
return Toast.show({
|
||||
type,
|
||||
position,
|
||||
text1: message,
|
||||
text2: description,
|
||||
visibilityTime: 1500,
|
||||
autoHide,
|
||||
topOffset: 0,
|
||||
bottomOffset: 0,
|
||||
onShow: onShow,
|
||||
onHide: onHide
|
||||
})
|
||||
case 'android':
|
||||
return ToastAndroid.show(message, ToastAndroid.SHORT)
|
||||
}
|
||||
}
|
||||
|
||||
const ToastBase = ({ config }: { config: Config }) => {
|
||||
|
Reference in New Issue
Block a user