1
0
mirror of https://github.com/tooot-app/app synced 2025-06-05 22:19:13 +02:00

Merge branch 'main' into release

This commit is contained in:
xmflsct
2023-01-17 13:00:02 +01:00
67 changed files with 571 additions and 462 deletions

View File

@ -0,0 +1,14 @@
diff --git a/src/functions/Helpers.ts b/src/functions/Helpers.ts
index e04486540494891ab07ec130b686dc4acddf2d0c..265e6ac11439276a1c52c222dfc4c50daf1689ae 100644
--- a/src/functions/Helpers.ts
+++ b/src/functions/Helpers.ts
@@ -77,7 +77,8 @@ export function getNativeNodeHandle(nativeRef: React.Component){
const nodeHandle = findNodeHandle(nativeRef);
if(nodeHandle == null){
- throw new Error('Unable to get the node handle for the native ref.');
+ return 0
+ // throw new Error('Unable to get the node handle for the native ref.');
};
return nodeHandle;

View File

@ -68,9 +68,9 @@ PODS:
- libwebp/mux (1.2.4): - libwebp/mux (1.2.4):
- libwebp/demux - libwebp/demux
- libwebp/webp (1.2.4) - libwebp/webp (1.2.4)
- MMKV (1.2.14): - MMKV (1.2.15):
- MMKVCore (~> 1.2.14) - MMKVCore (~> 1.2.15)
- MMKVCore (1.2.14) - MMKVCore (1.2.15)
- RCT-Folly (2021.07.22.00): - RCT-Folly (2021.07.22.00):
- boost - boost
- DoubleConversion - DoubleConversion
@ -321,6 +321,8 @@ PODS:
- react-native-paste-input (0.5.2): - react-native-paste-input (0.5.2):
- React-Core - React-Core
- Swime (= 3.0.6) - Swime (= 3.0.6)
- react-native-quick-base64 (2.0.5):
- React-Core
- react-native-safe-area-context (4.4.1): - react-native-safe-area-context (4.4.1):
- RCT-Folly - RCT-Folly
- RCTRequired - RCTRequired
@ -508,6 +510,7 @@ DEPENDENCIES:
- "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)" - "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)"
- react-native-pager-view (from `../node_modules/react-native-pager-view`) - react-native-pager-view (from `../node_modules/react-native-pager-view`)
- "react-native-paste-input (from `../node_modules/@mattermost/react-native-paste-input`)" - "react-native-paste-input (from `../node_modules/@mattermost/react-native-paste-input`)"
- react-native-quick-base64 (from `../node_modules/react-native-quick-base64`)
- react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`) - react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`)
- "react-native-segmented-control (from `../node_modules/@react-native-community/segmented-control`)" - "react-native-segmented-control (from `../node_modules/@react-native-community/segmented-control`)"
- React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`) - React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`)
@ -652,6 +655,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-pager-view" :path: "../node_modules/react-native-pager-view"
react-native-paste-input: react-native-paste-input:
:path: "../node_modules/@mattermost/react-native-paste-input" :path: "../node_modules/@mattermost/react-native-paste-input"
react-native-quick-base64:
:path: "../node_modules/react-native-quick-base64"
react-native-safe-area-context: react-native-safe-area-context:
:path: "../node_modules/react-native-safe-area-context" :path: "../node_modules/react-native-safe-area-context"
react-native-segmented-control: react-native-segmented-control:
@ -732,8 +737,8 @@ SPEC CHECKSUMS:
hermes-engine: 2af7b7a59128f250adfd86f15aa1d5a2ecd39995 hermes-engine: 2af7b7a59128f250adfd86f15aa1d5a2ecd39995
libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913
libwebp: f62cb61d0a484ba548448a4bd52aabf150ff6eef libwebp: f62cb61d0a484ba548448a4bd52aabf150ff6eef
MMKV: 9c4663aa7ca255d478ff10f2f5cb7d17c1651ccd MMKV: 7f34558bbb5a33b0eaefae2de4b6a20a2ffdad6f
MMKVCore: 89f5c8a66bba2dcd551779dea4d412eeec8ff5bb MMKVCore: ddf41b9d9262f058419f9ba7598719af56c02cd3
RCT-Folly: 0080d0a6ebf2577475bda044aa59e2ca1f909cda RCT-Folly: 0080d0a6ebf2577475bda044aa59e2ca1f909cda
RCTRequired: e1866f61af7049eb3d8e08e8b133abd38bc1ca7a RCTRequired: e1866f61af7049eb3d8e08e8b133abd38bc1ca7a
RCTTypeSafety: 27c2ac1b00609a432ced1ae701247593f07f901e RCTTypeSafety: 27c2ac1b00609a432ced1ae701247593f07f901e
@ -760,6 +765,7 @@ SPEC CHECKSUMS:
react-native-netinfo: 2517ad504b3d303e90d7a431b0fcaef76d207983 react-native-netinfo: 2517ad504b3d303e90d7a431b0fcaef76d207983
react-native-pager-view: 54bed894cecebe28cede54c01038d9d1e122de43 react-native-pager-view: 54bed894cecebe28cede54c01038d9d1e122de43
react-native-paste-input: 88709b4fd586ea8cc56ba5e2fc4cdfe90597730c react-native-paste-input: 88709b4fd586ea8cc56ba5e2fc4cdfe90597730c
react-native-quick-base64: e657e9197e61b60a9dec49807843052b830da254
react-native-safe-area-context: 99b24a0c5acd0d5dcac2b1a7f18c49ea317be99a react-native-safe-area-context: 99b24a0c5acd0d5dcac2b1a7f18c49ea317be99a
react-native-segmented-control: 65df6cd0619b780b3843d574a72d4c7cec396097 react-native-segmented-control: 65df6cd0619b780b3843d574a72d4c7cec396097
React-perflogger: 8c79399b0500a30ee8152d0f9f11beae7fc36595 React-perflogger: 8c79399b0500a30ee8152d0f9f11beae7fc36595

View File

@ -1,6 +1,6 @@
{ {
"name": "tooot", "name": "tooot",
"version": "4.8.2", "version": "4.8.3",
"description": "tooot for Mastodon", "description": "tooot for Mastodon",
"author": "xmflsct <me@xmflsct.com>", "author": "xmflsct <me@xmflsct.com>",
"license": "GPL-3.0-or-later", "license": "GPL-3.0-or-later",
@ -70,7 +70,6 @@
"react-i18next": "^12.1.4", "react-i18next": "^12.1.4",
"react-intl": "^6.2.5", "react-intl": "^6.2.5",
"react-native": "^0.70.6", "react-native": "^0.70.6",
"react-native-animated-spinkit": "^1.5.2",
"react-native-blurhash": "^1.1.10", "react-native-blurhash": "^1.1.10",
"react-native-fast-image": "^8.6.3", "react-native-fast-image": "^8.6.3",
"react-native-feather": "^1.1.2", "react-native-feather": "^1.1.2",
@ -120,6 +119,7 @@
"react-native-fast-image@^8.6.3": "patch:react-native-fast-image@npm%3A8.6.3#./.yarn/patches/react-native-fast-image-npm-8.6.3-03ee2d23c0.patch", "react-native-fast-image@^8.6.3": "patch:react-native-fast-image@npm%3A8.6.3#./.yarn/patches/react-native-fast-image-npm-8.6.3-03ee2d23c0.patch",
"expo-av@^13.0.2": "patch:expo-av@npm%3A13.0.2#./.yarn/patches/expo-av-npm-13.0.2-7a651776f1.patch", "expo-av@^13.0.2": "patch:expo-av@npm%3A13.0.2#./.yarn/patches/expo-av-npm-13.0.2-7a651776f1.patch",
"react-native-share-menu@^6.0.0": "patch:react-native-share-menu@npm%3A6.0.0#./.yarn/patches/react-native-share-menu-npm-6.0.0-f1094c3204.patch", "react-native-share-menu@^6.0.0": "patch:react-native-share-menu@npm%3A6.0.0#./.yarn/patches/react-native-share-menu-npm-6.0.0-f1094c3204.patch",
"@types/react-native-share-menu@^5.0.2": "patch:@types/react-native-share-menu@npm%3A5.0.2#./.yarn/patches/@types-react-native-share-menu-npm-5.0.2-373df17ecc.patch" "@types/react-native-share-menu@^5.0.2": "patch:@types/react-native-share-menu@npm%3A5.0.2#./.yarn/patches/@types-react-native-share-menu-npm-5.0.2-373df17ecc.patch",
"react-native-ios-context-menu@^1.15.1": "patch:react-native-ios-context-menu@npm%3A1.15.1#./.yarn/patches/react-native-ios-context-menu-npm-1.15.1-0034bfa5ba.patch"
} }
} }

View File

