More profile refactor updates (#1886)
* Update the profile avatar lightbox * Update profile editor * Add dynamic likes tab * Add dynamic feeds and lists tabs * Implement lists listing on profileszio/stable
parent
8217761363
commit
a01463788d
|
@ -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'
|
||||
|
||||
|
@ -20,7 +19,7 @@ export interface ConfirmModal {
|
|||
|
||||
export interface EditProfileModal {
|
||||
name: 'edit-profile'
|
||||
profileView: ProfileModel
|
||||
profile: AppBskyActorDefs.ProfileViewDetailed
|
||||
onUpdate?: () => void
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
|
@ -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
|
||||
|
@ -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}>({
|
||||
|
@ -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}),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
// =
|
||||
|
@ -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}}>
|
||||
|
@ -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}
|
|
@ -0,0 +1,197 @@
|
|||
import React, {MutableRefObject} from 'react'
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Dimensions,
|
||||
RefreshControl,
|
||||
StyleProp,
|
||||
StyleSheet,
|
||||
View,
|
||||
ViewStyle,
|
||||
} from 'react-native'
|
||||
import {FlatList} from '../util/Views'
|
||||
import {ListCard} from './ListCard'
|
||||
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'
|
||||
import {useProfileListsQuery} from '#/state/queries/profile-lists'
|
||||
import {OnScrollHandler} from '#/lib/hooks/useOnMainScroll'
|
||||
import {logger} from '#/logger'
|
||||
import {Trans} from '@lingui/macro'
|
||||
import {cleanError} from '#/lib/strings/errors'
|
||||
import {useAnimatedScrollHandler} from 'react-native-reanimated'
|
||||
import {useTheme} from '#/lib/ThemeContext'
|
||||
|
||||
const LOADING = {_reactKey: '__loading__'}
|
||||
const EMPTY = {_reactKey: '__empty__'}
|
||||
const ERROR_ITEM = {_reactKey: '__error__'}
|
||||
const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'}
|
||||
|
||||
export function ProfileLists({
|
||||
did,
|
||||
scrollElRef,
|
||||
onScroll,
|
||||
scrollEventThrottle,
|
||||
headerOffset,
|
||||
style,
|
||||
testID,
|
||||
}: {
|
||||
did: string
|
||||
scrollElRef?: MutableRefObject<FlatList<any> | null>
|
||||
onScroll?: OnScrollHandler
|
||||
scrollEventThrottle?: number
|
||||
headerOffset: number
|
||||
style?: StyleProp<ViewStyle>
|
||||
testID?: string
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
const theme = useTheme()
|
||||
const {track} = useAnalytics()
|
||||
const [isPTRing, setIsPTRing] = React.useState(false)
|
||||
const {
|
||||
data,
|
||||
isFetching,
|
||||
isFetched,
|
||||
hasNextPage,
|
||||
fetchNextPage,
|
||||
isError,
|
||||
error,
|
||||
refetch,
|
||||
} = useProfileListsQuery(did)
|
||||
const isEmpty = !isFetching && !data?.pages[0]?.lists.length
|
||||
|
||||
const items = React.useMemo(() => {
|
||||
let items: any[] = []
|
||||
if (isError && isEmpty) {
|
||||
items = items.concat([ERROR_ITEM])
|
||||
}
|
||||
if (!isFetched && isFetching) {
|
||||
items = items.concat([LOADING])
|
||||
} else if (isEmpty) {
|
||||
items = items.concat([EMPTY])
|
||||
} else if (data?.pages) {
|
||||
for (const page of data?.pages) {
|
||||
items = items.concat(page.lists)
|
||||
}
|
||||
}
|
||||
if (isError && !isEmpty) {
|
||||
items = items.concat([LOAD_MORE_ERROR_ITEM])
|
||||
}
|
||||
return items
|
||||
}, [isError, isEmpty, isFetched, isFetching, data])
|
||||
|
||||
// events
|
||||
// =
|
||||
|
||||
const onRefresh = React.useCallback(async () => {
|
||||
track('Lists:onRefresh')
|
||||
setIsPTRing(true)
|
||||
try {
|
||||
await refetch()
|
||||
} catch (err) {
|
||||
logger.error('Failed to refresh lists', {error: err})
|
||||
}
|
||||
setIsPTRing(false)
|
||||
}, [refetch, track, setIsPTRing])
|
||||
|
||||
const onEndReached = React.useCallback(async () => {
|
||||
if (isFetching || !hasNextPage || isError) return
|
||||
|
||||
track('Lists:onEndReached')
|
||||
try {
|
||||
await fetchNextPage()
|
||||
} catch (err) {
|
||||
logger.error('Failed to load more lists', {error: err})
|
||||
}
|
||||
}, [isFetching, hasNextPage, isError, fetchNextPage, track])
|
||||
|
||||
const onPressRetryLoadMore = React.useCallback(() => {
|
||||
fetchNextPage()
|
||||
}, [fetchNextPage])
|
||||
|
||||
// rendering
|
||||
// =
|
||||
|
||||
const renderItemInner = React.useCallback(
|
||||
({item}: {item: any}) => {
|
||||
if (item === EMPTY) {
|
||||
return (
|
||||
<View
|
||||
testID="listsEmpty"
|
||||
style={[{padding: 18, borderTopWidth: 1}, pal.border]}>
|
||||
<Text style={pal.textLight}>
|
||||
<Trans>You have no lists.</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
} else if (item === ERROR_ITEM) {
|
||||
return (
|
||||
<ErrorMessage message={cleanError(error)} onPressTryAgain={refetch} />
|
||||
)
|
||||
} else if (item === LOAD_MORE_ERROR_ITEM) {
|
||||
return (
|
||||
<LoadMoreRetryBtn
|
||||
label="There was an issue fetching your lists. Tap here to try again."
|
||||
onPress={onPressRetryLoadMore}
|
||||
/>
|
||||
)
|
||||
} else if (item === LOADING) {
|
||||
return (
|
||||
<View style={{padding: 20}}>
|
||||
<ActivityIndicator />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<ListCard
|
||||
list={item}
|
||||
testID={`list-${item.name}`}
|
||||
style={styles.item}
|
||||
/>
|
||||
)
|
||||
},
|
||||
[error, refetch, onPressRetryLoadMore, pal],
|
||||
)
|
||||
|
||||
const scrollHandler = useAnimatedScrollHandler(onScroll || {})
|
||||
return (
|
||||
<View testID={testID} style={style}>
|
||||
<FlatList
|
||||
testID={testID ? `${testID}-flatlist` : undefined}
|
||||
ref={scrollElRef}
|
||||
data={items}
|
||||
keyExtractor={(item: any) => item._reactKey}
|
||||
renderItem={renderItemInner}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={isPTRing}
|
||||
onRefresh={onRefresh}
|
||||
tintColor={pal.colors.text}
|
||||
titleColor={pal.colors.text}
|
||||
progressViewOffset={headerOffset}
|
||||
/>
|
||||
}
|
||||
contentContainerStyle={{
|
||||
minHeight: Dimensions.get('window').height * 1.5,
|
||||
}}
|
||||
style={{paddingTop: headerOffset}}
|
||||
onScroll={onScroll != null ? scrollHandler : undefined}
|
||||
scrollEventThrottle={scrollEventThrottle}
|
||||
indicatorStyle={theme.colorScheme === 'dark' ? 'white' : 'black'}
|
||||
removeClippedSubviews={true}
|
||||
contentOffset={{x: 0, y: headerOffset * -1}}
|
||||
// @ts-ignore our .web version only -prf
|
||||
desktopFixedHeight
|
||||
onEndReached={onEndReached}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
item: {
|
||||
paddingHorizontal: 18,
|
||||
paddingVertical: 4,
|
||||
},
|
||||
})
|
|
@ -11,9 +11,9 @@ import {
|
|||
} from 'react-native'
|
||||
import LinearGradient from 'react-native-linear-gradient'
|
||||
import {Image as RNImage} from 'react-native-image-crop-picker'
|
||||
import {AppBskyActorDefs} from '@atproto/api'
|
||||
import {Text} from '../util/text/Text'
|
||||
import {ErrorMessage} from '../util/error/ErrorMessage'
|
||||
import {ProfileModel} from 'state/models/content/profile'
|
||||
import {s, colors, gradients} from 'lib/styles'
|
||||
import {enforceLen} from 'lib/strings/helpers'
|
||||
import {MAX_DISPLAY_NAME, MAX_DESCRIPTION} from 'lib/constants'
|
||||
|
@ -23,12 +23,14 @@ import {EditableUserAvatar} from '../util/UserAvatar'
|
|||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {useTheme} from 'lib/ThemeContext'
|
||||
import {useAnalytics} from 'lib/analytics/analytics'
|
||||
import {cleanError, isNetworkError} from 'lib/strings/errors'
|
||||
import {cleanError} from 'lib/strings/errors'
|
||||
import Animated, {FadeOut} from 'react-native-reanimated'
|
||||
import {isWeb} from 'platform/detection'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
import {useProfileUpdateMutation} from '#/state/queries/profile'
|
||||
import {logger} from '#/logger'
|
||||
|
||||
const AnimatedTouchableOpacity =
|
||||
Animated.createAnimatedComponent(TouchableOpacity)
|
||||
|
@ -36,31 +38,30 @@ const AnimatedTouchableOpacity =
|
|||
export const snapPoints = ['fullscreen']
|
||||
|
||||
export function Component({
|
||||
profileView,
|
||||
profile,
|
||||
onUpdate,
|
||||
}: {
|
||||
profileView: ProfileModel
|
||||
profile: AppBskyActorDefs.ProfileViewDetailed
|
||||
onUpdate?: () => void
|
||||
}) {
|
||||
const [error, setError] = useState<string>('')
|
||||
const pal = usePalette('default')
|
||||
const theme = useTheme()
|
||||
const {track} = useAnalytics()
|
||||
const {_} = useLingui()
|
||||
const {closeModal} = useModalControls()
|
||||
|
||||
const [isProcessing, setProcessing] = useState<boolean>(false)
|
||||
const updateMutation = useProfileUpdateMutation()
|
||||
const [imageError, setImageError] = useState<string>('')
|
||||
const [displayName, setDisplayName] = useState<string>(
|
||||
profileView.displayName || '',
|
||||
profile.displayName || '',
|
||||
)
|
||||
const [description, setDescription] = useState<string>(
|
||||
profileView.description || '',
|
||||
profile.description || '',
|
||||
)
|
||||
const [userBanner, setUserBanner] = useState<string | undefined | null>(
|
||||
profileView.banner,
|
||||
profile.banner,
|
||||
)
|
||||
const [userAvatar, setUserAvatar] = useState<string | undefined | null>(
|
||||
profileView.avatar,
|
||||
profile.avatar,
|
||||
)
|
||||
const [newUserBanner, setNewUserBanner] = useState<
|
||||
RNImage | undefined | null
|
||||
|
@ -73,6 +74,7 @@ export function Component({
|
|||
}
|
||||
const onSelectNewAvatar = useCallback(
|
||||
async (img: RNImage | null) => {
|
||||
setImageError('')
|
||||
if (img === null) {
|
||||
setNewUserAvatar(null)
|
||||
setUserAvatar(null)
|
||||
|
@ -84,14 +86,15 @@ export function Component({
|
|||
setNewUserAvatar(finalImg)
|
||||
setUserAvatar(finalImg.path)
|
||||
} catch (e: any) {
|
||||
setError(cleanError(e))
|
||||
setImageError(cleanError(e))
|
||||
}
|
||||
},
|
||||
[track, setNewUserAvatar, setUserAvatar, setError],
|
||||
[track, setNewUserAvatar, setUserAvatar, setImageError],
|
||||
)
|
||||
|
||||
const onSelectNewBanner = useCallback(
|
||||
async (img: RNImage | null) => {
|
||||
setImageError('')
|
||||
if (!img) {
|
||||
setNewUserBanner(null)
|
||||
setUserBanner(null)
|
||||
|
@ -103,52 +106,42 @@ export function Component({
|
|||
setNewUserBanner(finalImg)
|
||||
setUserBanner(finalImg.path)
|
||||
} catch (e: any) {
|
||||
setError(cleanError(e))
|
||||
setImageError(cleanError(e))
|
||||
}
|
||||
},
|
||||
[track, setNewUserBanner, setUserBanner, setError],
|
||||
[track, setNewUserBanner, setUserBanner, setImageError],
|
||||
)
|
||||
|
||||
const onPressSave = useCallback(async () => {
|
||||
track('EditProfile:Save')
|
||||
setProcessing(true)
|
||||
if (error) {
|
||||
setError('')
|
||||
}
|
||||
setImageError('')
|
||||
try {
|
||||
await profileView.updateProfile(
|
||||
{
|
||||
await updateMutation.mutateAsync({
|
||||
profile,
|
||||
updates: {
|
||||
displayName,
|
||||
description,
|
||||
},
|
||||
newUserAvatar,
|
||||
newUserBanner,
|
||||
)
|
||||
})
|
||||
Toast.show('Profile updated')
|
||||
onUpdate?.()
|
||||
closeModal()
|
||||
} catch (e: any) {
|
||||
if (isNetworkError(e)) {
|
||||
setError(
|
||||
'Failed to save your profile. Check your internet connection and try again.',
|
||||
)
|
||||
} else {
|
||||
setError(cleanError(e))
|
||||
}
|
||||
logger.error('Failed to update user profile', {error: String(e)})
|
||||
}
|
||||
setProcessing(false)
|
||||
}, [
|
||||
track,
|
||||
setProcessing,
|
||||
setError,
|
||||
error,
|
||||
profileView,
|
||||
updateMutation,
|
||||
profile,
|
||||
onUpdate,
|
||||
closeModal,
|
||||
displayName,
|
||||
description,
|
||||
newUserAvatar,
|
||||
newUserBanner,
|
||||
setImageError,
|
||||
])
|
||||
|
||||
return (
|
||||
|
@ -170,9 +163,14 @@ export function Component({
|
|||
/>
|
||||
</View>
|
||||
</View>
|
||||
{error !== '' && (
|
||||
{updateMutation.isError && (
|
||||
<View style={styles.errorContainer}>
|
||||
<ErrorMessage message={error} />
|
||||
<ErrorMessage message={cleanError(updateMutation.error)} />
|
||||
</View>
|
||||
)}
|
||||
{imageError !== '' && (
|
||||
<View style={styles.errorContainer}>
|
||||
<ErrorMessage message={imageError} />
|
||||
</View>
|
||||
)}
|
||||
<View style={styles.form}>
|
||||
|
@ -212,7 +210,7 @@ export function Component({
|
|||
accessibilityHint="Edit your profile description"
|
||||
/>
|
||||
</View>
|
||||
{isProcessing ? (
|
||||
{updateMutation.isPending ? (
|
||||
<View style={[styles.btn, s.mt10, {backgroundColor: colors.gray2}]}>
|
||||
<ActivityIndicator />
|
||||
</View>
|
||||
|
@ -235,7 +233,7 @@ export function Component({
|
|||
</LinearGradient>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
{!isProcessing && (
|
||||
{!updateMutation.isPending && (
|
||||
<AnimatedTouchableOpacity
|
||||
exiting={!isWeb ? FadeOut : undefined}
|
||||
testID="editProfileCancelBtn"
|
||||
|
|
|
@ -3,7 +3,7 @@ import {ActivityIndicator, StyleSheet, View} from 'react-native'
|
|||
import {AppBskyGraphDefs as GraphDefs} from '@atproto/api'
|
||||
import {Text} from '../util/text/Text'
|
||||
import {UserAvatar} from '../util/UserAvatar'
|
||||
import {ListsList} from '../lists/ListsList'
|
||||
import {MyLists} from '../lists/MyLists'
|
||||
import {Button} from '../util/forms/Button'
|
||||
import * as Toast from '../util/Toast'
|
||||
import {sanitizeDisplayName} from 'lib/strings/display-names'
|
||||
|
@ -51,7 +51,7 @@ export function Component({
|
|||
<Text style={[styles.title, pal.text]}>
|
||||
<Trans>Update {displayName} in Lists</Trans>
|
||||
</Text>
|
||||
<ListsList
|
||||
<MyLists
|
||||
filter="all"
|
||||
inline
|
||||
renderItem={(list, index) => (
|
||||
|
|
|
@ -197,7 +197,7 @@ function ProfileHeaderLoaded({
|
|||
track('ProfileHeader:EditProfileButtonClicked')
|
||||
openModal({
|
||||
name: 'edit-profile',
|
||||
profileView: profile,
|
||||
profile,
|
||||
})
|
||||
}, [track, openModal, profile])
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
|||
import {AtUri} from '@atproto/api'
|
||||
import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
|
||||
import {withAuthRequired} from 'view/com/auth/withAuthRequired'
|
||||
import {ListsList} from 'view/com/lists/ListsList'
|
||||
import {MyLists} from '#/view/com/lists/MyLists'
|
||||
import {Text} from 'view/com/util/text/Text'
|
||||
import {Button} from 'view/com/util/forms/Button'
|
||||
import {NavigationProp} from 'lib/routes/types'
|
||||
|
@ -79,7 +79,7 @@ export const ListsScreen = withAuthRequired(
|
|||
</Button>
|
||||
</View>
|
||||
</SimpleViewHeader>
|
||||
<ListsList filter="curate" style={s.flexGrow1} />
|
||||
<MyLists filter="curate" style={s.flexGrow1} />
|
||||
</View>
|
||||
)
|
||||
},
|
||||
|
|
|
@ -5,7 +5,7 @@ import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
|||
import {AtUri} from '@atproto/api'
|
||||
import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
|
||||
import {withAuthRequired} from 'view/com/auth/withAuthRequired'
|
||||
import {ListsList} from 'view/com/lists/ListsList'
|
||||
import {MyLists} from '#/view/com/lists/MyLists'
|
||||
import {Text} from 'view/com/util/text/Text'
|
||||
import {Button} from 'view/com/util/forms/Button'
|
||||
import {NavigationProp} from 'lib/routes/types'
|
||||
|
@ -79,7 +79,7 @@ export const ModerationModlistsScreen = withAuthRequired(
|
|||
</Button>
|
||||
</View>
|
||||
</SimpleViewHeader>
|
||||
<ListsList filter="mod" style={s.flexGrow1} />
|
||||
<MyLists filter="mod" style={s.flexGrow1} />
|
||||
</View>
|
||||
)
|
||||
},
|
||||
|
|
|
@ -10,6 +10,7 @@ import {ViewSelectorHandle} from '../com/util/ViewSelector'
|
|||
import {CenteredView} from '../com/util/Views'
|
||||
import {ScreenHider} from 'view/com/util/moderation/ScreenHider'
|
||||
import {Feed} from 'view/com/posts/Feed'
|
||||
import {ProfileLists} from '../com/lists/ProfileLists'
|
||||
import {useStores} from 'state/index'
|
||||
import {ProfileHeader} from '../com/profile/ProfileHeader'
|
||||
import {PagerWithHeader} from 'view/com/pager/PagerWithHeader'
|
||||
|
@ -28,11 +29,10 @@ import {useProfileQuery} from '#/state/queries/profile'
|
|||
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
||||
import {useSession} from '#/state/session'
|
||||
import {useModerationOpts} from '#/state/queries/preferences'
|
||||
import {useProfileExtraInfoQuery} from '#/state/queries/profile-extra-info'
|
||||
import {useSetDrawerSwipeDisabled, useSetMinimalShellMode} from '#/state/shell'
|
||||
import {cleanError} from '#/lib/strings/errors'
|
||||
|
||||
const SECTION_TITLES_PROFILE = ['Posts', 'Posts & Replies', 'Media', 'Likes']
|
||||
|
||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'Profile'>
|
||||
export const ProfileScreen = withAuthRequired(function ProfileScreenImpl({
|
||||
route,
|
||||
|
@ -129,6 +129,7 @@ function ProfileScreenLoaded({
|
|||
const {_} = useLingui()
|
||||
const viewSelectorRef = React.useRef<ViewSelectorHandle>(null)
|
||||
const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled()
|
||||
const extraInfoQuery = useProfileExtraInfoQuery(profile.did)
|
||||
|
||||
useSetTitle(combinedDisplayName(profile))
|
||||
|
||||
|
@ -137,6 +138,21 @@ function ProfileScreenLoaded({
|
|||
[profile, moderationOpts],
|
||||
)
|
||||
|
||||
const isMe = profile.did === currentAccount?.did
|
||||
const showLikesTab = isMe
|
||||
const showFeedsTab = isMe || extraInfoQuery.data?.hasFeeds
|
||||
const showListsTab = isMe || extraInfoQuery.data?.hasLists
|
||||
const sectionTitles = useMemo<string[]>(() => {
|
||||
return [
|
||||
'Posts',
|
||||
'Posts & Replies',
|
||||
'Media',
|
||||
showLikesTab ? 'Likes' : undefined,
|
||||
showFeedsTab ? 'Feeds' : undefined,
|
||||
showListsTab ? 'Lists' : undefined,
|
||||
].filter(Boolean) as string[]
|
||||
}, [showLikesTab, showFeedsTab, showListsTab])
|
||||
|
||||
/*
|
||||
- todo
|
||||
- feeds
|
||||
|
@ -204,7 +220,7 @@ function ProfileScreenLoaded({
|
|||
moderation={moderation.account}>
|
||||
<PagerWithHeader
|
||||
isHeaderReady={true}
|
||||
items={SECTION_TITLES_PROFILE}
|
||||
items={sectionTitles}
|
||||
onPageSelected={onPageSelected}
|
||||
renderHeader={renderHeader}>
|
||||
{({onScroll, headerHeight, isScrolledDown, scrollElRef}) => (
|
||||
|
@ -237,16 +253,40 @@ function ProfileScreenLoaded({
|
|||
scrollElRef={scrollElRef}
|
||||
/>
|
||||
)}
|
||||
{({onScroll, headerHeight, isScrolledDown, scrollElRef}) => (
|
||||
<FeedSection
|
||||
ref={null}
|
||||
feed={`likes|${profile.did}`}
|
||||
onScroll={onScroll}
|
||||
headerHeight={headerHeight}
|
||||
isScrolledDown={isScrolledDown}
|
||||
scrollElRef={scrollElRef}
|
||||
/>
|
||||
)}
|
||||
{showLikesTab
|
||||
? ({onScroll, headerHeight, isScrolledDown, scrollElRef}) => (
|
||||
<FeedSection
|
||||
ref={null}
|
||||
feed={`likes|${profile.did}`}
|
||||
onScroll={onScroll}
|
||||
headerHeight={headerHeight}
|
||||
isScrolledDown={isScrolledDown}
|
||||
scrollElRef={scrollElRef}
|
||||
/>
|
||||
)
|
||||
: null}
|
||||
{showFeedsTab
|
||||
? ({onScroll, headerHeight, scrollElRef}) => (
|
||||
<ProfileLists // TODO put feeds here, using this temporarily to avoid bugs
|
||||
did={profile.did}
|
||||
scrollElRef={scrollElRef}
|
||||
onScroll={onScroll}
|
||||
scrollEventThrottle={1}
|
||||
headerOffset={headerHeight}
|
||||
/>
|
||||
)
|
||||
: null}
|
||||
{showListsTab
|
||||
? ({onScroll, headerHeight, scrollElRef}) => (
|
||||
<ProfileLists
|
||||
did={profile.did}
|
||||
scrollElRef={scrollElRef}
|
||||
onScroll={onScroll}
|
||||
scrollEventThrottle={1}
|
||||
headerOffset={headerHeight}
|
||||
/>
|
||||
)
|
||||
: null}
|
||||
</PagerWithHeader>
|
||||
<FAB
|
||||
testID="composeFAB"
|
||||
|
|
Loading…
Reference in New Issue