Skip to content

Commit

Permalink
More profile refactor updates (#1886)
Browse files Browse the repository at this point in the history
* Update the profile avatar lightbox

* Update profile editor

* Add dynamic likes tab

* Add dynamic feeds and lists tabs

* Implement lists listing on profiles
  • Loading branch information
pfrazee authored Nov 13, 2023
1 parent 8217761 commit a014637
Show file tree
Hide file tree
Showing 14 changed files with 432 additions and 84 deletions.
3 changes: 1 addition & 2 deletions src/state/modals/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import {AppBskyActorDefs, AppBskyGraphDefs, ModerationUI} from '@atproto/api'
import {StyleProp, ViewStyle, DeviceEventEmitter} from 'react-native'
import {Image as RNImage} from 'react-native-image-crop-picker'

import {ProfileModel} from '#/state/models/content/profile'
import {ImageModel} from '#/state/models/media/image'
import {GalleryModel} from '#/state/models/media/gallery'

Expand All @@ -20,7 +19,7 @@ export interface ConfirmModal {

export interface EditProfileModal {
name: 'edit-profile'
profileView: ProfileModel
profile: AppBskyActorDefs.ProfileViewDetailed
onUpdate?: () => void
}

Expand Down
5 changes: 2 additions & 3 deletions src/state/models/ui/shell.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import {AppBskyEmbedRecord} from '@atproto/api'
import {AppBskyEmbedRecord, AppBskyActorDefs} from '@atproto/api'
import {RootStoreModel} from '../root-store'
import {makeAutoObservable, runInAction} from 'mobx'
import {ProfileModel} from '../content/profile'
import {
shouldRequestEmailConfirmation,
setEmailConfirmationRequested,
Expand All @@ -18,7 +17,7 @@ interface LightboxModel {}

export class ProfileImageLightbox implements LightboxModel {
name = 'profile-image'
constructor(public profileView: ProfileModel) {
constructor(public profile: AppBskyActorDefs.ProfileViewDetailed) {
makeAutoObservable(this)
}
}
Expand Down
31 changes: 31 additions & 0 deletions src/state/queries/profile-extra-info.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {useQuery} from '@tanstack/react-query'
import {useSession} from '../session'

export const RQKEY = (did: string) => ['profile-extra-info', did]

/**
* Fetches some additional information for the profile screen which
* is not available in the API's ProfileView
*/
export function useProfileExtraInfoQuery(did: string) {
const {agent} = useSession()
return useQuery({
queryKey: RQKEY(did),
async queryFn() {
const [listsRes, feedsRes] = await Promise.all([
agent.app.bsky.graph.getLists({
actor: did,
limit: 1,
}),
agent.app.bsky.feed.getActorFeeds({
actor: did,
limit: 1,
}),
])
return {
hasLists: listsRes.data.lists.length > 0,
hasFeeds: feedsRes.data.feeds.length > 0,
}
},
})
}
99 changes: 96 additions & 3 deletions src/state/queries/profile.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
import {AtUri} from '@atproto/api'
import {useQuery, useMutation} from '@tanstack/react-query'
import {
AtUri,
AppBskyActorDefs,
AppBskyActorProfile,
AppBskyActorGetProfile,
BskyAgent,
} from '@atproto/api'
import {useQuery, useQueryClient, useMutation} from '@tanstack/react-query'
import {Image as RNImage} from 'react-native-image-crop-picker'
import {useSession} from '../session'
import {updateProfileShadow} from '../cache/profile-shadow'
import {uploadBlob} from '#/lib/api'
import {until} from '#/lib/async/until'

export const RQKEY = (did: string) => ['profile', did]

export function useProfileQuery({did}: {did: string | undefined}) {
const {agent} = useSession()
return useQuery({
queryKey: RQKEY(did),
queryKey: RQKEY(did || ''),
queryFn: async () => {
const res = await agent.getProfile({actor: did || ''})
return res.data
Expand All @@ -17,6 +26,77 @@ export function useProfileQuery({did}: {did: string | undefined}) {
})
}

interface ProfileUpdateParams {
profile: AppBskyActorDefs.ProfileView
updates: AppBskyActorProfile.Record
newUserAvatar: RNImage | undefined | null
newUserBanner: RNImage | undefined | null
}
export function useProfileUpdateMutation() {
const {agent} = useSession()
const queryClient = useQueryClient()
return useMutation<void, Error, ProfileUpdateParams>({
mutationFn: async ({profile, updates, newUserAvatar, newUserBanner}) => {
await agent.upsertProfile(async existing => {
existing = existing || {}
existing.displayName = updates.displayName
existing.description = updates.description
if (newUserAvatar) {
const res = await uploadBlob(
agent,
newUserAvatar.path,
newUserAvatar.mime,
)
existing.avatar = res.data.blob
} else if (newUserAvatar === null) {
existing.avatar = undefined
}
if (newUserBanner) {
const res = await uploadBlob(
agent,
newUserBanner.path,
newUserBanner.mime,
)
existing.banner = res.data.blob
} else if (newUserBanner === null) {
existing.banner = undefined
}
return existing
})
await whenAppViewReady(agent, profile.did, res => {
if (typeof newUserAvatar !== 'undefined') {
if (newUserAvatar === null && res.data.avatar) {
// url hasnt cleared yet
return false
} else if (res.data.avatar === profile.avatar) {
// url hasnt changed yet
return false
}
}
if (typeof newUserBanner !== 'undefined') {
if (newUserBanner === null && res.data.banner) {
// url hasnt cleared yet
return false
} else if (res.data.banner === profile.banner) {
// url hasnt changed yet
return false
}
}
return (
res.data.displayName === updates.displayName &&
res.data.description === updates.description
)
})
},
onSuccess(data, variables) {
// invalidate cache
queryClient.invalidateQueries({
queryKey: RQKEY(variables.profile.did),
})
},
})
}

export function useProfileFollowMutation() {
const {agent} = useSession()
return useMutation<{uri: string; cid: string}, Error, {did: string}>({
Expand Down Expand Up @@ -167,3 +247,16 @@ export function useProfileUnblockMutation() {
},
})
}

async function whenAppViewReady(
agent: BskyAgent,
actor: string,
fn: (res: AppBskyActorGetProfile.Response) => boolean,
) {
await until(
5, // 5 tries
1e3, // 1s delay between tries
fn,
() => agent.app.bsky.actor.getProfile({actor}),
)
}
2 changes: 1 addition & 1 deletion src/view/com/lightbox/Lightbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export const Lightbox = observer(function Lightbox() {
const opts = store.shell.activeLightbox as models.ProfileImageLightbox
return (
<ImageView
images={[{uri: opts.profileView.avatar || ''}]}
images={[{uri: opts.profile.avatar || ''}]}
initialImageIndex={0}
visible
onRequestClose={onClose}
Expand Down
4 changes: 2 additions & 2 deletions src/view/com/lightbox/Lightbox.web.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ export const Lightbox = observer(function Lightbox() {
let imgs: Img[] | undefined
if (activeLightbox instanceof models.ProfileImageLightbox) {
const opts = activeLightbox
if (opts.profileView.avatar) {
imgs = [{uri: opts.profileView.avatar}]
if (opts.profile.avatar) {
imgs = [{uri: opts.profile.avatar}]
}
} else if (activeLightbox instanceof models.ImagesLightbox) {
const opts = activeLightbox
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import {AppBskyGraphDefs as GraphDefs} from '@atproto/api'
import {ListCard} from './ListCard'
import {MyListsFilter, useMyListsQuery} from '#/state/queries/my-lists'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
import {Text} from '../util/text/Text'
import {useAnalytics} from 'lib/analytics/analytics'
import {usePalette} from 'lib/hooks/usePalette'
Expand All @@ -25,9 +24,8 @@ import {cleanError} from '#/lib/strings/errors'
const LOADING = {_reactKey: '__loading__'}
const EMPTY = {_reactKey: '__empty__'}
const ERROR_ITEM = {_reactKey: '__error__'}
const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'}

export function ListsList({
export function MyLists({
filter,
inline,
style,
Expand All @@ -42,7 +40,7 @@ export function ListsList({
}) {
const pal = usePalette('default')
const {track} = useAnalytics()
const [isRefreshing, setIsRefreshing] = React.useState(false)
const [isPTRing, setIsPTRing] = React.useState(false)
const {data, isFetching, isFetched, isError, error, refetch} =
useMyListsQuery(filter)
const isEmpty = !isFetching && !data?.length
Expand All @@ -67,14 +65,14 @@ export function ListsList({

const onRefresh = React.useCallback(async () => {
track('Lists:onRefresh')
setIsRefreshing(true)
setIsPTRing(true)
try {
await refetch()
} catch (err) {
logger.error('Failed to refresh lists', {error: err})
}
setIsRefreshing(false)
}, [refetch, track, setIsRefreshing])
setIsPTRing(false)
}, [refetch, track, setIsPTRing])

// rendering
// =
Expand All @@ -98,13 +96,6 @@ export function ListsList({
onPressTryAgain={onRefresh}
/>
)
} else if (item === LOAD_MORE_ERROR_ITEM) {
return (
<LoadMoreRetryBtn
label="There was an issue fetching your lists. Tap here to try again."
onPress={onRefresh}
/>
)
} else if (item === LOADING) {
return (
<View style={{padding: 20}}>
Expand Down Expand Up @@ -136,7 +127,7 @@ export function ListsList({
renderItem={renderItemInner}
refreshControl={
<RefreshControl
refreshing={isRefreshing}
refreshing={isPTRing}
onRefresh={onRefresh}
tintColor={pal.colors.text}
titleColor={pal.colors.text}
Expand Down
Loading

0 comments on commit a014637

Please sign in to comment.