Skip to content

Commit

Permalink
Refactor profile screen to use new pager and react-query (#1870)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
gaearon and pfrazee authored Nov 13, 2023
1 parent c3edde8 commit e193893
Show file tree
Hide file tree
Showing 9 changed files with 718 additions and 444 deletions.
88 changes: 88 additions & 0 deletions src/state/cache/profile-shadow.ts
Original file line number Diff line number Diff line change
@@ -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,
},
}
}
166 changes: 161 additions & 5 deletions src/state/queries/profile.ts
Original file line number Diff line number Diff line change
@@ -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,
})
},
})
}
15 changes: 10 additions & 5 deletions src/state/queries/resolve-uri.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
8 changes: 7 additions & 1 deletion src/view/com/posts/FeedErrorMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {logger} from '#/logger'
import {useModalControls} from '#/state/modals'
import {FeedDescriptor} from '#/state/queries/post-feed'
import {EmptyState} from '../util/EmptyState'
import {cleanError} from '#/lib/strings/errors'

enum KnownError {
Block,
Expand Down Expand Up @@ -69,7 +70,12 @@ export function FeedErrorMessage({
)
}

return <ErrorMessage message={error} onPressTryAgain={onPressTryAgain} />
return (
<ErrorMessage
message={cleanError(error)}
onPressTryAgain={onPressTryAgain}
/>
)
}

function FeedgenErrorMessage({
Expand Down
Loading

0 comments on commit e193893

Please sign in to comment.