Merge pull request #76 from tooot-app/nightly-210319

Nightly 210319
This commit is contained in:
xmflsct 2021-03-21 23:12:00 +01:00 committed by GitHub
commit 63e8fb00e4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
71 changed files with 1555 additions and 226 deletions

View File

@ -16,8 +16,6 @@ jobs:
id: branch id: branch
- name: -- Step 1 -- Checkout code - name: -- Step 1 -- Checkout code
uses: actions/checkout@v2 uses: actions/checkout@v2
with:
submodules: true
- name: -- Step 2 -- Setup node - name: -- Step 2 -- Setup node
uses: actions/setup-node@v2 uses: actions/setup-node@v2
with: with:

3
.gitmodules vendored
View File

@ -1,3 +0,0 @@
[submodule "src/modules/react-native-image-viewing"]
path = src/modules/react-native-image-viewing
url = https://github.com/xmflsct/react-native-image-viewing.git

View File

@ -1,4 +1,6 @@
declare namespace Nav { declare namespace Nav {
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
type RootStackParamList = { type RootStackParamList = {
'Screen-Tabs': undefined 'Screen-Tabs': undefined
'Screen-Actions': 'Screen-Actions':
@ -58,7 +60,6 @@ declare namespace Nav {
url: Mastodon.AttachmentImage['url'] url: Mastodon.AttachmentImage['url']
width?: number width?: number
height?: number height?: number
preview_url: Mastodon.AttachmentImage['preview_url']
remote_url?: Mastodon.AttachmentImage['remote_url'] remote_url?: Mastodon.AttachmentImage['remote_url']
}[] }[]
id: Mastodon.Attachment['id'] id: Mastodon.Attachment['id']
@ -90,7 +91,7 @@ declare namespace Nav {
'Tab-Shared-Search': { text: string | undefined } 'Tab-Shared-Search': { text: string | undefined }
'Tab-Shared-Toot': { 'Tab-Shared-Toot': {
toot: Mastodon.Status toot: Mastodon.Status
rootQueryKey: any rootQueryKey?: QueryKeyTimeline
} }
'Tab-Shared-Users': 'Tab-Shared-Users':
| { | {

View File

@ -45,7 +45,7 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => {
dark = 'light-content' dark = 'light-content'
} }
const routeNameRef = useRef<string | undefined>() const routeRef = useRef<{ name?: string; params?: {} }>()
const isConnected = useNetInfo().isConnected const isConnected = useNetInfo().isConnected
useEffect(() => { useEffect(() => {
@ -114,35 +114,37 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => {
}, [instanceActive]) }, [instanceActive])
// Callbacks // Callbacks
const navigationContainerOnReady = useCallback( const navigationContainerOnReady = useCallback(() => {
() => const currentRoute = navigationRef.current?.getCurrentRoute()
(routeNameRef.current = navigationRef.current?.getCurrentRoute()?.name), routeRef.current = {
[] name: currentRoute?.name,
) params: currentRoute?.params
}
}, [])
const navigationContainerOnStateChange = useCallback(() => { const navigationContainerOnStateChange = useCallback(() => {
const previousRouteName = routeNameRef.current const previousRoute = routeRef.current
const currentRouteName = navigationRef.current?.getCurrentRoute()?.name const currentRoute = navigationRef.current?.getCurrentRoute()
const matchTabName = currentRouteName?.match(/(Tab-.*)-Root/) const matchTabName = currentRoute?.name?.match(/(Tab-.*)-Root/)
if (matchTabName) { if (matchTabName) {
//@ts-ignore //@ts-ignore
dispatch(updatePreviousTab(matchTabName[1])) dispatch(updatePreviousTab(matchTabName[1]))
} }
if (previousRouteName !== currentRouteName) { if (previousRoute?.name !== currentRoute?.name) {
Analytics.setCurrentScreen(currentRouteName) Analytics.setCurrentScreen(currentRoute?.name)
Sentry.Native.setContext('page', { Sentry.Native.setContext('page', {
previous: previousRouteName, previous: previousRoute,
current: currentRouteName current: currentRoute
}) })
} }
routeNameRef.current = currentRouteName routeRef.current = currentRoute
}, []) }, [])
return ( return (
<> <>
<StatusBar barStyle={barStyle[mode]} backgroundColor={theme.background} /> <StatusBar barStyle={barStyle[mode]} backgroundColor={theme.backgroundDefault} />
<NavigationContainer <NavigationContainer
ref={navigationRef} ref={navigationRef}
theme={themes[mode]} theme={themes[mode]}

View File

@ -1,5 +1,5 @@
import { RootState } from '@root/store' import { RootState } from '@root/store'
import axios from 'axios' import axios, { AxiosRequestConfig } from 'axios'
import chalk from 'chalk' import chalk from 'chalk'
import li from 'li' import li from 'li'
@ -14,7 +14,10 @@ export type Params = {
} }
headers?: { [key: string]: string } headers?: { [key: string]: string }
body?: FormData body?: FormData
onUploadProgress?: (progressEvent: any) => void extras?: Omit<
AxiosRequestConfig,
'method' | 'url' | 'params' | 'headers' | 'data'
>
} }
const apiInstance = async <T = unknown>({ const apiInstance = async <T = unknown>({
@ -24,7 +27,7 @@ const apiInstance = async <T = unknown>({
params, params,
headers, headers,
body, body,
onUploadProgress extras
}: Params): Promise<{ body: T; links: { prev?: string; next?: string } }> => { }: Params): Promise<{ body: T; links: { prev?: string; next?: string } }> => {
const { store } = require('@root/store') const { store } = require('@root/store')
const state = store.getState() as RootState const state = store.getState() as RootState
@ -70,7 +73,7 @@ const apiInstance = async <T = unknown>({
}) })
}, },
...(body && { data: body }), ...(body && { data: body }),
...(onUploadProgress && { onUploadProgress: onUploadProgress }) ...extras
}) })
.then(response => { .then(response => {
let prev let prev

View File

@ -81,7 +81,7 @@ const Button: React.FC<Props> = ({
if (destructive) { if (destructive) {
return theme.red return theme.red
} else { } else {
return theme.primary return theme.primaryDefault
} }
} }
} }
@ -97,16 +97,16 @@ const Button: React.FC<Props> = ({
if (destructive) { if (destructive) {
return theme.red return theme.red
} else { } else {
return theme.primary return theme.primaryDefault
} }
} }
} }
}, [mode, loading, disabled]) }, [mode, loading, disabled])
const colorBackground = useMemo(() => { const colorBackground = useMemo(() => {
if (overlay) { if (overlay) {
return theme.backgroundOverlay return theme.backgroundOverlayInvert
} else { } else {
return theme.background return theme.backgroundDefault
} }
}, [mode]) }, [mode])

View File