@ -10,14 +10,7 @@ import log from '@utils/startup/log'
import netInfo from '@utils/startup/netInfo' import netInfo from '@utils/startup/netInfo'
import push from '@utils/startup/push' import push from '@utils/startup/push'
import sentry from '@utils/startup/sentry' import sentry from '@utils/startup/sentry'
import timezone from '@utils/startup/timezone' import { getGlobalStorage, setAccount, setGlobalStorage } from '@utils/storage/actions'
import { storage } from '@utils/storage'
import {
getGlobalStorage,
removeAccount,
setAccount,
setGlobalStorage
} from '@utils/storage/actions'
import { migrateFromAsyncStorage, versionStorageGlobal } from '@utils/storage/migrations/toMMKV' import { migrateFromAsyncStorage, versionStorageGlobal } from '@utils/storage/migrations/toMMKV'
import ThemeManager from '@utils/styles/ThemeManager' import ThemeManager from '@utils/styles/ThemeManager'
import * as Localization from 'expo-localization' import * as Localization from 'expo-localization'
@ -25,7 +18,6 @@ import * as SplashScreen from 'expo-splash-screen'
import React, { useCallback, useEffect, useState } from 'react' import React, { useCallback, useEffect, useState } from 'react'
import { LogBox, Platform } from 'react-native' import { LogBox, Platform } from 'react-native'
import { GestureHandlerRootView } from 'react-native-gesture-handler' import { GestureHandlerRootView } from 'react-native-gesture-handler'
import { MMKV } from 'react-native-mmkv'
import { SafeAreaProvider } from 'react-native-safe-area-context' import { SafeAreaProvider } from 'react-native-safe-area-context'
import { enableFreeze } from 'react-native-screens' import { enableFreeze } from 'react-native-screens'
import i18n from './i18n' import i18n from './i18n'
@ -37,9 +29,9 @@ Platform.select({
dev() dev()
sentry() sentry()
netInfo()
audio() audio()
push() push()
timezone()
enableFreeze(true) enableFreeze(true)
log('log', 'App', 'delay splash') log('log', 'App', 'delay splash')
@ -48,7 +40,6 @@ SplashScreen.preventAutoHideAsync()
const App: React.FC = () => { const App: React.FC = () => {
log('log', 'App', 'rendering App') log('log', 'App', 'rendering App')
const [appIsReady, setAppIsReady] = useState(false) const [appIsReady, setAppIsReady] = useState(false)
const [localCorrupt, setLocalCorrupt] = useState<string>()
const [hasMigrated, setHasMigrated] = useState<boolean>(versionStorageGlobal !== undefined) const [hasMigrated, setHasMigrated] = useState<boolean>(versionStorageGlobal !== undefined)
@ -63,36 +54,19 @@ const App: React.FC = () => {
log('log', 'App', 'loading from MMKV') log('log', 'App', 'loading from MMKV')
const account = getGlobalStorage.string('account.active') const account = getGlobalStorage.string('account.active')
if (account) { if (account) {
const storageAccount = new MMKV({ id: account }) await setAccount(account)
const token = storageAccount.getString('auth.token')
if (token) {
log('log', 'App', `Binding storage of ${account}`)
storage.account = storageAccount
} else {
log('log', 'App', `Token not found for ${account}`)
removeAccount(account)
}
} else { } else {
log('log', 'App', 'No active account available') log('log', 'App', 'No active account available')
const accounts = getGlobalStorage.object('accounts') const accounts = getGlobalStorage.object('accounts')
if (accounts?.length) { if (accounts?.length) {
log('log', 'App', `Setting active account ${accounts[accounts.length - 1]}`) log('log', 'App', `Setting active account ${accounts[accounts.length - 1]}`)
setAccount(accounts[accounts.length - 1]) await setAccount(accounts[accounts.length - 1])
} else { } else {
setGlobalStorage('account.active', undefined) setGlobalStorage('account.active', undefined)
} }
} }
} }
let netInfoRes = undefined
try {
netInfoRes = await netInfo()
} catch {}
if (netInfoRes && netInfoRes.corrupted && netInfoRes.corrupted.length) {
setLocalCorrupt(netInfoRes.corrupted)
}
log('log', 'App', `locale: ${Localization.locale}`) log('log', 'App', `locale: ${Localization.locale}`)
const language = getLanguage() const language = getLanguage()
if (!language) { if (!language) {
@ -126,7 +100,7 @@ const App: React.FC = () => {
<ActionSheetProvider> <ActionSheetProvider>
<AccessibilityManager> <AccessibilityManager>
<ThemeManager> <ThemeManager>
<Screens localCorrupt={localCorrupt} /> <Screens />
</ThemeManager> </ThemeManager>
</AccessibilityManager> </AccessibilityManager>
</ActionSheetProvider> </ActionSheetProvider>

View File

@ -3,7 +3,7 @@ import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React, { useState } from 'react' import React, { useState } from 'react'
import { AccessibilityProps, Pressable, StyleProp, View, ViewStyle } from 'react-native' import { AccessibilityProps, Pressable, StyleProp, View, ViewStyle } from 'react-native'
import { Flow } from 'react-native-animated-spinkit' import { Loading } from './Loading'
import CustomText from './Text' import CustomText from './Text'
export interface Props { export interface Props {
@ -53,7 +53,7 @@ const Button: React.FC<Props> = ({
const loadingSpinkit = () => const loadingSpinkit = () =>
loading ? ( loading ? (
<View style={{ position: 'absolute' }}> <View style={{ position: 'absolute' }}>
<Flow size={StyleConstants.Font.Size[size]} color={colors.secondary} /> <Loading />
</View> </View>
) : null ) : null

View File

@ -1,10 +1,10 @@
import Icon from '@components/Icon' import Icon from '@components/Icon'
import { Loading } from '@components/Loading'
import CustomText from '@components/Text' import CustomText from '@components/Text'
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'
import { AccessibilityProps, Pressable, View } from 'react-native' import { AccessibilityProps, Pressable, View } from 'react-native'
import { Flow } from 'react-native-animated-spinkit'
export interface Props { export interface Props {
accessibilityLabel?: string accessibilityLabel?: string
@ -43,7 +43,7 @@ const HeaderRight: React.FC<Props> = ({
const loadingSpinkit = () => const loadingSpinkit = () =>
loading ? ( loading ? (
<View style={{ position: 'absolute' }}> <View style={{ position: 'absolute' }}>
<Flow size={StyleConstants.Font.Size.M * 1.25} color={colors.secondary} /> <Loading />
</View> </View>
) : null ) : null

View File

@ -193,10 +193,12 @@ const ComponentInstance: React.FC<Props> = ({
} }
}) })
const scopes = featureCheck('deprecate_auth_follow')
? ['read', 'write', 'push']
: ['read', 'write', 'follow', 'push']
const processUpdate = useCallback(() => { const processUpdate = useCallback(() => {
const scopes = () =>
featureCheck('deprecate_auth_follow', instanceQuery.data?.version)
? ['read', 'write', 'push']
: ['read', 'write', 'follow', 'push']
if (domain) { if (domain) {
const accounts = getGlobalStorage.object('accounts') const accounts = getGlobalStorage.object('accounts')
if (accounts?.filter(account => account.startsWith(`${domain}/`)).length) { if (accounts?.filter(account => account.startsWith(`${domain}/`)).length) {
@ -210,15 +212,15 @@ const ComponentInstance: React.FC<Props> = ({
}, },
{ {
text: t('common:buttons.continue'), text: t('common:buttons.continue'),
onPress: () => appsMutation.mutate({ domain, scopes }) onPress: () => appsMutation.mutate({ domain, scopes: scopes() })
} }
] ]
) )
} else { } else {
appsMutation.mutate({ domain, scopes }) appsMutation.mutate({ domain, scopes: scopes() })
} }
} }
}, [domain]) }, [domain, instanceQuery.data?.version])
return ( return (
<KeyboardAvoidingView <KeyboardAvoidingView

View File

@ -0,0 +1,12 @@
import { useTheme } from '@utils/styles/ThemeManager'
import { ActivityIndicator, ViewProps } from 'react-native'
export type Props = {
size?: 'small' | 'large'
} & ViewProps
export const Loading: React.FC<Props> = ({ size = 'small', ...rest }) => {
const { colors } = useTheme()
return <ActivityIndicator size={size} color={colors.secondary} {...rest} />
}

View File

@ -1,4 +1,5 @@
import Icon from '@components/Icon' import Icon from '@components/Icon'
import { Loading } from '@components/Loading'
import CustomText from '@components/Text' import CustomText from '@components/Text'
import { useAccessibility } from '@utils/accessibility/AccessibilityManager' import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
@ -6,7 +7,6 @@ import { useTheme } from '@utils/styles/ThemeManager'
import { ColorDefinitions } from '@utils/styles/themes' import { ColorDefinitions } from '@utils/styles/themes'
import React from 'react' import React from 'react'
import { View } from 'react-native' import { View } from 'react-native'
import { Flow } from 'react-native-animated-spinkit'
import { State, Switch, TapGestureHandler } from 'react-native-gesture-handler' import { State, Switch, TapGestureHandler } from 'react-native-gesture-handler'
export interface Props { export interface Props {
@ -150,7 +150,7 @@ const MenuRow: React.FC<Props> = ({
) : null} ) : null}
{loading ? ( {loading ? (
<View style={{ position: 'absolute' }}> <View style={{ position: 'absolute' }}>
<Flow size={StyleConstants.Font.Size.M * 1.25} color={colors.secondary} /> <Loading />
</View> </View>
) : null} ) : null}
</View> </View>

View File

@ -36,6 +36,7 @@ export interface Props {
disableDetails?: boolean disableDetails?: boolean
disableOnPress?: boolean disableOnPress?: boolean
isConversation?: boolean isConversation?: boolean
noBackground?: boolean
} }
// When the poll is long // When the poll is long
@ -45,7 +46,8 @@ const TimelineDefault: React.FC<Props> = ({
highlighted = false, highlighted = false,
disableDetails = false, disableDetails = false,
disableOnPress = false, disableOnPress = false,
isConversation = false isConversation = false,
noBackground = false
}) => { }) => {
const status = item.reblog ? item.reblog : item const status = item.reblog ? item.reblog : item
const rawContent = useRef<string[]>([]) const rawContent = useRef<string[]>([])
@ -77,7 +79,7 @@ const TimelineDefault: React.FC<Props> = ({
padding: disableDetails padding: disableDetails
? StyleConstants.Spacing.Global.PagePadding / 1.5 ? StyleConstants.Spacing.Global.PagePadding / 1.5
: StyleConstants.Spacing.Global.PagePadding, : StyleConstants.Spacing.Global.PagePadding,
backgroundColor: colors.backgroundDefault, backgroundColor: noBackground ? undefined : colors.backgroundDefault,
paddingBottom: disableDetails ? StyleConstants.Spacing.Global.PagePadding / 1.5 : 0 paddingBottom: disableDetails ? StyleConstants.Spacing.Global.PagePadding / 1.5 : 0
} }
const main = () => ( const main = () => (

View File

@ -1,5 +1,6 @@
import Button from '@components/Button' import Button from '@components/Button'
import Icon from '@components/Icon' import Icon from '@components/Icon'
import { Loading } from '@components/Loading'
import CustomText from '@components/Text' import CustomText from '@components/Text'
import { QueryKeyTimeline, useTimelineQuery } from '@utils/queryHooks/timeline' import { QueryKeyTimeline, useTimelineQuery } from '@utils/queryHooks/timeline'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
@ -7,7 +8,6 @@ import { useTheme } from '@utils/styles/ThemeManager'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { View } from 'react-native' import { View } from 'react-native'
import { Circle } from 'react-native-animated-spinkit'
export interface Props { export interface Props {
queryKey: QueryKeyTimeline queryKey: QueryKeyTimeline
@ -25,7 +25,7 @@ const TimelineEmpty: React.FC<Props> = ({ queryKey }) => {
const children = () => { const children = () => {
switch (status) { switch (status) {
case 'loading': case 'loading':
return <Circle size={StyleConstants.Font.Size.L} color={colors.secondary} /> return <Loading />
case 'error': case 'error':
return ( return (
<> <>

View File

@ -1,4 +1,5 @@
import Icon from '@components/Icon' import Icon from '@components/Icon'
import { Loading } from '@components/Loading'
import CustomText from '@components/Text' import CustomText from '@components/Text'
import { QueryKeyTimeline, useTimelineQuery } from '@utils/queryHooks/timeline' import { QueryKeyTimeline, useTimelineQuery } from '@utils/queryHooks/timeline'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
@ -6,7 +7,6 @@ import { useTheme } from '@utils/styles/ThemeManager'
import React from 'react' import React from 'react'
import { Trans } from 'react-i18next' import { Trans } from 'react-i18next'
import { View } from 'react-native' import { View } from 'react-native'
import { Circle } from 'react-native-animated-spinkit'
export interface Props { export interface Props {
queryKey: QueryKeyTimeline queryKey: QueryKeyTimeline
@ -31,7 +31,7 @@ const TimelineFooter: React.FC<Props> = ({ queryKey, disableInfinity }) => {
}} }}
> >
{!disableInfinity && hasNextPage ? ( {!disableInfinity && hasNextPage ? (
<Circle size={StyleConstants.Font.Size.L} color={colors.secondary} /> <Loading />
) : ( ) : (
<CustomText fontStyle='S' style={{ color: colors.secondary }}> <CustomText fontStyle='S' style={{ color: colors.secondary }}>
<Trans <Trans

View File

@ -9,7 +9,12 @@ export const aspectRatio = ({
width?: number width?: number
height?: number height?: number
}): number => { }): number => {
const cropTooTall = (height || 1) / (width || 1) > 3 / 2 ? 2 / 3 : (width || 1) / (height || 1) const defaultCrop =
(height || 1) / (width || 1) > 3 / 2
? 2 / 3
: (width || 1) / (height || 1) > 4
? 4
: (width || 1) / (height || 1)
const isEven = total % 2 == 0 const isEven = total % 2 == 0
if (total > 5) { if (total > 5) {
@ -26,12 +31,12 @@ export const aspectRatio = ({
} else { } else {
switch (isEven) { switch (isEven) {
case true: case true:
return cropTooTall return defaultCrop
case false: case false:
if ((index || -2) + 1 == total) { if ((index || -2) + 1 == total) {
return cropTooTall * 2 return defaultCrop * 2
} else { } else {
return cropTooTall return defaultCrop
} }
} }
} }

View File

@ -64,6 +64,9 @@ const TimelineCard: React.FC = () => {
if (loading) { if (loading) {
return null return null
} }
if (status.media_attachments.length) {
return null
}
if ((!status.card?.image || !status.card.title) && !status.card?.description) { if ((!status.card?.image || !status.card.title) && !status.card?.description) {
return null return null
} }

View File

@ -1,3 +1,4 @@
import { Loading } from '@components/Loading'
import { ParseHTML } from '@components/Parse' import { ParseHTML } from '@components/Parse'
import CustomText from '@components/Text' import CustomText from '@components/Text'
import detectLanguage from '@utils/helpers/detectLanguage' import detectLanguage from '@utils/helpers/detectLanguage'
@ -9,7 +10,6 @@ import * as Localization from 'expo-localization'
import React, { useContext, useEffect, useState } from 'react' import React, { useContext, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Platform, Pressable } from 'react-native' import { Platform, Pressable } from 'react-native'
import { Circle } from 'react-native-animated-spinkit'
import StatusContext from './Context' import StatusContext from './Context'
const TimelineTranslate = () => { const TimelineTranslate = () => {
@ -111,13 +111,7 @@ const TimelineTranslate = () => {
}) })
: t('componentTimeline:shared.translate.default')} : t('componentTimeline:shared.translate.default')}
</CustomText> </CustomText>
{isFetching ? ( {isFetching ? <Loading style={{ marginLeft: StyleConstants.Spacing.S }} /> : null}
<Circle
size={StyleConstants.Font.Size.M}
color={colors.disabled}
style={{ marginLeft: StyleConstants.Spacing.S }}
/>
) : null}
</Pressable> </Pressable>
{devView()} {devView()}
{data && data.error === undefined {data && data.error === undefined

View File

@ -58,6 +58,7 @@ const menuStatus = ({
const menus: ContextMenu = [] const menus: ContextMenu = []
const [accountId] = useAccountStorage.string('auth.account.id') const [accountId] = useAccountStorage.string('auth.account.id')
const [accountAcct] = useAccountStorage.string('auth.account.acct')
const ownAccount = accountId === status.account?.id const ownAccount = accountId === status.account?.id
const canEditPost = featureCheck('edit_post') const canEditPost = featureCheck('edit_post')
@ -193,7 +194,13 @@ const menuStatus = ({
}), }),
disabled: false, disabled: false,
destructive: false, destructive: false,
hidden: !ownAccount hidden:
!ownAccount &&
queryKey[1].page !== 'Notifications' &&
!status.mentions.find(
mention => mention.acct === accountAcct && mention.username === accountAcct
) &&
!status.muted
}, },
title: t('componentContextMenu:status.mute.action', { title: t('componentContextMenu:status.mute.action', {
defaultValue: 'false', defaultValue: 'false',

View File

@ -9,13 +9,7 @@ import { getGlobalStorage } from '@utils/storage/actions'
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'
export let loadingLink = false
const openLink = async (url: string, navigation?: any) => { const openLink = async (url: string, navigation?: any) => {
if (loadingLink) {
return
}
const handleNavigation = (page: 'Tab-Shared-Toot' | 'Tab-Shared-Account', options: any) => { const handleNavigation = (page: 'Tab-Shared-Toot' | 'Tab-Shared-Account', options: any) => {
if (navigation) { if (navigation) {
navigation.push(page, options) navigation.push(page, options)
@ -28,7 +22,6 @@ const openLink = async (url: string, navigation?: any) => {
const match = urlMatcher(url) const match = urlMatcher(url)
// If a tooot can be found // If a tooot can be found
if (match?.status?.id) { if (match?.status?.id) {
loadingLink = true
let response: Mastodon.Status | undefined = undefined let response: Mastodon.Status | undefined = undefined
const queryKey: QueryKeyStatus = [ const queryKey: QueryKeyStatus = [
@ -39,15 +32,13 @@ const openLink = async (url: string, navigation?: any) => {
if (cache) { if (cache) {
handleNavigation('Tab-Shared-Toot', { toot: cache }) handleNavigation('Tab-Shared-Toot', { toot: cache })
loadingLink = false
return return
} else { } else {
try { try {
response = await searchLocalStatus(url) response = await searchLocalStatus(url, true)
} catch {} } catch {}
if (response) { if (response) {
handleNavigation('Tab-Shared-Toot', { toot: response }) handleNavigation('Tab-Shared-Toot', { toot: response })
loadingLink = false
return return
} }
} }
@ -60,7 +51,6 @@ const openLink = async (url: string, navigation?: any) => {
return return
} }
loadingLink = true
let response: Mastodon.Account | undefined = undefined let response: Mastodon.Account | undefined = undefined
const queryKey: QueryKeyAccount = [ const queryKey: QueryKeyAccount = [
@ -71,21 +61,18 @@ const openLink = async (url: string, navigation?: any) => {
if (cache) { if (cache) {
handleNavigation('Tab-Shared-Account', { account: cache }) handleNavigation('Tab-Shared-Account', { account: cache })
loadingLink = false
return return
} else { } else {
try { try {
response = await searchLocalAccount(url) response = await searchLocalAccount(url, true)
} catch {} } catch {}
if (response) { if (response) {
handleNavigation('Tab-Shared-Account', { account: response }) handleNavigation('Tab-Shared-Account', { account: response })
loadingLink = false
return return
} }
} }
} }
loadingLink = false
switch (getGlobalStorage.string('app.browser')) { switch (getGlobalStorage.string('app.browser')) {
// Some links might end with an empty space at the end that triggers an error // Some links might end with an empty space at the end that triggers an error
case 'internal': case 'internal':

View File

@ -123,7 +123,7 @@
"muted": { "muted": {
"accessibilityLabel": "Publicació silenciada" "accessibilityLabel": "Publicació silenciada"
}, },
"replies": "Respostes <0 />", "replies": "En resposta a <0 />",
"visibility": { "visibility": {
"direct": { "direct": {
"accessibilityLabel": "La publicació és un missatge directe" "accessibilityLabel": "La publicació és un missatge directe"

View File

@ -373,7 +373,8 @@
"hashtags": "Etiqueta", "hashtags": "Etiqueta",
"statuses": "Publicació" "statuses": "Publicació"
}, },
"notFound": "No s'ha trobat <bold>{{searchTerm}}</bold> relacionat {{type}}" "notFound": "No s'ha trobat <bold>{{searchTerm}}</bold> relacionat {{type}}",
"noResult": "No s'ha trobat res, prova-ho amb un altre terme"
}, },
"toot": { "toot": {
"name": "Discussions", "name": "Discussions",

View File

@ -373,7 +373,8 @@
"hashtags": "", "hashtags": "",
"statuses": "" "statuses": ""
}, },
"notFound": "" "notFound": "",
"noResult": ""
}, },
"toot": { "toot": {
"name": "", "name": "",

View File

@ -373,7 +373,8 @@
"hashtags": "Hashtag", "hashtags": "Hashtag",
"statuses": "Tröt" "statuses": "Tröt"
}, },
"notFound": "Konnte <bold>{{searchTerm}}</bold>-bezogene {{type}} nicht finden" "notFound": "Konnte <bold>{{searchTerm}}</bold>-bezogene {{type}} nicht finden",
"noResult": "Nichts gefunden, bitte versuchen Sie es mit einem anderen Begriff"
}, },
"toot": { "toot": {
"name": "Diskussionen", "name": "Diskussionen",

View File

@ -373,7 +373,8 @@
"hashtags": "Hashtag", "hashtags": "Hashtag",
"statuses": "Ανάρτηση" "statuses": "Ανάρτηση"
}, },
"notFound": "Δεν είναι εφικτό να βρεθεί <bold>{{searchTerm}}</bold> που να έχει σχέση με {{type}}" "notFound": "Δεν είναι εφικτό να βρεθεί <bold>{{searchTerm}}</bold> που να έχει σχέση με {{type}}",
"noResult": ""
}, },
"toot": { "toot": {
"name": "Συζητήσεις", "name": "Συζητήσεις",

View File

@ -373,7 +373,8 @@
"hashtags": "Hashtag", "hashtags": "Hashtag",
"statuses": "Toot" "statuses": "Toot"
}, },
"notFound": "Cannot find <bold>{{searchTerm}}</bold> related {{type}}" "notFound": "Cannot find <bold>{{searchTerm}}</bold> related {{type}}",
"noResult": "Cannot find anything, please try a different term"
}, },
"toot": { "toot": {
"name": "Discussions", "name": "Discussions",

View File

@ -123,7 +123,7 @@
"muted": { "muted": {
"accessibilityLabel": "Toot silenciado" "accessibilityLabel": "Toot silenciado"
}, },
"replies": "Respuestas <0 />", "replies": "En respuesta a <0 />",
"visibility": { "visibility": {
"direct": { "direct": {
"accessibilityLabel": "El toot es un mensaje directo" "accessibilityLabel": "El toot es un mensaje directo"

View File

@ -373,7 +373,8 @@
"hashtags": "Hashtag", "hashtags": "Hashtag",
"statuses": "Toot" "statuses": "Toot"
}, },
"notFound": "No se pudo encontrar <bold>{{searchTerm}}</bold> relacionado con {{type}}" "notFound": "No se pudo encontrar <bold>{{searchTerm}}</bold> relacionado con {{type}}",
"noResult": "No se ha podido encontrar nada, por favor, inténtelo con un término diferente"
}, },
"toot": { "toot": {
"name": "Discusiones", "name": "Discusiones",

View File

@ -373,7 +373,8 @@
"hashtags": "Hashtag", "hashtags": "Hashtag",
"statuses": "Pouet" "statuses": "Pouet"
}, },
"notFound": "Impossible de trouver <bold>{{searchTerm}}</bold> lié à {{type}}" "notFound": "Impossible de trouver <bold>{{searchTerm}}</bold> lié à {{type}}",
"noResult": ""
}, },
"toot": { "toot": {
"name": "Discussions", "name": "Discussions",

View File

@ -1,3 +1,4 @@
import * as Localization from 'expo-localization'
import i18n from 'i18next' import i18n from 'i18next'
import { initReactI18next } from 'react-i18next' import { initReactI18next } from 'react-i18next'
@ -128,4 +129,12 @@ i18n.use(initReactI18next).init({
} }
}) })
const timezone = Localization.getCalendars()[0].timeZone
if (timezone && '__setDefaultTimeZone' in Intl.DateTimeFormat) {
try {
// @ts-ignore
Intl.DateTimeFormat.__setDefaultTimeZone(timezone)
} catch {}
}
export default i18n export default i18n

View File

@ -373,7 +373,8 @@
"hashtags": "Hashtag", "hashtags": "Hashtag",
"statuses": "Toot" "statuses": "Toot"
}, },
"notFound": "Impossibile trovare <bold>{{searchTerm}}</bold> come {{type}}" "notFound": "Impossibile trovare <bold>{{searchTerm}}</bold> come {{type}}",
"noResult": ""
}, },
"toot": { "toot": {
"name": "Discussioni", "name": "Discussioni",

View File

@ -373,7 +373,8 @@
"hashtags": "ハッシュタグ", "hashtags": "ハッシュタグ",
"statuses": "投稿" "statuses": "投稿"
}, },
"notFound": "{{type}} <bold>{{searchTerm}}</bold> は見つかりませんでした" "notFound": "{{type}} <bold>{{searchTerm}}</bold> は見つかりませんでした",
"noResult": ""
}, },
"toot": { "toot": {
"name": "スレッド", "name": "スレッド",

View File

@ -373,7 +373,8 @@
"hashtags": "해시태그", "hashtags": "해시태그",
"statuses": "툿" "statuses": "툿"
}, },
"notFound": "<bold>{{searchTerm}}</bold>와 관련된 {{type}}을 찾을 수 없어요" "notFound": "<bold>{{searchTerm}}</bold>와 관련된 {{type}}을 찾을 수 없어요",
"noResult": ""
}, },
"toot": { "toot": {
"name": "대화", "name": "대화",

View File

@ -373,7 +373,8 @@
"hashtags": "Hashtag", "hashtags": "Hashtag",
"statuses": "Toot" "statuses": "Toot"
}, },
"notFound": "Kan <bold>{{searchTerm}}</bold> niet vinden gerelateerd aan {{type}}" "notFound": "Kan <bold>{{searchTerm}}</bold> niet vinden gerelateerd aan {{type}}",
"noResult": "Kan niets vinden, probeer een andere term"
}, },
"toot": { "toot": {
"name": "Gesprek", "name": "Gesprek",

View File

@ -373,7 +373,8 @@
"hashtags": "Hashtag", "hashtags": "Hashtag",
"statuses": "Toot" "statuses": "Toot"
}, },
"notFound": "Nie można odnaleźć <bold>{{searchTerm}}</bold> powiązanego {{type}}" "notFound": "Nie można odnaleźć <bold>{{searchTerm}}</bold> powiązanego {{type}}",
"noResult": ""
}, },
"toot": { "toot": {
"name": "Dyskusje", "name": "Dyskusje",

View File

@ -373,7 +373,8 @@
"hashtags": "Palavras-chave", "hashtags": "Palavras-chave",
"statuses": "Toot" "statuses": "Toot"
}, },
"notFound": "Não foi possível encontrar <bold>{{searchTerm}}</bold> {{type}} relacionado" "notFound": "Não foi possível encontrar <bold>{{searchTerm}}</bold> {{type}} relacionado",
"noResult": ""
}, },
"toot": { "toot": {
"name": "Discussões", "name": "Discussões",

View File

@ -373,7 +373,8 @@
"hashtags": "", "hashtags": "",
"statuses": "" "statuses": ""
}, },
"notFound": "" "notFound": "",
"noResult": ""
}, },
"toot": { "toot": {
"name": "", "name": "",

View File

@ -373,7 +373,8 @@
"hashtags": "Hashtagg", "hashtags": "Hashtagg",
"statuses": "Inlägg" "statuses": "Inlägg"
}, },
"notFound": "Kan inte hitta <bold>{{searchTerm}}</bold>-relaterade {{type}}" "notFound": "Kan inte hitta <bold>{{searchTerm}}</bold>-relaterade {{type}}",
"noResult": ""
}, },
"toot": { "toot": {
"name": "Diskussioner", "name": "Diskussioner",

View File

@ -49,7 +49,7 @@
}, },
"favourited": { "favourited": {
"accessibilityLabel": "Додати дмух до улюблених", "accessibilityLabel": "Додати дмух до улюблених",
"function": "Улюблені дмухи" "function": "Вподобати дмух"
}, },
"bookmarked": { "bookmarked": {
"accessibilityLabel": "Додати цей дмух до закладок", "accessibilityLabel": "Додати цей дмух до закладок",

View File

@ -373,7 +373,8 @@
"hashtags": "Ґештег", "hashtags": "Ґештег",
"statuses": "Дмух" "statuses": "Дмух"
}, },
"notFound": "Не вдалося знайти <bold>{{searchTerm}}</bold> пов'язаний з {{type}}" "notFound": "Не вдалося знайти <bold>{{searchTerm}}</bold> пов'язаний з {{type}}",
"noResult": "Нічого не знайдено, спробуйте інший термін"
}, },
"toot": { "toot": {
"name": "Обговорення", "name": "Обговорення",

View File

@ -373,7 +373,8 @@
"hashtags": "Hashtag", "hashtags": "Hashtag",
"statuses": "Tút" "statuses": "Tút"
}, },
"notFound": "Không tìm thấy {{type}} <bold>{{searchTerm}}</bold>" "notFound": "Không tìm thấy {{type}} <bold>{{searchTerm}}</bold>",
"noResult": ""
}, },
"toot": { "toot": {
"name": "Nội dung tút", "name": "Nội dung tút",

View File

@ -373,7 +373,8 @@
"hashtags": "话题标签", "hashtags": "话题标签",
"statuses": "嘟文" "statuses": "嘟文"
}, },
"notFound": "找不到 <bold>{{searchTerm}}</bold> 相关的 {{type}}" "notFound": "找不到 <bold>{{searchTerm}}</bold> 相关的 {{type}}",
"noResult": "搜索不到相关信息,请尝试不同关键词"
}, },
"toot": { "toot": {
"name": "对话", "name": "对话",

View File

@ -373,7 +373,8 @@
"hashtags": "主題標籤", "hashtags": "主題標籤",
"statuses": "嘟文" "statuses": "嘟文"
}, },
"notFound": "找不到 <bold>{{searchTerm}}</bold> 相關的 {{type}}" "notFound": "找不到 <bold>{{searchTerm}}</bold> 相關的 {{type}}",
"noResult": "找不到任何東西,請嘗試其它用詞"
}, },
"toot": { "toot": {
"name": "討論", "name": "討論",

View File

@ -1,5 +1,6 @@
import Button from '@components/Button' import Button from '@components/Button'
import haptics from '@components/haptics' import haptics from '@components/haptics'
import { Loading } from '@components/Loading'
import { ParseHTML } from '@components/Parse' import { ParseHTML } from '@components/Parse'
import RelativeTime from '@components/RelativeTime' import RelativeTime from '@components/RelativeTime'
import CustomText from '@components/Text' import CustomText from '@components/Text'
@ -20,7 +21,6 @@ import {
StyleSheet, StyleSheet,
View View
} from 'react-native' } from 'react-native'
import { Circle } from 'react-native-animated-spinkit'
import FastImage from 'react-native-fast-image' import FastImage from 'react-native-fast-image'
import { FlatList, ScrollView } from 'react-native-gesture-handler' import { FlatList, ScrollView } from 'react-native-gesture-handler'
import { SafeAreaView } from 'react-native-safe-area-context' import { SafeAreaView } from 'react-native-safe-area-context'
@ -191,14 +191,8 @@ const ScreenAnnouncements: React.FC<RootStackScreenProps<'Screen-Announcements'>
const ListEmptyComponent = () => { const ListEmptyComponent = () => {
return ( return (
<View <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
style={{ <Loading />
width: Dimensions.get('window').width,
justifyContent: 'center',
alignItems: 'center'
}}
>
<Circle size={StyleConstants.Font.Size.L} color={colors.secondary} />
</View> </View>
) )
} }

View File

@ -1,6 +1,7 @@
import Button from '@components/Button' import Button from '@components/Button'
import haptics from '@components/haptics' import haptics from '@components/haptics'
import Icon from '@components/Icon' import Icon from '@components/Icon'
import { Loading } from '@components/Loading'
import { MAX_MEDIA_ATTACHMENTS } from '@components/mediaSelector' import { MAX_MEDIA_ATTACHMENTS } from '@components/mediaSelector'
import CustomText from '@components/Text' import CustomText from '@components/Text'
import { useActionSheet } from '@expo/react-native-action-sheet' import { useActionSheet } from '@expo/react-native-action-sheet'
@ -11,7 +12,6 @@ import { useTheme } from '@utils/styles/ThemeManager'
import React, { RefObject, useContext, useEffect, useRef } from 'react' import React, { RefObject, useContext, useEffect, useRef } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { FlatList, Pressable, StyleSheet, View } from 'react-native' import { FlatList, Pressable, StyleSheet, View } from 'react-native'
import { Circle } from 'react-native-animated-spinkit'
import FastImage from 'react-native-fast-image' import FastImage from 'react-native-fast-image'
import ComposeContext from '../../utils/createContext' import ComposeContext from '../../utils/createContext'
import { ExtendedAttachment } from '../../utils/types' import { ExtendedAttachment } from '../../utils/types'
@ -135,7 +135,7 @@ const ComposeAttachments: React.FC<Props> = ({ accessibleRefAttachments }) => {
backgroundColor: colors.backgroundOverlayInvert backgroundColor: colors.backgroundOverlayInvert
}} }}
> >
<Circle size={StyleConstants.Font.Size.L} color={colors.primaryOverlay} /> <Loading />
</View> </View>
) : ( ) : (
<View <View

View File

@ -2,13 +2,13 @@ import ComponentAccount from '@components/Account'
import haptics from '@components/haptics' import haptics from '@components/haptics'
import ComponentHashtag from '@components/Hashtag' import ComponentHashtag from '@components/Hashtag'
import Icon from '@components/Icon' import Icon from '@components/Icon'
import { Loading } from '@components/Loading'
import ComponentSeparator from '@components/Separator' import ComponentSeparator from '@components/Separator'
import { useSearchQuery } from '@utils/queryHooks/search' import { useSearchQuery } from '@utils/queryHooks/search'
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, { Fragment, useContext, useEffect } from 'react' import React, { Fragment, useContext, useEffect } from 'react'
import { View } from 'react-native' import { View } from 'react-native'
import { Circle } from 'react-native-animated-spinkit'
import ComposeContext from '../utils/createContext' import ComposeContext from '../utils/createContext'
import { formatText } from '../utils/processText' import { formatText } from '../utils/processText'
@ -38,7 +38,7 @@ const ComposeRootSuggestions: React.FC = () => {
const { isFetching, data, refetch } = useSearchQuery({ const { isFetching, data, refetch } = useSearchQuery({
type: mapSchemaToType(), type: mapSchemaToType(),
term: composeState.tag?.raw.substring(1), term: composeState.tag?.raw.substring(1),
limit: 10, ...(mapSchemaToType() === 'accounts' && { following: true }),
options: { enabled: false } options: { enabled: false }
}) })
useEffect(() => { useEffect(() => {
@ -125,7 +125,7 @@ const ComposeRootSuggestions: React.FC = () => {
key='listEmpty' key='listEmpty'
style={{ flex: 1, alignItems: 'center', marginVertical: StyleConstants.Spacing.M }} style={{ flex: 1, alignItems: 'center', marginVertical: StyleConstants.Spacing.M }}
> >
<Circle size={StyleConstants.Font.Size.M * 1.25} color={colors.secondary} /> <Loading />
</View> </View>
) : ( ) : (
<>{main()}</> <>{main()}</>

View File

@ -4,7 +4,7 @@ import { CameraRoll } from '@react-native-camera-roll/camera-roll'
import { RootStackParamList } from '@utils/navigation/navigators' import { RootStackParamList } from '@utils/navigation/navigators'
import * as FileSystem from 'expo-file-system' import * as FileSystem from 'expo-file-system'
import i18next from 'i18next' import i18next from 'i18next'
import { PermissionsAndroid, Platform } from 'react-native' import { Linking, PermissionsAndroid, Platform } from 'react-native'
type CommonProps = { type CommonProps = {
image: RootStackParamList['Screen-ImagesViewer']['imageUrls'][0] image: RootStackParamList['Screen-ImagesViewer']['imageUrls'][0]
@ -19,7 +19,11 @@ const saveIos = async ({ image }: CommonProps) => {
message: i18next.t('screenImageViewer:content.save.succeed') message: i18next.t('screenImageViewer:content.save.succeed')
}) })
}) })
.catch(() => { .catch(err => {
if (err?.code === 'E_PHOTO_LIBRARY_AUTH_DENIED') {
Linking.openSettings()
return
}
if (image.remote_url) { if (image.remote_url) {
CameraRoll.save(image.remote_url) CameraRoll.save(image.remote_url)
.then(() => { .then(() => {

View File

@ -8,7 +8,7 @@ import Root from './Root'
const Stack = createNativeStackNavigator<TabLocalStackParamList>() const Stack = createNativeStackNavigator<TabLocalStackParamList>()
const TabLocal: React.FC = () => { const TabLocal: React.FC = () => {
usePopToTop() usePopToTop('Tab-Local-Root')
return ( return (
<Stack.Navigator screenOptions={{ headerShadowVisible: false }}> <Stack.Navigator screenOptions={{ headerShadowVisible: false }}>
<Stack.Screen name='Tab-Local-Root' component={Root} /> <Stack.Screen name='Tab-Local-Root' component={Root} />

View File

@ -60,7 +60,7 @@ const Collections: React.FC = () => {
title={t('screenTabs:me.stacks.favourites.name')} title={t('screenTabs:me.stacks.favourites.name')}
onPress={() => navigation.navigate('Tab-Me-Favourites')} onPress={() => navigation.navigate('Tab-Me-Favourites')}
/> />
{pageMe.lists?.shown ? ( {pageMe?.lists?.shown ? (
<MenuRow <MenuRow
iconFront='List' iconFront='List'
iconBack='ChevronRight' iconBack='ChevronRight'
@ -68,7 +68,7 @@ const Collections: React.FC = () => {
onPress={() => navigation.navigate('Tab-Me-List-List')} onPress={() => navigation.navigate('Tab-Me-List-List')}
/> />
) : null} ) : null}
{pageMe.followedTags?.shown ? ( {pageMe?.followedTags?.shown ? (
<MenuRow <MenuRow
iconFront='Hash' iconFront='Hash'
iconBack='ChevronRight' iconBack='ChevronRight'
@ -76,7 +76,7 @@ const Collections: React.FC = () => {
onPress={() => navigation.navigate('Tab-Me-FollowedTags')} onPress={() => navigation.navigate('Tab-Me-FollowedTags')}
/> />
) : null} ) : null}
{pageMe.announcements?.shown ? ( {pageMe?.announcements?.shown ? (
<MenuRow <MenuRow
iconFront='Clipboard' iconFront='Clipboard'
iconBack='ChevronRight' iconBack='ChevronRight'

View File

@ -32,7 +32,7 @@ const Logout: React.FC = () => {
onPress: () => { onPress: () => {
if (accountActive) { if (accountActive) {
haptics('Light') haptics('Light')
removeAccount(accountActive) removeAccount(accountActive, false)
} }
} }
}, },

View File

@ -44,7 +44,7 @@ const Root: React.FC<
} }
const TabNotifications: React.FC = () => { const TabNotifications: React.FC = () => {
usePopToTop() usePopToTop('Tab-Notifications-Root')
return ( return (
<Stack.Navigator screenOptions={{ headerShadowVisible: false }}> <Stack.Navigator screenOptions={{ headerShadowVisible: false }}>

View File

@ -1,8 +1,7 @@
import { HeaderRight } from '@components/Header' import { HeaderRight } from '@components/Header'
import Timeline from '@components/Timeline' import Timeline from '@components/Timeline'
import SegmentedControl from '@react-native-community/segmented-control' import SegmentedControl from '@react-native-community/segmented-control'
import { useNavigation } from '@react-navigation/native' import { NativeStackScreenProps } from '@react-navigation/native-stack'
import { NativeStackNavigationProp, NativeStackScreenProps } from '@react-navigation/native-stack'
import { TabPublicStackParamList } from '@utils/navigation/navigators' import { TabPublicStackParamList } from '@utils/navigation/navigators'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline' import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { getGlobalStorage, setGlobalStorage } from '@utils/storage/actions' import { getGlobalStorage, setGlobalStorage } from '@utils/storage/actions'
@ -14,12 +13,7 @@ import { Dimensions } from 'react-native'
import { SceneMap, TabView } from 'react-native-tab-view' import { SceneMap, TabView } from 'react-native-tab-view'
const Route = ({ route: { key: page } }: { route: any }) => { const Route = ({ route: { key: page } }: { route: any }) => {
const navigation =
useNavigation<NativeStackNavigationProp<TabPublicStackParamList, 'Tab-Public-Root'>>()
const queryKey: QueryKeyTimeline = ['Timeline', { page }] const queryKey: QueryKeyTimeline = ['Timeline', { page }]
useEffect(() => {
navigation.setParams({ queryKey })
}, [])
return <Timeline queryKey={queryKey} disableRefresh={page === 'Trending'} /> return <Timeline queryKey={queryKey} disableRefresh={page === 'Trending'} />
} }
@ -35,12 +29,11 @@ const Root: React.FC<NativeStackScreenProps<TabPublicStackParamList, 'Tab-Public
const { mode } = useTheme() const { mode } = useTheme()
const { t } = useTranslation('screenTabs') const { t } = useTranslation('screenTabs')
const previousSegment = getGlobalStorage.string('app.prev_public_segment')
const segments: StorageGlobal['app.prev_public_segment'][] = ['Local', 'LocalPublic', 'Trending'] const segments: StorageGlobal['app.prev_public_segment'][] = ['Local', 'LocalPublic', 'Trending']
const [segment, setSegment] = useState<number>( const [segment, setSegment] = useState<number>(
Math.max( Math.max(
0, 0,
segments.findIndex(segment => segment === previousSegment) segments.findIndex(segment => segment === getGlobalStorage.string('app.prev_public_segment'))
) )
) )
const [routes] = useState([ const [routes] = useState([
@ -48,6 +41,10 @@ const Root: React.FC<NativeStackScreenProps<TabPublicStackParamList, 'Tab-Public
{ key: 'LocalPublic', title: t('tabs.public.segments.federated') }, { key: 'LocalPublic', title: t('tabs.public.segments.federated') },
{ key: 'Trending', title: t('tabs.public.segments.trending') } { key: 'Trending', title: t('tabs.public.segments.trending') }
]) ])
useEffect(() => {
const page = segments[segment]
page && navigation.setParams({ queryKey: ['Timeline', { page }] })
}, [segment])
useEffect(() => { useEffect(() => {
navigation.setOptions({ navigation.setOptions({

View File

@ -8,7 +8,7 @@ import Root from './Root'
const Stack = createNativeStackNavigator<TabPublicStackParamList>() const Stack = createNativeStackNavigator<TabPublicStackParamList>()
const TabPublic: React.FC = () => { const TabPublic: React.FC = () => {
usePopToTop() usePopToTop('Tab-Public-Root')
return ( return (
<Stack.Navigator screenOptions={{ headerShadowVisible: false }}> <Stack.Navigator screenOptions={{ headerShadowVisible: false }}>
<Stack.Screen name='Tab-Public-Root' component={Root} /> <Stack.Screen name='Tab-Public-Root' component={Root} />

View File

@ -1,4 +1,5 @@
import ComponentHashtag from '@components/Hashtag' import ComponentHashtag from '@components/Hashtag'
import { Loading } from '@components/Loading'
import ComponentSeparator from '@components/Separator' import ComponentSeparator from '@components/Separator'
import CustomText from '@components/Text' import CustomText from '@components/Text'
import { useTrendsQuery } from '@utils/queryHooks/trends' import { useTrendsQuery } from '@utils/queryHooks/trends'
@ -6,26 +7,37 @@ 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'
import { Trans, useTranslation } from 'react-i18next' import { Trans, useTranslation } from 'react-i18next'
import { StyleSheet, TextInput, View } from 'react-native' import { StyleSheet, View } from 'react-native'
import { Circle } from 'react-native-animated-spinkit'
export interface Props { export interface Props {
isFetching: boolean isFetching: boolean
inputRef: React.RefObject<TextInput> searchTerm: string
setSearchTerm: React.Dispatch<React.SetStateAction<string>>
} }
const SearchEmpty: React.FC<Props> = ({ isFetching, inputRef, setSearchTerm }) => { const SearchEmpty: React.FC<Props> = ({ isFetching, searchTerm }) => {
const { colors } = useTheme() const { colors } = useTheme()
const { t } = useTranslation('screenTabs') const { t } = useTranslation('screenTabs')
const trendsTags = useTrendsQuery({ type: 'tags' }) const trendsTags = useTrendsQuery({ type: 'tags' })
return ( return (
<View style={{ paddingVertical: StyleConstants.Spacing.Global.PagePadding }}> <View
style={{
flex: 1,
minHeight: '100%',
paddingVertical: StyleConstants.Spacing.Global.PagePadding
}}
>
{isFetching ? ( {isFetching ? (
<View style={{ flex: 1, alignItems: 'center' }}> <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Circle size={StyleConstants.Font.Size.M * 1.25} color={colors.secondary} /> <Loading />
</View>
) : searchTerm.length ? (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<CustomText
style={{ color: colors.primaryDefault }}
children={t('shared.search.noResult')}
/>
</View> </View>
) : ( ) : (
<> <>

View File

@ -131,7 +131,6 @@ const TabSharedSearch: React.FC<TabSharedStackScreenProps<'Tab-Shared-Search'>>
style={{ flex: 1 }} style={{ flex: 1 }}
> >
<SectionList <SectionList
style={{ minHeight: '100%' }}
sections={data || []} sections={data || []}
renderItem={({ item, section }: { item: any; section: any }) => { renderItem={({ item, section }: { item: any; section: any }) => {
switch (section.title) { switch (section.title) {
@ -146,9 +145,7 @@ const TabSharedSearch: React.FC<TabSharedStackScreenProps<'Tab-Shared-Search'>>
} }
}} }}
stickySectionHeadersEnabled stickySectionHeadersEnabled
ListEmptyComponent={ ListEmptyComponent={<SearchEmpty isFetching={isFetching} searchTerm={searchTerm} />}
<SearchEmpty isFetching={isFetching} inputRef={inputRef} setSearchTerm={setSearchTerm} />
}
keyboardShouldPersistTaps='always' keyboardShouldPersistTaps='always'
renderSectionHeader={({ section: { translation } }) => ( renderSectionHeader={({ section: { translation } }) => (
<View <View

View File

@ -1,5 +1,6 @@
import { HeaderLeft } from '@components/Header' import { HeaderLeft } from '@components/Header'
import Icon from '@components/Icon' import Icon from '@components/Icon'
import { Loading } from '@components/Loading'
import ComponentSeparator from '@components/Separator' import ComponentSeparator from '@components/Separator'
import CustomText from '@components/Text' import CustomText from '@components/Text'
import TimelineDefault from '@components/Timeline/Default' import TimelineDefault from '@components/Timeline/Default'
@ -15,8 +16,7 @@ import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React, { useEffect, useRef, useState } from 'react' import React, { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Alert, FlatList, Pressable, View } from 'react-native' import { Alert, FlatList, Platform, Pressable, View } from 'react-native'
import { Circle } from 'react-native-animated-spinkit'
import { Path, Svg } from 'react-native-svg' import { Path, Svg } from 'react-native-svg'
const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
@ -34,6 +34,8 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
remote: ['Timeline', { page: 'Toot', toot: toot.id, remote: true }] remote: ['Timeline', { page: 'Toot', toot: toot.id, remote: true }]
} }
const flRef = useRef<FlatList<Mastodon.Status & { _level?: number; key?: string }>>(null)
useEffect(() => { useEffect(() => {
navigation.setOptions({ navigation.setOptions({
headerTitle: () => ( headerTitle: () => (
@ -69,12 +71,61 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
navigation.setParams({ toot, queryKey: queryKey.local }) navigation.setParams({ toot, queryKey: queryKey.local })
}, [hasRemoteContent]) }, [hasRemoteContent])
const flRef = useRef<FlatList>(null) const PREV_PER_BATCH = 1
const scrolled = useRef(false) const ancestorsCache = useRef<(Mastodon.Status & { _level?: number; key?: string })[]>()
const loaded = useRef<boolean>(false)
const prependContent = async () => {
loaded.current = true
if (ancestorsCache.current?.length) {
switch (Platform.OS) {
case 'ios':
for (let [] of Array(
Math.ceil(ancestorsCache.current.length / PREV_PER_BATCH)
).entries()) {
await new Promise(promise => setTimeout(promise, 64))
queryClient.setQueryData<{ pages: { body: Mastodon.Status[] }[] }>(
queryKey.local,
old => {
const insert = ancestorsCache.current?.slice(-PREV_PER_BATCH)
ancestorsCache.current = ancestorsCache.current?.slice(0, -PREV_PER_BATCH)
if (insert) {
old?.pages[0].body.unshift(...insert)
}
return old
}
)
}
break
default:
queryClient.setQueryData<{ pages: { body: Mastodon.Status[] }[] }>(
queryKey.local,
old => {
ancestorsCache.current && old?.pages[0].body.unshift(...ancestorsCache.current)
return old
}
)
setTimeout(() => {
flRef.current?.scrollToIndex({
index: ancestorsCache.current?.length || 0,
viewOffset: 50
})
}, 50)
break
}
}
}
const match = urlMatcher(toot.url || toot.uri) const match = urlMatcher(toot.url || toot.uri)
const highlightIndex = useRef<number>(0) const remoteQueryEnabled =
const query = useQuery<{ pages: { body: Mastodon.Status[] }[] }>( ['public', 'unlisted'].includes(toot.visibility) &&
match?.domain !== getAccountStorage.string('auth.domain')
const query = useQuery<{
pages: { body: (Mastodon.Status & { _level?: number; key?: string })[] }[]
}>(
queryKey.local, queryKey.local,
async () => { async () => {
const context = await apiInstance<{ const context = await apiInstance<{
@ -85,15 +136,14 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
url: `statuses/${toot.id}/context` url: `statuses/${toot.id}/context`
}).then(res => res.body) }).then(res => res.body)
highlightIndex.current = context.ancestors.length ancestorsCache.current = [...context.ancestors]
const statuses = [{ ...toot }, ...context.descendants]
const statuses = [...context.ancestors, { ...toot }, ...context.descendants]
return { return {
pages: [ pages: [
{ {
body: statuses.map((status, index) => { body: statuses.map((status, index) => {
if (index < highlightIndex.current || status.id === toot.id) { if (index === 0) {
status._level = 0 status._level = 0
return status return status
} else { } else {
@ -108,39 +158,23 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
} }
}, },
{ {
placeholderData: { pages: [{ body: [toot] }] }, placeholderData: { pages: [{ body: [{ ...toot, _level: 0, key: `${toot.id}_cache` }] }] },
enabled: !toot._remote, enabled: !toot._remote,
staleTime: 0, staleTime: 0,
refetchOnMount: true, refetchOnMount: true,
onSuccess: data => { onSuccess: async data => {
if (data.pages[0].body.length < 1) { if (data.pages[0].body.length < 1) {
navigation.goBack() navigation.goBack()
return return
} }
if (!scrolled.current) { if (!remoteQueryEnabled) {
scrolled.current = true await prependContent()
const pointer = data.pages[0].body.findIndex(({ id }) => id === toot.id)
if (pointer < 1) return
const length = flRef.current?.props.data?.length
if (!length) return
try {
setTimeout(() => {
try {
flRef.current?.scrollToIndex({
index: pointer,
viewOffset: 100
})
} catch {}
}, 500)
} catch (error) {
return
}
} }
} }
} }
) )
useQuery<Mastodon.Status[]>( const remoteQuery = useQuery<Mastodon.Status[]>(
queryKey.remote, queryKey.remote,
async () => { async () => {
const domain = match?.domain const domain = match?.domain
@ -165,29 +199,55 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
return Promise.resolve([{ ...toot }]) return Promise.resolve([{ ...toot }])
} }
highlightIndex.current = context.ancestors.length if ((ancestorsCache.current?.length || 0) < context.ancestors.length) {
ancestorsCache.current = context.ancestors.map(ancestor => {
const statuses = [...context.ancestors, { ...toot }, ...context.descendants] const localMatch = ancestorsCache.current?.find(local => local.uri === ancestor.uri)
if (localMatch) {
return localMatch
} else {
return {
...ancestor,
_remote: true,
account: { ...ancestor.account, _remote: true },
mentions: ancestor.mentions.map(mention => ({
...mention,
_remote: true
})),
...(ancestor.reblog && {
reblog: {
...ancestor.reblog,
_remote: true,
account: { ...ancestor.reblog.account, _remote: true },
mentions: ancestor.reblog.mentions.map(mention => ({
...mention,
_remote: true
}))
}
})
}
}
})
}
const statuses = [{ ...toot }, ...context.descendants]
return statuses.map((status, index) => { return statuses.map((status, index) => {
if (index < highlightIndex.current || status.id === toot.id) { if (index === 0) {
status._level = 0 status._level = 0
return status return status
} else {
const repliedLevel: number =
statuses.find(s => s.id === status.in_reply_to_id)?._level || 0
status._level = repliedLevel + 1
return status
} }
const repliedLevel: number = statuses.find(s => s.id === status.in_reply_to_id)?._level || 0
status._level = repliedLevel + 1
return status
}) })
}, },
{ {
enabled: enabled: (toot._remote ? true : query.isFetched) && remoteQueryEnabled,
(toot._remote ? true : query.isFetched) &&
['public', 'unlisted'].includes(toot.visibility) &&
match?.domain !== getAccountStorage.string('auth.domain'),
staleTime: 0, staleTime: 0,
refetchOnMount: true, refetchOnMount: true,
onSuccess: data => { retry: false,
onSuccess: async data => {
if ((query.data?.pages[0].body.length || 0) < 1 && data.length < 1) { if ((query.data?.pages[0].body.length || 0) < 1 && data.length < 1) {
navigation.goBack() navigation.goBack()
return return
@ -195,60 +255,46 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
if ((query.data?.pages[0].body.length || 0) < data.length) { if ((query.data?.pages[0].body.length || 0) < data.length) {
queryClient.cancelQueries(queryKey.local) queryClient.cancelQueries(queryKey.local)
queryClient.setQueryData<{ queryClient.setQueryData<{ pages: { body: Mastodon.Status[] }[] }>(
pages: { body: Mastodon.Status[] }[] queryKey.local,
}>(queryKey.local, old => { old => {
setHasRemoteContent(true) setHasRemoteContent(true)
return { return {
pages: [ pages: [
{ {
body: data.map(remote => { body: data.map(remote => {
const localMatch = old?.pages[0].body.find(local => local.uri === remote.uri) const localMatch = old?.pages[0].body.find(local => local.uri === remote.uri)
if (localMatch) { if (localMatch) {
return { ...localMatch, _level: remote._level } return { ...localMatch, _level: remote._level }
} else { } else {
return { return {
...remote, ...remote,
_remote: true, _remote: true,
account: { ...remote.account, _remote: true }, account: { ...remote.account, _remote: true },
mentions: remote.mentions.map(mention => ({ ...mention, _remote: true })), mentions: remote.mentions.map(mention => ({ ...mention, _remote: true })),
...(remote.reblog && { ...(remote.reblog && {
reblog: { reblog: {
...remote.reblog, ...remote.reblog,
_remote: true, _remote: true,
account: { ...remote.reblog.account, _remote: true }, account: { ...remote.reblog.account, _remote: true },
mentions: remote.reblog.mentions.map(mention => ({ mentions: remote.reblog.mentions.map(mention => ({
...mention, ...mention,
_remote: true _remote: true
})) }))
} }
}) })
}
} }
} })
}) }
} ]
] }
} }
}) )
}
scrolled.current = true
const pointer = data.findIndex(({ id }) => id === toot.id)
if (pointer < 1) return
const length = flRef.current?.props.data?.length
if (!length) return
try {
setTimeout(() => {
try {
flRef.current?.scrollToIndex({
index: pointer,
viewOffset: 100
})
} catch {}
}, 500)
} catch (error) {
return
} }
},
onSettled: async () => {
await prependContent()
} }
} }
) )
@ -265,29 +311,18 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
data={query.data?.pages?.[0].body} data={query.data?.pages?.[0].body}
renderItem={({ item, index }) => { renderItem={({ item, index }) => {
const prev = query.data?.pages[0].body[index - 1]?._level || 0 const prev = query.data?.pages[0].body[index - 1]?._level || 0
const curr = item._level const curr = item._level || 0
const next = query.data?.pages[0].body[index + 1]?._level || 0 const next = query.data?.pages[0].body[index + 1]?._level || 0
return ( return (
<View <View
style={{ style={{ paddingLeft: Math.min(curr - 1, MAX_LEVEL) * StyleConstants.Spacing.S }}
paddingLeft:
index > highlightIndex.current
? Math.min(item._level - 1, MAX_LEVEL) * StyleConstants.Spacing.S
: undefined
}}
onLayout={({ onLayout={({
nativeEvent: { nativeEvent: {
layout: { height } layout: { height }
} }
}) => setHeights({ ...heights, [index]: height })} }) => setHeights({ ...heights, [index]: height })}
> >
<TimelineDefault
item={item}
queryKey={item._remote ? queryKey.remote : queryKey.local}
highlighted={toot.id === item.id || item.id === 'cached'}
isConversation={toot.id !== item.id && item.id !== 'cached'}
/>
{curr > 1 || next > 1 {curr > 1 || next > 1
? [...new Array(curr)].map((_, i) => { ? [...new Array(curr)].map((_, i) => {
if (i > MAX_LEVEL) return null if (i > MAX_LEVEL) return null
@ -371,6 +406,13 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
} }
}) })
: null} : null}
<TimelineDefault
item={item}
queryKey={item._remote ? queryKey.remote : queryKey.local}
highlighted={toot.id === item.id || item.id === 'cached'}
isConversation={toot.id !== item.id && item.id !== 'cached'}
noBackground
/>
{/* <CustomText {/* <CustomText
children={query.data?.pages[0].body[index - 1]?._level} children={query.data?.pages[0].body[index - 1]?._level}
style={{ position: 'absolute', top: 4, left: 4, color: colors.red }} style={{ position: 'absolute', top: 4, left: 4, color: colors.red }}
@ -397,7 +439,7 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
? 0 ? 0
: StyleConstants.Avatar.XS + : StyleConstants.Avatar.XS +
StyleConstants.Spacing.S + StyleConstants.Spacing.S +
Math.min(Math.max(0, leadingItem._level - 1), MAX_LEVEL) * Math.min(Math.max(0, (leadingItem._level || 0) - 1), MAX_LEVEL) *
StyleConstants.Spacing.S StyleConstants.Spacing.S
} }
/> />
@ -416,35 +458,27 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
</> </>
) )
}} }}
onScrollToIndexFailed={error => { ListFooterComponent={
const offset = error.averageItemLength * error.index query.isFetching || remoteQuery.isFetching ? (
flRef.current?.scrollToOffset({ offset }) <View style={{ flex: 1, alignItems: 'center' }} children={<Loading />} />
try { ) : null
error.index < (query.data?.pages[0].body.length || 0) && }
{...(loaded.current && { maintainVisibleContentPosition: { minIndexForVisible: 0 } })}
{...(Platform.OS !== 'ios' && {
onScrollToIndexFailed: error => {
const offset = error.averageItemLength * error.index
flRef.current?.scrollToOffset({ offset })
error.index < (ancestorsCache.current?.length || 0) &&
setTimeout( setTimeout(
() => () =>
flRef.current?.scrollToIndex({ flRef.current?.scrollToIndex({
index: error.index, index: error.index,
viewOffset: 100 viewOffset: 50
}), }),
500 50
) )
} catch {} }
}} })}
ListFooterComponent={
<View
style={{
flex: 1,
alignItems: 'center',
backgroundColor: colors.backgroundDefault,
marginHorizontal: StyleConstants.Spacing.M
}}
>
{query.isFetching ? (
<Circle size={StyleConstants.Font.Size.L} color={colors.secondary} />
) : null}
</View>
}
/> />
) )
} }

View File

@ -1,6 +1,7 @@
import ComponentAccount from '@components/Account' import ComponentAccount from '@components/Account'
import { HeaderLeft } from '@components/Header' import { HeaderLeft } from '@components/Header'
import Icon from '@components/Icon' import Icon from '@components/Icon'
import { Loading } from '@components/Loading'
import ComponentSeparator from '@components/Separator' import ComponentSeparator from '@components/Separator'
import CustomText from '@components/Text' import CustomText from '@components/Text'
import apiInstance from '@utils/api/instance' import apiInstance from '@utils/api/instance'
@ -13,7 +14,6 @@ import { useTheme } from '@utils/styles/ThemeManager'
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { View } from 'react-native' import { View } from 'react-native'
import { Circle, Flow } from 'react-native-animated-spinkit'
import { FlatList } from 'react-native-gesture-handler' import { FlatList } from 'react-native-gesture-handler'
const TabSharedUsers: React.FC<TabSharedStackScreenProps<'Tab-Shared-Users'>> = ({ const TabSharedUsers: React.FC<TabSharedStackScreenProps<'Tab-Shared-Users'>> = ({
@ -36,7 +36,7 @@ const TabSharedUsers: React.FC<TabSharedStackScreenProps<'Tab-Shared-Users'>> =
...queryKey[1] ...queryKey[1]
}) })
const [isSearching, setIsSearching] = useState(false) const [isSearching, setIsSearching] = useState<number | null>(null)
return ( return (
<FlatList <FlatList
@ -46,14 +46,14 @@ const TabSharedUsers: React.FC<TabSharedStackScreenProps<'Tab-Shared-Users'>> =
minHeight: '100%', minHeight: '100%',
paddingVertical: StyleConstants.Spacing.Global.PagePadding paddingVertical: StyleConstants.Spacing.Global.PagePadding
}} }}
renderItem={({ item }) => ( renderItem={({ item, index }) => (
<ComponentAccount <ComponentAccount
account={item} account={item}
props={{ props={{
disabled: isSearching, disabled: isSearching === index,
onPress: () => { onPress: () => {
if (data?.pages[0]?.remoteData) { if (data?.pages[0]?.remoteData) {
setIsSearching(true) setIsSearching(index)
apiInstance<SearchResult>({ apiInstance<SearchResult>({
version: 'v2', version: 'v2',
method: 'get', method: 'get',
@ -66,18 +66,18 @@ const TabSharedUsers: React.FC<TabSharedStackScreenProps<'Tab-Shared-Users'>> =
} }
}) })
.then(res => { .then(res => {
setIsSearching(false) setIsSearching(null)
if (res.body.accounts[0]) { if (res.body.accounts[0]) {
navigation.push('Tab-Shared-Account', { account: res.body.accounts[0] }) navigation.push('Tab-Shared-Account', { account: res.body.accounts[0] })
} }
}) })
.catch(() => setIsSearching(false)) .catch(() => setIsSearching(null))
} else { } else {
navigation.push('Tab-Shared-Account', { account: item }) navigation.push('Tab-Shared-Account', { account: item })
} }
} }
}} }}
children={<Flow size={StyleConstants.Font.Size.L} color={colors.secondary} />} children={<Loading />}
/> />
)} )}
onEndReached={() => hasNextPage && !isFetchingNextPage && fetchNextPage()} onEndReached={() => hasNextPage && !isFetchingNextPage && fetchNextPage()}
@ -93,7 +93,7 @@ const TabSharedUsers: React.FC<TabSharedStackScreenProps<'Tab-Shared-Users'>> =
alignItems: 'center' alignItems: 'center'
}} }}
> >
<Circle size={StyleConstants.Font.Size.L} color={colors.secondary} /> <Loading />
</View> </View>
) : null ) : null
} }

View File

@ -32,11 +32,7 @@ import { routingInstrumentation } from '../utils/startup/sentry'
const Stack = createNativeStackNavigator<RootStackParamList>() const Stack = createNativeStackNavigator<RootStackParamList>()
export interface Props { const Screens: React.FC = () => {
localCorrupt?: string
}
const Screens: React.FC<Props> = ({ localCorrupt }) => {
const { t, i18n } = useTranslation([ const { t, i18n } = useTranslation([
'common', 'common',
'screens', 'screens',
@ -64,24 +60,6 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => {
return () => screenshotListener.remove() return () => screenshotListener.remove()
}, []) }, [])
// On launch display login credentials corrupt information
useEffect(() => {
const showLocalCorrect = () => {
if (localCorrupt) {
displayMessage({
message: t('screens:localCorrupt.message'),
description: localCorrupt.length ? localCorrupt : undefined,
type: 'danger'
})
// @ts-ignore
navigationRef.navigate('Screen-Tabs', {
screen: 'Tab-Me'
})
}
}
return showLocalCorrect()
}, [localCorrupt])
// Lazily update users's preferences, for e.g. composing default visibility // Lazily update users's preferences, for e.g. composing default visibility
useInstanceQuery({ options: { enabled: !!accountActive } }) useInstanceQuery({ options: { enabled: !!accountActive } })
useProfileQuery({ options: { enabled: !!accountActive } }) useProfileQuery({ options: { enabled: !!accountActive } })

View File

@ -13,7 +13,7 @@ export type Params = {
} }
headers?: { [key: string]: string } headers?: { [key: string]: string }
body?: FormData body?: FormData
extras?: Omit<AxiosRequestConfig, 'method' | 'url' | 'params' | 'headers' | 'data'> extras?: Omit<AxiosRequestConfig, 'method' | 'baseURL' | 'url' | 'params' | 'headers' | 'data'>
} }
const apiInstance = async <T = unknown>({ const apiInstance = async <T = unknown>({

View File

@ -51,8 +51,6 @@ const features = [
} }
] ]
export const featureCheck = (feature: string): boolean => { export const featureCheck = (feature: string, v?: string): boolean =>
const version = getAccountStorage.string('version') (features.find(f => f.feature === feature)?.version || 999) <=
return !!features.filter(f => f.feature === feature).filter(f => parseFloat(version) >= f.version) parseFloat(v || getAccountStorage.string('version'))
?.length
}

View File

@ -1,6 +1,8 @@
import * as htmlparser2 from 'htmlparser2' import * as htmlparser2 from 'htmlparser2'
const removeHTML = (text: string): string => { const removeHTML = (text: string): string => {
if (!text) return ''
let raw: string = '' let raw: string = ''
const parser = new htmlparser2.Parser({ const parser = new htmlparser2.Parser({

View File

@ -1,15 +1,18 @@
import { StackActions, useFocusEffect, useNavigation } from '@react-navigation/native' import { StackActions } from '@react-navigation/native'
import { useGlobalStorage } from '@utils/storage/actions' import { useGlobalStorage } from '@utils/storage/actions'
import { useEffect } from 'react' import { useEffect } from 'react'
import navigationRef from './navigationRef'
// Mostly used when switching account and sub pages were still querying the old instance // Mostly used when switching account and sub pages were still querying the old instance
const usePopToTop = () => { const usePopToTop = (name: string) => {
const navigation = useNavigation()
const [accountActive] = useGlobalStorage.string('account.active') const [accountActive] = useGlobalStorage.string('account.active')
useEffect(() => { useEffect(() => {
navigation.dispatch(StackActions.popToTop()) const currentRoute = navigationRef.getCurrentRoute()
if (currentRoute && currentRoute.name !== name) {
navigationRef.dispatch(StackActions.popToTop())
}
}, [accountActive]) }, [accountActive])
} }

View File

@ -4,7 +4,7 @@ import { getGlobalStorage, setGlobalStorage } from '@utils/storage/actions'
import * as Notifications from 'expo-notifications' import * as Notifications from 'expo-notifications'
import { Platform } from 'react-native' import { Platform } from 'react-native'
export const updateExpoToken = async () => { export const updateExpoToken = async (): Promise<string> => {
const expoToken = getGlobalStorage.string('app.expo_token') const expoToken = getGlobalStorage.string('app.expo_token')
if (Platform.OS === 'android') { if (Platform.OS === 'android') {
@ -12,16 +12,19 @@ export const updateExpoToken = async () => {
} }
if (expoToken?.length) { if (expoToken?.length) {
return Promise.resolve() return Promise.resolve(expoToken)
} else { } else {
if (isDevelopment) { if (isDevelopment) {
setGlobalStorage('app.expo_token', 'ExponentPushToken[DEVELOPMENT_1]') setGlobalStorage('app.expo_token', 'ExponentPushToken[DEVELOPMENT_1]')
return Promise.resolve() return Promise.resolve('ExponentPushToken[DEVELOPMENT_1]')
} }
return await Notifications.getExpoPushTokenAsync({ return await Notifications.getExpoPushTokenAsync({
experienceId: '@xmflsct/tooot', experienceId: '@xmflsct/tooot',
applicationId: 'com.xmflsct.app.tooot' applicationId: 'com.xmflsct.app.tooot'
}).then(({ data }) => setGlobalStorage('app.expo_token', data)) }).then(({ data }) => {
setGlobalStorage('app.expo_token', data)
return data
})
} }
} }

View File

@ -5,6 +5,7 @@ import apiGeneral from '@utils/api/general'
import apiTooot from '@utils/api/tooot' import apiTooot from '@utils/api/tooot'
import navigationRef from '@utils/navigation/navigationRef' import navigationRef from '@utils/navigation/navigationRef'
import { import {
generateAccountKey,
getAccountDetails, getAccountDetails,
getGlobalStorage, getGlobalStorage,
setAccountStorage, setAccountStorage,
@ -12,7 +13,7 @@ import {
} from '@utils/storage/actions' } from '@utils/storage/actions'
import { AxiosError } from 'axios' import { AxiosError } from 'axios'
import * as Notifications from 'expo-notifications' import * as Notifications from 'expo-notifications'
import { useEffect } from 'react' import { useEffect, useRef } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { AppState } from 'react-native' import { AppState } from 'react-native'
import { updateExpoToken } from './updateExpoToken' import { updateExpoToken } from './updateExpoToken'
@ -20,22 +21,32 @@ import { updateExpoToken } from './updateExpoToken'
const pushUseConnect = () => { const pushUseConnect = () => {
const { t } = useTranslation('screens') const { t } = useTranslation('screens')
useEffect(() => { const startupChecked = useRef<boolean>(false)
updateExpoToken()
}, [])
const [expoToken] = useGlobalStorage.string('app.expo_token') const [expoToken] = useGlobalStorage.string('app.expo_token')
const pushEnabledCount = getGlobalStorage.object('accounts')?.filter(account => { const accounts = getGlobalStorage.object('accounts')
return getAccountDetails(['push'], account)?.push?.global const pushEnabled = accounts
}).length ?.map(account => {
const details = getAccountDetails(['push', 'auth.domain', 'auth.account.id'], account)
if (details?.push?.global) {
return {
accountKey: generateAccountKey({
domain: details['auth.domain'],
id: details['auth.account.id']
}),
push: details.push
}
}
})
.filter(d => !!d)
const connectQuery = useQuery<any, AxiosError>( const connectQuery = useQuery<{ accounts: string[] } | undefined, AxiosError>(
['tooot', { endpoint: 'push/connect' }], ['tooot', { endpoint: 'push/connect' }],
() => () =>
apiTooot<Mastodon.Status>({ apiTooot<{ accounts: string[] } | undefined>({
method: 'get', method: 'get',
url: `push/connect/${expoToken}` url: `push/connect/${expoToken}`
}), }).then(res => res.body),
{ {
enabled: false, enabled: false,
retry: (failureCount, error) => { retry: (failureCount, error) => {
@ -48,6 +59,17 @@ const pushUseConnect = () => {
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
refetchOnReconnect: false, refetchOnReconnect: false,
onSettled: () => Notifications.setBadgeCountAsync(0), onSettled: () => Notifications.setBadgeCountAsync(0),
onSuccess: data => {
if (!startupChecked.current && data?.accounts) {
startupChecked.current = true
for (const acct of data.accounts) {
const matchedAcct = pushEnabled?.find(p => p?.accountKey === acct)
if (matchedAcct && !matchedAcct.push.global) {
setAccountStorage([{ key: 'push', value: { ...matchedAcct.push, global: true } }])
}
}
}
},
onError: error => { onError: error => {
if (error?.status == 404) { if (error?.status == 404) {
displayMessage({ displayMessage({
@ -96,18 +118,21 @@ const pushUseConnect = () => {
) )
useEffect(() => { useEffect(() => {
Sentry.setTags({ expoToken, pushEnabledCount }) updateExpoToken().then(async token => {
const badgeCount = await Notifications.getBadgeCountAsync()
if (token && (pushEnabled?.length || badgeCount)) {
connectQuery.refetch()
}
})
}, [])
if (expoToken && pushEnabledCount) { useEffect(() => {
connectQuery.refetch() Sentry.setTags({ expoToken, pushEnabledCount: pushEnabled?.length })
}
const appStateListener = AppState.addEventListener('change', state => { const appStateListener = AppState.addEventListener('change', state => {
if (expoToken && pushEnabledCount && state === 'active') { if (expoToken && pushEnabled?.length && state === 'active') {
Notifications.getBadgeCountAsync().then(count => { Notifications.getBadgeCountAsync().then(count => {
if (count > 0) { if (count > 0) connectQuery.refetch()
connectQuery.refetch()
}
}) })
} }
}) })
@ -115,7 +140,7 @@ const pushUseConnect = () => {
return () => { return () => {
appStateListener.remove() appStateListener.remove()
} }
}, [expoToken, pushEnabledCount]) }, [expoToken, pushEnabled?.length])
} }
export default pushUseConnect export default pushUseConnect

View File

@ -9,6 +9,7 @@ export type QueryKeySearch = [
type?: 'accounts' | 'hashtags' | 'statuses' type?: 'accounts' | 'hashtags' | 'statuses'
term?: string term?: string
limit?: number limit?: number
following?: boolean
} }
] ]
@ -18,8 +19,8 @@ export type SearchResult = {
statuses: Mastodon.Status[] statuses: Mastodon.Status[]
} }
const queryFunction = async ({ queryKey }: QueryFunctionContext<QueryKeySearch>) => { const queryFunction = async ({ queryKey, meta }: QueryFunctionContext<QueryKeySearch>) => {
const { type, term, limit = 20 } = queryKey[1] const { type, term, limit = 10, following = false } = queryKey[1]
if (!term?.length) { if (!term?.length) {
return Promise.reject('Empty search term') return Promise.reject('Empty search term')
} }
@ -31,8 +32,10 @@ const queryFunction = async ({ queryKey }: QueryFunctionContext<QueryKeySearch>)
q: term, q: term,
...(type && { type }), ...(type && { type }),
limit, limit,
resolve: true resolve: true,
} following
},
...(meta && { extras: meta })
}) })
return res.body return res.body
} }
@ -47,10 +50,18 @@ const useSearchQuery = <T = SearchResult>({
return useQuery(queryKey, queryFunction, { ...options, staleTime: 3600, cacheTime: 3600 }) return useQuery(queryKey, queryFunction, { ...options, staleTime: 3600, cacheTime: 3600 })
} }
export const searchLocalStatus = async (uri: Mastodon.Status['uri']): Promise<Mastodon.Status> => { export const searchLocalStatus = async (
uri: Mastodon.Status['uri'],
timeout: boolean = false
): Promise<Mastodon.Status> => {
const queryKey: QueryKeySearch = ['Search', { type: 'statuses', term: uri, limit: 1 }] const queryKey: QueryKeySearch = ['Search', { type: 'statuses', term: uri, limit: 1 }]
return await queryClient return await queryClient
.fetchQuery(queryKey, queryFunction, { staleTime: 3600, cacheTime: 3600 }) .fetchQuery(queryKey, queryFunction, {
staleTime: 3600,
cacheTime: 3600,
retry: false,
...(timeout && { meta: { timeout: 1000 } })
})
.then(res => .then(res =>
res.statuses[0]?.uri === uri || res.statuses[0]?.url === uri res.statuses[0]?.uri === uri || res.statuses[0]?.url === uri
? res.statuses[0] ? res.statuses[0]
@ -59,11 +70,17 @@ export const searchLocalStatus = async (uri: Mastodon.Status['uri']): Promise<Ma
} }
export const searchLocalAccount = async ( export const searchLocalAccount = async (
url: Mastodon.Account['url'] url: Mastodon.Account['url'],
timeout: boolean = false
): Promise<Mastodon.Account> => { ): Promise<Mastodon.Account> => {
const queryKey: QueryKeySearch = ['Search', { type: 'accounts', term: url, limit: 1 }] const queryKey: QueryKeySearch = ['Search', { type: 'accounts', term: url, limit: 1 }]
return await queryClient return await queryClient
.fetchQuery(queryKey, queryFunction, { staleTime: 3600, cacheTime: 3600 }) .fetchQuery(queryKey, queryFunction, {
staleTime: 3600,
cacheTime: 3600,
retry: false,
...(timeout && { meta: { timeout: 1000 } })
})
.then(res => (res.accounts[0].url === url ? res.accounts[0] : Promise.reject())) .then(res => (res.accounts[0].url === url ? res.accounts[0] : Promise.reject()))
} }

View File

@ -11,7 +11,7 @@ import apiInstance from '@utils/api/instance'
import { featureCheck } from '@utils/helpers/featureCheck' import { featureCheck } from '@utils/helpers/featureCheck'
import { useNavState } from '@utils/navigation/navigators' import { useNavState } from '@utils/navigation/navigators'
import { queryClient } from '@utils/queryHooks' import { queryClient } from '@utils/queryHooks'
import { getAccountStorage } from '@utils/storage/actions' import { getAccountStorage, setAccountStorage } from '@utils/storage/actions'
import { AxiosError } from 'axios' import { AxiosError } from 'axios'
import { uniqBy } from 'lodash' import { uniqBy } from 'lodash'
import { searchLocalStatus } from './search' import { searchLocalStatus } from './search'
@ -85,6 +85,11 @@ export const queryFunctionTimeline = async ({
url: 'timelines/home', url: 'timelines/home',
params params
}).then(res => { }).then(res => {
if (marker && !res.body.length) {
setAccountStorage([{ key: 'read_marker_following', value: undefined }])
return Promise.reject()
}
if (!page.showBoosts || !page.showReplies) { if (!page.showBoosts || !page.showReplies) {
return { return {
...res, ...res,

View File

@ -1,67 +1,15 @@
import NetInfo from '@react-native-community/netinfo' import NetInfo from '@react-native-community/netinfo'
import { onlineManager } from '@tanstack/react-query' import { onlineManager } from '@tanstack/react-query'
import apiInstance from '@utils/api/instance'
import { storage } from '@utils/storage'
import { getAccountStorage, removeAccount, setAccountStorage } from '@utils/storage/actions'
import log from './log' import log from './log'
const netInfo = async (): Promise<{ const netInfo = () => {
connected?: boolean
corrupted?: string
} | void> => {
log('log', 'netInfo', 'initializing') log('log', 'netInfo', 'initializing')
const netInfo = await NetInfo.fetch()
onlineManager.setEventListener(setOnline => { onlineManager.setEventListener(setOnline => {
return NetInfo.addEventListener(state => { return NetInfo.addEventListener(state => {
setOnline(!!state.isConnected) setOnline(!!state.isConnected)
}) })
}) })
if (netInfo.isConnected) {
log('log', 'netInfo', 'network connected')
if (storage.account) {
const domain = getAccountStorage.string('auth.domain')
const id = getAccountStorage.string('auth.account.id')
const account = `${domain}/${id}`
log('log', 'netInfo', 'checking locally stored credentials')
let resVerify: Mastodon.Account
try {
resVerify = await apiInstance<Mastodon.Account>({
method: 'get',
url: `accounts/verify_credentials`
}).then(res => res.body)
} catch (error: any) {
log('error', 'netInfo', 'local credential check failed')
if (error?.status && error.status == 401) {
removeAccount(account)
}
return Promise.resolve({ corrupted: error.data?.error })
}
log('log', 'netInfo', 'local credential check passed')
if (resVerify.id !== id) {
log('error', 'netInfo', 'local id does not match remote id')
removeAccount(account)
return Promise.resolve({ connected: true, corrupted: '' })
} else {
setAccountStorage([
{ key: 'auth.account.acct', value: resVerify.acct },
{ key: 'auth.account.avatar_static', value: resVerify.avatar_static }
])
return Promise.resolve({ connected: true })
}
} else {
log('log', 'netInfo', 'no local credential found')
return Promise.resolve()
}
} else {
log('warn', 'netInfo', 'network not connected')
return Promise.resolve()
}
} }
export default netInfo export default netInfo

View File

@ -1,14 +0,0 @@
import * as Localization from 'expo-localization'
import log from './log'
const timezone = () => {
log('log', 'Timezone', Localization.getCalendars()[0].timeZone || 'unknown')
if ('__setDefaultTimeZone' in Intl.DateTimeFormat) {
try {
// @ts-ignore
Intl.DateTimeFormat.__setDefaultTimeZone(Intl.DateTimeFormat.__setDefaultTimeZone('xxx'))
} catch {}
}
}
export default timezone

View File

@ -1,4 +1,9 @@
import { displayMessage } from '@components/Message'
import i18n from '@i18n/index'
import apiGeneral from '@utils/api/general'
import navigationRef from '@utils/navigation/navigationRef'
import { queryClient } from '@utils/queryHooks' import { queryClient } from '@utils/queryHooks'
import log from '@utils/startup/log'
import { storage } from '@utils/storage' import { storage } from '@utils/storage'
import { Platform } from 'react-native' import { Platform } from 'react-native'
import { import {
@ -222,10 +227,98 @@ export const generateAccountKey = ({
}) => `${domain}/${id}` }) => `${domain}/${id}`
export const setAccount = async (account: string) => { export const setAccount = async (account: string) => {
storage.account = new MMKV({ id: account }) const temp = new MMKV({ id: account })
setGlobalStorage('account.active', account) const token = temp.getString('auth.token')
await queryClient.resetQueries() const domain = temp.getString('auth.domain')
queryClient.clear()
if (!token || !domain) {
await removeAccount(account)
return
}
await apiGeneral<Mastodon.Account>({
method: 'get',
domain,
url: 'api/v1/accounts/verify_credentials',
headers: {
Authorization: `Bearer ${token}`
}
})
.then(res => res.body)
.then(async a => {
temp.set('auth.account.acct', a.acct)
temp.set('auth.account.avatar_static', a.avatar_static)
log('log', 'setAccount', `binding storage of ${account}`)
await queryClient.resetQueries()
queryClient.clear()
storage.account = temp
setGlobalStorage('account.active', account)
})
.catch(async error => {
if (error?.status && error.status == 401) {
log('log', 'setAccount', `unauthorised ${account}`)
await removeAccount(account)
}
})
}
export const removeAccount = async (account: string, warning: boolean = true) => {
const temp = new MMKV({ id: account })
if (warning) {
const acct = temp.getString('auth.account.acct')
const domain = temp.getString('auth.account.domain')
displayMessage({
message: i18n.t('screens:localCorrupt.message'),
...(acct && domain && { description: `@${acct}@${domain}` }),
type: 'danger'
})
}
// @ts-ignore
navigationRef.navigate('Screen-Tabs', { screen: 'Tab-Me' })
const revokeDetails = {
domain: temp.getString('auth.domain'),
client_id: temp.getString('auth.clientId'),
client_secret: temp.getString('auth.clientSecret'),
token: temp.getString('auth.token')
}
if (
revokeDetails.domain &&
revokeDetails.client_id &&
revokeDetails.client_secret &&
revokeDetails.token
) {
const body = new FormData()
body.append('client_id', revokeDetails.client_id)
body.append('client_secret', revokeDetails.client_secret)
body.append('token', revokeDetails.token)
apiGeneral({ method: 'post', domain: revokeDetails.domain, url: '/oauth/revoke', body })
}
const currAccounts: NonNullable<StorageGlobal['accounts']> =
getGlobalStorage.object('accounts') || []
const nextAccounts: NonNullable<StorageGlobal['accounts']> = currAccounts.filter(
a => a !== account
)
storage.global.set('accounts', JSON.stringify(nextAccounts))
if (nextAccounts.length) {
log('log', 'removeAccount', `trying next account ${nextAccounts[nextAccounts.length - 1]}`)
await setAccount(nextAccounts[nextAccounts.length - 1])
} else {
log('log', 'removeAccount', 'setting to undefined')
await queryClient.resetQueries()
queryClient.clear()
storage.account = undefined
setGlobalStorage('account.active', undefined)
}
new MMKV({ id: account }).clearAll()
} }
export type ReadableAccountType = { export type ReadableAccountType = {
@ -263,25 +356,3 @@ export const getReadableAccounts = (withoutActive: boolean = false): ReadableAcc
}) || [] }) || []
).filter(a => a.acct.length) ).filter(a => a.acct.length)
} }
export const removeAccount = async (account: string) => {
const currAccounts: NonNullable<StorageGlobal['accounts']> = JSON.parse(
storage.global.getString('accounts') || '[]'
)
const nextAccounts: NonNullable<StorageGlobal['accounts']> = currAccounts.filter(
a => a !== account
)
storage.global.set('accounts', JSON.stringify(nextAccounts))
if (nextAccounts.length) {
await setAccount(nextAccounts[nextAccounts.length - 1])
} else {
storage.account = undefined
setGlobalStorage('account.active', undefined)
queryClient.clear()
}
const temp = new MMKV({ id: account })
temp.clearAll()
}

View File

@ -9549,16 +9549,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"react-native-animated-spinkit@npm:^1.5.2":
version: 1.5.2
resolution: "react-native-animated-spinkit@npm:1.5.2"
peerDependencies:
react: "*"
react-native: "*"
checksum: 5d84b0958b3f9db5223d7c2af242eae45f133cf50715be56f1e266d7c04fe5ecdbd7e01b08146748b06daaf914fe8bc64f13df7faaee98bc687094ca10cae61e
languageName: node
linkType: hard
"react-native-blurhash@npm:^1.1.10": "react-native-blurhash@npm:^1.1.10":
version: 1.1.10 version: 1.1.10
resolution: "react-native-blurhash@npm:1.1.10" resolution: "react-native-blurhash@npm:1.1.10"
@ -9667,7 +9657,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"react-native-ios-context-menu@npm:^1.15.1": "react-native-ios-context-menu@npm:1.15.1":
version: 1.15.1 version: 1.15.1
resolution: "react-native-ios-context-menu@npm:1.15.1" resolution: "react-native-ios-context-menu@npm:1.15.1"
dependencies: dependencies:
@ -9679,6 +9669,18 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"react-native-ios-context-menu@patch:react-native-ios-context-menu@npm%3A1.15.1#./.yarn/patches/react-native-ios-context-menu-npm-1.15.1-0034bfa5ba.patch::locator=tooot%40workspace%3A.":
version: 1.15.1
resolution: "react-native-ios-context-menu@patch:react-native-ios-context-menu@npm%3A1.15.1#./.yarn/patches/react-native-ios-context-menu-npm-1.15.1-0034bfa5ba.patch::version=1.15.1&hash=e3f0c8&locator=tooot%40workspace%3A."
dependencies:
"@dominicstop/ts-event-emitter": ^1.1.0
peerDependencies:
react: "*"
react-native: "*"
checksum: ad6bcca2cb3816bc6c52922540cb83e02a5a5217347293e8e4d710742c533afbd244cfd8d3fbe7cf453ba0715435987168bfc1326efec841422ea70a1c9f4a6a
languageName: node
linkType: hard
"react-native-iphone-screen-helper@npm:^2.0.2": "react-native-iphone-screen-helper@npm:^2.0.2":
version: 2.0.2 version: 2.0.2
resolution: "react-native-iphone-screen-helper@npm:2.0.2" resolution: "react-native-iphone-screen-helper@npm:2.0.2"
@ -11412,7 +11414,6 @@ __metadata:
react-i18next: ^12.1.4 react-i18next: ^12.1.4
react-intl: ^6.2.5 react-intl: ^6.2.5
react-native: ^0.70.6 react-native: ^0.70.6
react-native-animated-spinkit: ^1.5.2
react-native-blurhash: ^1.1.10 react-native-blurhash: ^1.1.10
react-native-clean-project: ^4.0.1 react-native-clean-project: ^4.0.1
react-native-fast-image: ^8.6.3 react-native-fast-image: ^8.6.3