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

Continue refine remote logic #638

This commit is contained in:
xmflsct
2023-01-03 23:57:23 +01:00
parent b067b9bdb1
commit 0bcd0c1725
46 changed files with 548 additions and 531 deletions

View File

@ -1,62 +1,69 @@
import { getAccountStorage } from '@utils/storage/actions'
import parse from 'url-parse'
const getHost = (url: unknown): string | undefined | null => {
if (typeof url !== 'string') return undefined
const matches = url.match(/^(?:https?:\/\/)?(?:[^@\/\n]+@)?(?:www\.)?([^:\/\n]+)/i)
return matches?.[1]
}
const matchStatus = (
url: string
): { id: string; style: 'default' | 'pretty'; sameInstance: boolean } | null => {
// https://social.xmflsct.com/web/statuses/105590085754428765 <- default
// https://social.xmflsct.com/@tooot/105590085754428765 <- pretty
const matcherStatus = new RegExp(/(https?:\/\/)?([^\/]+)\/(web\/statuses|@.+)\/([0-9]+)/)
const matched = url.match(matcherStatus)
if (matched) {
const hostname = matched[2]
const style = matched[3] === 'web/statuses' ? 'default' : 'pretty'
const id = matched[4]
const sameInstance = hostname === getAccountStorage.string('auth.domain')
return { id, style, sameInstance }
}
return null
}
const matchAccount = (
export const urlMatcher = (
url: string
):
| { id: string; style: 'default'; sameInstance: boolean }
| { username: string; style: 'pretty'; sameInstance: boolean }
| null => {
// https://social.xmflsct.com/web/accounts/14195 <- default
// https://social.xmflsct.com/web/@tooot <- pretty ! cannot be searched on the same instance
// https://social.xmflsct.com/@tooot <- pretty
const matcherAccount = new RegExp(
/(https?:\/\/)?([^\/]+)(\/web\/accounts\/([0-9]+)|\/web\/(@.+)|\/(@.+))/
)
const matched = url.match(matcherAccount)
if (matched) {
const hostname = matched[2]
const account = matched.filter(i => i).reverse()?.[0]
if (account) {
const style = account.startsWith('@') ? 'pretty' : 'default'
const sameInstance = hostname === getAccountStorage.string('auth.domain')
return style === 'default'
? { id: account, style, sameInstance }
: { username: account, style, sameInstance }
} else {
return null
| {
domain: string
account?: Partial<Pick<Mastodon.Account, 'id' | 'acct' | '_remote'>>
status?: Partial<Pick<Mastodon.Status, 'id' | '_remote'>>
}
| undefined => {
const parsed = parse(url)
if (!parsed.hostname.length || !parsed.pathname.length) return undefined
const domain = parsed.hostname
const _remote = parsed.hostname !== getAccountStorage.string('auth.domain')
let statusId: string | undefined
let accountId: string | undefined
let accountAcct: string | undefined
const segments = parsed.pathname.split('/')
const last = segments[segments.length - 1]
const length = segments.length // there is a starting slash
switch (last?.startsWith('@')) {
case true:
if (length === 2 || (length === 3 && segments[length - 2] === 'web')) {
// https://social.xmflsct.com/@tooot <- Mastodon v4.0 and above
// https://social.xmflsct.com/web/@tooot <- Mastodon v3.5 and below ! cannot be searched on the same instance
accountAcct = `${last}@${domain}`
}
break
case false:
const nextToLast = segments[length - 2]
if (nextToLast) {
if (nextToLast === 'statuses') {
if (length === 4 && segments[length - 3] === 'web') {
// https://social.xmflsct.com/web/statuses/105590085754428765 <- old
statusId = last
} else if (
length === 5 &&
segments[length - 2] === 'statuses' &&
segments[length - 4] === 'users'
) {
// https://social.xmflsct.com/users/tooot/statuses/105590085754428765 <- default Mastodon
statusId = last
// accountAcct = `@${segments[length - 3]}@${domain}`
}
} else if (
nextToLast.startsWith('@') &&
(length === 3 || (length === 4 && segments[length - 3] === 'web'))
) {
// https://social.xmflsct.com/web/@tooot/105590085754428765 <- pretty Mastodon v3.5 and below
// https://social.xmflsct.com/@tooot/105590085754428765 <- pretty Mastodon v4.0 and above
statusId = last
// accountAcct = `${nextToLast}@${domain}`
}
}
break
}
return null
return {
domain,
...((accountId || accountAcct) && { account: { id: accountId, acct: accountAcct, _remote } }),
...(statusId && { status: { id: statusId, _remote } })
}
}
export { getHost, matchStatus, matchAccount }

View File

@ -94,7 +94,7 @@ export type TabSharedStackParamList = {
hashtag: Mastodon.Tag['name']
}
'Tab-Shared-History': {
id: Mastodon.Status['id']
status: Mastodon.Status
detectedLanguage: string
}
'Tab-Shared-Report': {

View File

@ -5,7 +5,7 @@ import {
PERMISSION_MANAGE_REPORTS,
PERMISSION_MANAGE_USERS
} from '@utils/helpers/permissions'
import queryClient from '@utils/queryHooks'
import { queryClient } from '@utils/queryHooks'
import { QueryKeyProfile } from '@utils/queryHooks/profile'
import { getAccountDetails, getGlobalStorage } from '@utils/storage/actions'
import * as Notifications from 'expo-notifications'

View File

@ -1,5 +1,5 @@
import { displayMessage } from '@components/Message'
import queryClient from '@utils/queryHooks'
import { queryClient } from '@utils/queryHooks'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { generateAccountKey, setAccount, useGlobalStorage } from '@utils/storage/actions'
import * as Notifications from 'expo-notifications'

View File

@ -1,4 +1,4 @@
import queryClient from '@utils/queryHooks'
import { queryClient } from '@utils/queryHooks'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { generateAccountKey, setAccount, useGlobalStorage } from '@utils/storage/actions'
import * as Notifications from 'expo-notifications'

View File

@ -1,68 +1,89 @@
import { QueryFunctionContext, useQuery, UseQueryOptions } from '@tanstack/react-query'
import apiGeneral from '@utils/api/general'
import apiInstance from '@utils/api/instance'
import { urlMatcher } from '@utils/helpers/urlMatcher'
import { AxiosError } from 'axios'
import { SearchResult } from './search'
import { searchLocalAccount } from './search'
export type QueryKeyAccount = [
'Account',
Pick<Mastodon.Account, 'id' | 'url' | '_remote'> | undefined
(
| (Partial<Pick<Mastodon.Account, 'id' | 'acct' | 'username' | '_remote'>> &
Pick<Mastodon.Account, 'url'> & { _local?: boolean })
| undefined
)
]
const accountQueryFunction = async ({ queryKey }: QueryFunctionContext<QueryKeyAccount>) => {
const key = queryKey[1]
if (!key) return Promise.reject()
let matchedId = key.id
let matchedAccount: Mastodon.Account | undefined = undefined
if (key._remote) {
await apiInstance<SearchResult>({
version: 'v2',
method: 'get',
url: 'search',
params: {
q: key.url,
type: 'accounts',
limit: 1,
resolve: true
}
})
.then(res => {
const account = res.body.accounts[0]
if (account.url !== key.url) {
return Promise.reject()
} else {
matchedId = account.id
}
})
.catch(() => Promise.reject())
}
const match = urlMatcher(key.url)
const res = await apiInstance<Mastodon.Account>({
method: 'get',
url: `accounts/${matchedId}`
})
return res.body
const domain = match?.domain
const id = key.id || match?.account?.id
const acct = key.acct || key.username || match?.account?.acct
if (!key._local && domain) {
try {
if (id) {
matchedAccount = await apiGeneral<Mastodon.Account>({
method: 'get',
domain: domain,
url: `api/v1/accounts/${id}`
}).then(res => ({ ...res.body, _remote: true }))
} else if (acct) {
matchedAccount = await apiGeneral<Mastodon.Account>({
method: 'get',
domain: domain,
url: 'api/v1/accounts/lookup',
params: { acct }
}).then(res => ({ ...res.body, _remote: true }))
}
} catch {}
}
if (!matchedAccount) {
matchedAccount = await searchLocalAccount(key.url)
}
} else {
if (!matchedAccount) {
matchedAccount = await apiInstance<Mastodon.Account>({
method: 'get',
url: `accounts/${key.id}`
}).then(res => res.body)
}
}
return matchedAccount
}
const useAccountQuery = ({
options,
...queryKeyParams
}: { account?: QueryKeyAccount[1] } & {
account,
_local,
options
}: {
account?: QueryKeyAccount[1]
_local?: boolean
options?: UseQueryOptions<Mastodon.Account, AxiosError>
}) => {
const queryKey: QueryKeyAccount = [
'Account',
queryKeyParams.account
account
? {
id: queryKeyParams.account.id,
url: queryKeyParams.account.url,
_remote: queryKeyParams.account._remote
id: account.id,
username: account.username,
url: account.url,
_remote: account._remote,
...(_local && { _local })
}
: undefined
]
return useQuery(queryKey, accountQueryFunction, {
...options,
enabled: (queryKeyParams.account?._remote ? !!queryKeyParams.account : true) && options?.enabled
enabled: (account?._remote ? !!account : true) && options?.enabled
})
}

View File

@ -1,6 +1,6 @@
import { QueryClient } from '@tanstack/react-query'
const queryClient = new QueryClient({
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5,
@ -15,12 +15,10 @@ const queryClient = new QueryClient({
}
}
}
},
logger: {
log: log => console.log(log),
warn: () => {},
error: () => {}
}
})
// @ts-ignore
import('react-query-native-devtools').then(({ addPlugin }) => {
addPlugin({ queryClient })
})
export default queryClient

View File

@ -2,7 +2,7 @@ import haptics from '@components/haptics'
import { displayMessage } from '@components/Message'
import { useMutation, useQuery, UseQueryOptions } from '@tanstack/react-query'
import apiInstance from '@utils/api/instance'
import queryClient from '@utils/queryHooks'
import { queryClient } from '@utils/queryHooks'
import { AxiosError } from 'axios'
import i18next from 'i18next'
import { RefObject } from 'react'

View File

@ -1,5 +1,6 @@
import { QueryFunctionContext, useQuery, UseQueryOptions } from '@tanstack/react-query'
import apiInstance from '@utils/api/instance'
import { queryClient } from '@utils/queryHooks'
import { AxiosError } from 'axios'
export type QueryKeySearch = [
@ -43,22 +44,27 @@ const useSearchQuery = <T = SearchResult>({
options?: UseQueryOptions<SearchResult, AxiosError, T>
}) => {
const queryKey: QueryKeySearch = ['Search', { ...queryKeyParams }]
return useQuery(queryKey, queryFunction, options)
return useQuery(queryKey, queryFunction, { ...options, staleTime: 3600, cacheTime: 3600 })
}
export const searchFetchToot = (uri: Mastodon.Status['uri']): Promise<Mastodon.Status | void> =>
apiInstance<SearchResult>({
version: 'v2',
method: 'get',
url: 'search',
params: {
q: uri,
type: 'statuses',
limit: 1,
resolve: true
}
})
.then(res => res.body.statuses[0])
.catch(err => console.warn(err))
export const searchLocalStatus = async (uri: Mastodon.Status['uri']): Promise<Mastodon.Status> => {
const queryKey: QueryKeySearch = ['Search', { type: 'statuses', term: uri, limit: 1 }]
return await queryClient
.fetchQuery(queryKey, queryFunction, { staleTime: 3600, cacheTime: 3600 })
.then(res =>
res.statuses[0].uri === uri || res.statuses[0].url === uri
? res.statuses[0]
: Promise.reject()
)
}
export const searchLocalAccount = async (
url: Mastodon.Account['url']
): Promise<Mastodon.Account> => {
const queryKey: QueryKeySearch = ['Search', { type: 'accounts', term: url, limit: 1 }]
return await queryClient
.fetchQuery(queryKey, queryFunction, { staleTime: 3600, cacheTime: 3600 })
.then(res => (res.accounts[0].url === url ? res.accounts[0] : Promise.reject()))
}
export { useSearchQuery }

View File

@ -1,26 +1,59 @@
import { QueryFunctionContext, useQuery, UseQueryOptions } from '@tanstack/react-query'
import apiGeneral from '@utils/api/general'
import apiInstance from '@utils/api/instance'
import { urlMatcher } from '@utils/helpers/urlMatcher'
import { AxiosError } from 'axios'
import { searchLocalStatus } from './search'
export type QueryKeyStatus = ['Status', { id: Mastodon.Status['id'] }]
export type QueryKeyStatus = [
'Status',
(Pick<Mastodon.Status, 'uri'> & Partial<Pick<Mastodon.Status, 'id' | '_remote'>>) | undefined
]
const queryFunction = async ({ queryKey }: QueryFunctionContext<QueryKeyStatus>) => {
const { id } = queryKey[1]
const key = queryKey[1]
if (!key) return Promise.reject()
const res = await apiInstance<Mastodon.Status>({
method: 'get',
url: `statuses/${id}`
})
return res.body
let matchedStatus: Mastodon.Status | undefined = undefined
const match = urlMatcher(key.uri)
const domain = match?.domain
const id = key.id || match?.status?.id
if (key._remote && domain && id) {
try {
matchedStatus = await apiGeneral<Mastodon.Status>({
method: 'get',
domain,
url: `api/v1/statuses/${id}`
}).then(res => ({ ...res.body, _remote: true }))
} catch {}
}
if (!matchedStatus && !key._remote && id) {
matchedStatus = await apiInstance<Mastodon.Status>({
method: 'get',
url: `statuses/${id}`
}).then(res => res.body)
}
if (!matchedStatus) {
matchedStatus = await searchLocalStatus(key.uri)
}
return matchedStatus
}
const useStatusQuery = ({
options,
...queryKeyParams
}: QueryKeyStatus[1] & {
status
}: { status?: QueryKeyStatus[1] } & {
options?: UseQueryOptions<Mastodon.Status, AxiosError>
}) => {
const queryKey: QueryKeyStatus = ['Status', { ...queryKeyParams }]
const queryKey: QueryKeyStatus = [
'Status',
status ? { id: status.id, uri: status.uri, _remote: status._remote } : undefined
]
return useQuery(queryKey, queryFunction, options)
}

View File

@ -1,34 +1,53 @@
import { QueryFunctionContext, useQuery, UseQueryOptions } from '@tanstack/react-query'
import apiGeneral from '@utils/api/general'
import apiInstance from '@utils/api/instance'
import { urlMatcher } from '@utils/helpers/urlMatcher'
import { AxiosError } from 'axios'
export type QueryKeyStatusesHistory = [
'StatusesHistory',
{ id: Mastodon.Status['id'] }
Pick<Mastodon.Status, 'id' | 'uri' | 'edited_at' | '_remote'> &
Partial<Pick<Mastodon.Status, 'edited_at'>>
]
const queryFunction = async ({
queryKey
}: QueryFunctionContext<QueryKeyStatusesHistory>) => {
const { id } = queryKey[1]
const res = await apiInstance<Mastodon.StatusHistory[]>({
const queryFunction = async ({ queryKey }: QueryFunctionContext<QueryKeyStatusesHistory>) => {
const { id, uri, _remote } = queryKey[1]
if (_remote) {
const match = urlMatcher(uri)
const domain = match?.domain
if (!domain) {
return Promise.reject('Cannot find remote domain to retrieve status histories')
}
return await apiGeneral<Mastodon.StatusHistory[]>({
method: 'get',
domain,
url: `api/v1/statuses/${id}/history`
}).then(res => res.body)
}
return await apiInstance<Mastodon.StatusHistory[]>({
method: 'get',
url: `statuses/${id}/history`
})
return res.body
}).then(res => res.body)
}
const useStatusHistory = ({
options,
...queryKeyParams
}: QueryKeyStatusesHistory[1] & {
status
}: { status: QueryKeyStatusesHistory[1] } & {
options?: UseQueryOptions<Mastodon.StatusHistory[], AxiosError>
}) => {
const queryKey: QueryKeyStatusesHistory = [
'StatusesHistory',
{ ...queryKeyParams }
{ id: status.id, uri: status.uri, edited_at: status.edited_at, _remote: status._remote }
]
return useQuery(queryKey, queryFunction, options)
return useQuery(queryKey, queryFunction, {
...options,
enabled: !!status.edited_at,
staleTime: 3600,
cacheTime: 3600
})
}
export { useStatusHistory }

View File

@ -9,11 +9,11 @@ import {
import { PagedResponse } from '@utils/api/helpers'
import apiInstance from '@utils/api/instance'
import { featureCheck } from '@utils/helpers/featureCheck'
import queryClient from '@utils/queryHooks'
import { queryClient } from '@utils/queryHooks'
import { getAccountStorage } from '@utils/storage/actions'
import { AxiosError } from 'axios'
import { uniqBy } from 'lodash'
import { searchFetchToot } from './search'
import { searchLocalStatus } from './search'
import deleteItem from './timeline/deleteItem'
import editItem from './timeline/editItem'
import updateStatusProperty from './timeline/updateStatusProperty'
@ -342,7 +342,7 @@ const mutationFunction = async (params: MutationVarsTimeline) => {
default:
let tootId = params.status.id
if (params.status._remote) {
const fetched = await searchFetchToot(params.status.uri)
const fetched = await searchLocalStatus(params.status.uri)
if (fetched) {
tootId = fetched.id
} else {

View File

@ -1,19 +1,13 @@
import { InfiniteData } from '@tanstack/react-query'
import queryClient from '@utils/queryHooks'
import { queryClient } from '@utils/queryHooks'
import { MutationVarsTimelineDeleteItem } from '../timeline'
const deleteItem = ({
queryKey,
rootQueryKey,
id
}: MutationVarsTimelineDeleteItem) => {
const deleteItem = ({ queryKey, rootQueryKey, id }: MutationVarsTimelineDeleteItem) => {
queryKey &&
queryClient.setQueryData<InfiniteData<any> | undefined>(queryKey, old => {
if (old) {
old.pages = old.pages.map(page => {
page.body = page.body.filter(
(item: Mastodon.Status) => item.id !== id
)
page.body = page.body.filter((item: Mastodon.Status) => item.id !== id)
return page
})
return old
@ -21,20 +15,15 @@ const deleteItem = ({
})
rootQueryKey &&
queryClient.setQueryData<InfiniteData<any> | undefined>(
rootQueryKey,
old => {
if (old) {
old.pages = old.pages.map(page => {
page.body = page.body.filter(
(item: Mastodon.Status) => item.id !== id
)
return page
})
return old
}
queryClient.setQueryData<InfiniteData<any> | undefined>(rootQueryKey, old => {
if (old) {
old.pages = old.pages.map(page => {
page.body = page.body.filter((item: Mastodon.Status) => item.id !== id)
return page
})
return old
}
)
})
}
export default deleteItem

View File

@ -1,12 +1,8 @@
import { InfiniteData } from '@tanstack/react-query'
import queryClient from '@utils/queryHooks'
import { queryClient } from '@utils/queryHooks'
import { MutationVarsTimelineEditItem } from '../timeline'
const editItem = ({
queryKey,
rootQueryKey,
status
}: MutationVarsTimelineEditItem) => {
const editItem = ({ queryKey, rootQueryKey, status }: MutationVarsTimelineEditItem) => {
queryKey &&
queryClient.setQueryData<InfiniteData<any> | undefined>(queryKey, old => {
if (old) {
@ -24,23 +20,20 @@ const editItem = ({
})
rootQueryKey &&
queryClient.setQueryData<InfiniteData<any> | undefined>(
rootQueryKey,
old => {
if (old) {
old.pages = old.pages.map(page => {
page.body = page.body.map((item: Mastodon.Status) => {
if (item.id === status.id) {
item = status
}
return item
})
return page
queryClient.setQueryData<InfiniteData<any> | undefined>(rootQueryKey, old => {
if (old) {
old.pages = old.pages.map(page => {
page.body = page.body.map((item: Mastodon.Status) => {
if (item.id === status.id) {
item = status
}
return item
})
return old
}
return page
})
return old
}
)
})
}
export default editItem

View File

@ -1,5 +1,5 @@
import { InfiniteData } from '@tanstack/react-query'
import queryClient from '@utils/queryHooks'
import { queryClient } from '@utils/queryHooks'
import { MutationVarsTimelineUpdateStatusProperty, TimelineData } from '../timeline'
const updateStatusProperty = ({
@ -9,7 +9,7 @@ const updateStatusProperty = ({
payload,
poll
}: MutationVarsTimelineUpdateStatusProperty & { poll?: Mastodon.Poll }) => {
for (const key of [queryKey]) {
for (const key of [queryKey, rootQueryKey]) {
if (!key) continue
queryClient.setQueryData<InfiniteData<TimelineData> | undefined>(key, old => {

View File

@ -1,22 +1,18 @@
import {
QueryFunctionContext,
useInfiniteQuery,
UseInfiniteQueryOptions
QueryFunctionContext,
useInfiniteQuery,
UseInfiniteQueryOptions
} from '@tanstack/react-query'
import apiGeneral from '@utils/api/general'
import { PagedResponse } from '@utils/api/helpers'
import apiInstance from '@utils/api/instance'
import { getHost } from '@utils/helpers/urlMatcher'
import { urlMatcher } from '@utils/helpers/urlMatcher'
import { TabSharedStackParamList } from '@utils/navigation/navigators'
import { AxiosError } from 'axios'
export type QueryKeyUsers = ['Users', TabSharedStackParamList['Tab-Shared-Users']]
const queryFunction = async ({
queryKey,
pageParam,
meta
}: QueryFunctionContext<QueryKeyUsers>) => {
const queryFunction = async ({ queryKey, pageParam }: QueryFunctionContext<QueryKeyUsers>) => {
const page = queryKey[1]
let params: { [key: string]: string } = { ...pageParam }
@ -24,7 +20,7 @@ const queryFunction = async ({
case 'statuses':
return apiInstance<Mastodon.Account[]>({
method: 'get',
url: `${page.reference}/${page.status.id}/${page.type}`,
url: `statuses/${page.status.id}/${page.type}`,
params
})
case 'accounts':
@ -32,14 +28,14 @@ const queryFunction = async ({
if (localInstance) {
return apiInstance<Mastodon.Account[]>({
method: 'get',
url: `${page.reference}/${page.account.id}/${page.type}`,
url: `accounts/${page.account.id}/${page.type}`,
params
})
} else {
let res: PagedResponse<Mastodon.Account[]>
try {
const domain = getHost(page.account.url)
const domain = urlMatcher(page.account.url)?.domain
if (!domain?.length) {
throw new Error()
}
@ -54,7 +50,7 @@ const queryFunction = async ({
res = await apiGeneral<Mastodon.Account[]>({
method: 'get',
domain,
url: `api/v1/${page.reference}/${resLookup.body.id}/${page.type}`,
url: `api/v1/accounts/${resLookup.body.id}/${page.type}`,
params
})
return { ...res, remoteData: true }

12
src/utils/startup/dev.ts Normal file
View File

@ -0,0 +1,12 @@
import { queryClient } from '@utils/queryHooks'
import log from './log'
export const dev = () => {
if (__DEV__) {
log('log', 'dev', 'loading tools')
// @ts-ignore
import('react-query-native-devtools').then(({ addPlugin }) => {
addPlugin({ queryClient })
})
}
}

View File

@ -1,4 +1,4 @@
import queryClient from '@utils/queryHooks'
import { queryClient } from '@utils/queryHooks'
import { storage } from '@utils/storage'
import {
MMKV,