From 2f1d0485f8f280a2c8cfb7909d712baa7fb56364 Mon Sep 17 00:00:00 2001 From: Whitewater Date: Tue, 11 Feb 2025 17:57:05 +0800 Subject: [PATCH] feat: add collections table and implement collection management services (#2718) * feat: add collections table and implement collection management services * chore: fix typo in star entry hook and add toast for star * chore: generate drizzle migration script * chore: clean code --- .../drizzle/0008_last_the_santerians.sql | 6 + apps/mobile/drizzle/meta/0008_snapshot.json | 539 ++++++++++++++++++ apps/mobile/drizzle/meta/_journal.json | 7 + apps/mobile/drizzle/migrations.js | 2 + apps/mobile/src/database/schemas/index.ts | 7 + apps/mobile/src/database/schemas/types.ts | 3 + .../mobile/src/modules/context-menu/entry.tsx | 33 +- apps/mobile/src/morph/hono.ts | 20 + apps/mobile/src/services/collection.ts | 40 ++ apps/mobile/src/services/index.ts | 2 + apps/mobile/src/store/collection/hooks.ts | 13 + apps/mobile/src/store/collection/store.ts | 104 ++++ apps/mobile/src/store/collection/types.ts | 3 + apps/mobile/src/store/entry/store.ts | 13 +- apps/mobile/src/store/entry/types.ts | 1 + 15 files changed, 790 insertions(+), 3 deletions(-) create mode 100644 apps/mobile/drizzle/0008_last_the_santerians.sql create mode 100644 apps/mobile/drizzle/meta/0008_snapshot.json create mode 100644 apps/mobile/src/services/collection.ts create mode 100644 apps/mobile/src/store/collection/hooks.ts create mode 100644 apps/mobile/src/store/collection/store.ts create mode 100644 apps/mobile/src/store/collection/types.ts diff --git a/apps/mobile/drizzle/0008_last_the_santerians.sql b/apps/mobile/drizzle/0008_last_the_santerians.sql new file mode 100644 index 000000000..c63edffa6 --- /dev/null +++ b/apps/mobile/drizzle/0008_last_the_santerians.sql @@ -0,0 +1,6 @@ +CREATE TABLE `collections` ( + `feed_id` text, + `entry_id` text PRIMARY KEY NOT NULL, + `created_at` text, + `view` integer NOT NULL +); diff --git a/apps/mobile/drizzle/meta/0008_snapshot.json b/apps/mobile/drizzle/meta/0008_snapshot.json new file mode 100644 index 000000000..0baa66955 --- /dev/null +++ b/apps/mobile/drizzle/meta/0008_snapshot.json @@ -0,0 +1,539 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "25bbf165-d945-4ec7-9571-b7bd36a7b9b5", + "prevId": "0d5c6f89-f31c-405c-8c2d-7b59ab03a275", + "tables": { + "collections": { + "name": "collections", + "columns": { + "feed_id": { + "name": "feed_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "entry_id": { + "name": "entry_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "view": { + "name": "view", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "entries": { + "name": "entries", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "guid": { + "name": "guid", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "author_url": { + "name": "author_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "author_avatar": { + "name": "author_avatar", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "inserted_at": { + "name": "inserted_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "published_at": { + "name": "published_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "media": { + "name": "media", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "categories": { + "name": "categories", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "attachments": { + "name": "attachments", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "extra": { + "name": "extra", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "feed_id": { + "name": "feed_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "inbox_handle": { + "name": "inbox_handle", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "read": { + "name": "read", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "feeds": { + "name": "feeds", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_at": { + "name": "error_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "site_url": { + "name": "site_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "inboxes": { + "name": "inboxes", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "lists": { + "name": "lists", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "feed_ids": { + "name": "feed_ids", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "view": { + "name": "view", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "fee": { + "name": "fee", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "entry_ids": { + "name": "entry_ids", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "subscriptions": { + "name": "subscriptions", + "columns": { + "feed_id": { + "name": "feed_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "list_id": { + "name": "list_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "inbox_id": { + "name": "inbox_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "view": { + "name": "view", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_private": { + "name": "is_private", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "unread": { + "name": "unread", + "columns": { + "subscription_id": { + "name": "subscription_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "handle": { + "name": "handle", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_me": { + "name": "is_me", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} diff --git a/apps/mobile/drizzle/meta/_journal.json b/apps/mobile/drizzle/meta/_journal.json index 9ec202076..dd5e7c112 100644 --- a/apps/mobile/drizzle/meta/_journal.json +++ b/apps/mobile/drizzle/meta/_journal.json @@ -57,6 +57,13 @@ "when": 1737108493439, "tag": "0007_curvy_tarantula", "breakpoints": true + }, + { + "idx": 8, + "version": "6", + "when": 1739265018895, + "tag": "0008_last_the_santerians", + "breakpoints": true } ] } diff --git a/apps/mobile/drizzle/migrations.js b/apps/mobile/drizzle/migrations.js index 48decb123..153d35a1b 100644 --- a/apps/mobile/drizzle/migrations.js +++ b/apps/mobile/drizzle/migrations.js @@ -8,6 +8,7 @@ import m0004 from "./0004_majestic_thunderbolt_ross.sql" import m0005 from "./0005_tense_sleepwalker.sql" import m0006 from "./0006_exotic_kid_colt.sql" import m0007 from "./0007_curvy_tarantula.sql" +import m0008 from "./0008_last_the_santerians.sql" import journal from "./meta/_journal.json" export default { @@ -21,5 +22,6 @@ export default { m0005, m0006, m0007, + m0008, }, } diff --git a/apps/mobile/src/database/schemas/index.ts b/apps/mobile/src/database/schemas/index.ts index bab071cfa..9bcff481b 100644 --- a/apps/mobile/src/database/schemas/index.ts +++ b/apps/mobile/src/database/schemas/index.ts @@ -84,3 +84,10 @@ export const entriesTable = sqliteTable("entries", { inboxHandle: text("inbox_handle"), read: integer("read", { mode: "boolean" }), }) + +export const collectionsTable = sqliteTable("collections", { + feedId: text("feed_id"), + entryId: text("entry_id").notNull().primaryKey(), + createdAt: text("created_at"), + view: integer("view").notNull().$type(), +}) diff --git a/apps/mobile/src/database/schemas/types.ts b/apps/mobile/src/database/schemas/types.ts index 5c5176119..f81ac247e 100644 --- a/apps/mobile/src/database/schemas/types.ts +++ b/apps/mobile/src/database/schemas/types.ts @@ -1,4 +1,5 @@ import type { + collectionsTable, entriesTable, feedsTable, inboxesTable, @@ -22,6 +23,8 @@ export type UserSchema = typeof usersTable.$inferSelect export type EntrySchema = typeof entriesTable.$inferSelect +export type CollectionSchema = typeof collectionsTable.$inferSelect + export type MediaModel = { url: string type: "photo" | "video" diff --git a/apps/mobile/src/modules/context-menu/entry.tsx b/apps/mobile/src/modules/context-menu/entry.tsx index 8a41b1e31..bdea0258c 100644 --- a/apps/mobile/src/modules/context-menu/entry.tsx +++ b/apps/mobile/src/modules/context-menu/entry.tsx @@ -10,11 +10,15 @@ import { setWebViewEntry, } from "@/src/components/native/webview/EntryContentWebView" import { openLink } from "@/src/lib/native" +import { toast } from "@/src/lib/toast" +import { useIsEntryStarred } from "@/src/store/collection/hooks" +import { collectionSyncService } from "@/src/store/collection/store" import { useEntry } from "@/src/store/entry/hooks" import { unreadSyncService } from "@/src/store/unread/store" export const EntryItemContextMenu = ({ id, children }: PropsWithChildren<{ id: string }>) => { const entry = useEntry(id) + const isEntryStarred = useIsEntryStarred(id) const handlePressPreview = useCallback(() => { if (!entry) return @@ -51,8 +55,33 @@ export const EntryItemContextMenu = ({ id, children }: PropsWithChildren<{ id: s Mark As Read - - Star + { + if (isEntryStarred) { + collectionSyncService.unstarEntry({ + createdAt: new Date().toISOString(), + feedId: entry.feedId, + entryId: id, + 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") + } + }} + > + {isEntryStarred ? "Unstar" : "Star"} {entry.url && ( diff --git a/apps/mobile/src/morph/hono.ts b/apps/mobile/src/morph/hono.ts index 0bbe08aaf..01e7f69e2 100644 --- a/apps/mobile/src/morph/hono.ts +++ b/apps/mobile/src/morph/hono.ts @@ -1,4 +1,7 @@ +import type { FeedViewType } from "@follow/constants" + import type { FeedSchema, InboxSchema } from "../database/schemas/types" +import type { CollectionModel } from "../store/collection/types" import type { EntryModel } from "../store/entry/types" import type { FeedModel } from "../store/feed/types" import type { ListModel } from "../store/list/store" @@ -131,6 +134,23 @@ class Morph { return entries } + toCollections(data: HonoApiClient.Entry_Post, view: FeedViewType): CollectionModel[] { + if (!data) return [] satisfies CollectionModel[] + return data + .map((item) => { + if (!item.collections) { + return null + } + return { + createdAt: item.collections.createdAt, + entryId: item.entries.id, + feedId: item.feeds.id, + view, + } satisfies CollectionModel + }) + .filter((i) => i !== null) + } + toEntry(data?: HonoApiClient.Entry_Get): EntryModel | null { if (!data) return null diff --git a/apps/mobile/src/services/collection.ts b/apps/mobile/src/services/collection.ts new file mode 100644 index 000000000..2428f6dd5 --- /dev/null +++ b/apps/mobile/src/services/collection.ts @@ -0,0 +1,40 @@ +import { eq, inArray } from "drizzle-orm" + +import { db } from "../database" +import { collectionsTable } from "../database/schemas" +import type { CollectionSchema } from "../database/schemas/types" +import { collectionActions } from "../store/collection/store" +import type { Hydratable, Resetable } from "./internal/base" +import { conflictUpdateAllExcept } from "./internal/utils" + +class CollectionServiceStatic implements Hydratable, Resetable { + async reset() { + await db.delete(collectionsTable).execute() + } + + async upsertMany(collections: CollectionSchema[]) { + if (collections.length === 0) return + await db + .insert(collectionsTable) + .values(collections) + .onConflictDoUpdate({ + target: [collectionsTable.entryId], + set: conflictUpdateAllExcept(collectionsTable, ["entryId"]), + }) + } + + async delete(entryId: string) { + await db.delete(collectionsTable).where(eq(collectionsTable.entryId, entryId)) + } + + getCollectionMany(entryId: string[]) { + return db.query.collectionsTable.findMany({ where: inArray(collectionsTable.entryId, entryId) }) + } + + async hydrate() { + const collections = await db.query.collectionsTable.findMany() + collectionActions.upsertManyInSession(collections) + } +} + +export const CollectionService = new CollectionServiceStatic() diff --git a/apps/mobile/src/services/index.ts b/apps/mobile/src/services/index.ts index 272c387b9..4f406952c 100644 --- a/apps/mobile/src/services/index.ts +++ b/apps/mobile/src/services/index.ts @@ -1,3 +1,4 @@ +import { CollectionService } from "./collection" import { EntryService } from "./entry" import { FeedService } from "./feed" import { InboxService } from "./inbox" @@ -15,6 +16,7 @@ const hydrates: Hydratable[] = [ UnreadService, UserService, EntryService, + CollectionService, ] export const hydrateDatabaseToStore = async () => { diff --git a/apps/mobile/src/store/collection/hooks.ts b/apps/mobile/src/store/collection/hooks.ts new file mode 100644 index 000000000..2e30a3308 --- /dev/null +++ b/apps/mobile/src/store/collection/hooks.ts @@ -0,0 +1,13 @@ +import { useCollectionStore } from "./store" + +export const useCollectionEntry = (entryId: string) => { + return useCollectionStore((state) => { + return state.collections[entryId] + }) +} + +export const useIsEntryStarred = (entryId: string) => { + return useCollectionStore((state) => { + return !!state.collections[entryId] + }) +} diff --git a/apps/mobile/src/store/collection/store.ts b/apps/mobile/src/store/collection/store.ts new file mode 100644 index 000000000..217db356f --- /dev/null +++ b/apps/mobile/src/store/collection/store.ts @@ -0,0 +1,104 @@ +import type { FeedViewType } from "@follow/constants" + +import type { CollectionSchema } from "@/src/database/schemas/types" +import { apiClient } from "@/src/lib/api-fetch" +import { CollectionService } from "@/src/services/collection" + +import { createTransaction, createZustandStore } from "../internal/helper" + +interface CollectionState { + collections: Record +} + +const defaultState = { + collections: {}, +} + +export const useCollectionStore = createZustandStore("collection")( + () => defaultState, +) + +// const get = useCollectionStore.getState +const set = useCollectionStore.setState + +class CollectionSyncService { + async starEntry(collection: CollectionSchema, view: FeedViewType) { + await apiClient.collections.$post({ + json: { + entryId: collection.entryId, + view, + }, + }) + + await collectionActions.upsertMany([collection]) + return + } + + async unstarEntry(collection: CollectionSchema) { + await apiClient.collections.$delete({ + json: { + entryId: collection.entryId, + }, + }) + + await collectionActions.delete(collection.entryId) + } +} + +class CollectionActions { + async upsertManyInSession(collections: CollectionSchema[]) { + const state = useCollectionStore.getState() + const nextCollections: CollectionState["collections"] = { + ...state.collections, + } + collections.forEach((collection) => { + if (!collection.entryId) return + nextCollections[collection.entryId] = collection + }) + set({ + ...state, + collections: nextCollections, + }) + } + + async upsertMany(collections: CollectionSchema[]) { + const tx = createTransaction() + tx.store(() => { + this.upsertManyInSession(collections) + }) + tx.persist(() => { + return CollectionService.upsertMany(collections) + }) + tx.run() + } + + async deleteInSession(entryId: string) { + const state = useCollectionStore.getState() + const nextCollections: CollectionState["collections"] = { + ...state.collections, + } + delete nextCollections[entryId] + set({ + ...state, + collections: nextCollections, + }) + } + + async delete(entryId: string) { + const tx = createTransaction() + tx.store(() => { + this.deleteInSession(entryId) + }) + tx.persist(() => { + return CollectionService.delete(entryId) + }) + tx.run() + } + + reset() { + set(defaultState) + } +} + +export const collectionActions = new CollectionActions() +export const collectionSyncService = new CollectionSyncService() diff --git a/apps/mobile/src/store/collection/types.ts b/apps/mobile/src/store/collection/types.ts new file mode 100644 index 000000000..9f0dd5bec --- /dev/null +++ b/apps/mobile/src/store/collection/types.ts @@ -0,0 +1,3 @@ +import type { CollectionSchema } from "@/src/database/schemas/types" + +export type CollectionModel = CollectionSchema diff --git a/apps/mobile/src/store/entry/store.ts b/apps/mobile/src/store/entry/store.ts index 00278415e..2f4bd957f 100644 --- a/apps/mobile/src/store/entry/store.ts +++ b/apps/mobile/src/store/entry/store.ts @@ -8,6 +8,7 @@ import { honoMorph } from "@/src/morph/hono" import { storeDbMorph } from "@/src/morph/store-db" import { EntryService } from "@/src/services/entry" +import { collectionActions } from "../collection/store" import { createImmerSetter, createTransaction, createZustandStore } from "../internal/helper" import { listActions } from "../list/store" import { getSubscription } from "../subscription/getter" @@ -169,7 +170,8 @@ class EntryActions { class EntrySyncServices { async fetchEntries(props: FetchEntriesProps) { - const { feedId, inboxId, listId, view, read, limit, pageParam, isArchived } = props + const { feedId, inboxId, listId, view, read, limit, pageParam, isArchived, isCollection } = + props const params = getEntriesParams({ feedId, inboxId, @@ -182,6 +184,7 @@ class EntrySyncServices { read, limit, isArchived, + isCollection, ...params, }, }) @@ -217,6 +220,14 @@ class EntrySyncServices { } } + if (isCollection && res.data) { + if (!view) { + console.error("view is required for collection") + } + const collections = honoMorph.toCollections(res.data, view ?? 0) + await collectionActions.upsertMany(collections) + } + return res } diff --git a/apps/mobile/src/store/entry/types.ts b/apps/mobile/src/store/entry/types.ts index 30c7eb98e..2555a6caf 100644 --- a/apps/mobile/src/store/entry/types.ts +++ b/apps/mobile/src/store/entry/types.ts @@ -10,4 +10,5 @@ export type FetchEntriesProps = { limit?: number pageParam?: string isArchived?: boolean + isCollection?: boolean }