@ -102,7 +102,11 @@ const GracefullyImage = React.memo(
return ( return (
<Pressable <Pressable
style={[style, dimension, { backgroundColor: theme.shimmerDefault }]} style={[
style,
dimension,
{ backgroundColor: theme.backgroundOverlayDefault }
]}
{...(onPress {...(onPress
? hidden ? hidden
? { disabled: true } ? { disabled: true }

View File

@ -29,7 +29,7 @@ const ComponentHashtag: React.FC<Props> = ({
return ( return (
<Pressable style={styles.itemDefault} onPress={customOnPress || onPress}> <Pressable style={styles.itemDefault} onPress={customOnPress || onPress}>
<Text style={[styles.itemHashtag, { color: theme.primary }]}> <Text style={[styles.itemHashtag, { color: theme.primaryDefault }]}>
#{hashtag.name} #{hashtag.name}
</Text> </Text>
</Pressable> </Pressable>

View File

@ -17,7 +17,7 @@ const HeaderCenter = React.memo(
<Text <Text
style={[ style={[
styles.text, styles.text,
{ color: inverted ? theme.primaryOverlay : theme.primary } { color: inverted ? theme.primaryOverlay : theme.primaryDefault }
]} ]}
children={content} children={content}
/> />

View File

@ -25,7 +25,7 @@ const HeaderLeft: React.FC<Props> = ({
case 'icon': case 'icon':
return ( return (
<Icon <Icon
color={theme.primary} color={theme.primaryDefault}
name={content || 'ChevronLeft'} name={content || 'ChevronLeft'}
size={StyleConstants.Spacing.M * 1.25} size={StyleConstants.Spacing.M * 1.25}
/> />
@ -33,7 +33,7 @@ const HeaderLeft: React.FC<Props> = ({
case 'text': case 'text':
return ( return (
<Text <Text
style={[styles.text, { color: theme.primary }]} style={[styles.text, { color: theme.primaryDefault }]}
children={content} children={content}
/> />
) )
@ -47,7 +47,7 @@ const HeaderLeft: React.FC<Props> = ({
style={[ style={[
styles.base, styles.base,
{ {
backgroundColor: theme.backgroundGradientStart, backgroundColor: theme.backgroundOverlayDefault,
...(type === 'icon' && { ...(type === 'icon' && {
height: 44, height: 44,
width: 44, width: 44,

View File

@ -47,7 +47,7 @@ const HeaderRight: React.FC<Props> = ({
name={content} name={content}
style={{ opacity: loading ? 0 : 1 }} style={{ opacity: loading ? 0 : 1 }}
size={StyleConstants.Spacing.M * 1.25} size={StyleConstants.Spacing.M * 1.25}
color={disabled ? theme.secondary : theme.primary} color={disabled ? theme.secondary : theme.primaryDefault}
/> />
{loading && loadingSpinkit} {loading && loadingSpinkit}
</> </>
@ -59,7 +59,7 @@ const HeaderRight: React.FC<Props> = ({
style={[ style={[
styles.text, styles.text,
{ {
color: disabled ? theme.secondary : theme.primary, color: disabled ? theme.secondary : theme.primaryDefault,
opacity: loading ? 0 : 1 opacity: loading ? 0 : 1
} }
]} ]}
@ -79,7 +79,7 @@ const HeaderRight: React.FC<Props> = ({
style={[ style={[
styles.base, styles.base,
{ {
backgroundColor: theme.backgroundGradientStart, backgroundColor: theme.backgroundOverlayDefault,
...(type === 'icon' && { ...(type === 'icon' && {
height: 44, height: 44,
width: 44, width: 44,

View File

@ -142,7 +142,7 @@ const ComponentInstance: React.FC<Props> = ({
style={[ style={[
styles.textInput, styles.textInput,
{ {
color: theme.primary, color: theme.primaryDefault,
borderBottomColor: instanceQuery.isError borderBottomColor: instanceQuery.isError
? theme.red ? theme.red
: theme.border : theme.border

View File

@ -17,9 +17,9 @@ const InstanceInfo = React.memo(
return ( return (
<View style={[styles.base, style]}> <View style={[styles.base, style]}>
<Text style={[styles.header, { color: theme.primary }]}>{header}</Text> <Text style={[styles.header, { color: theme.primaryDefault }]}>{header}</Text>
{content ? ( {content ? (
<Text style={[styles.content, { color: theme.primary }]}> <Text style={[styles.content, { color: theme.primaryDefault }]}>
{content} {content}
</Text> </Text>
) : ( ) : (

View File

@ -28,7 +28,7 @@ export interface Props {
const MenuRow: React.FC<Props> = ({ const MenuRow: React.FC<Props> = ({
iconFront, iconFront,
iconFrontColor = 'primary', iconFrontColor = 'primaryDefault',
title, title,
description, description,
content, content,
@ -73,7 +73,7 @@ const MenuRow: React.FC<Props> = ({
)} )}
<View style={styles.main}> <View style={styles.main}>
<Text <Text
style={[styles.title, { color: theme.primary }]} style={[styles.title, { color: theme.primaryDefault }]}
numberOfLines={1} numberOfLines={1}
> >
{title} {title}

View File

@ -87,18 +87,18 @@ const Message = React.memo(
position='top' position='top'
floating floating
style={{ style={{
backgroundColor: theme.background, backgroundColor: theme.backgroundDefault,
shadowColor: theme.primary, shadowColor: theme.primaryDefault,
shadowOffset: { width: 0, height: 0 }, shadowOffset: { width: 0, height: 0 },
shadowOpacity: mode === 'light' ? 0.16 : 0.24, shadowOpacity: mode === 'light' ? 0.16 : 0.24,
shadowRadius: 4 shadowRadius: 4
}} }}
titleStyle={{ titleStyle={{
color: theme.primary, color: theme.primaryDefault,
...StyleConstants.FontStyle.M, ...StyleConstants.FontStyle.M,
fontWeight: StyleConstants.Font.Weight.Bold fontWeight: StyleConstants.Font.Weight.Bold
}} }}
textStyle={{ color: theme.primary, ...StyleConstants.FontStyle.S }} textStyle={{ color: theme.primaryDefault, ...StyleConstants.FontStyle.S }}
// @ts-ignore // @ts-ignore
textProps={{ numberOfLines: 2 }} textProps={{ numberOfLines: 2 }}
/> />

View File

@ -38,7 +38,7 @@ const ParseEmojis = React.memo(
const styles = useMemo(() => { const styles = useMemo(() => {
return StyleSheet.create({ return StyleSheet.create({
text: { text: {
color: theme.primary, color: theme.primaryDefault,
fontSize: adaptedFontsize, fontSize: adaptedFontsize,
lineHeight: adaptedLineheight, lineHeight: adaptedLineheight,
...(fontBold && { fontWeight: StyleConstants.Font.Weight.Bold }) ...(fontBold && { fontWeight: StyleConstants.Font.Weight.Bold })

View File

@ -83,7 +83,7 @@ const renderNode = ({
<Text <Text
key={index} key={index}
style={{ style={{
color: accountIndex !== -1 ? theme.blue : theme.primary, color: accountIndex !== -1 ? theme.blue : theme.primaryDefault,
fontSize: adaptedFontsize, fontSize: adaptedFontsize,
lineHeight: adaptedLineheight lineHeight: adaptedLineheight
}} }}
@ -121,7 +121,7 @@ const renderNode = ({
onPress={async () => { onPress={async () => {
analytics('status_link_press') analytics('status_link_press')
!disableDetails && !shouldBeTag !disableDetails && !shouldBeTag
? await openLink(href) ? await openLink(href, navigation)
: navigation.push('Tab-Shared-Hashtag', { : navigation.push('Tab-Shared-Hashtag', {
hashtag: content.substring(1) hashtag: content.substring(1)
}) })
@ -258,14 +258,14 @@ const ParseHTML = React.memo(
justifyContent: 'center', justifyContent: 'center',
marginTop: expanded ? 0 : -adaptedLineheight, marginTop: expanded ? 0 : -adaptedLineheight,
minHeight: 44, minHeight: 44,
backgroundColor: theme.background backgroundColor: theme.backgroundDefault
}} }}
> >
<Text <Text
style={{ style={{
textAlign: 'center', textAlign: 'center',
...StyleConstants.FontStyle.S, ...StyleConstants.FontStyle.S,
color: theme.primary color: theme.primaryDefault
}} }}
children={t(`HTML.expanded.${expanded.toString()}`, { children={t(`HTML.expanded.${expanded.toString()}`, {
hint: expandHint hint: expandHint

View File

@ -15,7 +15,7 @@ const ComponentSeparator = React.memo(
return ( return (
<View <View
style={{ style={{
backgroundColor: theme.background, backgroundColor: theme.backgroundDefault,
borderTopColor: theme.border, borderTopColor: theme.border,
borderTopWidth: StyleSheet.hairlineWidth, borderTopWidth: StyleSheet.hairlineWidth,
marginLeft: marginLeft:

View File

@ -115,8 +115,8 @@ const Timeline: React.FC<Props> = ({
refreshControl: ( refreshControl: (
<RefreshControl <RefreshControl
enabled enabled
colors={[theme.primary]} colors={[theme.primaryDefault]}
progressBackgroundColor={theme.background} progressBackgroundColor={theme.backgroundDefault}
refreshing={isFetching || isLoading} refreshing={isFetching || isLoading}
onRefresh={() => refetch()} onRefresh={() => refetch()}
/> />

View File

@ -95,7 +95,7 @@ const TimelineConversation: React.FC<Props> = ({
<Pressable <Pressable
style={[ style={[
styles.base, styles.base,
{ backgroundColor: theme.background }, { backgroundColor: theme.backgroundDefault },
conversation.unread && { conversation.unread && {
borderLeftWidth: StyleConstants.Spacing.XS, borderLeftWidth: StyleConstants.Spacing.XS,
borderLeftColor: theme.blue, borderLeftColor: theme.blue,

View File

@ -65,7 +65,7 @@ const TimelineDefault: React.FC<Props> = ({
style={[ style={[
styles.statusView, styles.statusView,
{ {
backgroundColor: theme.background, backgroundColor: theme.backgroundDefault,
paddingBottom: paddingBottom:
disableDetails && disableOnPress disableDetails && disableOnPress
? StyleConstants.Spacing.Global.PagePadding ? StyleConstants.Spacing.Global.PagePadding
@ -141,8 +141,8 @@ const TimelineDefault: React.FC<Props> = ({
([actualStatus.account] as Mastodon.Account[] & Mastodon.Mention[]) ([actualStatus.account] as Mastodon.Account[] & Mastodon.Mention[])
.concat(actualStatus.mentions) .concat(actualStatus.mentions)
.filter(d => d?.id !== instanceAccount?.id), .filter(d => d?.id !== instanceAccount?.id),
d => d.id d => d?.id
).map(d => d.acct)} ).map(d => d?.acct)}
reblog={item.reblog ? true : false} reblog={item.reblog ? true : false}
/> />
)} )}

View File

@ -35,9 +35,9 @@ const TimelineEmpty = React.memo(
<Icon <Icon
name='Frown' name='Frown'
size={StyleConstants.Font.Size.L} size={StyleConstants.Font.Size.L}
color={theme.primary} color={theme.primaryDefault}
/> />
<Text style={[styles.error, { color: theme.primary }]}> <Text style={[styles.error, { color: theme.primaryDefault }]}>
{t('empty.error.message')} {t('empty.error.message')}
</Text> </Text>
<Button <Button
@ -56,9 +56,9 @@ const TimelineEmpty = React.memo(
<Icon <Icon
name='Smartphone' name='Smartphone'
size={StyleConstants.Font.Size.L} size={StyleConstants.Font.Size.L}
color={theme.primary} color={theme.primaryDefault}
/> />
<Text style={[styles.error, { color: theme.primary }]}> <Text style={[styles.error, { color: theme.primaryDefault }]}>
{t('empty.success.message')} {t('empty.success.message')}
</Text> </Text>
</> </>
@ -67,7 +67,7 @@ const TimelineEmpty = React.memo(
}, [mode, i18n.language, status]) }, [mode, i18n.language, status])
return ( return (
<View <View
style={[styles.base, { backgroundColor: theme.background }]} style={[styles.base, { backgroundColor: theme.backgroundDefault }]}
children={children} children={children}
/> />
) )

View File

@ -56,7 +56,7 @@ const TimelineNotifications: React.FC<Props> = ({
style={[ style={[
styles.notificationView, styles.notificationView,
{ {
backgroundColor: theme.background, backgroundColor: theme.backgroundDefault,
paddingBottom: notification.status paddingBottom: notification.status
? 0 ? 0
: StyleConstants.Spacing.Global.PagePadding : StyleConstants.Spacing.Global.PagePadding
@ -138,9 +138,9 @@ const TimelineNotifications: React.FC<Props> = ({
([notification.status.account] as Mastodon.Account[] & ([notification.status.account] as Mastodon.Account[] &
Mastodon.Mention[]) Mastodon.Mention[])
.concat(notification.status.mentions) .concat(notification.status.mentions)
.filter(d => d.id !== instanceAccount?.id), .filter(d => d?.id !== instanceAccount?.id),
d => d.id d => d?.id
).map(d => d.acct)} ).map(d => d?.acct)}
reblog={false} reblog={false}
/> />
) : null} ) : null}

View File

@ -254,7 +254,7 @@ const TimelineRefresh: React.FC<Props> = ({
<> <>
<View style={styles.container1}> <View style={styles.container1}>
<Text <Text
style={[styles.explanation, { color: theme.primary }]} style={[styles.explanation, { color: theme.primaryDefault }]}
onLayout={onLayout} onLayout={onLayout}
children={t('refresh.fetchPreviousPage')} children={t('refresh.fetchPreviousPage')}
/> />
@ -271,14 +271,14 @@ const TimelineRefresh: React.FC<Props> = ({
<Icon <Icon
name='ArrowLeft' name='ArrowLeft'
size={StyleConstants.Font.Size.M} size={StyleConstants.Font.Size.M}
color={theme.primary} color={theme.primaryDefault}
/> />
} }
/> />
</View> </View>
<View style={styles.container2}> <View style={styles.container2}>
<Text <Text
style={[styles.explanation, { color: theme.primary }]} style={[styles.explanation, { color: theme.primaryDefault }]}
onLayout={onLayout} onLayout={onLayout}
children={t('refresh.refetch')} children={t('refresh.refetch')}
/> />

View File

@ -23,7 +23,7 @@ const TimelineActioned = React.memo(
StackNavigationProp<Nav.TabLocalStackParamList> StackNavigationProp<Nav.TabLocalStackParamList>
>() >()
const name = account.display_name || account.username const name = account.display_name || account.username
const iconColor = theme.primary const iconColor = theme.primaryDefault
const content = (content: string) => ( const content = (content: string) => (
<ParseEmojis content={content} emojis={account.emojis} size='S' /> <ParseEmojis content={content} emojis={account.emojis} size='S' />

View File

@ -30,7 +30,7 @@ const TimelineActions = React.memo(
const { mode, theme } = useTheme() const { mode, theme } = useTheme()
const iconColor = theme.secondary const iconColor = theme.secondary
const iconColorAction = (state: boolean) => const iconColorAction = (state: boolean) =>
state ? theme.primary : theme.secondary state ? theme.primaryDefault : theme.secondary
const queryClient = useQueryClient() const queryClient = useQueryClient()
const mutation = useTimelineMutation({ const mutation = useTimelineMutation({
@ -194,22 +194,23 @@ const TimelineActions = React.memo(
), ),
[status.replies_count] [status.replies_count]
) )
const childrenReblog = useMemo( const childrenReblog = useMemo(() => {
() => ( const color = (state: boolean) => (state ? theme.green : theme.secondary)
return (
<> <>
<Icon <Icon
name='Repeat' name='Repeat'
color={ color={
status.visibility === 'private' || status.visibility === 'direct' status.visibility === 'private' || status.visibility === 'direct'
? theme.disabled ? theme.disabled
: iconColorAction(status.reblogged) : color(status.reblogged)
} }
size={StyleConstants.Font.Size.L} size={StyleConstants.Font.Size.L}
/> />
{status.reblogs_count > 0 && ( {status.reblogs_count > 0 && (
<Text <Text
style={{ style={{
color: iconColorAction(status.reblogged), color: color(status.reblogged),
fontSize: StyleConstants.Font.Size.M, fontSize: StyleConstants.Font.Size.M,
marginLeft: StyleConstants.Spacing.XS marginLeft: StyleConstants.Spacing.XS
}} }}
@ -218,21 +219,21 @@ const TimelineActions = React.memo(
</Text> </Text>
)} )}
</> </>
), )
[status.reblogged, status.reblogs_count] }, [status.reblogged, status.reblogs_count])
) const childrenFavourite = useMemo(() => {
const childrenFavourite = useMemo( const color = (state: boolean) => (state ? theme.red : theme.secondary)
() => ( return (
<> <>
<Icon <Icon
name='Heart' name='Heart'
color={iconColorAction(status.favourited)} color={color(status.favourited)}
size={StyleConstants.Font.Size.L} size={StyleConstants.Font.Size.L}
/> />
{status.favourites_count > 0 && ( {status.favourites_count > 0 && (
<Text <Text
style={{ style={{
color: iconColorAction(status.favourited), color: color(status.favourited),
fontSize: StyleConstants.Font.Size.M, fontSize: StyleConstants.Font.Size.M,
marginLeft: StyleConstants.Spacing.XS, marginLeft: StyleConstants.Spacing.XS,
marginTop: 0 marginTop: 0
@ -242,19 +243,18 @@ const TimelineActions = React.memo(
</Text> </Text>
)} )}
</> </>
), )
[status.favourited, status.favourites_count] }, [status.favourited, status.favourites_count])
) const childrenBookmark = useMemo(() => {
const childrenBookmark = useMemo( const color = (state: boolean) => (state ? theme.yellow : theme.secondary)
() => ( return (
<Icon <Icon
name='Bookmark' name='Bookmark'
color={iconColorAction(status.bookmarked)} color={color(status.bookmarked)}
size={StyleConstants.Font.Size.L} size={StyleConstants.Font.Size.L}
/> />
), )
[status.bookmarked] }, [status.bookmarked])
)
return ( return (
<View <View

View File

@ -48,7 +48,6 @@ const TimelineAttachment = React.memo(
imageUrls.push({ imageUrls.push({
id: attachment.id, id: attachment.id,
url: attachment.url, url: attachment.url,
preview_url: attachment.preview_url,
remote_url: attachment.remote_url, remote_url: attachment.remote_url,
width: attachment.meta?.original?.width, width: attachment.meta?.original?.width,
height: attachment.meta?.original?.height height: attachment.meta?.original?.height

View File

@ -100,7 +100,7 @@ const AttachmentAudio: React.FC<Props> = ({
alignSelf: 'flex-end', alignSelf: 'flex-end',
width: '100%', width: '100%',
height: StyleConstants.Spacing.M + StyleConstants.Spacing.S * 2, height: StyleConstants.Spacing.M + StyleConstants.Spacing.S * 2,
backgroundColor: theme.backgroundOverlay, backgroundColor: theme.backgroundOverlayInvert,
paddingHorizontal: StyleConstants.Spacing.Global.PagePadding, paddingHorizontal: StyleConstants.Spacing.Global.PagePadding,
borderRadius: 100, borderRadius: 100,
opacity: sensitiveShown ? 0.35 : undefined opacity: sensitiveShown ? 0.35 : undefined

View File

@ -47,7 +47,7 @@ const AttachmentUnsupported: React.FC<Props> = ({
<Text <Text
style={[ style={[
styles.text, styles.text,
{ color: attachment.blurhash ? theme.background : theme.primary } { color: attachment.blurhash ? theme.backgroundDefault : theme.primaryDefault }
]} ]}
> >
{t('shared.attachment.unsupported.text')} {t('shared.attachment.unsupported.text')}

View File

@ -1,6 +1,7 @@
import analytics from '@components/analytics' import analytics from '@components/analytics'
import GracefullyImage from '@components/GracefullyImage' import GracefullyImage from '@components/GracefullyImage'
import openLink from '@components/openLink' import openLink from '@components/openLink'
import { useNavigation } from '@react-navigation/native'
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 from 'react' import React from 'react'
@ -13,13 +14,14 @@ export interface Props {
const TimelineCard = React.memo( const TimelineCard = React.memo(
({ card }: Props) => { ({ card }: Props) => {
const { theme } = useTheme() const { theme } = useTheme()
const navigation = useNavigation()
return ( return (
<Pressable <Pressable
style={[styles.card, { borderColor: theme.border }]} style={[styles.card, { borderColor: theme.border }]}
onPress={async () => { onPress={async () => {
analytics('timeline_shared_card_press') analytics('timeline_shared_card_press')
await openLink(card.url) await openLink(card.url, navigation)
}} }}
testID='base' testID='base'
> >
@ -34,7 +36,7 @@ const TimelineCard = React.memo(
<View style={styles.right}> <View style={styles.right}>
<Text <Text
numberOfLines={2} numberOfLines={2}
style={[styles.rightTitle, { color: theme.primary }]} style={[styles.rightTitle, { color: theme.primaryDefault }]}
testID='title' testID='title'
> >
{card.title} {card.title}
@ -42,7 +44,7 @@ const TimelineCard = React.memo(
{card.description ? ( {card.description ? (
<Text <Text
numberOfLines={1} numberOfLines={1}
style={[styles.rightDescription, { color: theme.primary }]} style={[styles.rightDescription, { color: theme.primaryDefault }]}
testID='description' testID='description'
> >
{card.description} {card.description}

View File

@ -186,7 +186,7 @@ const TimelinePoll: React.FC<Props> = ({
<Text style={styles.optionText}> <Text style={styles.optionText}>
<ParseEmojis content={option.title} emojis={poll.emojis} /> <ParseEmojis content={option.title} emojis={poll.emojis} />
</Text> </Text>
<Text style={[styles.optionPercentage, { color: theme.primary }]}> <Text style={[styles.optionPercentage, { color: theme.primaryDefault }]}>
{poll.votes_count {poll.votes_count
? Math.round( ? Math.round(
(option.votes_count / (option.votes_count /
@ -246,7 +246,7 @@ const TimelinePoll: React.FC<Props> = ({
style={styles.optionSelection} style={styles.optionSelection}
name={isSelected(index)} name={isSelected(index)}
size={StyleConstants.Font.Size.M} size={StyleConstants.Font.Size.M}
color={theme.primary} color={theme.primaryDefault}
/> />
<Text style={styles.optionText}> <Text style={styles.optionText}>
<ParseEmojis content={option.title} emojis={poll.emojis} /> <ParseEmojis content={option.title} emojis={poll.emojis} />

View File

@ -1,9 +1,127 @@
import apiInstance from '@api/instance'
import { NavigationProp, ParamListBase } from '@react-navigation/native'
import { navigationRef } from '@root/Screens'
import { store } from '@root/store' import { store } from '@root/store'
import { SearchResult } from '@utils/queryHooks/search'
import { getInstanceUrl } from '@utils/slices/instancesSlice'
import { getSettingsBrowser } from '@utils/slices/settingsSlice' import { getSettingsBrowser } from '@utils/slices/settingsSlice'
import * as Linking from 'expo-linking' import * as Linking from 'expo-linking'
import * as WebBrowser from 'expo-web-browser' import * as WebBrowser from 'expo-web-browser'
const openLink = async (url: string) => { // https://social.xmflsct.com/web/statuses/105590085754428765 <- default
// https://social.xmflsct.com/@tooot/105590085754428765 <- pretty
const matcherStatus = new RegExp(
/http[s]?:\/\/(.*)\/(web\/statuses|@.*)\/([0-9]*)/
)
// https://social.xmflsct.com/web/accounts/14195 <- default
// https://social.xmflsct.com/@tooot <- pretty
const matcherAccount = new RegExp(
/http[s]?:\/\/(.*)\/(web\/accounts\/([0-9]*)|@.*)/
)
export let loadingLink = false
const openLink = async (
url: string,
navigation?: NavigationProp<
ParamListBase,
string,
Readonly<{
key: string
index: number
routeNames: string[]
history?: unknown[] | undefined
routes: any[]
type: string
stale: false
}>,
{},
{}
>
) => {
if (loadingLink) {
return
}
const handleNavigation = (
page: 'Tab-Shared-Toot' | 'Tab-Shared-Account',
options: {}
) => {
if (navigation) {
// @ts-ignore
navigation.push(page, options)
} else {
navigationRef.current?.navigate(page, options)
}
}
// If a tooot can be found
const matchedStatus = url.match(matcherStatus)
if (matchedStatus) {
// If the link in current instance
const instanceUrl = getInstanceUrl(store.getState())
if (matchedStatus[1] === instanceUrl) {
handleNavigation('Tab-Shared-Toot', {
toot: { id: matchedStatus[3] }
})
return
}
loadingLink = true
let response
try {
response = await apiInstance<SearchResult>({
version: 'v2',
method: 'get',
url: 'search',
params: { type: 'statuses', q: url, limit: 1, resolve: true }
})
} catch {}
if (response && response.body && response.body.statuses.length) {
handleNavigation('Tab-Shared-Toot', {
toot: response.body.statuses[0]
})
loadingLink = false
return
}
}
// If an account can be found
const matchedAccount = url.match(matcherAccount)
console.log(matchedAccount)
if (matchedAccount) {
// If the link in current instance
const instanceUrl = getInstanceUrl(store.getState())
if (matchedAccount[1] === instanceUrl) {
if (matchedAccount[3] && matchedAccount[3].match(/[0-9]*/)) {
handleNavigation('Tab-Shared-Account', {
account: { id: matchedAccount[3] }
})
return
}
}
loadingLink = true
let response
try {
response = await apiInstance<SearchResult>({
version: 'v2',
method: 'get',
url: 'search',
params: { type: 'accounts', q: url, limit: 1, resolve: true }
})
} catch {}
if (response && response.body && response.body.accounts.length) {
handleNavigation('Tab-Shared-Account', {
account: response.body.accounts[0]
})
loadingLink = false
return
}
}
loadingLink = false
switch (getSettingsBrowser(store.getState())) { switch (getSettingsBrowser(store.getState())) {
case 'internal': case 'internal':
await WebBrowser.openBrowserAsync(url, { await WebBrowser.openBrowserAsync(url, {

@ -1 +0,0 @@
Subproject commit bba2f756a9d45c79a5ebf7e5e3124eac49b0c9f7

View File

@ -185,7 +185,7 @@ const ScreenActionsRoot = React.memo(
<Animated.View <Animated.View
style={[ style={[
styles.overlay, styles.overlay,
{ backgroundColor: theme.backgroundOverlay } { backgroundColor: theme.backgroundOverlayInvert }
]} ]}
> >
<PanGestureHandler onGestureEvent={onGestureEvent}> <PanGestureHandler onGestureEvent={onGestureEvent}>
@ -194,7 +194,7 @@ const ScreenActionsRoot = React.memo(
styles.container, styles.container,
styleTop, styleTop,
{ {
backgroundColor: theme.background, backgroundColor: theme.backgroundDefault,
paddingBottom: insets.bottom || StyleConstants.Spacing.L paddingBottom: insets.bottom || StyleConstants.Spacing.L
} }
]} ]}

View File

@ -74,8 +74,8 @@ const ScreenAnnouncements: React.FC<ScreenAnnouncementsProp> = ({
style={[ style={[
styles.announcement, styles.announcement,
{ {
borderColor: theme.primary, borderColor: theme.primaryDefault,
backgroundColor: theme.background backgroundColor: theme.backgroundDefault
} }
]} ]}
> >
@ -102,10 +102,10 @@ const ScreenAnnouncements: React.FC<ScreenAnnouncementsProp> = ({
style={[ style={[
styles.reaction, styles.reaction,
{ {
borderColor: reaction.me ? theme.disabled : theme.primary, borderColor: reaction.me ? theme.disabled : theme.primaryDefault,
backgroundColor: reaction.me backgroundColor: reaction.me
? theme.disabled ? theme.disabled
: theme.background : theme.backgroundDefault
} }
]} ]}
onPress={() => { onPress={() => {
@ -130,7 +130,7 @@ const ScreenAnnouncements: React.FC<ScreenAnnouncementsProp> = ({
)} )}
{reaction.count ? ( {reaction.count ? (
<Text <Text
style={[styles.reactionCount, { color: theme.primary }]} style={[styles.reactionCount, { color: theme.primaryDefault }]}
> >
{reaction.count} {reaction.count}
</Text> </Text>
@ -138,13 +138,13 @@ const ScreenAnnouncements: React.FC<ScreenAnnouncementsProp> = ({
</Pressable> </Pressable>
))} ))}
{/* <Pressable {/* <Pressable
style={[styles.reaction, { borderColor: theme.primary }]} style={[styles.reaction, { borderColor: theme.primaryDefault }]}
onPress={() => invisibleTextInputRef.current?.focus()} onPress={() => invisibleTextInputRef.current?.focus()}
> >
<Icon <Icon
name='Plus' name='Plus'
size={StyleConstants.Font.Size.M} size={StyleConstants.Font.Size.M}
color={theme.primary} color={theme.primaryDefault}
/> />
</Pressable> */} </Pressable> */}
</View> </View>
@ -202,7 +202,7 @@ const ScreenAnnouncements: React.FC<ScreenAnnouncementsProp> = ({
blurType={mode} blurType={mode}
blurAmount={20} blurAmount={20}
style={styles.base} style={styles.base}
reducedTransparencyFallbackColor={theme.background} reducedTransparencyFallbackColor={theme.backgroundDefault}
> >
<SafeAreaView style={styles.base}> <SafeAreaView style={styles.base}>
<View <View
@ -245,8 +245,8 @@ const ScreenAnnouncements: React.FC<ScreenAnnouncementsProp> = ({
style={[ style={[
styles.indicator, styles.indicator,
{ {
borderColor: theme.primary, borderColor: theme.primaryDefault,
backgroundColor: i === index ? theme.primary : undefined, backgroundColor: i === index ? theme.primaryDefault : undefined,
marginLeft: marginLeft:
i === query.data.length ? 0 : StyleConstants.Spacing.S i === query.data.length ? 0 : StyleConstants.Spacing.S
} }

View File

@ -55,7 +55,7 @@ const ComposeDraftsListRoot: React.FC<Props> = ({ timestamp }) => {
({ item }: { item: ComposeStateDraft }) => { ({ item }: { item: ComposeStateDraft }) => {
return ( return (
<Pressable <Pressable
style={[styles.draft, { backgroundColor: theme.background }]} style={[styles.draft, { backgroundColor: theme.backgroundDefault }]}
onPress={async () => { onPress={async () => {
setCheckingAttachments(true) setCheckingAttachments(true)
let tempDraft = item let tempDraft = item
@ -103,7 +103,7 @@ const ComposeDraftsListRoot: React.FC<Props> = ({ timestamp }) => {
<HeaderSharedCreated created_at={item.timestamp} /> <HeaderSharedCreated created_at={item.timestamp} />
<Text <Text
numberOfLines={2} numberOfLines={2}
style={[styles.text, { color: theme.primary }]} style={[styles.text, { color: theme.primaryDefault }]}
> >
{item.text || {item.text ||
item.spoiler || item.spoiler ||
@ -181,7 +181,7 @@ const ComposeDraftsListRoot: React.FC<Props> = ({ timestamp }) => {
visible={checkingAttachments} visible={checkingAttachments}
children={ children={
<View <View
style={[styles.modal, { backgroundColor: theme.backgroundOverlay }]} style={[styles.modal, { backgroundColor: theme.backgroundOverlayInvert }]}
children={ children={
<Text <Text
children='检查附件在服务器的状态…' children='检查附件在服务器的状态…'

View File

@ -144,7 +144,7 @@ const ComposeEditAttachmentImage: React.FC<Props> = ({ index }) => {
<G> <G>
<Path <Path
d='M1000,0 L1000,1000 L0,1000 L0,0 L1000,0 Z M500,475 C486.192881,475 475,486.192881 475,500 C475,513.807119 486.192881,525 500,525 C513.807119,525 525,513.807119 525,500 C525,486.192881 513.807119,475 500,475 Z' d='M1000,0 L1000,1000 L0,1000 L0,0 L1000,0 Z M500,475 C486.192881,475 475,486.192881 475,500 C475,513.807119 486.192881,525 500,525 C513.807119,525 525,513.807119 525,500 C525,486.192881 513.807119,475 500,475 Z'
fill={theme.backgroundOverlay} fill={theme.backgroundOverlayInvert}
/> />
<Circle <Circle
stroke={theme.primaryOverlay} stroke={theme.primaryOverlay}
@ -160,7 +160,7 @@ const ComposeEditAttachmentImage: React.FC<Props> = ({ index }) => {
</Animated.View> </Animated.View>
</PanGestureHandler> </PanGestureHandler>
</View> </View>
<Text style={[styles.imageFocusText, { color: theme.primary }]}> <Text style={[styles.imageFocusText, { color: theme.primaryDefault }]}>
{t('content.editAttachment.content.imageFocus')} {t('content.editAttachment.content.imageFocus')}
</Text> </Text>
</> </>

View File

@ -61,13 +61,13 @@ const ComposeEditAttachmentRoot: React.FC<Props> = ({ index }) => {
<ScrollView ref={scrollViewRef}> <ScrollView ref={scrollViewRef}>
{mediaDisplay} {mediaDisplay}
<View style={styles.altTextContainer}> <View style={styles.altTextContainer}>
<Text style={[styles.altTextInputHeading, { color: theme.primary }]}> <Text style={[styles.altTextInputHeading, { color: theme.primaryDefault }]}>
{t('content.editAttachment.content.altText.heading')} {t('content.editAttachment.content.altText.heading')}
</Text> </Text>
<TextInput <TextInput
style={[ style={[
styles.altTextInput, styles.altTextInput,
{ borderColor: theme.border, color: theme.primary } { borderColor: theme.border, color: theme.primaryDefault }
]} ]}
onFocus={() => scrollViewRef.current?.scrollToEnd()} onFocus={() => scrollViewRef.current?.scrollToEnd()}
autoCapitalize='none' autoCapitalize='none'

View File

@ -14,7 +14,7 @@ const ComposePosting = React.memo(
animationType='fade' animationType='fade'
visible={composeState.posting} visible={composeState.posting}
children={ children={
<View style={{ flex: 1, backgroundColor: theme.backgroundOverlay }} /> <View style={{ flex: 1, backgroundColor: theme.backgroundOverlayInvert }} />
} }
/> />
) )

View File

@ -20,7 +20,7 @@ const ComposeActions: React.FC = () => {
if (composeState.poll.active) return theme.disabled if (composeState.poll.active) return theme.disabled
if (composeState.attachments.uploads.length) { if (composeState.attachments.uploads.length) {
return theme.primary return theme.primaryDefault
} else { } else {
return theme.secondary return theme.secondary
} }
@ -43,7 +43,7 @@ const ComposeActions: React.FC = () => {
if (composeState.attachments.uploads.length) return theme.disabled if (composeState.attachments.uploads.length) return theme.disabled
if (composeState.poll.active) { if (composeState.poll.active) {
return theme.primary return theme.primaryDefault
} else { } else {
return theme.secondary return theme.secondary
} }
@ -144,7 +144,7 @@ const ComposeActions: React.FC = () => {
if (!composeState.emoji.emojis) return theme.disabled if (!composeState.emoji.emojis) return theme.disabled
if (composeState.emoji.active) { if (composeState.emoji.active) {
return theme.primary return theme.primaryDefault
} else { } else {
return theme.secondary return theme.secondary
} }
@ -166,7 +166,7 @@ const ComposeActions: React.FC = () => {
<View <View
style={[ style={[
styles.additions, styles.additions,
{ backgroundColor: theme.background, borderTopColor: theme.border } { backgroundColor: theme.backgroundDefault, borderTopColor: theme.border }
]} ]}
> >
<Pressable <Pressable
@ -196,7 +196,7 @@ const ComposeActions: React.FC = () => {
name='AlertTriangle' name='AlertTriangle'
size={24} size={24}
color={ color={
composeState.spoiler.active ? theme.primary : theme.secondary composeState.spoiler.active ? theme.primaryDefault : theme.secondary
} }
/> />
} }

View File

@ -130,8 +130,8 @@ const ComposeAttachments: React.FC = () => {
style={[ style={[
styles.duration, styles.duration,
{ {
color: theme.background, color: theme.backgroundDefault,
backgroundColor: theme.backgroundOverlay backgroundColor: theme.backgroundOverlayInvert
} }
]} ]}
> >
@ -142,7 +142,7 @@ const ComposeAttachments: React.FC = () => {
<View <View
style={[ style={[
styles.uploading, styles.uploading,
{ backgroundColor: theme.backgroundOverlay } { backgroundColor: theme.backgroundOverlayInvert }
]} ]}
> >
<Circle <Circle
@ -196,7 +196,7 @@ const ComposeAttachments: React.FC = () => {
styles.container, styles.container,
{ {
width: DEFAULT_HEIGHT, width: DEFAULT_HEIGHT,
backgroundColor: theme.backgroundOverlay backgroundColor: theme.backgroundOverlayInvert
} }
]} ]}
onPress={async () => { onPress={async () => {
@ -238,9 +238,9 @@ const ComposeAttachments: React.FC = () => {
<Icon <Icon
name={composeState.attachments.sensitive ? 'CheckCircle' : 'Circle'} name={composeState.attachments.sensitive ? 'CheckCircle' : 'Circle'}
size={StyleConstants.Font.Size.L} size={StyleConstants.Font.Size.L}
color={theme.primary} color={theme.primaryDefault}
/> />
<Text style={[styles.sensitiveText, { color: theme.primary }]}> <Text style={[styles.sensitiveText, { color: theme.primaryDefault }]}>
{t('content.root.footer.attachments.sensitive')} {t('content.root.footer.attachments.sensitive')}
</Text> </Text>
</Pressable> </Pressable>

View File

@ -54,7 +54,7 @@ const ComposePoll: React.FC = () => {
styles.textInput, styles.textInput,
{ {
borderColor: theme.border, borderColor: theme.border,
color: hasConflict ? theme.red : theme.primary color: hasConflict ? theme.red : theme.primaryDefault
} }
]} ]}
placeholder={ placeholder={

View File

@ -17,7 +17,7 @@ const ComposeSpoilerInput: React.FC = () => {
style={[ style={[
styles.spoilerInput, styles.spoilerInput,
{ {
color: theme.primary, color: theme.primaryDefault,
borderBottomColor: theme.border borderBottomColor: theme.border
} }
]} ]}

View File

@ -17,7 +17,7 @@ const ComposeTextInput: React.FC = () => {
style={[ style={[
styles.textInput, styles.textInput,
{ {
color: theme.primary, color: theme.primaryDefault,
borderBottomColor: theme.border borderBottomColor: theme.border
} }
]} ]}

View File

@ -15,7 +15,7 @@ const composePost = async (
url: `statuses/${composeState.replyToStatus.id}` url: `statuses/${composeState.replyToStatus.id}`
}) })
} catch (err) { } catch (err) {
if (err.status == 404) { if (err.status && err.status == 404) {
return Promise.reject({ removeReply: true }) return Promise.reject({ removeReply: true })
} }
} }

View File

@ -0,0 +1,7 @@
import * as rn from "react-native";
declare module "react-native" {
class VirtualizedList<ItemT> extends React.Component<
VirtualizedListProps<ItemT>
> {}
}

View File

@ -0,0 +1,17 @@
/**
* Copyright (c) JOB TODAY S.A. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
export type Dimensions = {
width: number
height: number
}
export type Position = {
x: number
y: number
}

View File

@ -0,0 +1,133 @@
/**
* Copyright (c) JOB TODAY S.A. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import React, { ComponentType, useCallback, useEffect } from 'react'
import {
Animated,
Dimensions,
StyleSheet,
View,
VirtualizedList
} from 'react-native'
import ImageItem from './components/ImageItem'
import useAnimatedComponents from './hooks/useAnimatedComponents'
import useImageIndexChange from './hooks/useImageIndexChange'
import useRequestClose from './hooks/useRequestClose'
type Props = {
images: Nav.RootStackParamList['Screen-ImagesViewer']['imageUrls']
imageIndex: number
onRequestClose: () => void
onLongPress?: (
image: Nav.RootStackParamList['Screen-ImagesViewer']['imageUrls'][0]
) => void
onImageIndexChange?: (imageIndex: number) => void
backgroundColor?: string
swipeToCloseEnabled?: boolean
delayLongPress?: number
HeaderComponent: ComponentType<{ imageIndex: number }>
}
const DEFAULT_BG_COLOR = '#000'
const DEFAULT_DELAY_LONG_PRESS = 800
const SCREEN = Dimensions.get('screen')
const SCREEN_WIDTH = SCREEN.width
function ImageViewer ({
images,
imageIndex,
onRequestClose,
onLongPress = () => {},
onImageIndexChange,
backgroundColor = DEFAULT_BG_COLOR,
swipeToCloseEnabled,
delayLongPress = DEFAULT_DELAY_LONG_PRESS,
HeaderComponent
}: Props) {
const imageList = React.createRef<
VirtualizedList<
Nav.RootStackParamList['Screen-ImagesViewer']['imageUrls'][0]
>
>()
const [opacity, onRequestCloseEnhanced] = useRequestClose(onRequestClose)
const [currentImageIndex, onScroll] = useImageIndexChange(imageIndex, SCREEN)
const [headerTransform, toggleBarsVisible] = useAnimatedComponents()
useEffect(() => {
if (onImageIndexChange) {
onImageIndexChange(currentImageIndex)
}
}, [currentImageIndex])
const onZoom = useCallback(
(isScaled: boolean) => {
// @ts-ignore
imageList?.current?.setNativeProps({ scrollEnabled: !isScaled })
toggleBarsVisible(!isScaled)
},
[imageList]
)
return (
<View style={[styles.container, { opacity, backgroundColor }]}>
<Animated.View style={[styles.header, { transform: headerTransform }]}>
{React.createElement(HeaderComponent, {
imageIndex: currentImageIndex
})}
</Animated.View>
<VirtualizedList
ref={imageList}
data={images}
horizontal
pagingEnabled
windowSize={2}
initialNumToRender={1}
maxToRenderPerBatch={1}
showsHorizontalScrollIndicator={false}
showsVerticalScrollIndicator={false}
initialScrollIndex={
imageIndex > images.length - 1 ? images.length - 1 : imageIndex
}
getItem={(_, index) => images[index]}
getItemCount={() => images.length}
getItemLayout={(_, index) => ({
length: SCREEN_WIDTH,
offset: SCREEN_WIDTH * index,
index
})}
renderItem={({ item: imageSrc }) => (
<ImageItem
onZoom={onZoom}
imageSrc={imageSrc}
onRequestClose={onRequestCloseEnhanced}
onLongPress={onLongPress}
delayLongPress={delayLongPress}
swipeToCloseEnabled={swipeToCloseEnabled}
/>
)}
onMomentumScrollEnd={onScroll}
keyExtractor={imageSrc => imageSrc.url}
/>
</View>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#000'
},
header: {
position: 'absolute',
width: '100%',
zIndex: 1,
top: 0
}
})
export default ImageViewer

View File

@ -0,0 +1,117 @@
/**
* Copyright (c) JOB TODAY S.A. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import GracefullyImage from '@components/GracefullyImage'
import React, { useState, useCallback } from 'react'
import { Animated, Dimensions, StyleSheet } from 'react-native'
import usePanResponder from '../hooks/usePanResponder'
import { getImageStyles, getImageTransform } from '../utils'
const SCREEN = Dimensions.get('window')
const SCREEN_WIDTH = SCREEN.width
const SCREEN_HEIGHT = SCREEN.height
type Props = {
imageSrc: Nav.RootStackParamList['Screen-ImagesViewer']['imageUrls'][0]
onRequestClose: () => void
onZoom: (isZoomed: boolean) => void
onLongPress: (
image: Nav.RootStackParamList['Screen-ImagesViewer']['imageUrls'][0]
) => void
delayLongPress: number
swipeToCloseEnabled?: boolean
doubleTapToZoomEnabled?: boolean
}
const ImageItem = ({
imageSrc,
onZoom,
onRequestClose,
onLongPress,
delayLongPress,
doubleTapToZoomEnabled = true
}: Props) => {
const imageContainer = React.createRef<any>()
const [imageDimensions, setImageDimensions] = useState({
width: imageSrc.width || 0,
height: imageSrc.height || 0
})
const [translate, scale] = getImageTransform(imageDimensions, SCREEN)
const onZoomPerformed = (isZoomed: boolean) => {
onZoom(isZoomed)
if (imageContainer?.current) {
// @ts-ignore
imageContainer.current.setNativeProps({
scrollEnabled: !isZoomed
})
}
}
const onLongPressHandler = useCallback(() => {
onLongPress(imageSrc)
}, [imageSrc, onLongPress])
const [panHandlers, scaleValue, translateValue] = usePanResponder({
initialScale: scale || 1,
initialTranslate: translate || { x: 0, y: 0 },
onZoom: onZoomPerformed,
doubleTapToZoomEnabled,
onLongPress: onLongPressHandler,
delayLongPress,
onRequestClose
})
const imagesStyles = getImageStyles(
imageDimensions,
translateValue,
scaleValue
)
return (
<Animated.ScrollView
ref={imageContainer}
style={styles.listItem}
pagingEnabled
nestedScrollEnabled
showsHorizontalScrollIndicator={false}
showsVerticalScrollIndicator={false}
contentContainerStyle={styles.imageScrollContainer}
scrollEnabled={false}
>
<Animated.View
{...panHandlers}
style={imagesStyles}
children={
<GracefullyImage
uri={{
original: imageSrc.url,
remote: imageSrc.remote_url
}}
{...((!imageSrc.width || !imageSrc.height) && {
setImageDimensions
})}
style={{ flex: 1 }}
/>
}
/>
</Animated.ScrollView>
)
}
const styles = StyleSheet.create({
listItem: {
width: SCREEN_WIDTH,
height: SCREEN_HEIGHT
},
imageScrollContainer: {
height: SCREEN_HEIGHT * 2
}
})
export default React.memo(ImageItem)

View File

@ -0,0 +1,32 @@
/**
* Copyright (c) JOB TODAY S.A. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import React from "react";
import { GestureResponderEvent } from "react-native";
import { ImageSource } from "../@types";
declare type Props = {
imageSrc: ImageSource;
onRequestClose: () => void;
onZoom: (isZoomed: boolean) => void;
onLongPress: (image: ImageSource) => void;
delayLongPress: number;
swipeToCloseEnabled?: boolean;
doubleTapToZoomEnabled?: boolean;
};
declare const _default: React.MemoExoticComponent<({
imageSrc,
onZoom,
onRequestClose,
onLongPress,
delayLongPress,
swipeToCloseEnabled,
}: Props) => JSX.Element>;
export default _default;

View File

@ -0,0 +1,175 @@
/**
* Copyright (c) JOB TODAY S.A. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import GracefullyImage from '@components/GracefullyImage'
import React, { createRef, useCallback, useRef, useState } from 'react'
import {
Animated,
Dimensions,
NativeScrollEvent,
NativeSyntheticEvent,
ScrollView,
StyleSheet
} from 'react-native'
import {
LongPressGestureHandler,
State,
TapGestureHandler
} from 'react-native-gesture-handler'
import useDoubleTapToZoom from '../hooks/useDoubleTapToZoom'
import { getImageStyles, getImageTransform } from '../utils'
const SWIPE_CLOSE_OFFSET = 75
const SWIPE_CLOSE_VELOCITY = 0.55
const SCREEN = Dimensions.get('screen')
const SCREEN_WIDTH = SCREEN.width
const SCREEN_HEIGHT = SCREEN.height
type Props = {
imageSrc: Nav.RootStackParamList['Screen-ImagesViewer']['imageUrls'][0]
onRequestClose: () => void
onZoom: (scaled: boolean) => void
onLongPress: (
image: Nav.RootStackParamList['Screen-ImagesViewer']['imageUrls'][0]
) => void
swipeToCloseEnabled?: boolean
}
const doubleTap = createRef()
const ImageItem = ({
imageSrc,
onZoom,
onRequestClose,
onLongPress,
swipeToCloseEnabled = true
}: Props) => {
const scrollViewRef = useRef<ScrollView>(null)
const [scaled, setScaled] = useState(false)
const [imageDimensions, setImageDimensions] = useState({
width: imageSrc.width || 0,
height: imageSrc.height || 0
})
const handleDoubleTap = useDoubleTapToZoom(scrollViewRef, scaled, SCREEN)
const [translate, scale] = getImageTransform(imageDimensions, SCREEN)
const scrollValueY = new Animated.Value(0)
const scaleValue = new Animated.Value(scale || 1)
const translateValue = new Animated.ValueXY(translate)
const maxScale = scale && scale > 0 ? Math.max(1 / scale, 1) : 1
const imageOpacity = scrollValueY.interpolate({
inputRange: [-SWIPE_CLOSE_OFFSET, 0, SWIPE_CLOSE_OFFSET],
outputRange: [0.5, 1, 0.5]
})
const imagesStyles = getImageStyles(
imageDimensions,
translateValue,
scaleValue
)
const imageStylesWithOpacity = { ...imagesStyles, opacity: imageOpacity }
const onScrollEndDrag = useCallback(
({ nativeEvent }: NativeSyntheticEvent<NativeScrollEvent>) => {
const velocityY = nativeEvent?.velocity?.y ?? 0
const scaled = nativeEvent?.zoomScale > 1
onZoom(scaled)
setScaled(scaled)
if (
!scaled &&
swipeToCloseEnabled &&
Math.abs(velocityY) > SWIPE_CLOSE_VELOCITY
) {
onRequestClose()
}
},
[scaled]
)
const onScroll = ({
nativeEvent
}: NativeSyntheticEvent<NativeScrollEvent>) => {
const offsetY = nativeEvent?.contentOffset?.y ?? 0
if (nativeEvent?.zoomScale > 1) {
return
}
scrollValueY.setValue(offsetY)
}
return (
<LongPressGestureHandler
onHandlerStateChange={({ nativeEvent }) => {
if (nativeEvent.state === State.ACTIVE) {
onLongPress(imageSrc)
}
}}
>
<TapGestureHandler
onHandlerStateChange={({ nativeEvent }) =>
nativeEvent.state === State.ACTIVE && onRequestClose()
}
waitFor={doubleTap}
>
<TapGestureHandler
ref={doubleTap}
onHandlerStateChange={handleDoubleTap}
numberOfTaps={2}
>
<ScrollView
ref={scrollViewRef}
style={styles.listItem}
pinchGestureEnabled
nestedScrollEnabled={true}
showsHorizontalScrollIndicator={false}
showsVerticalScrollIndicator={false}
maximumZoomScale={maxScale}
contentContainerStyle={styles.imageScrollContainer}
scrollEnabled={swipeToCloseEnabled}
onScrollEndDrag={onScrollEndDrag}
scrollEventThrottle={1}
{...(swipeToCloseEnabled && {
onScroll
})}
>
<Animated.View
style={imageStylesWithOpacity}
children={
<GracefullyImage
uri={{
original: imageSrc.url,
remote: imageSrc.remote_url
}}
{...((!imageSrc.width || !imageSrc.height) && {
setImageDimensions
})}
style={{ flex: 1 }}
/>
}
/>
</ScrollView>
</TapGestureHandler>
</TapGestureHandler>
</LongPressGestureHandler>
)
}
const styles = StyleSheet.create({
listItem: {
width: SCREEN_WIDTH,
height: SCREEN_HEIGHT
},
imageScrollContainer: {
height: SCREEN_HEIGHT
}
})
export default React.memo(ImageItem)

View File

@ -0,0 +1,40 @@
/**
* Copyright (c) JOB TODAY S.A. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import { Animated } from 'react-native'
const INITIAL_POSITION = { x: 0, y: 0 }
const ANIMATION_CONFIG = {
duration: 200,
useNativeDriver: true
}
const useAnimatedComponents = () => {
const headerTranslate = new Animated.ValueXY(INITIAL_POSITION)
const toggleVisible = (isVisible: boolean) => {
if (isVisible) {
Animated.parallel([
Animated.timing(headerTranslate.y, { ...ANIMATION_CONFIG, toValue: 0 })
]).start()
} else {
Animated.parallel([
Animated.timing(headerTranslate.y, {
...ANIMATION_CONFIG,
toValue: -300
})
]).start()
}
}
const headerTransform = headerTranslate.getTranslateTransform()
return [headerTransform, toggleVisible] as const
}
export default useAnimatedComponents

View File

@ -0,0 +1,64 @@
/**
* Copyright (c) JOB TODAY S.A. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import React, { useCallback } from 'react'
import { ScrollView } from 'react-native'
import {
HandlerStateChangeEvent,
State,
TapGestureHandlerEventPayload
} from 'react-native-gesture-handler'
import { Dimensions } from '../@types'
/**
* This is iOS only.
* Same functionality for Android implemented inside usePanResponder hook.
*/
function useDoubleTapToZoom (
scrollViewRef: React.RefObject<ScrollView>,
scaled: boolean,
screen: Dimensions
) {
const handleDoubleTap = useCallback(
({
nativeEvent
}: HandlerStateChangeEvent<TapGestureHandlerEventPayload>) => {
if (nativeEvent.state === State.ACTIVE) {
const scrollResponderRef = scrollViewRef?.current?.getScrollResponder()
const { absoluteX, absoluteY } = nativeEvent
let targetX = 0
let targetY = 0
let targetWidth = screen.width
let targetHeight = screen.height
// Zooming in
// TODO: Add more precise calculation of targetX, targetY based on touch
if (!scaled) {
targetX = absoluteX / 2
targetY = absoluteY / 2
targetWidth = screen.width / 2
targetHeight = screen.height / 2
}
scrollResponderRef?.scrollResponderZoomTo({
x: targetX,
y: targetY,
width: targetWidth,
height: targetHeight,
animated: true
})
}
},
[scaled]
)
return handleDoubleTap
}
export default useDoubleTapToZoom

View File

@ -0,0 +1,31 @@
/**
* Copyright (c) JOB TODAY S.A. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import { useState } from 'react'
import { NativeSyntheticEvent, NativeScrollEvent } from 'react-native'
import { Dimensions } from '../@types'
const useImageIndexChange = (imageIndex: number, screen: Dimensions) => {
const [currentImageIndex, setImageIndex] = useState(imageIndex)
const onScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
const {
nativeEvent: {
contentOffset: { x: scrollX }
}
} = event
if (screen.width) {
const nextIndex = Math.round(scrollX / screen.width)
setImageIndex(nextIndex < 0 ? 0 : nextIndex)
}
}
return [currentImageIndex, onScroll] as const
}
export default useImageIndexChange

View File

@ -0,0 +1,407 @@
/**
* Copyright (c) JOB TODAY S.A. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import { useMemo, useEffect } from 'react'
import {
Animated,
Dimensions,
GestureResponderEvent,
GestureResponderHandlers,
NativeTouchEvent,
PanResponderGestureState
} from 'react-native'
import { Position } from '../@types'
import {
createPanResponder,
getDistanceBetweenTouches,
getImageTranslate,
getImageDimensionsByTranslate
} from '../utils'
const SCREEN = Dimensions.get('window')
const SCREEN_WIDTH = SCREEN.width
const SCREEN_HEIGHT = SCREEN.height
const MIN_DIMENSION = Math.min(SCREEN_WIDTH, SCREEN_HEIGHT)
const SCALE_MAX = 1
const DOUBLE_TAP_DELAY = 300
const OUT_BOUND_MULTIPLIER = 0.75
type Props = {
initialScale: number
initialTranslate: Position
onZoom: (isZoomed: boolean) => void
doubleTapToZoomEnabled: boolean
onLongPress: () => void
delayLongPress: number
onRequestClose: () => void
}
const usePanResponder = ({
initialScale,
initialTranslate,
onZoom,
doubleTapToZoomEnabled,
onLongPress,
delayLongPress,
onRequestClose
}: Props): Readonly<[
GestureResponderHandlers,
Animated.Value,
Animated.ValueXY
]> => {
let numberInitialTouches = 1
let initialTouches: NativeTouchEvent[] = []
let currentScale = initialScale
let currentTranslate = initialTranslate
let tmpScale = 0
let tmpTranslate: Position | null = null
let isDoubleTapPerformed = false
let lastTapTS: number | null = null
let timer: number | null = null
let longPressHandlerRef: number | null = null
const meaningfulShift = MIN_DIMENSION * 0.01
const scaleValue = new Animated.Value(initialScale)
const translateValue = new Animated.ValueXY(initialTranslate)
const imageDimensions = getImageDimensionsByTranslate(
initialTranslate,
SCREEN
)
const getBounds = (scale: number) => {
const scaledImageDimensions = {
width: imageDimensions.width * scale,
height: imageDimensions.height * scale
}
const translateDelta = getImageTranslate(scaledImageDimensions, SCREEN)
const left = initialTranslate.x - translateDelta.x
const right = left - (scaledImageDimensions.width - SCREEN.width)
const top = initialTranslate.y - translateDelta.y
const bottom = top - (scaledImageDimensions.height - SCREEN.height)
return [top, left, bottom, right]
}
const getTranslateInBounds = (translate: Position, scale: number) => {
const inBoundTranslate = { x: translate.x, y: translate.y }
const [topBound, leftBound, bottomBound, rightBound] = getBounds(scale)
if (translate.x > leftBound) {
inBoundTranslate.x = leftBound
} else if (translate.x < rightBound) {
inBoundTranslate.x = rightBound
}
if (translate.y > topBound) {
inBoundTranslate.y = topBound
} else if (translate.y < bottomBound) {
inBoundTranslate.y = bottomBound
}
return inBoundTranslate
}
const fitsScreenByWidth = () =>
imageDimensions.width * currentScale < SCREEN_WIDTH
const fitsScreenByHeight = () =>
imageDimensions.height * currentScale < SCREEN_HEIGHT
useEffect(() => {
scaleValue.addListener(({ value }) => {
if (typeof onZoom === 'function') {
onZoom(value !== initialScale)
}
})
return () => scaleValue.removeAllListeners()
})
const cancelLongPressHandle = () => {
longPressHandlerRef && clearTimeout(longPressHandlerRef)
}
const handlers = {
onGrant: (
_: GestureResponderEvent,
gestureState: PanResponderGestureState
) => {
numberInitialTouches = gestureState.numberActiveTouches
if (gestureState.numberActiveTouches > 1) return
longPressHandlerRef = setTimeout(onLongPress, delayLongPress)
},
onStart: (
event: GestureResponderEvent,
gestureState: PanResponderGestureState
) => {
initialTouches = event.nativeEvent.touches
numberInitialTouches = gestureState.numberActiveTouches
if (gestureState.numberActiveTouches > 1) return
const tapTS = Date.now()
!timer &&
(timer = setTimeout(() => onRequestClose(), DOUBLE_TAP_DELAY + 50))
// Handle double tap event by calculating diff between first and second taps timestamps
isDoubleTapPerformed = Boolean(
lastTapTS && tapTS - lastTapTS < DOUBLE_TAP_DELAY
)
if (doubleTapToZoomEnabled && isDoubleTapPerformed) {
clearTimeout(timer)
const isScaled = currentTranslate.x !== initialTranslate.x // currentScale !== initialScale;
const { pageX: touchX, pageY: touchY } = event.nativeEvent.touches[0]
const targetScale = SCALE_MAX
const nextScale = isScaled ? initialScale : targetScale
const nextTranslate = isScaled
? initialTranslate
: getTranslateInBounds(
{
x:
initialTranslate.x +
(SCREEN_WIDTH / 2 - touchX) * (targetScale / currentScale),
y:
initialTranslate.y +
(SCREEN_HEIGHT / 2 - -touchY) * (targetScale / currentScale)
},
targetScale
)
onZoom(!isScaled)
Animated.parallel(
[
Animated.timing(translateValue.x, {
toValue: nextTranslate.x,
duration: 300,
useNativeDriver: true
}),
Animated.timing(translateValue.y, {
toValue: nextTranslate.y,
duration: 300,
useNativeDriver: true
}),
Animated.timing(scaleValue, {
toValue: nextScale,
duration: 300,
useNativeDriver: true
})
],
{ stopTogether: false }
).start(() => {
currentScale = nextScale
currentTranslate = nextTranslate
})
lastTapTS = null
timer = null
} else {
lastTapTS = Date.now()
}
},
onMove: (
event: GestureResponderEvent,
gestureState: PanResponderGestureState
) => {
const { dx, dy } = gestureState
if (Math.abs(dx) >= meaningfulShift || Math.abs(dy) >= meaningfulShift) {
cancelLongPressHandle()
timer && clearTimeout(timer)
}
// Don't need to handle move because double tap in progress (was handled in onStart)
if (doubleTapToZoomEnabled && isDoubleTapPerformed) {
cancelLongPressHandle()
timer && clearTimeout(timer)
return
}
if (
numberInitialTouches === 1 &&
gestureState.numberActiveTouches === 2
) {
numberInitialTouches = 2
initialTouches = event.nativeEvent.touches
}
const isTapGesture =
numberInitialTouches == 1 && gestureState.numberActiveTouches === 1
const isPinchGesture =
numberInitialTouches === 2 && gestureState.numberActiveTouches === 2
if (isPinchGesture) {
cancelLongPressHandle()
timer && clearTimeout(timer)
const initialDistance = getDistanceBetweenTouches(initialTouches)
const currentDistance = getDistanceBetweenTouches(
event.nativeEvent.touches
)
let nextScale = (currentDistance / initialDistance) * currentScale
/**
* In case image is scaling smaller than initial size ->
* slow down this transition by applying OUT_BOUND_MULTIPLIER
*/
if (nextScale < initialScale) {
nextScale =
nextScale + (initialScale - nextScale) * OUT_BOUND_MULTIPLIER
}
/**
* In case image is scaling down -> move it in direction of initial position
*/
if (currentScale > initialScale && currentScale > nextScale) {
const k = (currentScale - initialScale) / (currentScale - nextScale)
const nextTranslateX =
nextScale < initialScale
? initialTranslate.x
: currentTranslate.x -
(currentTranslate.x - initialTranslate.x) / k
const nextTranslateY =
nextScale < initialScale
? initialTranslate.y
: currentTranslate.y -
(currentTranslate.y - initialTranslate.y) / k
translateValue.x.setValue(nextTranslateX)
translateValue.y.setValue(nextTranslateY)
tmpTranslate = { x: nextTranslateX, y: nextTranslateY }
}
scaleValue.setValue(nextScale)
tmpScale = nextScale
}
if (isTapGesture && currentScale > initialScale) {
const { x, y } = currentTranslate
const { dx, dy } = gestureState
const [topBound, leftBound, bottomBound, rightBound] = getBounds(
currentScale
)
let nextTranslateX = x + dx
let nextTranslateY = y + dy
if (nextTranslateX > leftBound) {
nextTranslateX =
nextTranslateX - (nextTranslateX - leftBound) * OUT_BOUND_MULTIPLIER
}
if (nextTranslateX < rightBound) {
nextTranslateX =
nextTranslateX -
(nextTranslateX - rightBound) * OUT_BOUND_MULTIPLIER
}
if (nextTranslateY > topBound) {
nextTranslateY =
nextTranslateY - (nextTranslateY - topBound) * OUT_BOUND_MULTIPLIER
}
if (nextTranslateY < bottomBound) {
nextTranslateY =
nextTranslateY -
(nextTranslateY - bottomBound) * OUT_BOUND_MULTIPLIER
}
if (fitsScreenByWidth()) {
nextTranslateX = x
}
if (fitsScreenByHeight()) {
nextTranslateY = y
}
translateValue.x.setValue(nextTranslateX)
translateValue.y.setValue(nextTranslateY)
tmpTranslate = { x: nextTranslateX, y: nextTranslateY }
}
},
onRelease: () => {
cancelLongPressHandle()
if (isDoubleTapPerformed) {
isDoubleTapPerformed = false
}
if (tmpScale > 0) {
if (tmpScale < initialScale || tmpScale > SCALE_MAX) {
tmpScale = tmpScale < initialScale ? initialScale : SCALE_MAX
Animated.timing(scaleValue, {
toValue: tmpScale,
duration: 100,
useNativeDriver: true
}).start()
}
currentScale = tmpScale
tmpScale = 0
}
if (tmpTranslate) {
const { x, y } = tmpTranslate
const [topBound, leftBound, bottomBound, rightBound] = getBounds(
currentScale
)
let nextTranslateX = x
let nextTranslateY = y
if (!fitsScreenByWidth()) {
if (nextTranslateX > leftBound) {
nextTranslateX = leftBound
} else if (nextTranslateX < rightBound) {
nextTranslateX = rightBound
}
}
if (!fitsScreenByHeight()) {
if (nextTranslateY > topBound) {
nextTranslateY = topBound
} else if (nextTranslateY < bottomBound) {
nextTranslateY = bottomBound
}
}
Animated.parallel([
Animated.timing(translateValue.x, {
toValue: nextTranslateX,
duration: 100,
useNativeDriver: true
}),
Animated.timing(translateValue.y, {
toValue: nextTranslateY,
duration: 100,
useNativeDriver: true
})
]).start()
currentTranslate = { x: nextTranslateX, y: nextTranslateY }
tmpTranslate = null
}
}
}
const panResponder = useMemo(() => createPanResponder(handlers), [handlers])
return [panResponder.panHandlers, scaleValue, translateValue]
}
export default usePanResponder

View File

@ -0,0 +1,23 @@
/**
* Copyright (c) JOB TODAY S.A. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import { useState } from 'react'
const useRequestClose = (onRequestClose: () => void) => {
const [opacity, setOpacity] = useState(1)
return [
opacity,
() => {
setOpacity(0)
onRequestClose()
}
] as const
}
export default useRequestClose

View File

@ -0,0 +1,146 @@
/**
* Copyright (c) JOB TODAY S.A. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import {
Animated,
GestureResponderEvent,
PanResponder,
PanResponderGestureState,
PanResponderInstance,
NativeTouchEvent
} from 'react-native'
import { Dimensions, Position } from './@types'
export const getImageTransform = (
image: Dimensions | null,
screen: Dimensions
) => {
if (!image?.width || !image?.height) {
return [] as const
}
const wScale = screen.width / image.width
const hScale = screen.height / image.height
const scale = Math.min(wScale, hScale)
const { x, y } = getImageTranslate(image, screen)
return [{ x, y }, scale] as const
}
export const getImageStyles = (
image: Dimensions | null,
translate: Animated.ValueXY,
scale?: Animated.Value
) => {
if (!image?.width || !image?.height) {
return { width: 0, height: 0 }
}
const transform = translate.getTranslateTransform()
if (scale) {
transform.push({ scale }, { perspective: new Animated.Value(1000) })
}
return {
width: image.width,
height: image.height,
transform
}
}
export const getImageTranslate = (
image: Dimensions,
screen: Dimensions
): Position => {
const getTranslateForAxis = (axis: 'x' | 'y'): number => {
const imageSize = axis === 'x' ? image.width : image.height
const screenSize = axis === 'x' ? screen.width : screen.height
return (screenSize - imageSize) / 2
}
return {
x: getTranslateForAxis('x'),
y: getTranslateForAxis('y')
}
}
export const getImageDimensionsByTranslate = (
translate: Position,
screen: Dimensions
): Dimensions => ({
width: screen.width - translate.x * 2,
height: screen.height - translate.y * 2
})
export const getImageTranslateForScale = (
currentTranslate: Position,
targetScale: number,
screen: Dimensions
): Position => {
const { width, height } = getImageDimensionsByTranslate(
currentTranslate,
screen
)
const targetImageDimensions = {
width: width * targetScale,
height: height * targetScale
}
return getImageTranslate(targetImageDimensions, screen)
}
type HandlerType = (
event: GestureResponderEvent,
state: PanResponderGestureState
) => void
type PanResponderProps = {
onGrant: HandlerType
onStart?: HandlerType
onMove: HandlerType
onRelease?: HandlerType
onTerminate?: HandlerType
}
export const createPanResponder = ({
onGrant,
onStart,
onMove,
onRelease,
onTerminate
}: PanResponderProps): PanResponderInstance =>
PanResponder.create({
onStartShouldSetPanResponder: () => true,
onStartShouldSetPanResponderCapture: () => true,
onMoveShouldSetPanResponder: () => true,
onMoveShouldSetPanResponderCapture: () => true,
onPanResponderGrant: onGrant,
onPanResponderStart: onStart,
onPanResponderMove: onMove,
onPanResponderRelease: onRelease,
onPanResponderTerminate: onTerminate,
onPanResponderTerminationRequest: () => false,
onShouldBlockNativeResponder: () => false
})
export const getDistanceBetweenTouches = (
touches: NativeTouchEvent[]
): number => {
const [a, b] = touches
if (a == null || b == null) {
return 0
}
return Math.sqrt(
Math.pow(a.pageX - b.pageX, 2) + Math.pow(a.pageY - b.pageY, 2)
)
}

View File

@ -4,7 +4,6 @@ import { HeaderCenter, HeaderLeft, HeaderRight } from '@components/Header'
import { useActionSheet } from '@expo/react-native-action-sheet' import { useActionSheet } from '@expo/react-native-action-sheet'
import CameraRoll from '@react-native-community/cameraroll' import CameraRoll from '@react-native-community/cameraroll'
import { StackScreenProps } from '@react-navigation/stack' import { StackScreenProps } from '@react-navigation/stack'
import ImageView from '@root/modules/react-native-image-viewing/src/index'
import { findIndex } from 'lodash' import { findIndex } from 'lodash'
import React, { useCallback, useState } from 'react' import React, { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@ -19,6 +18,38 @@ import {
SafeAreaProvider, SafeAreaProvider,
useSafeAreaInsets useSafeAreaInsets
} from 'react-native-safe-area-context' } from 'react-native-safe-area-context'
import ImageViewer from './ImageViewer/Root'
const saveImage = async (
image: Nav.RootStackParamList['Screen-ImagesViewer']['imageUrls'][0]
) => {
const hasAndroidPermission = async () => {
const permission = PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE
const hasPermission = await PermissionsAndroid.check(permission)
if (hasPermission) {
return true
}
const status = await PermissionsAndroid.request(permission)
return status === 'granted'
}
if (Platform.OS === 'android' && !(await hasAndroidPermission())) {
return
}
CameraRoll.save(image.url)
.then(() => haptics('Success'))
.catch(() => {
if (image.remote_url) {
CameraRoll.save(image.remote_url)
.then(() => haptics('Success'))
.catch(() => haptics('Error'))
} else {
haptics('Error')
}
})
}
const HeaderComponent = React.memo( const HeaderComponent = React.memo(
({ ({
@ -28,43 +59,12 @@ const HeaderComponent = React.memo(
}: { }: {
navigation: ScreenImagesViewerProp['navigation'] navigation: ScreenImagesViewerProp['navigation']
currentIndex: number currentIndex: number
imageUrls: { imageUrls: Nav.RootStackParamList['Screen-ImagesViewer']['imageUrls']
url: string
width?: number | undefined
height?: number | undefined
preview_url: string
remote_url?: string | undefined
}[]
}) => { }) => {
const insets = useSafeAreaInsets() const insets = useSafeAreaInsets()
const { t } = useTranslation('screenImageViewer') const { t } = useTranslation('screenImageViewer')
const { showActionSheetWithOptions } = useActionSheet() const { showActionSheetWithOptions } = useActionSheet()
const hasAndroidPermission = async () => {
const permission = PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE
const hasPermission = await PermissionsAndroid.check(permission)
if (hasPermission) {
return true
}
const status = await PermissionsAndroid.request(permission)
return status === 'granted'
}
const saveImage = async () => {
if (Platform.OS === 'android' && !(await hasAndroidPermission())) {
return
}
CameraRoll.save(
imageUrls[currentIndex].url ||
imageUrls[currentIndex].remote_url ||
imageUrls[currentIndex].preview_url
)
.then(() => haptics('Success'))
.catch(() => haptics('Error'))
}
const onPress = useCallback(() => { const onPress = useCallback(() => {
analytics('imageviewer_more_press') analytics('imageviewer_more_press')
showActionSheetWithOptions( showActionSheetWithOptions(
@ -80,7 +80,7 @@ const HeaderComponent = React.memo(
switch (buttonIndex) { switch (buttonIndex) {
case 0: case 0:
analytics('imageviewer_more_save_press') analytics('imageviewer_more_save_press')
saveImage() saveImage(imageUrls[currentIndex])
break break
case 1: case 1:
analytics('imageviewer_more_share_press') analytics('imageviewer_more_share_press')
@ -147,11 +147,12 @@ const ScreenImagesViewer = ({
return ( return (
<SafeAreaProvider> <SafeAreaProvider>
<StatusBar backgroundColor='rgb(0,0,0)' /> <StatusBar backgroundColor='rgb(0,0,0)' />
<ImageView <ImageViewer
images={imageUrls} images={imageUrls}
imageIndex={initialIndex} imageIndex={initialIndex}
onImageIndexChange={index => setCurrentIndex(index)} onImageIndexChange={index => setCurrentIndex(index)}
onRequestClose={() => navigation.goBack()} onRequestClose={() => navigation.goBack()}
onLongPress={saveImage}
HeaderComponent={() => ( HeaderComponent={() => (
<HeaderComponent <HeaderComponent
navigation={navigation} navigation={navigation}

View File

@ -96,7 +96,7 @@ const ScreenTabs = React.memo(
) )
const tabBarOptions = useMemo( const tabBarOptions = useMemo(
() => ({ () => ({
activeTintColor: theme.primary, activeTintColor: theme.primaryDefault,
inactiveTintColor: theme.secondary, inactiveTintColor: theme.secondary,
showLabel: false, showLabel: false,
...(Platform.OS === 'android' && { keyboardHidesTabBar: true }) ...(Platform.OS === 'android' && { keyboardHidesTabBar: true })

View File

@ -90,7 +90,8 @@ const ScreenMeSettingsFontsize: React.FC<StackScreenProps<
initialSize === size initialSize === size
? StyleConstants.Font.Weight.Bold ? StyleConstants.Font.Weight.Bold
: undefined, : undefined,
color: initialSize === size ? theme.primary : theme.secondary, color:
initialSize === size ? theme.primaryDefault : theme.secondary,
borderWidth: StyleSheet.hairlineWidth, borderWidth: StyleSheet.hairlineWidth,
borderColor: theme.border borderColor: theme.border
} }
@ -105,7 +106,7 @@ const ScreenMeSettingsFontsize: React.FC<StackScreenProps<
return ( return (
<ScrollView scrollEnabled={false}> <ScrollView scrollEnabled={false}>
<Text style={[styles.header, { color: theme.primary }]}> <Text style={[styles.header, { color: theme.primaryDefault }]}>
{t('content.showcase')} {t('content.showcase')}
</Text> </Text>
<View> <View>
@ -119,7 +120,7 @@ const ScreenMeSettingsFontsize: React.FC<StackScreenProps<
extraMarginRight={-StyleConstants.Spacing.Global.PagePadding} extraMarginRight={-StyleConstants.Spacing.Global.PagePadding}
/> />
</View> </View>
<Text style={[styles.header, { color: theme.primary }]}> <Text style={[styles.header, { color: theme.primaryDefault }]}>
{t('content.availableSizes')} {t('content.availableSizes')}
</Text> </Text>
<View style={styles.sizesDemo}>{sizesDemo}</View> <View style={styles.sizesDemo}>{sizesDemo}</View>

View File

@ -94,7 +94,7 @@ const ScreenMeSettingsPush: React.FC = () => {
setPushEnabled(result.granted) setPushEnabled(result.granted)
setPushCanAskAgain(result.canAskAgain) setPushCanAskAgain(result.canAskAgain)
} else { } else {
Linking.openURL('app-settings:') Linking.openSettings()
} }
}} }}
/> />

View File

@ -22,7 +22,7 @@ const SettingsDev: React.FC = () => {
style={{ style={{
paddingHorizontal: StyleConstants.Spacing.Global.PagePadding, paddingHorizontal: StyleConstants.Spacing.Global.PagePadding,
...StyleConstants.FontStyle.S, ...StyleConstants.FontStyle.S,
color: theme.primary color: theme.primaryDefault
}} }}
> >
{instances[instanceActive]?.token} {instances[instanceActive]?.token}

View File

@ -56,7 +56,7 @@ const ScreenMeSwitchRoot: React.FC = () => {
return ( return (
<ScrollView style={styles.base} keyboardShouldPersistTaps='always'> <ScrollView style={styles.base} keyboardShouldPersistTaps='always'>
<View style={[styles.firstSection, { borderBottomColor: theme.border }]}> <View style={[styles.firstSection, { borderBottomColor: theme.border }]}>
<Text style={[styles.header, { color: theme.primary }]}> <Text style={[styles.header, { color: theme.primaryDefault }]}>
{t('content.existing')} {t('content.existing')}
</Text> </Text>
<View style={styles.accountButtons}> <View style={styles.accountButtons}>
@ -87,7 +87,7 @@ const ScreenMeSwitchRoot: React.FC = () => {
</View> </View>
<View style={styles.secondSection}> <View style={styles.secondSection}>
<Text style={[styles.header, { color: theme.primary }]}> <Text style={[styles.header, { color: theme.primaryDefault }]}>
{t('content.new')} {t('content.new')}
</Text> </Text>
<ComponentInstance disableHeaderImage goBack /> <ComponentInstance disableHeaderImage goBack />

View File

@ -70,7 +70,7 @@ const AccountAttachments = React.memo(
<View <View
style={{ style={{
marginHorizontal: StyleConstants.Spacing.Global.PagePadding, marginHorizontal: StyleConstants.Spacing.Global.PagePadding,
backgroundColor: theme.backgroundOverlay, backgroundColor: theme.backgroundOverlayInvert,
width: width, width: width,
height: width, height: width,
justifyContent: 'center', justifyContent: 'center',

View File

@ -34,7 +34,7 @@ const AccountInformationFields = React.memo(
<Icon <Icon
name='CheckCircle' name='CheckCircle'
size={StyleConstants.Font.Size.M} size={StyleConstants.Font.Size.M}
color={theme.primary} color={theme.primaryDefault}
style={styles.fieldCheck} style={styles.fieldCheck}
/> />
) : null} ) : null}

View File

@ -24,7 +24,7 @@ const AccountInformationStats: React.FC<Props> = ({ account, myInfo }) => {
<View style={[styles.stats, { flexDirection: 'row' }]}> <View style={[styles.stats, { flexDirection: 'row' }]}>
{account ? ( {account ? (
<Text <Text
style={[styles.stat, { color: theme.primary }]} style={[styles.stat, { color: theme.primaryDefault }]}
children={t('content.summary.statuses_count', { children={t('content.summary.statuses_count', {
count: account.statuses_count || 0 count: account.statuses_count || 0
})} })}
@ -46,7 +46,7 @@ const AccountInformationStats: React.FC<Props> = ({ account, myInfo }) => {
)} )}
{account ? ( {account ? (
<Text <Text
style={[styles.stat, { color: theme.primary, textAlign: 'right' }]} style={[styles.stat, { color: theme.primaryDefault, textAlign: 'right' }]}
children={t('content.summary.following_count', { children={t('content.summary.following_count', {
count: account.following_count count: account.following_count
})} })}
@ -73,7 +73,7 @@ const AccountInformationStats: React.FC<Props> = ({ account, myInfo }) => {
)} )}
{account ? ( {account ? (
<Text <Text
style={[styles.stat, { color: theme.primary, textAlign: 'center' }]} style={[styles.stat, { color: theme.primaryDefault, textAlign: 'center' }]}
children={t('content.summary.followers_count', { children={t('content.summary.followers_count', {
count: account.followers_count count: account.followers_count
})} })}

View File

@ -49,7 +49,7 @@ const AccountNav: React.FC<Props> = ({ scrollY, account }) => {
style={[ style={[
styles.base, styles.base,
styleOpacity, styleOpacity,
{ backgroundColor: theme.background, height: headerHeight } { backgroundColor: theme.backgroundDefault, height: headerHeight }
]} ]}
> >
<View <View

View File

@ -73,7 +73,7 @@ const TabSharedSearch: React.FC<SharedSearchProp> = ({
style={[ style={[
styles.emptyDefault, styles.emptyDefault,
styles.emptyFontSize, styles.emptyFontSize,
{ color: theme.primary } { color: theme.primaryDefault }
]} ]}
> >
<Trans <Trans
@ -81,25 +81,25 @@ const TabSharedSearch: React.FC<SharedSearchProp> = ({
components={{ bold: <Text style={styles.emptyFontBold} /> }} components={{ bold: <Text style={styles.emptyFontBold} /> }}
/> />
</Text> </Text>
<Text style={[styles.emptyAdvanced, { color: theme.primary }]}> <Text style={[styles.emptyAdvanced, { color: theme.primaryDefault }]}>
{t('content.empty.advanced.header')} {t('content.empty.advanced.header')}
</Text> </Text>
<Text style={[styles.emptyAdvanced, { color: theme.primary }]}> <Text style={[styles.emptyAdvanced, { color: theme.primaryDefault }]}>
<Text style={{ color: theme.secondary }}>@username@domain</Text> <Text style={{ color: theme.secondary }}>@username@domain</Text>
{' '} {' '}
{t('content.empty.advanced.example.account')} {t('content.empty.advanced.example.account')}
</Text> </Text>
<Text style={[styles.emptyAdvanced, { color: theme.primary }]}> <Text style={[styles.emptyAdvanced, { color: theme.primaryDefault }]}>
<Text style={{ color: theme.secondary }}>#example</Text> <Text style={{ color: theme.secondary }}>#example</Text>
{' '} {' '}
{t('content.empty.advanced.example.hashtag')} {t('content.empty.advanced.example.hashtag')}
</Text> </Text>
<Text style={[styles.emptyAdvanced, { color: theme.primary }]}> <Text style={[styles.emptyAdvanced, { color: theme.primaryDefault }]}>
<Text style={{ color: theme.secondary }}>URL</Text> <Text style={{ color: theme.secondary }}>URL</Text>
{' '} {' '}
{t('content.empty.advanced.example.statusLink')} {t('content.empty.advanced.example.statusLink')}
</Text> </Text>
<Text style={[styles.emptyAdvanced, { color: theme.primary }]}> <Text style={[styles.emptyAdvanced, { color: theme.primaryDefault }]}>
<Text style={{ color: theme.secondary }}>URL</Text> <Text style={{ color: theme.secondary }}>URL</Text>
{' '} {' '}
{t('content.empty.advanced.example.accountLink')} {t('content.empty.advanced.example.accountLink')}
@ -113,9 +113,9 @@ const TabSharedSearch: React.FC<SharedSearchProp> = ({
const sectionHeader = useCallback( const sectionHeader = useCallback(
({ section: { translation } }) => ( ({ section: { translation } }) => (
<View <View
style={[styles.sectionHeader, { backgroundColor: theme.background }]} style={[styles.sectionHeader, { backgroundColor: theme.backgroundDefault }]}
> >
<Text style={[styles.sectionHeaderText, { color: theme.primary }]}> <Text style={[styles.sectionHeaderText, { color: theme.primaryDefault }]}>
{translation} {translation}
</Text> </Text>
</View> </View>
@ -126,7 +126,7 @@ const TabSharedSearch: React.FC<SharedSearchProp> = ({
({ section: { data, translation } }) => ({ section: { data, translation } }) =>
!data.length ? ( !data.length ? (
<View <View
style={[styles.sectionFooter, { backgroundColor: theme.background }]} style={[styles.sectionFooter, { backgroundColor: theme.backgroundDefault }]}
> >
<Text style={[styles.sectionFooterText, { color: theme.secondary }]}> <Text style={[styles.sectionFooterText, { color: theme.secondary }]}>
<Trans <Trans

View File

@ -14,11 +14,11 @@ import { debounce } from 'lodash'
import React from 'react' import React from 'react'
import { Trans, useTranslation } from 'react-i18next' import { Trans, useTranslation } from 'react-i18next'
import { Platform, StyleSheet, Text, TextInput, View } from 'react-native' import { Platform, StyleSheet, Text, TextInput, View } from 'react-native'
import { NativeStackNavigationOptions } from 'react-native-screens/lib/typescript/native-stack' import { NativeStackNavigationOptions } from 'react-native-screens/lib/typescript'
import { import {
NativeStackNavigationEventMap, NativeStackNavigationEventMap,
NativeStackNavigatorProps NativeStackNavigatorProps
} from 'react-native-screens/lib/typescript/native-stack/types' } from 'react-native-screens/lib/typescript/types'
export type BaseScreens = export type BaseScreens =
| Nav.TabLocalStackParamList | Nav.TabLocalStackParamList
@ -103,7 +103,7 @@ const sharedScreens = (
<Text <Text
style={{ style={{
...StyleConstants.FontStyle.M, ...StyleConstants.FontStyle.M,
color: theme.primary, color: theme.primaryDefault,
fontWeight: StyleConstants.Font.Weight.Bold fontWeight: StyleConstants.Font.Weight.Bold
}} }}
/> />
@ -153,7 +153,7 @@ const sharedScreens = (
style={[ style={[
styles.textInput, styles.textInput,
{ {
color: theme.primary color: theme.primaryDefault
} }
]} ]}
children={t('sharedSearch:content.header.prefix')} children={t('sharedSearch:content.header.prefix')}
@ -166,7 +166,7 @@ const sharedScreens = (
styles.textInput, styles.textInput,
{ {
flex: 1, flex: 1,
color: theme.primary, color: theme.primaryDefault,
paddingLeft: StyleConstants.Spacing.XS paddingLeft: StyleConstants.Spacing.XS
} }
]} ]}

View File

@ -11,7 +11,7 @@ export type QueryKey = [
} }
] ]
type SearchResult = { export type SearchResult = {
accounts: Mastodon.Account[] accounts: Mastodon.Account[]
hashtags: Mastodon.Tag[] hashtags: Mastodon.Tag[]
statuses: Mastodon.Status[] statuses: Mastodon.Status[]
@ -23,7 +23,12 @@ const queryFunction = ({ queryKey }: { queryKey: QueryKey }) => {
version: 'v2', version: 'v2',
method: 'get', method: 'get',
url: 'search', url: 'search',
params: { ...(type && { type }), ...(term && { q: term }), limit } params: {
...(type && { type }),
...(term && { q: term }),
limit,
resolve: true
}
}).then(res => res.body) }).then(res => res.body)
} }

View File

@ -1,16 +1,17 @@
import { DefaultTheme, DarkTheme } from '@react-navigation/native' import { DefaultTheme, DarkTheme } from '@react-navigation/native'
export type ColorDefinitions = export type ColorDefinitions =
| 'primary' | 'primaryDefault'
| 'primaryOverlay' | 'primaryOverlay'
| 'secondary' | 'secondary'
| 'disabled' | 'disabled'
| 'blue' | 'blue'
| 'red' | 'red'
| 'background' | 'green'
| 'backgroundGradientStart' | 'yellow'
| 'backgroundGradientEnd' | 'backgroundDefault'
| 'backgroundOverlay' | 'backgroundOverlayDefault'
| 'backgroundOverlayInvert'
| 'border' | 'border'
| 'shimmerDefault' | 'shimmerDefault'
| 'shimmerHighlight' | 'shimmerHighlight'
@ -21,7 +22,7 @@ const themeColors: {
dark: string dark: string
} }
} = { } = {
primary: { primaryDefault: {
light: 'rgb(18, 18, 18)', light: 'rgb(18, 18, 18)',
dark: 'rgb(180, 180, 180)' dark: 'rgb(180, 180, 180)'
}, },
@ -45,27 +46,33 @@ const themeColors: {
light: 'rgb(225, 45, 35)', light: 'rgb(225, 45, 35)',
dark: 'rgb(225, 78, 79)' dark: 'rgb(225, 78, 79)'
}, },
green: {
light: 'rgb(18, 158, 80)',
dark: 'rgb(18, 158, 80)'
},
yellow: {
light: 'rgb(230, 166, 30)',
dark: 'rgb(200, 145, 25)'
},
background: { backgroundDefault: {
light: 'rgb(250, 250, 250)', light: 'rgb(250, 250, 250)',
dark: 'rgb(18, 18, 18)' dark: 'rgb(18, 18, 18)'
}, },
backgroundGradientStart: { backgroundOverlayDefault: {
light: 'rgba(250, 250, 250, 0.5)', light: 'rgba(250, 250, 250, 0.5)',
dark: 'rgba(18, 18, 18, 0.5)' dark: 'rgba(0, 0, 0, 0.5)'
}, },
backgroundGradientEnd: { backgroundOverlayInvert: {
light: 'rgba(250, 250, 250, 1)',
dark: 'rgba(18, 18, 18, 1)'
},
backgroundOverlay: {
light: 'rgba(25, 25, 25, 0.5)', light: 'rgba(25, 25, 25, 0.5)',
dark: 'rgba(0, 0, 0, 0.5)' dark: 'rgba(0, 0, 0, 0.5)'
}, },
border: { border: {
light: 'rgba(25, 25, 25, 0.3)', light: 'rgba(25, 25, 25, 0.3)',
dark: 'rgba(255, 255, 255, 0.3)' dark: 'rgba(255, 255, 255, 0.3)'
}, },
shimmerDefault: { shimmerDefault: {
light: 'rgba(25, 25, 25, 0.05)', light: 'rgba(25, 25, 25, 0.05)',
dark: 'rgba(250, 250, 250, 0.05)' dark: 'rgba(250, 250, 250, 0.05)'
@ -91,10 +98,10 @@ const themes = {
...DefaultTheme, ...DefaultTheme,
colors: { colors: {
...DefaultTheme.colors, ...DefaultTheme.colors,
primary: themeColors.primary.light, primary: themeColors.primaryDefault.light,
background: themeColors.background.light, background: themeColors.backgroundDefault.light,
card: themeColors.background.light, card: themeColors.backgroundDefault.light,
text: themeColors.primary.light, text: themeColors.primaryDefault.light,
border: themeColors.border.light, border: themeColors.border.light,
notification: themeColors.red.light notification: themeColors.red.light
} }
@ -103,10 +110,10 @@ const themes = {
...DarkTheme, ...DarkTheme,
colors: { colors: {
...DarkTheme.colors, ...DarkTheme.colors,
primary: themeColors.primary.dark, primary: themeColors.primaryDefault.dark,
background: themeColors.background.dark, background: themeColors.backgroundDefault.dark,
card: themeColors.background.dark, card: themeColors.backgroundDefault.dark,
text: themeColors.primary.dark, text: themeColors.primaryDefault.dark,
border: themeColors.border.dark, border: themeColors.border.dark,
notification: themeColors.red.dark notification: themeColors.red.dark
} }