Refactor profile screen to use new pager and react-query (#1870)
* Profile tabs WIP * Refactor the profile screen to use react-query (WIP) * Add the profile shadow and get follow, mute, and block working * Cleanup --------- Co-authored-by: Paul Frazee <pfrazee@gmail.com>
This commit is contained in:
parent
c3edde8ac6
commit
e1938931e0
9 changed files with 730 additions and 456 deletions
88
src/state/cache/profile-shadow.ts
vendored
Normal file
88
src/state/cache/profile-shadow.ts
vendored
Normal file
|
@ -0,0 +1,88 @@
|
|||
import {useEffect, useState, useCallback, useRef} from 'react'
|
||||
import EventEmitter from 'eventemitter3'
|
||||
import {AppBskyActorDefs} from '@atproto/api'
|
||||
|
||||
const emitter = new EventEmitter()
|
||||
|
||||
export interface ProfileShadow {
|
||||
followingUri: string | undefined
|
||||
muted: boolean | undefined
|
||||
blockingUri: string | undefined
|
||||
}
|
||||
|
||||
interface CacheEntry {
|
||||
ts: number
|
||||
value: ProfileShadow
|
||||
}
|
||||
|
||||
type ProfileView =
|
||||
| AppBskyActorDefs.ProfileView
|
||||
| AppBskyActorDefs.ProfileViewBasic
|
||||
| AppBskyActorDefs.ProfileViewDetailed
|
||||
|
||||
export function useProfileShadow<T extends ProfileView>(
|
||||
profile: T,
|
||||
ifAfterTS: number,
|
||||
): T {
|
||||
const [state, setState] = useState<CacheEntry>({
|
||||
ts: Date.now(),
|
||||
value: fromProfile(profile),
|
||||
})
|
||||
const firstRun = useRef(true)
|
||||
|
||||
const onUpdate = useCallback(
|
||||
(value: Partial<ProfileShadow>) => {
|
||||
setState(s => ({ts: Date.now(), value: {...s.value, ...value}}))
|
||||
},
|
||||
[setState],
|
||||
)
|
||||
|
||||
// react to shadow updates
|
||||
useEffect(() => {
|
||||
emitter.addListener(profile.did, onUpdate)
|
||||
return () => {
|
||||
emitter.removeListener(profile.did, onUpdate)
|
||||
}
|
||||
}, [profile.did, onUpdate])
|
||||
|
||||
// react to profile updates
|
||||
useEffect(() => {
|
||||
// dont fire on first run to avoid needless re-renders
|
||||
if (!firstRun.current) {
|
||||
setState({ts: Date.now(), value: fromProfile(profile)})
|
||||
}
|
||||
firstRun.current = false
|
||||
}, [profile])
|
||||
|
||||
return state.ts > ifAfterTS ? mergeShadow(profile, state.value) : profile
|
||||
}
|
||||
|
||||
export function updateProfileShadow(
|
||||
uri: string,
|
||||
value: Partial<ProfileShadow>,
|
||||
) {
|
||||
emitter.emit(uri, value)
|
||||
}
|
||||
|
||||
function fromProfile(profile: ProfileView): ProfileShadow {
|
||||
return {
|
||||
followingUri: profile.viewer?.following,
|
||||
muted: profile.viewer?.muted,
|
||||
blockingUri: profile.viewer?.blocking,
|
||||
}
|
||||
}
|
||||
|
||||
function mergeShadow<T extends ProfileView>(
|
||||
profile: T,
|
||||
shadow: ProfileShadow,
|
||||
): T {
|
||||
return {
|
||||
...profile,
|
||||
viewer: {
|
||||
...(profile.viewer || {}),
|
||||
following: shadow.followingUri,
|
||||
muted: shadow.muted,
|
||||
blocking: shadow.blockingUri,
|
||||
},
|
||||
}
|
||||
}
|
|
@ -1,13 +1,169 @@
|
|||
import {useQuery} from '@tanstack/react-query'
|
||||
import {AtUri} from '@atproto/api'
|
||||
import {useQuery, useMutation} from '@tanstack/react-query'
|
||||
import {useSession} from '../session'
|
||||
import {updateProfileShadow} from '../cache/profile-shadow'
|
||||
|
||||
import {PUBLIC_BSKY_AGENT} from '#/state/queries'
|
||||
export const RQKEY = (did: string) => ['profile', did]
|
||||
|
||||
export function useProfileQuery({did}: {did: string}) {
|
||||
export function useProfileQuery({did}: {did: string | undefined}) {
|
||||
const {agent} = useSession()
|
||||
return useQuery({
|
||||
queryKey: ['getProfile', did],
|
||||
queryKey: RQKEY(did),
|
||||
queryFn: async () => {
|
||||
const res = await PUBLIC_BSKY_AGENT.getProfile({actor: did})
|
||||
const res = await agent.getProfile({actor: did || ''})
|
||||
return res.data
|
||||
},
|
||||
enabled: !!did,
|
||||
})
|
||||
}
|
||||
|
||||
export function useProfileFollowMutation() {
|
||||
const {agent} = useSession()
|
||||
return useMutation<{uri: string; cid: string}, Error, {did: string}>({
|
||||
mutationFn: async ({did}) => {
|
||||
return await agent.follow(did)
|
||||
},
|
||||
onMutate(variables) {
|
||||
// optimstically update
|
||||
updateProfileShadow(variables.did, {
|
||||
followingUri: 'pending',
|
||||
})
|
||||
},
|
||||
onSuccess(data, variables) {
|
||||
// finalize
|
||||
updateProfileShadow(variables.did, {
|
||||
followingUri: data.uri,
|
||||
})
|
||||
},
|
||||
onError(error, variables) {
|
||||
// revert the optimistic update
|
||||
updateProfileShadow(variables.did, {
|
||||
followingUri: undefined,
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useProfileUnfollowMutation() {
|
||||
const {agent} = useSession()
|
||||
return useMutation<void, Error, {did: string; followUri: string}>({
|
||||
mutationFn: async ({followUri}) => {
|
||||
return await agent.deleteFollow(followUri)
|
||||
},
|
||||
onMutate(variables) {
|
||||
// optimstically update
|
||||
updateProfileShadow(variables.did, {
|
||||
followingUri: undefined,
|
||||
})
|
||||
},
|
||||
onError(error, variables) {
|
||||
// revert the optimistic update
|
||||
updateProfileShadow(variables.did, {
|
||||
followingUri: variables.followUri,
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useProfileMuteMutation() {
|
||||
const {agent} = useSession()
|
||||
return useMutation<void, Error, {did: string}>({
|
||||
mutationFn: async ({did}) => {
|
||||
await agent.mute(did)
|
||||
},
|
||||
onMutate(variables) {
|
||||
// optimstically update
|
||||
updateProfileShadow(variables.did, {
|
||||
muted: true,
|
||||
})
|
||||
},
|
||||
onError(error, variables) {
|
||||
// revert the optimistic update
|
||||
updateProfileShadow(variables.did, {
|
||||
muted: false,
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useProfileUnmuteMutation() {
|
||||
const {agent} = useSession()
|
||||
return useMutation<void, Error, {did: string}>({
|
||||
mutationFn: async ({did}) => {
|
||||
await agent.unmute(did)
|
||||
},
|
||||
onMutate(variables) {
|
||||
// optimstically update
|
||||
updateProfileShadow(variables.did, {
|
||||
muted: false,
|
||||
})
|
||||
},
|
||||
onError(error, variables) {
|
||||
// revert the optimistic update
|
||||
updateProfileShadow(variables.did, {
|
||||
muted: true,
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useProfileBlockMutation() {
|
||||
const {agent, currentAccount} = useSession()
|
||||
return useMutation<{uri: string; cid: string}, Error, {did: string}>({
|
||||
mutationFn: async ({did}) => {
|
||||
if (!currentAccount) {
|
||||
throw new Error('Not signed in')
|
||||
}
|
||||
return await agent.app.bsky.graph.block.create(
|
||||
{repo: currentAccount.did},
|
||||
{subject: did, createdAt: new Date().toISOString()},
|
||||
)
|
||||
},
|
||||
onMutate(variables) {
|
||||
// optimstically update
|
||||
updateProfileShadow(variables.did, {
|
||||
blockingUri: 'pending',
|
||||
})
|
||||
},
|
||||
onSuccess(data, variables) {
|
||||
// finalize
|
||||
updateProfileShadow(variables.did, {
|
||||
blockingUri: data.uri,
|
||||
})
|
||||
},
|
||||
onError(error, variables) {
|
||||
// revert the optimistic update
|
||||
updateProfileShadow(variables.did, {
|
||||
blockingUri: undefined,
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useProfileUnblockMutation() {
|
||||
const {agent, currentAccount} = useSession()
|
||||
return useMutation<void, Error, {did: string; blockUri: string}>({
|
||||
mutationFn: async ({blockUri}) => {
|
||||
if (!currentAccount) {
|
||||
throw new Error('Not signed in')
|
||||
}
|
||||
const {rkey} = new AtUri(blockUri)
|
||||
await agent.app.bsky.graph.block.delete({
|
||||
repo: currentAccount.did,
|
||||
rkey,
|
||||
})
|
||||
},
|
||||
onMutate(variables) {
|
||||
// optimstically update
|
||||
updateProfileShadow(variables.did, {
|
||||
blockingUri: undefined,
|
||||
})
|
||||
},
|
||||
onError(error, variables) {
|
||||
// revert the optimistic update
|
||||
updateProfileShadow(variables.did, {
|
||||
blockingUri: variables.blockUri,
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
|
@ -4,17 +4,22 @@ import {useSession} from '../session'
|
|||
|
||||
export const RQKEY = (uri: string) => ['resolved-uri', uri]
|
||||
|
||||
export function useResolveUriQuery(uri: string) {
|
||||
export function useResolveUriQuery(uri: string | undefined) {
|
||||
const {agent} = useSession()
|
||||
return useQuery<string | undefined, Error>({
|
||||
queryKey: RQKEY(uri),
|
||||
return useQuery<{uri: string; did: string}, Error>({
|
||||
queryKey: RQKEY(uri || ''),
|
||||
async queryFn() {
|
||||
const urip = new AtUri(uri)
|
||||
const urip = new AtUri(uri || '')
|
||||
if (!urip.host.startsWith('did:')) {
|
||||
const res = await agent.resolveHandle({handle: urip.host})
|
||||
urip.host = res.data.did
|
||||
}
|
||||
return urip.toString()
|
||||
return {did: urip.host, uri: urip.toString()}
|
||||
},
|
||||
enabled: !!uri,
|
||||
})
|
||||
}
|
||||
|
||||
export function useResolveDidQuery(didOrHandle: string | undefined) {
|
||||
return useResolveUriQuery(didOrHandle ? `at://${didOrHandle}/` : undefined)
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue