Fetch remote user's toots

This commit is contained in:
xmflsct 2023-02-25 23:42:04 +01:00
parent f505f78193
commit 702ecef243
16 changed files with 184 additions and 105 deletions

View File

@ -1,3 +1,4 @@
Enjoy toooting! This version includes following improvements and fixes: Enjoy toooting! This version includes following improvements and fixes:
- Added following remote instance - Added following remote instance
- Added set note of followed users - Added set note of followed users
- Best effort load remote user's toots

View File

@ -1,3 +1,4 @@
toooting愉快此版本包括以下改进和修复 toooting愉快此版本包括以下改进和修复
- 新增关注远程实例功能 - 新增关注远程实例功能
- 新增关注用户备注功能 - 新增关注用户备注功能
- 加载远程用户的嘟文

View File

@ -33,7 +33,7 @@ declare namespace Mastodon {
role?: Role role?: Role
// Internal // Internal
_remote?: boolean _remote?: string // domain
} }
type Announcement = { type Announcement = {
@ -400,7 +400,7 @@ declare namespace Mastodon {
url: string url: string
// Internal // Internal
_remote?: boolean _remote?: string // domain
} }
type Notification = type Notification =

View File

@ -28,7 +28,7 @@ const TabMeRoot: React.FC = () => {
}) })
return ( return (
<AccountContext.Provider value={{ account: data, pageMe: true }}> <AccountContext.Provider value={{ account: data, pageMe: true, localInstance: true }}>
{accountActive && data ? <AccountNav scrollY={scrollY} /> : null} {accountActive && data ? <AccountNav scrollY={scrollY} /> : null}
<Animated.ScrollView <Animated.ScrollView
ref={scrollRef} ref={scrollRef}

View File

@ -13,7 +13,12 @@ import { Dimensions, Pressable, View } from 'react-native'
import { FlatList } from 'react-native-gesture-handler' import { FlatList } from 'react-native-gesture-handler'
import AccountContext from './Context' import AccountContext from './Context'
const AccountAttachments: React.FC = () => { export type Props = {
remote_id?: Mastodon.Status['id']
remote_domain?: string
}
const AccountAttachments: React.FC<Props> = ({ remote_id, remote_domain }) => {
const { account } = useContext(AccountContext) const { account } = useContext(AccountContext)
if (account?.suspended) return null if (account?.suspended) return null
@ -32,7 +37,8 @@ const AccountAttachments: React.FC = () => {
id: account?.id, id: account?.id,
exclude_reblogs: false, exclude_reblogs: false,
only_media: true, only_media: true,
options: { enabled: !!account?.id } ...(remote_id && remote_domain && { remote_id, remote_domain }),
options: { enabled: !!account?.id || (!!remote_id && !!remote_domain) }
}) })
const flattenData = flattenPages(data) const flattenData = flattenPages(data)
@ -55,7 +61,7 @@ const AccountAttachments: React.FC = () => {
horizontal horizontal
data={flattenData} data={flattenData}
renderItem={({ item, index }) => { renderItem={({ item, index }) => {
if (index === DISPLAY_AMOUNT - 1) { if (index === DISPLAY_AMOUNT - 1 && (!remote_id || !remote_domain)) {
return ( return (
<Pressable <Pressable
onPress={() => { onPress={() => {

View File

@ -4,6 +4,7 @@ type AccountContextType = {
account?: Mastodon.Account account?: Mastodon.Account
relationship?: Mastodon.Relationship relationship?: Mastodon.Relationship
pageMe?: boolean pageMe?: boolean
localInstance: boolean
} }
const AccountContext = createContext<AccountContextType>({} as AccountContextType) const AccountContext = createContext<AccountContextType>({} as AccountContextType)

View File

@ -10,7 +10,7 @@ import { PlaceholderLine } from 'rn-placeholder'
import AccountContext from '../Context' import AccountContext from '../Context'
const AccountInformationAccount: React.FC = () => { const AccountInformationAccount: React.FC = () => {
const { account, relationship, pageMe } = useContext(AccountContext) const { account, relationship, pageMe, localInstance } = useContext(AccountContext)
const { t } = useTranslation('screenTabs') const { t } = useTranslation('screenTabs')
const { colors } = useTheme() const { colors } = useTheme()
@ -18,10 +18,6 @@ const AccountInformationAccount: React.FC = () => {
const [acct] = useAccountStorage.string('auth.account.acct') const [acct] = useAccountStorage.string('auth.account.acct')
const [domain] = useAccountStorage.string('auth.account.domain') const [domain] = useAccountStorage.string('auth.account.domain')
const localInstance = account?.acct?.includes('@')
? account?.acct?.includes(`@${domain}`)
: !account?._remote
if (account || pageMe) { if (account || pageMe) {
return ( return (
<View <View

View File

@ -35,6 +35,7 @@ const AccountInformationAvatar: React.FC = () => {
} }
}} }}
dim dim
withoutTransition
/> />
) )
} }

View File

@ -9,6 +9,7 @@ import { TabSharedStackScreenProps } from '@utils/navigation/navigators'
import { useAccountQuery } from '@utils/queryHooks/account' import { useAccountQuery } from '@utils/queryHooks/account'
import { useRelationshipQuery } from '@utils/queryHooks/relationship' import { useRelationshipQuery } from '@utils/queryHooks/relationship'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline' import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { useAccountStorage } from '@utils/storage/actions'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React, { Fragment, useEffect, useMemo, useState } from 'react' import React, { Fragment, useEffect, useMemo, useState } from 'react'
@ -31,33 +32,19 @@ const TabSharedAccount: React.FC<TabSharedStackScreenProps<'Tab-Shared-Account'>
const { t } = useTranslation('screenTabs') const { t } = useTranslation('screenTabs')
const { colors, mode } = useTheme() const { colors, mode } = useTheme()
const { data, dataUpdatedAt } = useAccountQuery({ const { data, dataUpdatedAt, isFetched } = useAccountQuery({
account, account,
_local: true, _local: true,
options: { options: {
placeholderData: (account._remote placeholderData: (account._remote
? { ...account, id: undefined } ? { ...account, id: undefined }
: account) as Mastodon.Account, : account) as Mastodon.Account,
onSuccess: a => {
if (account._remote) {
setQueryKey([
queryKey[0],
{
...queryKey[1],
page: 'Account',
id: a.id,
exclude_reblogs: true,
only_media: false
}
])
}
},
onError: () => navigation.goBack() onError: () => navigation.goBack()
} }
}) })
const { data: dataRelationship } = useRelationshipQuery({ const { data: dataRelationship } = useRelationshipQuery({
id: account._remote ? data?.id : account.id, id: data?.id,
options: { enabled: account._remote ? !!data?.id : true } options: { enabled: account._remote ? isFetched : true }
}) })
const queryClient = useQueryClient() const queryClient = useQueryClient()
@ -65,9 +52,10 @@ const TabSharedAccount: React.FC<TabSharedStackScreenProps<'Tab-Shared-Account'>
'Timeline', 'Timeline',
{ {
page: 'Account', page: 'Account',
id: account._remote ? data?.id : account.id, id: data?.id,
exclude_reblogs: true, exclude_reblogs: true,
only_media: false only_media: false,
...(account._remote && { remote_id: account.id, remote_domain: account._remote })
} }
]) ])
@ -76,9 +64,7 @@ const TabSharedAccount: React.FC<TabSharedStackScreenProps<'Tab-Shared-Account'>
useEffect(() => { useEffect(() => {
navigation.setOptions({ navigation.setOptions({
headerTransparent: true, headerTransparent: true,
headerStyle: { headerStyle: { backgroundColor: `rgba(255, 255, 255, 0)` },
backgroundColor: `rgba(255, 255, 255, 0)`
},
title: '', title: '',
headerLeft: () => <HeaderLeft onPress={() => navigation.goBack()} background />, headerLeft: () => <HeaderLeft onPress={() => navigation.goBack()} background />,
headerRight: () => { headerRight: () => {
@ -179,7 +165,9 @@ const TabSharedAccount: React.FC<TabSharedStackScreenProps<'Tab-Shared-Account'>
<View style={{ borderBottomWidth: 1, borderBottomColor: colors.border }}> <View style={{ borderBottomWidth: 1, borderBottomColor: colors.border }}>
<AccountHeader /> <AccountHeader />
<AccountInformation /> <AccountInformation />
<AccountAttachments /> <AccountAttachments
{...(account._remote && { remote_id: account.id, remote_domain: account._remote })}
/>
</View> </View>
{!data?.suspended ? ( {!data?.suspended ? (
// @ts-ignore // @ts-ignore
@ -246,8 +234,18 @@ const TabSharedAccount: React.FC<TabSharedStackScreenProps<'Tab-Shared-Account'>
) )
}, [segment, dataUpdatedAt, mode]) }, [segment, dataUpdatedAt, mode])
const [domain] = useAccountStorage.string('auth.account.domain')
return ( return (
<AccountContext.Provider value={{ account: data, relationship: dataRelationship }}> <AccountContext.Provider
value={{
account: data,
relationship: dataRelationship,
localInstance: account?.acct?.includes('@')
? account?.acct?.includes(`@${domain}`)
: !account?._remote
}}
>
<AccountNav scrollY={scrollY} /> <AccountNav scrollY={scrollY} />
{data?.suspended ? ( {data?.suspended ? (
@ -256,7 +254,6 @@ const TabSharedAccount: React.FC<TabSharedStackScreenProps<'Tab-Shared-Account'>
<Timeline <Timeline
queryKey={queryKey} queryKey={queryKey}
disableRefresh disableRefresh
queryOptions={{ enabled: account._remote ? !!data?.id : true }}
customProps={{ customProps={{
keyboardShouldPersistTaps: 'always', keyboardShouldPersistTaps: 'always',
renderItem: ({ item }) => <TimelineDefault item={item} queryKey={queryKey} />, renderItem: ({ item }) => <TimelineDefault item={item} queryKey={queryKey} />,

View File

@ -206,7 +206,7 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
if (localMatch) { if (localMatch) {
return localMatch return localMatch
} else { } else {
return appendRemote.status(ancestor) return appendRemote.status(ancestor, domain)
} }
}) })
} }
@ -256,7 +256,7 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
if (localMatch) { if (localMatch) {
return { ...localMatch, _level: remote._level } return { ...localMatch, _level: remote._level }
} else { } else {
return appendRemote.status(remote) return appendRemote.status(remote, match!.domain)
} }
}) })
} }

View File

@ -1,23 +1,23 @@
// Central place appending _remote internal prop // Central place appending _remote internal prop
export const appendRemote = { export const appendRemote = {
status: (status: Mastodon.Status) => ({ status: (status: Mastodon.Status, domain: string) => ({
...status, ...status,
...(status.reblog && { ...(status.reblog && {
reblog: { reblog: {
...status.reblog, ...status.reblog,
account: appendRemote.account(status.reblog.account), account: appendRemote.account(status.reblog.account, domain),
mentions: appendRemote.mentions(status.reblog.mentions) mentions: appendRemote.mentions(status.reblog.mentions, domain)
} }
}), }),
account: appendRemote.account(status.account), account: appendRemote.account(status.account, domain),
mentions: appendRemote.mentions(status.mentions), mentions: appendRemote.mentions(status.mentions, domain),
_remote: true _remote: true
}), }),
account: (account: Mastodon.Account) => ({ account: (account: Mastodon.Account, domain: string) => ({
...account, ...account,
_remote: true _remote: domain
}), }),
mentions: (mentions: Mastodon.Mention[]) => mentions: (mentions: Mastodon.Mention[], domain: string) =>
mentions?.map(mention => ({ ...mention, _remote: true })) mentions?.map(mention => ({ ...mention, _remote: domain }))
} }

View File

@ -75,7 +75,7 @@ export const urlMatcher = (
return { return {
domain, domain,
...(accountAcct && { account: { acct: accountAcct, _remote } }), ...(accountAcct && { account: { acct: accountAcct, _remote: domain } }),
...(statusId && { status: { id: statusId, _remote } }) ...(statusId && { status: { id: statusId, _remote } })
} }
} }

View File

@ -33,16 +33,16 @@ const accountQueryFunction = async ({ queryKey }: QueryFunctionContext<QueryKeyA
if (id) { if (id) {
matchedAccount = await apiGeneral<Mastodon.Account>({ matchedAccount = await apiGeneral<Mastodon.Account>({
method: 'get', method: 'get',
domain: domain, domain,
url: `api/v1/accounts/${id}` url: `api/v1/accounts/${id}`
}).then(res => appendRemote.account(res.body)) }).then(res => appendRemote.account(res.body, domain))
} else if (acct) { } else if (acct) {
matchedAccount = await apiGeneral<Mastodon.Account>({ matchedAccount = await apiGeneral<Mastodon.Account>({
method: 'get', method: 'get',
domain: domain, domain,
url: 'api/v1/accounts/lookup', url: 'api/v1/accounts/lookup',
params: { acct } params: { acct }
}).then(res => appendRemote.account(res.body)) }).then(res => appendRemote.account(res.body, domain))
} }
} catch {} } catch {}
} }

View File

@ -27,7 +27,7 @@ const queryFunction = async ({ queryKey }: QueryFunctionContext<QueryKeyStatus>)
method: 'get', method: 'get',
domain, domain,
url: `api/v1/statuses/${id}` url: `api/v1/statuses/${id}`
}).then(res => appendRemote.status(res.body)) }).then(res => appendRemote.status(res.body, domain))
} catch {} } catch {}
} }

View File

@ -46,6 +46,9 @@ export type QueryKeyTimeline = [
id?: Mastodon.Account['id'] id?: Mastodon.Account['id']
exclude_reblogs: boolean exclude_reblogs: boolean
only_media: boolean only_media: boolean
// remote info
remote_id?: Mastodon.Account['id']
remote_domain?: string
} }
| { | {
page: 'Toot' page: 'Toot'
@ -107,10 +110,7 @@ export const queryFunctionTimeline = async ({
return apiInstance<Mastodon.Status[]>({ return apiInstance<Mastodon.Status[]>({
method: 'get', method: 'get',
url: 'timelines/public', url: 'timelines/public',
params: { params: { ...params, local: true }
...params,
local: 'true'
}
}) })
case 'LocalPublic': case 'LocalPublic':
@ -126,11 +126,11 @@ export const queryFunctionTimeline = async ({
method: 'get', method: 'get',
domain: page.domain, domain: page.domain,
url: 'api/v1/timelines/public', url: 'api/v1/timelines/public',
params: { params: { ...params, local: true }
...params, }).then(res => ({
local: 'true' ...res,
} body: res.body.map(status => appendRemote.status(status, page.domain!))
}).then(res => ({ ...res, body: res.body.map(status => appendRemote.status(status)) })) }))
} else { } else {
return apiInstance<Mastodon.Status[]>({ return apiInstance<Mastodon.Status[]>({
method: 'get', method: 'get',
@ -163,61 +163,137 @@ export const queryFunctionTimeline = async ({
}) })
case 'Account': case 'Account':
if (!page.id) return Promise.reject('Timeline query account id not provided') const reject = Promise.reject('Timeline query account id not provided')
if (page.only_media) { if (page.only_media) {
return apiInstance<Mastodon.Status[]>({ let res
method: 'get', if (page.remote_domain && page.remote_id) {
url: `accounts/${page.id}/statuses`, res = await apiGeneral<Mastodon.Status[]>({
params: {
only_media: 'true',
...params
}
})
} else if (page.exclude_reblogs) {
if (pageParam && pageParam.hasOwnProperty('max_id')) {
return apiInstance<Mastodon.Status[]>({
method: 'get', method: 'get',
url: `accounts/${page.id}/statuses`, domain: page.remote_domain,
url: `api/v1/accounts/${page.remote_id}/statuses`,
params: { params: {
exclude_replies: 'true', only_media: true,
...params ...params
} }
}) })
} else { .then(res => ({
const res1 = await apiInstance<(Mastodon.Status & { _pinned: boolean })[]>({ ...res,
body: res.body.map(status => appendRemote.status(status, page.remote_domain!))
}))
.catch(() => {})
}
if (!res && page.id) {
res = await apiInstance<Mastodon.Status[]>({
method: 'get', method: 'get',
url: `accounts/${page.id}/statuses`, url: `accounts/${page.id}/statuses`,
params: { params: {
pinned: 'true' only_media: true,
...params
} }
}) })
res1.body = res1.body.map(status => { }
status._pinned = true return res || reject
return status } else if (page.exclude_reblogs) {
}) if (pageParam && pageParam.hasOwnProperty('max_id')) {
const res2 = await apiInstance<Mastodon.Status[]>({ let res
method: 'get', if (page.remote_domain && page.remote_id) {
url: `accounts/${page.id}/statuses`, res = await apiGeneral<Mastodon.Status[]>({
params: { method: 'get',
exclude_replies: 'true' domain: page.remote_domain,
} url: `api/v1/accounts/${page.remote_id}/statuses`,
}) params: {
return { exclude_replies: true,
body: uniqBy([...res1.body, ...res2.body], 'id'), ...params
...(res2.links?.next && { links: { next: res2.links.next } }) }
})
.then(res => ({
...res,
body: res.body.map(status => appendRemote.status(status, page.remote_domain!))
}))
.catch(() => {})
} }
if (!res && page.id) {
res = await apiInstance<Mastodon.Status[]>({
method: 'get',
url: `accounts/${page.id}/statuses`,
params: {
exclude_replies: true,
...params
}
})
}
return res || reject
} else {
let res
if (page.remote_domain && page.remote_id) {
res = await apiGeneral<Mastodon.Status[]>({
method: 'get',
domain: page.remote_domain,
url: `api/v1/accounts/${page.remote_id}/statuses`,
params: { exclude_replies: true }
})
.then(res => ({
...res,
body: res.body.map(status => appendRemote.status(status, page.remote_domain!))
}))
.catch(() => {})
}
if (!res && page.id) {
const resPinned = await apiInstance<(Mastodon.Status & { _pinned: boolean })[]>({
method: 'get',
url: `accounts/${page.id}/statuses`,
params: { pinned: true }
}).then(res => ({
...res,
body: res.body.map(status => {
status._pinned = true
return status
})
}))
const resDefault = await apiInstance<Mastodon.Status[]>({
method: 'get',
url: `accounts/${page.id}/statuses`,
params: { exclude_replies: true }
})
return {
body: uniqBy([...resPinned.body, ...resDefault.body], 'id'),
links: resDefault.links
}
}
return res || reject
} }
} else { } else {
return apiInstance<Mastodon.Status[]>({ let res
method: 'get', if (page.remote_domain && page.remote_id) {
url: `accounts/${page.id}/statuses`, res = await apiGeneral<Mastodon.Status[]>({
params: { method: 'get',
...params, domain: page.remote_domain,
exclude_replies: false, url: `api/v1/accounts/${page.remote_id}/statuses`,
only_media: false params: {
} ...params,
}) exclude_replies: false,
only_media: false
}
})
.then(res => ({
...res,
body: res.body.map(status => appendRemote.status(status, page.remote_domain!))
}))
.catch(() => {})
}
if (!res && page.id) {
res = await apiInstance<Mastodon.Status[]>({
method: 'get',
url: `accounts/${page.id}/statuses`,
params: {
...params,
exclude_replies: false,
only_media: false
}
})
}
return res || reject
} }
case 'Hashtag': case 'Hashtag':

View File

@ -57,7 +57,7 @@ const queryFunction = async ({ queryKey, pageParam }: QueryFunctionContext<Query
}) })
return { return {
...res, ...res,
body: res.body.map(account => appendRemote.account(account)), body: res.body.map(account => appendRemote.account(account, domain)),
remoteData: true remoteData: true
} }
} else { } else {