From fd7c39e3bb8f6fa957106ff9a665f9345659fcad Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Fri, 10 Nov 2023 22:33:19 +0000 Subject: [PATCH] Profile tabs WIP --- src/view/screens/Profile.tsx | 432 ++++++++++++++++++----------------- 1 file changed, 224 insertions(+), 208 deletions(-) diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx index 945a8cc202..819404381e 100644 --- a/src/view/screens/Profile.tsx +++ b/src/view/screens/Profile.tsx @@ -1,4 +1,4 @@ -import React, {useEffect, useState} from 'react' +import React, {useEffect, useMemo, useState} from 'react' import {ActivityIndicator, StyleSheet, View} from 'react-native' import {observer} from 'mobx-react-lite' import {useFocusEffect} from '@react-navigation/native' @@ -8,9 +8,11 @@ import {ViewSelector, ViewSelectorHandle} from '../com/util/ViewSelector' import {CenteredView} from '../com/util/Views' import {ScreenHider} from 'view/com/util/moderation/ScreenHider' import {ProfileUiModel, Sections} from 'state/models/ui/profile' +import {Feed} from 'view/com/posts/Feed' import {useStores} from 'state/index' import {ProfileHeader} from '../com/profile/ProfileHeader' import {FeedSlice} from '../com/posts/FeedSlice' +import {PagerWithHeader} from 'view/com/pager/PagerWithHeader' import {ListCard} from 'view/com/lists/ListCard' import { PostFeedLoadingPlaceholder, @@ -29,9 +31,19 @@ import {FeedSourceModel} from 'state/models/content/feed-source' import {useSetTitle} from 'lib/hooks/useSetTitle' import {combinedDisplayName} from 'lib/strings/display-names' import {logger} from '#/logger' +import {useAnimatedScrollHandler} from 'react-native-reanimated' + +import {ProfileModel} from 'state/models/content/profile' +import {PostsFeedModel} from 'state/models/feeds/posts' +import {ActorFeedsModel} from 'state/models/lists/actor-feeds' +import {ListsListModel} from 'state/models/lists-list' + +const SECTION_TITLES_PROFILE = ['Posts', 'Posts & Replies', 'Media', 'Likes'] + import {Trans, msg} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {useSetMinimalShellMode} from '#/state/shell' +import {useSetDrawerSwipeDisabled, useSetMinimalShellMode} from '#/state/shell' +import {OnScrollCb} from '#/lib/hooks/useOnMainScroll' type Props = NativeStackScreenProps export const ProfileScreen = withAuthRequired( @@ -39,56 +51,117 @@ export const ProfileScreen = withAuthRequired( const store = useStores() const setMinimalShellMode = useSetMinimalShellMode() const {screen, track} = useAnalytics() + const [currentPage, setCurrentPage] = React.useState(0) const {_} = useLingui() const viewSelectorRef = React.useRef(null) + const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled() const name = route.params.name === 'me' ? store.me.did : route.params.name - useEffect(() => { - screen('Profile') - }, [screen]) + const profile = useMemo(() => { + const model = new ProfileModel(store, {actor: name}) + model.setup() // TODO + return model + }, [name, store]) - const [hasSetup, setHasSetup] = useState(false) - const uiState = React.useMemo( - () => new ProfileUiModel(store, {user: name}), - [name, store], - ) - useSetTitle(combinedDisplayName(uiState.profile)) + const postsFeed = useMemo(() => { + const model = new PostsFeedModel(store, 'author', { + actor: name, + limit: 10, + filter: 'posts_no_replies', + }) + return model + }, [name, store]) - const onSoftReset = React.useCallback(() => { - viewSelectorRef.current?.scrollToTop() - }, []) + const postsWithRepliesFeed = useMemo(() => { + const model = new PostsFeedModel(store, 'author', { + actor: name, + limit: 10, + filter: 'posts_with_replies', + }) + return model + }, [name, store]) - useEffect(() => { - setHasSetup(false) - }, [name]) + const postsWithMediaFeed = useMemo(() => { + const model = new PostsFeedModel(store, 'author', { + actor: name, + limit: 10, + filter: 'posts_with_media', + }) + return model + }, [name, store]) + + const likesFeed = useMemo(() => { + const model = new PostsFeedModel( + store, + 'likes', + { + actor: name, + limit: 10, + }, + { + isSimpleFeed: true, + }, + ) + return model + }, [name, store]) - // We don't need this to be reactive, so we can just register the listeners once useEffect(() => { - const listCleanup = uiState.lists.registerListeners() - return () => listCleanup() - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) + switch (currentPage) { + case 0: { + postsFeed.setup() + break + } + case 1: { + postsWithRepliesFeed.setup() + break + } + case 2: { + postsWithMediaFeed.setup() + break + } + case 3: { + likesFeed.setup() + break + } + } + }, [currentPage, postsFeed, postsWithRepliesFeed, postsWithMediaFeed, likesFeed]) + + /* + - todo + - feeds + - lists + */ useFocusEffect( React.useCallback(() => { - const softResetSub = store.onScreenSoftReset(onSoftReset) - let aborted = false setMinimalShellMode(false) - const feedCleanup = uiState.feed.registerListeners() - if (!hasSetup) { - uiState.setup().then(() => { - if (aborted) { - return - } - setHasSetup(true) - }) + const softResetSub = store.onScreenSoftReset(() => { + viewSelectorRef.current?.scrollToTop() + }) + return () => softResetSub.remove() + }, [store, viewSelectorRef, setMinimalShellMode]), + ) + + useFocusEffect( + React.useCallback(() => { + setDrawerSwipeDisabled(currentPage > 0) + return () => { + setDrawerSwipeDisabled(false) } + }, [setDrawerSwipeDisabled, currentPage]), + ) + + useFocusEffect( + React.useCallback(() => { + const subs = [] + subs.push(postsFeed.registerListeners()) + subs.push(postsWithRepliesFeed.registerListeners()) + subs.push(postsWithMediaFeed.registerListeners()) + subs.push(likesFeed.registerListeners()) return () => { - aborted = true - feedCleanup() - softResetSub.remove() + subs.forEach(unsub => unsub()) } - }, [store, onSoftReset, uiState, hasSetup, setMinimalShellMode]), + }, [postsFeed, postsWithRepliesFeed, postsWithMediaFeed, likesFeed]), ) // events @@ -97,205 +170,106 @@ export const ProfileScreen = withAuthRequired( const onPressCompose = React.useCallback(() => { track('ProfileScreen:PressCompose') const mention = - uiState.profile.handle === store.me.handle || - uiState.profile.handle === 'handle.invalid' + profile.handle === store.me.handle || + profile.handle === 'handle.invalid' ? undefined - : uiState.profile.handle + : profile.handle store.shell.openComposer({mention}) - }, [store, track, uiState]) - const onSelectView = React.useCallback( - (index: number) => { - uiState.setSelectedViewIndex(index) - }, - [uiState], - ) + }, [store, track, profile]) + const onRefresh = React.useCallback(() => { - uiState - .refresh() - .catch((err: any) => - logger.error('Failed to refresh user profile', {error: err}), - ) - }, [uiState]) - const onEndReached = React.useCallback(() => { - uiState.loadMore().catch((err: any) => - logger.error('Failed to load more entries in user profile', { - error: err, - }), - ) - }, [uiState]) + // TODO + }, []) + const onPressTryAgain = React.useCallback(() => { - uiState.setup() - }, [uiState]) + // TODO + }, []) + + const onPageSelected = React.useCallback( + i => { + setCurrentPage(i) + }, + [setCurrentPage], + ) // rendering // = const renderHeader = React.useCallback(() => { - if (!uiState) { - return - } return ( ) - }, [uiState, onRefresh, route.params.hideBackButton]) - - const Footer = React.useMemo(() => { - return uiState.showLoadingMoreFooter ? LoadingMoreFooter : undefined - }, [uiState.showLoadingMoreFooter]) - const renderItem = React.useCallback( - (item: any) => { - // if section is lists - if (uiState.selectedView === Sections.Lists) { - if (item === ProfileUiModel.LOADING_ITEM) { - return - } else if (item._reactKey === '__error__') { - return ( - - - - ) - } else if (item === ProfileUiModel.EMPTY_ITEM) { - return ( - - ) - } else { - return - } - // if section is custom algorithms - } else if (uiState.selectedView === Sections.CustomAlgorithms) { - if (item === ProfileUiModel.LOADING_ITEM) { - return - } else if (item._reactKey === '__error__') { - return ( - - - - ) - } else if (item === ProfileUiModel.EMPTY_ITEM) { - return ( - - ) - } else if (item instanceof FeedSourceModel) { - return ( - - ) - } - // if section is posts or posts & replies - } else { - if (item === ProfileUiModel.END_ITEM) { - return ( - - - end of feed - - - ) - } else if (item === ProfileUiModel.LOADING_ITEM) { - return - } else if (item._reactKey === '__error__') { - if (uiState.feed.isBlocking) { - return ( - - ) - } - if (uiState.feed.isBlockedBy) { - return ( - - ) - } - return ( - - - - ) - } else if (item === ProfileUiModel.EMPTY_ITEM) { - return ( - - ) - } else if (item instanceof PostsFeedSliceModel) { - return ( - - ) - } - } - return - }, - [ - onPressTryAgain, - uiState.selectedView, - uiState.profile.did, - uiState.feed.isBlocking, - uiState.feed.isBlockedBy, - ], - ) + }, [profile, onRefresh, route.params.hideBackButton]) return ( - {uiState.profile.hasError ? ( + moderation={profile.moderation.account}> + {profile.hasError ? ( - ) : uiState.profile.hasLoaded ? ( - ) : ( - {renderHeader()} + + + {({onScroll, headerHeight, isScrolledDown, scrollElRef}) => ( + + )} + {({onScroll, headerHeight, isScrolledDown, scrollElRef}) => ( + + )} + {({onScroll, headerHeight, isScrolledDown, scrollElRef}) => ( + + )} + {({onScroll, headerHeight, isScrolledDown, scrollElRef}) => ( + + )} + + )} ( + function FeedSectionImpl( + {feed, onScroll, headerHeight, isScrolledDown, scrollElRef}, + ref, + ) { + const hasNew = feed.hasNewLatest && !feed.isRefreshing + + const onScrollToTop = React.useCallback(() => { + scrollElRef.current?.scrollToOffset({offset: -headerHeight}) + feed.refresh() + }, [feed, scrollElRef, headerHeight]) + React.useImperativeHandle(ref, () => ({ + scrollToTop: onScrollToTop, + })) + + const renderPostsEmpty = React.useCallback(() => { + return + }, []) + + return ( + + + + ) + }, +) + function LoadingMoreFooter() { return (