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
This commit is contained in:
parent
43b49d8619
commit
2f1d0485f8
|
|
@ -0,0 +1,6 @@
|
|||
CREATE TABLE `collections` (
|
||||
`feed_id` text,
|
||||
`entry_id` text PRIMARY KEY NOT NULL,
|
||||
`created_at` text,
|
||||
`view` integer NOT NULL
|
||||
);
|
||||
|
|
@ -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": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<FeedViewType>(),
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
|||
<ContextMenu.ItemTitle>Mark As Read</ContextMenu.ItemTitle>
|
||||
</ContextMenu.Item>
|
||||
|
||||
<ContextMenu.Item key="Star">
|
||||
<ContextMenu.ItemTitle>Star</ContextMenu.ItemTitle>
|
||||
<ContextMenu.Item
|
||||
key="Star"
|
||||
onSelect={() => {
|
||||
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")
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ContextMenu.ItemTitle>{isEntryStarred ? "Unstar" : "Star"}</ContextMenu.ItemTitle>
|
||||
</ContextMenu.Item>
|
||||
|
||||
{entry.url && (
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
})
|
||||
}
|
||||
|
|
@ -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<string, CollectionSchema>
|
||||
}
|
||||
|
||||
const defaultState = {
|
||||
collections: {},
|
||||
}
|
||||
|
||||
export const useCollectionStore = createZustandStore<CollectionState>("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()
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
import type { CollectionSchema } from "@/src/database/schemas/types"
|
||||
|
||||
export type CollectionModel = CollectionSchema
|
||||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,4 +10,5 @@ export type FetchEntriesProps = {
|
|||
limit?: number
|
||||
pageParam?: string
|
||||
isArchived?: boolean
|
||||
isCollection?: boolean
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue