feat: server side readability (#3498)
This commit is contained in:
parent
6d9af8aeb1
commit
cf6d7567bc
|
|
@ -57,6 +57,12 @@ export const useEntryIsInReadability = (entryId?: string) =>
|
|||
[entryId],
|
||||
)
|
||||
|
||||
export const useEntryIsInReadabilitySuccess = (entryId?: string) =>
|
||||
useReadabilityStatusSelector(
|
||||
(map) => (entryId ? map[entryId] === ReadabilityStatus.SUCCESS : false),
|
||||
[entryId],
|
||||
)
|
||||
|
||||
export const useEntryInReadabilityStatus = (entryId?: string) =>
|
||||
useReadabilityStatusSelector(
|
||||
(map) => (entryId ? map[entryId] || ReadabilityStatus.INITIAL : ReadabilityStatus.INITIAL),
|
||||
|
|
|
|||
|
|
@ -6,22 +6,22 @@ import { useMemo } from "react"
|
|||
import { useShowAISummaryAuto, useShowAISummaryOnce } from "~/atoms/ai-summary"
|
||||
import { useShowAITranslationAuto, useShowAITranslationOnce } from "~/atoms/ai-translation"
|
||||
import {
|
||||
getReadabilityContent,
|
||||
getReadabilityStatus,
|
||||
isInReadability,
|
||||
ReadabilityStatus,
|
||||
setReadabilityContent,
|
||||
setReadabilityStatus,
|
||||
useEntryInReadabilityStatus,
|
||||
useEntryIsInReadability,
|
||||
} from "~/atoms/readability"
|
||||
import { useShowSourceContent } from "~/atoms/source-content"
|
||||
import { useUserRole, whoami } from "~/atoms/user"
|
||||
import { shortcuts } from "~/constants/shortcuts"
|
||||
import { tipcClient } from "~/lib/client"
|
||||
import { apiClient } from "~/lib/api-fetch"
|
||||
import { COMMAND_ID } from "~/modules/command/commands/id"
|
||||
import { useRunCommandFn } from "~/modules/command/hooks/use-command"
|
||||
import type { FollowCommandId } from "~/modules/command/types"
|
||||
import { useToolbarOrderMap } from "~/modules/customize-toolbar/hooks"
|
||||
import { useEntry } from "~/store/entry"
|
||||
import { getEntry, useEntry } from "~/store/entry"
|
||||
import { useFeedById } from "~/store/feed"
|
||||
import { useInboxById } from "~/store/inbox"
|
||||
|
||||
|
|
@ -35,24 +35,39 @@ export const toggleEntryReadability = async ({ id, url }: { id: string; url: str
|
|||
setReadabilityStatus({
|
||||
[id]: ReadabilityStatus.WAITING,
|
||||
})
|
||||
const result = await tipcClient
|
||||
?.readability({
|
||||
url,
|
||||
})
|
||||
.catch(() => {
|
||||
setReadabilityStatus({
|
||||
[id]: ReadabilityStatus.FAILURE,
|
||||
})
|
||||
})
|
||||
try {
|
||||
let data = getReadabilityContent()[id]
|
||||
if (!data) {
|
||||
const entry = getEntry(id)
|
||||
if (
|
||||
entry &&
|
||||
"readabilityContent" in entry.entries &&
|
||||
entry.entries.readabilityContent !== null
|
||||
) {
|
||||
data = { content: entry.entries.readabilityContent }
|
||||
}
|
||||
|
||||
if (result) {
|
||||
const status = getReadabilityStatus()[id]
|
||||
if (status !== ReadabilityStatus.WAITING) return
|
||||
if (!data) {
|
||||
const result = await apiClient.entries.readability.$get({ query: { id } })
|
||||
if (result.data) {
|
||||
data = result.data
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (data) {
|
||||
const status = getReadabilityStatus()[id]
|
||||
if (status !== ReadabilityStatus.WAITING) return
|
||||
setReadabilityStatus({
|
||||
[id]: ReadabilityStatus.SUCCESS,
|
||||
})
|
||||
setReadabilityContent({
|
||||
[id]: data,
|
||||
})
|
||||
}
|
||||
} catch {
|
||||
setReadabilityStatus({
|
||||
[id]: ReadabilityStatus.SUCCESS,
|
||||
})
|
||||
setReadabilityContent({
|
||||
[id]: result,
|
||||
[id]: ReadabilityStatus.FAILURE,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
|
|
@ -69,6 +84,12 @@ export type EntryActionItem = {
|
|||
shortcut?: string
|
||||
active?: boolean
|
||||
disabled?: boolean
|
||||
notice?: boolean
|
||||
entryId: string
|
||||
}
|
||||
|
||||
function hasHTMLTags(text?: string | null): boolean {
|
||||
return /<[^>]+>/.test(text || "")
|
||||
}
|
||||
|
||||
export const useEntryActions = ({
|
||||
|
|
@ -81,7 +102,7 @@ export const useEntryActions = ({
|
|||
compact?: boolean
|
||||
}) => {
|
||||
const entry = useEntry(entryId)
|
||||
const entryReadabilityStatus = useEntryInReadabilityStatus(entry?.entries.id)
|
||||
const isEntryInReadability = useEntryIsInReadability(entry?.entries.id)
|
||||
const imageLength = entry?.entries.media?.filter((a) => a.type === "photo").length || 0
|
||||
const feed = useFeedById(entry?.feedId, (feed) => {
|
||||
return {
|
||||
|
|
@ -94,6 +115,7 @@ export const useEntryActions = ({
|
|||
const inList = !!listId
|
||||
const inbox = useInboxById(entry?.inboxId)
|
||||
const isInbox = !!inbox
|
||||
const isContentContainsHTMLTags = hasHTMLTags(entry?.entries.content)
|
||||
|
||||
const isShowSourceContent = useShowSourceContent()
|
||||
const isShowAISummaryAuto = useShowAISummaryAuto(entry)
|
||||
|
|
@ -231,23 +253,36 @@ export const useEntryActions = ({
|
|||
onClick: runCmdFn(COMMAND_ID.entry.readability, [
|
||||
{ entryId, entryUrl: entry?.entries.url },
|
||||
]),
|
||||
hide: !IN_ELECTRON || compact || (view && views[view]!.wideMode) || !entry?.entries.url,
|
||||
active: isInReadability(entryReadabilityStatus),
|
||||
hide:
|
||||
!!entry.settings?.readability ||
|
||||
compact ||
|
||||
(view && views[view]!.wideMode) ||
|
||||
!entry?.entries.url,
|
||||
active: isEntryInReadability,
|
||||
notice: !isContentContainsHTMLTags && !isEntryInReadability,
|
||||
},
|
||||
{
|
||||
id: COMMAND_ID.settings.customizeToolbar,
|
||||
onClick: runCmdFn(COMMAND_ID.settings.customizeToolbar, []),
|
||||
},
|
||||
].filter((config) => !config.hide)
|
||||
]
|
||||
.filter((config) => !config.hide)
|
||||
.map((config) => {
|
||||
return {
|
||||
...config,
|
||||
entryId,
|
||||
}
|
||||
})
|
||||
}, [
|
||||
compact,
|
||||
entry?.collections,
|
||||
entry?.entries.content,
|
||||
entry?.entries.url,
|
||||
entry?.read,
|
||||
entry?.settings?.readability,
|
||||
entry?.view,
|
||||
entryId,
|
||||
entryReadabilityStatus,
|
||||
isEntryInReadability,
|
||||
feed?.id,
|
||||
feed?.ownerUserId,
|
||||
hasEntry,
|
||||
|
|
@ -259,6 +294,7 @@ export const useEntryActions = ({
|
|||
isShowAITranslationAuto,
|
||||
isShowAITranslationOnce,
|
||||
isShowSourceContent,
|
||||
isContentContainsHTMLTags,
|
||||
runCmdFn,
|
||||
userRole,
|
||||
view,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import type { SupportedActionLanguage } from "@follow/shared"
|
|||
import { ACTION_LANGUAGE_MAP } from "@follow/shared"
|
||||
import { franc } from "franc-min"
|
||||
|
||||
import { getReadabilityContent } from "~/atoms/readability"
|
||||
import type { FlatEntryModel } from "~/store/entry"
|
||||
|
||||
import { apiClient } from "./api-fetch"
|
||||
|
|
@ -57,6 +58,16 @@ export async function translate({
|
|||
}
|
||||
|
||||
fields = fields.filter((field) => {
|
||||
if (language && field === "readabilityContent") {
|
||||
const content = getReadabilityContent()[entry.entries.id]?.content
|
||||
if (!content) return false
|
||||
const isLanguageMatch = checkLanguage({
|
||||
content,
|
||||
language,
|
||||
})
|
||||
return !isLanguageMatch
|
||||
}
|
||||
|
||||
if (language && entry.entries[field]) {
|
||||
const isLanguageMatch = checkLanguage({
|
||||
content: entry.entries[field],
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { cn } from "@follow/utils/utils"
|
|||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import { useShowAISummary } from "~/atoms/ai-summary"
|
||||
import { useEntryIsInReadabilitySuccess } from "~/atoms/readability"
|
||||
import { useActionLanguage } from "~/atoms/settings/general"
|
||||
import { CopyButton } from "~/components/ui/button/CopyButton"
|
||||
import { useAuthQuery } from "~/hooks/common"
|
||||
|
|
@ -12,12 +13,14 @@ import { useEntry } from "~/store/entry"
|
|||
export function AISummary({ entryId }: { entryId: string }) {
|
||||
const { t } = useTranslation()
|
||||
const entry = useEntry(entryId)
|
||||
const isInReadabilitySuccess = useEntryIsInReadabilitySuccess(entryId)
|
||||
const showAISummary = useShowAISummary(entry)
|
||||
const actionLanguage = useActionLanguage()
|
||||
const summary = useAuthQuery(
|
||||
Queries.ai.summary({
|
||||
entryId,
|
||||
language: actionLanguage,
|
||||
target: isInReadabilitySuccess ? "readabilityContent" : "content",
|
||||
}),
|
||||
{
|
||||
enabled: showAISummary,
|
||||
|
|
|
|||
|
|
@ -39,6 +39,8 @@ export const EntryHeaderActions = ({
|
|||
onClick={config.onClick}
|
||||
shortcut={config.shortcut}
|
||||
clickableDisabled={config.disabled}
|
||||
tooltipDefaultOpen={config.notice}
|
||||
id={`${config.entryId}/${config.id}`}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -2,14 +2,17 @@ import { MemoedDangerousHTMLStyle } from "@follow/components/common/MemoedDanger
|
|||
import { ScrollArea } from "@follow/components/ui/scroll-area/index.js"
|
||||
import { useTitle } from "@follow/hooks"
|
||||
import type { FeedModel, InboxModel } from "@follow/models/types"
|
||||
import { IN_ELECTRON } from "@follow/shared/constants"
|
||||
import { stopPropagation } from "@follow/utils/dom"
|
||||
import { cn } from "@follow/utils/utils"
|
||||
import { ErrorBoundary } from "@sentry/react"
|
||||
import * as React from "react"
|
||||
import { useEffect, useMemo, useRef } from "react"
|
||||
|
||||
import { useEntryIsInReadability } from "~/atoms/readability"
|
||||
import {
|
||||
useEntryIsInReadability,
|
||||
useEntryIsInReadabilitySuccess,
|
||||
useEntryReadabilityContent,
|
||||
} from "~/atoms/readability"
|
||||
import { useUISettingKey } from "~/atoms/settings/ui"
|
||||
import { ShadowDOM } from "~/components/common/ShadowDOM"
|
||||
import { useInPeekModal } from "~/components/ui/modal/inspire/PeekModal"
|
||||
|
|
@ -35,7 +38,7 @@ import {
|
|||
ContainerToc,
|
||||
NoContent,
|
||||
ReadabilityAutoToggleEffect,
|
||||
ReadabilityContent,
|
||||
ReadabilityNotice,
|
||||
RenderError,
|
||||
TitleMetaHandler,
|
||||
ViewSourceContentAutoToggleEffect,
|
||||
|
|
@ -62,11 +65,13 @@ export const EntryContent: Component<EntryContentProps> = ({
|
|||
staleTime: 300_000,
|
||||
},
|
||||
)
|
||||
const readabilityContent = useEntryReadabilityContent(entryId)
|
||||
|
||||
const readerFontFamily = useUISettingKey("readerFontFamily")
|
||||
const view = useRouteParamsSelector((route) => route.view)
|
||||
|
||||
const isInReadabilityMode = useEntryIsInReadability(entryId)
|
||||
const isReadabilitySuccess = useEntryIsInReadabilitySuccess(entryId)
|
||||
const scrollerRef = useRef<HTMLDivElement>(null)
|
||||
useEffect(() => {
|
||||
scrollerRef.current?.scrollTo(0, 0)
|
||||
|
|
@ -114,14 +119,21 @@ export const EntryContent: Component<EntryContentProps> = ({
|
|||
)
|
||||
const customCSS = useUISettingKey("customCSS")
|
||||
|
||||
const contentTranslated = useEntryTranslation({ entry, extraFields: ["content"] })
|
||||
const contentTranslated = useEntryTranslation({
|
||||
entry,
|
||||
extraFields: isReadabilitySuccess ? ["readabilityContent"] : ["content"],
|
||||
})
|
||||
|
||||
const isInPeekModal = useInPeekModal()
|
||||
|
||||
if (!entry) return null
|
||||
|
||||
const entryContent = entry?.entries.content ?? data?.entries.content
|
||||
const translatedContent = contentTranslated.data?.content
|
||||
const entryContent = isInReadabilityMode
|
||||
? readabilityContent?.content
|
||||
: (entry?.entries.content ?? data?.entries.content)
|
||||
const translatedContent = isInReadabilityMode
|
||||
? contentTranslated.data?.readabilityContent
|
||||
: contentTranslated.data?.content
|
||||
const content = translatedContent || entryContent
|
||||
|
||||
const isInbox = !!inbox
|
||||
|
|
@ -166,34 +178,31 @@ export const EntryContent: Component<EntryContentProps> = ({
|
|||
<TitleMetaHandler entryId={entry.entries.id} />
|
||||
<AISummary entryId={entry.entries.id} />
|
||||
<ErrorBoundary fallback={RenderError}>
|
||||
{!isInReadabilityMode ? (
|
||||
<ShadowDOM injectHostStyles={!isInbox}>
|
||||
{!!customCSS && (
|
||||
<MemoedDangerousHTMLStyle>{customCSS}</MemoedDangerousHTMLStyle>
|
||||
)}
|
||||
<EntryContentHTMLRenderer
|
||||
view={view}
|
||||
feedId={feed?.id}
|
||||
entryId={entryId}
|
||||
mediaInfo={mediaInfo}
|
||||
noMedia={noMedia}
|
||||
accessory={contentAccessories}
|
||||
as="article"
|
||||
className="prose dark:prose-invert prose-h1:text-[1.6em] prose-h1:font-bold !max-w-full hyphens-auto"
|
||||
style={stableRenderStyle}
|
||||
renderInlineStyle={readerRenderInlineStyle}
|
||||
>
|
||||
{content}
|
||||
</EntryContentHTMLRenderer>
|
||||
</ShadowDOM>
|
||||
) : (
|
||||
<ReadabilityContent entryId={entryId} feedId={feed.id} />
|
||||
)}
|
||||
<ReadabilityNotice entryId={entryId} />
|
||||
<ShadowDOM injectHostStyles={!isInbox}>
|
||||
{!!customCSS && (
|
||||
<MemoedDangerousHTMLStyle>{customCSS}</MemoedDangerousHTMLStyle>
|
||||
)}
|
||||
<EntryContentHTMLRenderer
|
||||
view={view}
|
||||
feedId={feed?.id}
|
||||
entryId={entryId}
|
||||
mediaInfo={mediaInfo}
|
||||
noMedia={noMedia}
|
||||
accessory={contentAccessories}
|
||||
as="article"
|
||||
className="prose dark:prose-invert prose-h1:text-[1.6em] prose-h1:font-bold !max-w-full hyphens-auto"
|
||||
style={stableRenderStyle}
|
||||
renderInlineStyle={readerRenderInlineStyle}
|
||||
>
|
||||
{content}
|
||||
</EntryContentHTMLRenderer>
|
||||
</ShadowDOM>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</WrappedElementProvider>
|
||||
|
||||
{entry.settings?.readability && IN_ELECTRON && (
|
||||
{entry.settings?.readability && (
|
||||
<ReadabilityAutoToggleEffect id={entry.entries.id} url={entry.entries.url ?? ""} />
|
||||
)}
|
||||
{entry.settings?.sourceContent && <ViewSourceContentAutoToggleEffect />}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { Button, MotionButtonBase } from "@follow/components/ui/button/index.js"
|
|||
import { LoadingWithIcon } from "@follow/components/ui/loading/index.jsx"
|
||||
import { RootPortal } from "@follow/components/ui/portal/index.jsx"
|
||||
import { useScrollViewElement } from "@follow/components/ui/scroll-area/hooks.js"
|
||||
import { IN_ELECTRON, WEB_BUILD } from "@follow/shared/constants"
|
||||
import { WEB_BUILD } from "@follow/shared/constants"
|
||||
import { EventBus } from "@follow/utils/event-bus"
|
||||
import { springScrollTo } from "@follow/utils/scroller"
|
||||
import { cn } from "@follow/utils/utils"
|
||||
|
|
@ -17,12 +17,12 @@ import {
|
|||
ReadabilityStatus,
|
||||
setReadabilityStatus,
|
||||
useEntryInReadabilityStatus,
|
||||
useEntryIsInReadability,
|
||||
useEntryReadabilityContent,
|
||||
} from "~/atoms/readability"
|
||||
import { enableShowSourceContent } from "~/atoms/source-content"
|
||||
import { Toc } from "~/components/ui/markdown/components/Toc"
|
||||
import { toggleEntryReadability } from "~/hooks/biz/useEntryActions"
|
||||
import { useRouteParamsSelector } from "~/hooks/biz/useRouteParams"
|
||||
import { getNewIssueUrl } from "~/lib/issues"
|
||||
import {
|
||||
useIsSoFWrappedElement,
|
||||
|
|
@ -33,7 +33,6 @@ import { useEntry } from "~/store/entry"
|
|||
import { useFeedById } from "~/store/feed"
|
||||
import { useInboxById } from "~/store/inbox"
|
||||
|
||||
import { EntryContentHTMLRenderer } from "../renderer/html"
|
||||
import { setEntryContentScrollToTop, setEntryTitleMeta } from "./atoms"
|
||||
|
||||
export interface EntryContentProps {
|
||||
|
|
@ -85,10 +84,13 @@ export const TitleMetaHandler: Component<{
|
|||
return null
|
||||
}
|
||||
|
||||
export const ReadabilityContent = ({ entryId, feedId }: { entryId: string; feedId: string }) => {
|
||||
export const ReadabilityNotice = ({ entryId }: { entryId: string }) => {
|
||||
const { t } = useTranslation()
|
||||
const result = useEntryReadabilityContent(entryId)
|
||||
const view = useRouteParamsSelector((route) => route.view)
|
||||
const isInReadability = useEntryIsInReadability(entryId)
|
||||
if (!isInReadability) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grow">
|
||||
|
|
@ -103,16 +105,6 @@ export const ReadabilityContent = ({ entryId, feedId }: { entryId: string; feedI
|
|||
<span className="text-sm">{t("entry_content.fetching_content")}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<EntryContentHTMLRenderer
|
||||
view={view}
|
||||
feedId={feedId}
|
||||
entryId={entryId}
|
||||
as="article"
|
||||
className="prose dark:prose-invert prose-h1:text-[1.6em] prose-h1:font-bold hyphens-auto"
|
||||
>
|
||||
{result?.content ?? ""}
|
||||
</EntryContentHTMLRenderer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -134,12 +126,7 @@ export const NoContent: FC<{
|
|||
{(WEB_BUILD || status === ReadabilityStatus.FAILURE) && (
|
||||
<span>{t("entry_content.no_content")}</span>
|
||||
)}
|
||||
{WEB_BUILD && (
|
||||
<div>
|
||||
<span>{t("entry_content.web_app_notice")}</span>
|
||||
</div>
|
||||
)}
|
||||
{!sourceContent && url && IN_ELECTRON && <ReadabilityAutoToggleEffect url={url} id={id} />}
|
||||
{!sourceContent && url && <ReadabilityAutoToggleEffect url={url} id={id} />}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -22,12 +22,21 @@ export const ai = {
|
|||
defineQuery(["translation", entry?.entries.id, view, language, extraFields, part], () =>
|
||||
translate({ entry, view, language, extraFields, part }),
|
||||
),
|
||||
summary: ({ entryId, language }: { entryId: string; language?: SupportedLanguages }) =>
|
||||
defineQuery(["summary", entryId, language], async () => {
|
||||
summary: ({
|
||||
entryId,
|
||||
language,
|
||||
target = "content",
|
||||
}: {
|
||||
entryId: string
|
||||
language?: SupportedLanguages
|
||||
target?: "content" | "readabilityContent"
|
||||
}) =>
|
||||
defineQuery(["summary", entryId, language, target], async () => {
|
||||
const res = await apiClient.ai.summary.$get({
|
||||
query: {
|
||||
id: entryId,
|
||||
language,
|
||||
target,
|
||||
},
|
||||
})
|
||||
return res.data
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE `summaries` ADD `readability_summary` text;--> statement-breakpoint
|
||||
ALTER TABLE `translations` ADD `readability_content` text;
|
||||
|
|
@ -0,0 +1,708 @@
|
|||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "493dc0df-ddca-4c5c-96bc-f74802251bc6",
|
||||
"prevId": "0295d33b-ce34-41aa-a428-1dc63c6bb447",
|
||||
"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
|
||||
},
|
||||
"source_content": {
|
||||
"name": "source_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
|
||||
},
|
||||
"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
|
||||
}
|
||||
},
|
||||
"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": false,
|
||||
"autoincrement": false,
|
||||
"default": "(CURRENT_TIMESTAMP)"
|
||||
}
|
||||
},
|
||||
"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
|
||||
}
|
||||
},
|
||||
"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": {}
|
||||
},
|
||||
"summaries": {
|
||||
"name": "summaries",
|
||||
"columns": {
|
||||
"entry_id": {
|
||||
"name": "entry_id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"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": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"language": {
|
||||
"name": "language",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"title": {
|
||||
"name": "title",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"description": {
|
||||
"name": "description",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"content": {
|
||||
"name": "content",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"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": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"email_verified": {
|
||||
"name": "email_verified",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
"enums": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
},
|
||||
"internal": {
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -141,6 +141,13 @@
|
|||
"when": 1743153830369,
|
||||
"tag": "0019_wonderful_shape",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 20,
|
||||
"version": "6",
|
||||
"when": 1744793226628,
|
||||
"tag": "0020_little_marauders",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import m0016 from "./0016_curious_carnage.sql"
|
|||
import m0017 from "./0017_talented_captain_cross.sql"
|
||||
import m0018 from "./0018_dashing_the_fury.sql"
|
||||
import m0019 from "./0019_wonderful_shape.sql"
|
||||
import m0020 from "./0020_little_marauders.sql"
|
||||
import journal from "./meta/_journal.json"
|
||||
|
||||
export default {
|
||||
|
|
@ -45,5 +46,6 @@ export default {
|
|||
m0017,
|
||||
m0018,
|
||||
m0019,
|
||||
m0020,
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,21 +66,16 @@ export function EntryContentWebView(props: EntryContentWebViewProps) {
|
|||
}, [codeThemeLight, codeThemeDark, mode])
|
||||
|
||||
const entryInWebview = React.useMemo(() => {
|
||||
if (showReadability) {
|
||||
return {
|
||||
...entry,
|
||||
content: entry.readabilityContent,
|
||||
}
|
||||
}
|
||||
const entryContent = showReadability ? entry.readabilityContent : entry.content
|
||||
const translatedContent = showReadability
|
||||
? entry.translation?.readabilityContent
|
||||
: entry.translation?.content
|
||||
const content = showTranslation ? translatedContent || entryContent : entryContent
|
||||
|
||||
if (showTranslation) {
|
||||
return {
|
||||
...entry,
|
||||
content: entry.translation?.content || entry.content,
|
||||
}
|
||||
return {
|
||||
...entry,
|
||||
content,
|
||||
}
|
||||
|
||||
return entry
|
||||
}, [entry, showReadability, showTranslation])
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -109,12 +109,11 @@ export const summariesTable = sqliteTable(
|
|||
{
|
||||
entryId: text("entry_id").notNull().primaryKey(),
|
||||
summary: text("summary").notNull(),
|
||||
readabilitySummary: text("readability_summary"),
|
||||
createdAt: text("created_at").$defaultFn(() => new Date().toISOString()),
|
||||
language: text("language"),
|
||||
},
|
||||
(table) => ({
|
||||
unq: uniqueIndex("unq").on(table.entryId, table.language),
|
||||
}),
|
||||
(t) => [uniqueIndex("unq").on(t.entryId, t.language)],
|
||||
)
|
||||
|
||||
export const translationsTable = sqliteTable(
|
||||
|
|
@ -125,6 +124,7 @@ export const translationsTable = sqliteTable(
|
|||
title: t.text("title").notNull(),
|
||||
description: t.text("description").notNull(),
|
||||
content: t.text("content").notNull(),
|
||||
readabilityContent: t.text("readability_content"),
|
||||
createdAt: t
|
||||
.text("created_at")
|
||||
.notNull()
|
||||
|
|
|
|||
|
|
@ -13,10 +13,13 @@ export const EntryAISummary: FC<{
|
|||
entryId: string
|
||||
}> = ({ entryId }) => {
|
||||
const ctx = useEntryContentContext()
|
||||
const showReadability = useAtomValue(ctx.showReadabilityAtom)
|
||||
const showAISummaryOnce = useAtomValue(ctx.showAISummaryAtom)
|
||||
const showAISummary = useGeneralSettingKey("summary") || showAISummaryOnce
|
||||
const summary = useSummary(entryId)
|
||||
usePrefetchSummary(entryId, { enabled: showAISummary })
|
||||
usePrefetchSummary(entryId, showReadability ? "readabilityContent" : "content", {
|
||||
enabled: showAISummary,
|
||||
})
|
||||
|
||||
const status = useSummaryStore((state) => state.generatingStatus[entryId])
|
||||
if (!showAISummary) return null
|
||||
|
|
@ -24,7 +27,11 @@ export const EntryAISummary: FC<{
|
|||
return (
|
||||
<AISummary
|
||||
className="my-3"
|
||||
summary={summary?.summary || ""}
|
||||
summary={
|
||||
showReadability
|
||||
? summary?.readabilitySummary || summary?.summary || ""
|
||||
: summary?.summary || ""
|
||||
}
|
||||
pending={status === SummaryGeneratingStatus.Pending}
|
||||
error={status === SummaryGeneratingStatus.Error ? "Failed to generate summary" : undefined}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -111,7 +111,10 @@ const HeaderRightActionsImpl = ({
|
|||
if (hasSummary) return
|
||||
|
||||
const hideGlowEffect = showIntelligenceGlowEffect()
|
||||
await summarySyncService.generateSummary(entryId)
|
||||
await summarySyncService.generateSummary(
|
||||
entryId,
|
||||
showReadability ? "readabilityContent" : "content",
|
||||
)
|
||||
hideGlowEffect()
|
||||
}
|
||||
|
||||
|
|
@ -127,6 +130,7 @@ const HeaderRightActionsImpl = ({
|
|||
entryId,
|
||||
language: getGeneralSettings().actionLanguage as SupportedLanguages,
|
||||
withContent: true,
|
||||
target: showReadability ? "readabilityContent" : "content",
|
||||
})
|
||||
setShowTranslation((prev) => !prev)
|
||||
}
|
||||
|
|
@ -173,7 +177,7 @@ const HeaderRightActionsImpl = ({
|
|||
onPress: toggleReadability,
|
||||
active: showReadability,
|
||||
isCheckbox: true,
|
||||
inMenu: true,
|
||||
// inMenu: true,
|
||||
},
|
||||
!showAISummarySetting && {
|
||||
key: "GenerateSummary",
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@ export const EntryTitle = ({ title, entryId }: { title: string; entryId: string
|
|||
className="text-label px-4 text-4xl font-bold leading-snug"
|
||||
source={title}
|
||||
target={translation?.title}
|
||||
inline
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ export const EntryListContentArticle = forwardRef<
|
|||
|
||||
useImperativeHandle(forwardRef, () => ref.current!)
|
||||
|
||||
usePrefetchEntryTranslation(active ? viewableItems.map((item) => item.key) : [])
|
||||
usePrefetchEntryTranslation({ entryIds: active ? viewableItems.map((item) => item.key) : [] })
|
||||
|
||||
return (
|
||||
<TimelineSelectorList
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ export const EntryListContentPicture = forwardRef<
|
|||
onScroll: hackOnScroll,
|
||||
})
|
||||
|
||||
usePrefetchEntryTranslation(active ? viewableItems.map((item) => item.key) : [])
|
||||
usePrefetchEntryTranslation({ entryIds: active ? viewableItems.map((item) => item.key) : [] })
|
||||
|
||||
return (
|
||||
<TimelineSelectorMasonryList
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ export const EntryListContentSocial = forwardRef<
|
|||
onScroll: hackOnScroll,
|
||||
})
|
||||
|
||||
usePrefetchEntryTranslation(active ? viewableItems.map((item) => item.key) : [])
|
||||
usePrefetchEntryTranslation({ entryIds: active ? viewableItems.map((item) => item.key) : [] })
|
||||
|
||||
return (
|
||||
<TimelineSelectorList
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ export const EntryListContentVideo = forwardRef<
|
|||
onScroll: hackOnScroll,
|
||||
})
|
||||
|
||||
usePrefetchEntryTranslation(active ? viewableItems.map((item) => item.key) : [])
|
||||
usePrefetchEntryTranslation({ entryIds: active ? viewableItems.map((item) => item.key) : [] })
|
||||
|
||||
const ListFooterComponent = useMemo(
|
||||
() =>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import { cn } from "@follow/utils"
|
||||
import { useMemo } from "react"
|
||||
import type { TextProps } from "react-native"
|
||||
import { Text, View } from "react-native"
|
||||
|
|
@ -47,19 +46,14 @@ export const EntryTranslation = ({
|
|||
|
||||
return (
|
||||
<View>
|
||||
{nextTarget && (
|
||||
<>
|
||||
<Text {...props} className={className}>
|
||||
{nextTarget}
|
||||
</Text>
|
||||
<Text {...props} className={cn("my-2", className)}>
|
||||
⇋
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
<Text {...props} className={className}>
|
||||
{nextSource}
|
||||
</Text>
|
||||
{nextTarget && (
|
||||
<Text {...props} className={className}>
|
||||
{nextTarget}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -108,7 +108,8 @@ class Morph {
|
|||
title: item.entries.title,
|
||||
url: item.entries.url,
|
||||
content: null,
|
||||
readabilityContent: null,
|
||||
readabilityContent:
|
||||
"readabilityContent" in item.entries ? item.entries.readabilityContent : null,
|
||||
description: item.entries.description,
|
||||
guid: item.entries.guid,
|
||||
author: item.entries.author,
|
||||
|
|
@ -163,7 +164,8 @@ class Morph {
|
|||
title: data.entries.title,
|
||||
url: data.entries.url,
|
||||
content: data.entries.content,
|
||||
readabilityContent: null,
|
||||
readabilityContent:
|
||||
"readabilityContent" in data.entries ? data.entries.readabilityContent : null,
|
||||
description: data.entries.description,
|
||||
guid: data.entries.guid,
|
||||
author: data.entries.author,
|
||||
|
|
|
|||
|
|
@ -34,7 +34,6 @@ export const EntryDetailScreen: NavigationControllerView<{
|
|||
view: FeedViewType
|
||||
}> = ({ entryId, view: viewType }) => {
|
||||
usePrefetchEntryDetail(entryId)
|
||||
usePrefetchEntryTranslation([entryId], true)
|
||||
useAutoMarkAsRead(entryId)
|
||||
const entry = useEntry(entryId)
|
||||
const translation = useEntryTranslation(entryId)
|
||||
|
|
@ -110,6 +109,11 @@ const EntryContentWebViewWithContext = ({ entry }: { entry: EntryWithTranslation
|
|||
const showReadability = useAtomValue(showReadabilityAtom)
|
||||
const translationSetting = useGeneralSettingKey("translation")
|
||||
const showTranslation = useAtomValue(showAITranslationAtom)
|
||||
usePrefetchEntryTranslation({
|
||||
entryIds: [entry.id],
|
||||
withContent: true,
|
||||
target: showReadability ? "readabilityContent" : "content",
|
||||
})
|
||||
return (
|
||||
<EntryContentWebView
|
||||
entry={entry}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,13 @@ import type { SummarySchema } from "../database/schemas/types"
|
|||
|
||||
class SummaryServiceStatic {
|
||||
async insertSummary(data: Omit<SummarySchema, "createdAt">) {
|
||||
const updateExceptEmpty = Object.fromEntries(
|
||||
Object.entries({
|
||||
summary: data.summary,
|
||||
readabilitySummary: data.readabilitySummary,
|
||||
}).filter(([_, value]) => !!value),
|
||||
)
|
||||
|
||||
await db
|
||||
.insert(summariesTable)
|
||||
.values({
|
||||
|
|
@ -14,9 +21,7 @@ class SummaryServiceStatic {
|
|||
})
|
||||
.onConflictDoUpdate({
|
||||
target: [summariesTable.entryId, summariesTable.language],
|
||||
set: {
|
||||
summary: data.summary,
|
||||
},
|
||||
set: updateExceptEmpty,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ class TranslationServiceStatic implements Hydratable, Resetable {
|
|||
title: data.title,
|
||||
description: data.description,
|
||||
content: data.content,
|
||||
readabilityContent: data.readabilityContent,
|
||||
}).filter(([_, value]) => !!value),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { FeedViewType } from "@follow/constants"
|
||||
import { readability } from "@follow/utils"
|
||||
import { debounce } from "es-toolkit/compat"
|
||||
import { fetch as expoFetch } from "expo/fetch"
|
||||
|
||||
|
|
@ -436,8 +435,19 @@ class EntrySyncServices {
|
|||
const entry = honoMorph.toEntry(res.data)
|
||||
if (!currentEntry && entry) {
|
||||
await entryActions.upsertMany([entry])
|
||||
} else if (entry?.content && currentEntry?.content !== entry.content) {
|
||||
await entryActions.updateEntryContent({ entryId, content: entry.content })
|
||||
} else {
|
||||
if (entry?.content && currentEntry?.content !== entry.content) {
|
||||
await entryActions.updateEntryContent({ entryId, content: entry.content })
|
||||
}
|
||||
if (
|
||||
entry?.readabilityContent &&
|
||||
currentEntry?.readabilityContent !== entry.readabilityContent
|
||||
) {
|
||||
await entryActions.updateEntryContent({
|
||||
entryId,
|
||||
readabilityContent: entry.readabilityContent,
|
||||
})
|
||||
}
|
||||
}
|
||||
return entry
|
||||
}
|
||||
|
|
@ -445,8 +455,12 @@ class EntrySyncServices {
|
|||
async fetchEntryReadabilityContent(entryId: EntryId) {
|
||||
const entry = getEntry(entryId)
|
||||
|
||||
if (entry?.url && !entry?.readabilityContent) {
|
||||
const contentByFetch = await readability(entry.url)
|
||||
if (entry?.url && entry?.readabilityContent === null) {
|
||||
const { data: contentByFetch } = await apiClient.entries.readability.$get({
|
||||
query: {
|
||||
id: entryId,
|
||||
},
|
||||
})
|
||||
if (contentByFetch?.content && entry?.readabilityContent !== contentByFetch.content) {
|
||||
await entryActions.updateEntryContent({
|
||||
entryId,
|
||||
|
|
|
|||
|
|
@ -12,11 +12,15 @@ export const useSummaryStatus = (entryId: string) => {
|
|||
return status
|
||||
}
|
||||
|
||||
export const usePrefetchSummary = (entryId: string, options?: { enabled?: boolean }) => {
|
||||
export const usePrefetchSummary = (
|
||||
entryId: string,
|
||||
target: "content" | "readabilityContent",
|
||||
options?: { enabled?: boolean },
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: ["summary", entryId],
|
||||
queryKey: ["summary", entryId, target],
|
||||
queryFn: () => {
|
||||
return summarySyncService.generateSummary(entryId)
|
||||
return summarySyncService.generateSummary(entryId, target)
|
||||
},
|
||||
enabled: options?.enabled,
|
||||
staleTime: 1000 * 60 * 60 * 24,
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ type SummaryModel = Omit<SummarySchema, "createdAt">
|
|||
interface SummaryData {
|
||||
lang?: string
|
||||
summary: string
|
||||
readabilitySummary: string | null
|
||||
lastAccessed: number
|
||||
}
|
||||
|
||||
|
|
@ -41,7 +42,9 @@ class SummaryActions {
|
|||
immerSet((state) => {
|
||||
state.data[summary.entryId] = {
|
||||
lang: summary.language ?? undefined,
|
||||
summary: summary.summary,
|
||||
summary: summary.summary || state.data[summary.entryId]?.summary || "",
|
||||
readabilitySummary:
|
||||
summary.readabilitySummary || state.data[summary.entryId]?.readabilitySummary || null,
|
||||
lastAccessed: now,
|
||||
}
|
||||
})
|
||||
|
|
@ -96,7 +99,7 @@ export const summaryActions = new SummaryActions()
|
|||
class SummarySyncService {
|
||||
private pendingPromises: Record<string, Promise<string>> = {}
|
||||
|
||||
async generateSummary(entryId: string) {
|
||||
async generateSummary(entryId: string, target: "content" | "readabilityContent") {
|
||||
const entry = getEntry(entryId)
|
||||
if (!entry) return
|
||||
|
||||
|
|
@ -116,6 +119,7 @@ class SummarySyncService {
|
|||
query: {
|
||||
id: entryId,
|
||||
language: actionLanguage as SupportedLanguages,
|
||||
target,
|
||||
},
|
||||
})
|
||||
.then((summary) => {
|
||||
|
|
@ -127,7 +131,11 @@ class SummarySyncService {
|
|||
|
||||
state.data[entryId] = {
|
||||
lang: actionLanguage,
|
||||
summary: summary.data,
|
||||
summary: target === "content" ? summary.data : state.data[entryId]?.summary || "",
|
||||
readabilitySummary:
|
||||
target === "readabilityContent"
|
||||
? summary.data
|
||||
: state.data[entryId]?.readabilitySummary || null,
|
||||
lastAccessed: Date.now(),
|
||||
}
|
||||
state.generatingStatus[entryId] = SummaryGeneratingStatus.Success
|
||||
|
|
@ -153,8 +161,9 @@ class SummarySyncService {
|
|||
summaryActions.upsertMany([
|
||||
{
|
||||
entryId,
|
||||
summary,
|
||||
summary: target === "content" ? summary : "",
|
||||
language: actionLanguage ?? null,
|
||||
readabilitySummary: target === "readabilityContent" ? summary : null,
|
||||
},
|
||||
])
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,15 @@ import type { SupportedLanguages } from "@/src/lib/language"
|
|||
import { useEntryList } from "../entry/hooks"
|
||||
import { translationSyncService, useTranslationStore } from "./store"
|
||||
|
||||
export const usePrefetchEntryTranslation = (entryIds: string[], withContent?: boolean) => {
|
||||
export const usePrefetchEntryTranslation = ({
|
||||
entryIds,
|
||||
withContent,
|
||||
target = "content",
|
||||
}: {
|
||||
entryIds: string[]
|
||||
withContent?: boolean
|
||||
target?: "content" | "readabilityContent"
|
||||
}) => {
|
||||
const translation = useGeneralSettingKey("translation")
|
||||
const entryList =
|
||||
useEntryList(entryIds)
|
||||
|
|
@ -18,12 +26,13 @@ export const usePrefetchEntryTranslation = (entryIds: string[], withContent?: bo
|
|||
|
||||
return useQueries({
|
||||
queries: entryList.map((entryId) => ({
|
||||
queryKey: ["translation", entryId, actionLanguage, withContent],
|
||||
queryKey: ["translation", entryId, actionLanguage, withContent, target],
|
||||
queryFn: () =>
|
||||
translationSyncService.generateTranslation({
|
||||
entryId,
|
||||
language: actionLanguage,
|
||||
withContent,
|
||||
target,
|
||||
}),
|
||||
})),
|
||||
})
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ class TranslationActions {
|
|||
title: translation.title,
|
||||
description: translation.description,
|
||||
content: translation.content,
|
||||
readabilityContent: translation.readabilityContent,
|
||||
}
|
||||
|
||||
if (!state.data[translation.entryId]) {
|
||||
|
|
@ -61,17 +62,19 @@ class TranslationSyncService {
|
|||
entryId,
|
||||
language,
|
||||
withContent,
|
||||
target,
|
||||
}: {
|
||||
entryId: string
|
||||
language: SupportedLanguages
|
||||
withContent?: boolean
|
||||
target: "content" | "readabilityContent"
|
||||
}) {
|
||||
const entry = getEntry(entryId)
|
||||
if (!entry) return
|
||||
const translationSession = translationActions.getTranslation(entryId, language)
|
||||
|
||||
const fields = (
|
||||
["title", "description", ...(withContent ? ["content"] : [])] as Array<
|
||||
["title", "description", ...(withContent ? [target] : [])] as Array<
|
||||
"title" | "description" | "content"
|
||||
>
|
||||
).filter((field) => {
|
||||
|
|
@ -100,6 +103,7 @@ class TranslationSyncService {
|
|||
title: res.data.title || "",
|
||||
description: res.data.description || "",
|
||||
content: res.data.content || "",
|
||||
readabilityContent: res.data.readabilityContent || "",
|
||||
}
|
||||
|
||||
await translationActions.upsertMany([translation])
|
||||
|
|
|
|||
|
|
@ -2,4 +2,5 @@ export interface EntryTranslation {
|
|||
title: string
|
||||
description: string
|
||||
content: string
|
||||
readabilityContent: string | null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,12 +7,13 @@ import type { Options } from "react-hotkeys-hook"
|
|||
import { useHotkeys } from "react-hotkeys-hook"
|
||||
|
||||
import { KbdCombined } from "../kbd/Kbd"
|
||||
import { Tooltip, TooltipContent, TooltipPortal, TooltipTrigger } from "../tooltip"
|
||||
import { Tooltip, TooltipContent, TooltipPortal, TooltipRoot, TooltipTrigger } from "../tooltip"
|
||||
|
||||
export interface ActionButtonProps {
|
||||
icon?: React.ReactNode | ((props: { isActive?: boolean; className: string }) => React.ReactNode)
|
||||
tooltip?: React.ReactNode
|
||||
tooltipSide?: "top" | "bottom"
|
||||
tooltipDefaultOpen?: boolean
|
||||
active?: boolean
|
||||
disabled?: boolean
|
||||
clickableDisabled?: boolean
|
||||
|
|
@ -20,6 +21,7 @@ export interface ActionButtonProps {
|
|||
disableTriggerShortcut?: boolean
|
||||
enableHoverableContent?: boolean
|
||||
size?: "sm" | "base" | "lg"
|
||||
id?: string
|
||||
|
||||
/**
|
||||
* @description only trigger shortcut when focus with in `<Focusable />`
|
||||
|
|
@ -43,10 +45,11 @@ export const ActionButton = React.forwardRef<
|
|||
(
|
||||
{
|
||||
icon,
|
||||
|
||||
id,
|
||||
tooltip,
|
||||
className,
|
||||
tooltipSide,
|
||||
tooltipDefaultOpen,
|
||||
children,
|
||||
active,
|
||||
shortcut,
|
||||
|
|
@ -126,19 +129,24 @@ export const ActionButton = React.forwardRef<
|
|||
)}
|
||||
{tooltip ? (
|
||||
<Tooltip disableHoverableContent={!enableHoverableContent}>
|
||||
<TooltipTrigger aria-label={typeof tooltip === "string" ? tooltip : undefined} asChild>
|
||||
{Trigger}
|
||||
</TooltipTrigger>
|
||||
<TooltipPortal>
|
||||
<TooltipContent className="flex items-center gap-1" side={tooltipSide ?? "bottom"}>
|
||||
{tooltip}
|
||||
{!!finalShortcut && (
|
||||
<div className="ml-1">
|
||||
<KbdCombined className="text-foreground/80">{finalShortcut}</KbdCombined>
|
||||
</div>
|
||||
)}
|
||||
</TooltipContent>
|
||||
</TooltipPortal>
|
||||
<TooltipRoot defaultOpen={tooltipDefaultOpen} key={id}>
|
||||
<TooltipTrigger
|
||||
aria-label={typeof tooltip === "string" ? tooltip : undefined}
|
||||
asChild
|
||||
>
|
||||
{Trigger}
|
||||
</TooltipTrigger>
|
||||
<TooltipPortal>
|
||||
<TooltipContent className="flex items-center gap-1" side={tooltipSide ?? "bottom"}>
|
||||
{tooltip}
|
||||
{!!finalShortcut && (
|
||||
<div className="ml-1">
|
||||
<KbdCombined className="text-foreground/80">{finalShortcut}</KbdCombined>
|
||||
</div>
|
||||
)}
|
||||
</TooltipContent>
|
||||
</TooltipPortal>
|
||||
</TooltipRoot>
|
||||
</Tooltip>
|
||||
) : (
|
||||
Trigger
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import * as React from "react"
|
|||
import { tooltipStyle } from "./styles"
|
||||
|
||||
const TooltipProvider = TooltipPrimitive.Provider
|
||||
const TooltipRoot = TooltipPrimitive.Root
|
||||
|
||||
const Tooltip: typeof TooltipProvider = ({ children, ...props }) => (
|
||||
<TooltipProvider {...props}>
|
||||
|
|
@ -45,6 +46,6 @@ const TooltipContent = React.forwardRef<
|
|||
))
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName
|
||||
|
||||
export { Tooltip, TooltipContent, TooltipTrigger }
|
||||
export { Tooltip, TooltipContent, TooltipRoot, TooltipTrigger }
|
||||
|
||||
export { RootPortal as TooltipPortal } from "../portal"
|
||||
|
|
|
|||
Loading…
Reference in New Issue