diff --git a/apps/mobile/src/modules/context-menu/entry.tsx b/apps/mobile/src/modules/context-menu/entry.tsx
index bdea0258cc..3bd809729f 100644
--- a/apps/mobile/src/modules/context-menu/entry.tsx
+++ b/apps/mobile/src/modules/context-menu/entry.tsx
@@ -59,24 +59,19 @@ export const EntryItemContextMenu = ({ id, children }: PropsWithChildren<{ id: s
key="Star"
onSelect={() => {
if (isEntryStarred) {
- collectionSyncService.unstarEntry({
- createdAt: new Date().toISOString(),
+ collectionSyncService.unstarEntry(id)
+ toast.info("Unstarred")
+ } else {
+ if (!entry.feedId) {
+ toast.error("Feed not found")
+ return
+ }
+ collectionSyncService.starEntry({
feedId: entry.feedId,
entryId: id,
+ // TODO update view
view: 0,
})
- toast.info("Unstarred")
- } else {
- collectionSyncService.starEntry(
- {
- createdAt: new Date().toISOString(),
- feedId: entry.feedId,
- entryId: id,
- // TODO update view
- view: 0,
- },
- 0,
- )
toast.info("Starred")
}
}}
diff --git a/apps/mobile/src/modules/entry-list/action.tsx b/apps/mobile/src/modules/entry-list/action.tsx
index 24636f6a32..8ccb0f2573 100644
--- a/apps/mobile/src/modules/entry-list/action.tsx
+++ b/apps/mobile/src/modules/entry-list/action.tsx
@@ -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() {
)
}
+
+export function LoadArchiveButton() {
+ const isLoadingArchivedEntries = useIsLoadingArchivedEntries()
+ if (isLoadingArchivedEntries) return null
+ return (
+
+ {
+ setIsLoadingArchivedEntries(true)
+ }}
+ >
+ Load archived entries
+
+
+ )
+}
diff --git a/apps/mobile/src/modules/entry-list/entry-list.tsx b/apps/mobile/src/modules/entry-list/entry-list.tsx
index 958da08df5..fb073388a2 100644
--- a/apps/mobile/src/modules/entry-list/entry-list.tsx
+++ b/apps/mobile/src/modules/entry-list/entry-list.tsx
@@ -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) => {
@@ -95,6 +95,13 @@ function EntryListContent({ entryIds }: { entryIds: string[] }) {
)
const tabBarHeight = useBottomTabBarHeight()
+
+ const ListFooterComponent = useMemo(
+ () =>
+ isFetching ? : screenType === "feed" ? : null,
+ [isFetching, screenType],
+ )
+
return (
: null}
+ ListFooterComponent={ListFooterComponent}
/>
)
}
diff --git a/apps/mobile/src/modules/feed-drawer/atoms.ts b/apps/mobile/src/modules/feed-drawer/atoms.ts
index e7a1466dd0..c4b5af482c 100644
--- a/apps/mobile/src/modules/feed-drawer/atoms.ts
+++ b/apps/mobile/src/modules/feed-drawer/atoms.ts
@@ -103,6 +103,15 @@ const selectedTimelineAtom = atom({
const selectedFeedAtom = atom(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) => {
diff --git a/apps/mobile/src/services/entry.ts b/apps/mobile/src/services/entry.ts
index af7c5affdf..854f7515aa 100644
--- a/apps/mobile/src/services/entry.ts
+++ b/apps/mobile/src/services/entry.ts
@@ -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",
+ )
}
}
diff --git a/apps/mobile/src/store/entry/store.ts b/apps/mobile/src/store/entry/store.ts
index 2f4bd957f1..fde5d36bda 100644
--- a/apps/mobile/src/store/entry/store.ts
+++ b/apps/mobile/src/store/entry/store.ts
@@ -48,8 +48,10 @@ export const useEntryStore = createZustandStore("entry")(() => defau
const immerSet = createImmerSetter(useEntryStore)
+type UpsertPosition = "all" | "view" | "category" | "feed" | "inbox" | "list"
+
class EntryActions {
- private addEntryIdToFeed({
+ private addEntryIdToView({
draft,
feedId,
entryId,
@@ -59,13 +61,6 @@ class EntryActions {
entryId: EntryId
}) {
if (!feedId) return
- const entryIdSetByFeed = draft.entryIdByFeed[feedId]
- if (!entryIdSetByFeed) {
- draft.entryIdByFeed[feedId] = new Set([entryId])
- } else {
- entryIdSetByFeed.add(entryId)
- }
-
const subscription = getSubscription(feedId)
if (typeof subscription?.view === "number") {
draft.entryIdByView[subscription.view].add(entryId)
@@ -80,6 +75,45 @@ class EntryActions {
}
}
+ 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,
+ entryId,
+ }: {
+ draft: EntryState
+ feedId?: FeedId | null
+ entryId: EntryId
+ }) {
+ if (!feedId) return
+ const entryIdSetByFeed = draft.entryIdByFeed[feedId]
+ if (!entryIdSetByFeed) {
+ draft.entryIdByFeed[feedId] = new Set([entryId])
+ } else {
+ entryIdSetByFeed.add(entryId)
+ }
+ }
+
private addEntryIdToInbox({
draft,
inboxHandle,
@@ -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,
- })
-
- this.addEntryIdToInbox({
- draft,
- inboxHandle,
- entryId: entry.id,
- })
+ if (position === "all" || position === "feed") {
+ this.addEntryIdToFeed({
+ draft,
+ feedId,
+ 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) {
diff --git a/apps/mobile/src/store/subscription/getter.ts b/apps/mobile/src/store/subscription/getter.ts
index bad22e63c3..6dd0bf2716 100644
--- a/apps/mobile/src/store/subscription/getter.ts
+++ b/apps/mobile/src/store/subscription/getter.ts
@@ -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[] => {