feat: basic entry data sync (#2597)
This commit is contained in:
parent
0ea516c9f9
commit
3479a89b1f
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE `lists` ADD `entry_ids` text;
|
||||
|
|
@ -0,0 +1,501 @@
|
|||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "0d5c6f89-f31c-405c-8c2d-7b59ab03a275",
|
||||
"prevId": "9f943ca5-ddaf-4916-98cd-d3e3f7ead201",
|
||||
"tables": {
|
||||
"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": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -50,6 +50,13 @@
|
|||
"when": 1737080887634,
|
||||
"tag": "0006_exotic_kid_colt",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 7,
|
||||
"version": "6",
|
||||
"when": 1737108493439,
|
||||
"tag": "0007_curvy_tarantula",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import m0003 from "./0003_known_roland_deschain.sql"
|
|||
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 journal from "./meta/_journal.json"
|
||||
|
||||
export default {
|
||||
|
|
@ -19,5 +20,6 @@ export default {
|
|||
m0004,
|
||||
m0005,
|
||||
m0006,
|
||||
m0007,
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ export const listsTable = sqliteTable("lists", {
|
|||
image: text("image"),
|
||||
fee: integer("fee"),
|
||||
ownerUserId: text("owner_user_id"),
|
||||
entryIds: text("entry_ids", { mode: "json" }).$type<string[]>(),
|
||||
})
|
||||
|
||||
export const unreadTable = sqliteTable("unread", {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { SubscriptionSchema } from "../database/schemas/types"
|
||||
import type { EntrySchema, SubscriptionSchema } from "../database/schemas/types"
|
||||
import type { EntryModel } from "../store/entry/types"
|
||||
import type { SubscriptionModel } from "../store/subscription/store"
|
||||
|
||||
class DbStoreMorph {
|
||||
|
|
@ -8,6 +9,10 @@ class DbStoreMorph {
|
|||
isPrivate: subscription.isPrivate ? true : false,
|
||||
}
|
||||
}
|
||||
|
||||
toEntryModel(entry: EntrySchema): EntryModel {
|
||||
return entry
|
||||
}
|
||||
}
|
||||
|
||||
export const dbStoreMorph = new DbStoreMorph()
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { FeedSchema, InboxSchema } from "../database/schemas/types"
|
||||
import type { EntryModel } from "../store/entry/types"
|
||||
import type { ListModel } from "../store/list/store"
|
||||
import type { SubscriptionModel } from "../store/subscription/store"
|
||||
import type { HonoApiClient } from "./types"
|
||||
|
|
@ -72,6 +73,7 @@ class Morph {
|
|||
ownerUserId: list.owner.id,
|
||||
feedIds: list.feedIds!,
|
||||
fee: list.fee!,
|
||||
entryIds: [],
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -91,8 +93,42 @@ class Morph {
|
|||
ownerUserId: data.ownerUserId!,
|
||||
feedIds: data.feedIds!,
|
||||
fee: data.fee!,
|
||||
entryIds: [],
|
||||
}
|
||||
}
|
||||
|
||||
toEntry(data?: HonoApiClient.Entry_Get): EntryModel[] {
|
||||
const entries: EntryModel[] = []
|
||||
for (const item of data ?? []) {
|
||||
entries.push({
|
||||
id: item.entries.id,
|
||||
title: item.entries.title,
|
||||
url: item.entries.url,
|
||||
content: "",
|
||||
description: item.entries.description,
|
||||
guid: item.entries.guid,
|
||||
author: item.entries.author,
|
||||
authorUrl: item.entries.authorUrl,
|
||||
authorAvatar: item.entries.authorAvatar,
|
||||
insertedAt: new Date(item.entries.insertedAt),
|
||||
publishedAt: new Date(item.entries.publishedAt),
|
||||
media: item.entries.media ?? null,
|
||||
categories: item.entries.categories ?? null,
|
||||
attachments: item.entries.attachments ?? null,
|
||||
extra: item.entries.extra
|
||||
? {
|
||||
links: item.entries.extra.links ?? undefined,
|
||||
}
|
||||
: null,
|
||||
language: item.entries.language,
|
||||
feedId: item.feeds.id,
|
||||
// TODO: handle inboxHandle
|
||||
inboxHandle: "",
|
||||
read: false,
|
||||
})
|
||||
}
|
||||
return entries
|
||||
}
|
||||
}
|
||||
|
||||
export const honoMorph = new Morph()
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { ListSchema, SubscriptionSchema } from "../database/schemas/types"
|
||||
import type { EntrySchema, ListSchema, SubscriptionSchema } from "../database/schemas/types"
|
||||
import type { EntryModel } from "../store/entry/types"
|
||||
import type { ListModel } from "../store/list/store"
|
||||
import type { SubscriptionModel } from "../store/subscription/store"
|
||||
|
||||
|
|
@ -16,6 +17,9 @@ class StoreDbMorph {
|
|||
isPrivate: subscription.isPrivate ? 1 : 0,
|
||||
}
|
||||
}
|
||||
toEntrySchema(entry: EntryModel): EntrySchema {
|
||||
return entry
|
||||
}
|
||||
}
|
||||
|
||||
export const storeDbMorph = new StoreDbMorph()
|
||||
|
|
|
|||
|
|
@ -3,9 +3,10 @@ import type { apiClient } from "../lib/api-fetch"
|
|||
|
||||
// Add ExtractData type utility
|
||||
type ExtractData<T extends (...args: any) => any> =
|
||||
Awaited<ReturnType<T>> extends { data: infer D } ? D : never
|
||||
Awaited<ReturnType<T>> extends { data?: infer D } ? D : never
|
||||
|
||||
export namespace HonoApiClient {
|
||||
export type Subscription_Get = ExtractData<typeof apiClient.subscriptions.$get>
|
||||
export type List_Get = ExtractData<typeof apiClient.lists.$get>
|
||||
export type Entry_Get = ExtractData<typeof apiClient.entries.$post>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
import { eq } from "drizzle-orm"
|
||||
|
||||
import { db } from "../database"
|
||||
import { entriesTable } from "../database/schemas"
|
||||
import type { EntrySchema } from "../database/schemas/types"
|
||||
import { dbStoreMorph } from "../morph/db-store"
|
||||
import { entryActions } from "../store/entry/store"
|
||||
import type { Hydratable, Resetable } from "./internal/base"
|
||||
import { conflictUpdateAllExcept } from "./internal/utils"
|
||||
|
||||
class EntryServiceStatic implements Hydratable, Resetable {
|
||||
async reset() {
|
||||
await db.delete(entriesTable).execute()
|
||||
}
|
||||
|
||||
async upsertMany(entries: EntrySchema[]) {
|
||||
if (entries.length === 0) return
|
||||
await db
|
||||
.insert(entriesTable)
|
||||
.values(entries)
|
||||
.onConflictDoUpdate({
|
||||
target: [entriesTable.id],
|
||||
set: conflictUpdateAllExcept(entriesTable, ["id"]),
|
||||
})
|
||||
}
|
||||
|
||||
async patch(entry: Partial<EntrySchema> & { id: string }) {
|
||||
await db.update(entriesTable).set(entry).where(eq(entriesTable.id, entry.id))
|
||||
}
|
||||
|
||||
async hydrate() {
|
||||
const entries = await db.query.entriesTable.findMany()
|
||||
entryActions.upsertManyInSession(entries.map((e) => dbStoreMorph.toEntryModel(e)))
|
||||
}
|
||||
}
|
||||
|
||||
export const EntryService = new EntryServiceStatic()
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
import { eq } from "drizzle-orm"
|
||||
|
||||
import { db } from "../database"
|
||||
import { listsTable } from "../database/schemas"
|
||||
import type { ListSchema } from "../database/schemas/types"
|
||||
|
|
@ -28,6 +30,20 @@ class ListServiceStatic implements Hydratable, Resetable {
|
|||
set: conflictUpdateAllExcept(listsTable, ["id"]),
|
||||
})
|
||||
}
|
||||
async addEntryIds(params: { listId: string; entryIds: string[] }) {
|
||||
const list = await db.query.listsTable.findFirst({
|
||||
where: eq(listsTable.id, params.listId),
|
||||
})
|
||||
|
||||
if (!list) return
|
||||
|
||||
await db
|
||||
.update(listsTable)
|
||||
.set({
|
||||
entryIds: [...new Set([...(list.entryIds ?? []), ...params.entryIds])],
|
||||
})
|
||||
.where(eq(listsTable.id, params.listId))
|
||||
}
|
||||
}
|
||||
|
||||
export const ListService = new ListServiceStatic()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
import { useEntryStore } from "./store"
|
||||
import type { EntryModel } from "./types"
|
||||
|
||||
const get = useEntryStore.getState
|
||||
|
||||
export const getEntry = (id: string): EntryModel | undefined => {
|
||||
return get().data[id]
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
import type { FeedViewType } from "@follow/constants"
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
import { useCallback } from "react"
|
||||
|
||||
import { getEntry } from "./getter"
|
||||
import { entrySyncServices, useEntryStore } from "./store"
|
||||
import type { EntryModel, FetchEntriesProps } from "./types"
|
||||
|
||||
export const usePrefetchEntries = (props: FetchEntriesProps) => {
|
||||
const { feedId, inboxId, listId, view, read, limit, pageParam, isArchived } = props
|
||||
return useQuery({
|
||||
queryKey: ["entries", feedId, inboxId, listId, view, read, limit, pageParam, isArchived],
|
||||
queryFn: () => entrySyncServices.fetchEntries(props),
|
||||
})
|
||||
}
|
||||
|
||||
export const useEntry = (id: string): EntryModel | undefined => {
|
||||
return useEntryStore((state) => state.data[id])
|
||||
}
|
||||
|
||||
function sortEntryIdsByPublishDate(a: string, b: string) {
|
||||
const entryA = getEntry(a)
|
||||
const entryB = getEntry(b)
|
||||
if (!entryA || !entryB) return 0
|
||||
return entryB.publishedAt.getTime() - entryA.publishedAt.getTime()
|
||||
}
|
||||
|
||||
export const useEntryIdsByView = (view: FeedViewType) => {
|
||||
return useEntryStore(
|
||||
useCallback(
|
||||
(state) => {
|
||||
const ids = state.entryIdByView[view]
|
||||
if (!ids) return []
|
||||
return Array.from(ids).sort((a, b) => sortEntryIdsByPublishDate(a, b))
|
||||
},
|
||||
[view],
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
export const useEntryIdsByFeedId = (feedId: string) => {
|
||||
return useEntryStore(
|
||||
useCallback(
|
||||
(state) => {
|
||||
const ids = state.entryIdByFeed[feedId]
|
||||
if (!ids) return []
|
||||
return Array.from(ids).sort((a, b) => sortEntryIdsByPublishDate(a, b))
|
||||
},
|
||||
[feedId],
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
export const useEntryIdsByInboxId = (inboxId: string) => {
|
||||
return useEntryStore(
|
||||
useCallback(
|
||||
(state) => {
|
||||
const ids = state.entryIdByInbox[inboxId]
|
||||
if (!ids) return []
|
||||
return Array.from(ids).sort((a, b) => sortEntryIdsByPublishDate(a, b))
|
||||
},
|
||||
[inboxId],
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
export const useEntryIdsByCategory = (category: string) => {
|
||||
return useEntryStore(
|
||||
useCallback(
|
||||
(state) => {
|
||||
const ids = state.entryIdByCategory[category]
|
||||
if (!ids) return []
|
||||
return Array.from(ids).sort((a, b) => sortEntryIdsByPublishDate(a, b))
|
||||
},
|
||||
[category],
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,174 @@
|
|||
import { FeedViewType } from "@follow/constants"
|
||||
|
||||
import { apiClient } from "@/src/lib/api-fetch"
|
||||
import { honoMorph } from "@/src/morph/hono"
|
||||
import { storeDbMorph } from "@/src/morph/store-db"
|
||||
import { EntryService } from "@/src/services/entry"
|
||||
|
||||
import { createImmerSetter, createTransaction, createZustandStore } from "../internal/helper"
|
||||
import { listActions } from "../list/store"
|
||||
import { getSubscription } from "../subscription/getter"
|
||||
import type { EntryModel, FetchEntriesProps } from "./types"
|
||||
import { getEntriesParams } from "./utils"
|
||||
|
||||
type EntryId = string
|
||||
type FeedId = string
|
||||
type InboxId = string
|
||||
type Category = string
|
||||
|
||||
interface EntryState {
|
||||
data: Record<EntryId, EntryModel>
|
||||
entryIdByView: Record<FeedViewType, Set<EntryId>>
|
||||
entryIdByCategory: Record<Category, Set<EntryId>>
|
||||
entryIdByFeed: Record<FeedId, Set<EntryId>>
|
||||
entryIdByInbox: Record<InboxId, Set<EntryId>>
|
||||
}
|
||||
|
||||
const defaultState: EntryState = {
|
||||
data: {},
|
||||
entryIdByView: {
|
||||
[FeedViewType.Articles]: new Set(),
|
||||
[FeedViewType.Audios]: new Set(),
|
||||
[FeedViewType.Notifications]: new Set(),
|
||||
[FeedViewType.Pictures]: new Set(),
|
||||
[FeedViewType.SocialMedia]: new Set(),
|
||||
[FeedViewType.Videos]: new Set(),
|
||||
},
|
||||
entryIdByCategory: {},
|
||||
entryIdByFeed: {},
|
||||
entryIdByInbox: {},
|
||||
}
|
||||
|
||||
export const useEntryStore = createZustandStore<EntryState>("entry")(() => defaultState)
|
||||
|
||||
const set = useEntryStore.setState
|
||||
const immerSet = createImmerSetter(useEntryStore)
|
||||
|
||||
class EntryActions {
|
||||
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)
|
||||
}
|
||||
|
||||
const subscription = getSubscription(feedId)
|
||||
if (subscription?.view) {
|
||||
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({
|
||||
draft,
|
||||
inboxHandle,
|
||||
entryId,
|
||||
}: {
|
||||
draft: EntryState
|
||||
inboxHandle?: InboxId | null
|
||||
entryId: EntryId
|
||||
}) {
|
||||
if (!inboxHandle) return
|
||||
const entryIdSetByInbox = draft.entryIdByInbox[inboxHandle]
|
||||
if (!entryIdSetByInbox) {
|
||||
draft.entryIdByInbox[inboxHandle] = new Set([entryId])
|
||||
} else {
|
||||
entryIdSetByInbox.add(entryId)
|
||||
}
|
||||
}
|
||||
|
||||
upsertManyInSession(entries: EntryModel[]) {
|
||||
if (entries.length === 0) return
|
||||
|
||||
immerSet((draft) => {
|
||||
for (const entry of entries) {
|
||||
draft.data[entry.id] = entry
|
||||
|
||||
const { feedId, inboxHandle } = entry
|
||||
this.addEntryIdToFeed({
|
||||
draft,
|
||||
feedId,
|
||||
entryId: entry.id,
|
||||
})
|
||||
|
||||
this.addEntryIdToInbox({
|
||||
draft,
|
||||
inboxHandle,
|
||||
entryId: entry.id,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async upsertMany(entries: EntryModel[]) {
|
||||
const tx = createTransaction()
|
||||
tx.store(() => {
|
||||
this.upsertManyInSession(entries)
|
||||
})
|
||||
|
||||
tx.persist(() => {
|
||||
return EntryService.upsertMany(entries.map((e) => storeDbMorph.toEntrySchema(e)))
|
||||
})
|
||||
|
||||
await tx.run()
|
||||
}
|
||||
|
||||
reset() {
|
||||
set(defaultState)
|
||||
}
|
||||
}
|
||||
|
||||
class EntrySyncServices {
|
||||
async fetchEntries(props: FetchEntriesProps) {
|
||||
const { feedId, inboxId, listId, view, read, limit, pageParam, isArchived } = props
|
||||
const params = getEntriesParams({
|
||||
feedId,
|
||||
inboxId,
|
||||
listId,
|
||||
view,
|
||||
})
|
||||
const res = await apiClient.entries.$post({
|
||||
json: {
|
||||
publishedAfter: pageParam,
|
||||
read,
|
||||
limit,
|
||||
isArchived,
|
||||
...params,
|
||||
},
|
||||
})
|
||||
|
||||
if (!pageParam) {
|
||||
entryActions.reset()
|
||||
}
|
||||
|
||||
const entries = honoMorph.toEntry(res.data)
|
||||
await entryActions.upsertMany(entries)
|
||||
if (params.listId) {
|
||||
await listActions.addEntryIds({
|
||||
listId: params.listId,
|
||||
entryIds: entries.map((e) => e.id),
|
||||
})
|
||||
}
|
||||
return entries
|
||||
}
|
||||
}
|
||||
|
||||
export const entrySyncServices = new EntrySyncServices()
|
||||
export const entryActions = new EntryActions()
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import type { EntrySchema } from "@/src/database/schemas/types"
|
||||
|
||||
export type EntryModel = EntrySchema
|
||||
export type FetchEntriesProps = {
|
||||
feedId?: number | string
|
||||
inboxId?: number | string
|
||||
listId?: number | string
|
||||
view?: number
|
||||
read?: boolean
|
||||
limit?: number
|
||||
pageParam?: string
|
||||
isArchived?: boolean
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
import { FeedViewType } from "@follow/constants"
|
||||
|
||||
/// Feed
|
||||
export const FEED_COLLECTION_LIST = "collections"
|
||||
|
||||
/// Route Keys
|
||||
export const ROUTE_FEED_PENDING = "all"
|
||||
export const ROUTE_ENTRY_PENDING = "pending"
|
||||
export const ROUTE_FEED_IN_FOLDER = "folder-"
|
||||
export const ROUTE_FEED_IN_LIST = "list-"
|
||||
export const ROUTE_FEED_IN_INBOX = "inbox-"
|
||||
|
||||
export const INBOX_PREFIX_ID = "inbox-"
|
||||
|
||||
export function getEntriesParams({
|
||||
feedId,
|
||||
inboxId,
|
||||
listId,
|
||||
view,
|
||||
}: {
|
||||
feedId?: number | string
|
||||
inboxId?: number | string
|
||||
listId?: number | string
|
||||
view?: number
|
||||
}) {
|
||||
const params: {
|
||||
feedId?: string
|
||||
feedIdList?: string[]
|
||||
isCollection?: boolean
|
||||
withContent?: boolean
|
||||
inboxId?: string
|
||||
listId?: string
|
||||
} = {}
|
||||
if (inboxId) {
|
||||
params.inboxId = `${inboxId}`
|
||||
} else if (listId) {
|
||||
params.listId = `${listId}`
|
||||
} else if (feedId) {
|
||||
if (feedId === FEED_COLLECTION_LIST) {
|
||||
params.isCollection = true
|
||||
} else if (feedId !== ROUTE_FEED_PENDING) {
|
||||
if (feedId.toString().includes(",")) {
|
||||
params.feedIdList = `${feedId}`.split(",")
|
||||
} else {
|
||||
params.feedId = `${feedId}`
|
||||
}
|
||||
}
|
||||
}
|
||||
if (view === FeedViewType.SocialMedia) {
|
||||
params.withContent = true
|
||||
}
|
||||
return {
|
||||
view,
|
||||
...params,
|
||||
}
|
||||
}
|
||||
|
|
@ -12,3 +12,9 @@ export const useIsOwnList = (id: string) => {
|
|||
return state.lists[id]?.userId === whoami()?.id
|
||||
})
|
||||
}
|
||||
|
||||
export const useListEntryIds = (id: string) => {
|
||||
return useListStore((state) => {
|
||||
return state.lists[id]?.entryIds || []
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,6 +45,34 @@ class ListActions {
|
|||
})
|
||||
tx.run()
|
||||
}
|
||||
|
||||
addEntryIdsInSession(params: { listId: string; entryIds: string[] }) {
|
||||
const state = get()
|
||||
const list = state.lists[params.listId]
|
||||
|
||||
if (!list) return
|
||||
|
||||
set({
|
||||
...state,
|
||||
lists: {
|
||||
...state.lists,
|
||||
[params.listId]: { ...list, feedIds: [...list.feedIds, ...params.entryIds] },
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async addEntryIds(params: { listId: string; entryIds: string[] }) {
|
||||
const tx = createTransaction()
|
||||
tx.store(() => {
|
||||
this.addEntryIdsInSession(params)
|
||||
})
|
||||
|
||||
tx.persist(() => {
|
||||
return ListService.addEntryIds(params)
|
||||
})
|
||||
await tx.run()
|
||||
}
|
||||
|
||||
reset() {
|
||||
set(defaultState)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -211,6 +211,7 @@ class SubscriptionSyncService {
|
|||
{
|
||||
...data.list,
|
||||
userId: data.list.ownerUserId,
|
||||
entryIds: [],
|
||||
},
|
||||
])
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue