Skip to content

Commit

Permalink
Add the profile shadow and get follow, mute, and block working
Browse files Browse the repository at this point in the history
  • Loading branch information
pfrazee committed Nov 13, 2023
1 parent ef2d504 commit 94b180e
Show file tree
Hide file tree
Showing 6 changed files with 198 additions and 67 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,
},
}
}
95 changes: 72 additions & 23 deletions src/state/queries/profile.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {AtUri} from '@atproto/api'
import {useQuery, useQueryClient, useMutation} from '@tanstack/react-query'
import {useQuery, useMutation} from '@tanstack/react-query'
import {useSession} from '../session'
import {updateProfileShadow} from '../cache/profile-shadow'

export function useProfileQuery({did}: {did: string | undefined}) {
const {agent} = useSession()
Expand All @@ -16,67 +17,96 @@ export function useProfileQuery({did}: {did: string | undefined}) {

export function useProfileFollowMutation() {
const {agent} = useSession()
const queryClient = useQueryClient()
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) {
queryClient.invalidateQueries({
queryKey: ['profile', variables.did],
// 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()
const queryClient = useQueryClient()
return useMutation<void, Error, {did: string; followUri: string}>({
mutationFn: async ({followUri}) => {
return await agent.deleteFollow(followUri)
},
onSuccess(data, variables) {
queryClient.invalidateQueries({
queryKey: ['profile', variables.did],
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()
const queryClient = useQueryClient()
return useMutation<void, Error, {did: string}>({
mutationFn: async ({did}) => {
await agent.mute(did)
},
onSuccess(data, variables) {
queryClient.invalidateQueries({
queryKey: ['profile', variables.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()
const queryClient = useQueryClient()
return useMutation<void, Error, {did: string}>({
mutationFn: async ({did}) => {
await agent.unmute(did)
},
onSuccess(data, variables) {
queryClient.invalidateQueries({
queryKey: ['profile', variables.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()
const queryClient = useQueryClient()
return useMutation<{uri: string; cid: string}, Error, {did: string}>({
mutationFn: async ({did}) => {
if (!currentAccount) {
Expand All @@ -87,17 +117,29 @@ export function useProfileBlockMutation() {
{subject: did, createdAt: new Date().toISOString()},
)
},
onMutate(variables) {
// optimstically update
updateProfileShadow(variables.did, {
blockingUri: 'pending',
})
},
onSuccess(data, variables) {
queryClient.invalidateQueries({
queryKey: ['profile', variables.did],
// 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()
const queryClient = useQueryClient()
return useMutation<void, Error, {did: string; blockUri: string}>({
mutationFn: async ({blockUri}) => {
if (!currentAccount) {
Expand All @@ -109,9 +151,16 @@ export function useProfileUnblockMutation() {
rkey,
})
},
onSuccess(data, variables) {
queryClient.invalidateQueries({
queryKey: ['profile', variables.did],
onMutate(variables) {
// optimstically update
updateProfileShadow(variables.did, {
blockingUri: undefined,
})
},
onError(error, variables) {
// revert the optimistic update
updateProfileShadow(variables.did, {
blockingUri: variables.blockUri,
})
},
})
Expand Down
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 94b180e

Please sign in to comment.