diff --git a/src/state/queries/feed.ts b/src/state/queries/feed.ts index 36555c1813..687403ef58 100644 --- a/src/state/queries/feed.ts +++ b/src/state/queries/feed.ts @@ -482,16 +482,19 @@ export function usePinnedFeedsInfos() { export type SavedFeedItem = | { type: 'feed' + key: string config: AppBskyActorDefs.SavedFeed view: AppBskyFeedDefs.GeneratorView } | { type: 'list' + key: string config: AppBskyActorDefs.SavedFeed view: AppBskyGraphDefs.ListView } | { type: 'timeline' + key: string config: AppBskyActorDefs.SavedFeed view: undefined } @@ -511,6 +514,10 @@ export function useSavedFeeds() { previousData || { // The likely count before we try to resolve them. count: savedItems.length, + counts: { + pinned: savedItems.filter(f => f.pinned).length, + unpinned: savedItems.filter(f => !f.pinned).length, + }, feeds: [], } ) @@ -557,11 +564,13 @@ export function useSavedFeeds() { precacheList(queryClient, list) }) + let counter = 0 const result: SavedFeedItem[] = [] for (let savedItem of savedItems) { if (savedItem.type === 'timeline') { result.push({ type: 'timeline', + key: ['timeline', counter++].join('-'), config: savedItem, view: undefined, }) @@ -570,6 +579,7 @@ export function useSavedFeeds() { if (resolvedFeed) { result.push({ type: 'feed', + key: [resolvedFeed.uri, counter++].join('-'), config: savedItem, view: resolvedFeed, }) @@ -579,6 +589,7 @@ export function useSavedFeeds() { if (resolvedList) { result.push({ type: 'list', + key: [resolvedList.uri, counter++].join('-'), config: savedItem, view: resolvedList, }) @@ -589,6 +600,10 @@ export function useSavedFeeds() { return { // By this point we know the real count. count: result.length, + counts: { + pinned: result.filter(f => f.config.pinned).length, + unpinned: result.filter(f => !f.config.pinned).length, + }, feeds: result, } }, diff --git a/src/view/screens/SavedFeeds.tsx b/src/view/screens/SavedFeeds.tsx index d79c7708c6..39780ba649 100644 --- a/src/view/screens/SavedFeeds.tsx +++ b/src/view/screens/SavedFeeds.tsx @@ -1,5 +1,5 @@ import React from 'react' -import {ActivityIndicator, Pressable, StyleSheet, View} from 'react-native' +import {Pressable, StyleSheet, View} from 'react-native' import {AppBskyActorDefs} from '@atproto/api' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {msg, Trans} from '@lingui/macro' @@ -9,12 +9,12 @@ import {NativeStackScreenProps} from '@react-navigation/native-stack' import {track} from '#/lib/analytics/analytics' import {logger} from '#/logger' +import {SavedFeedItem, useSavedFeeds} from '#/state/queries/feed' import { useOverwriteSavedFeedsMutation, usePreferencesQuery, useUpdateSavedFeedsMutation, } from '#/state/queries/preferences' -import {UsePreferencesQueryResponse} from '#/state/queries/preferences/types' import {useSetMinimalShellMode} from '#/state/shell' import {useAnalytics} from 'lib/analytics/analytics' import {useHaptics} from 'lib/haptics' @@ -31,6 +31,7 @@ import {CenteredView, ScrollView} from 'view/com/util/Views' import {NoFollowingFeed} from '#/screens/Feeds/NoFollowingFeed' import {NoSavedFeedsOfAnyType} from '#/screens/Feeds/NoSavedFeedsOfAnyType' import {atoms as a, useTheme} from '#/alf' +import * as FeedCard from '#/components/FeedCard' import {FilterTimeline_Stroke2_Corner0_Rounded as FilterTimeline} from '#/components/icons/FilterTimeline' import hairlineWidth = StyleSheet.hairlineWidth @@ -49,17 +50,23 @@ const HITSLOP_BOTTOM = { type Props = NativeStackScreenProps export function SavedFeeds({}: Props) { + const t = useTheme() const pal = usePalette('default') const {_} = useLingui() const {isMobile, isTabletOrDesktop} = useWebMediaQueries() const {screen} = useAnalytics() const setMinimalShellMode = useSetMinimalShellMode() const {data: preferences} = usePreferencesQuery() + const { + data: savedFeeds, + isPlaceholderData: isSavedFeedsPlaceholder, + // error: savedFeedsError, // TODO + } = useSavedFeeds() const { mutateAsync: overwriteSavedFeeds, variables: optimisticSavedFeedsResponse, reset: resetSaveFeedsMutationState, - error: savedFeedsError, + error: writeSavedFeedsError, } = useOverwriteSavedFeedsMutation() /* @@ -67,7 +74,7 @@ export function SavedFeeds({}: Props) { * data */ const currentFeeds = - optimisticSavedFeedsResponse && !savedFeedsError + optimisticSavedFeedsResponse && !writeSavedFeedsError ? optimisticSavedFeedsResponse : preferences?.savedFeeds || [] const pinnedFeeds = currentFeeds.filter(f => f.pinned) @@ -76,6 +83,13 @@ export function SavedFeeds({}: Props) { const noFollowingFeed = currentFeeds.every(f => f.type !== 'timeline') && !noSavedFeedsOfAnyType + const isSavedFeedsLoading = + isSavedFeedsPlaceholder && !savedFeeds?.feeds.length + const numPinnedFeedsPlaceholders = savedFeeds ? savedFeeds.counts.pinned : 4 + const numUnpinnedFeedsPlaceholders = savedFeeds + ? savedFeeds.counts.unpinned + : 4 + useFocusEffect( React.useCallback(() => { screen('SavedFeeds') @@ -104,34 +118,52 @@ export function SavedFeeds({}: Props) { - {preferences ? ( - !pinnedFeeds.length ? ( - - - You don't have any pinned feeds. - - - ) : ( - pinnedFeeds.map(f => ( - + {isSavedFeedsLoading ? ( + Array(numPinnedFeedsPlaceholders) + .fill(0) + .map((_, i) => ( + + + + + + )) - ) ) : ( - + <> + {savedFeeds && savedFeeds.feeds.length ? ( + savedFeeds.feeds + .filter(f => f.config.pinned) + .map(f => ( + + )) + ) : ( + + + You don't have any pinned feeds. + + + )} + )} {noFollowingFeed && ( @@ -145,34 +177,53 @@ export function SavedFeeds({}: Props) { Saved Feeds - {preferences ? ( - !unpinnedFeeds.length ? ( - - - You don't have any saved feeds. - - - ) : ( - unpinnedFeeds.map(f => ( - + + {isSavedFeedsLoading ? ( + Array(numUnpinnedFeedsPlaceholders) + .fill(0) + .map((_, i) => ( + + + + + + )) - ) ) : ( - + <> + {savedFeeds && savedFeeds.feeds.length ? ( + savedFeeds.feeds + .filter(f => !f.config.pinned) + .map(f => ( + + )) + ) : ( + + + You don't have any saved feeds. + + + )} + )} @@ -198,13 +249,11 @@ export function SavedFeeds({}: Props) { function ListItem({ feed, - isPinned, currentFeeds, overwriteSavedFeeds, resetSaveFeedsMutationState, }: { - feed: AppBskyActorDefs.SavedFeed - isPinned: boolean + feed: SavedFeedItem currentFeeds: AppBskyActorDefs.SavedFeed[] overwriteSavedFeeds: ReturnType< typeof useOverwriteSavedFeedsMutation @@ -212,14 +261,13 @@ function ListItem({ resetSaveFeedsMutationState: ReturnType< typeof useOverwriteSavedFeedsMutation >['reset'] - preferences: UsePreferencesQueryResponse }) { const pal = usePalette('default') const {_} = useLingui() const playHaptic = useHaptics() const {isPending: isUpdatePending, mutateAsync: updateSavedFeeds} = useUpdateSavedFeedsMutation() - const feedUri = feed.value + const isPinned = feed.config.pinned const onTogglePinned = React.useCallback(async () => { playHaptic() @@ -229,8 +277,8 @@ function ListItem({ await updateSavedFeeds([ { - ...feed, - pinned: !feed.pinned, + ...feed.config, + pinned: !feed.config.pinned, }, ]) } catch (e) { @@ -244,7 +292,7 @@ function ListItem({ const nextFeeds = currentFeeds.slice() const ids = currentFeeds.map(f => f.id) - const index = ids.indexOf(feed.id) + const index = ids.indexOf(feed.config.id) const nextIndex = index - 1 if (index === -1 || index === 0) return @@ -256,7 +304,7 @@ function ListItem({ try { await overwriteSavedFeeds(nextFeeds) track('CustomFeed:Reorder', { - uri: feed.value, + uri: feed.config.value, index: nextIndex, }) } catch (e) { @@ -270,7 +318,7 @@ function ListItem({ const nextFeeds = currentFeeds.slice() const ids = currentFeeds.map(f => f.id) - const index = ids.indexOf(feed.id) + const index = ids.indexOf(feed.config.id) const nextIndex = index + 1 if (index === -1 || index >= nextFeeds.length - 1) return @@ -282,7 +330,7 @@ function ListItem({ try { await overwriteSavedFeeds(nextFeeds) track('CustomFeed:Reorder', { - uri: feed.value, + uri: feed.config.value, index: nextIndex, }) } catch (e) { @@ -297,8 +345,8 @@ function ListItem({ ) : (