diff --git a/apps/desktop/src/renderer/src/atoms/readability.ts b/apps/desktop/src/renderer/src/atoms/readability.ts index 961384487..e6d7f1f83 100644 --- a/apps/desktop/src/renderer/src/atoms/readability.ts +++ b/apps/desktop/src/renderer/src/atoms/readability.ts @@ -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), diff --git a/apps/desktop/src/renderer/src/hooks/biz/useEntryActions.tsx b/apps/desktop/src/renderer/src/hooks/biz/useEntryActions.tsx index 518a8d17f..faaba31c2 100644 --- a/apps/desktop/src/renderer/src/hooks/biz/useEntryActions.tsx +++ b/apps/desktop/src/renderer/src/hooks/biz/useEntryActions.tsx @@ -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, diff --git a/apps/desktop/src/renderer/src/lib/translate.ts b/apps/desktop/src/renderer/src/lib/translate.ts index bcf602b8d..58786b64c 100644 --- a/apps/desktop/src/renderer/src/lib/translate.ts +++ b/apps/desktop/src/renderer/src/lib/translate.ts @@ -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], diff --git a/apps/desktop/src/renderer/src/modules/entry-content/AISummary.tsx b/apps/desktop/src/renderer/src/modules/entry-content/AISummary.tsx index 6705ccb61..6fc40c3ae 100644 --- a/apps/desktop/src/renderer/src/modules/entry-content/AISummary.tsx +++ b/apps/desktop/src/renderer/src/modules/entry-content/AISummary.tsx @@ -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, diff --git a/apps/desktop/src/renderer/src/modules/entry-content/actions/header-actions.tsx b/apps/desktop/src/renderer/src/modules/entry-content/actions/header-actions.tsx index 573ef7ed9..fed404c4a 100644 --- a/apps/desktop/src/renderer/src/modules/entry-content/actions/header-actions.tsx +++ b/apps/desktop/src/renderer/src/modules/entry-content/actions/header-actions.tsx @@ -39,6 +39,8 @@ export const EntryHeaderActions = ({ onClick={config.onClick} shortcut={config.shortcut} clickableDisabled={config.disabled} + tooltipDefaultOpen={config.notice} + id={`${config.entryId}/${config.id}`} /> ) }) diff --git a/apps/desktop/src/renderer/src/modules/entry-content/index.electron.tsx b/apps/desktop/src/renderer/src/modules/entry-content/index.electron.tsx index e260e1651..b446ed226 100644 --- a/apps/desktop/src/renderer/src/modules/entry-content/index.electron.tsx +++ b/apps/desktop/src/renderer/src/modules/entry-content/index.electron.tsx @@ -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 = ({ 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(null) useEffect(() => { scrollerRef.current?.scrollTo(0, 0) @@ -114,14 +119,21 @@ export const EntryContent: Component = ({ ) 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 = ({ - {!isInReadabilityMode ? ( - - {!!customCSS && ( - {customCSS} - )} - - {content} - - - ) : ( - - )} + + + {!!customCSS && ( + {customCSS} + )} + + {content} + + - {entry.settings?.readability && IN_ELECTRON && ( + {entry.settings?.readability && ( )} {entry.settings?.sourceContent && } diff --git a/apps/desktop/src/renderer/src/modules/entry-content/index.shared.tsx b/apps/desktop/src/renderer/src/modules/entry-content/index.shared.tsx index 4f1b2c09a..d508a0629 100644 --- a/apps/desktop/src/renderer/src/modules/entry-content/index.shared.tsx +++ b/apps/desktop/src/renderer/src/modules/entry-content/index.shared.tsx @@ -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 (
@@ -103,16 +105,6 @@ export const ReadabilityContent = ({ entryId, feedId }: { entryId: string; feedI {t("entry_content.fetching_content")}
)} - - - {result?.content ?? ""} - ) } @@ -134,12 +126,7 @@ export const NoContent: FC<{ {(WEB_BUILD || status === ReadabilityStatus.FAILURE) && ( {t("entry_content.no_content")} )} - {WEB_BUILD && ( -
- {t("entry_content.web_app_notice")} -
- )} - {!sourceContent && url && IN_ELECTRON && } + {!sourceContent && url && } ) diff --git a/apps/desktop/src/renderer/src/queries/ai.ts b/apps/desktop/src/renderer/src/queries/ai.ts index d52cd2850..2451a2878 100644 --- a/apps/desktop/src/renderer/src/queries/ai.ts +++ b/apps/desktop/src/renderer/src/queries/ai.ts @@ -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 diff --git a/apps/mobile/drizzle/0020_little_marauders.sql b/apps/mobile/drizzle/0020_little_marauders.sql new file mode 100644 index 000000000..9a6f6ae3d --- /dev/null +++ b/apps/mobile/drizzle/0020_little_marauders.sql @@ -0,0 +1,2 @@ +ALTER TABLE `summaries` ADD `readability_summary` text;--> statement-breakpoint +ALTER TABLE `translations` ADD `readability_content` text; \ No newline at end of file diff --git a/apps/mobile/drizzle/meta/0020_snapshot.json b/apps/mobile/drizzle/meta/0020_snapshot.json new file mode 100644 index 000000000..0b7d8b6e4 --- /dev/null +++ b/apps/mobile/drizzle/meta/0020_snapshot.json @@ -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": {} + } +} diff --git a/apps/mobile/drizzle/meta/_journal.json b/apps/mobile/drizzle/meta/_journal.json index c3a1b8b57..69e0d760f 100644 --- a/apps/mobile/drizzle/meta/_journal.json +++ b/apps/mobile/drizzle/meta/_journal.json @@ -141,6 +141,13 @@ "when": 1743153830369, "tag": "0019_wonderful_shape", "breakpoints": true + }, + { + "idx": 20, + "version": "6", + "when": 1744793226628, + "tag": "0020_little_marauders", + "breakpoints": true } ] } diff --git a/apps/mobile/drizzle/migrations.js b/apps/mobile/drizzle/migrations.js index f66e18730..3ac3f929d 100644 --- a/apps/mobile/drizzle/migrations.js +++ b/apps/mobile/drizzle/migrations.js @@ -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, }, } diff --git a/apps/mobile/src/components/native/webview/EntryContentWebView.tsx b/apps/mobile/src/components/native/webview/EntryContentWebView.tsx index 3027c5d7f..55c5c7ad9 100644 --- a/apps/mobile/src/components/native/webview/EntryContentWebView.tsx +++ b/apps/mobile/src/components/native/webview/EntryContentWebView.tsx @@ -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(() => { diff --git a/apps/mobile/src/database/schemas/index.ts b/apps/mobile/src/database/schemas/index.ts index 4af8ed1c4..9a628c2b6 100644 --- a/apps/mobile/src/database/schemas/index.ts +++ b/apps/mobile/src/database/schemas/index.ts @@ -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() diff --git a/apps/mobile/src/modules/entry-content/EntryAISummary.tsx b/apps/mobile/src/modules/entry-content/EntryAISummary.tsx index d6cdf6707..8e30eb9b0 100644 --- a/apps/mobile/src/modules/entry-content/EntryAISummary.tsx +++ b/apps/mobile/src/modules/entry-content/EntryAISummary.tsx @@ -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 ( diff --git a/apps/mobile/src/modules/entry-content/EntryContentHeaderRightActions.tsx b/apps/mobile/src/modules/entry-content/EntryContentHeaderRightActions.tsx index 41db9029a..4b2a317ea 100644 --- a/apps/mobile/src/modules/entry-content/EntryContentHeaderRightActions.tsx +++ b/apps/mobile/src/modules/entry-content/EntryContentHeaderRightActions.tsx @@ -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", diff --git a/apps/mobile/src/modules/entry-content/EntryTitle.tsx b/apps/mobile/src/modules/entry-content/EntryTitle.tsx index 398f8ad49..2875c36b0 100644 --- a/apps/mobile/src/modules/entry-content/EntryTitle.tsx +++ b/apps/mobile/src/modules/entry-content/EntryTitle.tsx @@ -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 /> ) diff --git a/apps/mobile/src/modules/entry-list/EntryListContentArticle.tsx b/apps/mobile/src/modules/entry-list/EntryListContentArticle.tsx index 4c4c63160..a7582912f 100644 --- a/apps/mobile/src/modules/entry-list/EntryListContentArticle.tsx +++ b/apps/mobile/src/modules/entry-list/EntryListContentArticle.tsx @@ -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 ( item.key) : []) + usePrefetchEntryTranslation({ entryIds: active ? viewableItems.map((item) => item.key) : [] }) return ( item.key) : []) + usePrefetchEntryTranslation({ entryIds: active ? viewableItems.map((item) => item.key) : [] }) return ( item.key) : []) + usePrefetchEntryTranslation({ entryIds: active ? viewableItems.map((item) => item.key) : [] }) const ListFooterComponent = useMemo( () => diff --git a/apps/mobile/src/modules/entry-list/templates/EntryTranslation.tsx b/apps/mobile/src/modules/entry-list/templates/EntryTranslation.tsx index ccff4bb6d..7cc94ca0b 100644 --- a/apps/mobile/src/modules/entry-list/templates/EntryTranslation.tsx +++ b/apps/mobile/src/modules/entry-list/templates/EntryTranslation.tsx @@ -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 ( - {nextTarget && ( - <> - - {nextTarget} - - - ⇋ - - - )} {nextSource} + {nextTarget && ( + + {nextTarget} + + )} ) } diff --git a/apps/mobile/src/morph/hono.ts b/apps/mobile/src/morph/hono.ts index 48fd5455e..cc6feca9a 100644 --- a/apps/mobile/src/morph/hono.ts +++ b/apps/mobile/src/morph/hono.ts @@ -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, diff --git a/apps/mobile/src/screens/(stack)/entries/[entryId]/EntryDetailScreen.tsx b/apps/mobile/src/screens/(stack)/entries/[entryId]/EntryDetailScreen.tsx index 4057c59fe..ad84ddea7 100644 --- a/apps/mobile/src/screens/(stack)/entries/[entryId]/EntryDetailScreen.tsx +++ b/apps/mobile/src/screens/(stack)/entries/[entryId]/EntryDetailScreen.tsx @@ -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 ( ) { + 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, }) } diff --git a/apps/mobile/src/services/translation.ts b/apps/mobile/src/services/translation.ts index 22e1e6d73..7bd1744b9 100644 --- a/apps/mobile/src/services/translation.ts +++ b/apps/mobile/src/services/translation.ts @@ -21,6 +21,7 @@ class TranslationServiceStatic implements Hydratable, Resetable { title: data.title, description: data.description, content: data.content, + readabilityContent: data.readabilityContent, }).filter(([_, value]) => !!value), ) diff --git a/apps/mobile/src/store/entry/store.ts b/apps/mobile/src/store/entry/store.ts index 165f7c302..3d06d96f9 100644 --- a/apps/mobile/src/store/entry/store.ts +++ b/apps/mobile/src/store/entry/store.ts @@ -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, diff --git a/apps/mobile/src/store/summary/hooks.ts b/apps/mobile/src/store/summary/hooks.ts index 8f5ac68d4..4fccbfa62 100644 --- a/apps/mobile/src/store/summary/hooks.ts +++ b/apps/mobile/src/store/summary/hooks.ts @@ -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, diff --git a/apps/mobile/src/store/summary/store.ts b/apps/mobile/src/store/summary/store.ts index 63ada4166..e56760ada 100644 --- a/apps/mobile/src/store/summary/store.ts +++ b/apps/mobile/src/store/summary/store.ts @@ -13,6 +13,7 @@ type SummaryModel = Omit 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> = {} - 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, }, ]) } diff --git a/apps/mobile/src/store/translation/hooks.ts b/apps/mobile/src/store/translation/hooks.ts index 4861f711d..126e658f7 100644 --- a/apps/mobile/src/store/translation/hooks.ts +++ b/apps/mobile/src/store/translation/hooks.ts @@ -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, }), })), }) diff --git a/apps/mobile/src/store/translation/store.ts b/apps/mobile/src/store/translation/store.ts index 0fb48895c..412d112f8 100644 --- a/apps/mobile/src/store/translation/store.ts +++ b/apps/mobile/src/store/translation/store.ts @@ -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]) diff --git a/apps/mobile/src/store/translation/types.ts b/apps/mobile/src/store/translation/types.ts index 02b675d56..fbb86ad40 100644 --- a/apps/mobile/src/store/translation/types.ts +++ b/apps/mobile/src/store/translation/types.ts @@ -2,4 +2,5 @@ export interface EntryTranslation { title: string description: string content: string + readabilityContent: string | null } diff --git a/packages/components/src/ui/button/action-button.tsx b/packages/components/src/ui/button/action-button.tsx index d951c9b77..9c77b88b9 100644 --- a/packages/components/src/ui/button/action-button.tsx +++ b/packages/components/src/ui/button/action-button.tsx @@ -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 `` @@ -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 ? ( - - {Trigger} - - - - {tooltip} - {!!finalShortcut && ( -
- {finalShortcut} -
- )} -
-
+ + + {Trigger} + + + + {tooltip} + {!!finalShortcut && ( +
+ {finalShortcut} +
+ )} +
+
+
) : ( Trigger diff --git a/packages/components/src/ui/tooltip/index.tsx b/packages/components/src/ui/tooltip/index.tsx index 3b8a87e9f..92d3f5fb7 100644 --- a/packages/components/src/ui/tooltip/index.tsx +++ b/packages/components/src/ui/tooltip/index.tsx @@ -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 }) => ( @@ -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"