feat(mobile): load archive entries (#2725)
* feat(mobile): load archive entries * only hydrate for view * leave note * fix type
This commit is contained in:
parent
3b1db30456
commit
d8a796d3ab
|
|
@ -59,24 +59,19 @@ export const EntryItemContextMenu = ({ id, children }: PropsWithChildren<{ id: s
|
|||
key="Star"
|
||||
onSelect={() => {
|
||||
if (isEntryStarred) {
|
||||
collectionSyncService.unstarEntry({
|
||||
createdAt: new Date().toISOString(),
|
||||
feedId: entry.feedId,
|
||||
entryId: id,
|
||||
view: 0,
|
||||
})
|
||||
collectionSyncService.unstarEntry(id)
|
||||
toast.info("Unstarred")
|
||||
} else {
|
||||
collectionSyncService.starEntry(
|
||||
{
|
||||
createdAt: new Date().toISOString(),
|
||||
feedId: entry.feedId,
|
||||
entryId: id,
|
||||
// TODO update view
|
||||
view: 0,
|
||||
},
|
||||
0,
|
||||
)
|
||||
if (!entry.feedId) {
|
||||
toast.error("Feed not found")
|
||||
return
|
||||
}
|
||||
collectionSyncService.starEntry({
|
||||
feedId: entry.feedId,
|
||||
entryId: id,
|
||||
// TODO update view
|
||||
view: 0,
|
||||
})
|
||||
toast.info("Starred")
|
||||
}
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,16 @@
|
|||
import { Link } from "expo-router"
|
||||
import { TouchableOpacity, View } from "react-native"
|
||||
import { Text, TouchableOpacity, View } from "react-native"
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context"
|
||||
|
||||
import { AddCuteReIcon } from "@/src/icons/add_cute_re"
|
||||
import { LayoutLeftbarOpenCuteReIcon } from "@/src/icons/layout_leftbar_open_cute_re"
|
||||
import { accentColor } from "@/src/theme/colors"
|
||||
|
||||
import { useFeedDrawer } from "../feed-drawer/atoms"
|
||||
import {
|
||||
setIsLoadingArchivedEntries,
|
||||
useFeedDrawer,
|
||||
useIsLoadingArchivedEntries,
|
||||
} from "../feed-drawer/atoms"
|
||||
|
||||
const useActionPadding = () => {
|
||||
const insets = useSafeAreaInsets()
|
||||
|
|
@ -42,3 +46,19 @@ export function HomeRightAction() {
|
|||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export function LoadArchiveButton() {
|
||||
const isLoadingArchivedEntries = useIsLoadingArchivedEntries()
|
||||
if (isLoadingArchivedEntries) return null
|
||||
return (
|
||||
<View className="items-center pt-2">
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
setIsLoadingArchivedEntries(true)
|
||||
}}
|
||||
>
|
||||
<Text className="text-label">Load archived entries</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ import { debouncedFetchEntryContentByStream } from "@/src/store/entry/store"
|
|||
|
||||
import { EntryItemContextMenu } from "../context-menu/entry"
|
||||
import { ViewSelector } from "../feed-drawer/view-selector"
|
||||
import { HomeLeftAction, HomeRightAction } from "./action"
|
||||
import { HomeLeftAction, HomeRightAction, LoadArchiveButton } from "./action"
|
||||
import { EntryListContentGrid } from "./entry-list-gird"
|
||||
|
||||
const headerHideableBottomHeight = 58
|
||||
|
|
@ -80,7 +80,7 @@ function EntryListContent({ entryIds }: { entryIds: string[] }) {
|
|||
const scrollY = useContext(NavigationContext)?.scrollY
|
||||
|
||||
const { colorScheme } = useColorScheme()
|
||||
const { fetchNextPage, isFetchingNextPage, refetch, isRefetching } = useFetchEntriesControls()
|
||||
const { fetchNextPage, isFetching, refetch, isRefetching } = useFetchEntriesControls()
|
||||
|
||||
const onScroll = useCallback(
|
||||
(e: NativeSyntheticEvent<NativeScrollEvent>) => {
|
||||
|
|
@ -95,6 +95,13 @@ function EntryListContent({ entryIds }: { entryIds: string[] }) {
|
|||
)
|
||||
|
||||
const tabBarHeight = useBottomTabBarHeight()
|
||||
|
||||
const ListFooterComponent = useMemo(
|
||||
() =>
|
||||
isFetching ? <EntryItemSkeleton /> : screenType === "feed" ? <LoadArchiveButton /> : null,
|
||||
[isFetching, screenType],
|
||||
)
|
||||
|
||||
return (
|
||||
<FlashList
|
||||
refreshControl={
|
||||
|
|
@ -128,7 +135,7 @@ function EntryListContent({ entryIds }: { entryIds: string[] }) {
|
|||
paddingBottom: tabBarHeight,
|
||||
}}
|
||||
ItemSeparatorComponent={ItemSeparator}
|
||||
ListFooterComponent={isFetchingNextPage ? <EntryItemSkeleton /> : null}
|
||||
ListFooterComponent={ListFooterComponent}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -103,6 +103,15 @@ const selectedTimelineAtom = atom<SelectedTimeline>({
|
|||
|
||||
const selectedFeedAtom = atom<SelectedFeed>(null)
|
||||
|
||||
const isLoadingArchivedEntriesAtom = atom(false)
|
||||
|
||||
export const useIsLoadingArchivedEntries = () => {
|
||||
return useAtomValue(isLoadingArchivedEntriesAtom)
|
||||
}
|
||||
export const setIsLoadingArchivedEntries = (isLoading: boolean) => {
|
||||
jotaiStore.set(isLoadingArchivedEntriesAtom, isLoading)
|
||||
}
|
||||
|
||||
export const EntryListContext = createContext<{ type: "timeline" | "feed" }>({ type: "timeline" })
|
||||
export const useEntryListContext = () => {
|
||||
return useContext(EntryListContext)
|
||||
|
|
@ -129,6 +138,7 @@ export function useSelectedView() {
|
|||
|
||||
function getFetchEntryPayload(
|
||||
selectedFeed: SelectedTimeline | SelectedFeed,
|
||||
isArchived = false,
|
||||
): FetchEntriesProps | null {
|
||||
if (!selectedFeed) {
|
||||
return null
|
||||
|
|
@ -158,6 +168,8 @@ function getFetchEntryPayload(
|
|||
}
|
||||
// No default
|
||||
}
|
||||
|
||||
payload.isArchived = isArchived
|
||||
return payload
|
||||
}
|
||||
|
||||
|
|
@ -167,8 +179,10 @@ export function useSelectedFeed() {
|
|||
const selectedTimeline = useAtomValue(selectedTimelineAtom)
|
||||
const selectedFeed = useAtomValue(selectedFeedAtom)
|
||||
|
||||
const isArchived = useIsLoadingArchivedEntries()
|
||||
const payload = getFetchEntryPayload(
|
||||
entryListContext.type === "feed" ? selectedFeed : selectedTimeline,
|
||||
entryListContext.type === "feed" ? isArchived : false,
|
||||
)
|
||||
usePrefetchEntries(payload)
|
||||
|
||||
|
|
@ -176,8 +190,15 @@ export function useSelectedFeed() {
|
|||
}
|
||||
|
||||
export function useFetchEntriesControls() {
|
||||
const entryListContext = useEntryListContext()
|
||||
|
||||
const selectedFeed = useSelectedFeed()
|
||||
const payload = getFetchEntryPayload(selectedFeed)
|
||||
const isArchived = useIsLoadingArchivedEntries()
|
||||
|
||||
const payload = getFetchEntryPayload(
|
||||
selectedFeed,
|
||||
entryListContext.type === "feed" ? isArchived : false,
|
||||
)
|
||||
return usePrefetchEntries(payload)
|
||||
}
|
||||
|
||||
|
|
@ -220,6 +241,7 @@ export const selectTimeline = (state: SelectedTimeline) => {
|
|||
|
||||
export const selectFeed = (state: SelectedFeed) => {
|
||||
jotaiStore.set(selectedFeedAtom, state)
|
||||
jotaiStore.set(isLoadingArchivedEntriesAtom, false)
|
||||
}
|
||||
|
||||
export const useViewDefinition = (view?: FeedViewType) => {
|
||||
|
|
|
|||
|
|
@ -34,7 +34,11 @@ class EntryServiceStatic implements Hydratable, Resetable {
|
|||
|
||||
async hydrate() {
|
||||
const entries = await db.query.entriesTable.findMany()
|
||||
entryActions.upsertManyInSession(entries.map((e) => dbStoreMorph.toEntryModel(e)))
|
||||
// TODO: Find a way to determine whether entry is archived, and then only hydrate unarchived entries
|
||||
entryActions.upsertManyInSession(
|
||||
entries.map((e) => dbStoreMorph.toEntryModel(e)),
|
||||
"view",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -48,7 +48,54 @@ export const useEntryStore = createZustandStore<EntryState>("entry")(() => defau
|
|||
|
||||
const immerSet = createImmerSetter(useEntryStore)
|
||||
|
||||
type UpsertPosition = "all" | "view" | "category" | "feed" | "inbox" | "list"
|
||||
|
||||
class EntryActions {
|
||||
private addEntryIdToView({
|
||||
draft,
|
||||
feedId,
|
||||
entryId,
|
||||
}: {
|
||||
draft: EntryState
|
||||
feedId?: FeedId | null
|
||||
entryId: EntryId
|
||||
}) {
|
||||
if (!feedId) return
|
||||
const subscription = getSubscription(feedId)
|
||||
if (typeof subscription?.view === "number") {
|
||||
draft.entryIdByView[subscription.view].add(entryId)
|
||||
}
|
||||
if (subscription?.category) {
|
||||
const entryIdSetByCategory = draft.entryIdByCategory[subscription.category]
|
||||
if (!entryIdSetByCategory) {
|
||||
draft.entryIdByCategory[subscription.category] = new Set([entryId])
|
||||
} else {
|
||||
entryIdSetByCategory.add(entryId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private addEntryIdToCategory({
|
||||
draft,
|
||||
feedId,
|
||||
entryId,
|
||||
}: {
|
||||
draft: EntryState
|
||||
feedId?: FeedId | null
|
||||
entryId: EntryId
|
||||
}) {
|
||||
if (!feedId) return
|
||||
const subscription = getSubscription(feedId)
|
||||
if (subscription?.category) {
|
||||
const entryIdSetByCategory = draft.entryIdByCategory[subscription.category]
|
||||
if (!entryIdSetByCategory) {
|
||||
draft.entryIdByCategory[subscription.category] = new Set([entryId])
|
||||
} else {
|
||||
entryIdSetByCategory.add(entryId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private addEntryIdToFeed({
|
||||
draft,
|
||||
feedId,
|
||||
|
|
@ -65,19 +112,6 @@ class EntryActions {
|
|||
} else {
|
||||
entryIdSetByFeed.add(entryId)
|
||||
}
|
||||
|
||||
const subscription = getSubscription(feedId)
|
||||
if (typeof subscription?.view === "number") {
|
||||
draft.entryIdByView[subscription.view].add(entryId)
|
||||
}
|
||||
if (subscription?.category) {
|
||||
const entryIdSetByCategory = draft.entryIdByCategory[subscription.category]
|
||||
if (!entryIdSetByCategory) {
|
||||
draft.entryIdByCategory[subscription.category] = new Set([entryId])
|
||||
} else {
|
||||
entryIdSetByCategory.add(entryId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private addEntryIdToInbox({
|
||||
|
|
@ -98,7 +132,7 @@ class EntryActions {
|
|||
}
|
||||
}
|
||||
|
||||
upsertManyInSession(entries: EntryModel[]) {
|
||||
upsertManyInSession(entries: EntryModel[], position: UpsertPosition) {
|
||||
if (entries.length === 0) return
|
||||
|
||||
immerSet((draft) => {
|
||||
|
|
@ -106,25 +140,45 @@ class EntryActions {
|
|||
draft.data[entry.id] = entry
|
||||
|
||||
const { feedId, inboxHandle } = entry
|
||||
this.addEntryIdToFeed({
|
||||
draft,
|
||||
feedId,
|
||||
entryId: entry.id,
|
||||
})
|
||||
if (position === "all" || position === "feed") {
|
||||
this.addEntryIdToFeed({
|
||||
draft,
|
||||
feedId,
|
||||
entryId: entry.id,
|
||||
})
|
||||
}
|
||||
|
||||
this.addEntryIdToInbox({
|
||||
draft,
|
||||
inboxHandle,
|
||||
entryId: entry.id,
|
||||
})
|
||||
if (position === "all" || position === "view") {
|
||||
this.addEntryIdToView({
|
||||
draft,
|
||||
feedId,
|
||||
entryId: entry.id,
|
||||
})
|
||||
}
|
||||
|
||||
if (position === "all" || position === "inbox") {
|
||||
this.addEntryIdToInbox({
|
||||
draft,
|
||||
inboxHandle,
|
||||
entryId: entry.id,
|
||||
})
|
||||
}
|
||||
|
||||
if (position === "all" || position === "category") {
|
||||
this.addEntryIdToCategory({
|
||||
draft,
|
||||
feedId,
|
||||
entryId: entry.id,
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async upsertMany(entries: EntryModel[]) {
|
||||
async upsertMany(entries: EntryModel[], position: UpsertPosition) {
|
||||
const tx = createTransaction()
|
||||
tx.store(() => {
|
||||
this.upsertManyInSession(entries)
|
||||
this.upsertManyInSession(entries, position)
|
||||
})
|
||||
|
||||
tx.persist(() => {
|
||||
|
|
@ -155,17 +209,26 @@ class EntryActions {
|
|||
await tx.run()
|
||||
}
|
||||
|
||||
resetByView({ view, entries }: { view: FeedViewType; entries: EntryModel[] }) {
|
||||
resetByView({ view, entries }: { view?: FeedViewType; entries: EntryModel[] }) {
|
||||
if (view === undefined) return
|
||||
immerSet((draft) => {
|
||||
draft.entryIdByView[view] = new Set(entries.map((e) => e.id))
|
||||
})
|
||||
}
|
||||
|
||||
resetByCategory({ category, entries }: { category: Category; entries: EntryModel[] }) {
|
||||
resetByCategory({ category, entries }: { category?: Category; entries: EntryModel[] }) {
|
||||
if (!category) return
|
||||
immerSet((draft) => {
|
||||
draft.entryIdByCategory[category] = new Set(entries.map((e) => e.id))
|
||||
})
|
||||
}
|
||||
|
||||
resetByFeed({ feedId, entries }: { feedId?: FeedId; entries: EntryModel[] }) {
|
||||
if (!feedId) return
|
||||
immerSet((draft) => {
|
||||
draft.entryIdByFeed[feedId] = new Set(entries.map((e) => e.id))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
class EntrySyncServices {
|
||||
|
|
@ -198,7 +261,20 @@ class EntrySyncServices {
|
|||
}
|
||||
}
|
||||
|
||||
await entryActions.upsertMany(entries)
|
||||
const position =
|
||||
params.view !== undefined
|
||||
? "view"
|
||||
: params.feedId
|
||||
? "feed"
|
||||
: params.feedIdList
|
||||
? "category"
|
||||
: params.inboxId
|
||||
? "inbox"
|
||||
: params.listId
|
||||
? "list"
|
||||
: "all"
|
||||
|
||||
await entryActions.upsertMany(entries, position)
|
||||
if (params.listId) {
|
||||
await listActions.addEntryIds({
|
||||
listId: params.listId,
|
||||
|
|
@ -208,16 +284,20 @@ class EntrySyncServices {
|
|||
|
||||
// After initial fetch, we can reset the state to prefer the entries data from the server
|
||||
if (!pageParam) {
|
||||
if (view !== undefined) {
|
||||
entryActions.resetByView({ view, entries })
|
||||
if (position === "view") {
|
||||
entryActions.resetByView({ view: params.view, entries })
|
||||
}
|
||||
|
||||
if (params.feedIdList && params.feedIdList.length > 0) {
|
||||
const category = getSubscription(params.feedIdList[0]!)?.category
|
||||
if (position === "category") {
|
||||
const category = getSubscription(params.feedIdList?.[0])?.category
|
||||
if (category) {
|
||||
entryActions.resetByCategory({ category, entries })
|
||||
}
|
||||
}
|
||||
|
||||
if (position === "feed") {
|
||||
entryActions.resetByFeed({ feedId: params.feedId, entries })
|
||||
}
|
||||
}
|
||||
|
||||
if (isCollection && res.data) {
|
||||
|
|
|
|||
|
|
@ -4,8 +4,9 @@ import type { SubscriptionModel } from "./store"
|
|||
import { useSubscriptionStore } from "./store"
|
||||
|
||||
const get = useSubscriptionStore.getState
|
||||
export const getSubscription = (id: string): SubscriptionModel | null => {
|
||||
return get().data[id] || null
|
||||
export const getSubscription = (id?: string): SubscriptionModel | undefined => {
|
||||
if (!id) return
|
||||
return get().data[id]
|
||||
}
|
||||
|
||||
export const getSubscriptionByView = (view: FeedViewType): string[] => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue