refactor: remove entry tags support

This commit is contained in:
DIYgod
2026-01-18 17:22:03 +08:00
parent 95b0da87a4
commit 9ea61dbfdb
18 changed files with 960 additions and 292 deletions
@@ -17,7 +17,6 @@ import type { FeedIconEntry } from "~/modules/feed/feed-icon"
import { FeedIcon } from "~/modules/feed/feed-icon"
import { getPreferredTitle } from "~/store/feed/hooks"
import { EntryTags } from "../../entry/EntryTags"
import { EntryTranslation } from "../../entry-column/translation"
import { EntryReadHistory } from "./entry-read-history"
@@ -38,8 +37,7 @@ export const EntryTitle = ({
entryId,
useShallow((state) => {
/// keep-sorted
const { author, authorAvatar, authorUrl, feedId, inboxHandle, publishedAt, tags, title } =
state
const { author, authorAvatar, authorUrl, feedId, inboxHandle, publishedAt, title } = state
const attachments = state.attachments || []
const { duration_in_seconds } =
@@ -61,7 +59,6 @@ export const EntryTitle = ({
firstPhotoUrl,
inboxId: inboxHandle,
publishedAt,
tags: tags ?? null,
title,
}
}),
@@ -185,7 +182,6 @@ export const EntryTitle = ({
)}
</div>
</div>
<EntryTags tags={entry.tags} feedId={entry.feedId} className="mt-1" />
{/* Recent Readers */}
{!noRecentReader && !hideRecentReader && <EntryReadHistory entryId={entryId} />}
</div>
@@ -1,146 +0,0 @@
import { ScrollArea } from "@follow/components/ui/scroll-area/ScrollArea.js"
import { SegmentGroup, SegmentItem } from "@follow/components/ui/segment/index.js"
import { FeedViewType } from "@follow/constants"
import type { EntryTagSummary } from "@follow/database/schemas/types"
import { useEntryTagsQuery } from "@follow/store/entry/hooks"
import { cn } from "@follow/utils"
import { useMemo, useState } from "react"
import { useTranslation } from "react-i18next"
import { titleCase } from "title-case"
import { useModalStack } from "~/components/ui/modal/stacked/hooks"
import { EntryItemSkeleton } from "../entry-column/EntryItemSkeleton"
import { EntryItem } from "../entry-column/item"
type TagKind = "category" | "topic"
interface EntryTagsProps {
tags?: EntryTagSummary | null
feedId?: string | null
size?: "sm" | "md"
className?: string
}
const normalizeLabel = (label: string) => titleCase(label.replaceAll(/[_-]+/g, " "))
const chipClassName: Record<NonNullable<EntryTagsProps["size"]>, string> = {
sm: "px-2 py-0.5 text-[11px]",
md: "px-2.5 py-1 text-xs",
}
type TagItem = { label: string; kind: TagKind }
const TagEntriesModal = ({ tag, feedId }: { tag: TagItem; feedId?: string | null }) => {
const { t } = useTranslation()
const [scope, setScope] = useState<"current" | "all">("current")
const currentFeedId = scope === "current" ? feedId : undefined
const { data, isLoading } = useEntryTagsQuery({
feedId: currentFeedId,
tag:
tag.kind === "category"
? { kind: "schemaOrgCategory", value: tag.label }
: { kind: "mediaTopic", value: tag.label },
})
const entryIds = data ?? []
return (
<div className="-mx-4 -mb-4 w-[min(720px,calc(100vw-2rem))] space-y-4">
<div className="flex flex-col gap-3 px-4 sm:flex-row sm:items-center sm:justify-between">
<div className="text-sm text-text-secondary">
Entries tagged with{" "}
<span className="font-medium text-text">{normalizeLabel(tag.label)}</span>
</div>
{feedId && (
<SegmentGroup value={scope} onValueChanged={(v) => setScope(v as "current" | "all")}>
<SegmentItem value="current" label={t("entry_tags.current_feed")} />
<SegmentItem value="all" label={t("entry_tags.all_feeds")} />
</SegmentGroup>
)}
</div>
{isLoading ? (
<div className="space-y-2">
<EntryItemSkeleton view={FeedViewType.All} count={3} />
</div>
) : entryIds.length === 0 ? (
<div className="flex flex-col items-center justify-center gap-2 py-6 text-sm text-text-secondary">
<i className="i-mgc-inbox-cute-re text-xl" />
<span>No entries found for this tag.</span>
</div>
) : (
<ScrollArea rootClassName="h-[70vh] w-full" flex viewportClassName="pb-4">
{entryIds.map((entryId) => (
<EntryItem key={entryId} entryId={entryId} view={FeedViewType.All} />
))}
</ScrollArea>
)}
</div>
)
}
export const EntryTags = ({
tags,
size = "md",
className,
feedId,
}: EntryTagsProps) => {
const { present } = useModalStack()
const items = useMemo(() => {
if (!tags) return []
const list: TagItem[] = []
if (tags.schemaOrgCategory) {
list.push({ label: tags.schemaOrgCategory, kind: "category" })
}
if (Array.isArray(tags.mediaTopics)) {
for (const topic of tags.mediaTopics) {
if (topic) {
list.push({ label: topic, kind: "topic" })
}
}
}
return list
}, [tags])
if (!tags) return null
if (items.length === 0) return null
const handleClick = (item: TagItem, event: React.MouseEvent) => {
event.stopPropagation()
event.preventDefault()
if (!feedId) return
present({
title: `Tag · ${normalizeLabel(item.label)}`,
content: () => <TagEntriesModal tag={item} feedId={feedId} />,
clickOutsideToDismiss: true,
overlay: true,
modal: true,
modalClassName: "max-w-[760px]",
})
}
return (
<div
className={cn("flex min-w-0 flex-wrap items-center gap-1.5 text-text-secondary", className)}
>
{items.map((item) => (
<button
key={`${item.kind}-${item.label}`}
type="button"
onClick={(event) => handleClick(item, event)}
className={cn(
"cursor-pointer truncate rounded-full bg-fill-secondary text-text-secondary transition-colors duration-200 hover:bg-fill-tertiary hover:text-text",
chipClassName[size],
)}
>
{item.kind === "category"
? `Category · ${normalizeLabel(item.label)}`
: normalizeLabel(item.label)}
</button>
))}
</div>
)
}
+1 -1
View File
@@ -87,5 +87,5 @@
"vite-tsconfig-paths": "5.1.4"
},
"productName": "Folo",
"mainHash": "b8ff378bbffadb942011f62fa49e53a92ed7c9c1bcd66d4052af84cad69706d8"
"mainHash": "517f2ce0e68cffd3da04d1eb27952c0d67fa21509c8bc2bc310f470d9e4df7ff"
}
-2
View File
@@ -209,8 +209,6 @@
"entry_list_header.show_unread_only": "Show unread Only",
"entry_list_header.timeline_summary": "Summarize timeline",
"entry_list_header.unread": "unread",
"entry_tags.all_feeds": "All Feeds",
"entry_tags.current_feed": "Current Feed",
"feed.actions.follow": "Follow",
"feed.actions.followed": "Followed",
"feed.followsAndFeeds": "{{subscriptionCount}} {{subscriptionNoun}} and {{feedsCount}} {{feedsNoun}} on {{appName}}",
-2
View File
@@ -208,8 +208,6 @@
"entry_list_header.show_unread_only": "未読のみ表示",
"entry_list_header.timeline_summary": "タイムラインを要約",
"entry_list_header.unread": "未読",
"entry_tags.all_feeds": "すべてのフィード",
"entry_tags.current_feed": "現在のフィード",
"feed.actions.follow": "フォロー",
"feed.actions.followed": "フォロー済み",
"feed.followsAndFeeds": "{{subscriptionCount}} {{subscriptionNoun}} と {{feedsCount}} {{feedsNoun}} on {{appName}}",
-2
View File
@@ -208,8 +208,6 @@
"entry_list_header.show_unread_only": "仅显示未读",
"entry_list_header.timeline_summary": "总结时间线",
"entry_list_header.unread": "未读",
"entry_tags.all_feeds": "全部订阅源",
"entry_tags.current_feed": "当前订阅源",
"feed.actions.follow": "订阅",
"feed.actions.followed": "已订阅",
"feed.followsAndFeeds": "在 {{appName}} 上有 {{subscriptionCount}} 个{{subscriptionNoun}}和 {{feedsCount}} 个{{feedsNoun}}",
-2
View File
@@ -199,8 +199,6 @@
"entry_list_header.show_unread_only": "僅顯示未讀",
"entry_list_header.timeline_summary": "摘要時間線",
"entry_list_header.unread": "未讀",
"entry_tags.all_feeds": "全部訂閱源",
"entry_tags.current_feed": "當前訂閱源",
"feed.actions.follow": "跟隨",
"feed.actions.followed": "已跟隨",
"feed.followsAndFeeds": "在 {{appName}} 上有 {{subscriptionCount}} 個 {{subscriptionNoun}} 和 {{feedsCount}} 個 {{feedsNoun}}",
@@ -0,0 +1 @@
ALTER TABLE `entries` DROP COLUMN `tags`;
@@ -0,0 +1,947 @@
{
"version": "6",
"dialect": "sqlite",
"id": "f6cb39bb-84e8-47fd-90d8-373f18bd6472",
"prevId": "31afa146-88d1-49ab-a139-0d34d54f8526",
"tables": {
"ai_chat_messages": {
"name": "ai_chat_messages",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"chat_id": {
"name": "chat_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"role": {
"name": "role",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(unixepoch() * 1000)"
},
"metadata": {
"name": "metadata",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"status": {
"name": "status",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "'completed'"
},
"finished_at": {
"name": "finished_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"message_parts": {
"name": "message_parts",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {
"idx_ai_chat_messages_chat_id_created_at": {
"name": "idx_ai_chat_messages_chat_id_created_at",
"columns": ["chat_id", "created_at"],
"isUnique": false
},
"idx_ai_chat_messages_status": {
"name": "idx_ai_chat_messages_status",
"columns": ["status"],
"isUnique": false
},
"idx_ai_chat_messages_chat_id_role": {
"name": "idx_ai_chat_messages_chat_id_role",
"columns": ["chat_id", "role"],
"isUnique": false
}
},
"foreignKeys": {
"ai_chat_messages_chat_id_ai_chat_sessions_id_fk": {
"name": "ai_chat_messages_chat_id_ai_chat_sessions_id_fk",
"tableFrom": "ai_chat_messages",
"tableTo": "ai_chat_sessions",
"columnsFrom": ["chat_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"ai_chat_sessions": {
"name": "ai_chat_sessions",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(unixepoch() * 1000)"
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(unixepoch() * 1000)"
},
"is_local": {
"name": "is_local",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": false
}
},
"indexes": {
"idx_ai_chat_sessions_updated_at": {
"name": "idx_ai_chat_sessions_updated_at",
"columns": ["updated_at"],
"isUnique": false
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"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
},
"source_content": {
"name": "source_content",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"readability_updated_at": {
"name": "readability_updated_at",
"type": "integer",
"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
},
"sources": {
"name": "sources",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"settings": {
"name": "settings",
"type": "text",
"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
},
"subscription_count": {
"name": "subscription_count",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"updates_per_week": {
"name": "updates_per_week",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"latest_entry_published_at": {
"name": "latest_entry_published_at",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"tip_users": {
"name": "tip_users",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"published_at": {
"name": "published_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"images": {
"name": "images",
"columns": {
"url": {
"name": "url",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"colors": {
"name": "colors",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(unixepoch() * 1000)"
}
},
"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
},
"secret": {
"name": "secret",
"type": "text",
"primaryKey": false,
"notNull": true,
"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": false,
"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
},
"subscription_count": {
"name": "subscription_count",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"purchase_amount": {
"name": "purchase_amount",
"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
},
"hide_from_timeline": {
"name": "hide_from_timeline",
"type": "integer",
"primaryKey": false,
"notNull": false,
"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": {}
},
"summaries": {
"name": "summaries",
"columns": {
"entry_id": {
"name": "entry_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"summary": {
"name": "summary",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"readability_summary": {
"name": "readability_summary",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"language": {
"name": "language",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {
"unq": {
"name": "unq",
"columns": ["entry_id", "language"],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"translations": {
"name": "translations",
"columns": {
"entry_id": {
"name": "entry_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"language": {
"name": "language",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"content": {
"name": "content",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"readability_content": {
"name": "readability_content",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"translation-unique-index": {
"name": "translation-unique-index",
"columns": ["entry_id", "language"],
"isUnique": true
}
},
"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": false,
"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": false,
"autoincrement": false
},
"email_verified": {
"name": "email_verified",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"bio": {
"name": "bio",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"website": {
"name": "website",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"social_links": {
"name": "social_links",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
}
},
"views": {},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}
@@ -260,6 +260,13 @@
"when": 1761930812300,
"tag": "0036_entry_tag_summary",
"breakpoints": true
},
{
"idx": 37,
"version": "6",
"when": 1768727702017,
"tag": "0037_bored_the_leader",
"breakpoints": true
}
]
}
@@ -37,6 +37,7 @@ import m0033 from "./0033_shiny_sebastian_shaw.sql"
import m0034 from "./0034_curly_darkstar.sql"
import m0035 from "./0035_last_valeria_richards.sql"
import m0036 from "./0036_entry_tag_summary.sql"
import m0037 from "./0037_bored_the_leader.sql"
import journal from "./meta/_journal.json"
export default {
@@ -79,5 +80,6 @@ export default {
m0034,
m0035,
m0036,
m0037,
},
}
@@ -4,13 +4,7 @@ import type { EntrySettings } from "@follow-app/client-sdk"
import { sql } from "drizzle-orm"
import { index, integer, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core"
import type {
AttachmentsModel,
EntryTagSummary,
ExtraModel,
ImageColorsResult,
MediaModel,
} from "./types"
import type { AttachmentsModel, ExtraModel, ImageColorsResult, MediaModel } from "./types"
export const feedsTable = sqliteTable("feeds", {
id: text("id").primaryKey(),
@@ -107,7 +101,6 @@ export const entriesTable = sqliteTable("entries", {
attachments: text("attachments", { mode: "json" }).$type<AttachmentsModel[]>(),
extra: text("extra", { mode: "json" }).$type<ExtraModel>(),
language: text("language"),
tags: text("tags", { mode: "json" }).$type<EntryTagSummary>(),
feedId: text("feed_id"),
@@ -60,11 +60,6 @@ export type ExtraModel = {
title_keyword?: string
}
export type EntryTagSummary = {
schemaOrgCategory: string | null
mediaTopics: string[]
}
// export { ImageColorsResult } from "react-native-image-colors"
interface AndroidImageColors {
@@ -219,37 +219,6 @@ export const useEntryIdsByListId = (listId: string | undefined) => {
return useEntryStore(useCallback((state) => getEntryIdsByListIdSelector(state)(listId), [listId]))
}
export const useEntryTagsQuery = ({
feedId,
tag,
match = "any",
withContent,
}: {
feedId?: string | null
tag?: { kind: "schemaOrgCategory" | "mediaTopic"; value: string }
match?: "any" | "all"
withContent?: boolean
}) => {
const schemaOrgCategories = tag?.kind === "schemaOrgCategory" ? [tag.value] : undefined
const mediaTopics = tag?.kind === "mediaTopic" ? [tag.value] : undefined
return useQuery({
queryKey: ["entry-tags", feedId, tag?.kind, tag?.value, match, withContent],
enabled: !!tag?.value,
queryFn: () =>
entrySyncServices.fetchEntriesByTags({
feedId: feedId ?? undefined,
match,
tags: {
schemaOrgCategories,
mediaTopics,
},
withContent,
}),
select: (res) => res.data?.map((entry) => entry.entries.id) ?? [],
})
}
export const useEntryIsInbox = (entryId: string) => {
return useEntryStore(useCallback((state) => getEntryIsInboxSelector(state)(entryId), [entryId]))
}
@@ -318,26 +318,6 @@ class EntryActions implements Hydratable, Resetable {
await tx.run()
}
updateEntryTagsInSession({ entryId, tags }: { entryId: EntryId; tags: EntryModel["tags"] }) {
immerSet((draft) => {
const entry = draft.data[entryId]
if (!entry) return
entry.tags = tags ?? null
})
}
async updateEntryTags({ entryId, tags }: { entryId: EntryId; tags: EntryModel["tags"] }) {
const tx = createTransaction()
const nextTags = tags ?? null
tx.store(() => {
this.updateEntryTagsInSession({ entryId, tags: nextTags })
})
tx.persist(() => EntryService.patch({ id: entryId, tags: nextTags }))
await tx.run()
}
markEntryReadStatusInSession({
entryIds,
ids,
@@ -554,9 +534,6 @@ class EntrySyncServices {
entry.content = entryInDB.content
entry.readabilityContent = entryInDB.readabilityContent
entry.readabilityUpdatedAt = entryInDB.readabilityUpdatedAt
if (!entry.tags && entryInDB.tags) {
entry.tags = entryInDB.tags
}
}
}
@@ -577,56 +554,6 @@ class EntrySyncServices {
return res
}
async fetchEntriesByTags({
feedId,
tags,
match = "any",
limit = 30,
cursor,
withContent,
}: {
feedId?: string
tags: {
schemaOrgCategories?: string[]
mediaTopics?: string[]
}
match?: "any" | "all"
limit?: number
cursor?: string
withContent?: boolean
}) {
const res = await api().entries.tagsQuery({
feedId,
tags,
match,
limit,
cursor,
withContent,
})
const entries = apiMorph.toEntryList(res.data)
const entriesInDB = await EntryService.getEntryMany(entries.map((e) => e.id))
for (const entry of entries) {
const entryInDB = entriesInDB.find((e) => e.id === entry.id)
if (entryInDB) {
entry.content = entryInDB.content
entry.readabilityContent = entryInDB.readabilityContent
entry.readabilityUpdatedAt = entryInDB.readabilityUpdatedAt
if (!entry.tags && entryInDB.tags) {
entry.tags = entryInDB.tags
}
}
}
await entryActions.upsertMany(entries)
const dataFeeds = res.data?.map((e) => e.feeds).filter((f) => f.type === "feed")
const feeds = dataFeeds?.map((f) => apiMorph.toFeed(f)) ?? []
feedActions.upsertMany(feeds)
return res
}
async fetchEntryDetail(entryId: EntryId | undefined, isInbox?: boolean) {
if (!isBizId(entryId)) return null
@@ -651,15 +578,6 @@ class EntrySyncServices {
readabilityContent: entry.readabilityContent,
})
}
const apiTags = entry?.tags ?? null
const storedTags = currentEntry?.tags ?? null
if (JSON.stringify(storedTags) !== JSON.stringify(apiTags)) {
await entryActions.updateEntryTags({
entryId,
tags: apiTags,
})
}
}
return entry
}
-4
View File
@@ -49,7 +49,6 @@ class APIMorph {
toEntry(data?: InboxEntryGetResponse["data"] | EntryGetByIdResponse["data"]): EntryModel | null {
if (!data) return null
const resolvedTags = "tags" in data.entries ? data.entries.tags : null
return {
id: data.entries.id,
title: data.entries.title,
@@ -66,7 +65,6 @@ class APIMorph {
media: data.entries.media ?? null,
categories: data.entries.categories ?? null,
attachments: data.entries.attachments ?? null,
tags: resolvedTags,
extra: data.entries.extra
? {
links: data.entries.extra.links ?? undefined,
@@ -197,7 +195,6 @@ class APIMorph {
toEntryList(data?: InboxListEntry[] | EntryWithFeed[]): EntryModel[] {
const entries: EntryModel[] = []
for (const item of data ?? []) {
const resolvedTags = "tags" in item.entries ? item.entries.tags : null
entries.push({
id: item.entries.id,
title: item.entries.title,
@@ -214,7 +211,6 @@ class APIMorph {
media: item.entries.media ?? null,
categories: item.entries.categories ?? null,
attachments: item.entries.attachments ?? null,
tags: resolvedTags,
extra: item.entries.extra
? {
links: item.entries.extra.links ?? undefined,
@@ -11,7 +11,6 @@ class DbStoreMorph {
toEntryModel(entry: EntrySchema): EntryModel {
return {
...entry,
tags: entry.tags ?? null,
}
}
}
@@ -20,7 +20,6 @@ class StoreDbMorph {
toEntrySchema(entry: EntryModel): EntrySchema {
return {
...entry,
tags: entry.tags ?? null,
}
}
}