From e5e74410d0d838d2cc0700ced70c565110946b3a Mon Sep 17 00:00:00 2001 From: xmflsct Date: Sat, 14 Jan 2023 15:21:31 +0100 Subject: [PATCH 01/23] Fix #659 --- package.json | 2 +- src/utils/queryHooks/timeline.ts | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 418088f3..4be79694 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tooot", - "version": "4.8.2", + "version": "4.8.3", "description": "tooot for Mastodon", "author": "xmflsct ", "license": "GPL-3.0-or-later", diff --git a/src/utils/queryHooks/timeline.ts b/src/utils/queryHooks/timeline.ts index 315cadf0..ed9d7bf2 100644 --- a/src/utils/queryHooks/timeline.ts +++ b/src/utils/queryHooks/timeline.ts @@ -11,7 +11,7 @@ import apiInstance from '@utils/api/instance' import { featureCheck } from '@utils/helpers/featureCheck' import { useNavState } from '@utils/navigation/navigators' import { queryClient } from '@utils/queryHooks' -import { getAccountStorage } from '@utils/storage/actions' +import { getAccountStorage, setAccountStorage } from '@utils/storage/actions' import { AxiosError } from 'axios' import { uniqBy } from 'lodash' import { searchLocalStatus } from './search' @@ -85,6 +85,11 @@ export const queryFunctionTimeline = async ({ url: 'timelines/home', params }).then(res => { + if (marker && !res.body.length) { + setAccountStorage([{ key: 'read_marker_following', value: undefined }]) + return Promise.reject() + } + if (!page.showBoosts || !page.showReplies) { return { ...res, From 26d2c7851727af16ed645e86930239a5e6578b4e Mon Sep 17 00:00:00 2001 From: xmflsct Date: Sat, 14 Jan 2023 15:26:11 +0100 Subject: [PATCH 02/23] Fix #653 --- src/components/Timeline/Shared/Card.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/Timeline/Shared/Card.tsx b/src/components/Timeline/Shared/Card.tsx index 4579a88a..0ce1df8f 100644 --- a/src/components/Timeline/Shared/Card.tsx +++ b/src/components/Timeline/Shared/Card.tsx @@ -64,6 +64,9 @@ const TimelineCard: React.FC = () => { if (loading) { return null } + if (status.media_attachments.length) { + return null + } if ((!status.card?.image || !status.card.title) && !status.card?.description) { return null } From 57f1ed62b50c32f0f4c80d4de027ff16a2519d66 Mon Sep 17 00:00:00 2001 From: xmflsct Date: Sat, 14 Jan 2023 16:20:37 +0100 Subject: [PATCH 03/23] Fix #657 No need for Android, as if permission was denied last time, the next time when requesting, permission would be checked again. --- src/screens/ImageViewer/save.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/screens/ImageViewer/save.ts b/src/screens/ImageViewer/save.ts index 4fbeffef..8936ebf8 100644 --- a/src/screens/ImageViewer/save.ts +++ b/src/screens/ImageViewer/save.ts @@ -4,7 +4,7 @@ import { CameraRoll } from '@react-native-camera-roll/camera-roll' import { RootStackParamList } from '@utils/navigation/navigators' import * as FileSystem from 'expo-file-system' import i18next from 'i18next' -import { PermissionsAndroid, Platform } from 'react-native' +import { Linking, PermissionsAndroid, Platform } from 'react-native' type CommonProps = { image: RootStackParamList['Screen-ImagesViewer']['imageUrls'][0] @@ -19,7 +19,11 @@ const saveIos = async ({ image }: CommonProps) => { 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) { CameraRoll.save(image.remote_url) .then(() => { From e7ca5ba63dc58cef0df3e0d4d8e20d81f0df116a Mon Sep 17 00:00:00 2001 From: xmflsct Date: Sat, 14 Jan 2023 16:29:28 +0100 Subject: [PATCH 04/23] Fix #655 --- src/App.tsx | 2 -- src/i18n/index.ts | 9 +++++++++ src/utils/startup/timezone.ts | 14 -------------- 3 files changed, 9 insertions(+), 16 deletions(-) delete mode 100644 src/utils/startup/timezone.ts diff --git a/src/App.tsx b/src/App.tsx index cf102047..427e18ad 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -10,7 +10,6 @@ import log from '@utils/startup/log' import netInfo from '@utils/startup/netInfo' import push from '@utils/startup/push' import sentry from '@utils/startup/sentry' -import timezone from '@utils/startup/timezone' import { storage } from '@utils/storage' import { getGlobalStorage, @@ -39,7 +38,6 @@ dev() sentry() audio() push() -timezone() enableFreeze(true) log('log', 'App', 'delay splash') diff --git a/src/i18n/index.ts b/src/i18n/index.ts index 32083f91..3499410d 100644 --- a/src/i18n/index.ts +++ b/src/i18n/index.ts @@ -1,3 +1,4 @@ +import * as Localization from 'expo-localization' import i18n from '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 diff --git a/src/utils/startup/timezone.ts b/src/utils/startup/timezone.ts deleted file mode 100644 index b1f0115f..00000000 --- a/src/utils/startup/timezone.ts +++ /dev/null @@ -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 From e5744aba061ef0ce7e9403875af10e78e52d7a88 Mon Sep 17 00:00:00 2001 From: xmflsct Date: Sat, 14 Jan 2023 16:58:01 +0100 Subject: [PATCH 05/23] Try to fix #648 Also reported by other users --- src/components/openLink.ts | 13 ------------- src/screens/Compose/Root/Suggestions.tsx | 1 - src/utils/api/instance.ts | 2 +- src/utils/queryHooks/search.ts | 21 ++++++++++++++++----- 4 files changed, 17 insertions(+), 20 deletions(-) diff --git a/src/components/openLink.ts b/src/components/openLink.ts index e096e313..3cda22bf 100644 --- a/src/components/openLink.ts +++ b/src/components/openLink.ts @@ -9,13 +9,7 @@ import { getGlobalStorage } from '@utils/storage/actions' import * as Linking from 'expo-linking' import * as WebBrowser from 'expo-web-browser' -export let loadingLink = false - const openLink = async (url: string, navigation?: any) => { - if (loadingLink) { - return - } - const handleNavigation = (page: 'Tab-Shared-Toot' | 'Tab-Shared-Account', options: any) => { if (navigation) { navigation.push(page, options) @@ -28,7 +22,6 @@ const openLink = async (url: string, navigation?: any) => { const match = urlMatcher(url) // If a tooot can be found if (match?.status?.id) { - loadingLink = true let response: Mastodon.Status | undefined = undefined const queryKey: QueryKeyStatus = [ @@ -39,7 +32,6 @@ const openLink = async (url: string, navigation?: any) => { if (cache) { handleNavigation('Tab-Shared-Toot', { toot: cache }) - loadingLink = false return } else { try { @@ -47,7 +39,6 @@ const openLink = async (url: string, navigation?: any) => { } catch {} if (response) { handleNavigation('Tab-Shared-Toot', { toot: response }) - loadingLink = false return } } @@ -60,7 +51,6 @@ const openLink = async (url: string, navigation?: any) => { return } - loadingLink = true let response: Mastodon.Account | undefined = undefined const queryKey: QueryKeyAccount = [ @@ -71,7 +61,6 @@ const openLink = async (url: string, navigation?: any) => { if (cache) { handleNavigation('Tab-Shared-Account', { account: cache }) - loadingLink = false return } else { try { @@ -79,13 +68,11 @@ const openLink = async (url: string, navigation?: any) => { } catch {} if (response) { handleNavigation('Tab-Shared-Account', { account: response }) - loadingLink = false return } } } - loadingLink = false switch (getGlobalStorage.string('app.browser')) { // Some links might end with an empty space at the end that triggers an error case 'internal': diff --git a/src/screens/Compose/Root/Suggestions.tsx b/src/screens/Compose/Root/Suggestions.tsx index b1ae64e9..89439541 100644 --- a/src/screens/Compose/Root/Suggestions.tsx +++ b/src/screens/Compose/Root/Suggestions.tsx @@ -38,7 +38,6 @@ const ComposeRootSuggestions: React.FC = () => { const { isFetching, data, refetch } = useSearchQuery({ type: mapSchemaToType(), term: composeState.tag?.raw.substring(1), - limit: 10, options: { enabled: false } }) useEffect(() => { diff --git a/src/utils/api/instance.ts b/src/utils/api/instance.ts index 9c71dadd..64f5b318 100644 --- a/src/utils/api/instance.ts +++ b/src/utils/api/instance.ts @@ -13,7 +13,7 @@ export type Params = { } headers?: { [key: string]: string } body?: FormData - extras?: Omit + extras?: Omit } const apiInstance = async ({ diff --git a/src/utils/queryHooks/search.ts b/src/utils/queryHooks/search.ts index 0f75cdc9..aafceff8 100644 --- a/src/utils/queryHooks/search.ts +++ b/src/utils/queryHooks/search.ts @@ -18,8 +18,8 @@ export type SearchResult = { statuses: Mastodon.Status[] } -const queryFunction = async ({ queryKey }: QueryFunctionContext) => { - const { type, term, limit = 20 } = queryKey[1] +const queryFunction = async ({ queryKey, meta }: QueryFunctionContext) => { + const { type, term, limit = 10 } = queryKey[1] if (!term?.length) { return Promise.reject('Empty search term') } @@ -32,7 +32,8 @@ const queryFunction = async ({ queryKey }: QueryFunctionContext) ...(type && { type }), limit, resolve: true - } + }, + ...(meta && { extras: meta }) }) return res.body } @@ -50,7 +51,12 @@ const useSearchQuery = ({ export const searchLocalStatus = async (uri: Mastodon.Status['uri']): Promise => { const queryKey: QueryKeySearch = ['Search', { type: 'statuses', term: uri, limit: 1 }] return await queryClient - .fetchQuery(queryKey, queryFunction, { staleTime: 3600, cacheTime: 3600 }) + .fetchQuery(queryKey, queryFunction, { + staleTime: 3600, + cacheTime: 3600, + retry: false, + meta: { timeout: 1000 } + }) .then(res => res.statuses[0]?.uri === uri || res.statuses[0]?.url === uri ? res.statuses[0] @@ -63,7 +69,12 @@ export const searchLocalAccount = async ( ): Promise => { const queryKey: QueryKeySearch = ['Search', { type: 'accounts', term: url, limit: 1 }] return await queryClient - .fetchQuery(queryKey, queryFunction, { staleTime: 3600, cacheTime: 3600 }) + .fetchQuery(queryKey, queryFunction, { + staleTime: 3600, + cacheTime: 3600, + retry: false, + meta: { timeout: 1000 } + }) .then(res => (res.accounts[0].url === url ? res.accounts[0] : Promise.reject())) } From 0efb7e5b70052deab35f786054ab0fae30e002f0 Mon Sep 17 00:00:00 2001 From: xmflsct Date: Sun, 15 Jan 2023 13:40:12 +0100 Subject: [PATCH 06/23] Fix #615 --- src/utils/push/updateExpoToken.ts | 11 ++++-- src/utils/push/useConnect.ts | 63 +++++++++++++++++++++---------- 2 files changed, 51 insertions(+), 23 deletions(-) diff --git a/src/utils/push/updateExpoToken.ts b/src/utils/push/updateExpoToken.ts index 543e8c6e..fe2cbc67 100644 --- a/src/utils/push/updateExpoToken.ts +++ b/src/utils/push/updateExpoToken.ts @@ -4,7 +4,7 @@ import { getGlobalStorage, setGlobalStorage } from '@utils/storage/actions' import * as Notifications from 'expo-notifications' import { Platform } from 'react-native' -export const updateExpoToken = async () => { +export const updateExpoToken = async (): Promise => { const expoToken = getGlobalStorage.string('app.expo_token') if (Platform.OS === 'android') { @@ -12,16 +12,19 @@ export const updateExpoToken = async () => { } if (expoToken?.length) { - return Promise.resolve() + return Promise.resolve(expoToken) } else { if (isDevelopment) { setGlobalStorage('app.expo_token', 'ExponentPushToken[DEVELOPMENT_1]') - return Promise.resolve() + return Promise.resolve('ExponentPushToken[DEVELOPMENT_1]') } return await Notifications.getExpoPushTokenAsync({ experienceId: '@xmflsct/tooot', applicationId: 'com.xmflsct.app.tooot' - }).then(({ data }) => setGlobalStorage('app.expo_token', data)) + }).then(({ data }) => { + setGlobalStorage('app.expo_token', data) + return data + }) } } diff --git a/src/utils/push/useConnect.ts b/src/utils/push/useConnect.ts index d583c165..e77aa0d4 100644 --- a/src/utils/push/useConnect.ts +++ b/src/utils/push/useConnect.ts @@ -5,6 +5,7 @@ import apiGeneral from '@utils/api/general' import apiTooot from '@utils/api/tooot' import navigationRef from '@utils/navigation/navigationRef' import { + generateAccountKey, getAccountDetails, getGlobalStorage, setAccountStorage, @@ -12,7 +13,7 @@ import { } from '@utils/storage/actions' import { AxiosError } from 'axios' import * as Notifications from 'expo-notifications' -import { useEffect } from 'react' +import { useEffect, useRef } from 'react' import { useTranslation } from 'react-i18next' import { AppState } from 'react-native' import { updateExpoToken } from './updateExpoToken' @@ -20,22 +21,32 @@ import { updateExpoToken } from './updateExpoToken' const pushUseConnect = () => { const { t } = useTranslation('screens') - useEffect(() => { - updateExpoToken() - }, []) + const startupChecked = useRef(false) const [expoToken] = useGlobalStorage.string('app.expo_token') - const pushEnabledCount = getGlobalStorage.object('accounts')?.filter(account => { - return getAccountDetails(['push'], account)?.push?.global - }).length + const accounts = getGlobalStorage.object('accounts') + const pushEnabled = accounts + ?.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( + const connectQuery = useQuery<{ accounts: string[] } | undefined, AxiosError>( ['tooot', { endpoint: 'push/connect' }], () => - apiTooot({ + apiTooot<{ accounts: string[] } | undefined>({ method: 'get', url: `push/connect/${expoToken}` - }), + }).then(res => res.body), { enabled: false, retry: (failureCount, error) => { @@ -48,6 +59,17 @@ const pushUseConnect = () => { refetchOnWindowFocus: false, refetchOnReconnect: false, 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 => { if (error?.status == 404) { displayMessage({ @@ -96,18 +118,21 @@ const pushUseConnect = () => { ) useEffect(() => { - Sentry.setTags({ expoToken, pushEnabledCount }) + updateExpoToken().then(async token => { + const badgeCount = await Notifications.getBadgeCountAsync() + if (token && (pushEnabled?.length || badgeCount)) { + connectQuery.refetch() + } + }) + }, []) - if (expoToken && pushEnabledCount) { - connectQuery.refetch() - } + useEffect(() => { + Sentry.setTags({ expoToken, pushEnabledCount: pushEnabled?.length }) const appStateListener = AppState.addEventListener('change', state => { - if (expoToken && pushEnabledCount && state === 'active') { + if (expoToken && pushEnabled?.length && state === 'active') { Notifications.getBadgeCountAsync().then(count => { - if (count > 0) { - connectQuery.refetch() - } + if (count > 0) connectQuery.refetch() }) } }) @@ -115,7 +140,7 @@ const pushUseConnect = () => { return () => { appStateListener.remove() } - }, [expoToken, pushEnabledCount]) + }, [expoToken, pushEnabled?.length]) } export default pushUseConnect From 8814161e0e71aaa569b3692e2483fada9721b86a Mon Sep 17 00:00:00 2001 From: xmflsct Date: Sun, 15 Jan 2023 18:00:58 +0100 Subject: [PATCH 07/23] Improve toot page loading --- src/components/openLink.ts | 4 +- src/screens/Tabs/Shared/Toot.tsx | 251 +++++++++++++++++-------------- src/utils/queryHooks/search.ts | 12 +- 3 files changed, 150 insertions(+), 117 deletions(-) diff --git a/src/components/openLink.ts b/src/components/openLink.ts index 3cda22bf..fbe4eabe 100644 --- a/src/components/openLink.ts +++ b/src/components/openLink.ts @@ -35,7 +35,7 @@ const openLink = async (url: string, navigation?: any) => { return } else { try { - response = await searchLocalStatus(url) + response = await searchLocalStatus(url, true) } catch {} if (response) { handleNavigation('Tab-Shared-Toot', { toot: response }) @@ -64,7 +64,7 @@ const openLink = async (url: string, navigation?: any) => { return } else { try { - response = await searchLocalAccount(url) + response = await searchLocalAccount(url, true) } catch {} if (response) { handleNavigation('Tab-Shared-Account', { account: response }) diff --git a/src/screens/Tabs/Shared/Toot.tsx b/src/screens/Tabs/Shared/Toot.tsx index a48e9202..7c791e72 100644 --- a/src/screens/Tabs/Shared/Toot.tsx +++ b/src/screens/Tabs/Shared/Toot.tsx @@ -15,7 +15,7 @@ import { StyleConstants } from '@utils/styles/constants' import { useTheme } from '@utils/styles/ThemeManager' import React, { useEffect, useRef, useState } from 'react' 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' @@ -34,6 +34,8 @@ const TabSharedToot: React.FC> = ({ remote: ['Timeline', { page: 'Toot', toot: toot.id, remote: true }] } + const flRef = useRef>(null) + useEffect(() => { navigation.setOptions({ headerTitle: () => ( @@ -69,12 +71,12 @@ const TabSharedToot: React.FC> = ({ navigation.setParams({ toot, queryKey: queryKey.local }) }, [hasRemoteContent]) - const flRef = useRef(null) - const scrolled = useRef(false) + const PREV_PER_BATCH = 1 + const ancestorsCache = useRef<(Mastodon.Status & { _level?: number })[]>() + const loaded = useRef(false) const match = urlMatcher(toot.url || toot.uri) - const highlightIndex = useRef(0) - const query = useQuery<{ pages: { body: Mastodon.Status[] }[] }>( + const query = useQuery<{ pages: { body: (Mastodon.Status & { _level?: number })[] }[] }>( queryKey.local, async () => { const context = await apiInstance<{ @@ -85,15 +87,14 @@ const TabSharedToot: React.FC> = ({ url: `statuses/${toot.id}/context` }).then(res => res.body) - highlightIndex.current = context.ancestors.length - - const statuses = [...context.ancestors, { ...toot }, ...context.descendants] + ancestorsCache.current = [...context.ancestors] + const statuses = [{ ...toot }, ...context.descendants] return { pages: [ { body: statuses.map((status, index) => { - if (index < highlightIndex.current || status.id === toot.id) { + if (index === 0) { status._level = 0 return status } else { @@ -117,26 +118,6 @@ const TabSharedToot: React.FC> = ({ navigation.goBack() return } - - if (!scrolled.current) { - scrolled.current = true - 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 - } - } } } ) @@ -165,19 +146,45 @@ const TabSharedToot: React.FC> = ({ return Promise.resolve([{ ...toot }]) } - highlightIndex.current = context.ancestors.length - - const statuses = [...context.ancestors, { ...toot }, ...context.descendants] + ancestorsCache.current = context.ancestors.map(ancestor => { + const localMatch = ancestorsCache.current?.find(local => local.uri === ancestor.uri) + if (localMatch) { + return { ...localMatch, _level: 0 } + } 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) => { - if (index < highlightIndex.current || status.id === toot.id) { + if (index === 0) { status._level = 0 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 }) }, { @@ -187,7 +194,7 @@ const TabSharedToot: React.FC> = ({ match?.domain !== getAccountStorage.string('auth.domain'), staleTime: 0, refetchOnMount: true, - onSuccess: data => { + onSuccess: async data => { if ((query.data?.pages[0].body.length || 0) < 1 && data.length < 1) { navigation.goBack() return @@ -195,59 +202,85 @@ const TabSharedToot: React.FC> = ({ if ((query.data?.pages[0].body.length || 0) < data.length) { queryClient.cancelQueries(queryKey.local) - queryClient.setQueryData<{ - pages: { body: Mastodon.Status[] }[] - }>(queryKey.local, old => { - setHasRemoteContent(true) - return { - pages: [ - { - body: data.map(remote => { - const localMatch = old?.pages[0].body.find(local => local.uri === remote.uri) - if (localMatch) { - return { ...localMatch, _level: remote._level } - } else { - return { - ...remote, - _remote: true, - account: { ...remote.account, _remote: true }, - mentions: remote.mentions.map(mention => ({ ...mention, _remote: true })), - ...(remote.reblog && { - reblog: { - ...remote.reblog, - _remote: true, - account: { ...remote.reblog.account, _remote: true }, - mentions: remote.reblog.mentions.map(mention => ({ - ...mention, - _remote: true - })) - } - }) + queryClient.setQueryData<{ pages: { body: Mastodon.Status[] }[] }>( + queryKey.local, + old => { + setHasRemoteContent(true) + return { + pages: [ + { + body: data.map(remote => { + const localMatch = old?.pages[0].body.find(local => local.uri === remote.uri) + if (localMatch) { + return { ...localMatch, _level: remote._level } + } else { + return { + ...remote, + _remote: true, + account: { ...remote.account, _remote: true }, + mentions: remote.mentions.map(mention => ({ ...mention, _remote: true })), + ...(remote.reblog && { + reblog: { + ...remote.reblog, + _remote: true, + account: { ...remote.reblog.account, _remote: true }, + mentions: remote.reblog.mentions.map(mention => ({ + ...mention, + _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 + 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 + } } } } @@ -265,17 +298,12 @@ const TabSharedToot: React.FC> = ({ data={query.data?.pages?.[0].body} renderItem={({ item, index }) => { 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 return ( highlightIndex.current - ? Math.min(item._level - 1, MAX_LEVEL) * StyleConstants.Spacing.S - : undefined - }} + style={{ paddingLeft: Math.min(curr - 1, MAX_LEVEL) * StyleConstants.Spacing.S }} onLayout={({ nativeEvent: { layout: { height } @@ -397,7 +425,7 @@ const TabSharedToot: React.FC> = ({ ? 0 : StyleConstants.Avatar.XS + 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 } /> @@ -416,21 +444,6 @@ const TabSharedToot: React.FC> = ({ ) }} - onScrollToIndexFailed={error => { - const offset = error.averageItemLength * error.index - flRef.current?.scrollToOffset({ offset }) - try { - error.index < (query.data?.pages[0].body.length || 0) && - setTimeout( - () => - flRef.current?.scrollToIndex({ - index: error.index, - viewOffset: 100 - }), - 500 - ) - } catch {} - }} ListFooterComponent={ > = ({ marginHorizontal: StyleConstants.Spacing.M }} > - {query.isFetching ? ( + {query.isLoading ? ( ) : null} } + {...(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( + () => + flRef.current?.scrollToIndex({ + index: error.index, + viewOffset: 50 + }), + 50 + ) + } + })} /> ) } diff --git a/src/utils/queryHooks/search.ts b/src/utils/queryHooks/search.ts index aafceff8..95be0875 100644 --- a/src/utils/queryHooks/search.ts +++ b/src/utils/queryHooks/search.ts @@ -48,14 +48,17 @@ const useSearchQuery = ({ return useQuery(queryKey, queryFunction, { ...options, staleTime: 3600, cacheTime: 3600 }) } -export const searchLocalStatus = async (uri: Mastodon.Status['uri']): Promise => { +export const searchLocalStatus = async ( + uri: Mastodon.Status['uri'], + timeout: boolean = false +): Promise => { const queryKey: QueryKeySearch = ['Search', { type: 'statuses', term: uri, limit: 1 }] return await queryClient .fetchQuery(queryKey, queryFunction, { staleTime: 3600, cacheTime: 3600, retry: false, - meta: { timeout: 1000 } + ...(timeout && { meta: { timeout: 1000 } }) }) .then(res => res.statuses[0]?.uri === uri || res.statuses[0]?.url === uri @@ -65,7 +68,8 @@ export const searchLocalStatus = async (uri: Mastodon.Status['uri']): Promise => { const queryKey: QueryKeySearch = ['Search', { type: 'accounts', term: url, limit: 1 }] return await queryClient @@ -73,7 +77,7 @@ export const searchLocalAccount = async ( staleTime: 3600, cacheTime: 3600, retry: false, - meta: { timeout: 1000 } + ...(timeout && { meta: { timeout: 1000 } }) }) .then(res => (res.accounts[0].url === url ? res.accounts[0] : Promise.reject())) } From 9a289489fa720ad4183be5253df721fa63c57633 Mon Sep 17 00:00:00 2001 From: xmflsct Date: Sun, 15 Jan 2023 19:38:45 +0100 Subject: [PATCH 08/23] Fix #661 The API does not support a better way to achieve this as suggested. Though the search API accepts a "following" param, and it will return data prioritising following accounts. --- src/screens/Compose/Root/Suggestions.tsx | 1 + src/screens/Tabs/Shared/Toot.tsx | 2 +- src/utils/queryHooks/search.ts | 6 ++++-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/screens/Compose/Root/Suggestions.tsx b/src/screens/Compose/Root/Suggestions.tsx index 89439541..b5c8b267 100644 --- a/src/screens/Compose/Root/Suggestions.tsx +++ b/src/screens/Compose/Root/Suggestions.tsx @@ -38,6 +38,7 @@ const ComposeRootSuggestions: React.FC = () => { const { isFetching, data, refetch } = useSearchQuery({ type: mapSchemaToType(), term: composeState.tag?.raw.substring(1), + ...(mapSchemaToType() === 'accounts' && { following: true }), options: { enabled: false } }) useEffect(() => { diff --git a/src/screens/Tabs/Shared/Toot.tsx b/src/screens/Tabs/Shared/Toot.tsx index 7c791e72..2379b634 100644 --- a/src/screens/Tabs/Shared/Toot.tsx +++ b/src/screens/Tabs/Shared/Toot.tsx @@ -275,7 +275,7 @@ const TabSharedToot: React.FC> = ({ setTimeout(() => { flRef.current?.scrollToIndex({ - index: (ancestorsCache.current?.length || 0), + index: ancestorsCache.current?.length || 0, viewOffset: 50 }) }, 50) diff --git a/src/utils/queryHooks/search.ts b/src/utils/queryHooks/search.ts index 95be0875..4f0ca471 100644 --- a/src/utils/queryHooks/search.ts +++ b/src/utils/queryHooks/search.ts @@ -9,6 +9,7 @@ export type QueryKeySearch = [ type?: 'accounts' | 'hashtags' | 'statuses' term?: string limit?: number + following?: boolean } ] @@ -19,7 +20,7 @@ export type SearchResult = { } const queryFunction = async ({ queryKey, meta }: QueryFunctionContext) => { - const { type, term, limit = 10 } = queryKey[1] + const { type, term, limit = 10, following = false } = queryKey[1] if (!term?.length) { return Promise.reject('Empty search term') } @@ -31,7 +32,8 @@ const queryFunction = async ({ queryKey, meta }: QueryFunctionContext Date: Sun, 15 Jan 2023 19:59:48 +0100 Subject: [PATCH 09/23] Fixed Android's svg spans full width --- src/components/Timeline/Default.tsx | 6 ++++-- src/screens/Tabs/Shared/Toot.tsx | 13 +++++++------ 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/components/Timeline/Default.tsx b/src/components/Timeline/Default.tsx index ccb284af..33e396da 100644 --- a/src/components/Timeline/Default.tsx +++ b/src/components/Timeline/Default.tsx @@ -36,6 +36,7 @@ export interface Props { disableDetails?: boolean disableOnPress?: boolean isConversation?: boolean + noBackground?: boolean } // When the poll is long @@ -45,7 +46,8 @@ const TimelineDefault: React.FC = ({ highlighted = false, disableDetails = false, disableOnPress = false, - isConversation = false + isConversation = false, + noBackground = false }) => { const status = item.reblog ? item.reblog : item const rawContent = useRef([]) @@ -77,7 +79,7 @@ const TimelineDefault: React.FC = ({ padding: disableDetails ? StyleConstants.Spacing.Global.PagePadding / 1.5 : StyleConstants.Spacing.Global.PagePadding, - backgroundColor: colors.backgroundDefault, + backgroundColor: noBackground ? undefined : colors.backgroundDefault, paddingBottom: disableDetails ? StyleConstants.Spacing.Global.PagePadding / 1.5 : 0 } const main = () => ( diff --git a/src/screens/Tabs/Shared/Toot.tsx b/src/screens/Tabs/Shared/Toot.tsx index 2379b634..3e7a920e 100644 --- a/src/screens/Tabs/Shared/Toot.tsx +++ b/src/screens/Tabs/Shared/Toot.tsx @@ -310,12 +310,6 @@ const TabSharedToot: React.FC> = ({ } }) => setHeights({ ...heights, [index]: height })} > - {curr > 1 || next > 1 ? [...new Array(curr)].map((_, i) => { if (i > MAX_LEVEL) return null @@ -399,6 +393,13 @@ const TabSharedToot: React.FC> = ({ } }) : null} + {/* Date: Sun, 15 Jan 2023 20:34:22 +0100 Subject: [PATCH 10/23] Use native loading spinner The lib is not being maintained and the animation quality is bad --- package.json | 1 - src/components/Button.tsx | 4 +-- src/components/Header/Right.tsx | 4 +-- src/components/Loading.tsx | 12 ++++++++ src/components/Menu/Row.tsx | 4 +-- src/components/Timeline/Empty.tsx | 4 +-- src/components/Timeline/Footer.tsx | 4 +-- src/components/Timeline/Shared/Translate.tsx | 10 ++----- src/i18n/en/screens/tabs.json | 3 +- src/screens/Announcements.tsx | 12 ++------ .../Compose/Root/Footer/Attachments.tsx | 4 +-- src/screens/Compose/Root/Suggestions.tsx | 4 +-- src/screens/Tabs/Shared/Search/Empty.tsx | 28 +++++++++++++------ src/screens/Tabs/Shared/Search/index.tsx | 5 +--- src/screens/Tabs/Shared/Toot.tsx | 21 ++++---------- src/screens/Tabs/Shared/Users.tsx | 18 ++++++------ yarn.lock | 11 -------- 17 files changed, 69 insertions(+), 80 deletions(-) create mode 100644 src/components/Loading.tsx diff --git a/package.json b/package.json index 4be79694..2dfb17f7 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,6 @@ "react-i18next": "^12.1.4", "react-intl": "^6.2.5", "react-native": "^0.70.6", - "react-native-animated-spinkit": "^1.5.2", "react-native-blurhash": "^1.1.10", "react-native-fast-image": "^8.6.3", "react-native-feather": "^1.1.2", diff --git a/src/components/Button.tsx b/src/components/Button.tsx index 7c81f55e..bcf90e21 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -3,7 +3,7 @@ import { StyleConstants } from '@utils/styles/constants' import { useTheme } from '@utils/styles/ThemeManager' import React, { useState } from 'react' import { AccessibilityProps, Pressable, StyleProp, View, ViewStyle } from 'react-native' -import { Flow } from 'react-native-animated-spinkit' +import { Loading } from './Loading' import CustomText from './Text' export interface Props { @@ -53,7 +53,7 @@ const Button: React.FC = ({ const loadingSpinkit = () => loading ? ( - + ) : null diff --git a/src/components/Header/Right.tsx b/src/components/Header/Right.tsx index 2b799d08..e0f460a0 100644 --- a/src/components/Header/Right.tsx +++ b/src/components/Header/Right.tsx @@ -1,10 +1,10 @@ import Icon from '@components/Icon' +import { Loading } from '@components/Loading' import CustomText from '@components/Text' import { StyleConstants } from '@utils/styles/constants' import { useTheme } from '@utils/styles/ThemeManager' import React from 'react' import { AccessibilityProps, Pressable, View } from 'react-native' -import { Flow } from 'react-native-animated-spinkit' export interface Props { accessibilityLabel?: string @@ -43,7 +43,7 @@ const HeaderRight: React.FC = ({ const loadingSpinkit = () => loading ? ( - + ) : null diff --git a/src/components/Loading.tsx b/src/components/Loading.tsx new file mode 100644 index 00000000..9307f35a --- /dev/null +++ b/src/components/Loading.tsx @@ -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 = ({ size = 'small', ...rest }) => { + const { colors } = useTheme() + + return +} diff --git a/src/components/Menu/Row.tsx b/src/components/Menu/Row.tsx index c360071f..2fcc8fbc 100644 --- a/src/components/Menu/Row.tsx +++ b/src/components/Menu/Row.tsx @@ -1,4 +1,5 @@ import Icon from '@components/Icon' +import { Loading } from '@components/Loading' import CustomText from '@components/Text' import { useAccessibility } from '@utils/accessibility/AccessibilityManager' import { StyleConstants } from '@utils/styles/constants' @@ -6,7 +7,6 @@ import { useTheme } from '@utils/styles/ThemeManager' import { ColorDefinitions } from '@utils/styles/themes' import React from 'react' import { View } from 'react-native' -import { Flow } from 'react-native-animated-spinkit' import { State, Switch, TapGestureHandler } from 'react-native-gesture-handler' export interface Props { @@ -150,7 +150,7 @@ const MenuRow: React.FC = ({ ) : null} {loading ? ( - + ) : null} diff --git a/src/components/Timeline/Empty.tsx b/src/components/Timeline/Empty.tsx index e948f343..5fe79703 100644 --- a/src/components/Timeline/Empty.tsx +++ b/src/components/Timeline/Empty.tsx @@ -1,5 +1,6 @@ import Button from '@components/Button' import Icon from '@components/Icon' +import { Loading } from '@components/Loading' import CustomText from '@components/Text' import { QueryKeyTimeline, useTimelineQuery } from '@utils/queryHooks/timeline' import { StyleConstants } from '@utils/styles/constants' @@ -7,7 +8,6 @@ import { useTheme } from '@utils/styles/ThemeManager' import React from 'react' import { useTranslation } from 'react-i18next' import { View } from 'react-native' -import { Circle } from 'react-native-animated-spinkit' export interface Props { queryKey: QueryKeyTimeline @@ -25,7 +25,7 @@ const TimelineEmpty: React.FC = ({ queryKey }) => { const children = () => { switch (status) { case 'loading': - return + return case 'error': return ( <> diff --git a/src/components/Timeline/Footer.tsx b/src/components/Timeline/Footer.tsx index 391c498f..db50cc12 100644 --- a/src/components/Timeline/Footer.tsx +++ b/src/components/Timeline/Footer.tsx @@ -1,4 +1,5 @@ import Icon from '@components/Icon' +import { Loading } from '@components/Loading' import CustomText from '@components/Text' import { QueryKeyTimeline, useTimelineQuery } from '@utils/queryHooks/timeline' import { StyleConstants } from '@utils/styles/constants' @@ -6,7 +7,6 @@ import { useTheme } from '@utils/styles/ThemeManager' import React from 'react' import { Trans } from 'react-i18next' import { View } from 'react-native' -import { Circle } from 'react-native-animated-spinkit' export interface Props { queryKey: QueryKeyTimeline @@ -31,7 +31,7 @@ const TimelineFooter: React.FC = ({ queryKey, disableInfinity }) => { }} > {!disableInfinity && hasNextPage ? ( - + ) : ( { @@ -111,13 +111,7 @@ const TimelineTranslate = () => { }) : t('componentTimeline:shared.translate.default')} - {isFetching ? ( - - ) : null} + {isFetching ? : null} {devView()} {data && data.error === undefined diff --git a/src/i18n/en/screens/tabs.json b/src/i18n/en/screens/tabs.json index d4dd8466..051ab6e5 100644 --- a/src/i18n/en/screens/tabs.json +++ b/src/i18n/en/screens/tabs.json @@ -373,7 +373,8 @@ "hashtags": "Hashtag", "statuses": "Toot" }, - "notFound": "Cannot find {{searchTerm}} related {{type}}" + "notFound": "Cannot find {{searchTerm}} related {{type}}", + "noResult": "Cannot find anything, please try a different term" }, "toot": { "name": "Discussions", diff --git a/src/screens/Announcements.tsx b/src/screens/Announcements.tsx index 681feb41..f52b3019 100644 --- a/src/screens/Announcements.tsx +++ b/src/screens/Announcements.tsx @@ -1,5 +1,6 @@ import Button from '@components/Button' import haptics from '@components/haptics' +import { Loading } from '@components/Loading' import { ParseHTML } from '@components/Parse' import RelativeTime from '@components/RelativeTime' import CustomText from '@components/Text' @@ -20,7 +21,6 @@ import { StyleSheet, View } from 'react-native' -import { Circle } from 'react-native-animated-spinkit' import FastImage from 'react-native-fast-image' import { FlatList, ScrollView } from 'react-native-gesture-handler' import { SafeAreaView } from 'react-native-safe-area-context' @@ -191,14 +191,8 @@ const ScreenAnnouncements: React.FC const ListEmptyComponent = () => { return ( - - + + ) } diff --git a/src/screens/Compose/Root/Footer/Attachments.tsx b/src/screens/Compose/Root/Footer/Attachments.tsx index 611e1685..008345c6 100644 --- a/src/screens/Compose/Root/Footer/Attachments.tsx +++ b/src/screens/Compose/Root/Footer/Attachments.tsx @@ -1,6 +1,7 @@ import Button from '@components/Button' import haptics from '@components/haptics' import Icon from '@components/Icon' +import { Loading } from '@components/Loading' import { MAX_MEDIA_ATTACHMENTS } from '@components/mediaSelector' import CustomText from '@components/Text' 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 { useTranslation } from 'react-i18next' import { FlatList, Pressable, StyleSheet, View } from 'react-native' -import { Circle } from 'react-native-animated-spinkit' import FastImage from 'react-native-fast-image' import ComposeContext from '../../utils/createContext' import { ExtendedAttachment } from '../../utils/types' @@ -135,7 +135,7 @@ const ComposeAttachments: React.FC = ({ accessibleRefAttachments }) => { backgroundColor: colors.backgroundOverlayInvert }} > - + ) : ( { key='listEmpty' style={{ flex: 1, alignItems: 'center', marginVertical: StyleConstants.Spacing.M }} > - + ) : ( <>{main()} diff --git a/src/screens/Tabs/Shared/Search/Empty.tsx b/src/screens/Tabs/Shared/Search/Empty.tsx index af8ff166..1d5b4b9b 100644 --- a/src/screens/Tabs/Shared/Search/Empty.tsx +++ b/src/screens/Tabs/Shared/Search/Empty.tsx @@ -1,4 +1,5 @@ import ComponentHashtag from '@components/Hashtag' +import { Loading } from '@components/Loading' import ComponentSeparator from '@components/Separator' import CustomText from '@components/Text' import { useTrendsQuery } from '@utils/queryHooks/trends' @@ -6,26 +7,37 @@ import { StyleConstants } from '@utils/styles/constants' import { useTheme } from '@utils/styles/ThemeManager' import React from 'react' import { Trans, useTranslation } from 'react-i18next' -import { StyleSheet, TextInput, View } from 'react-native' -import { Circle } from 'react-native-animated-spinkit' +import { StyleSheet, View } from 'react-native' export interface Props { isFetching: boolean - inputRef: React.RefObject - setSearchTerm: React.Dispatch> + searchTerm: string } -const SearchEmpty: React.FC = ({ isFetching, inputRef, setSearchTerm }) => { +const SearchEmpty: React.FC = ({ isFetching, searchTerm }) => { const { colors } = useTheme() const { t } = useTranslation('screenTabs') const trendsTags = useTrendsQuery({ type: 'tags' }) return ( - + {isFetching ? ( - - + + + + ) : searchTerm.length ? ( + + ) : ( <> diff --git a/src/screens/Tabs/Shared/Search/index.tsx b/src/screens/Tabs/Shared/Search/index.tsx index 89c530d0..d06e239e 100644 --- a/src/screens/Tabs/Shared/Search/index.tsx +++ b/src/screens/Tabs/Shared/Search/index.tsx @@ -131,7 +131,6 @@ const TabSharedSearch: React.FC> style={{ flex: 1 }} > { switch (section.title) { @@ -146,9 +145,7 @@ const TabSharedSearch: React.FC> } }} stickySectionHeadersEnabled - ListEmptyComponent={ - - } + ListEmptyComponent={} keyboardShouldPersistTaps='always' renderSectionHeader={({ section: { translation } }) => ( > = ({ @@ -109,7 +109,7 @@ const TabSharedToot: React.FC> = ({ } }, { - placeholderData: { pages: [{ body: [toot] }] }, + placeholderData: { pages: [{ body: [{ ...toot, _level: 0 }] }] }, enabled: !toot._remote, staleTime: 0, refetchOnMount: true, @@ -121,7 +121,7 @@ const TabSharedToot: React.FC> = ({ } } ) - useQuery( + const remoteQuery = useQuery( queryKey.remote, async () => { const domain = match?.domain @@ -446,18 +446,9 @@ const TabSharedToot: React.FC> = ({ ) }} ListFooterComponent={ - - {query.isLoading ? ( - - ) : null} - + query.isFetching || remoteQuery.isFetching ? ( + } /> + ) : null } {...(loaded.current && { maintainVisibleContentPosition: { minIndexForVisible: 0 } })} {...(Platform.OS !== 'ios' && { diff --git a/src/screens/Tabs/Shared/Users.tsx b/src/screens/Tabs/Shared/Users.tsx index e161b1f8..b2994a6e 100644 --- a/src/screens/Tabs/Shared/Users.tsx +++ b/src/screens/Tabs/Shared/Users.tsx @@ -1,6 +1,7 @@ import ComponentAccount from '@components/Account' import { HeaderLeft } from '@components/Header' import Icon from '@components/Icon' +import { Loading } from '@components/Loading' import ComponentSeparator from '@components/Separator' import CustomText from '@components/Text' import apiInstance from '@utils/api/instance' @@ -13,7 +14,6 @@ import { useTheme } from '@utils/styles/ThemeManager' import React, { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { View } from 'react-native' -import { Circle, Flow } from 'react-native-animated-spinkit' import { FlatList } from 'react-native-gesture-handler' const TabSharedUsers: React.FC> = ({ @@ -36,7 +36,7 @@ const TabSharedUsers: React.FC> = ...queryKey[1] }) - const [isSearching, setIsSearching] = useState(false) + const [isSearching, setIsSearching] = useState(null) return ( > = minHeight: '100%', paddingVertical: StyleConstants.Spacing.Global.PagePadding }} - renderItem={({ item }) => ( + renderItem={({ item, index }) => ( { if (data?.pages[0]?.remoteData) { - setIsSearching(true) + setIsSearching(index) apiInstance({ version: 'v2', method: 'get', @@ -66,18 +66,18 @@ const TabSharedUsers: React.FC> = } }) .then(res => { - setIsSearching(false) + setIsSearching(null) if (res.body.accounts[0]) { navigation.push('Tab-Shared-Account', { account: res.body.accounts[0] }) } }) - .catch(() => setIsSearching(false)) + .catch(() => setIsSearching(null)) } else { navigation.push('Tab-Shared-Account', { account: item }) } } }} - children={} + children={} /> )} onEndReached={() => hasNextPage && !isFetchingNextPage && fetchNextPage()} @@ -93,7 +93,7 @@ const TabSharedUsers: React.FC> = alignItems: 'center' }} > - + ) : null } diff --git a/yarn.lock b/yarn.lock index ef1812a4..224235d5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9549,16 +9549,6 @@ __metadata: languageName: node 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": version: 1.1.10 resolution: "react-native-blurhash@npm:1.1.10" @@ -11412,7 +11402,6 @@ __metadata: react-i18next: ^12.1.4 react-intl: ^6.2.5 react-native: ^0.70.6 - react-native-animated-spinkit: ^1.5.2 react-native-blurhash: ^1.1.10 react-native-clean-project: ^4.0.1 react-native-fast-image: ^8.6.3 From b52b529550041b6e8cd0b9fd411d7e0783a98efe Mon Sep 17 00:00:00 2001 From: xmflsct Date: Sun, 15 Jan 2023 21:51:27 +0100 Subject: [PATCH 11/23] Fix conversation view when remote returns error --- ios/Podfile.lock | 16 ++++++--- src/screens/Tabs/Shared/Toot.tsx | 57 +++++++++++++++++--------------- 2 files changed, 41 insertions(+), 32 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 6c4225a0..5ec02e74 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -68,9 +68,9 @@ PODS: - libwebp/mux (1.2.4): - libwebp/demux - libwebp/webp (1.2.4) - - MMKV (1.2.14): - - MMKVCore (~> 1.2.14) - - MMKVCore (1.2.14) + - MMKV (1.2.15): + - MMKVCore (~> 1.2.15) + - MMKVCore (1.2.15) - RCT-Folly (2021.07.22.00): - boost - DoubleConversion @@ -321,6 +321,8 @@ PODS: - react-native-paste-input (0.5.2): - React-Core - Swime (= 3.0.6) + - react-native-quick-base64 (2.0.5): + - React-Core - react-native-safe-area-context (4.4.1): - RCT-Folly - RCTRequired @@ -508,6 +510,7 @@ DEPENDENCIES: - "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)" - 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-quick-base64 (from `../node_modules/react-native-quick-base64`) - 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-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`) @@ -652,6 +655,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-pager-view" 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: :path: "../node_modules/react-native-safe-area-context" react-native-segmented-control: @@ -732,8 +737,8 @@ SPEC CHECKSUMS: hermes-engine: 2af7b7a59128f250adfd86f15aa1d5a2ecd39995 libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 libwebp: f62cb61d0a484ba548448a4bd52aabf150ff6eef - MMKV: 9c4663aa7ca255d478ff10f2f5cb7d17c1651ccd - MMKVCore: 89f5c8a66bba2dcd551779dea4d412eeec8ff5bb + MMKV: 7f34558bbb5a33b0eaefae2de4b6a20a2ffdad6f + MMKVCore: ddf41b9d9262f058419f9ba7598719af56c02cd3 RCT-Folly: 0080d0a6ebf2577475bda044aa59e2ca1f909cda RCTRequired: e1866f61af7049eb3d8e08e8b133abd38bc1ca7a RCTTypeSafety: 27c2ac1b00609a432ced1ae701247593f07f901e @@ -760,6 +765,7 @@ SPEC CHECKSUMS: react-native-netinfo: 2517ad504b3d303e90d7a431b0fcaef76d207983 react-native-pager-view: 54bed894cecebe28cede54c01038d9d1e122de43 react-native-paste-input: 88709b4fd586ea8cc56ba5e2fc4cdfe90597730c + react-native-quick-base64: e657e9197e61b60a9dec49807843052b830da254 react-native-safe-area-context: 99b24a0c5acd0d5dcac2b1a7f18c49ea317be99a react-native-segmented-control: 65df6cd0619b780b3843d574a72d4c7cec396097 React-perflogger: 8c79399b0500a30ee8152d0f9f11beae7fc36595 diff --git a/src/screens/Tabs/Shared/Toot.tsx b/src/screens/Tabs/Shared/Toot.tsx index da3f7080..cb17dc81 100644 --- a/src/screens/Tabs/Shared/Toot.tsx +++ b/src/screens/Tabs/Shared/Toot.tsx @@ -146,33 +146,35 @@ const TabSharedToot: React.FC> = ({ return Promise.resolve([{ ...toot }]) } - ancestorsCache.current = context.ancestors.map(ancestor => { - const localMatch = ancestorsCache.current?.find(local => local.uri === ancestor.uri) - if (localMatch) { - return { ...localMatch, _level: 0 } - } 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 - })) - } - }) + if ((ancestorsCache.current?.length || 0) < context.ancestors.length) { + ancestorsCache.current = context.ancestors.map(ancestor => { + 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) => { @@ -239,7 +241,8 @@ const TabSharedToot: React.FC> = ({ } ) } - + }, + onSettled: async () => { loaded.current = true if (ancestorsCache.current?.length) { From 34c0bbf4bbc2b0abe927a6a3d2f28097593bc20f Mon Sep 17 00:00:00 2001 From: xmflsct Date: Sun, 15 Jan 2023 22:04:25 +0100 Subject: [PATCH 12/23] Fix too thin images --- .../Timeline/Shared/Attachment/dimensions.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/components/Timeline/Shared/Attachment/dimensions.ts b/src/components/Timeline/Shared/Attachment/dimensions.ts index aa1d3564..ec069b85 100644 --- a/src/components/Timeline/Shared/Attachment/dimensions.ts +++ b/src/components/Timeline/Shared/Attachment/dimensions.ts @@ -9,7 +9,12 @@ export const aspectRatio = ({ width?: number height?: 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 if (total > 5) { @@ -26,12 +31,12 @@ export const aspectRatio = ({ } else { switch (isEven) { case true: - return cropTooTall + return defaultCrop case false: if ((index || -2) + 1 == total) { - return cropTooTall * 2 + return defaultCrop * 2 } else { - return cropTooTall + return defaultCrop } } } From 3883c0307ae681f65bc4d7ff138a978fc6f7b1a1 Mon Sep 17 00:00:00 2001 From: xmflsct Date: Mon, 16 Jan 2023 14:20:36 +0100 Subject: [PATCH 13/23] Fix public page switching timeline not updating nav params --- src/screens/Tabs/Public/Root.tsx | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/screens/Tabs/Public/Root.tsx b/src/screens/Tabs/Public/Root.tsx index c20a4398..b24929c6 100644 --- a/src/screens/Tabs/Public/Root.tsx +++ b/src/screens/Tabs/Public/Root.tsx @@ -1,8 +1,7 @@ import { HeaderRight } from '@components/Header' import Timeline from '@components/Timeline' import SegmentedControl from '@react-native-community/segmented-control' -import { useNavigation } from '@react-navigation/native' -import { NativeStackNavigationProp, NativeStackScreenProps } from '@react-navigation/native-stack' +import { NativeStackScreenProps } from '@react-navigation/native-stack' import { TabPublicStackParamList } from '@utils/navigation/navigators' import { QueryKeyTimeline } from '@utils/queryHooks/timeline' import { getGlobalStorage, setGlobalStorage } from '@utils/storage/actions' @@ -14,12 +13,7 @@ import { Dimensions } from 'react-native' import { SceneMap, TabView } from 'react-native-tab-view' const Route = ({ route: { key: page } }: { route: any }) => { - const navigation = - useNavigation>() const queryKey: QueryKeyTimeline = ['Timeline', { page }] - useEffect(() => { - navigation.setParams({ queryKey }) - }, []) return } @@ -35,12 +29,11 @@ const Root: React.FC( Math.max( 0, - segments.findIndex(segment => segment === previousSegment) + segments.findIndex(segment => segment === getGlobalStorage.string('app.prev_public_segment')) ) ) const [routes] = useState([ @@ -48,6 +41,10 @@ const Root: React.FC { + const page = segments[segment] + page && navigation.setParams({ queryKey: ['Timeline', { page }] }) + }, [segment]) useEffect(() => { navigation.setOptions({ From e97eff25c5af5d35ca2f89d39e011a9d0633b319 Mon Sep 17 00:00:00 2001 From: xmflsct Date: Mon, 16 Jan 2023 14:30:00 +0100 Subject: [PATCH 14/23] Fix cached toot not replaced --- src/screens/Tabs/Shared/Toot.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/screens/Tabs/Shared/Toot.tsx b/src/screens/Tabs/Shared/Toot.tsx index cb17dc81..e40147e0 100644 --- a/src/screens/Tabs/Shared/Toot.tsx +++ b/src/screens/Tabs/Shared/Toot.tsx @@ -34,7 +34,7 @@ const TabSharedToot: React.FC> = ({ remote: ['Timeline', { page: 'Toot', toot: toot.id, remote: true }] } - const flRef = useRef>(null) + const flRef = useRef>(null) useEffect(() => { navigation.setOptions({ @@ -72,11 +72,13 @@ const TabSharedToot: React.FC> = ({ }, [hasRemoteContent]) const PREV_PER_BATCH = 1 - const ancestorsCache = useRef<(Mastodon.Status & { _level?: number })[]>() + const ancestorsCache = useRef<(Mastodon.Status & { _level?: number; key?: string })[]>() const loaded = useRef(false) const match = urlMatcher(toot.url || toot.uri) - const query = useQuery<{ pages: { body: (Mastodon.Status & { _level?: number })[] }[] }>( + const query = useQuery<{ + pages: { body: (Mastodon.Status & { _level?: number; key?: string })[] }[] + }>( queryKey.local, async () => { const context = await apiInstance<{ @@ -109,7 +111,7 @@ const TabSharedToot: React.FC> = ({ } }, { - placeholderData: { pages: [{ body: [{ ...toot, _level: 0 }] }] }, + placeholderData: { pages: [{ body: [{ ...toot, _level: 0, key: `${toot.id}_cache` }] }] }, enabled: !toot._remote, staleTime: 0, refetchOnMount: true, From 261987cac9a81c7e393ddbfdc0a2c72034622f59 Mon Sep 17 00:00:00 2001 From: xmflsct Date: Mon, 16 Jan 2023 14:58:47 +0100 Subject: [PATCH 15/23] New Crowdin updates (#658) * New translations timeline.json (Spanish) * New translations timeline.json (Catalan) * New translations timeline.json (Ukrainian) * New translations tabs.json (German) * New translations tabs.json (Italian) * New translations tabs.json (Japanese) * New translations tabs.json (Korean) * New translations tabs.json (Chinese Simplified) * New translations tabs.json (Chinese Traditional) * New translations tabs.json (Vietnamese) * New translations tabs.json (Portuguese, Brazilian) * New translations tabs.json (French) * New translations tabs.json (Polish) * New translations tabs.json (Spanish) * New translations tabs.json (Swedish) * New translations tabs.json (Czech) * New translations tabs.json (Dutch) * New translations tabs.json (Catalan) * New translations tabs.json (Ukrainian) * New translations tabs.json (Russian) * New translations tabs.json (Greek) * New translations tabs.json (Ukrainian) * New translations tabs.json (Spanish) * New translations tabs.json (Dutch) * New translations tabs.json (Catalan) * New translations tabs.json (German) * New translations tabs.json (Chinese Simplified) --- src/i18n/ca/components/timeline.json | 2 +- src/i18n/ca/screens/tabs.json | 3 ++- src/i18n/cs/screens/tabs.json | 3 ++- src/i18n/de/screens/tabs.json | 3 ++- src/i18n/el/screens/tabs.json | 3 ++- src/i18n/es/components/timeline.json | 2 +- src/i18n/es/screens/tabs.json | 3 ++- src/i18n/fr/screens/tabs.json | 3 ++- src/i18n/it/screens/tabs.json | 3 ++- src/i18n/ja/screens/tabs.json | 3 ++- src/i18n/ko/screens/tabs.json | 3 ++- src/i18n/nl/screens/tabs.json | 3 ++- src/i18n/pl/screens/tabs.json | 3 ++- src/i18n/pt_BR/screens/tabs.json | 3 ++- src/i18n/ru/screens/tabs.json | 3 ++- src/i18n/sv/screens/tabs.json | 3 ++- src/i18n/uk/components/timeline.json | 2 +- src/i18n/uk/screens/tabs.json | 3 ++- src/i18n/vi/screens/tabs.json | 3 ++- src/i18n/zh-Hans/screens/tabs.json | 3 ++- src/i18n/zh-Hant/screens/tabs.json | 3 ++- 21 files changed, 39 insertions(+), 21 deletions(-) diff --git a/src/i18n/ca/components/timeline.json b/src/i18n/ca/components/timeline.json index f9178320..24fb514a 100644 --- a/src/i18n/ca/components/timeline.json +++ b/src/i18n/ca/components/timeline.json @@ -123,7 +123,7 @@ "muted": { "accessibilityLabel": "Publicació silenciada" }, - "replies": "Respostes <0 />", + "replies": "En resposta a <0 />", "visibility": { "direct": { "accessibilityLabel": "La publicació és un missatge directe" diff --git a/src/i18n/ca/screens/tabs.json b/src/i18n/ca/screens/tabs.json index 6975fa0d..06204002 100644 --- a/src/i18n/ca/screens/tabs.json +++ b/src/i18n/ca/screens/tabs.json @@ -373,7 +373,8 @@ "hashtags": "Etiqueta", "statuses": "Publicació" }, - "notFound": "No s'ha trobat {{searchTerm}} relacionat {{type}}" + "notFound": "No s'ha trobat {{searchTerm}} relacionat {{type}}", + "noResult": "No s'ha trobat res, prova-ho amb un altre terme" }, "toot": { "name": "Discussions", diff --git a/src/i18n/cs/screens/tabs.json b/src/i18n/cs/screens/tabs.json index 034969bc..09a175ac 100644 --- a/src/i18n/cs/screens/tabs.json +++ b/src/i18n/cs/screens/tabs.json @@ -373,7 +373,8 @@ "hashtags": "", "statuses": "" }, - "notFound": "" + "notFound": "", + "noResult": "" }, "toot": { "name": "", diff --git a/src/i18n/de/screens/tabs.json b/src/i18n/de/screens/tabs.json index 21a75a49..c83d2a8b 100644 --- a/src/i18n/de/screens/tabs.json +++ b/src/i18n/de/screens/tabs.json @@ -373,7 +373,8 @@ "hashtags": "Hashtag", "statuses": "Tröt" }, - "notFound": "Konnte {{searchTerm}}-bezogene {{type}} nicht finden" + "notFound": "Konnte {{searchTerm}}-bezogene {{type}} nicht finden", + "noResult": "Nichts gefunden, bitte versuchen Sie es mit einem anderen Begriff" }, "toot": { "name": "Diskussionen", diff --git a/src/i18n/el/screens/tabs.json b/src/i18n/el/screens/tabs.json index 5081789e..acc3d375 100644 --- a/src/i18n/el/screens/tabs.json +++ b/src/i18n/el/screens/tabs.json @@ -373,7 +373,8 @@ "hashtags": "Hashtag", "statuses": "Ανάρτηση" }, - "notFound": "Δεν είναι εφικτό να βρεθεί {{searchTerm}} που να έχει σχέση με {{type}}" + "notFound": "Δεν είναι εφικτό να βρεθεί {{searchTerm}} που να έχει σχέση με {{type}}", + "noResult": "" }, "toot": { "name": "Συζητήσεις", diff --git a/src/i18n/es/components/timeline.json b/src/i18n/es/components/timeline.json index 493fb880..bac8c396 100644 --- a/src/i18n/es/components/timeline.json +++ b/src/i18n/es/components/timeline.json @@ -123,7 +123,7 @@ "muted": { "accessibilityLabel": "Toot silenciado" }, - "replies": "Respuestas <0 />", + "replies": "En respuesta a <0 />", "visibility": { "direct": { "accessibilityLabel": "El toot es un mensaje directo" diff --git a/src/i18n/es/screens/tabs.json b/src/i18n/es/screens/tabs.json index 186c5fb5..f1c12d76 100644 --- a/src/i18n/es/screens/tabs.json +++ b/src/i18n/es/screens/tabs.json @@ -373,7 +373,8 @@ "hashtags": "Hashtag", "statuses": "Toot" }, - "notFound": "No se pudo encontrar {{searchTerm}} relacionado con {{type}}" + "notFound": "No se pudo encontrar {{searchTerm}} relacionado con {{type}}", + "noResult": "No se ha podido encontrar nada, por favor, inténtelo con un término diferente" }, "toot": { "name": "Discusiones", diff --git a/src/i18n/fr/screens/tabs.json b/src/i18n/fr/screens/tabs.json index 8d9ae38a..f9eacb10 100644 --- a/src/i18n/fr/screens/tabs.json +++ b/src/i18n/fr/screens/tabs.json @@ -373,7 +373,8 @@ "hashtags": "Hashtag", "statuses": "Pouet" }, - "notFound": "Impossible de trouver {{searchTerm}} lié à {{type}}" + "notFound": "Impossible de trouver {{searchTerm}} lié à {{type}}", + "noResult": "" }, "toot": { "name": "Discussions", diff --git a/src/i18n/it/screens/tabs.json b/src/i18n/it/screens/tabs.json index cd2c93a9..868d44e2 100644 --- a/src/i18n/it/screens/tabs.json +++ b/src/i18n/it/screens/tabs.json @@ -373,7 +373,8 @@ "hashtags": "Hashtag", "statuses": "Toot" }, - "notFound": "Impossibile trovare {{searchTerm}} come {{type}}" + "notFound": "Impossibile trovare {{searchTerm}} come {{type}}", + "noResult": "" }, "toot": { "name": "Discussioni", diff --git a/src/i18n/ja/screens/tabs.json b/src/i18n/ja/screens/tabs.json index e70e57ea..3367ce7d 100644 --- a/src/i18n/ja/screens/tabs.json +++ b/src/i18n/ja/screens/tabs.json @@ -373,7 +373,8 @@ "hashtags": "ハッシュタグ", "statuses": "投稿" }, - "notFound": "{{type}} {{searchTerm}} は見つかりませんでした" + "notFound": "{{type}} {{searchTerm}} は見つかりませんでした", + "noResult": "" }, "toot": { "name": "スレッド", diff --git a/src/i18n/ko/screens/tabs.json b/src/i18n/ko/screens/tabs.json index 0af83c0e..31f4b0e5 100644 --- a/src/i18n/ko/screens/tabs.json +++ b/src/i18n/ko/screens/tabs.json @@ -373,7 +373,8 @@ "hashtags": "해시태그", "statuses": "툿" }, - "notFound": "{{searchTerm}}와 관련된 {{type}}을 찾을 수 없어요" + "notFound": "{{searchTerm}}와 관련된 {{type}}을 찾을 수 없어요", + "noResult": "" }, "toot": { "name": "대화", diff --git a/src/i18n/nl/screens/tabs.json b/src/i18n/nl/screens/tabs.json index 0919ddcd..c4cd88c4 100644 --- a/src/i18n/nl/screens/tabs.json +++ b/src/i18n/nl/screens/tabs.json @@ -373,7 +373,8 @@ "hashtags": "Hashtag", "statuses": "Toot" }, - "notFound": "Kan {{searchTerm}} niet vinden gerelateerd aan {{type}}" + "notFound": "Kan {{searchTerm}} niet vinden gerelateerd aan {{type}}", + "noResult": "Kan niets vinden, probeer een andere term" }, "toot": { "name": "Gesprek", diff --git a/src/i18n/pl/screens/tabs.json b/src/i18n/pl/screens/tabs.json index 827efc5a..8c9f5a78 100644 --- a/src/i18n/pl/screens/tabs.json +++ b/src/i18n/pl/screens/tabs.json @@ -373,7 +373,8 @@ "hashtags": "Hashtag", "statuses": "Toot" }, - "notFound": "Nie można odnaleźć {{searchTerm}} powiązanego {{type}}" + "notFound": "Nie można odnaleźć {{searchTerm}} powiązanego {{type}}", + "noResult": "" }, "toot": { "name": "Dyskusje", diff --git a/src/i18n/pt_BR/screens/tabs.json b/src/i18n/pt_BR/screens/tabs.json index 9e8a5127..8a885df2 100644 --- a/src/i18n/pt_BR/screens/tabs.json +++ b/src/i18n/pt_BR/screens/tabs.json @@ -373,7 +373,8 @@ "hashtags": "Palavras-chave", "statuses": "Toot" }, - "notFound": "Não foi possível encontrar {{searchTerm}} {{type}} relacionado" + "notFound": "Não foi possível encontrar {{searchTerm}} {{type}} relacionado", + "noResult": "" }, "toot": { "name": "Discussões", diff --git a/src/i18n/ru/screens/tabs.json b/src/i18n/ru/screens/tabs.json index eb46c9d2..9e11e10a 100644 --- a/src/i18n/ru/screens/tabs.json +++ b/src/i18n/ru/screens/tabs.json @@ -373,7 +373,8 @@ "hashtags": "", "statuses": "" }, - "notFound": "" + "notFound": "", + "noResult": "" }, "toot": { "name": "", diff --git a/src/i18n/sv/screens/tabs.json b/src/i18n/sv/screens/tabs.json index 78aa9c08..dc9529d9 100644 --- a/src/i18n/sv/screens/tabs.json +++ b/src/i18n/sv/screens/tabs.json @@ -373,7 +373,8 @@ "hashtags": "Hashtagg", "statuses": "Inlägg" }, - "notFound": "Kan inte hitta {{searchTerm}}-relaterade {{type}}" + "notFound": "Kan inte hitta {{searchTerm}}-relaterade {{type}}", + "noResult": "" }, "toot": { "name": "Diskussioner", diff --git a/src/i18n/uk/components/timeline.json b/src/i18n/uk/components/timeline.json index 9dcfee7c..2a8f3ca0 100644 --- a/src/i18n/uk/components/timeline.json +++ b/src/i18n/uk/components/timeline.json @@ -49,7 +49,7 @@ }, "favourited": { "accessibilityLabel": "Додати дмух до улюблених", - "function": "Улюблені дмухи" + "function": "Вподобати дмух" }, "bookmarked": { "accessibilityLabel": "Додати цей дмух до закладок", diff --git a/src/i18n/uk/screens/tabs.json b/src/i18n/uk/screens/tabs.json index aad42845..6f7c1906 100644 --- a/src/i18n/uk/screens/tabs.json +++ b/src/i18n/uk/screens/tabs.json @@ -373,7 +373,8 @@ "hashtags": "Ґештег", "statuses": "Дмух" }, - "notFound": "Не вдалося знайти {{searchTerm}} пов'язаний з {{type}}" + "notFound": "Не вдалося знайти {{searchTerm}} пов'язаний з {{type}}", + "noResult": "Нічого не знайдено, спробуйте інший термін" }, "toot": { "name": "Обговорення", diff --git a/src/i18n/vi/screens/tabs.json b/src/i18n/vi/screens/tabs.json index e7934e50..c2e0c9da 100644 --- a/src/i18n/vi/screens/tabs.json +++ b/src/i18n/vi/screens/tabs.json @@ -373,7 +373,8 @@ "hashtags": "Hashtag", "statuses": "Tút" }, - "notFound": "Không tìm thấy {{type}} {{searchTerm}}" + "notFound": "Không tìm thấy {{type}} {{searchTerm}}", + "noResult": "" }, "toot": { "name": "Nội dung tút", diff --git a/src/i18n/zh-Hans/screens/tabs.json b/src/i18n/zh-Hans/screens/tabs.json index 43b07172..c7e6e9f8 100644 --- a/src/i18n/zh-Hans/screens/tabs.json +++ b/src/i18n/zh-Hans/screens/tabs.json @@ -373,7 +373,8 @@ "hashtags": "话题标签", "statuses": "嘟文" }, - "notFound": "找不到 {{searchTerm}} 相关的 {{type}}" + "notFound": "找不到 {{searchTerm}} 相关的 {{type}}", + "noResult": "搜索不到相关信息,请尝试不同关键词" }, "toot": { "name": "对话", diff --git a/src/i18n/zh-Hant/screens/tabs.json b/src/i18n/zh-Hant/screens/tabs.json index 1d662f10..8f992b34 100644 --- a/src/i18n/zh-Hant/screens/tabs.json +++ b/src/i18n/zh-Hant/screens/tabs.json @@ -373,7 +373,8 @@ "hashtags": "主題標籤", "statuses": "嘟文" }, - "notFound": "找不到 {{searchTerm}} 相關的 {{type}}" + "notFound": "找不到 {{searchTerm}} 相關的 {{type}}", + "noResult": "找不到任何東西,請嘗試其它用詞" }, "toot": { "name": "討論", From 18ad22302d736edca2c33b450837c338985e4013 Mon Sep 17 00:00:00 2001 From: xmflsct Date: Mon, 16 Jan 2023 18:50:18 +0100 Subject: [PATCH 16/23] Attempt to fix #644 --- ...ve-ios-context-menu-npm-1.15.1-0034bfa5ba.patch | 14 ++++++++++++++ package.json | 3 ++- 2 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 .yarn/patches/react-native-ios-context-menu-npm-1.15.1-0034bfa5ba.patch diff --git a/.yarn/patches/react-native-ios-context-menu-npm-1.15.1-0034bfa5ba.patch b/.yarn/patches/react-native-ios-context-menu-npm-1.15.1-0034bfa5ba.patch new file mode 100644 index 00000000..5e62a401 --- /dev/null +++ b/.yarn/patches/react-native-ios-context-menu-npm-1.15.1-0034bfa5ba.patch @@ -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; diff --git a/package.json b/package.json index 2dfb17f7..14f3da27 100644 --- a/package.json +++ b/package.json @@ -119,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", "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", - "@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" } } From 74e794a215149b567c6fb74c11b8f19d5ebd4108 Mon Sep 17 00:00:00 2001 From: xmflsct Date: Mon, 16 Jan 2023 18:56:15 +0100 Subject: [PATCH 17/23] Fix some Sentry reported crashes --- src/screens/Tabs/Me/Root/Collections.tsx | 6 +++--- src/utils/helpers/removeHTML.ts | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/screens/Tabs/Me/Root/Collections.tsx b/src/screens/Tabs/Me/Root/Collections.tsx index 8e99c785..ffe0e532 100644 --- a/src/screens/Tabs/Me/Root/Collections.tsx +++ b/src/screens/Tabs/Me/Root/Collections.tsx @@ -60,7 +60,7 @@ const Collections: React.FC = () => { title={t('screenTabs:me.stacks.favourites.name')} onPress={() => navigation.navigate('Tab-Me-Favourites')} /> - {pageMe.lists?.shown ? ( + {pageMe?.lists?.shown ? ( { onPress={() => navigation.navigate('Tab-Me-List-List')} /> ) : null} - {pageMe.followedTags?.shown ? ( + {pageMe?.followedTags?.shown ? ( { onPress={() => navigation.navigate('Tab-Me-FollowedTags')} /> ) : null} - {pageMe.announcements?.shown ? ( + {pageMe?.announcements?.shown ? ( { + if (!text) return '' + let raw: string = '' const parser = new htmlparser2.Parser({ From 196f51bfca7cc45b4826693f8f0259897d34f554 Mon Sep 17 00:00:00 2001 From: xmflsct Date: Mon, 16 Jan 2023 22:11:41 +0100 Subject: [PATCH 18/23] Refine account switching Part of #663 --- src/App.tsx | 34 ++--------- src/components/Instance/index.tsx | 14 +++-- src/screens/index.tsx | 24 +------- src/utils/helpers/featureCheck.ts | 8 +-- src/utils/startup/netInfo.ts | 54 +---------------- src/utils/storage/actions.ts | 97 ++++++++++++++++++++++--------- 6 files changed, 89 insertions(+), 142 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 427e18ad..c1d671e2 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -10,13 +10,7 @@ import log from '@utils/startup/log' import netInfo from '@utils/startup/netInfo' import push from '@utils/startup/push' import sentry from '@utils/startup/sentry' -import { storage } from '@utils/storage' -import { - getGlobalStorage, - removeAccount, - setAccount, - setGlobalStorage -} from '@utils/storage/actions' +import { getGlobalStorage, setAccount, setGlobalStorage } from '@utils/storage/actions' import { migrateFromAsyncStorage, versionStorageGlobal } from '@utils/storage/migrations/toMMKV' import ThemeManager from '@utils/styles/ThemeManager' import * as Localization from 'expo-localization' @@ -24,7 +18,6 @@ import * as SplashScreen from 'expo-splash-screen' import React, { useCallback, useEffect, useState } from 'react' import { LogBox, Platform } from 'react-native' import { GestureHandlerRootView } from 'react-native-gesture-handler' -import { MMKV } from 'react-native-mmkv' import { SafeAreaProvider } from 'react-native-safe-area-context' import { enableFreeze } from 'react-native-screens' import i18n from './i18n' @@ -36,6 +29,7 @@ Platform.select({ dev() sentry() +netInfo() audio() push() enableFreeze(true) @@ -46,7 +40,6 @@ SplashScreen.preventAutoHideAsync() const App: React.FC = () => { log('log', 'App', 'rendering App') const [appIsReady, setAppIsReady] = useState(false) - const [localCorrupt, setLocalCorrupt] = useState() const [hasMigrated, setHasMigrated] = useState(versionStorageGlobal !== undefined) @@ -61,36 +54,19 @@ const App: React.FC = () => { log('log', 'App', 'loading from MMKV') const account = getGlobalStorage.string('account.active') if (account) { - const storageAccount = new MMKV({ id: 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) - } + await setAccount(account) } else { log('log', 'App', 'No active account available') const accounts = getGlobalStorage.object('accounts') if (accounts?.length) { log('log', 'App', `Setting active account ${accounts[accounts.length - 1]}`) - setAccount(accounts[accounts.length - 1]) + await setAccount(accounts[accounts.length - 1]) } else { 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}`) const language = getLanguage() if (!language) { @@ -124,7 +100,7 @@ const App: React.FC = () => { - + diff --git a/src/components/Instance/index.tsx b/src/components/Instance/index.tsx index 9b53312d..5d8e829d 100644 --- a/src/components/Instance/index.tsx +++ b/src/components/Instance/index.tsx @@ -193,10 +193,12 @@ const ComponentInstance: React.FC = ({ } }) - const scopes = featureCheck('deprecate_auth_follow') - ? ['read', 'write', 'push'] - : ['read', 'write', 'follow', 'push'] const processUpdate = useCallback(() => { + const scopes = () => + featureCheck('deprecate_auth_follow', instanceQuery.data?.version) + ? ['read', 'write', 'push'] + : ['read', 'write', 'follow', 'push'] + if (domain) { const accounts = getGlobalStorage.object('accounts') if (accounts?.filter(account => account.startsWith(`${domain}/`)).length) { @@ -210,15 +212,15 @@ const ComponentInstance: React.FC = ({ }, { text: t('common:buttons.continue'), - onPress: () => appsMutation.mutate({ domain, scopes }) + onPress: () => appsMutation.mutate({ domain, scopes: scopes() }) } ] ) } else { - appsMutation.mutate({ domain, scopes }) + appsMutation.mutate({ domain, scopes: scopes() }) } } - }, [domain]) + }, [domain, instanceQuery.data?.version]) return ( () -export interface Props { - localCorrupt?: string -} - -const Screens: React.FC = ({ localCorrupt }) => { +const Screens: React.FC = () => { const { t, i18n } = useTranslation([ 'common', 'screens', @@ -64,24 +60,6 @@ const Screens: React.FC = ({ localCorrupt }) => { 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 useInstanceQuery({ options: { enabled: !!accountActive } }) useProfileQuery({ options: { enabled: !!accountActive } }) diff --git a/src/utils/helpers/featureCheck.ts b/src/utils/helpers/featureCheck.ts index ff6f4310..83acf784 100644 --- a/src/utils/helpers/featureCheck.ts +++ b/src/utils/helpers/featureCheck.ts @@ -51,8 +51,6 @@ const features = [ } ] -export const featureCheck = (feature: string): boolean => { - const version = getAccountStorage.string('version') - return !!features.filter(f => f.feature === feature).filter(f => parseFloat(version) >= f.version) - ?.length -} +export const featureCheck = (feature: string, v?: string): boolean => + (features.find(f => f.feature === feature)?.version || 999) <= + parseFloat(v || getAccountStorage.string('version')) diff --git a/src/utils/startup/netInfo.ts b/src/utils/startup/netInfo.ts index d20e558f..800e8af7 100644 --- a/src/utils/startup/netInfo.ts +++ b/src/utils/startup/netInfo.ts @@ -1,67 +1,15 @@ import NetInfo from '@react-native-community/netinfo' 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' -const netInfo = async (): Promise<{ - connected?: boolean - corrupted?: string -} | void> => { +const netInfo = () => { log('log', 'netInfo', 'initializing') - const netInfo = await NetInfo.fetch() - onlineManager.setEventListener(setOnline => { return NetInfo.addEventListener(state => { 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({ - 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 diff --git a/src/utils/storage/actions.ts b/src/utils/storage/actions.ts index 584c4005..d0383e1f 100644 --- a/src/utils/storage/actions.ts +++ b/src/utils/storage/actions.ts @@ -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 log from '@utils/startup/log' import { storage } from '@utils/storage' import { Platform } from 'react-native' import { @@ -222,10 +227,72 @@ export const generateAccountKey = ({ }) => `${domain}/${id}` export const setAccount = async (account: string) => { - storage.account = new MMKV({ id: account }) - setGlobalStorage('account.active', account) - await queryClient.resetQueries() - queryClient.clear() + const temp = new MMKV({ id: account }) + const token = temp.getString('auth.token') + const domain = temp.getString('auth.domain') + + if (!token || !domain) { + await removeAccount(account) + return + } + + await apiGeneral({ + 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) => { + displayMessage({ + message: i18n.t('screens:localCorrupt.message'), + type: 'danger' + }) + // @ts-ignore + navigationRef.navigate('Screen-Tabs', { screen: 'Tab-Me' }) + + const currAccounts: NonNullable = + getGlobalStorage.object('accounts') || [] + const nextAccounts: NonNullable = 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 = { @@ -263,25 +330,3 @@ export const getReadableAccounts = (withoutActive: boolean = false): ReadableAcc }) || [] ).filter(a => a.acct.length) } - -export const removeAccount = async (account: string) => { - const currAccounts: NonNullable = JSON.parse( - storage.global.getString('accounts') || '[]' - ) - const nextAccounts: NonNullable = 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() -} From adb7a765b424a2c23a7fe916fd969863853c4503 Mon Sep 17 00:00:00 2001 From: xmflsct Date: Mon, 16 Jan 2023 22:22:19 +0100 Subject: [PATCH 19/23] Fix #663 Properly revoke token when actively logging out --- src/screens/Tabs/Me/Root/Logout.tsx | 2 +- src/utils/storage/actions.ts | 36 +++++++++++++++++++++++++---- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/src/screens/Tabs/Me/Root/Logout.tsx b/src/screens/Tabs/Me/Root/Logout.tsx index fc11cdc8..25375653 100644 --- a/src/screens/Tabs/Me/Root/Logout.tsx +++ b/src/screens/Tabs/Me/Root/Logout.tsx @@ -32,7 +32,7 @@ const Logout: React.FC = () => { onPress: () => { if (accountActive) { haptics('Light') - removeAccount(accountActive) + removeAccount(accountActive, false) } } }, diff --git a/src/utils/storage/actions.ts b/src/utils/storage/actions.ts index d0383e1f..d25dd945 100644 --- a/src/utils/storage/actions.ts +++ b/src/utils/storage/actions.ts @@ -264,14 +264,40 @@ export const setAccount = async (account: string) => { }) } -export const removeAccount = async (account: string) => { - displayMessage({ - message: i18n.t('screens:localCorrupt.message'), - type: 'danger' - }) +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 = getGlobalStorage.object('accounts') || [] const nextAccounts: NonNullable = currAccounts.filter( From c2a180f4f5bd072fd145c65fec90c944d896fe5e Mon Sep 17 00:00:00 2001 From: xmflsct Date: Mon, 16 Jan 2023 22:36:49 +0100 Subject: [PATCH 20/23] Fix popToTop dev warning --- src/screens/Tabs/Local/index.tsx | 2 +- src/screens/Tabs/Notifications/index.tsx | 2 +- src/screens/Tabs/Public/index.tsx | 2 +- src/utils/navigation/usePopToTop.ts | 11 +++++++---- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/screens/Tabs/Local/index.tsx b/src/screens/Tabs/Local/index.tsx index a018b8a2..5c133c94 100644 --- a/src/screens/Tabs/Local/index.tsx +++ b/src/screens/Tabs/Local/index.tsx @@ -8,7 +8,7 @@ import Root from './Root' const Stack = createNativeStackNavigator() const TabLocal: React.FC = () => { - usePopToTop() + usePopToTop('Tab-Local-Root') return ( diff --git a/src/screens/Tabs/Notifications/index.tsx b/src/screens/Tabs/Notifications/index.tsx index 7f60b75a..06d1688b 100644 --- a/src/screens/Tabs/Notifications/index.tsx +++ b/src/screens/Tabs/Notifications/index.tsx @@ -44,7 +44,7 @@ const Root: React.FC< } const TabNotifications: React.FC = () => { - usePopToTop() + usePopToTop('Tab-Notifications-Root') return ( diff --git a/src/screens/Tabs/Public/index.tsx b/src/screens/Tabs/Public/index.tsx index a4cf7392..3135b4b5 100644 --- a/src/screens/Tabs/Public/index.tsx +++ b/src/screens/Tabs/Public/index.tsx @@ -8,7 +8,7 @@ import Root from './Root' const Stack = createNativeStackNavigator() const TabPublic: React.FC = () => { - usePopToTop() + usePopToTop('Tab-Public-Root') return ( diff --git a/src/utils/navigation/usePopToTop.ts b/src/utils/navigation/usePopToTop.ts index d16ccc49..1c99df13 100644 --- a/src/utils/navigation/usePopToTop.ts +++ b/src/utils/navigation/usePopToTop.ts @@ -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 { useEffect } from 'react' +import navigationRef from './navigationRef' // Mostly used when switching account and sub pages were still querying the old instance -const usePopToTop = () => { - const navigation = useNavigation() +const usePopToTop = (name: string) => { const [accountActive] = useGlobalStorage.string('account.active') useEffect(() => { - navigation.dispatch(StackActions.popToTop()) + const currentRoute = navigationRef.getCurrentRoute() + if (currentRoute && currentRoute.name !== name) { + navigationRef.dispatch(StackActions.popToTop()) + } }, [accountActive]) } From a131b1277ce2b603caf44dd13a9b83672894e855 Mon Sep 17 00:00:00 2001 From: xmflsct Date: Mon, 16 Jan 2023 22:52:30 +0100 Subject: [PATCH 21/23] Fix local content not loaded With the new prepend approach --- src/screens/Tabs/Shared/Toot.tsx | 102 +++++++++++++++++-------------- 1 file changed, 55 insertions(+), 47 deletions(-) diff --git a/src/screens/Tabs/Shared/Toot.tsx b/src/screens/Tabs/Shared/Toot.tsx index e40147e0..6210521c 100644 --- a/src/screens/Tabs/Shared/Toot.tsx +++ b/src/screens/Tabs/Shared/Toot.tsx @@ -74,8 +74,55 @@ const TabSharedToot: React.FC> = ({ const PREV_PER_BATCH = 1 const ancestorsCache = useRef<(Mastodon.Status & { _level?: number; key?: string })[]>() const loaded = useRef(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 remoteQueryEnabled = + ['public', 'unlisted'].includes(toot.visibility) && + match?.domain !== getAccountStorage.string('auth.domain') const query = useQuery<{ pages: { body: (Mastodon.Status & { _level?: number; key?: string })[] }[] }>( @@ -115,11 +162,15 @@ const TabSharedToot: React.FC> = ({ enabled: !toot._remote, staleTime: 0, refetchOnMount: true, - onSuccess: data => { + onSuccess: async data => { if (data.pages[0].body.length < 1) { navigation.goBack() return } + + if (!remoteQueryEnabled) { + await prependContent() + } } } ) @@ -192,12 +243,10 @@ const TabSharedToot: React.FC> = ({ }) }, { - enabled: - (toot._remote ? true : query.isFetched) && - ['public', 'unlisted'].includes(toot.visibility) && - match?.domain !== getAccountStorage.string('auth.domain'), + enabled: (toot._remote ? true : query.isFetched) && remoteQueryEnabled, staleTime: 0, refetchOnMount: true, + retry: false, onSuccess: async data => { if ((query.data?.pages[0].body.length || 0) < 1 && data.length < 1) { navigation.goBack() @@ -245,48 +294,7 @@ const TabSharedToot: React.FC> = ({ } }, onSettled: 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 - } - } + await prependContent() } } ) From d93c77c4cab09ef65313ed0e26ca33bb95bbb190 Mon Sep 17 00:00:00 2001 From: xmflsct Date: Mon, 16 Jan 2023 22:56:58 +0100 Subject: [PATCH 22/23] Update yarn.lock --- yarn.lock | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/yarn.lock b/yarn.lock index 224235d5..6c49a110 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9657,7 +9657,7 @@ __metadata: languageName: node linkType: hard -"react-native-ios-context-menu@npm:^1.15.1": +"react-native-ios-context-menu@npm:1.15.1": version: 1.15.1 resolution: "react-native-ios-context-menu@npm:1.15.1" dependencies: @@ -9669,6 +9669,18 @@ __metadata: languageName: node 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": version: 2.0.2 resolution: "react-native-iphone-screen-helper@npm:2.0.2" From fb6a111c557571b8492b604ee2d73571d7c9ba73 Mon Sep 17 00:00:00 2001 From: xmflsct Date: Tue, 17 Jan 2023 12:57:37 +0100 Subject: [PATCH 23/23] Fix #664 --- src/components/contextMenu/status.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/components/contextMenu/status.ts b/src/components/contextMenu/status.ts index ccdf7ae3..5641d38e 100644 --- a/src/components/contextMenu/status.ts +++ b/src/components/contextMenu/status.ts @@ -58,6 +58,7 @@ const menuStatus = ({ const menus: ContextMenu = [] const [accountId] = useAccountStorage.string('auth.account.id') + const [accountAcct] = useAccountStorage.string('auth.account.acct') const ownAccount = accountId === status.account?.id const canEditPost = featureCheck('edit_post') @@ -193,7 +194,13 @@ const menuStatus = ({ }), disabled: 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', { defaultValue: 'false',