mirror of
https://github.com/tooot-app/app
synced 2025-04-24 07:07:24 +02:00
Merge pull request #77 from tooot-app/main
Candidate v0.7 nightly 210319
This commit is contained in:
commit
a4fff0b59e
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@ -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
3
.gitmodules
vendored
@ -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
|
|
5
src/@types/react-navigation.d.ts
vendored
5
src/@types/react-navigation.d.ts
vendored
@ -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':
|
||||||
| {
|
| {
|
||||||
|
@ -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]}
|
||||||
|
@ -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
|
||||||
|
@ -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])
|
||||||
|
|
||||||
|
@ -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 }
|
||||||
|
@ -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>
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
) : (
|
) : (
|
||||||
|
@ -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}
|
||||||
|
@ -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 }}
|
||||||
/>
|
/>
|
||||||
|
@ -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 })
|
||||||
|
@ -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
|
||||||
|
@ -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:
|
||||||
|
@ -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()}
|
||||||
/>
|
/>
|
||||||
|
@ -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,
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
@ -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}
|
||||||
|
@ -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')}
|
||||||
/>
|
/>
|
||||||
|
@ -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' />
|
||||||
|
@ -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]
|
|
||||||
)
|
)
|
||||||
const childrenFavourite = useMemo(
|
}, [status.reblogged, status.reblogs_count])
|
||||||
() => (
|
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]
|
|
||||||
)
|
)
|
||||||
const childrenBookmark = useMemo(
|
}, [status.favourited, status.favourites_count])
|
||||||
() => (
|
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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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')}
|
||||||
|
@ -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}
|
||||||
|
@ -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} />
|
||||||
|
@ -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
|
|
@ -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
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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='检查附件在服务器的状态…'
|
||||||
|
@ -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>
|
||||||
</>
|
</>
|
||||||
|
@ -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'
|
||||||
|
@ -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 }} />
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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={
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
|
@ -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 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
7
src/screens/ImageViewer/@types/extensions.d.ts
vendored
Normal file
7
src/screens/ImageViewer/@types/extensions.d.ts
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import * as rn from "react-native";
|
||||||
|
|
||||||
|
declare module "react-native" {
|
||||||
|
class VirtualizedList<ItemT> extends React.Component<
|
||||||
|
VirtualizedListProps<ItemT>
|
||||||
|
> {}
|
||||||
|
}
|
17
src/screens/ImageViewer/@types/index.ts
Normal file
17
src/screens/ImageViewer/@types/index.ts
Normal 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
|
||||||
|
}
|
133
src/screens/ImageViewer/Root.tsx
Normal file
133
src/screens/ImageViewer/Root.tsx
Normal 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
|
117
src/screens/ImageViewer/components/ImageItem.android.tsx
Normal file
117
src/screens/ImageViewer/components/ImageItem.android.tsx
Normal 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)
|
32
src/screens/ImageViewer/components/ImageItem.d.ts
vendored
Normal file
32
src/screens/ImageViewer/components/ImageItem.d.ts
vendored
Normal 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;
|
175
src/screens/ImageViewer/components/ImageItem.ios.tsx
Normal file
175
src/screens/ImageViewer/components/ImageItem.ios.tsx
Normal 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)
|
40
src/screens/ImageViewer/hooks/useAnimatedComponents.ts
Normal file
40
src/screens/ImageViewer/hooks/useAnimatedComponents.ts
Normal 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
|
64
src/screens/ImageViewer/hooks/useDoubleTapToZoom.ts
Normal file
64
src/screens/ImageViewer/hooks/useDoubleTapToZoom.ts
Normal 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
|
31
src/screens/ImageViewer/hooks/useImageIndexChange.ts
Normal file
31
src/screens/ImageViewer/hooks/useImageIndexChange.ts
Normal 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
|
407
src/screens/ImageViewer/hooks/usePanResponder.ts
Normal file
407
src/screens/ImageViewer/hooks/usePanResponder.ts
Normal 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
|
23
src/screens/ImageViewer/hooks/useRequestClose.ts
Normal file
23
src/screens/ImageViewer/hooks/useRequestClose.ts
Normal 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
|
146
src/screens/ImageViewer/utils.ts
Normal file
146
src/screens/ImageViewer/utils.ts
Normal 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)
|
||||||
|
)
|
||||||
|
}
|
@ -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,27 +18,11 @@ import {
|
|||||||
SafeAreaProvider,
|
SafeAreaProvider,
|
||||||
useSafeAreaInsets
|
useSafeAreaInsets
|
||||||
} from 'react-native-safe-area-context'
|
} from 'react-native-safe-area-context'
|
||||||
|
import ImageViewer from './ImageViewer/Root'
|
||||||
|
|
||||||
const HeaderComponent = React.memo(
|
const saveImage = async (
|
||||||
({
|
image: Nav.RootStackParamList['Screen-ImagesViewer']['imageUrls'][0]
|
||||||
navigation,
|
) => {
|
||||||
currentIndex,
|
|
||||||
imageUrls
|
|
||||||
}: {
|
|
||||||
navigation: ScreenImagesViewerProp['navigation']
|
|
||||||
currentIndex: number
|
|
||||||
imageUrls: {
|
|
||||||
url: string
|
|
||||||
width?: number | undefined
|
|
||||||
height?: number | undefined
|
|
||||||
preview_url: string
|
|
||||||
remote_url?: string | undefined
|
|
||||||
}[]
|
|
||||||
}) => {
|
|
||||||
const insets = useSafeAreaInsets()
|
|
||||||
const { t } = useTranslation('screenImageViewer')
|
|
||||||
const { showActionSheetWithOptions } = useActionSheet()
|
|
||||||
|
|
||||||
const hasAndroidPermission = async () => {
|
const hasAndroidPermission = async () => {
|
||||||
const permission = PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE
|
const permission = PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE
|
||||||
|
|
||||||
@ -52,18 +35,35 @@ const HeaderComponent = React.memo(
|
|||||||
return status === 'granted'
|
return status === 'granted'
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveImage = async () => {
|
|
||||||
if (Platform.OS === 'android' && !(await hasAndroidPermission())) {
|
if (Platform.OS === 'android' && !(await hasAndroidPermission())) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
CameraRoll.save(
|
CameraRoll.save(image.url)
|
||||||
imageUrls[currentIndex].url ||
|
.then(() => haptics('Success'))
|
||||||
imageUrls[currentIndex].remote_url ||
|
.catch(() => {
|
||||||
imageUrls[currentIndex].preview_url
|
if (image.remote_url) {
|
||||||
)
|
CameraRoll.save(image.remote_url)
|
||||||
.then(() => haptics('Success'))
|
.then(() => haptics('Success'))
|
||||||
.catch(() => haptics('Error'))
|
.catch(() => haptics('Error'))
|
||||||
|
} else {
|
||||||
|
haptics('Error')
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const HeaderComponent = React.memo(
|
||||||
|
({
|
||||||
|
navigation,
|
||||||
|
currentIndex,
|
||||||
|
imageUrls
|
||||||
|
}: {
|
||||||
|
navigation: ScreenImagesViewerProp['navigation']
|
||||||
|
currentIndex: number
|
||||||
|
imageUrls: Nav.RootStackParamList['Screen-ImagesViewer']['imageUrls']
|
||||||
|
}) => {
|
||||||
|
const insets = useSafeAreaInsets()
|
||||||
|
const { t } = useTranslation('screenImageViewer')
|
||||||
|
const { showActionSheetWithOptions } = useActionSheet()
|
||||||
|
|
||||||
const onPress = useCallback(() => {
|
const onPress = useCallback(() => {
|
||||||
analytics('imageviewer_more_press')
|
analytics('imageviewer_more_press')
|
||||||
@ -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}
|
||||||
|
@ -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 })
|
||||||
|
@ -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>
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -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}
|
||||||
|
@ -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 />
|
||||||
|
@ -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',
|
||||||
|
@ -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}
|
||||||
|
@ -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
|
||||||
})}
|
})}
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user