From 69996e83e51ed571810acf8dae5f6fbb8113807b Mon Sep 17 00:00:00 2001 From: lawvs <18554747+lawvs@users.noreply.github.com> Date: Thu, 25 Sep 2025 18:36:33 +0800 Subject: [PATCH 001/601] feat: support hover on mention dropdown --- .../ai-chat/editor/plugins/mention/MentionPlugin.tsx | 4 +++- .../editor/plugins/mention/components/MentionDropdown.tsx | 7 ++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/mention/MentionPlugin.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/mention/MentionPlugin.tsx index a89816920..a703b7edc 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/mention/MentionPlugin.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/mention/MentionPlugin.tsx @@ -103,6 +103,7 @@ export function MentionPlugin() { suggestions, selectedIndex, isLoading, + onSetSelectIndex: setSelectedIndex, onSelect: selectMention, onClose: handleEscapeKey, query: mentionMatch?.matchingString || "", @@ -113,9 +114,10 @@ export function MentionPlugin() { suggestions, selectedIndex, isLoading, + setSelectedIndex, selectMention, handleEscapeKey, - mentionMatch, + mentionMatch?.matchingString, ]) return dropdownProps ? ( diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/mention/components/MentionDropdown.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/mention/components/MentionDropdown.tsx index e9db8b4c0..4927d6476 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/mention/components/MentionDropdown.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/mention/components/MentionDropdown.tsx @@ -28,6 +28,7 @@ interface MentionDropdownProps { selectedIndex: number isLoading: boolean onSelect: (mention: MentionData) => void + onSetSelectIndex: (index: number) => void onClose: () => void query: string anchor?: HTMLElement | null @@ -39,12 +40,13 @@ const MentionSuggestionItem = React.memo( isSelected, onClick, query, + ...props }: { mention: MentionData isSelected: boolean onClick: (mention: MentionData) => void query: string - }) => { + } & Omit, "onClick">) => { const { t, i18n } = useTranslation("ai") const language = i18n.language || i18n.resolvedLanguage || "en" @@ -95,6 +97,7 @@ const MentionSuggestionItem = React.memo( onClick={handleClick} role="option" aria-selected={isSelected} + {...props} > {/* Icon */} = ({ selectedIndex, isLoading, onSelect, + onSetSelectIndex, onClose, query, }) => { @@ -272,6 +276,7 @@ export const MentionDropdown: React.FC = ({ key={`${mention.type}-${mention.id}`} mention={mention} isSelected={index === selectedIndex} + onMouseMove={() => onSetSelectIndex(index)} onClick={onSelect} query={query} /> From 3e02f92971c73141ca971a5f85d2cfaa5f7d9b29 Mon Sep 17 00:00:00 2001 From: DIYgod Date: Thu, 25 Sep 2025 20:20:30 +0800 Subject: [PATCH 002/601] fix: posthog cors error --- apps/desktop/layer/main/src/manager/bootstrap.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/desktop/layer/main/src/manager/bootstrap.ts b/apps/desktop/layer/main/src/manager/bootstrap.ts index 45b6d3bbe..4e8f19d30 100644 --- a/apps/desktop/layer/main/src/manager/bootstrap.ts +++ b/apps/desktop/layer/main/src/manager/bootstrap.ts @@ -88,16 +88,16 @@ export class BootstrapManager { if (url.hostname === "us.i.posthog.com") { const responseHeaders = details.responseHeaders || {} - responseHeaders["Access-Control-Allow-Origin"] = ["*"] - responseHeaders["Access-Control-Allow-Methods"] = [ + responseHeaders["access-control-allow-origin"] = ["*"] + responseHeaders["access-control-allow-methods"] = [ "GET", "POST", "PUT", "DELETE", "OPTIONS", ] - responseHeaders["Access-Control-Allow-Headers"] = ["*"] - responseHeaders["Access-Control-Allow-Credentials"] = ["true"] + responseHeaders["access-control-allow-headers"] = ["*"] + responseHeaders["access-control-allow-credentials"] = ["true"] callback({ cancel: false, From 54e8c8750842bbe5d02746737186696b74834396 Mon Sep 17 00:00:00 2001 From: DIYgod Date: Thu, 25 Sep 2025 21:35:46 +0800 Subject: [PATCH 003/601] feat: remove tip and boost --- PRPs/adaptive-entry-content-layouts.md | 2 - .../src/hooks/biz/useEntryActions.tsx | 11 - .../renderer/src/hooks/biz/useFeedActions.tsx | 18 -- .../achievement/AchievementModalContent.tsx | 5 - .../layer/renderer/src/modules/boost/atom.ts | 11 - .../src/modules/boost/boost-certification.tsx | 51 ----- .../src/modules/boost/boost-progress.tsx | 70 ------ .../modules/boost/boosting-contributors.tsx | 63 ------ .../renderer/src/modules/boost/hooks.tsx | 36 ---- .../src/modules/boost/level-benefits.tsx | 83 ------- .../renderer/src/modules/boost/modal.tsx | 110 ---------- .../renderer/src/modules/boost/query.tsx | 67 ------ .../src/modules/boost/radio-cards.tsx | 61 ------ .../modules/command/command-button.test-d.ts | 5 +- .../src/modules/command/commands/entry.tsx | 32 +-- .../src/modules/command/commands/id.ts | 1 - .../command/hooks/use-command-binding.ts | 2 - .../command/hooks/use-command.test-d.ts | 16 +- .../components/SupportCreator.tsx | 96 --------- .../EntryCommandShortcutRegister.tsx | 6 - .../entry-content/EntryContent.legacy.tsx | 2 - .../components/layouts/ArticleLayout.tsx | 2 - .../renderer/src/modules/feed/feed-title.tsx | 8 +- .../renderer/src/modules/wallet/hooks.ts | 36 ---- .../renderer/src/modules/wallet/tip-modal.tsx | 202 ------------------ .../layer/renderer/src/queries/wallet.tsx | 21 +- locales/app/en.json | 25 --- locales/app/ja.json | 25 --- locales/app/zh-CN.json | 25 --- locales/app/zh-TW.json | 25 --- .../internal/store/src/modules/entry/store.ts | 8 - packages/internal/store/src/morph/api.ts | 2 - .../internal/tracker/src/adapters/firebase.ts | 18 -- packages/internal/tracker/src/enums.ts | 3 - .../internal/tracker/src/tracker-points.ts | 12 -- 35 files changed, 13 insertions(+), 1147 deletions(-) delete mode 100644 apps/desktop/layer/renderer/src/modules/boost/atom.ts delete mode 100644 apps/desktop/layer/renderer/src/modules/boost/boost-certification.tsx delete mode 100644 apps/desktop/layer/renderer/src/modules/boost/boost-progress.tsx delete mode 100644 apps/desktop/layer/renderer/src/modules/boost/boosting-contributors.tsx delete mode 100644 apps/desktop/layer/renderer/src/modules/boost/hooks.tsx delete mode 100644 apps/desktop/layer/renderer/src/modules/boost/level-benefits.tsx delete mode 100644 apps/desktop/layer/renderer/src/modules/boost/modal.tsx delete mode 100644 apps/desktop/layer/renderer/src/modules/boost/query.tsx delete mode 100644 apps/desktop/layer/renderer/src/modules/boost/radio-cards.tsx delete mode 100644 apps/desktop/layer/renderer/src/modules/entry-content/components/SupportCreator.tsx delete mode 100644 apps/desktop/layer/renderer/src/modules/wallet/hooks.ts delete mode 100644 apps/desktop/layer/renderer/src/modules/wallet/tip-modal.tsx diff --git a/PRPs/adaptive-entry-content-layouts.md b/PRPs/adaptive-entry-content-layouts.md index da04e447b..0b1873a7f 100644 --- a/PRPs/adaptive-entry-content-layouts.md +++ b/PRPs/adaptive-entry-content-layouts.md @@ -25,7 +25,6 @@ Transform the entry content display system from a fixed, article-centric layout // AI-generated summary // Main content // File attachments - // Creator support section ``` @@ -145,7 +144,6 @@ const EntryContentLayoutFactory = { - ``` ## Implementation Tasks diff --git a/apps/desktop/layer/renderer/src/hooks/biz/useEntryActions.tsx b/apps/desktop/layer/renderer/src/hooks/biz/useEntryActions.tsx index 1e1a875be..9a52807b4 100644 --- a/apps/desktop/layer/renderer/src/hooks/biz/useEntryActions.tsx +++ b/apps/desktop/layer/renderer/src/hooks/biz/useEntryActions.tsx @@ -7,7 +7,6 @@ import { entrySyncServices } from "@follow/store/entry/store" import type { EntryModel } from "@follow/store/entry/types" import { useFeedById } from "@follow/store/feed/hooks" import { useIsInbox } from "@follow/store/inbox/hooks" -import { whoami } from "@follow/store/user/getters" import { useUserRole } from "@follow/store/user/hooks" import { doesTextContainHTML } from "@follow/utils/utils" import { useMemo } from "react" @@ -301,16 +300,6 @@ export const useEntryActions = ({ entryId, view }: { entryId: string; view: Feed hide: !IN_ELECTRON || !entry.hasBitTorrent, entryId, }), - new EntryActionMenuItem({ - id: COMMAND_ID.entry.tip, - onClick: runCmdFn(COMMAND_ID.entry.tip, [ - { entryId, feedId: feed?.id, userId: feed?.ownerUserId }, - ]), - hide: isInbox || feed?.ownerUserId === whoami()?.id, - // shortcut: shortcuts.entry.tip.key, - shortcut: shortcuts[COMMAND_ID.entry.tip], - entryId, - }), new EntryActionMenuItem({ id: COMMAND_ID.entry.star, onClick: runCmdFn(COMMAND_ID.entry.star, [{ entryId, view }]), diff --git a/apps/desktop/layer/renderer/src/hooks/biz/useFeedActions.tsx b/apps/desktop/layer/renderer/src/hooks/biz/useFeedActions.tsx index 2d048fc1c..f37f64721 100644 --- a/apps/desktop/layer/renderer/src/hooks/biz/useFeedActions.tsx +++ b/apps/desktop/layer/renderer/src/hooks/biz/useFeedActions.tsx @@ -21,11 +21,9 @@ import { toast } from "sonner" import type { FollowMenuItem, MenuItemInput } from "~/atoms/context-menu" import { MenuItemSeparator, MenuItemText } from "~/atoms/context-menu" -import { useIsInMASReview } from "~/atoms/server-configs" import { useModalStack } from "~/components/ui/modal/stacked/hooks" import { copyToClipboard } from "~/lib/clipboard" import { UrlBuilder } from "~/lib/url-builder" -import { useBoostModal } from "~/modules/boost/hooks" import { useFeedClaimModal } from "~/modules/claim" import { COMMAND_ID } from "~/modules/command/commands/id" import { useCommandShortcuts } from "~/modules/command/hooks/use-command-binding" @@ -83,15 +81,12 @@ export const useFeedActions = ({ const { mutateAsync: resetFeed } = useResetFeed() const { mutate: addFeedsToCategoryMutation } = useBatchUpdateSubscription() const presentCategoryCreationModal = useCategoryCreationModal() - const openBoostModal = useBoostModal() const listByView = useOwnedListByView(view!) const categories = useCategoriesByView(view!) const isMultipleSelection = feedIds && feedIds.length > 1 && feedIds.includes(feedId) - const isInMASReview = useIsInMASReview() - const shortcuts = useCommandShortcuts() const items = useMemo(() => { @@ -136,17 +131,6 @@ export const useFeedActions = ({ MenuItemSeparator.default, ] : []), - ...(!isInMASReview - ? [ - new MenuItemText({ - label: t("words.boost"), - click: () => { - openBoostModal(feedId) - }, - }), - ] - : []), - new MenuItemSeparator(isEntryList), new MenuItemText({ label: t("sidebar.feed_column.context_menu.add_feeds_to_list"), @@ -335,12 +319,10 @@ export const useFeedActions = ({ feedIds, inbox, isEntryList, - isInMASReview, isInbox, isMultipleSelection, listByView, navigateEntry, - openBoostModal, present, presentCategoryCreationModal, presentDeleteSubscription, diff --git a/apps/desktop/layer/renderer/src/modules/achievement/AchievementModalContent.tsx b/apps/desktop/layer/renderer/src/modules/achievement/AchievementModalContent.tsx index 560bfae1e..50ad8cc5b 100644 --- a/apps/desktop/layer/renderer/src/modules/achievement/AchievementModalContent.tsx +++ b/apps/desktop/layer/renderer/src/modules/achievement/AchievementModalContent.tsx @@ -34,7 +34,6 @@ enum AchievementsActionIdMap { // TODO // FOLLOW_SPECIAL_FEED = 6, ALPHA_TESTER = 7, - FEED_BOOSTER = 8, } const achievementActionIdMetaMap: Record< @@ -80,10 +79,6 @@ const achievementActionIdMetaMap: Record< description: "achievement.alpha_tester_description", video: "https://assets.folo.is/AlphaBadge.webm", }, - [AchievementsActionIdMap.FEED_BOOSTER]: { - title: "achievement.feed_booster", - description: "achievement.feed_booster_description", - }, } const prefetchVideos = () => { diff --git a/apps/desktop/layer/renderer/src/modules/boost/atom.ts b/apps/desktop/layer/renderer/src/modules/boost/atom.ts deleted file mode 100644 index 1a14c8e7d..000000000 --- a/apps/desktop/layer/renderer/src/modules/boost/atom.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { createAtomHooks } from "@follow/utils/jotai" -import { atom } from "jotai" - -export const [, , useFeedBoostMap, , getFeedBoostMap, setFeedBoostMap] = createAtomHooks( - atom>({}), -) - -export const updateFeedBoostStatus = (feedId: string, isBoosted: boolean) => { - const prev = getFeedBoostMap() - setFeedBoostMap({ ...prev, [feedId]: isBoosted }) -} diff --git a/apps/desktop/layer/renderer/src/modules/boost/boost-certification.tsx b/apps/desktop/layer/renderer/src/modules/boost/boost-certification.tsx deleted file mode 100644 index 996d67920..000000000 --- a/apps/desktop/layer/renderer/src/modules/boost/boost-certification.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { - Tooltip, - TooltipContent, - TooltipPortal, - TooltipTrigger, -} from "@follow/components/ui/tooltip/index.js" -import type { FeedModel } from "@follow/store/feed/types" -import type { ListModel } from "@follow/store/list/types" -import { cn } from "@follow/utils/utils" -import { useTranslation } from "react-i18next" - -import { useBoostModal, useIsFeedBoosted } from "./hooks" - -export const BoostCertification = ({ - feed, - className, -}: { - feed: FeedModel | ListModel - className?: string -}) => { - const showBoostModal = useBoostModal() - const { t } = useTranslation() - const isFeedBoosted = useIsFeedBoosted(feed.id) - if (!isFeedBoosted) return null - - return ( - - - { - e.stopPropagation() - showBoostModal(feed.id) - }} - /> - - - - -
- - {t("boost.feed_being_boosted")} -
-
-
-
- ) -} diff --git a/apps/desktop/layer/renderer/src/modules/boost/boost-progress.tsx b/apps/desktop/layer/renderer/src/modules/boost/boost-progress.tsx deleted file mode 100644 index e0d033ffd..000000000 --- a/apps/desktop/layer/renderer/src/modules/boost/boost-progress.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { cn } from "@follow/utils/utils" -import { useState } from "react" - -import { useI18n } from "~/hooks/common" - -export const BoostProgress = ({ - level, - boostCount, - remainingBoostsToLevelUp, - lastValidBoost, -}: { - level: number - boostCount: number - remainingBoostsToLevelUp: number - lastValidBoost: { - hash: string | null - expiresAt: string - } | null -}) => { - const [numberFormater] = useState(() => new Intl.NumberFormat()) - const t = useI18n() - const rawPercentage = (boostCount / (boostCount + remainingBoostsToLevelUp)) * 100 - const percentage = Math.max(rawPercentage, 2) - const nextLevel = level + 1 - return ( -
-
- - - {numberFormater.format(boostCount)} - -
-
-
-
- -
- Lv. {level} - Lv. {nextLevel} -
- - {t("boost.remaining_boosts_to_level_up", { - remainingBoostsToLevelUp: numberFormater.format(remainingBoostsToLevelUp), - })} - - {lastValidBoost && ( - - {t("boost.expired_description", { - expiredDate: new Date(lastValidBoost.expiresAt), - interpolation: { escapeValue: false }, - })} - - )} -
- ) -} diff --git a/apps/desktop/layer/renderer/src/modules/boost/boosting-contributors.tsx b/apps/desktop/layer/renderer/src/modules/boost/boosting-contributors.tsx deleted file mode 100644 index fbdc94890..000000000 --- a/apps/desktop/layer/renderer/src/modules/boost/boosting-contributors.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { AutoResizeHeight } from "@follow/components/ui/auto-resize-height/index.js" -import { Avatar, AvatarFallback, AvatarImage } from "@follow/components/ui/avatar/index.js" -import type { UserModel } from "@follow/store/user/store" - -import { replaceImgUrlIfNeed } from "~/lib/img-proxy" -import { useFeedBoostersQuery } from "~/modules/boost/query" - -import { usePresentUserProfileModal } from "../profile/hooks" -import { UserGallery } from "../user/UserGallery" - -export const BoostingContributors = ({ feedId }: { feedId: string }) => { - const { data: boosters, isLoading } = useFeedBoostersQuery(feedId) - const presentUserProfile = usePresentUserProfileModal("drawer") - const hasData = !(isLoading || !boosters || boosters.length === 0) - - return ( - - {hasData && ( - <> -

- - Boosting Contributors -

-
    - {boosters.length < 8 ? ( - boosters.map((user) => ( - - )) - ) : ( - - )} -
- - )} -
- ) -} - -const UserListItem = ({ - user, - onClick, -}: { - user: UserModel - onClick: (userId: string) => void -}) => { - return ( -
  • onClick(user.id)} - > - - - {user.name?.slice(0, 2)} - -
    -
    {user.name || user.handle}
    - {user.handle &&
    @{user.handle}
    } -
    -
  • - ) -} diff --git a/apps/desktop/layer/renderer/src/modules/boost/hooks.tsx b/apps/desktop/layer/renderer/src/modules/boost/hooks.tsx deleted file mode 100644 index 2a84bcfa0..000000000 --- a/apps/desktop/layer/renderer/src/modules/boost/hooks.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { useCallback } from "react" - -import { useAsyncModal } from "~/components/ui/modal/helper/useAsyncModal" -import { useI18n } from "~/hooks/common" - -import { useFeedBoostMap } from "./atom" -import { BoostModalContent } from "./modal" -import { useBoostStatusQuery } from "./query" - -export const useBoostModal = () => { - const t = useI18n() - const present = useAsyncModal() - - return useCallback( - (feedId: string) => { - const useDataFetcher = () => useBoostStatusQuery(feedId) - - type ResponseType = Awaited["fn"]>> - - present({ - id: `boost-${feedId}`, - title: t("words.boost"), - content: () => , - overlay: true, - useDataFetcher, - clickOutsideToDismiss: true, - }) - }, - [present, t], - ) -} - -export const useIsFeedBoosted = (feedId: string) => { - const map = useFeedBoostMap() - return map[feedId] ?? false -} diff --git a/apps/desktop/layer/renderer/src/modules/boost/level-benefits.tsx b/apps/desktop/layer/renderer/src/modules/boost/level-benefits.tsx deleted file mode 100644 index 4e89180d6..000000000 --- a/apps/desktop/layer/renderer/src/modules/boost/level-benefits.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { cn } from "@follow/utils/utils" - -const benefits = [ - { - level: 1, - benefits: [ - { - icon: "i-mgc-certificate-cute-fi", - text: "Unique boost badge for feed", - }, - { icon: "i-mgc-star-cute-re", text: "Support feed owner" }, - { icon: "i-mgc-trophy-cute-re", text: "Booster achievement" }, - { icon: "i-mgc-rocket-cute-re", text: "Faster feed refresh time" }, - ], - }, - { - level: 2, - benefits: [{ icon: "i-mgc-rocket-cute-re", text: "Faster feed refresh time" }], - }, - { - level: 3, - benefits: [ - { icon: "i-mgc-eye-2-cute-re", text: "More developer attention" }, - { icon: "i-mgc-rocket-cute-re", text: "Faster feed refresh time" }, - ], - }, - { - level: 4, - benefits: [ - { icon: "i-mgc-eye-2-cute-re", text: "More developer attention" }, - { icon: "i-mgc-ai-cute-re", text: "AI summary for feed", comingSoon: true }, - { icon: "i-mgc-rocket-cute-re", text: "Faster feed refresh time" }, - ], - }, -] - -export const LevelBenefits = () => { - return ( -
    -
      - {benefits.map((benefit) => ( - - ))} -
    • ...
    • -
    -
    -

    More benefits coming soon

    -

    Stay tuned for more exciting benefits and updates!

    -
    -
    - ) -} - -const BoostLevel = ({ - level, - benefits, -}: { - level: number - benefits: { icon?: string; text: string; comingSoon?: boolean }[] -}) => { - return ( -
  • -
    -

    - - Lv. {level} - -

    -
      - {benefits.map((benefit) => ( -
    • - - {benefit.text} - {benefit.comingSoon && ( - (Coming Soon) - )} -
    • - ))} -
    -
    -
  • - ) -} diff --git a/apps/desktop/layer/renderer/src/modules/boost/modal.tsx b/apps/desktop/layer/renderer/src/modules/boost/modal.tsx deleted file mode 100644 index 431461264..000000000 --- a/apps/desktop/layer/renderer/src/modules/boost/modal.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import { Spring } from "@follow/components/constants/spring.js" -import { Button } from "@follow/components/ui/button/index.js" -import { LoadingWithIcon } from "@follow/components/ui/loading/index.jsx" -import { useFeedById } from "@follow/store/feed/hooks" -import { feedIconSelector } from "@follow/store/feed/selectors" -import { from } from "dnum" -import { AnimatePresence, m } from "motion/react" -import { useCallback, useState } from "react" - -import { useCurrentModal } from "~/components/ui/modal/stacked/hooks" -import { useI18n } from "~/hooks/common" -import { useBoostFeedMutation, useBoostStatusQuery } from "~/modules/boost/query" -import { useWallet } from "~/queries/wallet" - -import { FeedIcon } from "../feed/feed-icon" -import { useTOTPModalWrapper } from "../profile/hooks" -import { BoostProgress } from "./boost-progress" -import { BoostingContributors } from "./boosting-contributors" -import { LevelBenefits } from "./level-benefits" -import { RadioCards } from "./radio-cards" - -export const BoostModalContent = ({ feedId }: { feedId: string }) => { - const t = useI18n() - const myWallet = useWallet() - const myWalletData = myWallet.data?.[0] - - const dPowerBigInt = BigInt(myWalletData?.dailyPowerToken ?? 0) - const cPowerBigInt = BigInt(myWalletData?.cashablePowerToken ?? 0) - const balanceBigInt = cPowerBigInt + dPowerBigInt - const [amount, setAmount] = useState(0) - const amountBigInt = from(amount, 18)[0] - const wrongNumberRange = amountBigInt > balanceBigInt || amountBigInt <= 0n - - const { data: boostStatus, isLoading } = useBoostStatusQuery(feedId) - const boostFeedMutation = useBoostFeedMutation() - const { dismiss } = useCurrentModal() - const present = useTOTPModalWrapper(boostFeedMutation.mutateAsync) - - const handleBoost = useCallback(() => { - if (boostFeedMutation.isPending) return - present({ feedId, amount: amountBigInt.toString() }) - }, [amountBigInt, boostFeedMutation.isPending, feedId, present]) - - const feed = useFeedById(feedId, feedIconSelector) - - if (isLoading || !boostStatus) { - return ( -
    - } size="large" /> -
    - ) - } - - return ( -
    -
    - - -

    -
    - - {t("boost.boost_feed")} -
    - ใ€Œ{feed?.title}ใ€ -

    -
    -
    - - {t("boost.boost_feed_description")} - - - - - - {!boostFeedMutation.isSuccess && ( - - )} - -
    - - {boostFeedMutation.isSuccess ? ( - <> - - {t("boost.boost_success_thanks")} - - - - ) : ( - - )} - - - -
    - ) -} diff --git a/apps/desktop/layer/renderer/src/modules/boost/query.tsx b/apps/desktop/layer/renderer/src/modules/boost/query.tsx deleted file mode 100644 index 5b3e9a53e..000000000 --- a/apps/desktop/layer/renderer/src/modules/boost/query.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { tracker } from "@follow/tracker" -import type { BoostFeedRequest } from "@follow-app/client-sdk" -import { useMutation } from "@tanstack/react-query" -import { toast } from "sonner" - -import { useAuthQuery, useI18n } from "~/hooks/common" -import { followClient } from "~/lib/api-client" -import { defineQuery } from "~/lib/defineQuery" -import { toastFetchError } from "~/lib/error-parser" - -import { updateFeedBoostStatus } from "./atom" - -const query = { - getStatus: ({ feedId }: { feedId: string }) => - defineQuery(["boostFeed", feedId], async () => { - const res = await followClient.api.boosts.getFeedBoostLevel({ - feedId, - }) - return res.data - }), - getBoosters: ({ feedId }: { feedId: string }) => - defineQuery(["boosters", feedId], async () => { - const res = await followClient.api.boosts.getFeedBoosters({ - feedId, - }) - - return res.data - }), -} - -export const useBoostStatusQuery = (feedId: string) => - useAuthQuery(query.getStatus({ feedId }), { - staleTime: 1000 * 60 * 5, - }) - -export const useFeedBoostersQuery = (feedId: string | null | undefined) => - useAuthQuery(query.getBoosters({ feedId: feedId ?? "" }), { - staleTime: 1000 * 60 * 5, - enabled: feedId !== undefined, - }) - -export const useBoostFeedMutation = () => { - const t = useI18n() - return useMutation({ - mutationFn: (data: BoostFeedRequest) => - followClient.api.boosts.boostFeed({ - amount: data.amount, - feedId: data.feedId, - TOTPCode: data.TOTPCode, - }), - onError(err) { - toastFetchError(err) - }, - onSuccess(response, variables) { - query.getStatus({ feedId: variables.feedId }).invalidate() - query.getBoosters({ feedId: variables.feedId }).invalidate() - updateFeedBoostStatus(variables.feedId, true) - - tracker.boostSent({ - amount: variables.amount, - feedId: variables.feedId, - transactionId: response.data.transactionHash, - }) - toast(t("boost.boost_success")) - }, - }) -} diff --git a/apps/desktop/layer/renderer/src/modules/boost/radio-cards.tsx b/apps/desktop/layer/renderer/src/modules/boost/radio-cards.tsx deleted file mode 100644 index 0a5833b2f..000000000 --- a/apps/desktop/layer/renderer/src/modules/boost/radio-cards.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { Spring } from "@follow/components/constants/spring.js" -import { RadioCard } from "@follow/components/ui/radio-group/RadioCard.js" -import { RadioGroup } from "@follow/components/ui/radio-group/RadioGroup.js" -import dayjs from "dayjs" -import { m } from "motion/react" - -const radios = [ - { - value: 1, - }, - { - value: 3, - }, - { - value: 6, - }, - { - value: 12, - }, -] - -export const RadioCards = ({ - monthlyBoostCost, - value, - onValueChange, -}: { - monthlyBoostCost: number - value: number - onValueChange: (value: number) => void -}) => { - return ( - onValueChange(+value)}> - - {radios.map((item) => ( - -

    - {dayjs.duration(item.value, "months").humanize()} -

    -

    - {item.value * monthlyBoostCost} - -

    -
    - } - /> - ))} - - - ) -} diff --git a/apps/desktop/layer/renderer/src/modules/command/command-button.test-d.ts b/apps/desktop/layer/renderer/src/modules/command/command-button.test-d.ts index 3cc5d02fa..a6f640c2e 100644 --- a/apps/desktop/layer/renderer/src/modules/command/command-button.test-d.ts +++ b/apps/desktop/layer/renderer/src/modules/command/command-button.test-d.ts @@ -1,7 +1,7 @@ import { assertType, test } from "vitest" import { CommandActionButton, CommandIdButton } from "./command-button" -import type { OpenInBrowserCommand, TipCommand } from "./commands/entry" +import type { CopyLinkCommand, OpenInBrowserCommand } from "./commands/entry" import { COMMAND_ID } from "./commands/id" test("CommandActionButton types", () => { @@ -15,10 +15,9 @@ test("CommandActionButton types", () => { assertType( CommandActionButton({ - command: {} as TipCommand, + command: {} as CopyLinkCommand, args: [ { - feedId: "", // @ts-expect-error - invalid entryId type entryId: false, }, diff --git a/apps/desktop/layer/renderer/src/modules/command/commands/entry.tsx b/apps/desktop/layer/renderer/src/modules/command/commands/entry.tsx index 23e76a661..51dce0851 100644 --- a/apps/desktop/layer/renderer/src/modules/command/commands/entry.tsx +++ b/apps/desktop/layer/renderer/src/modules/command/commands/entry.tsx @@ -16,7 +16,6 @@ import { toggleShowAISummaryOnce } from "~/atoms/ai-summary" import { toggleShowAITranslationOnce } from "~/atoms/ai-translation" import { AudioPlayer, getAudioPlayerAtomValue } from "~/atoms/player" import { showPopover } from "~/atoms/popover" -import { useIsInMASReview } from "~/atoms/server-configs" import { useGeneralSettingKey } from "~/atoms/settings/general" import { getShowSourceContent, @@ -33,7 +32,6 @@ import { parseHtml } from "~/lib/parse-html" import { useActivationModal } from "~/modules/activation" import { markAllByRoute } from "~/modules/entry-column/hooks/useMarkAll" import { useGalleryModal } from "~/modules/entry-content/hooks" -import { useTipModal } from "~/modules/wallet/hooks" import { useRegisterFollowCommand } from "../hooks/use-register-command" import type { Command, CommandCategory } from "../types" @@ -109,7 +107,6 @@ export const useRegisterEntryCommands = () => { const uncollect = useUnCollect() const deleteInboxEntry = useDeleteInboxEntry() const showSourceContentModal = useSourceContentModal() - const openTipModal = useTipModal() const openGalleryModal = useGalleryModal() const read = useRead() const unread = useUnread() @@ -119,27 +116,8 @@ export const useRegisterEntryCommands = () => { const voice = useGeneralSettingKey("voice") - const isInMASReview = useIsInMASReview() - useRegisterFollowCommand( [ - ...(isInMASReview - ? ([] as any[]) - : [ - { - id: COMMAND_ID.entry.tip, - label: t("entry_actions.tip"), - icon: , - category, - run: ({ userId, feedId, entryId }) => { - openTipModal({ - userId, - feedId, - entryId, - }) - }, - }, - ]), { id: COMMAND_ID.entry.star, label: t("entry_actions.star"), @@ -410,9 +388,7 @@ export const useRegisterEntryCommands = () => { }, }, ], - { - deps: [isInMASReview], - }, + {}, ) useRegisterFollowCommand( @@ -450,11 +426,6 @@ export const useRegisterEntryCommands = () => { ) } -export type TipCommand = Command<{ - id: typeof COMMAND_ID.entry.tip - fn: (data: { userId?: string | null; feedId?: string; entryId?: string }) => void -}> - export type StarCommand = Command<{ id: typeof COMMAND_ID.entry.star fn: (data: { entryId: string; view: FeedViewType }) => void @@ -536,7 +507,6 @@ export type ReadabilityCommand = Command<{ }> export type EntryCommand = - | TipCommand | StarCommand | DeleteCommand | CopyLinkCommand diff --git a/apps/desktop/layer/renderer/src/modules/command/commands/id.ts b/apps/desktop/layer/renderer/src/modules/command/commands/id.ts index fa5d0894c..8994db841 100644 --- a/apps/desktop/layer/renderer/src/modules/command/commands/id.ts +++ b/apps/desktop/layer/renderer/src/modules/command/commands/id.ts @@ -14,7 +14,6 @@ export const COMMAND_ID = { copyTitle: "entry:copy-title", tts: "entry:tts", exportAsPDF: "entry:export-as-pdf", - tip: "entry:tip", delete: "entry:delete", share: "entry:share", }, diff --git a/apps/desktop/layer/renderer/src/modules/command/hooks/use-command-binding.ts b/apps/desktop/layer/renderer/src/modules/command/hooks/use-command-binding.ts index f7928f5e2..fb01b719c 100644 --- a/apps/desktop/layer/renderer/src/modules/command/hooks/use-command-binding.ts +++ b/apps/desktop/layer/renderer/src/modules/command/hooks/use-command-binding.ts @@ -37,7 +37,6 @@ export const defaultCommandShortcuts = { [COMMAND_ID.entry.read]: "M", [COMMAND_ID.entry.share]: transformShortcut("$mod+Alt+S"), [COMMAND_ID.entry.star]: "S", - [COMMAND_ID.entry.tip]: transformShortcut("Shift+$mod+T"), [COMMAND_ID.entry.tts]: transformShortcut("Shift+$mod+V"), // Entry render commands @@ -100,7 +99,6 @@ export const allowCustomizeCommands = new Set([ COMMAND_ID.entry.read, COMMAND_ID.entry.share, COMMAND_ID.entry.star, - COMMAND_ID.entry.tip, COMMAND_ID.entry.tts, ] as const) type ExtractSetType> = T extends Set ? U : never diff --git a/apps/desktop/layer/renderer/src/modules/command/hooks/use-command.test-d.ts b/apps/desktop/layer/renderer/src/modules/command/hooks/use-command.test-d.ts index 450d1784b..a88e2a38a 100644 --- a/apps/desktop/layer/renderer/src/modules/command/hooks/use-command.test-d.ts +++ b/apps/desktop/layer/renderer/src/modules/command/hooks/use-command.test-d.ts @@ -1,19 +1,19 @@ import { assertType, expectTypeOf, test } from "vitest" -import type { TipCommand } from "../commands/entry" +import type { CopyLinkCommand } from "../commands/entry" import { COMMAND_ID } from "../commands/id" import { getCommand, useCommand, useRunCommandFn } from "./use-command" test("getCommand types work properly", () => { - expectTypeOf(getCommand(COMMAND_ID.entry.tip)).toMatchTypeOf() + expectTypeOf(getCommand(COMMAND_ID.entry.copyLink)).toMatchTypeOf() // @ts-expect-error - get an unknown command should throw an error assertType(getCmd("unknown command")) }) test("useCommand types work properly", () => { - const tipCmd = useCommand(COMMAND_ID.entry.tip) - expectTypeOf(tipCmd).toMatchTypeOf() + const copyCmd = useCommand(COMMAND_ID.entry.copyLink) + expectTypeOf(copyCmd).toMatchTypeOf() // @ts-expect-error - get an unknown command should throw an error assertType(useCommand("unknown command")) @@ -23,11 +23,11 @@ test("useRunCommandFn types work properly", () => { const runCmdFn = useRunCommandFn() expectTypeOf(runCmdFn).toBeFunction() - assertType(runCmdFn(COMMAND_ID.entry.tip, [{ entryId: "1" }])) + assertType(runCmdFn(COMMAND_ID.entry.copyLink, [{ entryId: "1" }])) // @ts-expect-error - invalid argument type - assertType(runCmdFn(COMMAND_ID.entry.tip, [{ entryId: 1 }])) + assertType(runCmdFn(COMMAND_ID.entry.copyLink, [{ entryId: 1 }])) // @ts-expect-error - invalid argument type - assertType(runCmdFn(COMMAND_ID.entry.tip, [])) + assertType(runCmdFn(COMMAND_ID.entry.copyLink, [])) // @ts-expect-error - invalid argument type - assertType(runCmdFn(COMMAND_ID.entry.tip, [1])) + assertType(runCmdFn(COMMAND_ID.entry.copyLink, [1])) }) diff --git a/apps/desktop/layer/renderer/src/modules/entry-content/components/SupportCreator.tsx b/apps/desktop/layer/renderer/src/modules/entry-content/components/SupportCreator.tsx deleted file mode 100644 index 848a4e18e..000000000 --- a/apps/desktop/layer/renderer/src/modules/entry-content/components/SupportCreator.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import { Button } from "@follow/components/ui/button/index.js" -import { Divider } from "@follow/components/ui/divider/index.js" -import { useEntry } from "@follow/store/entry/hooks" -import { useFeedById } from "@follow/store/feed/hooks" -import { getUserList } from "@follow/store/user/getters" -import { useWhoami } from "@follow/store/user/hooks" -import { useMemo } from "react" -import { useTranslation } from "react-i18next" - -import { useIsInMASReview } from "~/atoms/server-configs" -import { useBoostModal } from "~/modules/boost/hooks" -import { useFeedBoostersQuery } from "~/modules/boost/query" -import { FeedIcon } from "~/modules/feed/feed-icon" -import { UserAvatar } from "~/modules/user/UserAvatar" -import { deduplicateUsers } from "~/modules/user/utils" - -import { UserGallery } from "../../user/UserGallery" -import { useTipModal } from "../../wallet/hooks" - -export const SupportCreator = ({ entryId }: { entryId: string }) => { - const { t } = useTranslation() - const entry = useEntry(entryId, (state) => ({ feedId: state.feedId })) - const feed = useFeedById(entry?.feedId) - const { data: feedBoosters } = useFeedBoostersQuery(entry?.feedId) - - const openTipModal = useTipModal() - const openBoostModal = useBoostModal() - - const isMyOwnedFeed = feed?.ownerUserId === useWhoami()?.id - - const allSupporters = useMemo(() => { - const tipUserIds = feed?.tipUserIds ?? [] - const tipUsers = getUserList(tipUserIds) - return deduplicateUsers([...tipUsers, ...(feedBoosters ?? [])]) - }, [feed?.tipUserIds, feedBoosters]) - const supportAmount = allSupporters.length - - const isInMASReview = useIsInMASReview() - - if (!feed || feed.type !== "feed") return null - - return ( - <> - - -
    - {feed.ownerUserId ? ( - - ) : ( - - )} - {feed.title} - - {!isInMASReview && ( -
    - {!isMyOwnedFeed && ( - - )} - -
    - )} - - {supportAmount > 0 && ( - <> -
    - {t("entry_content.support_amount", { amount: supportAmount })} -
    - - - )} -
    - - ) -} diff --git a/apps/desktop/layer/renderer/src/modules/entry-content/components/entry-content/EntryCommandShortcutRegister.tsx b/apps/desktop/layer/renderer/src/modules/entry-content/components/entry-content/EntryCommandShortcutRegister.tsx index d0798ed58..f93cf7d6e 100644 --- a/apps/desktop/layer/renderer/src/modules/entry-content/components/entry-content/EntryCommandShortcutRegister.tsx +++ b/apps/desktop/layer/renderer/src/modules/entry-content/components/entry-content/EntryCommandShortcutRegister.tsx @@ -49,12 +49,6 @@ export const EntryCommandShortcutRegister = ({ args: [{ entryId, view }], }) - useCommandBinding({ - when: baseCondition, - commandId: COMMAND_ID.entry.tip, - args: [{ entryId }], - }) - useCommandBinding({ when: baseCondition, commandId: COMMAND_ID.entry.copyTitle, diff --git a/apps/desktop/layer/renderer/src/modules/entry-content/components/entry-content/EntryContent.legacy.tsx b/apps/desktop/layer/renderer/src/modules/entry-content/components/entry-content/EntryContent.legacy.tsx index 651e8a5fd..dd8469fa9 100644 --- a/apps/desktop/layer/renderer/src/modules/entry-content/components/entry-content/EntryContent.legacy.tsx +++ b/apps/desktop/layer/renderer/src/modules/entry-content/components/entry-content/EntryContent.legacy.tsx @@ -44,7 +44,6 @@ import { EntryHeader } from "../entry-header" import { EntryAttachments } from "../EntryAttachments" import { EntryTitle } from "../EntryTitle" import { SourceContentPanel } from "../SourceContentView" -import { SupportCreator } from "../SupportCreator" import { ContainerToc } from "./accessories/ContainerToc" import { EntryCommandShortcutRegister } from "./EntryCommandShortcutRegister" import { EntryContentFallback } from "./EntryContentFallback" @@ -234,7 +233,6 @@ const EntryContentImpl: Component = ({ )} - diff --git a/apps/desktop/layer/renderer/src/modules/entry-content/components/layouts/ArticleLayout.tsx b/apps/desktop/layer/renderer/src/modules/entry-content/components/layouts/ArticleLayout.tsx index 3aef24c56..8d010047b 100644 --- a/apps/desktop/layer/renderer/src/modules/entry-content/components/layouts/ArticleLayout.tsx +++ b/apps/desktop/layer/renderer/src/modules/entry-content/components/layouts/ArticleLayout.tsx @@ -27,7 +27,6 @@ import { EntryRenderError } from "../entry-content/EntryRenderError" import { ReadabilityNotice } from "../entry-content/ReadabilityNotice" import { EntryAttachments } from "../EntryAttachments" import { EntryTitle } from "../EntryTitle" -import { SupportCreator } from "../SupportCreator" import { MediaTranscript, TranscriptToggle, useTranscription } from "./shared" import { ArticleAudioPlayer } from "./shared/AudioPlayer" import type { EntryLayoutProps } from "./types" @@ -120,7 +119,6 @@ export const ArticleLayout: React.FC = ({ - ) } diff --git a/apps/desktop/layer/renderer/src/modules/feed/feed-title.tsx b/apps/desktop/layer/renderer/src/modules/feed/feed-title.tsx index 5c6aefc44..aac22e4e8 100644 --- a/apps/desktop/layer/renderer/src/modules/feed/feed-title.tsx +++ b/apps/desktop/layer/renderer/src/modules/feed/feed-title.tsx @@ -5,7 +5,6 @@ import type { ListModel } from "@follow/store/list/types" import { cn } from "@follow/utils/utils" import { useUISettingKey } from "~/atoms/settings/ui" -import { BoostCertification } from "~/modules/boost/boost-certification" import { FeedCertification } from "~/modules/feed/feed-certification" import { getPreferredTitle } from "~/store/feed/hooks" @@ -31,12 +30,7 @@ export const FeedTitle = ({ {title || getPreferredTitle(feed)} - {!hideExtraBadge && feed.type !== "inbox" && ( - <> - - - - )} + {!hideExtraBadge && feed.type !== "inbox" && } ) } diff --git a/apps/desktop/layer/renderer/src/modules/wallet/hooks.ts b/apps/desktop/layer/renderer/src/modules/wallet/hooks.ts deleted file mode 100644 index 287de2be5..000000000 --- a/apps/desktop/layer/renderer/src/modules/wallet/hooks.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { tracker } from "@follow/tracker" -import { createElement, useCallback } from "react" -import { useTranslation } from "react-i18next" -import { toast } from "sonner" - -import { useModalStack } from "~/components/ui/modal/stacked/hooks" - -import { TipModalContent } from "./tip-modal" - -export const useTipModal = () => { - const { present } = useModalStack() - const { t } = useTranslation() - return useCallback( - ({ - userId, - feedId, - entryId, - }: { - userId?: string | null - feedId?: string | null - entryId?: string - }) => { - if (!feedId || !entryId) { - // this should not happen unless there is a bug in the code - toast.error("Invalid feed id or entry id") - return - } - tracker.tipModalOpened({ entryId }) - present({ - title: t("tip_modal.tip_title"), - content: () => createElement(TipModalContent, { userId, feedId, entryId }), - }) - }, - [present, t], - ) -} diff --git a/apps/desktop/layer/renderer/src/modules/wallet/tip-modal.tsx b/apps/desktop/layer/renderer/src/modules/wallet/tip-modal.tsx deleted file mode 100644 index 8f1cf7a0f..000000000 --- a/apps/desktop/layer/renderer/src/modules/wallet/tip-modal.tsx +++ /dev/null @@ -1,202 +0,0 @@ -import { Button } from "@follow/components/ui/button/index.js" -import { Divider } from "@follow/components/ui/divider/index.js" -import { LoadingWithIcon } from "@follow/components/ui/loading/index.jsx" -import { RadioGroup } from "@follow/components/ui/radio-group/index.js" -import { RadioCard } from "@follow/components/ui/radio-group/RadioCard.js" -import { nextFrame } from "@follow/utils/dom" -import { from } from "dnum" -import type { FC } from "react" -import { useState } from "react" -import { useNavigate } from "react-router" - -import { useCurrentModal } from "~/components/ui/modal/stacked/hooks" -import { useI18n } from "~/hooks/common" -import { UserAvatar } from "~/modules/user/UserAvatar" -import { useWallet, useWalletTipMutation } from "~/queries/wallet" - -import { useFeedClaimModal } from "../claim" -import { useTOTPModalWrapper } from "../profile/hooks" -import { Balance } from "./balance" - -const DEFAULT_RECOMMENDED_TIP = 10 - -const useMyWallet = () => { - const myWallet = useWallet() - return myWallet -} - -const Loading = () => ( -
    - } size="large" /> -
    -) - -export const TipModalContent: FC<{ - userId?: string | null - feedId: string - entryId: string -}> = (props) => { - const myWallet = useMyWallet() - - if (!myWallet.data) { - return - } - return -} -const TipModalContent_: FC<{ - userId?: string | null - feedId: string - entryId: string -}> = ({ userId, feedId, entryId }) => { - const t = useI18n() - const myWallet = useMyWallet() - const myWalletData = myWallet.data?.[0] - - const dPowerBigInt = BigInt(myWalletData?.dailyPowerToken ?? 0) - const cPowerBigInt = BigInt(myWalletData?.cashablePowerToken ?? 0) - const balanceBigInt = cPowerBigInt + dPowerBigInt - - const tipMutation = useWalletTipMutation() - const present = useTOTPModalWrapper(tipMutation.mutateAsync) - - const [amount, setAmount] = useState(DEFAULT_RECOMMENDED_TIP) - - const amountBigInt = from(amount, 18)[0] - - const wrongNumberRange = amountBigInt > balanceBigInt || amountBigInt <= 0n - - const { dismiss } = useCurrentModal() - - const claimFeed = useFeedClaimModal() - - const navigate = useNavigate() - - if (myWallet.isPending) { - return - } - - if (!myWalletData) { - return ( -
    -

    {t("tip_modal.no_wallet")}

    -
    - -
    -
    - ) - } - - if (tipMutation.isSuccess) { - return ( -
    -

    {t("tip_modal.tip_sent")}

    -

    - - {amountBigInt} - {" "} - {t("tip_modal.tip_amount_sent")} -

    - -
    - -
    -
    - ) - } - - return ( -
    - {userId ? ( - <> -

    {t("tip_modal.feed_owner")}

    - - - ) : ( - <> -

    - {t("tip_modal.unclaimed_feed")} -

    -
    - -
    - - )} - -

    {t("tip_modal.tip_support")}

    - -
    -
    - - {t("tip_modal.amount")} -
    - - setAmount(Number(value))}> -
    - - 10 - - } - value="10" - /> - - 20 - - } - value="20" - /> -
    -
    - - {/* low balance notice */} - {wrongNumberRange && ( - <> - -
    - {t("tip_modal.low_balance")} -
    - - )} -
    - -
    - -
    -
    - ) -} diff --git a/apps/desktop/layer/renderer/src/queries/wallet.tsx b/apps/desktop/layer/renderer/src/queries/wallet.tsx index 23f98f229..def8a3b3a 100644 --- a/apps/desktop/layer/renderer/src/queries/wallet.tsx +++ b/apps/desktop/layer/renderer/src/queries/wallet.tsx @@ -1,5 +1,5 @@ import { tracker } from "@follow/tracker" -import type { TipRequest, TransactionQuery } from "@follow-app/client-sdk" +import type { TransactionQuery } from "@follow-app/client-sdk" import { useMutation } from "@tanstack/react-query" import { useNavigate } from "react-router" import { toast } from "sonner" @@ -123,22 +123,3 @@ export const useClaimWalletDailyRewardMutation = () => { }, }) } - -export const useWalletTipMutation = () => - useMutation({ - mutationKey: ["walletTip"], - mutationFn: (data: TipRequest) => followClient.api.wallets.transactions.tip(data), - async onError(err) { - toastFetchError(err) - }, - onSuccess(response, variables) { - wallet.get().invalidate() - wallet.transactions.get({}).invalidate() - tracker.tipSent({ - amount: variables.amount, - entryId: variables.entryId!, - transactionId: response.data.txHash, - }) - toast("๐ŸŽ‰ Tipped.") - }, - }) diff --git a/locales/app/en.json b/locales/app/en.json index fca722285..ff9c0c58e 100644 --- a/locales/app/en.json +++ b/locales/app/en.json @@ -3,8 +3,6 @@ "achievement.alpha_tester": "Alpha Tester", "achievement.alpha_tester_description": "You are an alpha tester of Folo", "achievement.description": "Be a hardcore player and mint NFTs.", - "achievement.feed_booster": "Feed Booster", - "achievement.feed_booster_description": "You boosted the feed on Folo", "achievement.first_claim_feed": "Feed Owner", "achievement.first_claim_feed_description": "You own your feed on Folo", "achievement.first_create_list": "List Creator", @@ -30,13 +28,6 @@ "app.copy_logo_svg": "Copy Logo SVG", "app.copy_logo_text_svg": "Copy Logo Text SVG", "app.toggle_sidebar": "Toggle Sidebar", - "boost.boost_feed": "Boost Feed", - "boost.boost_feed_description": "Enhance feed to unlock additional benefits, and those who subscribe will be grateful for the benefits!", - "boost.boost_success": "๐ŸŽ‰ Boost successful!", - "boost.boost_success_thanks": "Thank you for your support!", - "boost.expired_description": "You can't add more boost points right now, but feel free to keep boosting. Your current boost will expire on {{expiredDate, datetime}}.", - "boost.feed_being_boosted": "Feed being boosted", - "boost.remaining_boosts_to_level_up": "{{remainingBoostsToLevelUp}} more boost will unlock the next level of benefits!", "discover.any_url_or_keyword": "Any URL or Keyword", "discover.default_option": " (default)", "discover.feed_description": "The description of this feed is as follows, and you can fill out the parameter form with the relevant information.", @@ -158,7 +149,6 @@ "entry_actions.share": "Share", "entry_actions.star": "Star / UnStar", "entry_actions.starred": "Starred.", - "entry_actions.tip": "Tip", "entry_actions.toggle_ai_summary": "AI Summary", "entry_actions.toggle_ai_translation": "AI Translation", "entry_actions.unstar": "Unstar", @@ -181,8 +171,6 @@ "entry_content.readability_notice": "This content is provided by Readability. If you find typographical anomalies, please go to the source site to view the original content.", "entry_content.render_error": "Render error:", "entry_content.report_issue": "Report issue", - "entry_content.support_amount": "{{amount}} people supported this feed's creator.", - "entry_content.support_creator": "Support Creator", "entry_content.web_app_notice": "Maybe web app doesn't support this content type. But you can download the desktop app.", "entry_list.zero_unread": "Zero Unread", "entry_list_header.grid": "Grid", @@ -417,18 +405,6 @@ "sync_indicator.disabled": "Due to security reasons, sync is disabled.", "sync_indicator.offline": "Offline", "sync_indicator.synced": "Synced with server", - "tip_modal.amount": "Amount", - "tip_modal.claim_feed": "Claim this feed", - "tip_modal.create_wallet": "Create For Free", - "tip_modal.feed_owner": "Feed Owner", - "tip_modal.low_balance": "Your balance is not enough to cover this tip. Please adjust the amount.", - "tip_modal.no_wallet": "You don't have a wallet yet. Please create a wallet to tip.", - "tip_modal.tip_amount_sent": "has been sent to the author.", - "tip_modal.tip_now": "Tip Now", - "tip_modal.tip_sent": "Tip sent successfully! Thank you for your support.", - "tip_modal.tip_support": "โญ Tip to show your support!", - "tip_modal.tip_title": "Tip Power", - "tip_modal.unclaimed_feed": "No one has claimed this feed yet. The received Power will be securely held in the blockchain contract until it is claimed.", "trending.entry": "Trending Entries", "trending.entry_no_results": "No trending entries found", "trending.feed": "Trending Feeds", @@ -458,7 +434,6 @@ "words.actions": "Actions", "words.add": "Add", "words.all": "All", - "words.boost": "Boost", "words.browser": "Browser", "words.categories": "Categories", "words.confirm": "Confirm", diff --git a/locales/app/ja.json b/locales/app/ja.json index 0b15b33d8..315a42b77 100644 --- a/locales/app/ja.json +++ b/locales/app/ja.json @@ -3,8 +3,6 @@ "achievement.alpha_tester": "ใ‚ขใƒซใƒ•ใ‚ก ใƒ†ใ‚นใ‚ฟใƒผ", "achievement.alpha_tester_description": "ใ‚ใชใŸใฏ Folo ใฎใ‚ขใƒซใƒ•ใ‚ก ใƒ†ใ‚นใ‚ฟใƒผใงใ™", "achievement.description": "ใƒใƒผใƒ‰ใ‚ณใ‚ข ใƒ—ใƒฌใ‚คใƒคใƒผใซใชใฃใฆ NFT ใ‚’ใƒŸใƒณใƒˆใ—ใ‚ˆใ†ใ€‚", - "achievement.feed_booster": "ใƒ•ใ‚ฃใƒผใƒ‰ ใƒ–ใƒผใ‚นใ‚ฟใƒผ", - "achievement.feed_booster_description": "Folo ใฎใƒ•ใ‚ฃใƒผใƒ‰ใ‚’ใƒ–ใƒผใ‚นใƒˆใ—ใพใ—ใŸ", "achievement.first_claim_feed": "ใƒ•ใ‚ฃใƒผใƒ‰ใฎๆ‰€ๆœ‰่€…", "achievement.first_claim_feed_description": "ใ‚ใชใŸใฏ Folo ใฎใƒ•ใ‚ฃใƒผใƒ‰ๆ‰€ๆœ‰่€…ใงใ™", "achievement.first_create_list": "ใƒชใ‚นใƒˆไฝœๆˆ่€…", @@ -29,13 +27,6 @@ "app.copy_logo_svg": "ใƒญใ‚ด SVG ใ‚’ใ‚ณใƒ”ใƒผ", "app.copy_logo_text_svg": "ใƒญใ‚ดใƒ†ใ‚ญใ‚นใƒˆ SVG ใ‚’ใ‚ณใƒ”ใƒผ", "app.toggle_sidebar": "ใ‚ตใ‚คใƒ‰ใƒใƒผใ‚’ๅˆ‡ใ‚Šๆ›ฟใˆ", - "boost.boost_feed": "ใƒ•ใ‚ฃใƒผใƒ‰ใ‚’ใƒ–ใƒผใ‚นใƒˆ", - "boost.boost_feed_description": "ใƒ•ใ‚ฃใƒผใƒ‰ใ‚’ๅผทๅŒ–ใ™ใ‚‹ใ“ใจใงใ€ใ•ใ‚‰ใชใ‚‹็‰นๅ…ธใ‚’ๅพ—ใ‚‹ใ“ใจใŒใงใใพใ™๏ผ", - "boost.boost_success": "๐ŸŽ‰ ใƒ–ใƒผใ‚นใƒˆๆˆๅŠŸ๏ผ", - "boost.boost_success_thanks": "ใ‚ตใƒใƒผใƒˆใซๆ„Ÿ่ฌใ—ใพใ™๏ผ", - "boost.expired_description": "ไปŠใฏใ“ใ‚ŒไปฅไธŠใฎใƒ–ใƒผใ‚นใƒˆใƒใ‚คใƒณใƒˆใ‚’็ฒๅพ—ใงใใพใ›ใ‚“ใ€ใ—ใ‹ใ—ใŠๆฐ—่ปฝใซใƒ–ใƒผใ‚นใƒˆใ‚’็ถšใ‘ใฆใใ ใ•ใ„ใ€‚ใ‚ใชใŸใฎ็พๅœจใฎใƒ–ใƒผใ‚นใƒˆๆœ‰ๅŠนๆœŸ้–“ใฏๆฌกใฎใจใŠใ‚Šใงใ™ {{expiredDate, datetime}}", - "boost.feed_being_boosted": "ใƒ•ใ‚ฃใƒผใƒ‰ใ‚’ใƒ–ใƒผใ‚นใƒˆใ—ใฆใ„ใพใ™", - "boost.remaining_boosts_to_level_up": "{{remainingBoostsToLevelUp}} ใ•ใ‚‰ใชใ‚‹ใƒฌใƒ™ใƒซใฎใƒ–ใƒผใ‚นใƒˆ ใ‚ขใƒณใƒญใƒƒใ‚ฏใฏ็‰นๅ…ธใจใ—ใฆๆไพ›ใ•ใ‚Œใพใ™๏ผ", "discover.any_url_or_keyword": "ไปปๆ„ใฎ URL ใพใŸใฏใ‚ญใƒผใƒฏใƒผใƒ‰", "discover.default_option": " (ใƒ‡ใƒ•ใ‚ฉใƒซใƒˆ)", "discover.feed_description": "ใ“ใฎใƒ•ใ‚ฃใƒผใƒ‰ใฎ่ชฌๆ˜Žใฏ Folo ใง่กจ็คบใ•ใ‚Œใพใ™ใ€ใพใŸใƒ‘ใƒฉใƒกใƒผใ‚ฟ ใƒ•ใ‚ฉใƒผใƒ ใซ้–ข้€ฃใ™ใ‚‹ๆƒ…ๅ ฑใ‚’่จ˜ๅ…ฅใ™ใ‚‹ใ“ใจใŒใงใใพใ™ใ€‚", @@ -156,7 +147,6 @@ "entry_actions.share": "ๅ…ฑๆœ‰", "entry_actions.star": "ใ‚นใ‚ฟใƒผ", "entry_actions.starred": "ใ‚นใ‚ฟใƒผใซ่ฟฝๅŠ ใ•ใ‚Œใพใ—ใŸใ€‚", - "entry_actions.tip": "ใƒใƒƒใƒ—", "entry_actions.toggle_ai_summary": "AI ่ฆ็ด„ใซๅˆ‡ใ‚Šๆ›ฟใˆ", "entry_actions.toggle_ai_translation": "AI ็ฟป่จณใซๅˆ‡ใ‚Šๆ›ฟใˆ", "entry_actions.unstar": "ใ‚นใ‚ฟใƒผใ‚’่งฃ้™ค", @@ -179,8 +169,6 @@ "entry_content.readability_notice": "ใ“ใฎใ‚ณใƒณใƒ†ใƒณใƒ„ใฏ Readability ใซใ‚ˆใ‚Šๆไพ›ใ•ใ‚Œใฆใ„ใพใ™ใ€‚่ชคๅญ—ใชใฉใ‚’็™บ่ฆ‹ใ—ใŸๅ ดๅˆใฏใ€ๅ…ƒใฎใ‚ตใ‚คใƒˆใงใ‚ชใƒชใ‚ธใƒŠใƒซใฎใ‚ณใƒณใƒ†ใƒณใƒ„ใ‚’ใ”่ฆงใใ ใ•ใ„ใ€‚", "entry_content.render_error": "ใƒฌใƒณใƒ€ใƒชใƒณใ‚ฐใ‚จใƒฉใƒผ:", "entry_content.report_issue": "ๅ•้กŒใ‚’ๅ ฑๅ‘Š", - "entry_content.support_amount": "ใ“ใฎใƒ•ใ‚ฃใƒผใƒ‰ใฏ {{amount}} ไบบใŒ่ณผ่ชญใ—ใฆใ„ใพใ™ใ€‚", - "entry_content.support_creator": "ไฝœๆˆ่€…ใ‚’ใ‚ตใƒใƒผใƒˆ", "entry_content.web_app_notice": "ใ“ใฎใ‚ณใƒณใƒ†ใƒณใƒ„ใ‚ฟใ‚คใƒ—ใฏใ‚ฆใ‚งใƒ–ใ‚ขใƒ—ใƒชใงใฏใ‚ตใƒใƒผใƒˆใ•ใ‚Œใฆใ„ใชใ„ใ‹ใ‚‚ใ—ใ‚Œใพใ›ใ‚“ใ€‚ใƒ‡ใ‚นใ‚ฏใƒˆใƒƒใƒ—ใ‚ขใƒ—ใƒชใ‚’ใƒ€ใ‚ฆใƒณใƒญใƒผใƒ‰ใ—ใฆใใ ใ•ใ„ใ€‚", "entry_list.zero_unread": "ๆœช่ชญใ‚ผใƒญ", "entry_list_header.grid": "ใ‚ฐใƒชใƒƒใƒ‰", @@ -413,18 +401,6 @@ "sync_indicator.disabled": "ใ‚ปใ‚ญใƒฅใƒชใƒ†ใ‚ฃไธŠใฎ็†็”ฑใซใ‚ˆใ‚Šใ€ๅŒๆœŸใฏ็„กๅŠนใซใชใฃใฆใ„ใพใ™ใ€‚", "sync_indicator.offline": "ใ‚ชใƒ•ใƒฉใ‚คใƒณ", "sync_indicator.synced": "ใ‚ตใƒผใƒใƒผใจๅŒๆœŸๆธˆใฟ", - "tip_modal.amount": "้‡‘้ก", - "tip_modal.claim_feed": "ใ“ใฎใƒ•ใ‚ฃใƒผใƒ‰ใ‚’ใ‚ฏใƒฌใƒผใƒ ", - "tip_modal.create_wallet": "็„กๆ–™ใงไฝœๆˆ", - "tip_modal.feed_owner": "ใƒ•ใ‚ฃใƒผใƒ‰ใฎๆ‰€ๆœ‰่€…", - "tip_modal.low_balance": "ใ“ใฎใƒใƒƒใƒ—ใ‚’ใ‚ซใƒใƒผใ™ใ‚‹ใซใฏๆฎ‹้ซ˜ใŒไธ่ถณใ—ใฆใ„ใพใ™ใ€‚้‡‘้กใ‚’่ชฟๆ•ดใ—ใฆใใ ใ•ใ„ใ€‚", - "tip_modal.no_wallet": "ใพใ ใ‚ฆใ‚ฉใƒฌใƒƒใƒˆใŒใ‚ใ‚Šใพใ›ใ‚“ใ€‚ใ‚ฆใ‚ฉใƒฌใƒƒใƒˆใ‚’ไฝœๆˆใ—ใฆใƒใƒƒใƒ—ใ‚’้€ไฟกใ—ใฆใใ ใ•ใ„ใ€‚", - "tip_modal.tip_amount_sent": "ใŒไฝœ่€…ใซ้€ไฟกใ•ใ‚Œใพใ—ใŸใ€‚", - "tip_modal.tip_now": "ไปŠใ™ใใƒใƒƒใƒ—", - "tip_modal.tip_sent": "ใƒใƒƒใƒ—ใŒๆญฃๅธธใซ้€ไฟกใ•ใ‚Œใพใ—ใŸ๏ผใ”ๆ”ฏๆดใ‚ใ‚ŠใŒใจใ†ใ”ใ–ใ„ใพใ™ใ€‚", - "tip_modal.tip_support": "โญ ใ‚ตใƒใƒผใƒˆใ‚’็คบใ™ใŸใ‚ใซใƒใƒƒใƒ—ใ‚’้€ใ‚‹๏ผ", - "tip_modal.tip_title": "Power ใƒใƒƒใƒ—", - "tip_modal.unclaimed_feed": "ใ“ใฎใƒ•ใ‚ฃใƒผใƒ‰ใฏใพใ ่ชฐใซใ‚‚ใ‚ฏใƒฌใƒผใƒ ใ•ใ‚Œใฆใ„ใพใ›ใ‚“ใ€‚ๅ—ใ‘ๅ–ใฃใŸ Power ใฏใ€ใƒ•ใ‚ฃใƒผใƒ‰ใŒใ‚ฏใƒฌใƒผใƒ ใ•ใ‚Œใ‚‹ใพใงใƒ–ใƒญใƒƒใ‚ฏใƒใ‚งใƒผใƒณๅฅ‘็ด„ใงๅฎ‰ๅ…จใซไฟๆŒใ•ใ‚Œใพใ™ใ€‚", "trending.entry": "ไบบๆฐ—ใ‚จใƒณใƒˆใƒชใƒผ", "trending.entry_no_results": "ไบบๆฐ—ใฎใ‚จใƒณใƒˆใƒชใƒผใฏ่ฆ‹ใคใ‹ใ‚Šใพใ›ใ‚“ใงใ—ใŸ", "trending.feed": "ไบบๆฐ—ใƒ•ใ‚ฃใƒผใƒ‰", @@ -453,7 +429,6 @@ "words.actions": "ใ‚ขใ‚ฏใ‚ทใƒงใƒณ", "words.add": "่ฟฝๅŠ ", "words.all": "ใ™ในใฆ", - "words.boost": "ใƒ–ใƒผใ‚นใƒˆ", "words.browser": "ใƒ–ใƒฉใ‚ฆใ‚ถใƒผ", "words.categories": "ใ‚ซใƒ†ใ‚ดใƒชใƒผ", "words.confirm": "็ขบ่ช", diff --git a/locales/app/zh-CN.json b/locales/app/zh-CN.json index d1f6c5f7c..2be9f3654 100644 --- a/locales/app/zh-CN.json +++ b/locales/app/zh-CN.json @@ -3,8 +3,6 @@ "achievement.alpha_tester": "Alpha ๅ†…ๆต‹็”จๆˆท", "achievement.alpha_tester_description": "ๆ—ฉๆœŸ Folo Alpha ็‰ˆๆœฌ็š„ๅ†…ๆต‹็”จๆˆท", "achievement.description": "ๆˆไธบ็กฌๆ ธ็Žฉๅฎถ๏ผŒ่ตšๅ– NFTใ€‚", - "achievement.feed_booster": "่ฎข้˜…ๆบๅŠฉๅŠ›่€…", - "achievement.feed_booster_description": "ๅœจ Folo ไธŠๅŠฉๅŠ›่ฎข้˜…ๆบ", "achievement.first_claim_feed": "่ฎข้˜…ๆบๆ‰€ๆœ‰่€…", "achievement.first_claim_feed_description": "ๅœจ Folo ไธŠ่ฎค่ฏ่ฎข้˜…ๆบ", "achievement.first_create_list": "ๅˆ—่กจๅˆ›ไฝœ่€…", @@ -29,13 +27,6 @@ "app.copy_logo_svg": "ๅคๅˆถ Logo SVG", "app.copy_logo_text_svg": "ๅคๅˆถๅพฝๆ ‡ SVG ๆ–‡ๆœฌ", "app.toggle_sidebar": "ๅˆ‡ๆขไพง่พนๆ ", - "boost.boost_feed": "ๅŠฉๅŠ›", - "boost.boost_feed_description": "ๅขžๅผบ่ฎข้˜…ๆบไปฅ่งฃ้”ๆ›ดๅคš็ฆๅˆฉ๏ผŒ่ฎข้˜…่€…ๅฐ†ไผšๆ„Ÿๆฟ€่ฟ™ไบ›็ฆๅˆฉ๏ผ", - "boost.boost_success": "๐ŸŽ‰ ๅŠฉๅŠ›ๆˆๅŠŸ๏ผ", - "boost.boost_success_thanks": "ๆ„Ÿ่ฐขไฝ ็š„ๆ”ฏๆŒ๏ผ", - "boost.expired_description": "ไฝ ็ŽฐๅœจๅŠฉๅŠ›ไธไผšๅขžๅŠ ๆ›ดๅคšๅŠฉๅŠ›ๅ€ผ๏ผŒไฝ†ไปๅฏไปฅ็ปง็ปญๅŠฉๅŠ›ใ€‚ไฝ ๅฝ“ๅ‰็š„ๅŠฉๅŠ›ๅฐ†ไบŽ {{expiredDate, datetime}} ๅˆฐๆœŸใ€‚", - "boost.feed_being_boosted": "ๅทฒๅŠฉๅŠ›", - "boost.remaining_boosts_to_level_up": "ๅ†ๅŠฉๅŠ› {{remainingBoostsToLevelUp}} ๆฌกๅณๅฏ่งฃ้”ไธ‹ไธ€็บง็ฆๅˆฉ๏ผ", "discover.any_url_or_keyword": "ไปปๆ„้“พๆŽฅๆˆ–ๅ…ณ้”ฎ่ฏ", "discover.default_option": "๏ผˆ้ป˜่ฎค๏ผ‰", "discover.feed_description": "ๆ นๆฎๆ่ฟฐๅฎŒๅ–„็›ฎๆ ‡่ฎข้˜…ๆบ็š„็›ธๅ…ณไฟกๆฏใ€‚", @@ -156,7 +147,6 @@ "entry_actions.share": "ๅˆ†ไบซ", "entry_actions.star": "ๆ”ถ่—", "entry_actions.starred": "ๅทฒๆ”ถ่—", - "entry_actions.tip": "ๆ‰“่ต", "entry_actions.toggle_ai_summary": "ๅˆ‡ๆข AI ๆ€ป็ป“", "entry_actions.toggle_ai_translation": "ๅˆ‡ๆข AI ็ฟป่ฏ‘", "entry_actions.unstar": "ๅ–ๆถˆๆ”ถ่—", @@ -179,8 +169,6 @@ "entry_content.readability_notice": "ๆญคๅ†…ๅฎน็”ฑ Readability ๆไพ›ใ€‚ๅฆ‚ๆžœไฝ ๅ‘็ŽฐๆŽ’็‰ˆๅผ‚ๅธธ๏ผŒ่ฏท่ฎฟ้—ฎๆบ็ซ™ๆŸฅ็œ‹ๅŽŸๅง‹ๅ†…ๅฎนใ€‚", "entry_content.render_error": "ๆธฒๆŸ“้”™่ฏฏ๏ผš", "entry_content.report_issue": "ๅ้ฆˆ้—ฎ้ข˜", - "entry_content.support_amount": "{{amount}} ไบบๆ”ฏๆŒไบ†ๆญค่ฎข้˜…ๆบ็š„ไฝœ่€…ใ€‚", - "entry_content.support_creator": "ๆ”ฏๆŒไฝœ่€…", "entry_content.web_app_notice": "็ฝ‘้กต็‰ˆไธๆ”ฏๆŒๅฑ•็คบๆญค็ฑปๅž‹๏ผŒ่ฏทไธ‹่ฝฝๅฎขๆˆท็ซฏๆŸฅ็œ‹ใ€‚", "entry_list.zero_unread": "ๅ…จ้ƒจๅทฒ่ฏป", "entry_list_header.grid": "็ฝ‘ๆ ผๅธƒๅฑ€", @@ -415,18 +403,6 @@ "sync_indicator.disabled": "ๅ‡บไบŽๅฎ‰ๅ…จๅŽŸๅ› ๏ผŒๅทฒ็ฆ็”จๅŒๆญฅ", "sync_indicator.offline": "็ฆป็บฟ", "sync_indicator.synced": "ๅทฒๅŒๆญฅ่‡ณไบ‘็ซฏ", - "tip_modal.amount": "ๆ•ฐ้‡", - "tip_modal.claim_feed": "่ฎค่ฏๆญค่ฎข้˜…ๆบ", - "tip_modal.create_wallet": "ๅ…่ดนๅˆ›ๅปบ", - "tip_modal.feed_owner": "่ฎข้˜…ๆบๆ‰€ๆœ‰่€…", - "tip_modal.low_balance": "ไฝ ็š„้’ฑๅŒ…ไฝ™้ขไธ่ถณไปฅๆ”ฏไป˜ๆญคๆ‰“่ตใ€‚่ฏท่ฐƒๆ•ดๆ‰“่ตๆ•ฐ้ขใ€‚", - "tip_modal.no_wallet": "ๅฝ“ๅ‰ๆฒกๆœ‰้’ฑๅŒ…๏ผŒ่ฏทๅ…ˆๅˆ›ๅปบ้’ฑๅŒ…ไปฅ็ปง็ปญๆ‰“่ตใ€‚", - "tip_modal.tip_amount_sent": "ๅทฒๅ‘้€็ป™ไฝœ่€…ใ€‚", - "tip_modal.tip_now": "็ซ‹ๅณๆ‰“่ต", - "tip_modal.tip_sent": "ๆˆๅŠŸๆ‰“่ต๏ผๆ„Ÿ่ฐขไฝ ็š„ๆ”ฏๆŒใ€‚", - "tip_modal.tip_support": "โญ ๆ‰“่ต่กจ็คบๆ”ฏๆŒ๏ผ", - "tip_modal.tip_title": "ๆ‰“่ต Power", - "tip_modal.unclaimed_feed": "่ฟ™ไธช่ฎข้˜…ๆบ่ฟ˜ๆฒกๆœ‰่ขซ่ฎค่ฏ๏ผŒๆ”ถๅˆฐ็š„ Power ๅฐ†ๅญ˜ๆ”พๅœจๅŒบๅ—้“พ็›ดๅˆฐๆบๆ‰€ๆœ‰่€…ๅฎŒๆˆ่ฎค่ฏใ€‚", "trending.entry": "็ƒญ้—จๆก็›ฎ", "trending.entry_no_results": "ๆฒกๆœ‰ๆ‰พๅˆฐ็ƒญ้—จๆก็›ฎ", "trending.feed": "็ƒญ้—จ่ฎข้˜…ๆบ", @@ -455,7 +431,6 @@ "words.actions": "่‡ชๅŠจๅŒ–", "words.add": "ๆทปๅŠ ", "words.all": "ๅ…จ้ƒจ", - "words.boost": "ๅŠฉๅŠ›", "words.browser": "ๆต่งˆๅ™จ", "words.categories": "ๅˆ†็ฑป", "words.confirm": "็กฎ่ฎค", diff --git a/locales/app/zh-TW.json b/locales/app/zh-TW.json index 5d6683cd9..221090a65 100644 --- a/locales/app/zh-TW.json +++ b/locales/app/zh-TW.json @@ -3,8 +3,6 @@ "achievement.alpha_tester": "Alpha ๆธฌ่ฉฆ่€…", "achievement.alpha_tester_description": "ๆ‚จๅƒ่ˆ‡ไบ† Folo ็š„ Alpha ๆธฌ่ฉฆใ€‚", "achievement.description": "ๆˆ็‚บ็กฌๆดพ็Žฉๅฎถ๏ผŒ้‘„้€  NFTใ€‚", - "achievement.feed_booster": "RSS ๆ‘˜่ฆๅŠ ๆˆ่€…", - "achievement.feed_booster_description": "ๅœจ Folo ไธŠๅŠ ๆˆ RSS ๆ‘˜่ฆ", "achievement.first_claim_feed": "RSS ๆ‘˜่ฆไฝœ่€…", "achievement.first_claim_feed_description": "ๅœจ Folo ไธŠ่ช่ญ‰ RSS ๆ‘˜่ฆ", "achievement.first_create_list": "ๅˆ—่กจๅ‰ตๅปบ่€…", @@ -26,13 +24,6 @@ "app.copy_logo_svg": "่ค‡่ฃฝ Logo SVG", "app.copy_logo_text_svg": "่ค‡่ฃฝๅœ–ๆจ™ SVG ๆ–‡ๅญ—", "app.toggle_sidebar": "ๅˆ‡ๆ›ๅด้‚Šๆฌ„", - "boost.boost_feed": "ๅŠ ๆˆ", - "boost.boost_feed_description": "ๅŠ ๆˆ RSS ๆ‘˜่ฆไปฅ่งฃ้Ž–ๆ›ดๅคš็Žๅ‹ต๏ผŒ่จ‚้–ฑ่€…ๅฐ‡ๆœƒๆ„Ÿๆฟ€้€™ไบ›็Žๅ‹ต๏ผ", - "boost.boost_success": "๐ŸŽ‰ ๅŠ ๆˆๆˆๅŠŸ๏ผ", - "boost.boost_success_thanks": "ๆ„Ÿ่ฌๆ‚จ็š„ๆ”ฏๆŒ๏ผ", - "boost.expired_description": "ๆ‚จ็พๅœจๅŠ ๆˆไธๆœƒๅขžๅŠ ๆ›ดๅคš็ถ“้ฉ—ๅ€ผ๏ผŒไฝ†ไปๅฏไปฅ็นผ็บŒๅŠ ๆˆใ€‚ๆ‚จ็•ถๅ‰็š„ๅŠ ๆˆๅฐ‡ๆ–ผ {{expiredDate, datetime}} ๅˆฐๆœŸใ€‚", - "boost.feed_being_boosted": "่จ‚้–ฑๆญฃๅœจๅŠ ๆˆ", - "boost.remaining_boosts_to_level_up": "ๅ†ๆฌกๅŠ ๆˆ {{remainingBoostsToLevelUp}} ๆฌกๅณๅฏ่งฃ้Ž–ไธ‹ไธ€็ดš็Žๅ‹ต๏ผ", "discover.any_url_or_keyword": "ไปปไฝ• URL ๆˆ–้—œ้ตๅญ—", "discover.default_option": "๏ผˆ้ ่จญ๏ผ‰", "discover.feed_description": "ๆ นๆ“šๆ่ฟฐๅฎŒๆˆ็›ฎๆจ™ RSS ๆ‘˜่ฆ็š„็›ธ้—œ่ณ‡่จŠใ€‚", @@ -142,7 +133,6 @@ "entry_actions.share": "ๅˆ†ไบซ", "entry_actions.star": "ๆ”ถ่—", "entry_actions.starred": "ๅทฒๆ”ถ่—", - "entry_actions.tip": "่ดŠๅŠฉ", "entry_actions.toggle_ai_summary": "ๅˆ‡ๆ› AI ็ธฝ็ต", "entry_actions.toggle_ai_translation": "ๅˆ‡ๆ› AI ็ฟป่ญฏ", "entry_actions.unstar": "ๅ–ๆถˆๆ”ถ่—", @@ -164,8 +154,6 @@ "entry_content.readability_notice": "ๆญคๅ…งๅฎน็”ฑ'ๅฏ่ฎ€ๆจกๅผ'ๆไพ›ใ€‚ๅฆ‚ๆžœๆ‚จ็™ผ็พๆŽ’็‰ˆ็•ฐๅธธ๏ผŒ่ซ‹ๅ‰ๅพ€็ถฒ็ซ™ๆŸฅ็œ‹ๅŽŸๅง‹ๅ…งๅฎนใ€‚", "entry_content.render_error": "ๆธฒๆŸ“้Œฏ่ชค๏ผš", "entry_content.report_issue": "ๅ ฑๅ‘Šๅ•้กŒ", - "entry_content.support_amount": "{{amount}} ไบบ่ดŠๅŠฉไบ†ๆญค RSS ๆ‘˜่ฆ็š„ไฝœ่€…ใ€‚", - "entry_content.support_creator": "่ดŠๅŠฉไฝœ่€…", "entry_content.web_app_notice": "Web ๆ‡‰็”จ็จ‹ๅผไธๆ”ฏๆดๆญค้กžๅž‹ๅ…งๅฎนใ€‚ๆ‚จๅฏไปฅไธ‹่ผ‰ๆกŒ้ขๆ‡‰็”จ็จ‹ๅผใ€‚", "entry_list.zero_unread": "ๅ…จ้ƒจๅทฒ่ฎ€", "entry_list_header.grid": "ๆ ผ็ทšไฝˆๅฑ€", @@ -382,18 +370,6 @@ "sync_indicator.disabled": "็”ฑๆ–ผๅฎ‰ๅ…จๅŽŸๅ› ๏ผŒๅŒๆญฅๅŠŸ่ƒฝๅทฒๅœ็”จใ€‚", "sync_indicator.offline": "้›ข็ทš", "sync_indicator.synced": "ๅทฒ่ˆ‡ไผบๆœๅ™จๅŒๆญฅ", - "tip_modal.amount": "้‡‘้ก", - "tip_modal.claim_feed": "่ช้ ˜ๆญค RSS ๆ‘˜่ฆ", - "tip_modal.create_wallet": "ๅ…่ฒปๅปบ็ซ‹", - "tip_modal.feed_owner": "RSS ๆ‘˜่ฆไฝœ่€…", - "tip_modal.low_balance": "ๆ‚จ็š„้ค˜้กไธ่ถณไปฅไป˜ๆฌพๆญค่ดŠๅŠฉใ€‚่ซ‹่ชฟๆ•ด่ดŠๅŠฉ้‡‘้กใ€‚", - "tip_modal.no_wallet": "ๆ‚จๅฐšๆœชๆ“ๆœ‰้ŒขๅŒ…ใ€‚่ซ‹ๅ‰ตๅปบ้ŒขๅŒ…ไปฅ้€ฒ่กŒ่ดŠๅŠฉใ€‚", - "tip_modal.tip_amount_sent": "ๅทฒ็ถ“็™ผ้€็ตฆไฝœ่€…ใ€‚", - "tip_modal.tip_now": "็ซ‹ๅˆป่ดŠๅŠฉ", - "tip_modal.tip_sent": "่ดŠๅŠฉๆˆๅŠŸ๏ผ่ฌ่ฌๆ‚จ็š„ๆ”ฏๆŒใ€‚", - "tip_modal.tip_support": "โญ ่ดŠๅŠฉไปฅ่กจ็คบไฝ ็š„ๆ”ฏๆŒ๏ผ", - "tip_modal.tip_title": "่ดŠๅŠฉ Power", - "tip_modal.unclaimed_feed": "็›ฎๅ‰ๅฐš็„กไบบ่ช้ ˜ๆญค RSS ๆ‘˜่ฆใ€‚ๆ”ถๅˆฐ็š„ Power ๅฐ‡ๅฎ‰ๅ…จ็š„ๅญ˜ๆ”พๅœจๅ€ๅกŠ้ˆๅˆ็ด„ไธญ๏ผŒ็›ดๅˆฐ่ขซ่ช้ ˜็‚บๆญขใ€‚", "trending.entry": "็†ฑ้–€ๆข็›ฎ", "trending.entry_no_results": "ๆฒ’ๆœ‰ๆ‰พๅˆฐ็†ฑ้–€ๆข็›ฎ", "trending.feed": "็†ฑ้–€ RSS ๆ‘˜่ฆ", @@ -416,7 +392,6 @@ "words.actions": "่‡ชๅ‹•ๅŒ–ๆ“ไฝœ", "words.add": "ๆ–ฐๅขž", "words.all": "ๅ…จ้ƒจ", - "words.boost": "ๅŠ ๆˆ", "words.browser": "็€่ฆฝๅ™จ", "words.categories": "้กžๅˆฅ", "words.confirm": "็ขบ่ช", diff --git a/packages/internal/store/src/modules/entry/store.ts b/packages/internal/store/src/modules/entry/store.ts index 811f38d26..b44d03fcf 100644 --- a/packages/internal/store/src/modules/entry/store.ts +++ b/packages/internal/store/src/modules/entry/store.ts @@ -20,7 +20,6 @@ import type { InsertedBeforeTimeRangeFilter, PublishAtTimeRangeFilter, } from "../unread/types" -import { whoami } from "../user/getters" import { userActions } from "../user/store" import { getEntry } from "./getter" import type { EntryModel, FetchEntriesProps, FetchEntriesPropsSettings } from "./types" @@ -539,14 +538,7 @@ class EntrySyncServices { const dataFeeds = res.data?.map((e) => e.feeds).filter((f) => f.type === "feed") const feeds = dataFeeds?.map((f) => apiMorph.toFeed(f)) ?? [] - const users = dataFeeds?.flatMap((f) => f.tipUsers).filter((u) => !!u) ?? [] feedActions.upsertMany(feeds) - userActions.upsertMany( - users.map((u) => ({ - ...u, - isMe: u.id === whoami()?.id, - })), - ) return res } diff --git a/packages/internal/store/src/morph/api.ts b/packages/internal/store/src/morph/api.ts index 9a073a4ed..fdea68470 100644 --- a/packages/internal/store/src/morph/api.ts +++ b/packages/internal/store/src/morph/api.ts @@ -239,7 +239,6 @@ class APIMorph { errorAt: data.errorAt, errorMessage: data.errorMessage, siteUrl: data.siteUrl, - tipUserIds: data.tipUsers ? data.tipUsers.map((user) => user.id) : [], } } @@ -255,7 +254,6 @@ class APIMorph { errorAt: data.errorAt, errorMessage: data.errorMessage, siteUrl: data.siteUrl, - tipUserIds: data.tipUsers ? data.tipUsers.map((user) => user.id) : [], } } diff --git a/packages/internal/tracker/src/adapters/firebase.ts b/packages/internal/tracker/src/adapters/firebase.ts index d5733e8b8..38820bb23 100644 --- a/packages/internal/tracker/src/adapters/firebase.ts +++ b/packages/internal/tracker/src/adapters/firebase.ts @@ -91,30 +91,12 @@ export class FirebaseAdapter implements TrackerAdapter { } break } - case TrackerMapper.BoostSent: { - await this.firebaseInstance.logEvent("purchase", { - currency: "POWER", - value: properties?.amount, - items: `feed/${properties?.feedId}`, - transaction_id: properties?.transactionId, - }) - break - } case TrackerMapper.DailyRewardClaimed: { await this.firebaseInstance.logEvent("earn_virtual_currency", { virtual_currency_name: "POWER", }) break } - case TrackerMapper.TipSent: { - await this.firebaseInstance.logEvent("purchase", { - currency: "POWER", - value: properties?.amount, - items: `entry/${properties?.entryId}`, - transaction_id: properties?.transactionId, - }) - break - } default: { // For other events, use the event name directly const eventName = (properties?.__eventName as string) || "unknown_event" diff --git a/packages/internal/tracker/src/enums.ts b/packages/internal/tracker/src/enums.ts index 69a52208d..65cd03f8b 100644 --- a/packages/internal/tracker/src/enums.ts +++ b/packages/internal/tracker/src/enums.ts @@ -5,7 +5,6 @@ export enum TrackerMapper { AppInit = 1003, // Biz NavigateEntry = 2000, - BoostSent = 2001, Integration = 2002, DailyReportModal = 2003, SwitchToMasonry = 2004, @@ -15,10 +14,8 @@ export enum TrackerMapper { QuickAddFeed = 2008, PlayerOpenDuration = 2009, UpdateRestart = 2010, - TipModalOpened = 2011, FeedClaimed = 2012, DailyRewardClaimed = 2013, - TipSent = 2014, SubscribeModalOpened = 2015, // https://docs.google.com/spreadsheets/d/1XlUxTxiXWIQDHFYa2eoPBeuosR1t2h8VFIjXEOqmjhY/edit?gid=0#gid=0 diff --git a/packages/internal/tracker/src/tracker-points.ts b/packages/internal/tracker/src/tracker-points.ts index 536f6fe77..754dd4175 100644 --- a/packages/internal/tracker/src/tracker-points.ts +++ b/packages/internal/tracker/src/tracker-points.ts @@ -42,10 +42,6 @@ export class TrackerPoints { this.track(TrackerMapper.NavigateEntry, props) } - boostSent(props: { amount: string; feedId: string; transactionId: string }) { - this.track(TrackerMapper.BoostSent, props) - } - integration(props: { type: string; event: string }) { this.track(TrackerMapper.Integration, props) } @@ -80,10 +76,6 @@ export class TrackerPoints { this.track(TrackerMapper.UpdateRestart, props) } - tipModalOpened(props: { entryId?: string }) { - this.track(TrackerMapper.TipModalOpened, props) - } - subscribeModalOpened(props: { feedId?: string listId?: string @@ -101,10 +93,6 @@ export class TrackerPoints { this.track(TrackerMapper.DailyRewardClaimed) } - tipSent(props: { amount: string; entryId: string; transactionId: string }) { - this.track(TrackerMapper.TipSent, props) - } - register(props: { type: "email" | "social" }) { this.track(TrackerMapper.Register, props) } From 13f5fcb0c9f6658518e98d6e15417ac8ff66b00c Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Fri, 26 Sep 2025 10:13:31 +0800 Subject: [PATCH 004/601] fix: remove refer feed and date, update mention format (#4522) --- .../context-bar/blocks/ContextBlock.tsx | 19 +- .../context-bar/menus/ContextMenuContent.tsx | 104 +------ .../context-bar/pickers/EntryPickers.tsx | 18 +- .../components/layouts/ChatInterface.tsx | 5 +- .../components/message/AIDataBlockItem.tsx | 7 +- .../components/message/ai-block-constants.ts | 14 - .../editor/plugins/mention/MentionNode.tsx | 21 +- .../editor/plugins/mention/MentionPlugin.tsx | 5 +- .../plugins/mention/hooks/dateMentionUtils.ts | 2 +- .../mention/hooks/useMentionBlockSync.ts | 276 ------------------ .../mention/hooks/useMentionIntegration.ts | 29 -- .../src/modules/ai-chat/store/types.ts | 9 +- .../store/src/modules/entry/getter.ts | 13 +- .../internal/store/src/modules/entry/hooks.ts | 2 +- 14 files changed, 48 insertions(+), 476 deletions(-) delete mode 100644 apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/mention/hooks/useMentionBlockSync.ts delete mode 100644 apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/mention/hooks/useMentionIntegration.ts diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/context-bar/blocks/ContextBlock.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/context-bar/blocks/ContextBlock.tsx index 2eec98e60..5bfd5401a 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/context-bar/blocks/ContextBlock.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/context-bar/blocks/ContextBlock.tsx @@ -12,7 +12,6 @@ import { getFileCategoryFromMimeType, getFileIconName, } from "~/modules/ai-chat/utils/file-validation" -import { formatMentionDateValue } from "~/modules/ai-chat/utils/mentionDate" import { EntryTitle, FeedTitle } from "./TitleComponents" @@ -120,13 +119,9 @@ export const ContextBlock: FC<{ block: AIChatContextBlock }> = memo(({ block }) case "referEntry": { return "i-mgc-paper-cute-fi" } - case "mainFeed": - case "referFeed": { + case "mainFeed": { return "i-mgc-rss-cute-fi" } - case "referDate": { - return "i-mgc-calendar-time-add-cute-re" - } case "selectedText": { return "i-mgc-quill-pen-cute-re" } @@ -158,13 +153,9 @@ export const ContextBlock: FC<{ block: AIChatContextBlock }> = memo(({ block }) case "referEntry": { return } - case "mainFeed": - case "referFeed": { + case "mainFeed": { return } - case "referDate": { - return formatMentionDateValue(block.value).label - } case "selectedText": { return `"${block.value}"` } @@ -261,12 +252,6 @@ export const ContextBlock: FC<{ block: AIChatContextBlock }> = memo(({ block }) case "referEntry": { return "Ref" } - case "referFeed": { - return "Feed" - } - case "referDate": { - return "Date" - } case "selectedText": { return "Text" } diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/context-bar/menus/ContextMenuContent.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/context-bar/menus/ContextMenuContent.tsx index c87e521c0..fe165e935 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/context-bar/menus/ContextMenuContent.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/context-bar/menus/ContextMenuContent.tsx @@ -1,27 +1,18 @@ -import { DateTimePicker } from "@follow/components/ui/input/index.js" -import dayjs from "dayjs" import type { FC } from "react" -import { useState } from "react" -import { useTranslation } from "react-i18next" import { DropdownMenuContent, - DropdownMenuItem, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, } from "~/components/ui/dropdown-menu/dropdown-menu" -import { RELATIVE_DATE_DEFINITIONS } from "~/modules/ai-chat/editor/plugins/mention/hooks/dateMentionConfig" -import { formatRangeValue } from "~/modules/ai-chat/editor/plugins/mention/hooks/dateMentionUtils" import { useChatBlockActions } from "~/modules/ai-chat/store/hooks" -import { CurrentFeedEntriesPickerList, FeedPickerList, RecentEntriesPickerList } from "../pickers" +import { CurrentFeedEntriesPickerList, RecentEntriesPickerList } from "../pickers" export const ContextMenuContent: FC = () => { const blockActions = useChatBlockActions() - const { t } = useTranslation("ai") - const today = dayjs().startOf("day") return ( @@ -59,99 +50,6 @@ export const ContextMenuContent: FC = () => { /> - - - - Reference Feed - - - - blockActions.addBlock({ - type: "referFeed", - value: feedId, - }) - } - /> - - - - {/* Reference Date */} - - - - Reference Date - - - {/* Presets based on RELATIVE_DATE_DEFINITIONS */} - {RELATIVE_DATE_DEFINITIONS.map((def) => { - const range = def.range(today) - if (!range) return null - const label = t(def.labelKey) - const value = formatRangeValue(range) - return ( - blockActions.addBlock({ type: "referDate", value })} - > - {String(label)} - - ) - })} - - - - {/* Custom range (opens a small inline range picker) */} - blockActions.addBlock({ type: "referDate", value })} - /> - - ) } - -function CustomRangePicker({ onConfirm }: { onConfirm: (value: string) => void }) { - const [range, setRange] = useState<{ start?: string; end?: string }>({}) - const buildValue = (): string | null => { - if (!range.start || !range.end) return null - return formatRangeValue({ - start: dayjs(range.start).startOf("day"), - end: dayjs(range.end).startOf("day"), - }) - } - - return ( -
    -
    Custom range
    - setRange(next)} - rangePlaceholder="Select range" - contentClassName="z-[999]" - /> -
    - - -
    -
    - ) -} diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/context-bar/pickers/EntryPickers.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/context-bar/pickers/EntryPickers.tsx index e23e1e361..77f5e372b 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/context-bar/pickers/EntryPickers.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/context-bar/pickers/EntryPickers.tsx @@ -1,4 +1,5 @@ -import { useEntry, useEntryIdsByFeedId, useEntryIdsByView } from "@follow/store/entry/hooks" +import { getEntry } from "@follow/store/entry/getter" +import { useEntryIdsByFeedId, useEntryIdsByView } from "@follow/store/entry/hooks" import { useEntryStore } from "@follow/store/entry/store" import type { FC } from "react" import { useMemo } from "react" @@ -12,13 +13,16 @@ import { PickerList } from "./PickerList" export const CurrentFeedEntriesPickerList: FC<{ onSelect: (entryId: string) => void }> = ({ onSelect, }) => { - const mainEntryId = useAIChatStore()((s) => { - const block = s.blocks.find((b) => b.type === "mainEntry") - return block && block.type === "mainEntry" ? block.value : undefined + const mainFeedId = useAIChatStore()((s) => { + const mainFeedBlock = s.blocks.find((b) => b.type === "mainFeed") + const mainEntryBlock = s.blocks.find((b) => b.type === "mainEntry") + return mainFeedBlock && mainFeedBlock.type === "mainFeed" + ? mainFeedBlock.value + : mainEntryBlock && mainEntryBlock.type === "mainEntry" + ? getEntry(mainEntryBlock.value)?.feedId + : null }) - const feedId = useEntry(mainEntryId, (e) => e?.feedId) - - const entryIds = useEntryIdsByFeedId(feedId!) + const entryIds = useEntryIdsByFeedId(mainFeedId) return } diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatInterface.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatInterface.tsx index 939c454b4..29fb2e8dd 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatInterface.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatInterface.tsx @@ -187,10 +187,7 @@ const ChatInterfaceContent = ({ centerInputOnEmpty }: ChatInterfaceProps) => { serverUrl: block.attachment.serverUrl, }, }) - } else if ( - (block.type === "mainFeed" || block.type === "referFeed") && - block.value.startsWith(ROUTE_FEED_IN_FOLDER) - ) { + } else if (block.type === "mainFeed" && block.value.startsWith(ROUTE_FEED_IN_FOLDER)) { const categoryName = block.value.slice(ROUTE_FEED_IN_FOLDER.length) const { view } = getRouteParams() const feedIds = getCategoryFeedIds(categoryName, view) diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIDataBlockItem.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIDataBlockItem.tsx index 1d041a67e..1317c47fd 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIDataBlockItem.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIDataBlockItem.tsx @@ -4,7 +4,6 @@ import { t } from "i18next" import * as React from "react" import type { AIChatContextBlock } from "~/modules/ai-chat/store/types" -import { formatMentionDateValue } from "~/modules/ai-chat/utils/mentionDate" import { getBlockIcon, @@ -34,13 +33,9 @@ const getDisplayContent = (block: AIChatContextBlock): React.ReactNode => { case "referEntry": { return } - case "mainFeed": - case "referFeed": { + case "mainFeed": { return } - case "referDate": { - return formatMentionDateValue(block.value).label - } case "selectedText": { return `"${block.value}"` } diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/ai-block-constants.ts b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/ai-block-constants.ts index 4e852d436..061c542e5 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/ai-block-constants.ts +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/ai-block-constants.ts @@ -25,16 +25,6 @@ export const BLOCK_STYLES = { icon: "bg-blue/10 text-blue", label: "text-blue", }, - referFeed: { - container: "from-green/5 to-green/10 border-green/20 hover:border-green/30", - icon: "bg-green/10 text-green", - label: "text-green", - }, - referDate: { - container: "from-purple/5 to-purple/10 border-purple/20 hover:border-purple/30", - icon: "bg-purple/10 text-purple", - label: "text-purple", - }, selectedText: { container: "from-purple/5 to-purple/10 border-purple/20 hover:border-purple/30", icon: "bg-purple/10 text-purple", @@ -63,8 +53,6 @@ export const BLOCK_ICONS = { mainEntry: "i-mgc-star-cute-fi", mainFeed: "i-mgc-rss-cute-fi", referEntry: "i-mgc-paper-cute-fi", - referFeed: "i-mgc-rss-cute-fi", - referDate: "i-mgc-calendar-time-add-cute-re", selectedText: "i-mgc-quill-pen-cute-re", fileAttachment: "i-mgc-file-upload-cute-re", } as const @@ -76,8 +64,6 @@ export const BLOCK_LABELS = { mainEntry: "Current", mainFeed: "Current", referEntry: "Ref", - referFeed: "Feed", - referDate: "Date", selectedText: "Text", fileAttachment: "File", mainView: "View", diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/mention/MentionNode.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/mention/MentionNode.tsx index 36a296a3a..e0dbe2d75 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/mention/MentionNode.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/mention/MentionNode.tsx @@ -1,3 +1,4 @@ +import { getCategoryFeedIds } from "@follow/store/subscription/getter" import i18next from "i18next" import type { DOMConversionMap, @@ -13,6 +14,9 @@ import type { import { $applyNodeReplacement, DecoratorNode } from "lexical" import * as React from "react" +import { ROUTE_FEED_IN_FOLDER } from "~/constants" +import { getRouteParams } from "~/hooks/biz/useRouteParams" + import { MentionComponent } from "./components/MentionComponent" import { RANGE_WITH_LABEL_KEY } from "./hooks/dateMentionConfig" import { getDateMentionDisplayName } from "./hooks/dateMentionUtils" @@ -96,7 +100,22 @@ export class MentionNode extends DecoratorNode { * For export markdown conversion */ override getTextContent(): string { - return `[[ref:${this.__mentionData.type}:${this.__mentionData.value}]]` + const { type, value } = this.__mentionData + if (type === "date" && value) { + return value as string + } + + if ( + type === "category" && + typeof value === "string" && + value.startsWith(ROUTE_FEED_IN_FOLDER) + ) { + const { view } = getRouteParams() + const ids = getCategoryFeedIds(value.slice(ROUTE_FEED_IN_FOLDER.length), view) + return `` + } + + return `` } override decorate(_editor: LexicalEditor): React.JSX.Element { diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/mention/MentionPlugin.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/mention/MentionPlugin.tsx index a703b7edc..22b898c75 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/mention/MentionPlugin.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/mention/MentionPlugin.tsx @@ -3,9 +3,9 @@ import { Suspense, useMemo } from "react" import { MentionDropdown } from "./components/MentionDropdown" import { DEFAULT_MAX_SUGGESTIONS } from "./constants" -import { useMentionIntegration } from "./hooks/useMentionIntegration" import { useMentionKeyboard } from "./hooks/useMentionKeyboard" import { useMentionSearch } from "./hooks/useMentionSearch" +import { useMentionSearchService } from "./hooks/useMentionSearchService" import { useMentionSelection } from "./hooks/useMentionSelection" import { useMentionTrigger } from "./hooks/useMentionTrigger" import { MentionNode } from "./MentionNode" @@ -13,7 +13,7 @@ import { defaultTriggerFn } from "./utils/triggerDetection" export function MentionPlugin() { // Get integrated search and context block handling - const { searchMentions, handleMentionInsert } = useMentionIntegration() + const { searchMentions } = useMentionSearchService() // Hook for detecting mention triggers const { mentionMatch, isActive, clearMentionMatch } = useMentionTrigger({ @@ -37,7 +37,6 @@ export function MentionPlugin() { // Hook for handling mention selection const { selectMention } = useMentionSelection({ mentionMatch, - onMentionInsert: handleMentionInsert, onSelectionComplete: () => { clearMentionMatch() clearSuggestions() diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/mention/hooks/dateMentionUtils.ts b/apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/mention/hooks/dateMentionUtils.ts index 65ca42dd9..6ebe4657c 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/mention/hooks/dateMentionUtils.ts +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/mention/hooks/dateMentionUtils.ts @@ -42,7 +42,7 @@ export const formatRangeValue = (range: DateRange): string => { const endExclusive = range.end.add(1, "day").startOf("day") const endIso = endExclusive.format(MENTION_DATE_VALUE_FORMAT) - return `${startIso}..${endIso}` + return `` } const DEFAULT_DATE_FORMAT: Intl.DateTimeFormatOptions = { diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/mention/hooks/useMentionBlockSync.ts b/apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/mention/hooks/useMentionBlockSync.ts deleted file mode 100644 index 4808deb2c..000000000 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/mention/hooks/useMentionBlockSync.ts +++ /dev/null @@ -1,276 +0,0 @@ -import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext" -import { $getNodeByKey } from "lexical" -import { useCallback, useEffect, useRef } from "react" - -import { useAIChatStore } from "~/modules/ai-chat/store/AIChatContext" -import { useChatBlockActions } from "~/modules/ai-chat/store/hooks" -import type { AIChatContextBlock, ValueContextBlock } from "~/modules/ai-chat/store/types" - -import { $isMentionNode, MentionNode } from "../MentionNode" -import type { MentionData, MentionType } from "../types" - -interface MentionBlockReference { - mentionNodeKey: string - blockId: string - resourceId: string // `${type}:${value}` - mentionData: MentionData -} - -const getResourceId = (type: string, value: string) => `${type}:${value}` - -const getBlockType = (mentionType: string): ValueContextBlock["type"] => { - switch (mentionType) { - case "feed": { - return "referFeed" - } - case "category": { - return "referFeed" - } - case "date": { - return "referDate" - } - default: { - return "referEntry" - } - } -} - -/** - * Hook that manages bidirectional synchronization between mention nodes and context blocks - * - When a mention is added, corresponding block is created - * - When a block is removed, corresponding mentions are removed - * - When a mention is removed, corresponding block is removed (if no other mentions reference it) - */ -export const useMentionBlockSync = (ignoreTypes: MentionType[]) => { - const [editor] = useLexicalComposerContext() - const blockActions = useChatBlockActions() - const blocks = useAIChatStore()((state) => state.blocks) - - // Reference tracking maps - const mentionToBlockRef = useRef>(undefined!) - if (!mentionToBlockRef.current) { - mentionToBlockRef.current = new Map() - } - - const resourceToMentionsRef = useRef>>(undefined!) - if (!resourceToMentionsRef.current) { - resourceToMentionsRef.current = new Map() - } - - const blockToResourceRef = useRef>(undefined!) - if (!blockToResourceRef.current) { - blockToResourceRef.current = new Map() - } - - // Add mention-block reference - const addMentionReference = useCallback( - (mentionData: MentionData, mentionNodeKey: string, blockId: string) => { - const resourceId = getResourceId(mentionData.type, mentionData.value as string) - - const reference: MentionBlockReference = { - mentionNodeKey, - blockId, - resourceId, - mentionData, - } - - // Update tracking maps - mentionToBlockRef.current.set(mentionNodeKey, reference) - - if (!resourceToMentionsRef.current.has(resourceId)) { - resourceToMentionsRef.current.set(resourceId, new Set()) - } - resourceToMentionsRef.current.get(resourceId)!.add(mentionNodeKey) - - blockToResourceRef.current.set(blockId, resourceId) - }, - [], - ) - - // Remove mention reference - const removeMentionReference = useCallback((mentionNodeKey: string) => { - const reference = mentionToBlockRef.current.get(mentionNodeKey) - if (!reference) return null - - const { resourceId, blockId } = reference - - // Clean up tracking maps - mentionToBlockRef.current.delete(mentionNodeKey) - - const mentionSet = resourceToMentionsRef.current.get(resourceId) - if (mentionSet) { - mentionSet.delete(mentionNodeKey) - if (mentionSet.size === 0) { - resourceToMentionsRef.current.delete(resourceId) - } - } - - blockToResourceRef.current.delete(blockId) - - return reference - }, []) - - // Handle mention insertion - create block and track reference - const handleMentionInsert = useCallback( - (mentionData: MentionData, mentionNodeKey: string) => { - if (ignoreTypes.includes(mentionData.type)) { - return - } - - const resourceId = getResourceId(mentionData.type, mentionData.value as string) - - // Check if block already exists for this resource - const existingBlock = blocks.find( - (block) => blockToResourceRef.current.get(block.id) === resourceId, - ) - - let blockId: string - if (existingBlock) { - // Use existing block - blockId = existingBlock.id - } else { - // Create new block - const blockType = getBlockType(mentionData.type) - - if (!blockType) { - return - } - - // Generate block ID (mimicking the block slice logic) - const newBlock: Omit = { - type: blockType, - value: mentionData.value as string, - } - - blockActions.addBlock(newBlock) - - // Use current blocks state directly from store instead of stale closure - const currentBlocks = blockActions.getBlocks() - const addedBlock = currentBlocks.find( - (block): block is Extract => - block.type === blockType && block.value === mentionData.value, - ) - - if (addedBlock) { - blockId = addedBlock.id - } else { - // Fallback to a predictable ID pattern if we can't find the block immediately - blockId = `${blockType}-${mentionData.value}-${mentionNodeKey}` - } - } - - // Track the reference - addMentionReference(mentionData, mentionNodeKey, blockId) - }, - [blocks, blockActions, addMentionReference], - ) - - // Handle mention removal - remove block if no other mentions reference it - const handleMentionRemove = useCallback( - (mentionNodeKey: string) => { - const reference = removeMentionReference(mentionNodeKey) - if (!reference) return - - const { resourceId, blockId } = reference - - // Check if any other mentions still reference this resource - const remainingMentions = resourceToMentionsRef.current.get(resourceId) - if (!remainingMentions || remainingMentions.size === 0) { - // No more mentions reference this resource, remove the block - - blockActions.removeBlock(blockId) - } - }, - [blockActions, removeMentionReference], - ) - - // Handle block removal - remove corresponding mentions - const handleBlockRemove = useCallback( - (blockId: string) => { - const resourceId = blockToResourceRef.current.get(blockId) - if (!resourceId) return - - const mentionKeys = resourceToMentionsRef.current.get(resourceId) - if (!mentionKeys) return - - // Remove all mention nodes for this resource - editor.update(() => { - Array.from(mentionKeys).forEach((mentionKey) => { - const node = $getNodeByKey(mentionKey) - if (node && $isMentionNode(node)) { - node.remove() - } - }) - }) - - // Clean up references - Array.from(mentionKeys).forEach((mentionKey) => { - removeMentionReference(mentionKey) - }) - }, - [editor, removeMentionReference], - ) - - // Monitor block changes - useEffect(() => { - const currentBlockIds = new Set(blocks.map((block) => block.id)) - const trackedBlockIds = new Set(blockToResourceRef.current.keys()) - - // Find removed blocks - for (const trackedBlockId of trackedBlockIds) { - if (!currentBlockIds.has(trackedBlockId)) { - handleBlockRemove(trackedBlockId) - } - } - }, [blocks, handleBlockRemove]) - - // Monitor mention node changes using mutation observer - useEffect(() => { - const removedMentionKeys = new Set() - - const unregisterMutationListener = editor.registerMutationListener( - MentionNode, - (mutatedNodes) => { - for (const [nodeKey, mutation] of mutatedNodes) { - // Only track destroyed mutations for mentions we're actually tracking - if (mutation === "destroyed" && mentionToBlockRef.current.has(nodeKey)) { - removedMentionKeys.add(nodeKey) - } - } - - // Process removed mentions in next tick to avoid state conflicts - if (removedMentionKeys.size > 0) { - const timeoutId = setTimeout(() => { - Array.from(removedMentionKeys).forEach((mentionKey) => { - handleMentionRemove(mentionKey) - }) - removedMentionKeys.clear() - }, 100) // Increase delay to avoid race conditions - - // Store timeout ID for potential cleanup - return () => clearTimeout(timeoutId) - } - }, - ) - - return unregisterMutationListener - }, [editor, handleMentionRemove]) - - // Cleanup on unmount - useEffect(() => { - const mentionToBlock = mentionToBlockRef.current - const resourceToMentions = resourceToMentionsRef.current - const blockToResource = blockToResourceRef.current - - return () => { - mentionToBlock.clear() - resourceToMentions.clear() - blockToResource.clear() - } - }, []) - - return { - handleMentionInsert, - handleMentionRemove, - } -} diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/mention/hooks/useMentionIntegration.ts b/apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/mention/hooks/useMentionIntegration.ts deleted file mode 100644 index 5c909b455..000000000 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/mention/hooks/useMentionIntegration.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { useCallback } from "react" - -import type { MentionData } from "../types" -import { useMentionBlockSync } from "./useMentionBlockSync" -import { useMentionSearchService } from "./useMentionSearchService" - -/** - * Hook that integrates mention search with context block management - * Provides search functionality and handles bidirectional synchronization - */ -export const useMentionIntegration = () => { - const { searchMentions } = useMentionSearchService() - const { handleMentionInsert: syncMentionInsert } = useMentionBlockSync(["date"]) - - // Handle mention insertion with node key tracking - const handleMentionInsert = useCallback( - (mention: MentionData, nodeKey?: string) => { - if (nodeKey) { - syncMentionInsert(mention, nodeKey) - } - }, - [syncMentionInsert], - ) - - return { - searchMentions, - handleMentionInsert, - } -} diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/store/types.ts b/apps/desktop/layer/renderer/src/modules/ai-chat/store/types.ts index a70c380da..fd005d0f7 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/store/types.ts +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/store/types.ts @@ -19,14 +19,7 @@ interface BaseContextBlock { } export interface ValueContextBlock extends BaseContextBlock { - type: - | "mainView" - | "mainEntry" - | "mainFeed" - | "referEntry" - | "referFeed" - | "referDate" - | "selectedText" + type: "mainView" | "mainEntry" | "mainFeed" | "referEntry" | "selectedText" value: string } diff --git a/packages/internal/store/src/modules/entry/getter.ts b/packages/internal/store/src/modules/entry/getter.ts index a24fc6d9b..6d7d27a2c 100644 --- a/packages/internal/store/src/modules/entry/getter.ts +++ b/packages/internal/store/src/modules/entry/getter.ts @@ -39,12 +39,13 @@ export const getEntryIdsByViewSelector = .sort((a, b) => sortEntryIdsByPublishDate(a, b)) } -export const getEntryIdsByFeedIdSelector = (state: StateType) => (feedId: string | undefined) => { - if (!feedId) return null - const ids = state.entryIdByFeed[feedId] - if (!ids) return null - return Array.from(ids).sort((a, b) => sortEntryIdsByPublishDate(a, b)) -} +export const getEntryIdsByFeedIdSelector = + (state: StateType) => (feedId: string | undefined | null) => { + if (!feedId) return null + const ids = state.entryIdByFeed[feedId] + if (!ids) return null + return Array.from(ids).sort((a, b) => sortEntryIdsByPublishDate(a, b)) + } export const getEntryIdsByFeedIdsSelector = (state: StateType) => (feedIds: string[] | undefined) => { diff --git a/packages/internal/store/src/modules/entry/hooks.ts b/packages/internal/store/src/modules/entry/hooks.ts index 7ecef47c4..2c48ea1f3 100644 --- a/packages/internal/store/src/modules/entry/hooks.ts +++ b/packages/internal/store/src/modules/entry/hooks.ts @@ -189,7 +189,7 @@ export const useEntryIdsByView = (view: FeedViewType, excludePrivate: boolean | ) } -export const useEntryIdsByFeedId = (feedId: string | undefined) => { +export const useEntryIdsByFeedId = (feedId: string | undefined | null) => { return useEntryStore(useCallback((state) => getEntryIdsByFeedIdSelector(state)(feedId), [feedId])) } From 3815c6c71c411c3f90c7f58d324ed3026d24f86b Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Fri, 26 Sep 2025 10:50:43 +0800 Subject: [PATCH 005/601] fix: update client sdk to fix string array query --- pnpm-lock.yaml | 593 +++++++++++++++++++++++++++----------------- pnpm-workspace.yaml | 5 +- 2 files changed, 366 insertions(+), 232 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9caa48373..cf79515ff 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,8 +7,8 @@ settings: catalogs: default: '@follow-app/client-sdk': - specifier: 0.3.66 - version: 0.3.66 + specifier: 0.3.67 + version: 0.3.67 tailwindcss-uikit-colors: specifier: 1.0.0 version: 1.0.0 @@ -339,7 +339,7 @@ importers: version: 4.3.0 '@follow-app/client-sdk': specifier: 'catalog:' - version: 0.3.66(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0)(utf-8-validate@6.0.5))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(kysely@0.28.7)(pg@8.16.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 0.3.67(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0)(utf-8-validate@6.0.5))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7)(pg@8.16.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@follow-app/readability': specifier: workspace:* version: link:../../../../packages/readability @@ -454,7 +454,7 @@ importers: version: 3.0.2(electron@38.1.0) '@follow-app/client-sdk': specifier: 'catalog:' - version: 0.3.66(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.7)(pg@8.16.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 0.3.67(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7)(pg@8.16.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@follow/database': specifier: workspace:* version: link:../../../../packages/internal/database @@ -1181,7 +1181,7 @@ importers: version: 6.2.1 '@follow-app/client-sdk': specifier: 'catalog:' - version: 0.3.66(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.7)(pg@8.16.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 0.3.67(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7)(pg@8.16.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@follow/tracker': specifier: workspace:* version: link:../../packages/internal/tracker @@ -1667,7 +1667,7 @@ importers: dependencies: '@follow-app/client-sdk': specifier: 'catalog:' - version: 0.3.66(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.7)(pg@8.16.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 0.3.67(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7)(pg@8.16.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@follow/configs': specifier: workspace:* version: link:../../configs @@ -1679,7 +1679,7 @@ importers: dependencies: '@follow-app/client-sdk': specifier: 'catalog:' - version: 0.3.66(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.7)(pg@8.16.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 0.3.67(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7)(pg@8.16.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@follow/constants': specifier: workspace:* version: link:../constants @@ -1751,7 +1751,7 @@ importers: dependencies: '@follow-app/client-sdk': specifier: 'catalog:' - version: 0.3.66(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.7)(pg@8.16.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 0.3.67(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7)(pg@8.16.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@follow/constants': specifier: workspace:* version: link:../constants @@ -1782,7 +1782,7 @@ importers: version: 1.0.1(@types/node@24.5.2) '@follow-app/client-sdk': specifier: 'catalog:' - version: 0.3.66(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.7)(pg@8.16.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 0.3.67(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7)(pg@8.16.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@folo-services/drizzle': specifier: 0.1.27 version: 0.1.27(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.7) @@ -1812,7 +1812,7 @@ importers: dependencies: '@follow-app/client-sdk': specifier: 'catalog:' - version: 0.3.66(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.7)(pg@8.16.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 0.3.67(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7)(pg@8.16.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@follow/configs': specifier: workspace:* version: link:../../configs @@ -1861,7 +1861,7 @@ importers: devDependencies: '@follow-app/client-sdk': specifier: 'catalog:' - version: 0.3.66(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.7)(pg@8.16.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 0.3.67(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7)(pg@8.16.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@follow/configs': specifier: workspace:* version: link:../../configs @@ -2023,6 +2023,11 @@ packages: '@ark/util@0.46.0': resolution: {integrity: sha512-JPy/NGWn/lvf1WmGCPw2VGpBg5utZraE84I7wli18EDF3p3zc/e9WolT35tINeZO3l7C77SjqRJeAUoT0CvMRg==} + '@asteasolutions/zod-to-openapi@8.1.0': + resolution: {integrity: sha512-tQFxVs05J/6QXXqIzj6rTRk3nj1HFs4pe+uThwE95jL5II2JfpVXkK+CqkO7aT0Do5AYqO6LDrKpleLUFXgY+g==} + peerDependencies: + zod: ^4.0.0 + '@babel/code-frame@7.10.4': resolution: {integrity: sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==} @@ -4210,23 +4215,26 @@ packages: '@floating-ui/utils@0.2.10': resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} - '@follow-app/client-sdk@0.3.66': - resolution: {integrity: sha512-njQdiY8IUkQj/rWRu9P019qi6AKqWVQ+nx1p8XkWU1og/RQZ+BRdffL+XVyRTSIV4PL7C5SXDZr861w1MKgpRQ==} + '@follow-app/client-sdk@0.3.67': + resolution: {integrity: sha512-6LMtHhmjHFgrtzE+Q0V5Jjx16TMVkNmcLwoBTTCyqC3+M1RWW1KpVfNULTQ1zWmRh71cRvUJJXz1gdd3FpBfEg==} '@folo-services/ai-tools@0.2.41': resolution: {integrity: sha512-tX2hTfLAZHYw7B0fWVuCA+uE1ZDGGKyDaPLEgn9WRdwYVnBdOqBFPbNx3FioB69fg+aLHeCKgHMkNEmlQZ/nUA==} - '@folo-services/constants@0.1.33': - resolution: {integrity: sha512-RF6hE4K5tKUUge+uEhM/6A276tuSeqq1oaQjou5/A+JEUa+VDYRIVfeEXoJhxSfdb+fs4oeMlWm0TS4GOa2SMA==} + '@folo-services/constants@0.1.34': + resolution: {integrity: sha512-l22gfh84z/8WB4E3Q84Nbpp2vU5qCAKBuE4eL94UxejryyTfYWosRz1vD13JS/FDUMNJxpmvuqbPjrMFHqd1eg==} '@folo-services/drizzle@0.1.27': resolution: {integrity: sha512-7bo9ZkBwGaYmDJqo+PX1eLYO0oJQMZqOkUEEchD78GldfnkaR2AQ5SK7BYrb57Qi6MQJ+A1AuFd3DV41o0cJfw==} + '@folo-services/drizzle@0.1.28': + resolution: {integrity: sha512-j6m3TVHC947vctWStj/9ak+H2uPNgp3G7+JP9x20V1YBmnqqt8Uyar+qmu0sUoI0HHgh0nqgYF6NBzIqTQ0rBA==} + '@folo-services/exceptions@0.1.16': resolution: {integrity: sha512-ThmLLe1Dv2CFsDIgcuY6kJhYJnjiQ7UEsewaqc25MkuImDabhzWnnad+3zRnJGLpveaMeEZ69uqUsTbKC+fR7g==} - '@folo-services/shared@0.0.25': - resolution: {integrity: sha512-U8dk5N+ajDvZlh0J9mChVyLrBDpDYJ4h0u0CLqC+JKIhLbXIy/P2q2ZtWtVyiMVCppfNDqHE82bLCl8INdb7lw==} + '@folo-services/shared@0.0.26': + resolution: {integrity: sha512-svM4YizoOpMfc7KAUCMLWIhzWuEmN3dn6cEdK1W1I04206Fo2iwVbQV+fP/saXQmHVSBD3zjww0PR/DLypw6PA==} '@fontsource/sn-pro@5.2.5': resolution: {integrity: sha512-rBdBv/0ygj6bkO7xDMMFpwobLdSrcQ2Jncb6DIwdeYGoAgeWkQRwVYhGDKasfLjEYCNYxrqr6wsXsM9+aU39RA==} @@ -4277,6 +4285,19 @@ packages: '@hexagon/base64@1.1.28': resolution: {integrity: sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==} + '@hono/zod-openapi@1.1.2': + resolution: {integrity: sha512-XqfK6cdEMo7Nz7N4rYgfndvQDPGf1T4TzZnG2dEqx5aIiSN4MNIC/73xy1KhUo7pGSKlf9fKL9VOtOkRUNHu6w==} + engines: {node: '>=16.0.0'} + peerDependencies: + hono: '>=4.3.6' + zod: ^4.0.0 + + '@hono/zod-validator@0.7.3': + resolution: {integrity: sha512-uYGdgVib3RlGD698WR5dVM0zB3UuPY5vHKXffGUbUh7r4xY+mFIhF3/v4AcQVLrU5CQdBso8BJr4wuVoCrjTuQ==} + peerDependencies: + hono: '>=3.9.0' + zod: ^3.25.0 || ^4.0.0 + '@hookform/resolvers@5.2.2': resolution: {integrity: sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==} peerDependencies: @@ -4753,6 +4774,9 @@ packages: '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} + '@napi-rs/wasm-runtime@1.0.5': + resolution: {integrity: sha512-TBr9Cf9onSAS2LQ2+QHx6XcC6h9+RIzJgbqG3++9TUZSH204AwEy5jg3BTQ0VATsyoGj4ee49tN/y6rvaOOtcg==} + '@noble/ciphers@2.0.0': resolution: {integrity: sha512-j/l6jpnpaIBM87cAYPJzi/6TgqmBv9spkqPyCXvRYsu5uxqh6tPJZDnD85yo8VWqzTuTQPgfv7NgT63u7kbwAQ==} engines: {node: '>= 20.19.0'} @@ -5106,12 +5130,8 @@ packages: peerDependencies: '@opentelemetry/api': ^1.1.0 - '@oxc-project/runtime@0.71.0': - resolution: {integrity: sha512-QwoF5WUXIGFQ+hSxWEib4U/aeLoiDN9JlP18MnBgx9LLPRDfn1iICtcow7Jgey6HLH4XFceWXQD5WBJ39dyJcw==} - engines: {node: '>=6.9.0'} - - '@oxc-project/types@0.71.0': - resolution: {integrity: sha512-5CwQ4MI+P4MQbjLWXgNurA+igGwu/opNetIE13LBs9+V93R64MLvDKOOLZIXSzEfovU3Zef3q3GjPnMTgJTn2w==} + '@oxc-project/types@0.92.0': + resolution: {integrity: sha512-PDLfCbwgXjGdTBxzcuDOUxJYNBl6P8dOp3eDKWw54dYvqONan9rwGDRQU0zrkdEMiItfXQQUOI17uOcMX5Zm7A==} '@peculiar/asn1-android@2.5.0': resolution: {integrity: sha512-t8A83hgghWQkcneRsgGs2ebAlRe54ns88p7ouv8PW2tzF1nAW4yHcL4uZKrFpIU+uszIRzTkcCuie37gpkId0A==} @@ -6069,75 +6089,98 @@ packages: resolution: {integrity: sha512-xBaJish5OeGmniDj9cW5PRa/PtmuVU3ziqrbr5xJj901ZDN4TosrVaNZpEiLZAxdfnhAe7uQ7QFWfjPe9d9K2Q==} engines: {node: '>= 10'} - '@rolldown/binding-darwin-arm64@1.0.0-beta.9-commit.d91dfb5': - resolution: {integrity: sha512-Mp0/gqiPdepHjjVm7e0yL1acWvI0rJVVFQEADSezvAjon9sjQ7CEg9JnXICD4B1YrPmN9qV/e7cQZCp87tTV4w==} + '@rolldown/binding-android-arm64@1.0.0-beta.40': + resolution: {integrity: sha512-9Ii9phC7QU6Lb+ncMfG1Xlosq0NBB1N/4sw+EGZ3y0BBWGy02TOb5ghWZalphAKv9rn1goqo5WkBjyd2YvsLmA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.0-beta.40': + resolution: {integrity: sha512-5O6d0y2tBQTL+ecQY3qXIwSnF1/Zik8q7LZMKeyF+VJ9l194d0IdMhl2zUF0cqWbYHuF4Pnxplk4OhurPQ/Z9Q==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.0.0-beta.9-commit.d91dfb5': - resolution: {integrity: sha512-40re4rMNrsi57oavRzIOpRGmg3QRlW6Ea8Q3znaqgOuJuKVrrm2bIQInTfkZJG7a4/5YMX7T951d0+toGLTdCA==} + '@rolldown/binding-darwin-x64@1.0.0-beta.40': + resolution: {integrity: sha512-izB9jygt3miPQbOTZfSu5K51isUplqa8ysByOKQqcJHgrBWmbTU8TM9eouv6tRmBR0kjcEcID9xhmA1CeZ1VIg==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.0.0-beta.9-commit.d91dfb5': - resolution: {integrity: sha512-8BDM939bbMariZupiHp3OmP5N+LXPT4mULA0hZjDaq970PCxv4krZOSMG+HkWUUwmuQROtV+/00xw39EO0P+8g==} + '@rolldown/binding-freebsd-x64@1.0.0-beta.40': + resolution: {integrity: sha512-2fdpEpKT+wwP0vig9dqxu+toTeWmVSjo3psJQVDeLJ51rO+GXcCJ1IkCXjhMKVEevNtZS7B8T8Z2vvmRV9MAdA==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.9-commit.d91dfb5': - resolution: {integrity: sha512-sntsPaPgrECpBB/+2xrQzVUt0r493TMPI+4kWRMhvMsmrxOqH1Ep5lM0Wua/ZdbfZNwm1aVa5pcESQfNfM4Fhw==} + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.40': + resolution: {integrity: sha512-HP2lo78OWULN+8TewpLbS9PS00jh0CaF04tA2u8z2I+6QgVgrYOYKvX+T0hlO5smgso4+qb3YchzumWJl3yCPQ==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.9-commit.d91dfb5': - resolution: {integrity: sha512-5clBW/I+er9F2uM1OFjJFWX86y7Lcy0M+NqsN4s3o07W+8467Zk8oQa4B45vdaXoNUF/yqIAgKkA/OEdQDxZqA==} + '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.40': + resolution: {integrity: sha512-ng00gfr9BhA2NPAOU5RWAlTiL+JcwAD+L+4yUD1sbBy6tgHdLiNBOvKtHISIF9RM9/eQeS0tAiWOYZGIH9JMew==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-arm64-musl@1.0.0-beta.9-commit.d91dfb5': - resolution: {integrity: sha512-wv+rnAfQDk9p/CheX8/Kmqk2o1WaFa4xhWI9gOyDMk/ljvOX0u0ubeM8nI1Qfox7Tnh71eV5AjzSePXUhFOyOg==} + '@rolldown/binding-linux-arm64-musl@1.0.0-beta.40': + resolution: {integrity: sha512-mF0R1l9kLcaag/9cLEiYYdNZ4v1uuX4jklSDZ1s6vJE4RB3LirUney0FavdVRwCJ5sDvfvsPgXgtBXWYr2M2tQ==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@rolldown/binding-linux-x64-gnu@1.0.0-beta.9-commit.d91dfb5': - resolution: {integrity: sha512-gxD0/xhU4Py47IH3bKZbWtvB99tMkUPGPJFRfSc5UB9Osoje0l0j1PPbxpUtXIELurYCqwLBKXIMTQGifox1BQ==} + '@rolldown/binding-linux-x64-gnu@1.0.0-beta.40': + resolution: {integrity: sha512-+wi08S7wT5iLPHRZb0USrS6n+T6m+yY++dePYedE5uvKIpWCJJioFTaRtWjpm0V6dVNLcq2OukrvfdlGtH9Wgg==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-musl@1.0.0-beta.9-commit.d91dfb5': - resolution: {integrity: sha512-HotuVe3XUjDwqqEMbm3o3IRkP9gdm8raY/btd/6KE3JGLF/cv4+3ff1l6nOhAZI8wulWDPEXPtE7v+HQEaTXnA==} + '@rolldown/binding-linux-x64-musl@1.0.0-beta.40': + resolution: {integrity: sha512-W5qBGAemUocIBKCcOsDjlV9GUt28qhl/+M6etWBeLS5gQK0J6XDg0YVzfOQdvq57ZGjYNP0NvhYzqhOOnEx+4g==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@rolldown/binding-wasm32-wasi@1.0.0-beta.9-commit.d91dfb5': - resolution: {integrity: sha512-8Cx+ucbd8n2dIr21FqBh6rUvTVL0uTgEtKR7l+MUZ5BgY4dFh1e4mPVX8oqmoYwOxBiXrsD2JIOCz4AyKLKxWA==} - engines: {node: '>=14.21.3'} + '@rolldown/binding-openharmony-arm64@1.0.0-beta.40': + resolution: {integrity: sha512-vJwoDehtt+yqj2zacq1AqNc2uE/oh7mnRGqAUbuldV6pgvU01OSQUJ7Zu+35hTopnjFoDNN6mIezkYlGAv5RFA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.0.0-beta.40': + resolution: {integrity: sha512-Oj3YyqVUPurr1FlMpEE/bJmMC+VWAWPM/SGUfklO5KUX97bk5Q/733nPg4RykK8q8/TluJoQYvRc05vL/B74dw==} + engines: {node: '>=14.0.0'} cpu: [wasm32] - '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.9-commit.d91dfb5': - resolution: {integrity: sha512-Vhq5vikrVDxAa75fxsyqj0c0Y/uti/TwshXI71Xb8IeUQJOBnmLUsn5dgYf5ljpYYkNa0z9BPAvUDIDMmyDi+w==} + '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.40': + resolution: {integrity: sha512-0ZtO6yN8XjVoFfN4HDWQj4nDu3ndMybr7jIM00DJqOmc+yFhly7rdOy7fNR9Sky3leCpBtsXfepVqRmVpYKPVA==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.9-commit.d91dfb5': - resolution: {integrity: sha512-lN7RIg9Iugn08zP2aZN9y/MIdG8iOOCE93M1UrFlrxMTqPf8X+fDzmR/OKhTSd1A2pYNipZHjyTcb5H8kyQSow==} + '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.40': + resolution: {integrity: sha512-BPl1inoJXPpIe38Ja46E4y11vXlJyuleo+9Rmu//pYL5fIDYJkXUj/oAXqjSuwLcssrcwnuPgzvzvlz9++cr3w==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.0.0-beta.9-commit.d91dfb5': - resolution: {integrity: sha512-7/7cLIn48Y+EpQ4CePvf8reFl63F15yPUlg4ZAhl+RXJIfydkdak1WD8Ir3AwAO+bJBXzrfNL+XQbxm0mcQZmw==} + '@rolldown/binding-win32-x64-msvc@1.0.0-beta.40': + resolution: {integrity: sha512-UguA4ltbAk+nbwHRxqaUP/etpTbR0HjyNlsu4Zjbh/ytNbFsbw8CA4tEBkwDyjgI5NIPea6xY11zpl7R2/ddVA==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] '@rolldown/pluginutils@1.0.0-beta.35': resolution: {integrity: sha512-slYrCpoxJUqzFDDNlvrOYRazQUNRvWPjXA17dAOISY3rDMxX6k8K4cj2H+hEYMHF81HO3uNd5rHVigAWRM5dSg==} - '@rolldown/pluginutils@1.0.0-beta.9-commit.d91dfb5': - resolution: {integrity: sha512-8sExkWRK+zVybw3+2/kBkYBFeLnEUWz1fT7BLHplpzmtqkOfTbAQ9gkt4pzwGIIZmg4Qn5US5ACjUBenrhezwQ==} + '@rolldown/pluginutils@1.0.0-beta.40': + resolution: {integrity: sha512-s3GeJKSQOwBlzdUrj4ISjJj5SfSh+aqn0wjOar4Bx95iV1ETI7F6S/5hLcfAxZ9kXDcyrAkxPlqmd1ZITttf+w==} '@rollup/plugin-babel@5.3.1': resolution: {integrity: sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==} @@ -7824,35 +7867,6 @@ packages: before-after-hook@2.2.3: resolution: {integrity: sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==} - better-auth@1.3.12: - resolution: {integrity: sha512-FckxiAexCkkk2F0EOPmhXjWhYYE8eYg2x68lOIirSgyQ0TWc4JDvA5y8Vax5Jc7iyXk5MjJBY3DfwTPDZ87Lbg==} - peerDependencies: - '@lynx-js/react': '*' - '@sveltejs/kit': ^2.0.0 - next: ^14.0.0 || ^15.0.0 - react: 19.0.0 - react-dom: 19.0.0 - solid-js: ^1.0.0 - svelte: ^4.0.0 || ^5.0.0 - vue: ^3.0.0 - peerDependenciesMeta: - '@lynx-js/react': - optional: true - '@sveltejs/kit': - optional: true - next: - optional: true - react: - optional: true - react-dom: - optional: true - solid-js: - optional: true - svelte: - optional: true - vue: - optional: true - better-auth@1.3.13: resolution: {integrity: sha512-vNIVExB87JTM19+1+p6SZ6xbzZFLw4ug8ir61qCArE8UbIN3SZQi0T3/PjubpWPwsLjINBsVizeHxPf5S7Ss0A==} peerDependencies: @@ -9125,6 +9139,12 @@ packages: drizzle-orm: '>=0.36.0' zod: '>=3.0.0' + drizzle-zod@0.8.3: + resolution: {integrity: sha512-66yVOuvGhKJnTdiqj1/Xaaz9/qzOdRJADpDa68enqS6g3t0kpNkwNYjUuaeXgZfO/UWuIM9HIhSlJ6C5ZraMww==} + peerDependencies: + drizzle-orm: '>=0.36.0' + zod: ^3.25.0 || ^4.0.0 + ds-store@0.1.6: resolution: {integrity: sha512-kY21M6Lz+76OS3bnCzjdsJSF7LBpLYGCVfavW8TgQD2XkcqIZ86W0y9qUDZu6fp7SIZzqosMDW2zi7zVFfv4hw==} @@ -10851,6 +10871,10 @@ packages: hoist-non-react-statics@3.3.2: resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + hono@4.9.8: + resolution: {integrity: sha512-JW8Bb4RFWD9iOKxg5PbUarBYGM99IcxFl2FPBo2gSJO11jjUDqlP1Bmfyqt8Z/dGhIQ63PMA9LdcLefXyIasyg==} + engines: {node: '>=16.9.0'} + hookable@5.5.3: resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} @@ -12728,6 +12752,9 @@ packages: resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} engines: {node: '>=12'} + openapi3-ts@4.5.0: + resolution: {integrity: sha512-jaL+HgTq2Gj5jRcfdutgRGLosCy/hT8sQf6VOy+P+g36cZOjI1iukdPnijC+4CmeRzg/jEllJUboEic2FhxhtQ==} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -14382,8 +14409,9 @@ packages: vue-tsc: optional: true - rolldown@1.0.0-beta.9-commit.d91dfb5: - resolution: {integrity: sha512-FHkj6gGEiEgmAXQchglofvUUdwj2Oiw603Rs+zgFAnn9Cb7T7z3fiaEc0DbN3ja4wYkW6sF2rzMEtC1V4BGx/g==} + rolldown@1.0.0-beta.40: + resolution: {integrity: sha512-VqEHbKpOgTPmQrZ4fVn4eshDQS/6g/fRpNE7cFSJY+eQLDZn4B9X61J6L+hnlt1u2uRI+pF7r1USs6S5fuWCvw==} + engines: {node: ^20.19.0 || >=22.12.0} hasBin: true rollup@2.79.2: @@ -16405,6 +16433,11 @@ snapshots: '@ark/util@0.46.0': optional: true + '@asteasolutions/zod-to-openapi@8.1.0(zod@4.1.11)': + dependencies: + openapi3-ts: 4.5.0 + zod: 4.1.11 + '@babel/code-frame@7.10.4': dependencies: '@babel/highlight': 7.25.9 @@ -20090,14 +20123,14 @@ snapshots: '@floating-ui/utils@0.2.10': {} - '@follow-app/client-sdk@0.3.66(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.7)(pg@8.16.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@follow-app/client-sdk@0.3.67(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7)(pg@8.16.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: - '@folo-services/constants': 0.1.33 - '@folo-services/drizzle': 0.1.27(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.7) + '@folo-services/constants': 0.1.34 + '@folo-services/drizzle': 0.1.28(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7) '@folo-services/exceptions': 0.1.16 - '@folo-services/shared': 0.0.25(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.7)(pg@8.16.3) - better-auth: 1.3.12(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - zod: 3.25.76 + '@folo-services/shared': 0.0.26(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7)(pg@8.16.3) + better-auth: 1.3.13(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + zod: 4.1.11 transitivePeerDependencies: - '@aws-sdk/client-rds-data' - '@cloudflare/workers-types' @@ -20122,6 +20155,7 @@ snapshots: - bun-types - expo-sqlite - gel + - hono - knex - kysely - mysql2 @@ -20138,14 +20172,14 @@ snapshots: - svelte - vue - '@follow-app/client-sdk@0.3.66(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0)(utf-8-validate@6.0.5))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(kysely@0.28.7)(pg@8.16.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@follow-app/client-sdk@0.3.67(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0)(utf-8-validate@6.0.5))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7)(pg@8.16.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: - '@folo-services/constants': 0.1.33 - '@folo-services/drizzle': 0.1.27(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0)(utf-8-validate@6.0.5))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(kysely@0.28.7) + '@folo-services/constants': 0.1.34 + '@folo-services/drizzle': 0.1.28(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0)(utf-8-validate@6.0.5))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7) '@folo-services/exceptions': 0.1.16 - '@folo-services/shared': 0.0.25(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0)(utf-8-validate@6.0.5))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(kysely@0.28.7)(pg@8.16.3) - better-auth: 1.3.12(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - zod: 3.25.76 + '@folo-services/shared': 0.0.26(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0)(utf-8-validate@6.0.5))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7)(pg@8.16.3) + better-auth: 1.3.13(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + zod: 4.1.11 transitivePeerDependencies: - '@aws-sdk/client-rds-data' - '@cloudflare/workers-types' @@ -20170,6 +20204,7 @@ snapshots: - bun-types - expo-sqlite - gel + - hono - knex - kysely - mysql2 @@ -20186,14 +20221,14 @@ snapshots: - svelte - vue - '@follow-app/client-sdk@0.3.66(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.7)(pg@8.16.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@follow-app/client-sdk@0.3.67(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7)(pg@8.16.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: - '@folo-services/constants': 0.1.33 - '@folo-services/drizzle': 0.1.27(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.7) + '@folo-services/constants': 0.1.34 + '@folo-services/drizzle': 0.1.28(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7) '@folo-services/exceptions': 0.1.16 - '@folo-services/shared': 0.0.25(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.7)(pg@8.16.3) - better-auth: 1.3.12(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - zod: 3.25.76 + '@folo-services/shared': 0.0.26(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7)(pg@8.16.3) + better-auth: 1.3.13(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + zod: 4.1.11 transitivePeerDependencies: - '@aws-sdk/client-rds-data' - '@cloudflare/workers-types' @@ -20218,6 +20253,7 @@ snapshots: - bun-types - expo-sqlite - gel + - hono - knex - kysely - mysql2 @@ -20273,9 +20309,9 @@ snapshots: - sql.js - sqlite3 - '@folo-services/constants@0.1.33': + '@folo-services/constants@0.1.34': dependencies: - zod: 3.25.76 + zod: 4.1.11 '@folo-services/drizzle@0.1.27(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.7)': dependencies: @@ -20316,45 +20352,6 @@ snapshots: - sql.js - sqlite3 - '@folo-services/drizzle@0.1.27(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0)(utf-8-validate@6.0.5))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(kysely@0.28.7)': - dependencies: - '@folo-services/exceptions': 0.1.16 - drizzle-orm: 0.44.5(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0)(utf-8-validate@6.0.5))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(kysely@0.28.7)(pg@8.16.3) - drizzle-zod: 0.7.1(drizzle-orm@0.44.5(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0)(utf-8-validate@6.0.5))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(kysely@0.28.7)(pg@8.16.3))(zod@3.25.76) - nanoid: 5.1.5 - pg: 8.16.3 - zod: 3.25.76 - transitivePeerDependencies: - - '@aws-sdk/client-rds-data' - - '@cloudflare/workers-types' - - '@electric-sql/pglite' - - '@libsql/client' - - '@libsql/client-wasm' - - '@neondatabase/serverless' - - '@op-engineering/op-sqlite' - - '@opentelemetry/api' - - '@planetscale/database' - - '@prisma/client' - - '@tidbcloud/serverless' - - '@types/better-sqlite3' - - '@types/pg' - - '@types/sql.js' - - '@upstash/redis' - - '@vercel/postgres' - - '@xata.io/client' - - better-sqlite3 - - bun-types - - expo-sqlite - - gel - - knex - - kysely - - mysql2 - - pg-native - - postgres - - prisma - - sql.js - - sqlite3 - '@folo-services/drizzle@0.1.27(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.7)': dependencies: '@folo-services/exceptions': 0.1.16 @@ -20394,14 +20391,138 @@ snapshots: - sql.js - sqlite3 + '@folo-services/drizzle@0.1.28(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7)': + dependencies: + '@folo-services/exceptions': 0.1.16 + '@hono/zod-openapi': 1.1.2(hono@4.9.8)(zod@4.1.11) + drizzle-orm: 0.44.5(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.7)(pg@8.16.3) + drizzle-zod: 0.8.3(drizzle-orm@0.44.5(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.7)(pg@8.16.3))(zod@4.1.11) + nanoid: 5.1.5 + pg: 8.16.3 + zod: 4.1.11 + transitivePeerDependencies: + - '@aws-sdk/client-rds-data' + - '@cloudflare/workers-types' + - '@electric-sql/pglite' + - '@libsql/client' + - '@libsql/client-wasm' + - '@neondatabase/serverless' + - '@op-engineering/op-sqlite' + - '@opentelemetry/api' + - '@planetscale/database' + - '@prisma/client' + - '@tidbcloud/serverless' + - '@types/better-sqlite3' + - '@types/pg' + - '@types/sql.js' + - '@upstash/redis' + - '@vercel/postgres' + - '@xata.io/client' + - better-sqlite3 + - bun-types + - expo-sqlite + - gel + - hono + - knex + - kysely + - mysql2 + - pg-native + - postgres + - prisma + - sql.js + - sqlite3 + + '@folo-services/drizzle@0.1.28(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0)(utf-8-validate@6.0.5))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7)': + dependencies: + '@folo-services/exceptions': 0.1.16 + '@hono/zod-openapi': 1.1.2(hono@4.9.8)(zod@4.1.11) + drizzle-orm: 0.44.5(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0)(utf-8-validate@6.0.5))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(kysely@0.28.7)(pg@8.16.3) + drizzle-zod: 0.8.3(drizzle-orm@0.44.5(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0)(utf-8-validate@6.0.5))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(kysely@0.28.7)(pg@8.16.3))(zod@4.1.11) + nanoid: 5.1.5 + pg: 8.16.3 + zod: 4.1.11 + transitivePeerDependencies: + - '@aws-sdk/client-rds-data' + - '@cloudflare/workers-types' + - '@electric-sql/pglite' + - '@libsql/client' + - '@libsql/client-wasm' + - '@neondatabase/serverless' + - '@op-engineering/op-sqlite' + - '@opentelemetry/api' + - '@planetscale/database' + - '@prisma/client' + - '@tidbcloud/serverless' + - '@types/better-sqlite3' + - '@types/pg' + - '@types/sql.js' + - '@upstash/redis' + - '@vercel/postgres' + - '@xata.io/client' + - better-sqlite3 + - bun-types + - expo-sqlite + - gel + - hono + - knex + - kysely + - mysql2 + - pg-native + - postgres + - prisma + - sql.js + - sqlite3 + + '@folo-services/drizzle@0.1.28(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7)': + dependencies: + '@folo-services/exceptions': 0.1.16 + '@hono/zod-openapi': 1.1.2(hono@4.9.8)(zod@4.1.11) + drizzle-orm: 0.44.5(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.7)(pg@8.16.3) + drizzle-zod: 0.8.3(drizzle-orm@0.44.5(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.7)(pg@8.16.3))(zod@4.1.11) + nanoid: 5.1.5 + pg: 8.16.3 + zod: 4.1.11 + transitivePeerDependencies: + - '@aws-sdk/client-rds-data' + - '@cloudflare/workers-types' + - '@electric-sql/pglite' + - '@libsql/client' + - '@libsql/client-wasm' + - '@neondatabase/serverless' + - '@op-engineering/op-sqlite' + - '@opentelemetry/api' + - '@planetscale/database' + - '@prisma/client' + - '@tidbcloud/serverless' + - '@types/better-sqlite3' + - '@types/pg' + - '@types/sql.js' + - '@upstash/redis' + - '@vercel/postgres' + - '@xata.io/client' + - better-sqlite3 + - bun-types + - expo-sqlite + - gel + - hono + - knex + - kysely + - mysql2 + - pg-native + - postgres + - prisma + - sql.js + - sqlite3 + '@folo-services/exceptions@0.1.16': {} - '@folo-services/shared@0.0.25(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.7)(pg@8.16.3)': + '@folo-services/shared@0.0.26(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7)(pg@8.16.3)': dependencies: - '@folo-services/drizzle': 0.1.27(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.7) + '@folo-services/drizzle': 0.1.28(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7) + '@hono/zod-openapi': 1.1.2(hono@4.9.8)(zod@4.1.11) drizzle-orm: 0.44.5(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.7)(pg@8.16.3) - drizzle-zod: 0.7.1(drizzle-orm@0.44.5(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.7)(pg@8.16.3))(zod@3.25.76) - zod: 3.25.76 + drizzle-zod: 0.8.3(drizzle-orm@0.44.5(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.7)(pg@8.16.3))(zod@4.1.11) + zod: 4.1.11 transitivePeerDependencies: - '@aws-sdk/client-rds-data' - '@cloudflare/workers-types' @@ -20424,6 +20545,7 @@ snapshots: - bun-types - expo-sqlite - gel + - hono - knex - kysely - mysql2 @@ -20434,12 +20556,13 @@ snapshots: - sql.js - sqlite3 - '@folo-services/shared@0.0.25(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0)(utf-8-validate@6.0.5))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(kysely@0.28.7)(pg@8.16.3)': + '@folo-services/shared@0.0.26(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0)(utf-8-validate@6.0.5))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7)(pg@8.16.3)': dependencies: - '@folo-services/drizzle': 0.1.27(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0)(utf-8-validate@6.0.5))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(kysely@0.28.7) + '@folo-services/drizzle': 0.1.28(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0)(utf-8-validate@6.0.5))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7) + '@hono/zod-openapi': 1.1.2(hono@4.9.8)(zod@4.1.11) drizzle-orm: 0.44.5(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0)(utf-8-validate@6.0.5))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(kysely@0.28.7)(pg@8.16.3) - drizzle-zod: 0.7.1(drizzle-orm@0.44.5(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0)(utf-8-validate@6.0.5))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(kysely@0.28.7)(pg@8.16.3))(zod@3.25.76) - zod: 3.25.76 + drizzle-zod: 0.8.3(drizzle-orm@0.44.5(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0)(utf-8-validate@6.0.5))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(kysely@0.28.7)(pg@8.16.3))(zod@4.1.11) + zod: 4.1.11 transitivePeerDependencies: - '@aws-sdk/client-rds-data' - '@cloudflare/workers-types' @@ -20462,6 +20585,7 @@ snapshots: - bun-types - expo-sqlite - gel + - hono - knex - kysely - mysql2 @@ -20472,12 +20596,13 @@ snapshots: - sql.js - sqlite3 - '@folo-services/shared@0.0.25(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.7)(pg@8.16.3)': + '@folo-services/shared@0.0.26(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7)(pg@8.16.3)': dependencies: - '@folo-services/drizzle': 0.1.27(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.7) + '@folo-services/drizzle': 0.1.28(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7) + '@hono/zod-openapi': 1.1.2(hono@4.9.8)(zod@4.1.11) drizzle-orm: 0.44.5(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.7)(pg@8.16.3) - drizzle-zod: 0.7.1(drizzle-orm@0.44.5(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.7)(pg@8.16.3))(zod@3.25.76) - zod: 3.25.76 + drizzle-zod: 0.8.3(drizzle-orm@0.44.5(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.7)(pg@8.16.3))(zod@4.1.11) + zod: 4.1.11 transitivePeerDependencies: - '@aws-sdk/client-rds-data' - '@cloudflare/workers-types' @@ -20500,6 +20625,7 @@ snapshots: - bun-types - expo-sqlite - gel + - hono - knex - kysely - mysql2 @@ -20561,6 +20687,19 @@ snapshots: '@hexagon/base64@1.1.28': {} + '@hono/zod-openapi@1.1.2(hono@4.9.8)(zod@4.1.11)': + dependencies: + '@asteasolutions/zod-to-openapi': 8.1.0(zod@4.1.11) + '@hono/zod-validator': 0.7.3(hono@4.9.8)(zod@4.1.11) + hono: 4.9.8 + openapi3-ts: 4.5.0 + zod: 4.1.11 + + '@hono/zod-validator@0.7.3(hono@4.9.8)(zod@4.1.11)': + dependencies: + hono: 4.9.8 + zod: 4.1.11 + '@hookform/resolvers@5.2.2(react-hook-form@7.62.0(react@19.0.0))': dependencies: '@standard-schema/utils': 0.3.0 @@ -21218,6 +21357,13 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true + '@napi-rs/wasm-runtime@1.0.5': + dependencies: + '@emnapi/core': 1.5.0 + '@emnapi/runtime': 1.5.0 + '@tybys/wasm-util': 0.10.1 + optional: true + '@noble/ciphers@2.0.0': {} '@noble/hashes@2.0.0': {} @@ -21702,9 +21848,7 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) - '@oxc-project/runtime@0.71.0': {} - - '@oxc-project/types@0.71.0': {} + '@oxc-project/types@0.92.0': {} '@peculiar/asn1-android@2.5.0': dependencies: @@ -22783,7 +22927,7 @@ snapshots: debug: 2.6.9 invariant: 2.2.4 metro: 0.82.4(bufferutil@4.0.9)(utf-8-validate@6.0.5) - metro-config: 0.82.4(bufferutil@4.0.9)(utf-8-validate@6.0.5) + metro-config: 0.82.4(bufferutil@4.0.9) metro-core: 0.82.4 semver: 7.7.2 transitivePeerDependencies: @@ -22941,47 +23085,53 @@ snapshots: '@resvg/resvg-js-win32-ia32-msvc': 2.6.2 '@resvg/resvg-js-win32-x64-msvc': 2.6.2 - '@rolldown/binding-darwin-arm64@1.0.0-beta.9-commit.d91dfb5': + '@rolldown/binding-android-arm64@1.0.0-beta.40': optional: true - '@rolldown/binding-darwin-x64@1.0.0-beta.9-commit.d91dfb5': + '@rolldown/binding-darwin-arm64@1.0.0-beta.40': optional: true - '@rolldown/binding-freebsd-x64@1.0.0-beta.9-commit.d91dfb5': + '@rolldown/binding-darwin-x64@1.0.0-beta.40': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.9-commit.d91dfb5': + '@rolldown/binding-freebsd-x64@1.0.0-beta.40': optional: true - '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.9-commit.d91dfb5': + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.40': optional: true - '@rolldown/binding-linux-arm64-musl@1.0.0-beta.9-commit.d91dfb5': + '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.40': optional: true - '@rolldown/binding-linux-x64-gnu@1.0.0-beta.9-commit.d91dfb5': + '@rolldown/binding-linux-arm64-musl@1.0.0-beta.40': optional: true - '@rolldown/binding-linux-x64-musl@1.0.0-beta.9-commit.d91dfb5': + '@rolldown/binding-linux-x64-gnu@1.0.0-beta.40': optional: true - '@rolldown/binding-wasm32-wasi@1.0.0-beta.9-commit.d91dfb5': + '@rolldown/binding-linux-x64-musl@1.0.0-beta.40': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.0-beta.40': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.0-beta.40': dependencies: - '@napi-rs/wasm-runtime': 0.2.12 + '@napi-rs/wasm-runtime': 1.0.5 optional: true - '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.9-commit.d91dfb5': + '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.40': optional: true - '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.9-commit.d91dfb5': + '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.40': optional: true - '@rolldown/binding-win32-x64-msvc@1.0.0-beta.9-commit.d91dfb5': + '@rolldown/binding-win32-x64-msvc@1.0.0-beta.40': optional: true '@rolldown/pluginutils@1.0.0-beta.35': {} - '@rolldown/pluginutils@1.0.0-beta.9-commit.d91dfb5': {} + '@rolldown/pluginutils@1.0.0-beta.40': {} '@rollup/plugin-babel@5.3.1(@babel/core@7.28.4)(@types/babel__core@7.20.5)(rollup@2.79.2)': dependencies: @@ -25083,24 +25233,6 @@ snapshots: before-after-hook@2.2.3: {} - better-auth@1.3.12(react-dom@19.0.0(react@19.0.0))(react@19.0.0): - dependencies: - '@better-auth/utils': 0.3.0 - '@better-fetch/fetch': 1.1.18 - '@noble/ciphers': 2.0.0 - '@noble/hashes': 2.0.0 - '@simplewebauthn/browser': 13.2.0 - '@simplewebauthn/server': 13.2.1 - better-call: 1.0.19 - defu: 6.1.4 - jose: 6.1.0 - kysely: 0.28.7 - nanostores: 1.0.1 - zod: 4.1.11 - optionalDependencies: - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) - better-auth@1.3.13(react-dom@19.0.0(react@19.0.0))(react@19.0.0): dependencies: '@better-auth/utils': 0.3.0 @@ -26402,16 +26534,26 @@ snapshots: drizzle-orm: 0.44.5(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.7)(pg@8.16.3) zod: 3.25.76 - drizzle-zod@0.7.1(drizzle-orm@0.44.5(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0)(utf-8-validate@6.0.5))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(kysely@0.28.7)(pg@8.16.3))(zod@3.25.76): - dependencies: - drizzle-orm: 0.44.5(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0)(utf-8-validate@6.0.5))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(kysely@0.28.7)(pg@8.16.3) - zod: 3.25.76 - drizzle-zod@0.7.1(drizzle-orm@0.44.5(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.7)(pg@8.16.3))(zod@3.25.76): dependencies: drizzle-orm: 0.44.5(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.7)(pg@8.16.3) zod: 3.25.76 + drizzle-zod@0.8.3(drizzle-orm@0.44.5(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.7)(pg@8.16.3))(zod@4.1.11): + dependencies: + drizzle-orm: 0.44.5(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.7)(pg@8.16.3) + zod: 4.1.11 + + drizzle-zod@0.8.3(drizzle-orm@0.44.5(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0)(utf-8-validate@6.0.5))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(kysely@0.28.7)(pg@8.16.3))(zod@4.1.11): + dependencies: + drizzle-orm: 0.44.5(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0)(utf-8-validate@6.0.5))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(kysely@0.28.7)(pg@8.16.3) + zod: 4.1.11 + + drizzle-zod@0.8.3(drizzle-orm@0.44.5(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.7)(pg@8.16.3))(zod@4.1.11): + dependencies: + drizzle-orm: 0.44.5(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.7)(pg@8.16.3) + zod: 4.1.11 + ds-store@0.1.6: dependencies: bplist-creator: 0.0.8 @@ -29093,6 +29235,8 @@ snapshots: dependencies: react-is: 16.13.1 + hono@4.9.8: {} + hookable@5.5.3: {} hosted-git-info@2.8.9: {} @@ -30506,22 +30650,6 @@ snapshots: - supports-color - utf-8-validate - metro-config@0.82.4(bufferutil@4.0.9)(utf-8-validate@6.0.5): - dependencies: - connect: 3.7.0 - cosmiconfig: 5.2.1 - flow-enums-runtime: 0.0.6 - jest-validate: 29.7.0 - metro: 0.82.4(bufferutil@4.0.9)(utf-8-validate@6.0.5) - metro-cache: 0.82.4 - metro-core: 0.82.4 - metro-runtime: 0.82.4 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - optional: true - metro-core@0.82.4: dependencies: flow-enums-runtime: 0.0.6 @@ -30707,7 +30835,7 @@ snapshots: metro-babel-transformer: 0.82.4 metro-cache: 0.82.4 metro-cache-key: 0.82.4 - metro-config: 0.82.4(bufferutil@4.0.9)(utf-8-validate@6.0.5) + metro-config: 0.82.4(bufferutil@4.0.9) metro-core: 0.82.4 metro-file-map: 0.82.4 metro-resolver: 0.82.4 @@ -31381,6 +31509,10 @@ snapshots: is-docker: 2.2.1 is-wsl: 2.2.0 + openapi3-ts@4.5.0: + dependencies: + yaml: 2.8.1 + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -33161,7 +33293,7 @@ snapshots: sprintf-js: 1.1.3 optional: true - rolldown-plugin-dts@0.16.7(rolldown@1.0.0-beta.9-commit.d91dfb5)(typescript@5.9.2): + rolldown-plugin-dts@0.16.7(rolldown@1.0.0-beta.40)(typescript@5.9.2): dependencies: '@babel/generator': 7.28.3 '@babel/parser': 7.28.4 @@ -33172,32 +33304,33 @@ snapshots: dts-resolver: 2.1.2 get-tsconfig: 4.10.1 magic-string: 0.30.19 - rolldown: 1.0.0-beta.9-commit.d91dfb5 + rolldown: 1.0.0-beta.40 optionalDependencies: typescript: 5.9.2 transitivePeerDependencies: - oxc-resolver - supports-color - rolldown@1.0.0-beta.9-commit.d91dfb5: + rolldown@1.0.0-beta.40: dependencies: - '@oxc-project/runtime': 0.71.0 - '@oxc-project/types': 0.71.0 - '@rolldown/pluginutils': 1.0.0-beta.9-commit.d91dfb5 + '@oxc-project/types': 0.92.0 + '@rolldown/pluginutils': 1.0.0-beta.40 ansis: 4.1.0 optionalDependencies: - '@rolldown/binding-darwin-arm64': 1.0.0-beta.9-commit.d91dfb5 - '@rolldown/binding-darwin-x64': 1.0.0-beta.9-commit.d91dfb5 - '@rolldown/binding-freebsd-x64': 1.0.0-beta.9-commit.d91dfb5 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-beta.9-commit.d91dfb5 - '@rolldown/binding-linux-arm64-gnu': 1.0.0-beta.9-commit.d91dfb5 - '@rolldown/binding-linux-arm64-musl': 1.0.0-beta.9-commit.d91dfb5 - '@rolldown/binding-linux-x64-gnu': 1.0.0-beta.9-commit.d91dfb5 - '@rolldown/binding-linux-x64-musl': 1.0.0-beta.9-commit.d91dfb5 - '@rolldown/binding-wasm32-wasi': 1.0.0-beta.9-commit.d91dfb5 - '@rolldown/binding-win32-arm64-msvc': 1.0.0-beta.9-commit.d91dfb5 - '@rolldown/binding-win32-ia32-msvc': 1.0.0-beta.9-commit.d91dfb5 - '@rolldown/binding-win32-x64-msvc': 1.0.0-beta.9-commit.d91dfb5 + '@rolldown/binding-android-arm64': 1.0.0-beta.40 + '@rolldown/binding-darwin-arm64': 1.0.0-beta.40 + '@rolldown/binding-darwin-x64': 1.0.0-beta.40 + '@rolldown/binding-freebsd-x64': 1.0.0-beta.40 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-beta.40 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-beta.40 + '@rolldown/binding-linux-arm64-musl': 1.0.0-beta.40 + '@rolldown/binding-linux-x64-gnu': 1.0.0-beta.40 + '@rolldown/binding-linux-x64-musl': 1.0.0-beta.40 + '@rolldown/binding-openharmony-arm64': 1.0.0-beta.40 + '@rolldown/binding-wasm32-wasi': 1.0.0-beta.40 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-beta.40 + '@rolldown/binding-win32-ia32-msvc': 1.0.0-beta.40 + '@rolldown/binding-win32-x64-msvc': 1.0.0-beta.40 rollup@2.79.2: optionalDependencies: @@ -34282,8 +34415,8 @@ snapshots: diff: 8.0.2 empathic: 2.0.0 hookable: 5.5.3 - rolldown: 1.0.0-beta.9-commit.d91dfb5 - rolldown-plugin-dts: 0.16.7(rolldown@1.0.0-beta.9-commit.d91dfb5)(typescript@5.9.2) + rolldown: 1.0.0-beta.40 + rolldown-plugin-dts: 0.16.7(rolldown@1.0.0-beta.40)(typescript@5.9.2) semver: 7.7.2 tinyexec: 1.0.1 tinyglobby: 0.2.15 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 11e1528c9..352858bda 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -8,7 +8,7 @@ packages: - "!**/example/**" catalog: - "@follow-app/client-sdk": 0.3.66 + "@follow-app/client-sdk": 0.3.67 tailwindcss-uikit-colors: 1.0.0 typescript: 5.9.2 @@ -17,7 +17,8 @@ ignorePatchFailures: false minimumReleaseAge: 1440 minimumReleaseAgeExclude: - - "@follow-app/client-sdk" + - "@follow-app/*" + - "@folo-services/*" onlyBuiltDependencies: - "@firebase/util" From 090ae03b428993a7fe6801eb3ed8b85858327f2e Mon Sep 17 00:00:00 2001 From: lawvs <18554747+lawvs@users.noreply.github.com> Date: Fri, 26 Sep 2025 15:18:22 +0800 Subject: [PATCH 006/601] fix: correct types in getDefaultFormData function --- .../renderer/src/modules/ai-task/components/ai-task-modal.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/desktop/layer/renderer/src/modules/ai-task/components/ai-task-modal.tsx b/apps/desktop/layer/renderer/src/modules/ai-task/components/ai-task-modal.tsx index 668ad95fe..c482a836d 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-task/components/ai-task-modal.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-task/components/ai-task-modal.tsx @@ -103,8 +103,7 @@ const getDefaultFormData = (task?: AITask, prompt?: string): TaskFormData => { name: task.name, prompt: task.prompt, schedule: formSchedule, - // @ts-expect-error -- Type fixed, remove this comment in next release - options: task.options ?? { notifyChannels: ["email"] }, + options: { notifyChannels: ["email"], ...task.options }, } } From 1a91d57cf019b2297d3eebbe6d3b0463debdcf47 Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Fri, 26 Sep 2025 16:05:07 +0800 Subject: [PATCH 007/601] refactor: merge main view and feed in data block --- .../components/layouts/AIChatContextBar.tsx | 39 +-------- .../components/message/AIDataBlockItem.tsx | 80 ++++++++++++++++--- .../components/message/AIDataBlockPart.tsx | 23 ++++-- .../modules/ai-chat/hooks/useDisplayBlocks.ts | 52 ++++++++++++ 4 files changed, 142 insertions(+), 52 deletions(-) create mode 100644 apps/desktop/layer/renderer/src/modules/ai-chat/hooks/useDisplayBlocks.ts diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/AIChatContextBar.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/AIChatContextBar.tsx index 2b73110dc..0a39e39e2 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/AIChatContextBar.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/AIChatContextBar.tsx @@ -4,9 +4,9 @@ import { memo, useCallback, useEffect, useMemo, useRef } from "react" import { useAISettingValue } from "~/atoms/settings/ai" import { DropdownMenu, DropdownMenuTrigger } from "~/components/ui/dropdown-menu/dropdown-menu" import { useRouteParamsSelector } from "~/hooks/biz/useRouteParams" +import { useDisplayBlocks } from "~/modules/ai-chat/hooks/useDisplayBlocks" import { useFileUploadWithDefaults } from "~/modules/ai-chat/hooks/useFileUpload" import { useAIChatStore } from "~/modules/ai-chat/store/AIChatContext" -import type { AIChatContextBlock, ValueContextBlock } from "~/modules/ai-chat/store/types" import { SUPPORTED_MIME_ACCEPT } from "~/modules/ai-chat/utils/file-validation" import { useBlockActions } from "../../store/hooks" @@ -14,10 +14,6 @@ import { BlockSliceAction } from "../../store/slices/block.slice" import { ContextBlock, MainViewFeedContextBlock } from "../context-bar/blocks" import { ContextMenuContent, ShortcutsMenuContent } from "../context-bar/menus" -type ValueBlockOf = Omit & { - type: Type -} - export const AIChatContextBar: Component<{ onSendShortcut?: (prompt: string) => void }> = memo( ({ className, onSendShortcut }) => { const blocks = useAIChatStore()((s) => s.blocks) @@ -75,38 +71,7 @@ export const AIChatContextBar: Component<{ onSendShortcut?: (prompt: string) => } }, [addOrUpdateBlock, feedId, removeBlock]) - const displayBlocks = useMemo(() => { - const mainViewBlock = blocks.find( - (block): block is ValueBlockOf<"mainView"> => block.type === "mainView", - ) - const mainFeedBlock = blocks.find( - (block): block is ValueBlockOf<"mainFeed"> => block.type === "mainFeed", - ) - - if (mainViewBlock && mainFeedBlock) { - const items: ( - | { - kind: "combined" - viewBlock: ValueBlockOf<"mainView"> - feedBlock: ValueBlockOf<"mainFeed"> - } - | { kind: "single"; block: AIChatContextBlock } - )[] = [] - - items.push({ kind: "combined", viewBlock: mainViewBlock, feedBlock: mainFeedBlock }) - - const otherBlocks = blocks.filter( - (block) => block.id !== mainViewBlock.id && block.id !== mainFeedBlock.id, - ) - otherBlocks.forEach((block) => { - items.push({ kind: "single", block }) - }) - - return items - } - - return blocks.map((block) => ({ kind: "single" as const, block })) - }, [blocks]) + const displayBlocks = useDisplayBlocks(blocks) return (
    diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIDataBlockItem.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIDataBlockItem.tsx index 1317c47fd..6ed7f9476 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIDataBlockItem.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIDataBlockItem.tsx @@ -3,7 +3,7 @@ import { cn } from "@follow/utils/utils" import { t } from "i18next" import * as React from "react" -import type { AIChatContextBlock } from "~/modules/ai-chat/store/types" +import type { AIChatContextBlock, ValueContextBlock } from "~/modules/ai-chat/store/types" import { getBlockIcon, @@ -20,6 +20,15 @@ interface AIDataBlockItemProps { index: number } +type ValueBlockOf = Omit & { + type: Type +} + +interface MainViewFeedDataBlockItemProps { + viewBlock: ValueBlockOf<"mainView"> + feedBlock: ValueBlockOf<"mainFeed"> +} + /** * Gets the display content for a context block */ @@ -87,13 +96,15 @@ const BlockIcon: React.FC<{ BlockIcon.displayName = "BlockIcon" /** - * Individual block item component with optimized rendering and animations + * Shared block container component to ensure consistency */ -export const AIDataBlockItem: React.FC = React.memo(({ block }) => { - const styles = React.useMemo(() => getBlockStyles(block.type), [block.type]) - const label = React.useMemo(() => getBlockLabel(block.type), [block.type]) - const displayContent = React.useMemo(() => getDisplayContent(block), [block]) - +const BlockContainer: React.FC<{ + styles: ReturnType + block: AIChatContextBlock + label?: string + displayContent: React.ReactNode + title?: string +}> = React.memo(({ styles, block, label, displayContent, title }) => { return (
    = React.memo(({ blo {/* Label and content */}
    - {label} - ยท + {label && ( + <> + {label} + ยท + + )} {displayContent} @@ -120,4 +135,49 @@ export const AIDataBlockItem: React.FC = React.memo(({ blo ) }) +BlockContainer.displayName = "BlockContainer" + +/** + * Individual block item component with optimized rendering and animations + */ +export const AIDataBlockItem: React.FC = React.memo(({ block }) => { + const styles = React.useMemo(() => getBlockStyles(block.type), [block.type]) + const label = React.useMemo(() => getBlockLabel(block.type), [block.type]) + const displayContent = React.useMemo(() => getDisplayContent(block), [block]) + + return ( + + ) +}) + AIDataBlockItem.displayName = "AIDataBlockItem" + +/** + * Combined block item component for main view and main feed + * Displays view icon with feed title content + */ +export const MainViewFeedDataBlockItem: React.FC = React.memo( + ({ viewBlock, feedBlock }) => { + const { styles, displayContent, title } = React.useMemo(() => { + const view = views.find((v) => v.view === Number(viewBlock.value)) + const viewName = view?.name ? t(view.name, { ns: "common" }) : viewBlock.value + + return { + styles: getBlockStyles("mainView"), + displayContent: , + title: `${viewName} - ${feedBlock.value}`, + } + }, [viewBlock.value, feedBlock.value]) + + return ( + + ) + }, +) + +MainViewFeedDataBlockItem.displayName = "MainViewFeedDataBlockItem" diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIDataBlockPart.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIDataBlockPart.tsx index a9710040f..1e1db7522 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIDataBlockPart.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIDataBlockPart.tsx @@ -1,9 +1,10 @@ import { cn } from "@follow/utils/utils" import * as React from "react" +import { useDisplayBlocks } from "~/modules/ai-chat/hooks/useDisplayBlocks" import type { AIChatContextBlock } from "~/modules/ai-chat/store/types" -import { AIDataBlockItem } from "./AIDataBlockItem" +import { AIDataBlockItem, MainViewFeedDataBlockItem } from "./AIDataBlockItem" interface AIDataBlockPartProps { blocks: AIChatContextBlock[] @@ -14,8 +15,10 @@ interface AIDataBlockPartProps { * Displays various types of context (entries, feeds, text, files) with compact styling */ export const AIDataBlockPart: React.FC = React.memo(({ blocks }) => { + const displayBlocks = useDisplayBlocks(blocks) + // Early return for empty blocks - if (!blocks?.length) { + if (displayBlocks.length === 0) { return null } @@ -34,9 +37,19 @@ export const AIDataBlockPart: React.FC = React.memo(({ blo
    {/* Render individual block items */} - {blocks.map((block, index) => ( - - ))} + {displayBlocks.map((item, index) => { + if (item.kind === "combined") { + return ( + + ) + } + + return + })}
    ) diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/hooks/useDisplayBlocks.ts b/apps/desktop/layer/renderer/src/modules/ai-chat/hooks/useDisplayBlocks.ts new file mode 100644 index 000000000..fe8fb20a8 --- /dev/null +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/hooks/useDisplayBlocks.ts @@ -0,0 +1,52 @@ +import { useMemo } from "react" + +import type { AIChatContextBlock, ValueContextBlock } from "~/modules/ai-chat/store/types" + +type ValueBlockOf = Omit & { + type: Type +} + +export type DisplayBlockItem = + | { + kind: "combined" + viewBlock: ValueBlockOf<"mainView"> + feedBlock: ValueBlockOf<"mainFeed"> + } + | { kind: "single"; block: AIChatContextBlock } + +/** + * Custom hook to process blocks and merge mainView + mainFeed when both exist + * Returns an array of display items that can be either combined or single blocks + */ +export const useDisplayBlocks = (blocks: AIChatContextBlock[]): DisplayBlockItem[] => { + return useMemo(() => { + // Early return for empty blocks + if (!blocks?.length) { + return [] + } + + const mainViewBlock = blocks.find( + (block): block is ValueBlockOf<"mainView"> => block.type === "mainView", + ) + const mainFeedBlock = blocks.find( + (block): block is ValueBlockOf<"mainFeed"> => block.type === "mainFeed", + ) + + if (mainViewBlock && mainFeedBlock) { + const items: DisplayBlockItem[] = [] + + items.push({ kind: "combined", viewBlock: mainViewBlock, feedBlock: mainFeedBlock }) + + const otherBlocks = blocks.filter( + (block) => block.id !== mainViewBlock.id && block.id !== mainFeedBlock.id, + ) + otherBlocks.forEach((block) => { + items.push({ kind: "single", block }) + }) + + return items + } + + return blocks.map((block) => ({ kind: "single" as const, block })) + }, [blocks]) +} From fa7a6e18d804dc2d058a799b46b53f44556fa5ce Mon Sep 17 00:00:00 2001 From: lawvs <18554747+lawvs@users.noreply.github.com> Date: Fri, 26 Sep 2025 16:29:02 +0800 Subject: [PATCH 008/601] feat(i18n): add translations for task management features - Implemented internationalization for task scheduling, actions, and modal titles. - Updated existing components to utilize translation functions for better localization support. --- .../modules/ai-task/components/task-item.tsx | 80 +++++++++++-------- .../modules/ai-task/components/task-list.tsx | 8 +- .../renderer/src/modules/settings/tabs/ai.tsx | 2 +- .../tabs/ai/tasks/TaskSchedulingSection.tsx | 16 ++-- locales/ai/en.json | 36 +++++++++ locales/ai/ja.json | 36 +++++++++ locales/ai/zh-CN.json | 36 +++++++++ 7 files changed, 167 insertions(+), 47 deletions(-) diff --git a/apps/desktop/layer/renderer/src/modules/ai-task/components/task-item.tsx b/apps/desktop/layer/renderer/src/modules/ai-task/components/task-item.tsx index 0feb0ece6..ac23436ef 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-task/components/task-item.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-task/components/task-item.tsx @@ -1,6 +1,7 @@ import { cn } from "@follow/utils/utils" import type { AITask, TaskSchedule } from "@follow-app/client-sdk" import dayjs from "dayjs" +import type { TFunction } from "i18next" import { memo, useCallback, useMemo, useState } from "react" import { useTranslation } from "react-i18next" import { toast } from "sonner" @@ -20,29 +21,42 @@ import { } from "../query" import { AITaskModal } from "./ai-task-modal" -const formatScheduleText = (schedule: TaskSchedule) => { - if (!schedule) return "Unknown schedule" - +const formatScheduleText = (schedule: TaskSchedule, t: TFunction<"ai", undefined>) => { + if (!schedule) return t("tasks.schedule.unknown") switch (schedule.type) { case "once": { const date = dayjs(schedule.date) - return `Once on ${date.format("MMM D, YYYY")} at ${date.format("h:mm A")}` + return t("tasks.schedule.once", { + date: date.format("MMM D, YYYY"), + time: date.format("h:mm A"), + }) } case "daily": { const time = dayjs(schedule.timeOfDay) - return `Daily at ${time.format("h:mm A")}` + return t("tasks.schedule.daily", { time: time.format("h:mm A") }) } case "weekly": { - const days = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"] + const days = [ + t("days.sunday", { ns: "common" }), + t("days.monday", { ns: "common" }), + t("days.tuesday", { ns: "common" }), + t("days.wednesday", { ns: "common" }), + t("days.thursday", { ns: "common" }), + t("days.friday", { ns: "common" }), + t("days.saturday", { ns: "common" }), + ] const time = dayjs(schedule.timeOfDay) - return `Weekly on ${days[schedule.dayOfWeek]} at ${time.format("h:mm A")}` + return t("tasks.schedule.weekly", { + day: days[schedule.dayOfWeek], + time: time.format("h:mm A"), + }) } case "monthly": { const time = dayjs(schedule.timeOfDay) - return `Monthly on day ${schedule.dayOfMonth} at ${time.format("h:mm A")}` + return t("tasks.schedule.monthly", { day: schedule.dayOfMonth, time: time.format("h:mm A") }) } default: { - return "Unknown schedule" + return t("tasks.schedule.unknown") } } } @@ -86,7 +100,7 @@ export const TaskItem = memo(({ task }: { task: AITask }) => { const updateTaskMutation = useUpdateAITaskMutation() const testRunMutation = useTestRunAITaskMutation() const { ask } = useDialog() - const { t } = useTranslation() + const { t } = useTranslation("ai") const sessions = useAIChatSessionListQuery() // const chatActions = useChatActions() const [openingReport, setOpeningReport] = useState(false) @@ -100,7 +114,7 @@ export const TaskItem = memo(({ task }: { task: AITask }) => { const handleEditTask = (task: AITask) => { present({ - title: "Edit AI Task", + title: t("tasks.modal.edit_title"), content: () => , }) } @@ -109,7 +123,7 @@ export const TaskItem = memo(({ task }: { task: AITask }) => { const handleOpenReport = useCallback(async () => { if (!taskSession) { - toast.error("No report session found for this task yet.") + toast.error(t("tasks.toast.no_report")) return } setOpeningReport(true) @@ -117,7 +131,7 @@ export const TaskItem = memo(({ task }: { task: AITask }) => { await AIChatSessionService.fetchAndPersistMessages(taskSession) } catch (e) { console.error("Failed to sync chat session messages:", e) - toast.error("Failed to load chat messages") + toast.error(t("tasks.toast.load_failed")) } setAIPanelVisibility(true) const chatActions = ChatSliceActions.getActiveInstance() @@ -126,8 +140,8 @@ export const TaskItem = memo(({ task }: { task: AITask }) => { } chatActions?.switchToChat(taskSession.chatId) setOpeningReport(false) - toast("Switch to the chat panel to view reports.") - }, [taskSession]) + toast(t("tasks.toast.switch_to_chat")) + }, [taskSession, t]) const actions: ActionButton[] = [ // Only show if the task has at least one run @@ -136,7 +150,7 @@ export const TaskItem = memo(({ task }: { task: AITask }) => { { icon: "i-mgc-history-cute-re" as const, onClick: handleOpenReport, - title: "View reports", + title: t("tasks.actions.view_reports"), loading: openingReport, disabled: openingReport, } satisfies ActionButton, @@ -146,34 +160,32 @@ export const TaskItem = memo(({ task }: { task: AITask }) => { icon: "i-mgc-test-tube-cute-re" as const, onClick: async () => { try { - const loadingId = toast.loading( - "Starting test runโ€ฆ This may take about 1 minute. You can view the result in the Chat panel.", - ) + const loadingId = toast.loading(t("tasks.toast.test_start")) await testRunMutation.mutateAsync({ id: task.id }) - toast.success("Test run finished. You can view the report in the Chat panel.", { + toast.success(t("tasks.toast.test_success"), { id: loadingId, }) } catch (error) { console.error("Failed to run test:", error) - toast.error("Failed to run test. Please try again.") + toast.error(t("tasks.toast.test_failed")) } }, - title: "Test run", + title: t("tasks.actions.test_run"), disabled: testRunMutation.isPending, loading: testRunMutation.isPending, }, { icon: "i-mgc-edit-cute-re", onClick: () => handleEditTask(task), - title: "Edit task", + title: t("tasks.actions.edit_task"), }, { icon: "i-mgc-delete-2-cute-re", onClick: async () => { const confirmed = await ask({ - title: "Delete Task", + title: t("tasks.modal.delete_title"), // translation fallback pattern; primary key then default string - message: `Are you sure you want to delete the task "${task.name}"?`, + message: t("tasks.modal.delete_confirm", { name: task.name }), confirmText: t("words.delete", { ns: "common" }), cancelText: t("words.cancel", { ns: "common" }), variant: "danger", @@ -181,13 +193,13 @@ export const TaskItem = memo(({ task }: { task: AITask }) => { if (!confirmed) return try { await deleteTaskMutation.mutateAsync({ id: task.id }) - toast.success("Task deleted successfully") + toast.success(t("tasks.toast.delete_success")) } catch (error) { console.error("Failed to delete task:", error) - toast.error("Failed to delete task. Please try again.") + toast.error(t("tasks.toast.delete_failed")) } }, - title: "Delete task", + title: t("tasks.actions.delete_task"), disabled: isDeleting, loading: isDeleting, }, @@ -210,20 +222,20 @@ export const TaskItem = memo(({ task }: { task: AITask }) => { )} {status === "paused" && } - {status.charAt(0).toUpperCase() + status.slice(1)} + {t(`tasks.status.${status}`)}

    - Schedule:{" "} - {formatScheduleText(task.schedule)} + {t("tasks.fields.schedule")}{" "} + {formatScheduleText(task.schedule, t)}

    - Prompt: {task.prompt} + {t("tasks.fields.prompt")} {task.prompt}

    {task.createdAt && (

    - Created:{" "} + {t("tasks.fields.created")}{" "} {dayjs(task.createdAt).format("MMM D, YYYY h:mm A")}

    )} @@ -238,7 +250,7 @@ export const TaskItem = memo(({ task }: { task: AITask }) => { await updateTaskMutation.mutateAsync({ id: task.id, isEnabled: !task.isEnabled }) } catch (error) { console.error("Failed to toggle task:", error) - toast.error("Failed to update task. Please try again.") + toast.error(t("tasks.toast.update_failed")) } }} /> diff --git a/apps/desktop/layer/renderer/src/modules/ai-task/components/task-list.tsx b/apps/desktop/layer/renderer/src/modules/ai-task/components/task-list.tsx index d1e6b8537..0cd1c9336 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-task/components/task-list.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-task/components/task-list.tsx @@ -1,5 +1,6 @@ import { cn } from "@follow/utils/utils" import { memo } from "react" +import { useTranslation } from "react-i18next" import { useAITaskListQuery } from "../query" import { TaskItem } from "./task-item" @@ -10,6 +11,7 @@ interface TaskListProps { export const AITaskList = memo(({ className }) => { const tasks = useAITaskListQuery() + const { t } = useTranslation("ai") if (tasks === undefined) { return null @@ -21,10 +23,8 @@ export const AITaskList = memo(({ className }) => {
    -

    No scheduled tasks

    -

    - Create your first AI task to automate your workflows. -

    +

    {t("tasks.empty.title")}

    +

    {t("tasks.empty.desc")}

    ) } diff --git a/apps/desktop/layer/renderer/src/modules/settings/tabs/ai.tsx b/apps/desktop/layer/renderer/src/modules/settings/tabs/ai.tsx index 6e63bdf72..5ce84d086 100644 --- a/apps/desktop/layer/renderer/src/modules/settings/tabs/ai.tsx +++ b/apps/desktop/layer/renderer/src/modules/settings/tabs/ai.tsx @@ -60,7 +60,7 @@ export const SettingAI = () => { , { type: "title", - value: "Task Scheduling", + value: t("tasks.section.title"), }, TaskSchedulingSection, AISecurityDisclosureSection, diff --git a/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/tasks/TaskSchedulingSection.tsx b/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/tasks/TaskSchedulingSection.tsx index aad5c4bf5..a2e778db2 100644 --- a/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/tasks/TaskSchedulingSection.tsx +++ b/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/tasks/TaskSchedulingSection.tsx @@ -1,6 +1,7 @@ import { Button } from "@follow/components/ui/button/index.js" import { Label } from "@follow/components/ui/label/index.js" import { useCallback } from "react" +import { useTranslation } from "react-i18next" import { useModalStack } from "~/components/ui/modal/stacked/hooks" import { AITaskList, AITaskModal, useCanCreateNewAITask } from "~/modules/ai-task" @@ -8,25 +9,24 @@ import { AITaskList, AITaskModal, useCanCreateNewAITask } from "~/modules/ai-tas export const TaskSchedulingSection = () => { const { present } = useModalStack() const canCreateNewTask = useCanCreateNewAITask() + const { t } = useTranslation("ai") const handleCreateTask = useCallback(() => { present({ - title: "New AI Task", + title: t("tasks.modal.new_title"), content: () => , }) - }, [present]) + }, [present, t]) return (
    - +
    - {"Create and manage automated AI tasks that run on your schedule."} + {t("tasks.manage.desc")} {!canCreateNewTask && ( - - {" (Limit reached: maximum number of tasks reached)"} - + {t("tasks.manage.limit_reached")} )}
    @@ -38,7 +38,7 @@ export const TaskSchedulingSection = () => { onClick={handleCreateTask} > - New Task + {t("tasks.actions.new_task")}
    diff --git a/locales/ai/en.json b/locales/ai/en.json index 9a6080362..4b82274db 100644 --- a/locales/ai/en.json +++ b/locales/ai/en.json @@ -181,10 +181,27 @@ "shortcuts.updated": "Shortcut updated successfully", "shortcuts.validation.required": "Name and prompt are required", "summary_not_available": "Summary not available", + "tasks.actions.delete_task": "Delete task", + "tasks.actions.edit_task": "Edit task", + "tasks.actions.new_task": "New Task", "tasks.actions.schedule": "Schedule Task", "tasks.actions.scheduling": "Scheduling...", + "tasks.actions.test_run": "Test run", "tasks.actions.update": "Update Task", "tasks.actions.updating": "Updating...", + "tasks.actions.view_reports": "View reports", + "tasks.empty.desc": "Create your first AI task to automate your workflows.", + "tasks.empty.title": "No scheduled tasks", + "tasks.fields.created": "Created:", + "tasks.fields.prompt": "Prompt:", + "tasks.fields.schedule": "Schedule:", + "tasks.manage.desc": "Create and manage automated AI tasks that run on your schedule.", + "tasks.manage.limit_reached": "(Limit reached: maximum number of tasks reached)", + "tasks.manage.title": "Schedule AI Tasks", + "tasks.modal.delete_confirm": "Are you sure you want to delete the task \"{{name}}\"?", + "tasks.modal.delete_title": "Delete Task", + "tasks.modal.edit_title": "Edit AI Task", + "tasks.modal.new_title": "New AI Task", "tasks.name": "Task Name", "tasks.name_placeholder": "Enter a descriptive name for your task...", "tasks.notify.coming_soon": "Coming soon", @@ -193,13 +210,32 @@ "tasks.prompt": "Prompt", "tasks.prompt_helper": "Provide clear, specific instructions for the AI to execute", "tasks.prompt_placeholder": "Describe what you want the AI to do when this task runs...", + "tasks.schedule.daily": "Daily at {{time}}", + "tasks.schedule.monthly": "Monthly on day {{day}} at {{time}}", + "tasks.schedule.once": "Once on {{date}} at {{time}}", + "tasks.schedule.unknown": "Unknown schedule", + "tasks.schedule.weekly": "Weekly on {{day}} at {{time}}", "tasks.section.info": "Task Information", "tasks.section.instructions": "AI Instructions", "tasks.section.notifications": "Notifications", "tasks.section.schedule": "Schedule Configuration", + "tasks.section.title": "AI Task", + "tasks.status.completed": "Completed", + "tasks.status.paused": "Paused", + "tasks.status.scheduled": "Scheduled", + "tasks.status.unknown": "Unknown", "tasks.toast.create_error": "Failed to schedule AI task. Please try again.", "tasks.toast.created": "AI task scheduled successfully", + "tasks.toast.delete_failed": "Failed to delete task. Please try again.", + "tasks.toast.delete_success": "Task deleted successfully", + "tasks.toast.load_failed": "Failed to load chat messages", + "tasks.toast.no_report": "No report session found for this task yet.", + "tasks.toast.switch_to_chat": "Switch to the chat panel to view reports.", + "tasks.toast.test_failed": "Failed to run test. Please try again.", + "tasks.toast.test_start": "Starting test runโ€ฆ This may take about 1 minute. You can view the result in the Chat panel.", + "tasks.toast.test_success": "Test run finished. You can view the report in the Chat panel.", "tasks.toast.update_error": "Failed to update AI task. Please try again.", + "tasks.toast.update_failed": "Failed to update task. Please try again.", "tasks.toast.updated": "AI task updated successfully", "tasks.validation.date_future": "Scheduled date must be in the future", "tasks.validation.prompt_max": "Prompt must be less than 2000 characters", diff --git a/locales/ai/ja.json b/locales/ai/ja.json index 90beb531e..18433d268 100644 --- a/locales/ai/ja.json +++ b/locales/ai/ja.json @@ -147,10 +147,27 @@ "shortcuts.updated": "ใ‚ทใƒงใƒผใƒˆใ‚ซใƒƒใƒˆใŒๆญฃๅธธใซๆ›ดๆ–ฐใ•ใ‚Œใพใ—ใŸ", "shortcuts.validation.required": "ๅๅ‰ใจใƒ—ใƒญใƒณใƒ—ใƒˆใฏๅฟ…้ ˆใงใ™", "summary_not_available": "่ฆ็ด„ใฏๅˆฉ็”จใงใใพใ›ใ‚“", + "tasks.actions.delete_task": "ใ‚ฟใ‚นใ‚ฏใ‚’ๅ‰Š้™ค", + "tasks.actions.edit_task": "ใ‚ฟใ‚นใ‚ฏใ‚’็ทจ้›†", + "tasks.actions.new_task": "ๆ–ฐ่ฆใ‚ฟใ‚นใ‚ฏ", "tasks.actions.schedule": "ใ‚ฟใ‚นใ‚ฏใ‚’ใ‚นใ‚ฑใ‚ธใƒฅใƒผใƒซ", "tasks.actions.scheduling": "ใ‚นใ‚ฑใ‚ธใƒฅใƒผใƒซไธญ...", + "tasks.actions.test_run": "ใƒ†ใ‚นใƒˆๅฎŸ่กŒ", "tasks.actions.update": "ใ‚ฟใ‚นใ‚ฏใ‚’ๆ›ดๆ–ฐ", "tasks.actions.updating": "ๆ›ดๆ–ฐไธญ...", + "tasks.actions.view_reports": "ใƒฌใƒใƒผใƒˆใ‚’่ฆ‹ใ‚‹", + "tasks.empty.desc": "ๆœ€ๅˆใฎ AI ใ‚ฟใ‚นใ‚ฏใ‚’ไฝœๆˆใ—ใฆใƒฏใƒผใ‚ฏใƒ•ใƒญใƒผใ‚’่‡ชๅ‹•ๅŒ–ใ—ใพใ—ใ‚‡ใ†ใ€‚", + "tasks.empty.title": "ไบˆๅฎšใ•ใ‚ŒใŸใ‚ฟใ‚นใ‚ฏใฏใ‚ใ‚Šใพใ›ใ‚“", + "tasks.fields.created": "ไฝœๆˆๆ—ฅๆ™‚:", + "tasks.fields.prompt": "ใƒ—ใƒญใƒณใƒ—ใƒˆ:", + "tasks.fields.schedule": "ใ‚นใ‚ฑใ‚ธใƒฅใƒผใƒซ:", + "tasks.manage.desc": "ใ‚นใ‚ฑใ‚ธใƒฅใƒผใƒซใซๅพ“ใฃใฆ่‡ชๅ‹•ๅฎŸ่กŒใ•ใ‚Œใ‚‹ AI ใ‚ฟใ‚นใ‚ฏใ‚’ไฝœๆˆใƒป็ฎก็†ใ—ใพใ™ใ€‚", + "tasks.manage.limit_reached": "(ๆœ€ๅคงใ‚ฟใ‚นใ‚ฏๆ•ฐใซ้”ใ—ใพใ—ใŸ)", + "tasks.manage.title": "AI ใ‚ฟใ‚นใ‚ฏใ‚’ใ‚นใ‚ฑใ‚ธใƒฅใƒผใƒซ", + "tasks.modal.delete_confirm": "ใ‚ฟใ‚นใ‚ฏ \"{{name}}\" ใ‚’ๅ‰Š้™คใ—ใฆใ‚‚ใ‚ˆใ‚ใ—ใ„ใงใ™ใ‹๏ผŸ", + "tasks.modal.delete_title": "ใ‚ฟใ‚นใ‚ฏใ‚’ๅ‰Š้™ค", + "tasks.modal.edit_title": "AI ใ‚ฟใ‚นใ‚ฏใ‚’็ทจ้›†", + "tasks.modal.new_title": "ๆ–ฐ่ฆ AI ใ‚ฟใ‚นใ‚ฏ", "tasks.name": "ใ‚ฟใ‚นใ‚ฏๅ", "tasks.name_placeholder": "ใ‚ฟใ‚นใ‚ฏใฎใ‚ใ‹ใ‚Šใ‚„ใ™ใ„ๅๅ‰ใ‚’ๅ…ฅๅŠ›...", "tasks.notify.coming_soon": "่ฟ‘ๆ—ฅๅ…ฌ้–‹", @@ -159,13 +176,32 @@ "tasks.prompt": "ใƒ—ใƒญใƒณใƒ—ใƒˆ", "tasks.prompt_helper": "AIใŒๅฎŸ่กŒใ™ใ‚‹ใŸใ‚ใฎๆ˜Ž็ขบใงๅ…ทไฝ“็š„ใชๆŒ‡็คบใ‚’ๆไพ›ใ—ใฆใใ ใ•ใ„", "tasks.prompt_placeholder": "ใ“ใฎใ‚ฟใ‚นใ‚ฏใŒๅฎŸ่กŒใ•ใ‚Œใ‚‹ใจใใซAIใซไฝ•ใ‚’ใ•ใ›ใŸใ„ใ‹ใ‚’่จ˜่ฟฐ...", + "tasks.schedule.daily": "ๆฏŽๆ—ฅ {{time}}", + "tasks.schedule.monthly": "ๆฏŽๆœˆ {{day}} ๆ—ฅ {{time}}", + "tasks.schedule.once": "{{date}} {{time}} ๅ˜ๅ›ž", + "tasks.schedule.unknown": "ไธๆ˜Žใชใ‚นใ‚ฑใ‚ธใƒฅใƒผใƒซ", + "tasks.schedule.weekly": "ๆฏŽ้€ฑ{{day}} {{time}}", "tasks.section.info": "ใ‚ฟใ‚นใ‚ฏๆƒ…ๅ ฑ", "tasks.section.instructions": "AI ๆŒ‡็คบ", "tasks.section.notifications": "้€š็Ÿฅ", "tasks.section.schedule": "ใ‚นใ‚ฑใ‚ธใƒฅใƒผใƒซ่จญๅฎš", + "tasks.section.title": "AI ใ‚ฟใ‚นใ‚ฏ", + "tasks.status.completed": "ๅฎŒไบ†", + "tasks.status.paused": "ไธ€ๆ™‚ๅœๆญข", + "tasks.status.scheduled": "ไบˆๅฎšๆธˆใฟ", + "tasks.status.unknown": "ไธๆ˜Ž", "tasks.toast.create_error": "AIใ‚ฟใ‚นใ‚ฏใฎใ‚นใ‚ฑใ‚ธใƒฅใƒผใƒซใซๅคฑๆ•—ใ—ใพใ—ใŸใ€‚ใ‚‚ใ†ไธ€ๅบฆใŠ่ฉฆใ—ใใ ใ•ใ„ใ€‚", "tasks.toast.created": "AIใ‚ฟใ‚นใ‚ฏใŒๆญฃๅธธใซใ‚นใ‚ฑใ‚ธใƒฅใƒผใƒซใ•ใ‚Œใพใ—ใŸ", + "tasks.toast.delete_failed": "ใ‚ฟใ‚นใ‚ฏใฎๅ‰Š้™คใซๅคฑๆ•—ใ—ใพใ—ใŸใ€‚ใ‚‚ใ†ไธ€ๅบฆใŠ่ฉฆใ—ใใ ใ•ใ„ใ€‚", + "tasks.toast.delete_success": "ใ‚ฟใ‚นใ‚ฏใŒๅ‰Š้™คใ•ใ‚Œใพใ—ใŸ", + "tasks.toast.load_failed": "ใƒใƒฃใƒƒใƒˆใƒกใƒƒใ‚ปใƒผใ‚ธใฎ่ชญใฟ่พผใฟใซๅคฑๆ•—ใ—ใพใ—ใŸ", + "tasks.toast.no_report": "ใ“ใฎใ‚ฟใ‚นใ‚ฏใฎใƒฌใƒใƒผใƒˆใ‚ปใƒƒใ‚ทใƒงใƒณใฏใพใ ใ‚ใ‚Šใพใ›ใ‚“ใ€‚", + "tasks.toast.switch_to_chat": "ใƒฌใƒใƒผใƒˆใ‚’่ฆ‹ใ‚‹ใซใฏใƒใƒฃใƒƒใƒˆใƒ‘ใƒใƒซใซๅˆ‡ใ‚Šๆ›ฟใˆใฆใใ ใ•ใ„ใ€‚", + "tasks.toast.test_failed": "ใƒ†ใ‚นใƒˆๅฎŸ่กŒใซๅคฑๆ•—ใ—ใพใ—ใŸใ€‚ใ‚‚ใ†ไธ€ๅบฆใŠ่ฉฆใ—ใใ ใ•ใ„ใ€‚", + "tasks.toast.test_start": "ใƒ†ใ‚นใƒˆๅฎŸ่กŒใ‚’้–‹ๅง‹โ€ฆ็ด„1ๅˆ†ใ‹ใ‹ใ‚‹ๅ ดๅˆใŒใ‚ใ‚Šใพใ™ใ€‚็ตๆžœใฏใƒใƒฃใƒƒใƒˆใƒ‘ใƒใƒซใง็ขบ่ชใงใใพใ™ใ€‚", + "tasks.toast.test_success": "ใƒ†ใ‚นใƒˆๅฎŸ่กŒใŒๅฎŒไบ†ใ—ใพใ—ใŸใ€‚ใƒฌใƒใƒผใƒˆใฏใƒใƒฃใƒƒใƒˆใƒ‘ใƒใƒซใง็ขบ่ชใงใใพใ™ใ€‚", "tasks.toast.update_error": "AIใ‚ฟใ‚นใ‚ฏใฎๆ›ดๆ–ฐใซๅคฑๆ•—ใ—ใพใ—ใŸใ€‚ใ‚‚ใ†ไธ€ๅบฆใŠ่ฉฆใ—ใใ ใ•ใ„ใ€‚", + "tasks.toast.update_failed": "ใ‚ฟใ‚นใ‚ฏใฎๆ›ดๆ–ฐใซๅคฑๆ•—ใ—ใพใ—ใŸใ€‚ใ‚‚ใ†ไธ€ๅบฆใŠ่ฉฆใ—ใใ ใ•ใ„ใ€‚", "tasks.toast.updated": "AIใ‚ฟใ‚นใ‚ฏใŒๆญฃๅธธใซๆ›ดๆ–ฐใ•ใ‚Œใพใ—ใŸ", "tasks.validation.date_future": "ใ‚นใ‚ฑใ‚ธใƒฅใƒผใƒซๆ—ฅๆ™‚ใฏๆœชๆฅใงใ‚ใ‚‹ๅฟ…่ฆใŒใ‚ใ‚Šใพใ™", "tasks.validation.prompt_max": "ใƒ—ใƒญใƒณใƒ—ใƒˆใฏ2000ๆ–‡ๅญ—ๆœชๆบ€ใงใ‚ใ‚‹ๅฟ…่ฆใŒใ‚ใ‚Šใพใ™", diff --git a/locales/ai/zh-CN.json b/locales/ai/zh-CN.json index 1657eb431..a0ca2187b 100644 --- a/locales/ai/zh-CN.json +++ b/locales/ai/zh-CN.json @@ -175,10 +175,27 @@ "shortcuts.updated": "ๅฟซๆทๆ–นๅผๆ›ดๆ–ฐๆˆๅŠŸ", "shortcuts.validation.required": "ๅ็งฐๅ’Œๆ็คบไธบๅฟ…ๅกซ้กน", "summary_not_available": "ๆ‘˜่ฆไธๅฏ็”จ", + "tasks.actions.delete_task": "ๅˆ ้™คไปปๅŠก", + "tasks.actions.edit_task": "็ผ–่พ‘ไปปๅŠก", + "tasks.actions.new_task": "ๆ–ฐๅปบไปปๅŠก", "tasks.actions.schedule": "ๅฎ‰ๆŽ’ไปปๅŠก", "tasks.actions.scheduling": "ๆญฃๅœจๅฎ‰ๆŽ’...", + "tasks.actions.test_run": "ๆต‹่ฏ•่ฟ่กŒ", "tasks.actions.update": "ๆ›ดๆ–ฐไปปๅŠก", "tasks.actions.updating": "ๆญฃๅœจๆ›ดๆ–ฐ...", + "tasks.actions.view_reports": "ๆŸฅ็œ‹ๆŠฅๅ‘Š", + "tasks.empty.desc": "ๅˆ›ๅปบๆ‚จ็š„็ฌฌไธ€ไธช AI ไปปๅŠกไปฅ่‡ชๅŠจๅŒ–ๆ‚จ็š„ๅทฅไฝœๆต็จ‹ใ€‚", + "tasks.empty.title": "ๆš‚ๆ— ่ฎกๅˆ’ไปปๅŠก", + "tasks.fields.created": "ๅˆ›ๅปบๆ—ถ้—ด:", + "tasks.fields.prompt": "ๆŒ‡ไปค:", + "tasks.fields.schedule": "่ฎกๅˆ’:", + "tasks.manage.desc": "ๅˆ›ๅปบๅนถ็ฎก็†ๆŒ‰่ฎกๅˆ’่‡ชๅŠจ่ฟ่กŒ็š„ AI ไปปๅŠกใ€‚", + "tasks.manage.limit_reached": "(ๅทฒ่พพๅˆฐๆœ€ๅคงไปปๅŠกๆ•ฐ้‡้™ๅˆถ)", + "tasks.manage.title": "่ฎกๅˆ’ AI ไปปๅŠก", + "tasks.modal.delete_confirm": "็กฎๅฎš่ฆๅˆ ้™คไปปๅŠก \"{{name}}\" ๅ—๏ผŸ", + "tasks.modal.delete_title": "ๅˆ ้™คไปปๅŠก", + "tasks.modal.edit_title": "็ผ–่พ‘ AI ไปปๅŠก", + "tasks.modal.new_title": "ๆ–ฐๅปบ AI ไปปๅŠก", "tasks.name": "ไปปๅŠกๅ็งฐ", "tasks.name_placeholder": "่พ“ๅ…ฅไธ€ไธชๆ่ฟฐๆ€ง็š„ไปปๅŠกๅ็งฐ...", "tasks.notify.coming_soon": "ๅณๅฐ†ๆŽจๅ‡บ", @@ -187,13 +204,32 @@ "tasks.prompt": "ๆŒ‡ไปค", "tasks.prompt_helper": "่ฏทๆไพ›ๆธ…ๆ™ฐใ€ๅ…ทไฝ“็š„ๆŒ‡ไปคไพ› AI ๆ‰ง่กŒ", "tasks.prompt_placeholder": "ๆ่ฟฐๅฝ“ๆญคไปปๅŠก่ฟ่กŒๆ—ถๆ‚จๅธŒๆœ› AI ๅšไป€ไนˆ...", + "tasks.schedule.daily": "ๆฏๅคฉ {{time}}", + "tasks.schedule.monthly": "ๆฏๆœˆ็ฌฌ {{day}} ๅคฉ {{time}}", + "tasks.schedule.once": "{{date}} {{time}} ๅ•ๆฌก", + "tasks.schedule.unknown": "ๆœช็Ÿฅ่ฎกๅˆ’", + "tasks.schedule.weekly": "ๆฏๅ‘จ{{day}} {{time}}", "tasks.section.info": "ไปปๅŠกไฟกๆฏ", "tasks.section.instructions": "AI ๆŒ‡ไปค", "tasks.section.notifications": "้€š็Ÿฅ", "tasks.section.schedule": "่ฎกๅˆ’้…็ฝฎ", + "tasks.section.title": "AI ไปปๅŠก", + "tasks.status.completed": "ๅทฒๅฎŒๆˆ", + "tasks.status.paused": "ๅทฒๆš‚ๅœ", + "tasks.status.scheduled": "ๅทฒ่ฎกๅˆ’", + "tasks.status.unknown": "ๆœช็Ÿฅ", "tasks.toast.create_error": "ๅฎ‰ๆŽ’ AI ไปปๅŠกๅคฑ่ดฅ๏ผŒ่ฏท้‡่ฏ•ใ€‚", "tasks.toast.created": "AI ไปปๅŠกๅฎ‰ๆŽ’ๆˆๅŠŸ", + "tasks.toast.delete_failed": "ๅˆ ้™คไปปๅŠกๅคฑ่ดฅ๏ผŒ่ฏท้‡่ฏ•ใ€‚", + "tasks.toast.delete_success": "ไปปๅŠกๅˆ ้™คๆˆๅŠŸ", + "tasks.toast.load_failed": "ๅŠ ่ฝฝ่Šๅคฉๆถˆๆฏๅคฑ่ดฅ", + "tasks.toast.no_report": "ๆญคไปปๅŠกๅฐšๆ— ๆŠฅๅ‘Šไผš่ฏใ€‚", + "tasks.toast.switch_to_chat": "ๅˆ‡ๆขๅˆฐ่Šๅคฉ้ขๆฟๆŸฅ็œ‹ๆŠฅๅ‘Šใ€‚", + "tasks.toast.test_failed": "ๆต‹่ฏ•่ฟ่กŒๅคฑ่ดฅ๏ผŒ่ฏท้‡่ฏ•ใ€‚", + "tasks.toast.test_start": "ๅผ€ๅง‹ๆต‹่ฏ•่ฟ่กŒโ€ฆๅคง็บฆ้œ€่ฆ 1 ๅˆ†้’Ÿใ€‚ๆ‚จๅฏไปฅๅœจ่Šๅคฉ้ขๆฟๆŸฅ็œ‹็ป“ๆžœใ€‚", + "tasks.toast.test_success": "ๆต‹่ฏ•่ฟ่กŒๅฎŒๆˆใ€‚ๆ‚จๅฏไปฅๅœจ่Šๅคฉ้ขๆฟๆŸฅ็œ‹ๆŠฅๅ‘Šใ€‚", "tasks.toast.update_error": "ๆ›ดๆ–ฐ AI ไปปๅŠกๅคฑ่ดฅ๏ผŒ่ฏท้‡่ฏ•ใ€‚", + "tasks.toast.update_failed": "ๆ›ดๆ–ฐไปปๅŠกๅคฑ่ดฅ๏ผŒ่ฏท้‡่ฏ•ใ€‚", "tasks.toast.updated": "AI ไปปๅŠกๆ›ดๆ–ฐๆˆๅŠŸ", "tasks.validation.date_future": "่ฎกๅˆ’ๆ—ถ้—ดๅฟ…้กปๆ˜ฏๆœชๆฅ็š„ๆ—ถ้—ด", "tasks.validation.prompt_max": "ๆŒ‡ไปคๅฟ…้กปๅฐ‘ไบŽ 2000 ไธชๅญ—็ฌฆ", From 78816d5b4a53f880f392ba41c0f140e0b17ae595 Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Fri, 26 Sep 2025 16:34:17 +0800 Subject: [PATCH 009/601] chore: fix type --- packages/internal/store/src/modules/feed/store.ts | 4 ++-- packages/internal/store/src/morph/api.ts | 3 ++- packages/internal/utils/src/utils.ts | 8 ++++++++ 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/internal/store/src/modules/feed/store.ts b/packages/internal/store/src/modules/feed/store.ts index 09a74b557..3f501cd35 100644 --- a/packages/internal/store/src/modules/feed/store.ts +++ b/packages/internal/store/src/modules/feed/store.ts @@ -1,6 +1,6 @@ import type { FeedSchema } from "@follow/database/schemas/types" import { FEED_EXTRA_DATA_KEYS, FeedService } from "@follow/database/services/feed" -import { isBizId } from "@follow/utils" +import { getDateISOString, isBizId } from "@follow/utils" import { api } from "../../context" import type { Hydratable, Resetable } from "../../lib/base" @@ -204,7 +204,7 @@ class FeedSyncServices { await feedActions.patch(id, { subscriptionCount: feedAnalytics.subscriptionCount, updatesPerWeek: feedAnalytics.updatesPerWeek, - latestEntryPublishedAt: feedAnalytics.latestEntryPublishedAt, + latestEntryPublishedAt: getDateISOString(feedAnalytics.latestEntryPublishedAt), }) } } diff --git a/packages/internal/store/src/morph/api.ts b/packages/internal/store/src/morph/api.ts index fdea68470..92fed2353 100644 --- a/packages/internal/store/src/morph/api.ts +++ b/packages/internal/store/src/morph/api.ts @@ -1,4 +1,5 @@ import type { FeedSchema, InboxSchema } from "@follow/database/schemas/types" +import { getDateISOString } from "@follow/utils/utils" import type { AddFeedsResponse, AuthUser, @@ -178,7 +179,7 @@ class APIMorph { } if (item.collections) collections.push({ - createdAt: item.collections.createdAt, + createdAt: getDateISOString(item.collections.createdAt), entryId: item.entries.id, feedId: item.feeds.id, view, diff --git a/packages/internal/utils/src/utils.ts b/packages/internal/utils/src/utils.ts index d23f1bdd4..ebd29611f 100644 --- a/packages/internal/utils/src/utils.ts +++ b/packages/internal/utils/src/utils.ts @@ -536,3 +536,11 @@ export const getMobilePlatform = once((): MobilePlatform => { export const isMobileDevice = once((): boolean => { return getMobilePlatform() !== null }) + +export function getDateISOString(dateOrDateString: Date | string | null): string | null { + if (!dateOrDateString) return null + if (typeof dateOrDateString === "string") { + return dateOrDateString + } + return dateOrDateString.toISOString() +} From cf725721a766d6e5079d24681f5a321a4b7cce3b Mon Sep 17 00:00:00 2001 From: lawvs <18554747+lawvs@users.noreply.github.com> Date: Fri, 26 Sep 2025 18:15:22 +0800 Subject: [PATCH 010/601] fix: update day of week i18n for weekly schedule --- .../modules/ai-task/components/task-item.tsx | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/apps/desktop/layer/renderer/src/modules/ai-task/components/task-item.tsx b/apps/desktop/layer/renderer/src/modules/ai-task/components/task-item.tsx index ac23436ef..657f5596a 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-task/components/task-item.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-task/components/task-item.tsx @@ -1,7 +1,7 @@ import { cn } from "@follow/utils/utils" import type { AITask, TaskSchedule } from "@follow-app/client-sdk" import dayjs from "dayjs" -import type { TFunction } from "i18next" +import type { i18n, TFunction } from "i18next" import { memo, useCallback, useMemo, useState } from "react" import { useTranslation } from "react-i18next" import { toast } from "sonner" @@ -21,7 +21,7 @@ import { } from "../query" import { AITaskModal } from "./ai-task-modal" -const formatScheduleText = (schedule: TaskSchedule, t: TFunction<"ai", undefined>) => { +const formatScheduleText = (schedule: TaskSchedule, t: TFunction<"ai", undefined>, i18n: i18n) => { if (!schedule) return t("tasks.schedule.unknown") switch (schedule.type) { case "once": { @@ -36,18 +36,14 @@ const formatScheduleText = (schedule: TaskSchedule, t: TFunction<"ai", undefined return t("tasks.schedule.daily", { time: time.format("h:mm A") }) } case "weekly": { - const days = [ - t("days.sunday", { ns: "common" }), - t("days.monday", { ns: "common" }), - t("days.tuesday", { ns: "common" }), - t("days.wednesday", { ns: "common" }), - t("days.thursday", { ns: "common" }), - t("days.friday", { ns: "common" }), - t("days.saturday", { ns: "common" }), - ] const time = dayjs(schedule.timeOfDay) + const locale = i18n.language + const dow = Math.min(6, Math.max(0, schedule.dayOfWeek)) + const dayName = new Intl.DateTimeFormat(locale, { weekday: "long" }).format( + dayjs().day(dow).toDate(), + ) return t("tasks.schedule.weekly", { - day: days[schedule.dayOfWeek], + day: dayName, time: time.format("h:mm A"), }) } @@ -100,7 +96,7 @@ export const TaskItem = memo(({ task }: { task: AITask }) => { const updateTaskMutation = useUpdateAITaskMutation() const testRunMutation = useTestRunAITaskMutation() const { ask } = useDialog() - const { t } = useTranslation("ai") + const { t, i18n } = useTranslation("ai") const sessions = useAIChatSessionListQuery() // const chatActions = useChatActions() const [openingReport, setOpeningReport] = useState(false) @@ -228,7 +224,7 @@ export const TaskItem = memo(({ task }: { task: AITask }) => {

    {t("tasks.fields.schedule")}{" "} - {formatScheduleText(task.schedule, t)} + {formatScheduleText(task.schedule, t, i18n)}

    {t("tasks.fields.prompt")} {task.prompt} From 81c6de6615d3168c633d9d4616b40e08b5033d9d Mon Sep 17 00:00:00 2001 From: Innei Date: Sat, 27 Sep 2025 23:01:56 +0800 Subject: [PATCH 011/601] refactor: enhance AIChatMessage and AnimatedMarkdown components - Updated AIChatMessage to conditionally render action buttons based on original message metadata. - Refactored AnimatedMarkdown to integrate tooltips for inline references, improving user interaction and accessibility. - Added new tooltip components for better display of entry and feed information. Signed-off-by: Innei --- .../components/message/AIChatMessage.tsx | 45 +++---- .../message/animated/AnimatedMarkdown.tsx | 118 +++++++++++++++--- 2 files changed, 123 insertions(+), 40 deletions(-) diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIChatMessage.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIChatMessage.tsx index bd2359856..d0131c3c0 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIChatMessage.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIChatMessage.tsx @@ -116,29 +116,30 @@ export const AIChatMessage: React.FC = React.memo(

    {/* Action buttons */} -
    - - - {message.metadata && ( - - - - )} -
    + {!!originalMessage.metadata?.finishTime && ( +
    + + {message.metadata && ( + + + + )} +
    + )}
    diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/animated/AnimatedMarkdown.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/animated/AnimatedMarkdown.tsx index 4a60b4446..500a3bf74 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/animated/AnimatedMarkdown.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/animated/AnimatedMarkdown.tsx @@ -3,7 +3,16 @@ */ import type { LinkProps } from "@follow/components/ui/link/LinkWithTooltip.js" -import { isBizId } from "@follow/utils" +import { + Tooltip, + TooltipContent, + TooltipPortal, + TooltipTrigger, +} from "@follow/components/ui/tooltip/index.js" +import { useEntry } from "@follow/store/entry/hooks" +import { useFeedById } from "@follow/store/feed/hooks" +import { feedIconSelector } from "@follow/store/feed/selectors" +import { cn, isBizId } from "@follow/utils" import * as React from "react" import type { Components } from "react-markdown" import ReactMarkdown from "react-markdown" @@ -14,6 +23,7 @@ import { MemoizedShikiCode } from "~/components/ui/code-highlighter" import { MarkdownLink } from "~/components/ui/markdown/renderers" import { useNavigateEntry } from "~/hooks/biz/useNavigateEntry" import { usePeekModal } from "~/hooks/biz/usePeekModal" +import { FeedIcon } from "~/modules/feed/feed-icon" import { ANIMATION_STYLE as ANIMATION_STYLE_DEFAULT } from "./constants" import { TokenizedText } from "./TokenizedText" @@ -180,13 +190,20 @@ export const MarkdownAnimateText: React.FC = ({ ) } -const InlineFoloReference: React.FC<{ +type BaseInlineFoloReferenceProps = { type: "entry" | "feed" - children?: React.ReactNode - className?: string - style?: React.CSSProperties id?: string -}> = ({ type, children, className, style, id }) => { + reason?: string + description?: string + title?: string +} +const InlineFoloReference: React.FC< + BaseInlineFoloReferenceProps & { + children?: React.ReactNode + className?: string + style?: React.CSSProperties + } +> = ({ type, children, className, style, id, title, reason, description }) => { const peekModal = usePeekModal() const navigateEntry = useNavigateEntry() @@ -223,23 +240,88 @@ const InlineFoloReference: React.FC<{ if (!targetId) return null - const baseClassName = - "inline-flex items-center align-middle cursor-pointer text-text-secondary mx-[0.15em] opacity-80 transition-opacity hover:opacity-100 hover:text-text" + if (!title) + return ( + + ) return ( - + + + + + + + + + + ) } +export const InlineTooltipContent: React.FC = ({ + title, + reason, + description, + type, + id, +}) => { + if (!id) return null + return ( +
    +

    + {type === "entry" ? : } + {title} +

    + {reason &&

    {reason}

    } + {description &&

    {description}

    } +
    + ) +} + +const TooltipEntryIcon = ({ entryId }: { entryId: string }) => { + const feedId = useEntry(entryId, (e) => e.feedId) + if (!feedId) return null + return +} + +const TooltipFeedIcon = ({ feedId }: { feedId: string }) => { + const target = useFeedById(feedId, feedIconSelector) + + if (!target) return null + return +} + const RelatedEntryLink = (props: LinkProps) => { const { href, children } = props const entryId = isBizId(href) ? href : null From 4cba9aedc30c0fa2c9b9e4a21f17fe59982a5c20 Mon Sep 17 00:00:00 2001 From: Innei Date: Sat, 27 Sep 2025 23:48:51 +0800 Subject: [PATCH 012/601] fix: keyboard interaction in ChatInterface - Improved keyboard event handling in ChatInterface to prevent focus shift when modifier keys are pressed. - Added functionality to handle incomplete custom mention tags and ` --- .../components/layouts/ChatInterface.tsx | 6 +- .../message/animated/AnimatedMarkdown.tsx | 6 +- .../message/parse-incomplete-markdown.ts | 213 +++++++++++++++++- 3 files changed, 217 insertions(+), 8 deletions(-) diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatInterface.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatInterface.tsx index 29fb2e8dd..dba6d14cd 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatInterface.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatInterface.tsx @@ -57,13 +57,17 @@ const ChatInterfaceContent = ({ centerInputOnEmpty }: ChatInterfaceProps) => { const aiPanelRefs = use(AIPanelRefsContext) - useEventListener("keydown", () => { + useEventListener("keydown", (e) => { if (isFocusWithIn) { const currentActiveElement = document.activeElement if (detectIsEditableElement(currentActiveElement as HTMLElement)) { return } + + if (e.shiftKey || e.metaKey || e.ctrlKey) { + return + } aiPanelRefs.inputRef?.current?.focus() } }) diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/animated/AnimatedMarkdown.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/animated/AnimatedMarkdown.tsx index 500a3bf74..0246c65a2 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/animated/AnimatedMarkdown.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/animated/AnimatedMarkdown.tsx @@ -298,8 +298,8 @@ export const InlineTooltipContent: React.FC = ({ }) => { if (!id) return null return ( -
    -

    +
    +

    {type === "entry" ? : } {title}

    @@ -319,7 +319,7 @@ const TooltipFeedIcon = ({ feedId }: { feedId: string }) => { const target = useFeedById(feedId, feedIconSelector) if (!target) return null - return + return } const RelatedEntryLink = (props: LinkProps) => { diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/parse-incomplete-markdown.ts b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/parse-incomplete-markdown.ts index 58d6212e0..614cd403b 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/parse-incomplete-markdown.ts +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/parse-incomplete-markdown.ts @@ -9,6 +9,123 @@ const singleUnderscorePattern = /(_)([^_]*)$/ const inlineCodePattern = /(`)([^`]*)$/ const strikethroughPattern = /(~~)([^~]*)$/ +// Detect custom inline reference tags +const mentionTagStartPattern = /<\s*mention-(?:entry|feed)\b/gi + +// Finds the end index of a self-closing tag (`/>`) starting at `startIndex`. +// Returns the index of `>` when a `/>` is found outside of quotes; otherwise -1. +const findSelfClosingTagEnd = (text: string, startIndex: number): number => { + // Don't process if inside a complete code block + if (hasCompleteCodeBlock(text)) return -1 + + let inQuote: '"' | "'" | null = null + for (let i = startIndex; i < text.length; i++) { + const char = text[i] + if (inQuote) { + if (char === inQuote && text[i - 1] !== "\\") { + inQuote = null + } + continue + } + if (char === '"' || char === "'") { + inQuote = char + continue + } + if (char === "/" && text[i + 1] === ">") { + return i + 1 // index of '>' in `/>` + } + } + return -1 +} + +// Trims trailing, incomplete `` or `` tags to avoid +// injecting broken raw HTML into markdown while streaming. +const handleIncompleteMentionTags = (text: string): string => { + // Don't process if inside a complete code block + if (hasCompleteCodeBlock(text)) { + return text + } + + let cutIndex: number | null = null + let match: RegExpExecArray | null + mentionTagStartPattern.lastIndex = 0 + while ((match = mentionTagStartPattern.exec(text))) { + const start = match.index + const end = findSelfClosingTagEnd(text, start) + if (end === -1) { + cutIndex = start + break + } else { + // continue scanning after this complete tag + mentionTagStartPattern.lastIndex = end + 1 + } + } + + if (cutIndex !== null) { + const nextNewlineIndex = text.indexOf("\n", cutIndex) + if (nextNewlineIndex !== -1) { + // Remove only the incomplete tag segment and preserve following lines + return text.substring(0, cutIndex) + text.substring(nextNewlineIndex) + } + // No newline after the incomplete tag; drop the trailing incomplete segment + return text.substring(0, cutIndex) + } + return text +} + +// Handles `` wrapper by: +// - Replacing the whole wrapper with only the inner `` when complete +// - Trimming from ` { + // Don't process if inside a complete code block + if (hasCompleteCodeBlock(text)) return text + + const usePattern = /<\s*Use:\s*/gi + let result = text + let match: RegExpExecArray | null + usePattern.lastIndex = 0 + + // We rebuild iteratively in case of multiple occurrences + while ((match = usePattern.exec(result))) { + const useStart = match.index + const mentionStart = result.indexOf("` with `` + const before = result.substring(0, useStart) + const mentionTag = result.substring(mentionStart, mentionEnd + 1) + const after = result.substring(mentionEnd + 1) + result = before + mentionTag + after + + // Reset the regex lastIndex to continue scanning after the replaced tag + usePattern.lastIndex = before.length + mentionTag.length + } + + return result +} + // Helper function to check if we have a complete code block const hasCompleteCodeBlock = (text: string): boolean => { const tripleBackticks = (text.match(/```/g) || []).length @@ -25,7 +142,7 @@ const handleIncompleteLinksAndImages = (text: string): string => { // For images, we still remove them as they can't show skeleton if (isImage) { const startIndex = text.lastIndexOf(linkMatch[1]!) - return text.slice(0, Math.max(0, startIndex)) + return text.substring(0, startIndex) } // For links, preserve the text and close the link with a @@ -54,6 +171,25 @@ const handleIncompleteBold = (text: string): string => { return text } + // Check if the bold marker is in a list item context + // Find the position of the matched bold marker + const markerIndex = text.lastIndexOf(boldMatch[1]!) + const beforeMarker = text.substring(0, markerIndex) + const lastNewlineBeforeMarker = beforeMarker.lastIndexOf("\n") + const lineStart = lastNewlineBeforeMarker === -1 ? 0 : lastNewlineBeforeMarker + 1 + const lineBeforeMarker = text.substring(lineStart, markerIndex) + + // Check if this line is a list item with just the bold marker + if (/^\s*[-*+]\s+$/.test(lineBeforeMarker)) { + // This is a list item with just emphasis markers + // Check if content after marker spans multiple lines + const hasNewlineInContent = contentAfterMarker.includes("\n") + if (hasNewlineInContent) { + // Don't complete if the content spans to another line + return text + } + } + const asteriskPairs = (text.match(/\*\*/g) || []).length if (asteriskPairs % 2 === 1) { return `${text}**` @@ -76,6 +212,25 @@ const handleIncompleteDoubleUnderscoreItalic = (text: string): string => { return text } + // Check if the underscore marker is in a list item context + // Find the position of the matched underscore marker + const markerIndex = text.lastIndexOf(italicMatch[1]!) + const beforeMarker = text.substring(0, markerIndex) + const lastNewlineBeforeMarker = beforeMarker.lastIndexOf("\n") + const lineStart = lastNewlineBeforeMarker === -1 ? 0 : lastNewlineBeforeMarker + 1 + const lineBeforeMarker = text.substring(lineStart, markerIndex) + + // Check if this line is a list item with just the underscore marker + if (/^\s*[-*+]\s+$/.test(lineBeforeMarker)) { + // This is a list item with just emphasis markers + // Check if content after marker spans multiple lines + const hasNewlineInContent = contentAfterMarker.includes("\n") + if (hasNewlineInContent) { + // Don't complete if the content spans to another line + return text + } + } + const underscorePairs = (text.match(/__/g) || []).length if (underscorePairs % 2 === 1) { return `${text}__` @@ -146,7 +301,7 @@ const handleIncompleteSingleAsteriskItalic = (text: string): string => { } // Get content after the first single asterisk - const contentAfterFirstAsterisk = text.slice(Math.max(0, firstSingleAsteriskIndex + 1)) + const contentAfterFirstAsterisk = text.substring(firstSingleAsteriskIndex + 1) // Check if there's meaningful content after the asterisk // Don't close if content is only whitespace or emphasis markers @@ -206,6 +361,15 @@ const countSingleUnderscores = (text: string): number => { if (isWithinMathBlock(text, index)) { return acc } + // Skip if underscore is word-internal (between word characters) + if ( + prevChar && + nextChar && + /[\p{L}\p{N}_]/u.test(prevChar) && + /[\p{L}\p{N}_]/u.test(nextChar) + ) { + return acc + } if (prevChar !== "_" && nextChar !== "_") { return acc + 1 } @@ -224,15 +388,28 @@ const handleIncompleteSingleUnderscoreItalic = (text: string): string => { const singleUnderscoreMatch = text.match(singleUnderscorePattern) if (singleUnderscoreMatch) { - // Find the first single underscore position (not part of __) + // Find the first single underscore position (not part of __ and not word-internal) let firstSingleUnderscoreIndex = -1 for (let i = 0; i < text.length; i++) { if ( text[i] === "_" && text[i - 1] !== "_" && text[i + 1] !== "_" && + text[i - 1] !== "\\" && !isWithinMathBlock(text, i) ) { + // Check if underscore is word-internal (between word characters) + const prevChar = i > 0 ? text[i - 1] : "" + const nextChar = i < text.length - 1 ? text[i + 1] : "" + if ( + prevChar && + nextChar && + /[\p{L}\p{N}_]/u.test(prevChar) && + /[\p{L}\p{N}_]/u.test(nextChar) + ) { + continue + } + firstSingleUnderscoreIndex = i break } @@ -243,7 +420,7 @@ const handleIncompleteSingleUnderscoreItalic = (text: string): string => { } // Get content after the first single underscore - const contentAfterFirstUnderscore = text.slice(Math.max(0, firstSingleUnderscoreIndex + 1)) + const contentAfterFirstUnderscore = text.substring(firstSingleUnderscoreIndex + 1) // Check if there's meaningful content after the underscore // Don't close if content is only whitespace or emphasis markers @@ -253,6 +430,12 @@ const handleIncompleteSingleUnderscoreItalic = (text: string): string => { const singleUnderscores = countSingleUnderscores(text) if (singleUnderscores % 2 === 1) { + // If text ends with newline(s), insert underscore before them + const trailingNewlineMatch = text.match(/\n+$/) + if (trailingNewlineMatch) { + const textBeforeNewlines = text.slice(0, -trailingNewlineMatch[0].length) + return `${textBeforeNewlines}_${trailingNewlineMatch[0]}` + } return `${text}_` } } @@ -357,6 +540,24 @@ const handleIncompleteStrikethrough = (text: string): string => { return text } +// Counts single dollar signs that are not part of double dollar signs and not escaped +const _countSingleDollarSigns = (text: string): number => { + return text.split("").reduce((acc, char, index) => { + if (char === "$") { + const prevChar = text[index - 1] + const nextChar = text[index + 1] + // Skip if escaped with backslash + if (prevChar === "\\") { + return acc + } + if (prevChar !== "$" && nextChar !== "$") { + return acc + 1 + } + } + return acc + }, 0) +} + // Completes incomplete block KaTeX formatting ($$) const handleIncompleteBlockKatex = (text: string): string => { // Count all $$ pairs in the text @@ -453,6 +654,10 @@ export const parseIncompleteMarkdown = (text: string): string => { // Handle various formatting completions // Handle triple asterisks first (most specific) result = handleIncompleteBoldItalic(result) + // Normalize and guard the ` Date: Sat, 27 Sep 2025 23:51:04 +0800 Subject: [PATCH 013/601] refactor: extract localized weekday name retrieval for task scheduling --- .../modules/ai-task/components/task-item.tsx | 36 ++++++++++++++----- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/apps/desktop/layer/renderer/src/modules/ai-task/components/task-item.tsx b/apps/desktop/layer/renderer/src/modules/ai-task/components/task-item.tsx index 657f5596a..b829546a9 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-task/components/task-item.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-task/components/task-item.tsx @@ -21,6 +21,31 @@ import { } from "../query" import { AITaskModal } from "./ai-task-modal" +/** + * Returns a localized weekday name for the given dayOfWeek index (0=Sunday ... 6=Saturday). + * + * @see https://stackoverflow.com/questions/30437134/how-to-get-the-weekday-names-using-intl + * + * @example + * ```ts + * getLocalizedWeekday(0, 'zh') // 'ๆ˜ŸๆœŸๆ—ฅ' + * getLocalizedWeekday(1, 'ja-jp') // 'ๆœˆๆ›œๆ—ฅ' + * ``` + */ +const getLocalizedWeekday = ( + dayOfWeek: number, + locale: string | undefined, + options: { format?: "long" | "short" | "narrow" } = {}, +): string => { + const fmt = options.format || "long" + const d = new Date() + d.setHours(15, 0, 0, 0) /* normalise */ + d.setDate(d.getDate() - d.getDay()) /* Sunday */ + const loc = locale || "en-US" + const date = d.setDate(d.getDate() + dayOfWeek) + return new Intl.DateTimeFormat(loc, { weekday: fmt }).format(date) +} + const formatScheduleText = (schedule: TaskSchedule, t: TFunction<"ai", undefined>, i18n: i18n) => { if (!schedule) return t("tasks.schedule.unknown") switch (schedule.type) { @@ -37,15 +62,8 @@ const formatScheduleText = (schedule: TaskSchedule, t: TFunction<"ai", undefined } case "weekly": { const time = dayjs(schedule.timeOfDay) - const locale = i18n.language - const dow = Math.min(6, Math.max(0, schedule.dayOfWeek)) - const dayName = new Intl.DateTimeFormat(locale, { weekday: "long" }).format( - dayjs().day(dow).toDate(), - ) - return t("tasks.schedule.weekly", { - day: dayName, - time: time.format("h:mm A"), - }) + const dayName = getLocalizedWeekday(schedule.dayOfWeek, i18n.language) + return t("tasks.schedule.weekly", { day: dayName, time: time.format("h:mm A") }) } case "monthly": { const time = dayjs(schedule.timeOfDay) From 6faaf722c7c593a2c4ab332b0f8e20c7e14525d8 Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Sun, 28 Sep 2025 16:23:04 +0800 Subject: [PATCH 014/601] feat: action settings in entry get --- packages/internal/store/src/morph/api.ts | 2 +- pnpm-lock.yaml | 104 +++++++++++++---------- pnpm-workspace.yaml | 2 +- 3 files changed, 62 insertions(+), 46 deletions(-) diff --git a/packages/internal/store/src/morph/api.ts b/packages/internal/store/src/morph/api.ts index 92fed2353..4b6e6346d 100644 --- a/packages/internal/store/src/morph/api.ts +++ b/packages/internal/store/src/morph/api.ts @@ -76,7 +76,7 @@ class APIMorph { inboxHandle: "feeds" in data ? (data.feeds.type === "inbox" ? data.feeds.id : null) : null, read: false, sources: null, - settings: null, + settings: "settings" in data ? data.settings || null : null, } } toSubscription( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cf79515ff..ccf9f298a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,8 +7,8 @@ settings: catalogs: default: '@follow-app/client-sdk': - specifier: 0.3.67 - version: 0.3.67 + specifier: 0.3.68 + version: 0.3.68 tailwindcss-uikit-colors: specifier: 1.0.0 version: 1.0.0 @@ -339,7 +339,7 @@ importers: version: 4.3.0 '@follow-app/client-sdk': specifier: 'catalog:' - version: 0.3.67(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0)(utf-8-validate@6.0.5))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7)(pg@8.16.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 0.3.68(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0)(utf-8-validate@6.0.5))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7)(pg@8.16.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@follow-app/readability': specifier: workspace:* version: link:../../../../packages/readability @@ -454,7 +454,7 @@ importers: version: 3.0.2(electron@38.1.0) '@follow-app/client-sdk': specifier: 'catalog:' - version: 0.3.67(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7)(pg@8.16.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 0.3.68(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7)(pg@8.16.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@follow/database': specifier: workspace:* version: link:../../../../packages/internal/database @@ -1181,7 +1181,7 @@ importers: version: 6.2.1 '@follow-app/client-sdk': specifier: 'catalog:' - version: 0.3.67(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7)(pg@8.16.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 0.3.68(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7)(pg@8.16.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@follow/tracker': specifier: workspace:* version: link:../../packages/internal/tracker @@ -1603,7 +1603,7 @@ importers: version: 7.62.0(react@19.0.0) react-i18next: specifier: 15.7.3 - version: 15.7.3(i18next@25.5.2(typescript@5.9.2))(react-dom@19.0.0(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0)(typescript@5.9.2) + version: 15.7.3(i18next@25.5.2(typescript@5.9.2))(react-dom@19.0.0(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0)(typescript@5.9.2) rehype-infer-description-meta: specifier: 2.0.0 version: 2.0.0 @@ -1667,7 +1667,7 @@ importers: dependencies: '@follow-app/client-sdk': specifier: 'catalog:' - version: 0.3.67(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7)(pg@8.16.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 0.3.68(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7)(pg@8.16.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@follow/configs': specifier: workspace:* version: link:../../configs @@ -1679,7 +1679,7 @@ importers: dependencies: '@follow-app/client-sdk': specifier: 'catalog:' - version: 0.3.67(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7)(pg@8.16.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 0.3.68(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7)(pg@8.16.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@follow/constants': specifier: workspace:* version: link:../constants @@ -1751,7 +1751,7 @@ importers: dependencies: '@follow-app/client-sdk': specifier: 'catalog:' - version: 0.3.67(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7)(pg@8.16.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 0.3.68(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7)(pg@8.16.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@follow/constants': specifier: workspace:* version: link:../constants @@ -1782,7 +1782,7 @@ importers: version: 1.0.1(@types/node@24.5.2) '@follow-app/client-sdk': specifier: 'catalog:' - version: 0.3.67(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7)(pg@8.16.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 0.3.68(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7)(pg@8.16.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@folo-services/drizzle': specifier: 0.1.27 version: 0.1.27(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.7) @@ -1812,7 +1812,7 @@ importers: dependencies: '@follow-app/client-sdk': specifier: 'catalog:' - version: 0.3.67(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7)(pg@8.16.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 0.3.68(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7)(pg@8.16.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@follow/configs': specifier: workspace:* version: link:../../configs @@ -1861,7 +1861,7 @@ importers: devDependencies: '@follow-app/client-sdk': specifier: 'catalog:' - version: 0.3.67(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7)(pg@8.16.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 0.3.68(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7)(pg@8.16.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@follow/configs': specifier: workspace:* version: link:../../configs @@ -4215,26 +4215,26 @@ packages: '@floating-ui/utils@0.2.10': resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} - '@follow-app/client-sdk@0.3.67': - resolution: {integrity: sha512-6LMtHhmjHFgrtzE+Q0V5Jjx16TMVkNmcLwoBTTCyqC3+M1RWW1KpVfNULTQ1zWmRh71cRvUJJXz1gdd3FpBfEg==} + '@follow-app/client-sdk@0.3.68': + resolution: {integrity: sha512-472Yx3gy/eyraQ1muFK/p/SuOW77kn6I+kApgEEN0q+aP21P6GWGOD04CUUmiZL7TQ99OX5ujMaa3aoBIQsF3w==} '@folo-services/ai-tools@0.2.41': resolution: {integrity: sha512-tX2hTfLAZHYw7B0fWVuCA+uE1ZDGGKyDaPLEgn9WRdwYVnBdOqBFPbNx3FioB69fg+aLHeCKgHMkNEmlQZ/nUA==} - '@folo-services/constants@0.1.34': - resolution: {integrity: sha512-l22gfh84z/8WB4E3Q84Nbpp2vU5qCAKBuE4eL94UxejryyTfYWosRz1vD13JS/FDUMNJxpmvuqbPjrMFHqd1eg==} + '@folo-services/constants@0.1.35': + resolution: {integrity: sha512-CRmOvQl8dCQ/iDHq4bqsmi/8MeKuCCzSOanFoeqpv1uaVj4T7yeLVmn4uqoH2nyasq8vXtiLL/3kk8PcYP/8Fg==} '@folo-services/drizzle@0.1.27': resolution: {integrity: sha512-7bo9ZkBwGaYmDJqo+PX1eLYO0oJQMZqOkUEEchD78GldfnkaR2AQ5SK7BYrb57Qi6MQJ+A1AuFd3DV41o0cJfw==} - '@folo-services/drizzle@0.1.28': - resolution: {integrity: sha512-j6m3TVHC947vctWStj/9ak+H2uPNgp3G7+JP9x20V1YBmnqqt8Uyar+qmu0sUoI0HHgh0nqgYF6NBzIqTQ0rBA==} + '@folo-services/drizzle@0.1.29': + resolution: {integrity: sha512-gds7kFpkDzIiVtV4MofeffaayCo6qJdiXMDEacYdQqOVcyn+BC4S3KRpYqcGNQKZCrgw0C0o16elEqZhj7X6dg==} '@folo-services/exceptions@0.1.16': resolution: {integrity: sha512-ThmLLe1Dv2CFsDIgcuY6kJhYJnjiQ7UEsewaqc25MkuImDabhzWnnad+3zRnJGLpveaMeEZ69uqUsTbKC+fR7g==} - '@folo-services/shared@0.0.26': - resolution: {integrity: sha512-svM4YizoOpMfc7KAUCMLWIhzWuEmN3dn6cEdK1W1I04206Fo2iwVbQV+fP/saXQmHVSBD3zjww0PR/DLypw6PA==} + '@folo-services/shared@0.0.27': + resolution: {integrity: sha512-DkvRHdZFjormVIjVzWgNSrz7y7XZ7t3CSHUsrDTnZgqc4Fddd4zBUV9Tzyn/2CTmBNm/chXIIhowezO+ochSZg==} '@fontsource/sn-pro@5.2.5': resolution: {integrity: sha512-rBdBv/0ygj6bkO7xDMMFpwobLdSrcQ2Jncb6DIwdeYGoAgeWkQRwVYhGDKasfLjEYCNYxrqr6wsXsM9+aU39RA==} @@ -20123,12 +20123,12 @@ snapshots: '@floating-ui/utils@0.2.10': {} - '@follow-app/client-sdk@0.3.67(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7)(pg@8.16.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@follow-app/client-sdk@0.3.68(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7)(pg@8.16.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: - '@folo-services/constants': 0.1.34 - '@folo-services/drizzle': 0.1.28(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7) + '@folo-services/constants': 0.1.35 + '@folo-services/drizzle': 0.1.29(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7) '@folo-services/exceptions': 0.1.16 - '@folo-services/shared': 0.0.26(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7)(pg@8.16.3) + '@folo-services/shared': 0.0.27(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7)(pg@8.16.3) better-auth: 1.3.13(react-dom@19.0.0(react@19.0.0))(react@19.0.0) zod: 4.1.11 transitivePeerDependencies: @@ -20172,12 +20172,12 @@ snapshots: - svelte - vue - '@follow-app/client-sdk@0.3.67(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0)(utf-8-validate@6.0.5))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7)(pg@8.16.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@follow-app/client-sdk@0.3.68(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0)(utf-8-validate@6.0.5))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7)(pg@8.16.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: - '@folo-services/constants': 0.1.34 - '@folo-services/drizzle': 0.1.28(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0)(utf-8-validate@6.0.5))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7) + '@folo-services/constants': 0.1.35 + '@folo-services/drizzle': 0.1.29(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0)(utf-8-validate@6.0.5))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7) '@folo-services/exceptions': 0.1.16 - '@folo-services/shared': 0.0.26(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0)(utf-8-validate@6.0.5))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7)(pg@8.16.3) + '@folo-services/shared': 0.0.27(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0)(utf-8-validate@6.0.5))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7)(pg@8.16.3) better-auth: 1.3.13(react-dom@19.0.0(react@19.0.0))(react@19.0.0) zod: 4.1.11 transitivePeerDependencies: @@ -20221,12 +20221,12 @@ snapshots: - svelte - vue - '@follow-app/client-sdk@0.3.67(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7)(pg@8.16.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@follow-app/client-sdk@0.3.68(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7)(pg@8.16.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: - '@folo-services/constants': 0.1.34 - '@folo-services/drizzle': 0.1.28(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7) + '@folo-services/constants': 0.1.35 + '@folo-services/drizzle': 0.1.29(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7) '@folo-services/exceptions': 0.1.16 - '@folo-services/shared': 0.0.26(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7)(pg@8.16.3) + '@folo-services/shared': 0.0.27(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7)(pg@8.16.3) better-auth: 1.3.13(react-dom@19.0.0(react@19.0.0))(react@19.0.0) zod: 4.1.11 transitivePeerDependencies: @@ -20309,7 +20309,7 @@ snapshots: - sql.js - sqlite3 - '@folo-services/constants@0.1.34': + '@folo-services/constants@0.1.35': dependencies: zod: 4.1.11 @@ -20391,7 +20391,7 @@ snapshots: - sql.js - sqlite3 - '@folo-services/drizzle@0.1.28(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7)': + '@folo-services/drizzle@0.1.29(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7)': dependencies: '@folo-services/exceptions': 0.1.16 '@hono/zod-openapi': 1.1.2(hono@4.9.8)(zod@4.1.11) @@ -20432,7 +20432,7 @@ snapshots: - sql.js - sqlite3 - '@folo-services/drizzle@0.1.28(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0)(utf-8-validate@6.0.5))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7)': + '@folo-services/drizzle@0.1.29(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0)(utf-8-validate@6.0.5))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7)': dependencies: '@folo-services/exceptions': 0.1.16 '@hono/zod-openapi': 1.1.2(hono@4.9.8)(zod@4.1.11) @@ -20473,7 +20473,7 @@ snapshots: - sql.js - sqlite3 - '@folo-services/drizzle@0.1.28(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7)': + '@folo-services/drizzle@0.1.29(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7)': dependencies: '@folo-services/exceptions': 0.1.16 '@hono/zod-openapi': 1.1.2(hono@4.9.8)(zod@4.1.11) @@ -20516,9 +20516,9 @@ snapshots: '@folo-services/exceptions@0.1.16': {} - '@folo-services/shared@0.0.26(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7)(pg@8.16.3)': + '@folo-services/shared@0.0.27(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7)(pg@8.16.3)': dependencies: - '@folo-services/drizzle': 0.1.28(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7) + '@folo-services/drizzle': 0.1.29(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7) '@hono/zod-openapi': 1.1.2(hono@4.9.8)(zod@4.1.11) drizzle-orm: 0.44.5(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.7)(pg@8.16.3) drizzle-zod: 0.8.3(drizzle-orm@0.44.5(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.0)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.7)(pg@8.16.3))(zod@4.1.11) @@ -20556,9 +20556,9 @@ snapshots: - sql.js - sqlite3 - '@folo-services/shared@0.0.26(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0)(utf-8-validate@6.0.5))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7)(pg@8.16.3)': + '@folo-services/shared@0.0.27(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0)(utf-8-validate@6.0.5))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7)(pg@8.16.3)': dependencies: - '@folo-services/drizzle': 0.1.28(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0)(utf-8-validate@6.0.5))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7) + '@folo-services/drizzle': 0.1.29(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0)(utf-8-validate@6.0.5))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7) '@hono/zod-openapi': 1.1.2(hono@4.9.8)(zod@4.1.11) drizzle-orm: 0.44.5(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0)(utf-8-validate@6.0.5))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(kysely@0.28.7)(pg@8.16.3) drizzle-zod: 0.8.3(drizzle-orm@0.44.5(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0)(utf-8-validate@6.0.5))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(kysely@0.28.7)(pg@8.16.3))(zod@4.1.11) @@ -20596,9 +20596,9 @@ snapshots: - sql.js - sqlite3 - '@folo-services/shared@0.0.26(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7)(pg@8.16.3)': + '@folo-services/shared@0.0.27(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7)(pg@8.16.3)': dependencies: - '@folo-services/drizzle': 0.1.28(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7) + '@folo-services/drizzle': 0.1.29(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(hono@4.9.8)(kysely@0.28.7) '@hono/zod-openapi': 1.1.2(hono@4.9.8)(zod@4.1.11) drizzle-orm: 0.44.5(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.7)(pg@8.16.3) drizzle-zod: 0.8.3(drizzle-orm@0.44.5(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.4)(@expo/metro-runtime@5.0.4(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.16.0(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.6(@babel/core@7.28.4)(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.7)(pg@8.16.3))(zod@4.1.11) @@ -22927,7 +22927,7 @@ snapshots: debug: 2.6.9 invariant: 2.2.4 metro: 0.82.4(bufferutil@4.0.9)(utf-8-validate@6.0.5) - metro-config: 0.82.4(bufferutil@4.0.9) + metro-config: 0.82.4(bufferutil@4.0.9)(utf-8-validate@6.0.5) metro-core: 0.82.4 semver: 7.7.2 transitivePeerDependencies: @@ -30650,6 +30650,22 @@ snapshots: - supports-color - utf-8-validate + metro-config@0.82.4(bufferutil@4.0.9)(utf-8-validate@6.0.5): + dependencies: + connect: 3.7.0 + cosmiconfig: 5.2.1 + flow-enums-runtime: 0.0.6 + jest-validate: 29.7.0 + metro: 0.82.4(bufferutil@4.0.9)(utf-8-validate@6.0.5) + metro-cache: 0.82.4 + metro-core: 0.82.4 + metro-runtime: 0.82.4 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + optional: true + metro-core@0.82.4: dependencies: flow-enums-runtime: 0.0.6 @@ -30835,7 +30851,7 @@ snapshots: metro-babel-transformer: 0.82.4 metro-cache: 0.82.4 metro-cache-key: 0.82.4 - metro-config: 0.82.4(bufferutil@4.0.9) + metro-config: 0.82.4(bufferutil@4.0.9)(utf-8-validate@6.0.5) metro-core: 0.82.4 metro-file-map: 0.82.4 metro-resolver: 0.82.4 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 352858bda..86052ca5f 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -8,7 +8,7 @@ packages: - "!**/example/**" catalog: - "@follow-app/client-sdk": 0.3.67 + "@follow-app/client-sdk": 0.3.68 tailwindcss-uikit-colors: 1.0.0 typescript: 5.9.2 From c64dcac1d0b96ab1a3a16d14037d8d4ef0304f23 Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Sun, 28 Sep 2025 16:45:20 +0800 Subject: [PATCH 015/601] fix: padding in entry content modal --- .../entry-content/components/EntryTitle.tsx | 17 +++++++++++------ .../entry-content/EntryContent.legacy.tsx | 2 +- .../components/layouts/ArticleLayout.tsx | 2 +- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/apps/desktop/layer/renderer/src/modules/entry-content/components/EntryTitle.tsx b/apps/desktop/layer/renderer/src/modules/entry-content/components/EntryTitle.tsx index d44249e34..3c971102e 100644 --- a/apps/desktop/layer/renderer/src/modules/entry-content/components/EntryTitle.tsx +++ b/apps/desktop/layer/renderer/src/modules/entry-content/components/EntryTitle.tsx @@ -2,7 +2,7 @@ import { useEntry } from "@follow/store/entry/hooks" import { useFeedById } from "@follow/store/feed/hooks" import { useInboxById } from "@follow/store/inbox/hooks" import { useEntryTranslation } from "@follow/store/translation/hooks" -import { clsx, formatEstimatedMins, formatTimeToSeconds } from "@follow/utils" +import { cn, formatEstimatedMins, formatTimeToSeconds } from "@follow/utils" import { titleCase } from "title-case" import { useShallow } from "zustand/shallow" @@ -10,7 +10,6 @@ import { useShowAITranslation } from "~/atoms/ai-translation" import { useActionLanguage } from "~/atoms/settings/general" import { useUISettingKey } from "~/atoms/settings/ui" import { RelativeTime } from "~/components/ui/datetime" -import { useFeature } from "~/hooks/biz/useFeature" import { useNavigateEntry } from "~/hooks/biz/useNavigateEntry" import { useFeedSafeUrl } from "~/hooks/common/useFeedSafeUrl" import type { FeedIconEntry } from "~/modules/feed/feed-icon" @@ -23,9 +22,16 @@ import { EntryReadHistory } from "./entry-read-history" interface EntryLinkProps { entryId: string compact?: boolean + containerClassName?: string + noRecentReader?: boolean } -export const EntryTitle = ({ entryId, compact }: EntryLinkProps) => { +export const EntryTitle = ({ + entryId, + compact, + containerClassName, + noRecentReader, +}: EntryLinkProps) => { const entry = useEntry( entryId, useShallow((state) => { @@ -58,7 +64,6 @@ export const EntryTitle = ({ entryId, compact }: EntryLinkProps) => { }), ) - const aiEnabled = useFeature("ai") const hideRecentReader = useUISettingKey("hideRecentReader") const feed = useFeedById(entry?.feedId) @@ -91,7 +96,7 @@ export const EntryTitle = ({ entryId, compact }: EntryLinkProps) => {

    ) : ( - ) diff --git a/apps/desktop/layer/renderer/src/modules/entry-content/components/entry-content/EntryContent.legacy.tsx b/apps/desktop/layer/renderer/src/modules/entry-content/components/entry-content/EntryContent.legacy.tsx index dd8469fa9..3908368ae 100644 --- a/apps/desktop/layer/renderer/src/modules/entry-content/components/entry-content/EntryContent.legacy.tsx +++ b/apps/desktop/layer/renderer/src/modules/entry-content/components/entry-content/EntryContent.legacy.tsx @@ -185,7 +185,7 @@ const EntryContentImpl: Component = ({ : "@[950px]:max-w-[70ch] @7xl:max-w-[80ch] max-w-[550px]", )} > - +
    diff --git a/apps/desktop/layer/renderer/src/modules/entry-content/components/layouts/ArticleLayout.tsx b/apps/desktop/layer/renderer/src/modules/entry-content/components/layouts/ArticleLayout.tsx index 8d010047b..755f69d59 100644 --- a/apps/desktop/layer/renderer/src/modules/entry-content/components/layouts/ArticleLayout.tsx +++ b/apps/desktop/layer/renderer/src/modules/entry-content/components/layouts/ArticleLayout.tsx @@ -72,7 +72,7 @@ export const ArticleLayout: React.FC = ({ return (
    - + From bd33ad5898ba5b31133954dff62af3130dc106b9 Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Sun, 28 Sep 2025 17:13:00 +0800 Subject: [PATCH 016/601] fix: no CustomizeSubscriptionTabs for legacy layout --- .../layer/renderer/src/modules/settings/tabs/appearance.tsx | 4 +++- .../subscription-column/TimelineTabsSettingsModal.tsx | 2 +- .../renderer/src/modules/subscription-column/index.tsx | 6 ++++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/apps/desktop/layer/renderer/src/modules/settings/tabs/appearance.tsx b/apps/desktop/layer/renderer/src/modules/settings/tabs/appearance.tsx index daeed3e5b..16569b61f 100644 --- a/apps/desktop/layer/renderer/src/modules/settings/tabs/appearance.tsx +++ b/apps/desktop/layer/renderer/src/modules/settings/tabs/appearance.tsx @@ -21,6 +21,7 @@ import { useUISettingValue, } from "~/atoms/settings/ui" import { useCurrentModal, useModalStack } from "~/components/ui/modal/stacked/hooks" +import { useFeature } from "~/hooks/biz/useFeature" import { useSetTheme } from "~/hooks/common" import { useShowCustomizeToolbarModal } from "~/modules/customize-toolbar/modal" @@ -40,6 +41,7 @@ const SettingBuilder = createSettingBuilder(useUISettingValue) const _defineItem = createDefineSettingItem(useUISettingValue, setUISetting) export const SettingAppearance = () => { + const aiEnabled = useFeature("ai") const { t } = useTranslation("settings") const isMobile = useMobile() const defineItem = useWrapEnhancedSettingItem(_defineItem, WrapEnhancedSettingTab.Appearance) @@ -161,7 +163,7 @@ export const SettingAppearance = () => { CustomCSS, CustomizeToolbar, - CustomizeSubscriptionTabs, + aiEnabled && CustomizeSubscriptionTabs, ]} />
    diff --git a/apps/desktop/layer/renderer/src/modules/subscription-column/TimelineTabsSettingsModal.tsx b/apps/desktop/layer/renderer/src/modules/subscription-column/TimelineTabsSettingsModal.tsx index 834e89bea..10da1788b 100644 --- a/apps/desktop/layer/renderer/src/modules/subscription-column/TimelineTabsSettingsModal.tsx +++ b/apps/desktop/layer/renderer/src/modules/subscription-column/TimelineTabsSettingsModal.tsx @@ -27,7 +27,7 @@ import { useModalStack } from "~/components/ui/modal/stacked/hooks" import { ROUTE_TIMELINE_OF_VIEW } from "~/constants" import { useTimelineList } from "~/hooks/biz/useTimelineList" -const MAX_VISIBLE = 4 +const MAX_VISIBLE = 5 function ContainerDroppable({ id, children }: { id: "visible" | "hidden"; children: ReactNode }) { const { setNodeRef, isOver } = useDroppable({ id, data: { container: id } }) diff --git a/apps/desktop/layer/renderer/src/modules/subscription-column/index.tsx b/apps/desktop/layer/renderer/src/modules/subscription-column/index.tsx index 5730a0900..2b36454b4 100644 --- a/apps/desktop/layer/renderer/src/modules/subscription-column/index.tsx +++ b/apps/desktop/layer/renderer/src/modules/subscription-column/index.tsx @@ -24,6 +24,7 @@ import { } from "~/atoms/sidebar" import { Focusable } from "~/components/common/Focusable" import { HotkeyScope, ROUTE_TIMELINE_OF_VIEW } from "~/constants" +import { useFeature } from "~/hooks/biz/useFeature" import { useBackHome } from "~/hooks/biz/useNavigateEntry" import { useReduceMotion } from "~/hooks/biz/useReduceMotion" import { useRouteParamsSelector } from "~/hooks/biz/useRouteParams" @@ -257,10 +258,11 @@ const SwipeWrapper: FC<{ active: string; children: React.JSX.Element[] }> = memo ) const TabsRow: FC = () => { + const aiEnabled = useFeature("ai") const timelineList = useTimelineList() const timelineTabs = useUISettingKey("timelineTabs") - if (timelineList.length <= 5) { + if (!aiEnabled) { return (
    {timelineList.map((timelineId, index) => ( @@ -279,7 +281,7 @@ const TabsRow: FC = () => { const savedVisible = (timelineTabs?.visible ?? []).filter((id) => rest.includes(id)) const visible: string[] = [...savedVisible] for (const id of rest) { - if (visible.length >= 4) break + if (visible.length >= 5) break if (!visible.includes(id)) visible.push(id) } From 1dbca292b05aebbde313d5dc421dd561068caad2 Mon Sep 17 00:00:00 2001 From: DIYgod Date: Mon, 29 Sep 2025 19:20:12 +0800 Subject: [PATCH 017/601] fix: missing display tools --- .../src/modules/ai-chat/components/message/AIMessageParts.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIMessageParts.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIMessageParts.tsx index f440868b1..cacd60ae6 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIMessageParts.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIMessageParts.tsx @@ -58,6 +58,7 @@ export const AIMessageParts: React.FC = React.memo( for (const part of message.parts) { const isReasoning = part.type === "reasoning" && !!(part as ReasoningUIPart).text const isTool = part.type.startsWith("tool-") + const isDisplayTool = isTool && part.type.startsWith("tool-display") if (isReasoning) { if (!chainReasoningParts) { @@ -70,7 +71,7 @@ export const AIMessageParts: React.FC = React.memo( } if (isTool) { - if (chainReasoningParts && chainReasoningParts.length > 0) { + if (chainReasoningParts && chainReasoningParts.length > 0 && !isDisplayTool) { chainReasoningParts.push(part as ToolUIPart) } else { parts.push(part as ToolUIPart) From 06076d81163cab9d7099814406cb42cc006a7232 Mon Sep 17 00:00:00 2001 From: Innei Date: Mon, 29 Sep 2025 19:54:14 +0800 Subject: [PATCH 018/601] fix: adjust some ai elements style - Modified ChatMoreDropdown to use group-data attributes for hover effects. - Adjusted WelcomeScreen's border radius for a more refined appearance. - Refactored AnimatedMarkdown to streamline inline reference handling and improve tooltip integration. Signed-off-by: Innei --- .../components/layouts/ChatMoreDropdown.tsx | 4 +- .../components/layouts/WelcomeScreen.tsx | 2 +- .../message/animated/AnimatedMarkdown.tsx | 39 ++++++++++++------- .../src/modules/ai-chat/services/index.ts | 14 +------ .../modules/ai-task/components/task-item.tsx | 4 +- 5 files changed, 31 insertions(+), 32 deletions(-) diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatMoreDropdown.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatMoreDropdown.tsx index 437df1a80..ae560a26f 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatMoreDropdown.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatMoreDropdown.tsx @@ -145,13 +145,13 @@ export const ChatMoreDropdown = ({ >

    {session.title || "New Chat"}

    -

    +

    {session.messageCount} {session.messageCount === 1 ? "message" : "messages"}

    - + + + {type === "entry" && } + {type === "feed" && {title}} @@ -289,6 +278,26 @@ const InlineFoloReference: React.FC< ) } +const InnerFoloEntryReference: React.FC<{ id: string }> = ({ id }) => { + const feedId = useEntry(id, (s) => s.feedId) + const feedTitle = useFeedById(feedId, (s) => s.title) + return {feedTitle} +} + +const BaseFoloReferenceBadge = (props: React.PropsWithChildren) => { + return ( + + ) +} + export const InlineTooltipContent: React.FC = ({ title, reason, @@ -299,7 +308,7 @@ export const InlineTooltipContent: React.FC = ({ if (!id) return null return (
    -

    +

    {type === "entry" ? : } {title}

    diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/services/index.ts b/apps/desktop/layer/renderer/src/modules/ai-chat/services/index.ts index cadb8eed8..ad013e968 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/services/index.ts +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/services/index.ts @@ -235,7 +235,7 @@ class AIPersistServiceStatic { } // Only query database if not in cache or cache shows it doesn't exist - if (cachedExists === undefined) { + if (!cachedExists) { const existing = await this.getChatSession(chatId) if (existing) { @@ -274,17 +274,7 @@ class AIPersistServiceStatic { updatedAt: true, }, }) - - // Explicitly check if the result is valid - if (!result || !result.chatId) { - // Mark as not existing in cache - this.markSessionExists(chatId, false) - return null - } - - // Mark as existing in cache - this.markSessionExists(chatId, true) - return result + return result?.chatId ? result : null } async getChatSessions(limit = 20) { diff --git a/apps/desktop/layer/renderer/src/modules/ai-task/components/task-item.tsx b/apps/desktop/layer/renderer/src/modules/ai-task/components/task-item.tsx index b829546a9..cb5bb535a 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-task/components/task-item.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-task/components/task-item.tsx @@ -162,7 +162,7 @@ export const TaskItem = memo(({ task }: { task: AITask }) => { ...(taskSession ? [ { - icon: "i-mgc-history-cute-re" as const, + icon: "i-mgc-history-cute-re", onClick: handleOpenReport, title: t("tasks.actions.view_reports"), loading: openingReport, @@ -171,7 +171,7 @@ export const TaskItem = memo(({ task }: { task: AITask }) => { ] : []), { - icon: "i-mgc-test-tube-cute-re" as const, + icon: "i-mgc-test-tube-cute-re", onClick: async () => { try { const loadingId = toast.loading(t("tasks.toast.test_start")) From c27aced55b7a72a28092e33af244c39d8e41f513 Mon Sep 17 00:00:00 2001 From: Innei Date: Mon, 29 Sep 2025 20:24:50 +0800 Subject: [PATCH 019/601] fix: timeline chat session unique - Introduced localization for "New Chat" and timeline summary title in multiple languages. - Updated AIChatRoot to accept chatId as a prop for better state management. - Refactored useChatHistory to utilize localized titles for chat sessions. - Improved ChatHeader and ChatMoreDropdown to leverage translation for tooltips and messages. - Enhanced WelcomeScreen to set a localized title for the timeline summary. Signed-off-by: Innei --- apps/desktop/layer/renderer/src/i18n.ts | 2 + .../ai-chat/components/layouts/AIChatRoot.tsx | 5 +- .../ai-chat/components/layouts/ChatHeader.tsx | 15 ++- .../components/layouts/ChatMoreDropdown.tsx | 6 +- .../components/layouts/WelcomeScreen.tsx | 33 +++++- .../modules/ai-chat/hooks/useChatHistory.ts | 5 +- .../ai-chat/store/chat-core/chat-state.ts | 2 +- .../ai-chat/store/slices/chat.slice.ts | 105 +++++++++--------- .../src/modules/ai-chat/store/store.ts | 3 +- .../src/modules/ai-chat/store/types.ts | 1 + locales/ai/en.json | 2 + locales/ai/ja.json | 2 + locales/ai/zh-CN.json | 2 + 13 files changed, 120 insertions(+), 63 deletions(-) diff --git a/apps/desktop/layer/renderer/src/i18n.ts b/apps/desktop/layer/renderer/src/i18n.ts index 64a6ddac4..e9fe604a2 100644 --- a/apps/desktop/layer/renderer/src/i18n.ts +++ b/apps/desktop/layer/renderer/src/i18n.ts @@ -13,6 +13,8 @@ import { jotaiStore } from "./lib/jotai" export const i18nAtom = atom(i18next) +export const getI18n = () => jotaiStore.get(i18nAtom) + export const langChain = new Chain() export class LocaleCache { diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/AIChatRoot.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/AIChatRoot.tsx index 7ab2755d1..a07ba4a02 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/AIChatRoot.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/AIChatRoot.tsx @@ -56,7 +56,10 @@ export const AIChatRoot: FC = ({ wrapFocusable = true, chatId: externalChatId, }) => { - const useAiContextStore = useMemo(createAIChatStore, []) + const useAiContextStore = useMemo( + () => createAIChatStore({ chatId: externalChatId }), + [externalChatId], + ) const chatActions = useAiContextStore((state) => state.chatActions) useEffect(() => { diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatHeader.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatHeader.tsx index c881affca..bc471a556 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatHeader.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatHeader.tsx @@ -3,6 +3,7 @@ import { cn } from "@follow/utils" import { useAtomValue } from "jotai" import type { ReactNode } from "react" import { useCallback } from "react" +import { useTranslation } from "react-i18next" import { AIChatPanelStyle, setAIPanelVisibility, useAIChatPanelStyle } from "~/atoms/settings/ai" import { useBlockActions, useChatActions, useCurrentTitle } from "~/modules/ai-chat/store/hooks" @@ -48,6 +49,7 @@ const ChatHeaderLayout = ({ const { isScrolledBeyondThreshold } = useAIRootState() const isScrolledBeyondThresholdValue = useAtomValue(isScrolledBeyondThreshold) + const { t } = useTranslation("ai") return (
    - +
    {/* Right side - Actions */} @@ -87,12 +93,13 @@ const ChatHeaderLayout = ({ export const ChatHeader = () => { const panelStyle = useAIChatPanelStyle() + const { t } = useTranslation("ai") return ( ( <> - + @@ -121,11 +128,13 @@ export const ChatHeader = () => { } export const ChatPageHeader = () => { + const { t } = useTranslation("ai") + return ( ( <> - + diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatMoreDropdown.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatMoreDropdown.tsx index ae560a26f..b29da573c 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatMoreDropdown.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatMoreDropdown.tsx @@ -63,7 +63,7 @@ export const ChatMoreDropdown = ({ const confirm = await ask({ title: t("delete_chat"), - message: t("delete_chat_message", { title: session.title || "New Chat" }), + message: t("delete_chat_message", { title: session.title || t("common.new_chat") }), variant: "danger", }) @@ -144,7 +144,9 @@ export const ChatMoreDropdown = ({ className="group flex h-12 cursor-pointer items-center justify-between rounded-md px-2 py-3" >
    -

    {session.title || "New Chat"}

    +

    + {session.title || t("common.new_chat")} +

    {session.messageCount} {session.messageCount === 1 ? "message" : "messages"} diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/WelcomeScreen.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/WelcomeScreen.tsx index 3fecf482d..2c77f4ad2 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/WelcomeScreen.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/WelcomeScreen.tsx @@ -4,8 +4,8 @@ import { FeedViewType } from "@follow/constants" import { clsx } from "@follow/utils" import type { EditorState, LexicalEditor } from "lexical" import { AnimatePresence, m } from "motion/react" -import { useEffect, useRef } from "react" -import { useTranslation } from "react-i18next" +import { useEffect, useMemo, useRef } from "react" +import { getI18n, useTranslation } from "react-i18next" import { useAISettingValue } from "~/atoms/settings/ai" import { ROUTE_ENTRY_PENDING } from "~/constants" @@ -14,6 +14,7 @@ import { AISpline } from "~/modules/ai-chat/components/3d-models/AISpline" import { useAttachScrollBeyond } from "../../hooks/useAttachScrollBeyond" import { useMainEntryId } from "../../hooks/useMainEntryId" +import { AIPersistService } from "../../services" import { useChatActions, useChatError, useChatStatus, useMessages } from "../../store/hooks" import { AIMessageParts } from "../message/AIMessageParts" import { DefaultWelcomeContent, EntrySummaryCard } from "../welcome" @@ -44,6 +45,32 @@ export const WelcomeScreen = ({ onSend, centerInputOnEmpty }: WelcomeScreenProps const { handleScroll } = useAttachScrollBeyond() const showTimelineSummary = shouldFetchTimelineSummary + const todayTimelineSummaryId = useMemo(() => { + const now = new Date() + + const day = now.getDate() + const month = now.getMonth() + 1 + const year = now.getFullYear() + + return `today-timeline-summary-${year}-${month}-${day}` + }, []) + + const status = useChatStatus() + const { setCurrentTitle, getCurrentTitle } = useChatActions() + + const onceRef = useRef(false) + useEffect(() => { + if (!onceRef.current && !getCurrentTitle()) { + onceRef.current = true + const title = t("timeline.summary.title_template", { + date: Intl.DateTimeFormat(getI18n().language, { + dateStyle: "short", + }).format(), + }) + setCurrentTitle(title) + AIPersistService.updateSessionTitle(todayTimelineSummaryId, title) + } + }, [getCurrentTitle, setCurrentTitle, status, t, todayTimelineSummaryId]) return (

    {showTimelineSummary ? ( - + ) : ( diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/hooks/useChatHistory.ts b/apps/desktop/layer/renderer/src/modules/ai-chat/hooks/useChatHistory.ts index abb9c2a96..b3a6d64c3 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/hooks/useChatHistory.ts +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/hooks/useChatHistory.ts @@ -1,5 +1,6 @@ import { useCallback, useState } from "react" +import { getI18n } from "~/i18n" import { AIPersistService } from "~/modules/ai-chat/services/index" import type { ChatSession } from "../types/ChatSession" @@ -10,11 +11,13 @@ export const useChatHistory = () => { const loadHistory = useCallback(async () => { setLoading(true) + + const { t } = getI18n() try { const result = await AIPersistService.getChatSessions() const sessions: ChatSession[] = result.map((row) => ({ chatId: row.chatId, - title: row.title || "New Chat", + title: row.title || t("ai:common.new_chat"), createdAt: new Date(row.createdAt), updatedAt: new Date(row.updatedAt), messageCount: row.messageCount, diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/store/chat-core/chat-state.ts b/apps/desktop/layer/renderer/src/modules/ai-chat/store/chat-core/chat-state.ts index 0a66b3088..89697caf6 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/store/chat-core/chat-state.ts +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/store/chat-core/chat-state.ts @@ -174,7 +174,7 @@ export class ZustandChatState implements ChatState { if (this.#messages.length === 0) return try { - await AIPersistService.ensureSession(this.chatId, "New Chat") + await AIPersistService.ensureSession(this.chatId) // Save messages using incremental updates await AIPersistService.upsertMessages(this.chatId, this.#messages) // Update session time after successfully saving messages diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/store/slices/chat.slice.ts b/apps/desktop/layer/renderer/src/modules/ai-chat/store/slices/chat.slice.ts index 6bebd46c7..0cc713d85 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/store/slices/chat.slice.ts +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/store/slices/chat.slice.ts @@ -1,4 +1,3 @@ -import { nanoid } from "nanoid" import type { StateCreator } from "zustand" import { AIPersistService } from "../../services" @@ -8,68 +7,72 @@ import { ZustandChat } from "../chat-core/chat-instance" import type { ChatSlice } from "../chat-core/types" import { createChatTransport } from "../transport" -export const createChatSlice: StateCreator = (...params) => { - const [set, get] = params - const chatId = nanoid() +export const createChatSlice: (options: { + chatId: string +}) => StateCreator = + (options) => + (...params) => { + const [set, get] = params + const { chatId } = options - // Create chat instance with Zustand integration - const chatInstance = new ZustandChat( - { - id: chatId, - messages: [], - transport: createChatTransport(), - onFinish: async (options) => { - const { message } = options + // Create chat instance with Zustand integration + const chatInstance = new ZustandChat( + { + id: chatId, + messages: [], + transport: createChatTransport(), + onFinish: async (options) => { + const { message } = options - // Only trigger title generation for assistant messages (AI responses) - if (message.role !== "assistant") return + // Only trigger title generation for assistant messages (AI responses) + if (message.role !== "assistant") return - // Get current messages to check if this is the first AI response - const allMessages = chatInstance.chatState.messages + // Get current messages to check if this is the first AI response + const allMessages = chatInstance.chatState.messages - // Check if we have exactly 2 messages (1 user + 1 assistant = first exchange) - // Or if we have 2+ messages and this is the first assistant message - const assistantMessages = allMessages.filter((m) => m.role === "assistant") - const isFirstAIResponse = assistantMessages.length === 1 + // Check if we have exactly 2 messages (1 user + 1 assistant = first exchange) + // Or if we have 2+ messages and this is the first assistant message + const assistantMessages = allMessages.filter((m) => m.role === "assistant") + const isFirstAIResponse = assistantMessages.length === 1 - if (isFirstAIResponse && allMessages.length >= 2) { - try { - // Generate title using the first user message and first AI response - const firstExchange = allMessages.slice(0, 2) + if (isFirstAIResponse && allMessages.length >= 2) { + try { + // Generate title using the first user message and first AI response + const firstExchange = allMessages.slice(0, 2) - const title = await generateChatTitle(firstExchange) + const title = await generateChatTitle(firstExchange) - if (title && chatId) { - try { - await AIPersistService.updateSessionTitle(chatId, title) - if (get().chatId === chatId) { - chatActions.setCurrentTitle(title) + if (title && chatId) { + try { + await AIPersistService.updateSessionTitle(chatId, title) + if (get().chatId === chatId) { + chatActions.setCurrentTitle(title) + } + } catch (error) { + console.error("Failed to update session title:", error) } - } catch (error) { - console.error("Failed to update session title:", error) } + } catch (error) { + console.error("Failed to generate chat title:", error) } - } catch (error) { - console.error("Failed to generate chat title:", error) } - } + }, }, - }, - set, // Pass set function for state updates - ) + set, // Pass set function for state updates + ) - // Create actions - const chatActions = new ChatSliceActions(params, chatInstance) + // Create actions + const chatActions = new ChatSliceActions(params, chatInstance) - return { - // Chat state - chatId, - messages: [], - status: "ready", - error: undefined, - isStreaming: false, - currentTitle: undefined, - chatInstance, - chatActions, + return { + // Chat state + chatId, + messages: [], + status: "ready", + error: undefined, + isStreaming: false, + currentTitle: undefined, + chatInstance, + chatActions, + } } -} diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/store/store.ts b/apps/desktop/layer/renderer/src/modules/ai-chat/store/store.ts index aadec5a1d..3aa48e213 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/store/store.ts +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/store/store.ts @@ -1,3 +1,4 @@ +import { nanoid } from "nanoid" import { createWithEqualityFn } from "zustand/traditional" import type { ChatSlice } from "./chat-core/types" @@ -14,7 +15,7 @@ export type AiChatStore = BlockSlice & export const createAIChatStore = (initialState?: Partial) => { return createWithEqualityFn((...a) => { const blockSlice = createBlockSlice(initialState?.blocks)(...a) - const chatSlice = createChatSlice(...a) + const chatSlice = createChatSlice({ chatId: initialState?.chatId || nanoid() })(...a) return { ...blockSlice, diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/store/types.ts b/apps/desktop/layer/renderer/src/modules/ai-chat/store/types.ts index fd005d0f7..b381d5f1f 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/store/types.ts +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/store/types.ts @@ -37,6 +37,7 @@ export type AIChatContextBlockInput = export interface AIChatStoreInitial { blocks: AIChatContextBlock[] + chatId?: string } export interface AIChatContextBlocks { diff --git a/locales/ai/en.json b/locales/ai/en.json index 4b82274db..5057203a7 100644 --- a/locales/ai/en.json +++ b/locales/ai/en.json @@ -22,6 +22,7 @@ "analytics.usage_trends": "Usage Trends (Last 30 Days)", "clear_chat": "Clear Chat", "clear_chat_message": "Are you sure you want to clear all history? This action will permanently delete all content, including all chat logs and data, and cannot be undone.", + "common.new_chat": "New Chat", "customize_shortcuts": "Customize shortcuts", "customize_shortcuts_title": "Customize AI shortcuts", "delete_chat": "Delete Chat", @@ -243,6 +244,7 @@ "tasks.validation.title_max": "Title must be less than 50 characters", "tasks.validation.title_required": "Title is required", "tasks.view_in_settings": "View scheduled tasks", + "timeline.summary.title_template": "Timeline summary for {{- date}}", "timeline_summary.empty": "Timeline summary will appear here shortly.", "timeline_summary.error": "Unable to generate timeline summary right now.", "timeline_summary.generating": "Summarizing your timeline...", diff --git a/locales/ai/ja.json b/locales/ai/ja.json index 18433d268..48259d063 100644 --- a/locales/ai/ja.json +++ b/locales/ai/ja.json @@ -22,6 +22,7 @@ "analytics.usage_trends": "ไฝฟ็”จๅ‚พๅ‘๏ผˆ้ŽๅŽป30ๆ—ฅ๏ผ‰", "clear_chat": "Chat ใ‚’ใ‚ฏใƒชใ‚ข", "clear_chat_message": "ๆœฌๅฝ“ใซใ™ในใฆใฎๅฑฅๆญดใ‚’ใ‚ฏใƒชใ‚ขใ—ใฆใ‚‚ใ‚ˆใ‚ใ—ใ„ใงใ™ใ‹๏ผŸใ“ใฎๆ“ไฝœใฏใ™ในใฆใฎใ‚ณใƒณใƒ†ใƒณใƒ„ใ€Chat ใƒญใ‚ฐใ€ใƒ‡ใƒผใ‚ฟใ‚’ๆฐธไน…ใซๅ‰Š้™คใ—ใ€ๅ…ƒใซๆˆปใ™ใ“ใจใฏใงใใพใ›ใ‚“ใ€‚", + "common.new_chat": "ๆ–ฐ่ฆใƒใƒฃใƒƒใƒˆ", "customize_shortcuts": "ใ‚ทใƒงใƒผใƒˆใ‚ซใƒƒใƒˆใ‚’ใ‚ซใ‚นใ‚ฟใƒžใ‚คใ‚บ", "customize_shortcuts_title": "AIใ‚ทใƒงใƒผใƒˆใ‚ซใƒƒใƒˆใ‚’ใ‚ซใ‚นใ‚ฟใƒžใ‚คใ‚บ", "delete_chat": "Chat ใ‚’ๅ‰Š้™ค", @@ -209,6 +210,7 @@ "tasks.validation.title_max": "ใ‚ฟใ‚คใƒˆใƒซใฏ50ๆ–‡ๅญ—ๆœชๆบ€ใงใ‚ใ‚‹ๅฟ…่ฆใŒใ‚ใ‚Šใพใ™", "tasks.validation.title_required": "ใ‚ฟใ‚คใƒˆใƒซใฏๅฟ…้ ˆใงใ™", "tasks.view_in_settings": "ใ‚นใ‚ฑใ‚ธใƒฅใƒผใƒซใ•ใ‚ŒใŸใ‚ฟใ‚นใ‚ฏใ‚’่กจ็คบ", + "timeline.summary.title_template": "{{- date}} ใฎใ‚ฟใ‚คใƒ ใƒฉใ‚คใƒณ่ฆ็ด„", "timeline_summary.empty": "ใพใ‚‚ใชใใ‚ฟใ‚คใƒ ใƒฉใ‚คใƒณใฎๆฆ‚่ฆใŒ่กจ็คบใ•ใ‚Œใพใ™ใ€‚", "timeline_summary.error": "็พๅœจใ‚ฟใ‚คใƒ ใƒฉใ‚คใƒณใฎๆฆ‚่ฆใ‚’็”Ÿๆˆใงใใพใ›ใ‚“ใ€‚", "timeline_summary.generating": "ใ‚ฟใ‚คใƒ ใƒฉใ‚คใƒณใ‚’่ฆ็ด„ใ—ใฆใ„ใพใ™...", diff --git a/locales/ai/zh-CN.json b/locales/ai/zh-CN.json index a0ca2187b..7f8a0f66a 100644 --- a/locales/ai/zh-CN.json +++ b/locales/ai/zh-CN.json @@ -22,6 +22,7 @@ "analytics.usage_trends": "ไฝฟ็”จ่ถ‹ๅŠฟ๏ผˆๆœ€่ฟ‘30ๅคฉ๏ผ‰", "clear_chat": "ๆธ…็ฉบๅฏน่ฏ", "clear_chat_message": "็กฎๅฎš่ฆๆธ…็ฉบๆ‰€ๆœ‰ๅކๅฒ่ฎฐๅฝ•ๅ—๏ผŸๆญคๆ“ไฝœๅฐ†ๆฐธไน…ๅˆ ้™คๆ‰€ๆœ‰ๅ†…ๅฎน๏ผŒๅŒ…ๆ‹ฌๆ‰€ๆœ‰่Šๅคฉ่ฎฐๅฝ•ๅ’Œๆ•ฐๆฎ๏ผŒไธ”ๆ— ๆณ•ๆ’ค้”€ใ€‚", + "common.new_chat": "ๆ–ฐๅฏน่ฏ", "customize_shortcuts": "่‡ชๅฎšไน‰ๅฟซๆทๆ–นๅผ", "customize_shortcuts_title": "่‡ชๅฎšไน‰ AI ๅฟซๆทๆ–นๅผ", "delete_chat": "ๅˆ ้™คๅฏน่ฏ", @@ -237,6 +238,7 @@ "tasks.validation.title_max": "ๆ ‡้ข˜ๅฟ…้กปๅฐ‘ไบŽ 50 ไธชๅญ—็ฌฆ", "tasks.validation.title_required": "ๆ ‡้ข˜ไธบๅฟ…ๅกซ้กน", "tasks.view_in_settings": "ๅœจ่ฎพ็ฝฎไธญๆŸฅ็œ‹ๅทฒ่ฎกๅˆ’ไปปๅŠก", + "timeline.summary.title_template": "{{- date}} ็š„ๆ—ถ้—ด็บฟๆ€ป็ป“", "timeline_summary.empty": "ๆ—ถ้—ด็บฟๆ€ป็ป“ๅณๅฐ†ๅ‘ˆ็Žฐใ€‚", "timeline_summary.error": "ๆš‚ๆ—ถๆ— ๆณ•็”Ÿๆˆๆ—ถ้—ด็บฟๆ€ป็ป“ใ€‚", "timeline_summary.generating": "ๆญฃๅœจ็”Ÿๆˆๆ—ถ้—ด็บฟๆ€ป็ป“...", From f1f9bc8c65fe460fc77ea64a9c1ca09ca618d5bb Mon Sep 17 00:00:00 2001 From: Innei Date: Mon, 29 Sep 2025 20:47:20 +0800 Subject: [PATCH 020/601] feat: add hover preview cards for entries and feeds - Introduced EntryPreviewCard and FeedPreviewCard components for enhanced hover previews. - Implemented prefetching of entry details and feed information on hover. - Updated AnimatedMarkdown to utilize the new hover preview components for better user interaction. Signed-off-by: Innei --- .../ui/hover-preview/EntryPreviewCard.tsx | 92 ++++++++++++++ .../ui/hover-preview/FeedPreviewCard.tsx | 56 +++++++++ .../src/components/ui/hover-preview/index.ts | 2 + .../message/animated/AnimatedMarkdown.tsx | 115 +++--------------- .../components/src/ui/hover-card/index.tsx | 39 ++++++ 5 files changed, 209 insertions(+), 95 deletions(-) create mode 100644 apps/desktop/layer/renderer/src/components/ui/hover-preview/EntryPreviewCard.tsx create mode 100644 apps/desktop/layer/renderer/src/components/ui/hover-preview/FeedPreviewCard.tsx create mode 100644 apps/desktop/layer/renderer/src/components/ui/hover-preview/index.ts create mode 100644 packages/internal/components/src/ui/hover-card/index.tsx diff --git a/apps/desktop/layer/renderer/src/components/ui/hover-preview/EntryPreviewCard.tsx b/apps/desktop/layer/renderer/src/components/ui/hover-preview/EntryPreviewCard.tsx new file mode 100644 index 000000000..8e4f02c84 --- /dev/null +++ b/apps/desktop/layer/renderer/src/components/ui/hover-preview/EntryPreviewCard.tsx @@ -0,0 +1,92 @@ +import { + HoverCard, + HoverCardContent, + HoverCardTrigger, +} from "@follow/components/ui/hover-card/index.js" +import { useEntry, usePrefetchEntryDetail } from "@follow/store/entry/hooks" +import { useFeedById } from "@follow/store/feed/hooks" +import { feedIconSelector } from "@follow/store/feed/selectors" +import { cn } from "@follow/utils" +import { m } from "motion/react" +import * as React from "react" + +import { RelativeTime } from "~/components/ui/datetime" +import { FeedIcon } from "~/modules/feed/feed-icon" + +interface EntryPreviewCardProps { + entryId: string + children: React.ReactNode + className?: string +} + +export const EntryPreviewCard: React.FC = ({ + entryId, + children, + className, +}) => { + // Prefetch entry details on hover + usePrefetchEntryDetail(entryId) + + const entry = useEntry(entryId, (state) => { + if (!state) return null + return { + title: state.title, + description: state.description, + author: state.author, + publishedAt: state.publishedAt, + feedId: state.feedId, + url: state.url, + } + }) + + const feed = useFeedById(entry?.feedId, feedIconSelector) + + if (!entry || !feed) { + return <>{children} + } + + return ( + + {children} + + + {/* Header */} +
    +
    + +
    +
    {feed.title}
    +
    {feed.url}
    +
    +
    +
    + + {/* Content */} +
    +
    +

    {entry.title}

    + + {entry.description && ( +

    {entry.description}

    + )} + +
    +
    + {entry.author && by {entry.author}} +
    +
    + +
    +
    +
    +
    +
    +
    +
    + ) +} diff --git a/apps/desktop/layer/renderer/src/components/ui/hover-preview/FeedPreviewCard.tsx b/apps/desktop/layer/renderer/src/components/ui/hover-preview/FeedPreviewCard.tsx new file mode 100644 index 000000000..8921ef5f5 --- /dev/null +++ b/apps/desktop/layer/renderer/src/components/ui/hover-preview/FeedPreviewCard.tsx @@ -0,0 +1,56 @@ +import { + HoverCard, + HoverCardContent, + HoverCardTrigger, +} from "@follow/components/ui/hover-card/index.js" +import { useFeedById } from "@follow/store/feed/hooks" +import { feedIconSelector } from "@follow/store/feed/selectors" +import { cn } from "@follow/utils" +import { m } from "motion/react" +import * as React from "react" + +import { FeedIcon } from "~/modules/feed/feed-icon" + +interface FeedPreviewCardProps { + feedId: string + children: React.ReactNode + className?: string +} + +export const FeedPreviewCard: React.FC = ({ + feedId, + children, + className, +}) => { + const feed = useFeedById(feedId, feedIconSelector) + + if (!feed) { + return <>{children} + } + + return ( + + {children} + + + {/* Header */} +
    +
    + +
    +
    +

    {feed.title}

    +
    +
    +
    +
    +
    +
    +
    + ) +} diff --git a/apps/desktop/layer/renderer/src/components/ui/hover-preview/index.ts b/apps/desktop/layer/renderer/src/components/ui/hover-preview/index.ts new file mode 100644 index 000000000..a63fd0cdc --- /dev/null +++ b/apps/desktop/layer/renderer/src/components/ui/hover-preview/index.ts @@ -0,0 +1,2 @@ +export { EntryPreviewCard } from "./EntryPreviewCard" +export { FeedPreviewCard } from "./FeedPreviewCard" diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/animated/AnimatedMarkdown.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/animated/AnimatedMarkdown.tsx index e150fdacc..d56e195ee 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/animated/AnimatedMarkdown.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/animated/AnimatedMarkdown.tsx @@ -3,15 +3,6 @@ */ import type { LinkProps } from "@follow/components/ui/link/LinkWithTooltip.js" -import { - Tooltip, - TooltipContent, - TooltipPortal, - TooltipTrigger, -} from "@follow/components/ui/tooltip/index.js" -import { useEntry } from "@follow/store/entry/hooks" -import { useFeedById } from "@follow/store/feed/hooks" -import { feedIconSelector } from "@follow/store/feed/selectors" import { cn, isBizId } from "@follow/utils" import * as React from "react" import type { Components } from "react-markdown" @@ -20,10 +11,10 @@ import rehypeRaw from "rehype-raw" import remarkGfm from "remark-gfm" import { MemoizedShikiCode } from "~/components/ui/code-highlighter" +import { EntryPreviewCard, FeedPreviewCard } from "~/components/ui/hover-preview" import { MarkdownLink } from "~/components/ui/markdown/renderers" import { useNavigateEntry } from "~/hooks/biz/useNavigateEntry" import { usePeekModal } from "~/hooks/biz/usePeekModal" -import { FeedIcon } from "~/modules/feed/feed-icon" import { ANIMATION_STYLE as ANIMATION_STYLE_DEFAULT } from "./constants" import { TokenizedText } from "./TokenizedText" @@ -193,9 +184,6 @@ export const MarkdownAnimateText: React.FC = ({ type BaseInlineFoloReferenceProps = { type: "entry" | "feed" id?: string - reason?: string - description?: string - title?: string } const InlineFoloReference: React.FC< BaseInlineFoloReferenceProps & { @@ -203,7 +191,7 @@ const InlineFoloReference: React.FC< className?: string style?: React.CSSProperties } -> = ({ type, children, className, style, id, title, reason, description }) => { +> = ({ type, children, className, style, id }) => { const peekModal = usePeekModal() const navigateEntry = useNavigateEntry() @@ -240,95 +228,32 @@ const InlineFoloReference: React.FC< if (!targetId) return null - if (!title) - return ( - - ) - - return ( - - - {type === "entry" && } - {type === "feed" && {title}} - - - - - - - - ) -} - -const InnerFoloEntryReference: React.FC<{ id: string }> = ({ id }) => { - const feedId = useEntry(id, (s) => s.feedId) - const feedTitle = useFeedById(feedId, (s) => s.title) - return {feedTitle} -} - -const BaseFoloReferenceBadge = (props: React.PropsWithChildren) => { - return ( + const isEntry = type === "entry" + const button = ( ) -} -export const InlineTooltipContent: React.FC = ({ - title, - reason, - description, - type, - id, -}) => { - if (!id) return null - return ( -
    -

    - {type === "entry" ? : } - {title} -

    - {reason &&

    {reason}

    } - {description &&

    {description}

    } -
    - ) -} - -const TooltipEntryIcon = ({ entryId }: { entryId: string }) => { - const feedId = useEntry(entryId, (e) => e.feedId) - if (!feedId) return null - return -} - -const TooltipFeedIcon = ({ feedId }: { feedId: string }) => { - const target = useFeedById(feedId, feedIconSelector) - - if (!target) return null - return + if (isEntry) { + return {button} + } else { + return {button} + } } const RelatedEntryLink = (props: LinkProps) => { diff --git a/packages/internal/components/src/ui/hover-card/index.tsx b/packages/internal/components/src/ui/hover-card/index.tsx new file mode 100644 index 000000000..2412d619c --- /dev/null +++ b/packages/internal/components/src/ui/hover-card/index.tsx @@ -0,0 +1,39 @@ +import { cn } from "@follow/utils/utils" +import * as HoverCardPrimitive from "@radix-ui/react-hover-card" +import * as React from "react" + +import { RootPortal } from "../portal" + +const HoverCard = HoverCardPrimitive.Root + +const HoverCardTrigger = HoverCardPrimitive.Trigger + +const HoverCardContent = ({ + ref, + className, + align = "center", + sideOffset = 8, + ...props +}: React.ComponentPropsWithoutRef & { + ref?: React.Ref | null> +}) => ( + + + +) +HoverCardContent.displayName = HoverCardPrimitive.Content.displayName + +const HoverCardArrow = HoverCardPrimitive.Arrow + +export { HoverCard, HoverCardArrow, HoverCardContent, HoverCardTrigger } From 15fce45807e27c1d863d71727bba8cb313e3a0f6 Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Tue, 30 Sep 2025 11:10:26 +0800 Subject: [PATCH 021/601] fix: ignore all view in select --- .../src/modules/feed/view-select-content.tsx | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/apps/desktop/layer/renderer/src/modules/feed/view-select-content.tsx b/apps/desktop/layer/renderer/src/modules/feed/view-select-content.tsx index 619508e15..b9d364538 100644 --- a/apps/desktop/layer/renderer/src/modules/feed/view-select-content.tsx +++ b/apps/desktop/layer/renderer/src/modules/feed/view-select-content.tsx @@ -1,5 +1,5 @@ import { SelectContent, SelectItem } from "@follow/components/ui/select/index.jsx" -import { views } from "@follow/constants" +import { FeedViewType, views } from "@follow/constants" import { cn } from "@follow/utils/utils" import { useTranslation } from "react-i18next" @@ -8,14 +8,16 @@ export const ViewSelectContent = () => { return ( - {views.map((view, index) => ( - -
    - {view.icon} - {t(view.name, { ns: "common" })} -
    -
    - ))} + {views + .filter((view) => view.view !== FeedViewType.All) + .map((view, index) => ( + +
    + {view.icon} + {t(view.name, { ns: "common" })} +
    +
    + ))}
    ) } From 4cc5a1cf5eae0191289fb10c80fcec26b62cd56c Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Tue, 30 Sep 2025 15:58:49 +0800 Subject: [PATCH 022/601] feat: add unread only context --- .../context-bar/blocks/ContextBlock.tsx | 48 ++++++++++++++++--- .../components/layouts/AIChatContextBar.tsx | 24 +++++++++- .../components/message/AIDataBlockItem.tsx | 43 +++++++++++++---- .../components/message/AIDataBlockPart.tsx | 5 +- .../components/message/ai-block-constants.ts | 7 +++ .../modules/ai-chat/hooks/useDisplayBlocks.ts | 27 ++++++++--- .../ai-chat/store/slices/block.slice.ts | 1 + .../src/modules/ai-chat/store/types.ts | 2 +- 8 files changed, 129 insertions(+), 28 deletions(-) diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/context-bar/blocks/ContextBlock.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/context-bar/blocks/ContextBlock.tsx index 5bfd5401a..b0819f423 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/context-bar/blocks/ContextBlock.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/context-bar/blocks/ContextBlock.tsx @@ -75,33 +75,58 @@ type ValueBlockOf = Omit type MainFeedBlock = ValueBlockOf<"mainFeed"> +type UnreadOnlyBlock = ValueBlockOf<"unreadOnly"> -export const MainViewFeedContextBlock: FC<{ +export const CombinedContextBlock: FC<{ viewBlock: MainViewBlock - feedBlock: MainFeedBlock -}> = memo(({ viewBlock, feedBlock }) => { + feedBlock?: MainFeedBlock + unreadOnlyBlock?: UnreadOnlyBlock +}> = memo(({ viewBlock, feedBlock, unreadOnlyBlock }) => { + const { t } = useTranslation("common") const blockActions = useChatBlockActions() const viewIcon = views.find((v) => v.view === Number(viewBlock.value))?.icon.props.className const canRemove = - !blockTypeCanNotBeRemoved.has(viewBlock.type) && !blockTypeCanNotBeRemoved.has(feedBlock.type) + !blockTypeCanNotBeRemoved.has(viewBlock.type) && + (!feedBlock || !blockTypeCanNotBeRemoved.has(feedBlock.type)) const handleRemove = () => { blockActions.removeBlock(viewBlock.id) - blockActions.removeBlock(feedBlock.id) + if (feedBlock) { + blockActions.removeBlock(feedBlock.id) + } + if (unreadOnlyBlock) { + blockActions.removeBlock(unreadOnlyBlock.id) + } } + // Determine what to display + const displayContent = feedBlock ? ( + + + {unreadOnlyBlock && } + + ) : ( + + {(() => { + const viewName = views.find((v) => v.view === Number(viewBlock.value))?.name + return viewName ? t(viewName) : viewBlock.value + })()} + {unreadOnlyBlock && } + + ) + return ( } + content={displayContent} /> ) }) -MainViewFeedContextBlock.displayName = "MainViewFeedContextBlock" +CombinedContextBlock.displayName = "CombinedContextBlock" export const ContextBlock: FC<{ block: AIChatContextBlock }> = memo(({ block }) => { const { t } = useTranslation("common") @@ -125,6 +150,9 @@ export const ContextBlock: FC<{ block: AIChatContextBlock }> = memo(({ block }) case "selectedText": { return "i-mgc-quill-pen-cute-re" } + case "unreadOnly": { + return "i-mgc-round-cute-fi" + } case "fileAttachment": { const { type, dataUrl, previewUrl } = block.attachment const fileCategory = getFileCategoryFromMimeType(type) @@ -159,6 +187,9 @@ export const ContextBlock: FC<{ block: AIChatContextBlock }> = memo(({ block }) case "selectedText": { return `"${block.value}"` } + case "unreadOnly": { + return "Unread Only" + } case "fileAttachment": { const { type, name, dataUrl, previewUrl, uploadStatus, errorMessage, uploadProgress } = block.attachment @@ -255,6 +286,9 @@ export const ContextBlock: FC<{ block: AIChatContextBlock }> = memo(({ block }) case "selectedText": { return "Text" } + case "unreadOnly": { + return "Filter" + } case "fileAttachment": { return "File" } diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/AIChatContextBar.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/AIChatContextBar.tsx index 0a39e39e2..460cfc6cb 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/AIChatContextBar.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/AIChatContextBar.tsx @@ -2,6 +2,7 @@ import { cn } from "@follow/utils/utils" import { memo, useCallback, useEffect, useMemo, useRef } from "react" import { useAISettingValue } from "~/atoms/settings/ai" +import { useGeneralSettingKey } from "~/atoms/settings/general" import { DropdownMenu, DropdownMenuTrigger } from "~/components/ui/dropdown-menu/dropdown-menu" import { useRouteParamsSelector } from "~/hooks/biz/useRouteParams" import { useDisplayBlocks } from "~/modules/ai-chat/hooks/useDisplayBlocks" @@ -11,7 +12,7 @@ import { SUPPORTED_MIME_ACCEPT } from "~/modules/ai-chat/utils/file-validation" import { useBlockActions } from "../../store/hooks" import { BlockSliceAction } from "../../store/slices/block.slice" -import { ContextBlock, MainViewFeedContextBlock } from "../context-bar/blocks" +import { CombinedContextBlock, ContextBlock } from "../context-bar/blocks" import { ContextMenuContent, ShortcutsMenuContent } from "../context-bar/menus" export const AIChatContextBar: Component<{ onSendShortcut?: (prompt: string) => void }> = memo( @@ -71,6 +72,24 @@ export const AIChatContextBar: Component<{ onSendShortcut?: (prompt: string) => } }, [addOrUpdateBlock, feedId, removeBlock]) + // Add unreadOnly context block only when unreadOnly is enabled + const unreadOnly = useGeneralSettingKey("unreadOnly") + useEffect(() => { + if (unreadOnly) { + addOrUpdateBlock({ + id: BlockSliceAction.SPECIAL_TYPES.unreadOnly, + type: "unreadOnly", + value: "true", + }) + } else { + removeBlock(BlockSliceAction.SPECIAL_TYPES.unreadOnly) + } + + return () => { + removeBlock(BlockSliceAction.SPECIAL_TYPES.unreadOnly) + } + }, [addOrUpdateBlock, unreadOnly, removeBlock]) + const displayBlocks = useDisplayBlocks(blocks) return ( @@ -128,10 +147,11 @@ export const AIChatContextBar: Component<{ onSendShortcut?: (prompt: string) => {displayBlocks.map((item) => { if (item.kind === "combined") { return ( - ) } diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIDataBlockItem.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIDataBlockItem.tsx index 6ed7f9476..9283c6cb3 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIDataBlockItem.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIDataBlockItem.tsx @@ -24,9 +24,10 @@ type ValueBlockOf = Omit - feedBlock: ValueBlockOf<"mainFeed"> + feedBlock?: ValueBlockOf<"mainFeed"> + unreadOnlyBlock?: ValueBlockOf<"unreadOnly"> } /** @@ -48,6 +49,9 @@ const getDisplayContent = (block: AIChatContextBlock): React.ReactNode => { case "selectedText": { return `"${block.value}"` } + case "unreadOnly": { + return "Unread Only" + } case "fileAttachment": { if (!block.attachment) { return "[File: Unknown]" @@ -153,21 +157,40 @@ export const AIDataBlockItem: React.FC = React.memo(({ blo AIDataBlockItem.displayName = "AIDataBlockItem" /** - * Combined block item component for main view and main feed - * Displays view icon with feed title content + * Combined block item component for main view with optional feed and unread filter + * Displays view icon with appropriate content based on available blocks */ -export const MainViewFeedDataBlockItem: React.FC = React.memo( - ({ viewBlock, feedBlock }) => { +export const CombinedDataBlockItem: React.FC = React.memo( + ({ viewBlock, feedBlock, unreadOnlyBlock }) => { const { styles, displayContent, title } = React.useMemo(() => { const view = views.find((v) => v.view === Number(viewBlock.value)) const viewName = view?.name ? t(view.name, { ns: "common" }) : viewBlock.value + const unreadOnlyText = unreadOnlyBlock ? " (Unread Only)" : "" + + // Determine content based on whether feedBlock exists + const content = feedBlock ? ( + + + {unreadOnlyBlock && } + + ) : ( + + {viewName} + {unreadOnlyBlock && } + + ) + + const titleText = feedBlock + ? `${viewName} - ${feedBlock.value}${unreadOnlyText}` + : `${viewName}${unreadOnlyText}` + return { styles: getBlockStyles("mainView"), - displayContent: , - title: `${viewName} - ${feedBlock.value}`, + displayContent: content, + title: titleText, } - }, [viewBlock.value, feedBlock.value]) + }, [viewBlock.value, feedBlock, unreadOnlyBlock]) return ( }, ) -MainViewFeedDataBlockItem.displayName = "MainViewFeedDataBlockItem" +CombinedDataBlockItem.displayName = "CombinedDataBlockItem" diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIDataBlockPart.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIDataBlockPart.tsx index 1e1db7522..79541fbc7 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIDataBlockPart.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIDataBlockPart.tsx @@ -4,7 +4,7 @@ import * as React from "react" import { useDisplayBlocks } from "~/modules/ai-chat/hooks/useDisplayBlocks" import type { AIChatContextBlock } from "~/modules/ai-chat/store/types" -import { AIDataBlockItem, MainViewFeedDataBlockItem } from "./AIDataBlockItem" +import { AIDataBlockItem, CombinedDataBlockItem } from "./AIDataBlockItem" interface AIDataBlockPartProps { blocks: AIChatContextBlock[] @@ -40,10 +40,11 @@ export const AIDataBlockPart: React.FC = React.memo(({ blo {displayBlocks.map((item, index) => { if (item.kind === "combined") { return ( - ) } diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/ai-block-constants.ts b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/ai-block-constants.ts index 061c542e5..04a7adecc 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/ai-block-constants.ts +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/ai-block-constants.ts @@ -35,6 +35,11 @@ export const BLOCK_STYLES = { icon: "bg-pink/10 text-pink", label: "text-pink", }, + unreadOnly: { + container: "from-green/5 to-green/10 border-green/20 hover:border-green/30", + icon: "bg-green/10 text-green", + label: "text-green", + }, } as const /** @@ -55,6 +60,7 @@ export const BLOCK_ICONS = { referEntry: "i-mgc-paper-cute-fi", selectedText: "i-mgc-quill-pen-cute-re", fileAttachment: "i-mgc-file-upload-cute-re", + unreadOnly: "i-mgc-round-cute-fi", } as const /** @@ -67,6 +73,7 @@ export const BLOCK_LABELS = { selectedText: "Text", fileAttachment: "File", mainView: "View", + unreadOnly: "Filter", } as const /** diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/hooks/useDisplayBlocks.ts b/apps/desktop/layer/renderer/src/modules/ai-chat/hooks/useDisplayBlocks.ts index fe8fb20a8..e8a72da98 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/hooks/useDisplayBlocks.ts +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/hooks/useDisplayBlocks.ts @@ -10,7 +10,8 @@ export type DisplayBlockItem = | { kind: "combined" viewBlock: ValueBlockOf<"mainView"> - feedBlock: ValueBlockOf<"mainFeed"> + feedBlock?: ValueBlockOf<"mainFeed"> + unreadOnlyBlock?: ValueBlockOf<"unreadOnly"> } | { kind: "single"; block: AIChatContextBlock } @@ -31,14 +32,27 @@ export const useDisplayBlocks = (blocks: AIChatContextBlock[]): DisplayBlockItem const mainFeedBlock = blocks.find( (block): block is ValueBlockOf<"mainFeed"> => block.type === "mainFeed", ) + const unreadOnlyBlock = blocks.find( + (block): block is ValueBlockOf<"unreadOnly"> => block.type === "unreadOnly", + ) - if (mainViewBlock && mainFeedBlock) { + // Always filter out unreadOnly from single blocks since it should only appear in combined blocks + const filteredBlocks = blocks.filter((block) => block.type !== "unreadOnly") + + if (mainViewBlock) { const items: DisplayBlockItem[] = [] - items.push({ kind: "combined", viewBlock: mainViewBlock, feedBlock: mainFeedBlock }) + // Create combined block with optional feedBlock + items.push({ + kind: "combined", + viewBlock: mainViewBlock, + ...(mainFeedBlock && { feedBlock: mainFeedBlock }), + ...(unreadOnlyBlock && { unreadOnlyBlock }), + }) - const otherBlocks = blocks.filter( - (block) => block.id !== mainViewBlock.id && block.id !== mainFeedBlock.id, + // Add other blocks (excluding mainView, mainFeed, and unreadOnly) + const otherBlocks = filteredBlocks.filter( + (block) => block.id !== mainViewBlock.id && block.id !== mainFeedBlock?.id, ) otherBlocks.forEach((block) => { items.push({ kind: "single", block }) @@ -47,6 +61,7 @@ export const useDisplayBlocks = (blocks: AIChatContextBlock[]): DisplayBlockItem return items } - return blocks.map((block) => ({ kind: "single" as const, block })) + // If no mainView, show all blocks except unreadOnly + return filteredBlocks.map((block) => ({ kind: "single" as const, block })) }, [blocks]) } diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/store/slices/block.slice.ts b/apps/desktop/layer/renderer/src/modules/ai-chat/store/slices/block.slice.ts index 1d2220853..a5d38bf52 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/store/slices/block.slice.ts +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/store/slices/block.slice.ts @@ -34,6 +34,7 @@ export class BlockSliceAction { mainEntry: "mainEntry", mainFeed: "mainFeed", selectedText: "selectedText", + unreadOnly: "unreadOnly", } get set() { return this.params[0] diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/store/types.ts b/apps/desktop/layer/renderer/src/modules/ai-chat/store/types.ts index b381d5f1f..8e31b57ee 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/store/types.ts +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/store/types.ts @@ -19,7 +19,7 @@ interface BaseContextBlock { } export interface ValueContextBlock extends BaseContextBlock { - type: "mainView" | "mainEntry" | "mainFeed" | "referEntry" | "selectedText" + type: "mainView" | "mainEntry" | "mainFeed" | "referEntry" | "selectedText" | "unreadOnly" value: string } From 9f2769b34c9eff169111a2d8a4b96b51d73da701 Mon Sep 17 00:00:00 2001 From: Innei Date: Tue, 30 Sep 2025 17:13:30 +0800 Subject: [PATCH 023/601] fix: timeline chat id unique, and message id generator Signed-off-by: Innei --- .../src/modules/ai-chat/components/layouts/AIChatRoot.tsx | 8 +++++++- .../src/modules/ai-chat/components/layouts/ChatHeader.tsx | 5 +++-- .../modules/ai-chat/components/layouts/WelcomeScreen.tsx | 5 +++-- .../layer/renderer/src/modules/ai-chat/constants/index.ts | 4 ++++ .../renderer/src/modules/ai-chat/store/chat-core/types.ts | 1 - .../src/modules/ai-chat/store/slices/chat.slice.ts | 5 ++++- .../layer/renderer/src/modules/ai-chat/store/store.ts | 5 +++-- .../layer/renderer/src/modules/ai-chat/store/types.ts | 2 ++ 8 files changed, 26 insertions(+), 9 deletions(-) diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/AIChatRoot.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/AIChatRoot.tsx index a07ba4a02..20bb449e0 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/AIChatRoot.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/AIChatRoot.tsx @@ -1,4 +1,5 @@ import type { LexicalRichEditorRef } from "@follow/components/ui/lexical-rich-editor/types.js" +import type { IdGenerator } from "ai" import { atom } from "jotai" import type { FC, PropsWithChildren } from "react" import { useEffect, useMemo, useRef } from "react" @@ -20,6 +21,7 @@ import { createAIChatStore } from "../../store/store" interface AIChatRootProps extends PropsWithChildren { wrapFocusable?: boolean chatId?: string + generateId?: IdGenerator } const AIChatRootInner: FC = ({ children, chatId: externalChatId }) => { @@ -55,9 +57,13 @@ export const AIChatRoot: FC = ({ children, wrapFocusable = true, chatId: externalChatId, + generateId, }) => { + const stableGenerateIdFn = useRef(generateId) + stableGenerateIdFn.current = generateId + const useAiContextStore = useMemo( - () => createAIChatStore({ chatId: externalChatId }), + () => createAIChatStore({ chatId: externalChatId, generateId: stableGenerateIdFn.current }), [externalChatId], ) const chatActions = useAiContextStore((state) => state.chatActions) diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatHeader.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatHeader.tsx index bc471a556..31b77992f 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatHeader.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatHeader.tsx @@ -29,13 +29,14 @@ const ChatHeaderLayout = ({ const handleNewChatClick = useCallback(() => { const messages = chatActions.getMessages() - if (messages.length === 0 && !currentTitle) { + + if (messages.length === 0) { return } chatActions.newChat() blockActions.clearBlocks({ keepSpecialTypes: true }) - }, [chatActions, currentTitle, blockActions]) + }, [chatActions, blockActions]) const handleTitleSave = useCallback( async (newTitle: string) => { diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/WelcomeScreen.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/WelcomeScreen.tsx index 2c77f4ad2..df60a28d4 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/WelcomeScreen.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/WelcomeScreen.tsx @@ -12,6 +12,7 @@ import { ROUTE_ENTRY_PENDING } from "~/constants" import { useRouteParamsSelector } from "~/hooks/biz/useRouteParams" import { AISpline } from "~/modules/ai-chat/components/3d-models/AISpline" +import { AI_CHAT_SPECIAL_ID_PREFIX } from "../../constants" import { useAttachScrollBeyond } from "../../hooks/useAttachScrollBeyond" import { useMainEntryId } from "../../hooks/useMainEntryId" import { AIPersistService } from "../../services" @@ -52,7 +53,7 @@ export const WelcomeScreen = ({ onSend, centerInputOnEmpty }: WelcomeScreenProps const month = now.getMonth() + 1 const year = now.getFullYear() - return `today-timeline-summary-${year}-${month}-${day}` + return `${AI_CHAT_SPECIAL_ID_PREFIX}${year}-${month}-${day}` }, []) const status = useChatStatus() @@ -81,7 +82,7 @@ export const WelcomeScreen = ({ onSend, centerInputOnEmpty }: WelcomeScreenProps >
    {showTimelineSummary ? ( - + todayTimelineSummaryId}> ) : ( diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/constants/index.ts b/apps/desktop/layer/renderer/src/modules/ai-chat/constants/index.ts index e3b6e31f7..74be46ffc 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/constants/index.ts +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/constants/index.ts @@ -1 +1,5 @@ export const SCROLLED_BEYOND_THRESHOLD = 100 + +export const AI_CHAT_SPECIAL_ID_PREFIX = { + TIMELINE_SUMMARY: "timeline-summary:", +} diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/store/chat-core/types.ts b/apps/desktop/layer/renderer/src/modules/ai-chat/store/chat-core/types.ts index 15a4101fe..3be582f45 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/store/chat-core/types.ts +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/store/chat-core/types.ts @@ -6,7 +6,6 @@ import type { ZustandChat } from "./chat-instance" // Zustand slice interface export interface ChatSlice { - // Chat state (mirrored from ChatState) chatId: string messages: BizUIMessage[] status: ChatStatus diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/store/slices/chat.slice.ts b/apps/desktop/layer/renderer/src/modules/ai-chat/store/slices/chat.slice.ts index 0cc713d85..54d0ca56a 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/store/slices/chat.slice.ts +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/store/slices/chat.slice.ts @@ -1,3 +1,4 @@ +import type { IdGenerator } from "ai" import type { StateCreator } from "zustand" import { AIPersistService } from "../../services" @@ -9,11 +10,12 @@ import { createChatTransport } from "../transport" export const createChatSlice: (options: { chatId: string + generateId?: IdGenerator }) => StateCreator = (options) => (...params) => { const [set, get] = params - const { chatId } = options + const { chatId, generateId } = options // Create chat instance with Zustand integration const chatInstance = new ZustandChat( @@ -21,6 +23,7 @@ export const createChatSlice: (options: { id: chatId, messages: [], transport: createChatTransport(), + generateId, onFinish: async (options) => { const { message } = options diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/store/store.ts b/apps/desktop/layer/renderer/src/modules/ai-chat/store/store.ts index 3aa48e213..142f9195e 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/store/store.ts +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/store/store.ts @@ -13,9 +13,10 @@ export type AiChatStore = BlockSlice & } export const createAIChatStore = (initialState?: Partial) => { + const { blocks, chatId, generateId } = initialState || {} return createWithEqualityFn((...a) => { - const blockSlice = createBlockSlice(initialState?.blocks)(...a) - const chatSlice = createChatSlice({ chatId: initialState?.chatId || nanoid() })(...a) + const blockSlice = createBlockSlice(blocks)(...a) + const chatSlice = createChatSlice({ chatId: chatId || nanoid(), generateId })(...a) return { ...blockSlice, diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/store/types.ts b/apps/desktop/layer/renderer/src/modules/ai-chat/store/types.ts index 8e31b57ee..3f4768ea1 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/store/types.ts +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/store/types.ts @@ -1,4 +1,5 @@ import type { BizUITools, ToolWithState } from "@folo-services/ai-tools" +import type { IdGenerator } from "ai" export interface FileAttachment { id: string @@ -38,6 +39,7 @@ export type AIChatContextBlockInput = export interface AIChatStoreInitial { blocks: AIChatContextBlock[] chatId?: string + generateId?: IdGenerator } export interface AIChatContextBlocks { From aada1b8b65cfa720d941a2a832c49bb677d3fefe Mon Sep 17 00:00:00 2001 From: Innei Date: Tue, 30 Sep 2025 19:45:35 +0800 Subject: [PATCH 024/601] fix: observer chat message container width - Added useElementWidth hook to observe and track the width of a referenced element using ResizeObserver. - Updated ChatInterface and WelcomeScreen components to utilize the new hook for responsive layout adjustments. - Enhanced AIDisplayEntriesPart and AIDisplayFeedsPart components with improved styling for better alignment and spacing. Signed-off-by: Innei --- .../displays/AIDisplayEntriesPart.tsx | 4 +- .../displays/AIDisplayFeedsPart.tsx | 4 +- .../components/layouts/ChatInterface.tsx | 50 +++++++------------ .../components/layouts/WelcomeScreen.tsx | 15 +++++- packages/internal/hooks/src/index.ts | 1 + .../internal/hooks/src/useElementWidth.ts | 39 +++++++++++++++ 6 files changed, 75 insertions(+), 38 deletions(-) create mode 100644 packages/internal/hooks/src/useElementWidth.ts diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/displays/AIDisplayEntriesPart.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/displays/AIDisplayEntriesPart.tsx index 722429bde..29796f5c0 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/displays/AIDisplayEntriesPart.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/displays/AIDisplayEntriesPart.tsx @@ -39,11 +39,11 @@ const SingleEntryCard = ({ entry }: { entry: SingleEntry }) => { target={undefined} siteUrl={entry.url ?? undefined} /> -

    +

    {entry.title || "Untitled Entry"}

    - +
    ) diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/displays/AIDisplayFeedsPart.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/displays/AIDisplayFeedsPart.tsx index e6f52b2f1..ae0b2a001 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/displays/AIDisplayFeedsPart.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/displays/AIDisplayFeedsPart.tsx @@ -40,11 +40,11 @@ const AIDisplayFeedPartBase = ({ }} siteUrl={siteUrl!} /> -

    +

    {title || "Untitled Feed"}

    - +
    {/* Description */} diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatInterface.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatInterface.tsx index dba6d14cd..fa729a119 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatInterface.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatInterface.tsx @@ -1,5 +1,6 @@ import { useFocusable } from "@follow/components/common/Focusable/hooks.js" import { ScrollArea } from "@follow/components/ui/scroll-area/ScrollArea.js" +import { useElementWidth } from "@follow/hooks" import { getCategoryFeedIds } from "@follow/store/subscription/getter" import { usePrefetchSummary } from "@follow/store/summary/hooks" import { tracker } from "@follow/tracker" @@ -10,7 +11,7 @@ import type { EditorState, LexicalEditor } from "lexical" import { AnimatePresence } from "motion/react" import { nanoid } from "nanoid" import type { FC, RefObject } from "react" -import { startTransition, Suspense, use, useEffect, useLayoutEffect, useRef, useState } from "react" +import { Suspense, use, useEffect, useLayoutEffect, useRef, useState } from "react" import { useEventCallback, useEventListener } from "usehooks-ts" import { useAISettingKey } from "~/atoms/settings/ai" @@ -391,26 +392,8 @@ export const ChatInterface = (props: ChatInterfaceProps) => ( const Messages: FC<{ contentRef?: RefObject }> = ({ contentRef }) => { const messages = useMessages() - - const [messageContainerWidth, setMessageContainerWidth] = useState(0) - - useLayoutEffect(() => { - if (!contentRef) return - - const setMessageContainerWidthTransition = () => - startTransition(() => { - setMessageContainerWidth(contentRef.current?.clientWidth ?? 0) - }) - setMessageContainerWidthTransition() - const resizeObserver = new ResizeObserver(() => { - setMessageContainerWidthTransition() - }) - resizeObserver.observe(contentRef.current) - - return () => { - resizeObserver.disconnect() - } - }, [contentRef]) + const fallbackRef = useRef(null) + const messageContainerWidth = useElementWidth(contentRef ?? fallbackRef) return (
    }> = ({ contentRef } } as React.CSSProperties } > - {messages.map((message, index) => { - const isLastMessage = index === messages.length - 1 - return ( - - {message.role === "user" ? ( - - ) : ( - - )} - - ) - })} + {!!messageContainerWidth && + messages.map((message, index) => { + const isLastMessage = index === messages.length - 1 + return ( + + {message.role === "user" ? ( + + ) : ( + + )} + + ) + })}
    ) } diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/WelcomeScreen.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/WelcomeScreen.tsx index df60a28d4..2e5cb79d9 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/WelcomeScreen.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/WelcomeScreen.tsx @@ -1,6 +1,7 @@ import { Folo } from "@follow/components/icons/folo.js" import { ScrollArea } from "@follow/components/ui/scroll-area/ScrollArea.js" import { FeedViewType } from "@follow/constants" +import { useElementWidth } from "@follow/hooks" import { clsx } from "@follow/utils" import type { EditorState, LexicalEditor } from "lexical" import { AnimatePresence, m } from "motion/react" @@ -174,6 +175,9 @@ const TimelineSummarySection = () => { const { t } = useTranslation("ai") + const messageContainerRef = useRef(null) + const messageContainerWidth = useElementWidth(messageContainerRef) + const isLoading = status === "streaming" const isError = status === "error" return ( @@ -196,7 +200,16 @@ const TimelineSummarySection = () => { )}
    -
    +
    0 ? 1 : 0, + } as React.CSSProperties + } + > {isError ? (

    {t("timeline_summary.error")}

    ) : hasContent && message ? ( diff --git a/packages/internal/hooks/src/index.ts b/packages/internal/hooks/src/index.ts index 4b6200bb9..97402522c 100644 --- a/packages/internal/hooks/src/index.ts +++ b/packages/internal/hooks/src/index.ts @@ -4,6 +4,7 @@ export * from "./useAnyPointDown" export * from "./useControlled" export * from "./useCountDown" export * from "./useDark" +export * from "./useElementWidth" export * from "./useInputComposition" export * from "./useInterval" export * from "./useIsOnline" diff --git a/packages/internal/hooks/src/useElementWidth.ts b/packages/internal/hooks/src/useElementWidth.ts new file mode 100644 index 000000000..8592c8b3e --- /dev/null +++ b/packages/internal/hooks/src/useElementWidth.ts @@ -0,0 +1,39 @@ +import type { RefObject } from "react" +import { startTransition, useLayoutEffect, useState } from "react" + +/** + * Hook to track the width of an element using ResizeObserver + * @param ref - RefObject pointing to the element to observe + * @returns The current width of the element (0 if element is not available) + */ +export function useElementWidth(ref: RefObject): number { + const [width, setWidth] = useState(0) + + useLayoutEffect(() => { + if (!ref.current) return + + const updateWidth = (newWidth: number) => { + startTransition(() => { + setWidth(newWidth) + }) + } + + // Set initial width + updateWidth(ref.current.clientWidth) + + // Create ResizeObserver to track width changes + const resizeObserver = new ResizeObserver(() => { + if (ref.current) { + updateWidth(ref.current.clientWidth) + } + }) + + resizeObserver.observe(ref.current) + + return () => { + resizeObserver.disconnect() + } + }, [ref]) + + return width +} From a52ee4dc77ced243b46ec9986b633ec5c9eeec6f Mon Sep 17 00:00:00 2001 From: Innei Date: Tue, 30 Sep 2025 22:14:07 +0800 Subject: [PATCH 025/601] fix(ai): edit message should updated message Signed-off-by: Innei --- .../components/layouts/WelcomeScreen.tsx | 2 +- .../components/message/AIChatMessage.tsx | 2 +- .../message/UserRichTextMessage.tsx | 24 +++++++++++++++++-- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/WelcomeScreen.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/WelcomeScreen.tsx index 2e5cb79d9..ac294782e 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/WelcomeScreen.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/WelcomeScreen.tsx @@ -54,7 +54,7 @@ export const WelcomeScreen = ({ onSend, centerInputOnEmpty }: WelcomeScreenProps const month = now.getMonth() + 1 const year = now.getFullYear() - return `${AI_CHAT_SPECIAL_ID_PREFIX}${year}-${month}-${day}` + return `${AI_CHAT_SPECIAL_ID_PREFIX.TIMELINE_SUMMARY}${year}-${month}-${day}` }, []) const status = useChatStatus() diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIChatMessage.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIChatMessage.tsx index d0131c3c0..e428e52db 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIChatMessage.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIChatMessage.tsx @@ -110,7 +110,7 @@ export const AIChatMessage: React.FC = React.memo(
    {/* Normal message display */}
    -
    +
    diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/UserRichTextMessage.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/UserRichTextMessage.tsx index 348b00cf8..630666b99 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/UserRichTextMessage.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/UserRichTextMessage.tsx @@ -3,11 +3,13 @@ import { defaultLexicalTheme } from "@follow/components/ui/lexical-rich-editor/t import { cn } from "@follow/utils" import type { InitialConfigType } from "@lexical/react/LexicalComposer" import { LexicalComposer } from "@lexical/react/LexicalComposer" +import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext" import { ContentEditable } from "@lexical/react/LexicalContentEditable" import { LexicalErrorBoundary } from "@lexical/react/LexicalErrorBoundary" import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin" import type { SerializedEditorState } from "lexical" import * as React from "react" +import isEqual from "react-fast-compare" import { FileAttachmentNode } from "../../editor" import { MentionNode } from "../../editor/plugins/mention/MentionNode" @@ -26,6 +28,7 @@ interface UserRichTextMessageProps { export const UserRichTextMessage: React.FC = React.memo( ({ data, className }) => { + const editorState = typeof data.state === "object" ? JSON.stringify(data.state) : data.state let initialConfig: InitialConfigType = null! if (!initialConfig) { initialConfig = { @@ -33,12 +36,12 @@ export const UserRichTextMessage: React.FC = React.mem theme: defaultLexicalTheme, onError, editable: false, - editorState: typeof data.state === "object" ? JSON.stringify(data.state) : data.state, + editorState, nodes: [...LexicalRichEditorNodes, MentionNode, FileAttachmentNode], } } return ( -
    +
    = React.mem ErrorBoundary={LexicalErrorBoundary} placeholder={null} /> +
    ) }, ) + +const ListenableContentChangedPlugin = ({ state }: { state: string }) => { + const [editor] = useLexicalComposerContext() + React.useEffect(() => { + const editorState = editor.getEditorState() + editorState.read(() => { + const text = editorState.toJSON() + + if (isEqual(text, state)) { + return + } + editor.setEditorState(editor.parseEditorState(state)) + }) + }, [editor, state]) + return null +} From 846d0ff9472ea14c94f3781c72a727d54766b22f Mon Sep 17 00:00:00 2001 From: Innei Date: Tue, 30 Sep 2025 23:17:55 +0800 Subject: [PATCH 026/601] fix(ai): message persist of created at and remove user message type of `text` - Updated message sending logic to utilize a static editor instance for improved performance. - Simplified the onSend function signature in WelcomeScreen and DefaultWelcomeContent components. - Enhanced message part handling in the chat state and persistence services. - Removed unused useSaveMessages hook to clean up the codebase. Signed-off-by: Innei --- .../components/layouts/AIErrorFallback.tsx | 2 +- .../components/layouts/ChatInterface.tsx | 137 ++++++++++-------- .../components/layouts/WelcomeScreen.tsx | 4 +- .../components/message/AIChatMessage.tsx | 5 +- .../message/UserRichTextMessage.tsx | 7 +- .../welcome/DefaultWelcomeContent.tsx | 6 +- .../src/modules/ai-chat/editor/index.ts | 6 + .../modules/ai-chat/hooks/useSaveMessages.ts | 33 ----- .../src/modules/ai-chat/services/index.ts | 74 ++-------- .../ai-chat/store/chat-core/chat-actions.ts | 4 +- .../ai-chat/store/chat-core/chat-state.ts | 66 ++++----- .../src/modules/ai-chat/store/types.ts | 20 ++- .../modules/ai-chat/utils/lexical-markdown.ts | 65 ++++----- .../internal/database/src/schemas/index.ts | 52 +------ 14 files changed, 183 insertions(+), 298 deletions(-) delete mode 100644 apps/desktop/layer/renderer/src/modules/ai-chat/hooks/useSaveMessages.ts diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/AIErrorFallback.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/AIErrorFallback.tsx index d425dcf8e..82695fd87 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/AIErrorFallback.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/AIErrorFallback.tsx @@ -31,7 +31,7 @@ export const AIErrorFallback: FallbackRender = (props) => { Show technical details -
    +            
                   {attachOpenInEditor(stack)}
                 
    diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatInterface.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatInterface.tsx index fa729a119..10bc17588 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatInterface.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatInterface.tsx @@ -5,13 +5,13 @@ import { getCategoryFeedIds } from "@follow/store/subscription/getter" import { usePrefetchSummary } from "@follow/store/summary/hooks" import { tracker } from "@follow/tracker" import { clsx, cn, detectIsEditableElement, nextFrame } from "@follow/utils" -import type { BizUIMessage } from "@folo-services/ai-tools" import { ErrorBoundary } from "@sentry/react" -import type { EditorState, LexicalEditor } from "lexical" +import type { EditorState } from "lexical" +import { createEditor } from "lexical" import { AnimatePresence } from "motion/react" import { nanoid } from "nanoid" import type { FC, RefObject } from "react" -import { Suspense, use, useEffect, useLayoutEffect, useRef, useState } from "react" +import { Suspense, use, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react" import { useEventCallback, useEventListener } from "usehooks-ts" import { useAISettingKey } from "~/atoms/settings/ai" @@ -36,10 +36,11 @@ import { useMessages, } from "~/modules/ai-chat/store/hooks" +import { LexicalAIEditorNodes } from "../../editor" import { useAttachScrollBeyond } from "../../hooks/useAttachScrollBeyond" import { AIPanelRefsContext } from "../../store/AIChatContext" -import type { AIChatContextBlock } from "../../store/types" -import { convertLexicalToMarkdown } from "../../utils/lexical-markdown" +import type { AIChatContextBlock, BizUIMessage } from "../../store/types" +import { convertLexicalToMarkdown, getEditorStateJSONString } from "../../utils/lexical-markdown" import { GlobalFileDropZone } from "../file/GlobalFileDropZone" import { AIErrorFallback } from "./AIErrorFallback" import { ChatInput } from "./ChatInput" @@ -174,74 +175,81 @@ const ChatInterfaceContent = ({ centerInputOnEmpty }: ChatInterfaceProps) => { }) }) - const handleSendMessage = useEventCallback( - (message: string | EditorState, editor: LexicalEditor | null) => { - resetScrollState() + const staticEditor = useMemo(() => { + return createEditor({ + nodes: LexicalAIEditorNodes, + }) + }, []) - const blocks = [] as AIChatContextBlock[] + const handleSendMessage = useEventCallback((message: string | EditorState) => { + resetScrollState() - for (const block of blockActions.getBlocks()) { - if (block.type === "fileAttachment" && block.attachment.serverUrl) { - blocks.push({ - ...block, - attachment: { - id: block.attachment.id, - name: block.attachment.name, - type: block.attachment.type, - size: block.attachment.size, - serverUrl: block.attachment.serverUrl, - }, - }) - } else if (block.type === "mainFeed" && block.value.startsWith(ROUTE_FEED_IN_FOLDER)) { - const categoryName = block.value.slice(ROUTE_FEED_IN_FOLDER.length) - const { view } = getRouteParams() - const feedIds = getCategoryFeedIds(categoryName, view) - blocks.push({ - ...block, - value: feedIds.join(","), - }) - } else { - blocks.push(block) - } - } + const blocks = [] as AIChatContextBlock[] - const parts: BizUIMessage["parts"] = [ - { - type: "data-block", - data: blocks, - }, - ] - - if (typeof message === "string") { - parts.push({ - type: "text", - text: message, - }) - } else if (editor) { - parts.push({ - type: "data-rich-text", - data: { - state: JSON.stringify(message.toJSON()), - text: convertLexicalToMarkdown(editor), + for (const block of blockActions.getBlocks()) { + if (block.type === "fileAttachment" && block.attachment.serverUrl) { + blocks.push({ + ...block, + attachment: { + id: block.attachment.id, + name: block.attachment.name, + type: block.attachment.type, + size: block.attachment.size, + serverUrl: block.attachment.serverUrl, }, }) + } else if (block.type === "mainFeed" && block.value.startsWith(ROUTE_FEED_IN_FOLDER)) { + const categoryName = block.value.slice(ROUTE_FEED_IN_FOLDER.length) + const { view } = getRouteParams() + const feedIds = getCategoryFeedIds(categoryName, view) + blocks.push({ + ...block, + value: feedIds.join(","), + }) + } else { + blocks.push(block) } + } - // Capture actual content height (messages container), not including reserved minHeight - scrollHeightBeforeSendingRef.current = messagesContentRef.current?.scrollHeight ?? 0 - chatActions.sendMessage({ - parts, - role: "user", - id: nanoid(), - }) - tracker.aiChatMessageSent() + const parts: BizUIMessage["parts"] = [ + { + type: "data-block", + data: blocks, + }, + ] - nextFrame(() => { - // Calculate and adjust scroll positioning immediately - handleScrollPositioning() + if (typeof message === "string") { + parts.push({ + type: "data-rich-text", + data: { + state: getEditorStateJSONString(message), + text: message, + }, }) - }, - ) + } else { + parts.push({ + type: "data-rich-text", + data: { + state: JSON.stringify(message.toJSON()), + text: convertLexicalToMarkdown(staticEditor), + }, + }) + } + + // Capture actual content height (messages container), not including reserved minHeight + scrollHeightBeforeSendingRef.current = messagesContentRef.current?.scrollHeight ?? 0 + chatActions.sendMessage({ + parts, + role: "user", + id: nanoid(), + }) + tracker.aiChatMessageSent() + + nextFrame(() => { + // Calculate and adjust scroll positioning immediately + handleScrollPositioning() + }) + }) const [bottomPanelHeight, setBottomPanelHeight] = useState(0) const bottomPanelRef = useRef(null) @@ -394,6 +402,7 @@ const Messages: FC<{ contentRef?: RefObject }> = ({ contentRef } const messages = useMessages() const fallbackRef = useRef(null) const messageContainerWidth = useElementWidth(contentRef ?? fallbackRef) + return (
    void + onSend: (message: EditorState | string) => void centerInputOnEmpty?: boolean } diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIChatMessage.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIChatMessage.tsx index e428e52db..921d5a6af 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIChatMessage.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIChatMessage.tsx @@ -9,7 +9,6 @@ import { copyToClipboard } from "~/lib/clipboard" import type { BizUIMessage } from "~/modules/ai-chat/store/types" import { MentionPlugin } from "../../editor" -import type { RichTextPart } from "../../types/ChatSession" import { convertLexicalToMarkdown } from "../../utils/lexical-markdown" import { AIMessageParts } from "./AIMessageParts" import { TokenUsagePill } from "./TokenUsagePill" @@ -39,9 +38,7 @@ const useMessageMarkdownFormat = (message: BizUIMessage) => { } case "data-rich-text": { lexicalEditor ||= createDefaultLexicalEditor([MentionPlugin]) - lexicalEditor.setEditorState( - lexicalEditor.parseEditorState((part as RichTextPart).data.state), - ) + lexicalEditor.setEditorState(lexicalEditor.parseEditorState(part.data.state)) content += convertLexicalToMarkdown(lexicalEditor) break } diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/UserRichTextMessage.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/UserRichTextMessage.tsx index 630666b99..f0e9eb209 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/UserRichTextMessage.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/UserRichTextMessage.tsx @@ -1,4 +1,3 @@ -import { LexicalRichEditorNodes } from "@follow/components/ui/lexical-rich-editor/nodes.js" import { defaultLexicalTheme } from "@follow/components/ui/lexical-rich-editor/theme.js" import { cn } from "@follow/utils" import type { InitialConfigType } from "@lexical/react/LexicalComposer" @@ -11,8 +10,7 @@ import type { SerializedEditorState } from "lexical" import * as React from "react" import isEqual from "react-fast-compare" -import { FileAttachmentNode } from "../../editor" -import { MentionNode } from "../../editor/plugins/mention/MentionNode" +import { LexicalAIEditorNodes } from "../../editor" function onError(error: Error) { console.error("Lexical Read-Only Editor Error:", error) @@ -29,6 +27,7 @@ interface UserRichTextMessageProps { export const UserRichTextMessage: React.FC = React.memo( ({ data, className }) => { const editorState = typeof data.state === "object" ? JSON.stringify(data.state) : data.state + let initialConfig: InitialConfigType = null! if (!initialConfig) { initialConfig = { @@ -37,7 +36,7 @@ export const UserRichTextMessage: React.FC = React.mem onError, editable: false, editorState, - nodes: [...LexicalRichEditorNodes, MentionNode, FileAttachmentNode], + nodes: LexicalAIEditorNodes, } } return ( diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/welcome/DefaultWelcomeContent.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/welcome/DefaultWelcomeContent.tsx index 812a7321f..82f44191d 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/welcome/DefaultWelcomeContent.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/welcome/DefaultWelcomeContent.tsx @@ -1,12 +1,12 @@ import { AIShortcutButton } from "@follow/components/ui/ai-shortcut-button/index.js" -import type { EditorState, LexicalEditor } from "lexical" +import type { EditorState } from "lexical" import { m } from "motion/react" import { useTranslation } from "react-i18next" import { useSettingModal } from "~/modules/settings/modal/use-setting-modal-hack" interface DefaultWelcomeContentProps { - onSend: (message: EditorState | string, editor: LexicalEditor | null) => void + onSend: (message: EditorState | string) => void shortcuts: Array<{ id: string; name: string; prompt: string; enabled: boolean; hotkey?: string }> } @@ -28,7 +28,7 @@ export const DefaultWelcomeContent: React.FC = ({ {enabledShortcuts.slice(0, 6).map((shortcut, index) => ( onSend(shortcut.prompt, null)} + onClick={() => onSend(shortcut.prompt)} animationDelay={index * 0.1} title={shortcut.hotkey ? `${shortcut.name} (${shortcut.hotkey})` : shortcut.name} > diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/editor/index.ts b/apps/desktop/layer/renderer/src/modules/ai-chat/editor/index.ts index 8ea7bb21d..8fbec9ba9 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/editor/index.ts +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/editor/index.ts @@ -1 +1,7 @@ +import { LexicalRichEditorNodes } from "@follow/components/ui/lexical-rich-editor/nodes.js" + +import { FileAttachmentNode, MentionNode } from "./plugins" + export * from "./plugins" + +export const LexicalAIEditorNodes = [...LexicalRichEditorNodes, MentionNode, FileAttachmentNode] diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/hooks/useSaveMessages.ts b/apps/desktop/layer/renderer/src/modules/ai-chat/hooks/useSaveMessages.ts deleted file mode 100644 index e27a265fd..000000000 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/hooks/useSaveMessages.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { useEffect } from "react" - -import { AIPersistService } from "../services" -import { useChatStatus, useMessages } from "../store/hooks" - -export const useSaveMessages = ( - chatId: string, - options: { - enabled: boolean - }, -) => { - const messages = useMessages() - const status = useChatStatus() - - const isStreaming = status === "streaming" - - useEffect(() => { - if (!options.enabled || isStreaming) { - return - } - - if (!chatId) { - return - } - - // Skip persistence for empty chats - if (messages.length === 0) { - return - } - - AIPersistService.replaceAllMessages(chatId, messages) - }, [chatId, messages, options.enabled, isStreaming]) -} diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/services/index.ts b/apps/desktop/layer/renderer/src/modules/ai-chat/services/index.ts index ad013e968..920d1ac5e 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/services/index.ts +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/services/index.ts @@ -1,9 +1,10 @@ import type { AsyncDb } from "@follow/database/db" import { db } from "@follow/database/db" +import type { AiChatMessagesModel } from "@follow/database/schemas/index" import { aiChatMessagesTable, aiChatTable } from "@follow/database/schemas/index" import { asc, count, eq, inArray, sql } from "drizzle-orm" -import type { BizUIMessage } from "../store/types" +import type { BizUIMessage, BizUIMessagePart } from "../store/types" import { isDataBlockPart, isFileAttachmentBlock } from "../utils/extractor" class AIPersistServiceStatic { @@ -27,7 +28,7 @@ class AIPersistServiceStatic { } } - async loadMessages(chatId: string) { + async loadMessages(chatId: string): Promise { return db.query.aiChatMessagesTable.findMany({ where: eq(aiChatMessagesTable.chatId, chatId), orderBy: [asc(aiChatMessagesTable.createdAt)], @@ -37,26 +38,19 @@ class AIPersistServiceStatic { /** * Convert enhanced database message to BizUIMessage format for compatibility */ - private convertToUIMessage(dbMessage: any): BizUIMessage { + private convertToUIMessage(dbMessage: AiChatMessagesModel): BizUIMessage { // Reconstruct UIMessage from database fields const uiMessage: BizUIMessage = { id: dbMessage.id, role: dbMessage.role, + createdAt: dbMessage.createdAt, parts: [], // AI SDK v5 uses parts array } // Add parts based on content format and data if (dbMessage.messageParts && dbMessage.messageParts.length > 0) { // For assistant messages with complex parts (tools, reasoning, etc) - uiMessage.parts = dbMessage.messageParts - } else { - // For simple text messages, create a text part - uiMessage.parts = [ - { - type: "text", - text: dbMessage.content, - }, - ] + uiMessage.parts = dbMessage.messageParts as BizUIMessagePart[] } return uiMessage @@ -95,53 +89,9 @@ class AIPersistServiceStatic { return { session, messages } } - async insertMessages(chatId: string, messages: BizUIMessage[]) { - if (messages.length === 0) { - return - } - - await db - .insert(aiChatMessagesTable) - .values( - messages.map((message) => { - // Store parts as-is since they're stored as JSON and the UI can handle them - const convertedParts = message.parts as any[] - const createdAt = - message.metadata?.finishTime && typeof message.metadata?.duration === "number" - ? new Date( - new Date(message.metadata.finishTime).getTime() - message.metadata.duration, - ) - : new Date() - - return { - id: message.id, - chatId, - role: message.role, - createdAt, - status: "completed" as const, - finishedAt: message.metadata?.finishTime - ? new Date(message.metadata.finishTime) - : undefined, - messageParts: convertedParts, - metadata: message.metadata, - } satisfies typeof aiChatMessagesTable.$inferInsert - }), - ) - .onConflictDoUpdate({ - target: [aiChatMessagesTable.id], - set: { - messageParts: sql`excluded.message_parts`, - metadata: sql`excluded.metadata`, - finishedAt: sql`excluded.finished_at`, - createdAt: sql`excluded.created_at`, - status: sql`excluded.status`, - }, - }) - } - async replaceAllMessages(chatId: string, messages: BizUIMessage[]) { await db.delete(aiChatMessagesTable).where(eq(aiChatMessagesTable.chatId, chatId)) - await this.insertMessages(chatId, messages) + await this.upsertMessages(chatId, messages) } /** @@ -155,15 +105,11 @@ class AIPersistServiceStatic { // Ensure the chat session exists first to avoid foreign key constraint failure await this.ensureSession(chatId) + const results = messages.reduce<(typeof aiChatMessagesTable.$inferInsert)[]>((acc, message) => { if (message.parts.length === 0) return acc - const createdAt = message.metadata?.finishTime - ? new Date( - new Date(message.metadata.finishTime).getTime() - (message.metadata.duration ?? 0), - ) - : new Date() - + const { createdAt } = message const cleanParts = [] as typeof message.parts for (const part of message.parts) { @@ -190,7 +136,7 @@ class AIPersistServiceStatic { finishedAt: message.metadata?.finishTime ? new Date(message.metadata.finishTime) : undefined, - messageParts: cleanParts as (typeof aiChatMessagesTable.$inferInsert)["messageParts"], + messageParts: cleanParts, metadata: message.metadata, }) diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/store/chat-core/chat-actions.ts b/apps/desktop/layer/renderer/src/modules/ai-chat/store/chat-core/chat-actions.ts index e6ff04aa4..0d0a87b62 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/store/chat-core/chat-actions.ts +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/store/chat-core/chat-actions.ts @@ -5,7 +5,7 @@ import type { StateCreator } from "zustand" import { AIPersistService } from "../../services" import { createChatTransport } from "../transport" -import type { BizUIMessage } from "../types" +import type { BizUIMessage, SendingUIMessage } from "../types" import { ZustandChat } from "./chat-instance" import type { ChatSlice } from "./types" @@ -147,7 +147,7 @@ export class ChatSliceActions { } // Core chat actions using AI SDK AbstractChat methods - sendMessage = async (message: string | BizUIMessage, options?: ChatRequestOptions) => { + sendMessage = async (message: string | SendingUIMessage, options?: ChatRequestOptions) => { try { // Convert string to message object if needed const messageObj = diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/store/chat-core/chat-state.ts b/apps/desktop/layer/renderer/src/modules/ai-chat/store/chat-core/chat-state.ts index 89697caf6..c2f768a1b 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/store/chat-core/chat-state.ts +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/store/chat-core/chat-state.ts @@ -6,7 +6,7 @@ import { startTransition } from "react" import { AIPersistService } from "../../services" import { ChatStateEventEmitter } from "../event-system/event-emitter" -import type { BizUIMessage } from "../types" +import type { BizUIMessage, SendingUIMessage } from "../types" import type { ChatSlice } from "./types" // Zustand Chat State that implements AI SDK ChatState interface @@ -76,6 +76,7 @@ export class ZustandChatState implements ChatState { }) } + //// AI SDK ChatState abstract override methods or properties get status(): ChatStatus { return this.#status } @@ -113,8 +114,8 @@ export class ZustandChatState implements ChatState { }) } - pushMessage = (message: BizUIMessage) => { - this.messages = this.#messages.concat(message) + pushMessage = (message: SendingUIMessage) => { + this.messages = this.#messages.concat(this.#fillMessageCreatedAt(message)) } popMessage = () => { @@ -128,46 +129,15 @@ export class ZustandChatState implements ChatState { this.messages = [ ...this.#messages.slice(0, index), - // Deep clone the message to ensure React detects changes - this.snapshot(message), + this.#fillMessageCreatedAt(message), ...this.#messages.slice(index + 1), ] } snapshot = (value: T): T => structuredClone(value) + //// AI SDK ChatState abstract override methods or properties + //// END - // Callback registration methods with proper AI SDK compatibility - registerMessagesCallback = (onChange: () => void, throttleWaitMs?: number): (() => void) => { - const callback = throttleWaitMs ? throttle(onChange, throttleWaitMs) : onChange - - // Convert payload-based event to AI SDK expected callback format - return this.#eventEmitter.on("messages", () => callback()) - } - - registerStatusCallback = (onChange: () => void): (() => void) => { - // Convert payload-based event to AI SDK expected callback format - return this.#eventEmitter.on("status", () => onChange()) - } - - registerErrorCallback = (onChange: () => void): (() => void) => { - // Convert payload-based event to AI SDK expected callback format - return this.#eventEmitter.on("error", () => onChange()) - } - - // Internal event subscription with payload access - onMessagesChange = (listener: (messages: BizUIMessage[]) => void): (() => void) => { - return this.#eventEmitter.on("messages", ({ messages }) => listener(messages)) - } - - onStatusChange = (listener: (status: ChatStatus) => void): (() => void) => { - return this.#eventEmitter.on("status", ({ status }) => listener(status)) - } - - onErrorChange = (listener: (error: Error | undefined) => void): (() => void) => { - return this.#eventEmitter.on("error", ({ error }) => listener(error)) - } - - // Persistence methods #persistMessages = throttle( async () => { // Skip if no messages @@ -176,7 +146,7 @@ export class ZustandChatState implements ChatState { try { await AIPersistService.ensureSession(this.chatId) // Save messages using incremental updates - await AIPersistService.upsertMessages(this.chatId, this.#messages) + await AIPersistService.replaceAllMessages(this.chatId, this.#messages) // Update session time after successfully saving messages await AIPersistService.updateSessionTime(this.chatId) } catch (error) { @@ -187,7 +157,25 @@ export class ZustandChatState implements ChatState { { leading: true, trailing: true }, ) - // Cleanup method + #fillMessageCreatedAt(message: SendingUIMessage | BizUIMessage): BizUIMessage { + const nextMessage = { ...message } as BizUIMessage + + if (nextMessage.createdAt) return nextMessage + if ( + nextMessage.role === "assistant" && + nextMessage.metadata?.finishTime && + nextMessage.metadata?.duration + ) { + nextMessage.createdAt = new Date( + new Date(nextMessage.metadata.finishTime).getTime() - nextMessage.metadata.duration, + ) + } else { + nextMessage.createdAt = new Date() + } + + return nextMessage + } + destroy(): void { this.#eventEmitter.clear() } diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/store/types.ts b/apps/desktop/layer/renderer/src/modules/ai-chat/store/types.ts index 3f4768ea1..b7ac27879 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/store/types.ts +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/store/types.ts @@ -1,5 +1,5 @@ -import type { BizUITools, ToolWithState } from "@folo-services/ai-tools" -import type { IdGenerator } from "ai" +import type { BizUIMetadata, BizUITools, ToolWithState } from "@folo-services/ai-tools" +import type { IdGenerator, UIMessage, UIMessagePart } from "ai" export interface FileAttachment { id: string @@ -51,4 +51,18 @@ export type AIDisplayEntriesTool = ToolWithState export type AIDisplaySubscriptionsTool = ToolWithState export type AIDisplayFlowTool = ToolWithState -export { type BizUIMessage, type BizUIMetadata, type BizUITools } from "@folo-services/ai-tools" +export { type BizUIMetadata, type BizUITools } from "@folo-services/ai-tools" +export type BizUIDataTypes = { + "rich-text": { + state: string + text: string + } + block: AIChatContextBlock[] +} +export type BizUIMessage = UIMessage & { + createdAt: Date +} + +export type BizUIMessagePart = UIMessagePart + +export type SendingUIMessage = Omit diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/utils/lexical-markdown.ts b/apps/desktop/layer/renderer/src/modules/ai-chat/utils/lexical-markdown.ts index 16bd2db52..67cac1f61 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/utils/lexical-markdown.ts +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/utils/lexical-markdown.ts @@ -1,20 +1,5 @@ import { $convertToMarkdownString, TRANSFORMERS } from "@lexical/markdown" -import type { LexicalEditor, SerializedEditorState } from "lexical" - -/** - * Message format types - */ -export type MessageFormat = "plaintext" | "richtext" - -/** - * Message content with format information - */ -export interface MessageContent { - /** The content format */ - format: MessageFormat - /** Raw content - markdown string for plaintext, Lexical schema for richtext */ - content: string | SerializedEditorState -} +import type { LexicalEditor } from "lexical" /** * Convert Lexical editor state to markdown string for AI communication @@ -29,22 +14,34 @@ export function convertLexicalToMarkdown(editor: LexicalEditor): string { return markdown } -/** - * Get markdown string for AI communication from any MessageContent - */ -export function getMarkdownForAI(message: MessageContent, editor?: LexicalEditor): string { - if (message.format === "plaintext") { - return message.content as string - } - - if (message.format === "richtext" && editor) { - // Set editor state from schema and convert to markdown - const schema = message.content as SerializedEditorState - const editorState = editor.parseEditorState(schema) - editor.setEditorState(editorState) - return convertLexicalToMarkdown(editor) - } - - // Fallback - return "" +export function getEditorStateJSONString(plainText: string): string { + return JSON.stringify({ + root: { + children: [ + { + children: [ + { + detail: 0, + format: 0, + mode: "normal", + style: "", + text: plainText, + type: "text", + version: 1, + }, + ], + direction: "ltr", + format: "", + indent: 0, + type: "paragraph", + version: 1, + }, + ], + direction: "ltr", + format: "", + indent: 0, + type: "root", + version: 1, + }, + }) } diff --git a/packages/internal/database/src/schemas/index.ts b/packages/internal/database/src/schemas/index.ts index 9b7af56a9..c19ef702b 100644 --- a/packages/internal/database/src/schemas/index.ts +++ b/packages/internal/database/src/schemas/index.ts @@ -173,49 +173,6 @@ export const aiChatTable = sqliteTable( (table) => [index("idx_ai_chat_sessions_updated_at").on(table.updatedAt)], ) -// Message Part types based on Vercel AI SDK UIMessage parts -interface TextUIPart { - type: "text" - text: string -} - -interface ReasoningUIPart { - type: "reasoning" - reasoning: string -} - -interface ToolInvocationUIPart { - type: "tool-invocation" - toolInvocation: { - state: "partial-call" | "call" | "result" - toolCallId: string - toolName: string - args: any - result?: any - } -} - -interface SourceUIPart { - type: "source" - source: { - sourceType: "url" - id: string - url: string - title?: string - } -} - -interface StepStartUIPart { - type: "step-start" -} - -type UIMessagePart = - | TextUIPart - | ReasoningUIPart - | ToolInvocationUIPart - | SourceUIPart - | StepStartUIPart - // AI Chat Messages Table - Rich text support export const aiChatMessagesTable = sqliteTable( "ai_chat_messages", @@ -228,7 +185,10 @@ export const aiChatMessagesTable = sqliteTable( role: t.text("role").notNull().$type<"user" | "assistant" | "system">(), - createdAt: t.integer("created_at", { mode: "timestamp_ms" }), + createdAt: t + .integer("created_at", { mode: "timestamp_ms" }) + .notNull() + .default(sql`(unixepoch() * 1000)`), metadata: t.text("metadata", { mode: "json" }).$type(), status: t @@ -238,7 +198,7 @@ export const aiChatMessagesTable = sqliteTable( finishedAt: t.integer("finished_at", { mode: "timestamp_ms" }), // Store UIMessage parts for complex assistant responses (tools, reasoning, etc) - messageParts: t.text("message_parts", { mode: "json" }).$type(), + messageParts: t.text("message_parts", { mode: "json" }).$type(), }), (table) => [ index("idx_ai_chat_messages_chat_id_created_at").on(table.chatId, table.createdAt), @@ -246,3 +206,5 @@ export const aiChatMessagesTable = sqliteTable( index("idx_ai_chat_messages_chat_id_role").on(table.chatId, table.role), ], ) + +export type AiChatMessagesModel = typeof aiChatMessagesTable.$inferSelect From b2a6dd4dd087c1af8c258e3ffe9bf50a3fa3b224 Mon Sep 17 00:00:00 2001 From: Innei Date: Tue, 30 Sep 2025 23:26:21 +0800 Subject: [PATCH 027/601] fix(ai): directly edit message object for createdAt handling - Updated the message creation logic in ZustandChatState to directly modify the existing message object instead of creating a new one, improving performance and reducing unnecessary object creation. Signed-off-by: Innei --- .../src/modules/ai-chat/store/chat-core/chat-state.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/store/chat-core/chat-state.ts b/apps/desktop/layer/renderer/src/modules/ai-chat/store/chat-core/chat-state.ts index c2f768a1b..205340a90 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/store/chat-core/chat-state.ts +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/store/chat-core/chat-state.ts @@ -129,7 +129,7 @@ export class ZustandChatState implements ChatState { this.messages = [ ...this.#messages.slice(0, index), - this.#fillMessageCreatedAt(message), + this.snapshot(this.#fillMessageCreatedAt(message)), ...this.#messages.slice(index + 1), ] } @@ -158,7 +158,8 @@ export class ZustandChatState implements ChatState { ) #fillMessageCreatedAt(message: SendingUIMessage | BizUIMessage): BizUIMessage { - const nextMessage = { ...message } as BizUIMessage + // we should directly edit the message object instead of creating a new one + const nextMessage = message as BizUIMessage if (nextMessage.createdAt) return nextMessage if ( From 8a68bde8544799290de433f89d0eff9d507aa24b Mon Sep 17 00:00:00 2001 From: Innei Date: Tue, 30 Sep 2025 23:41:51 +0800 Subject: [PATCH 028/601] fix(ai): rich editor state transformer - Updated ChatInterface to set editor state directly for improved message handling. - Adjusted UserChatMessage to stringify new state for rich text parts. - Included createdAt in message loading for better message tracking. - Refined message conversion in AIPersistServiceStatic to ensure proper type handling. - Modified types to align with updated message structure. Signed-off-by: Innei --- .../layer/renderer/src/modules/ai-chat-session/service.ts | 4 ++-- .../modules/ai-chat/components/layouts/ChatInterface.tsx | 1 + .../modules/ai-chat/components/message/UserChatMessage.tsx | 7 ++----- .../renderer/src/modules/ai-chat/hooks/useLoadMessages.ts | 1 + .../layer/renderer/src/modules/ai-chat/services/index.ts | 7 ++----- .../layer/renderer/src/modules/ai-chat/store/types.ts | 2 +- .../layer/renderer/src/modules/ai-chat/utils/export.ts | 5 ++--- 7 files changed, 11 insertions(+), 16 deletions(-) diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat-session/service.ts b/apps/desktop/layer/renderer/src/modules/ai-chat-session/service.ts index 3524c8240..b669d14e5 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat-session/service.ts +++ b/apps/desktop/layer/renderer/src/modules/ai-chat-session/service.ts @@ -3,7 +3,7 @@ import type { AIChatMessage, AIChatSession } from "@follow-app/client-sdk" import { followApi } from "../../lib/api-client" import { queryClient } from "../../lib/query-client" import { AIPersistService } from "../ai-chat/services" -import type { BizUIMessage, BizUIMetadata } from "../ai-chat/store/types" +import type { BizUIMessage, BizUIMetadata, SendingUIMessage } from "../ai-chat/store/types" import { aiChatSessionKeys } from "./query" // Hard cap on pagination to prevent excessive API calls while keeping initial sync fast. @@ -77,7 +77,7 @@ class AIChatSessionServiceStatic { /** * Normalize a remote message object into BizUIMessage shape expected by the local chat system. */ - private normalizeRemoteMessage = (msg: AIChatMessage): BizUIMessage => { + private normalizeRemoteMessage = (msg: AIChatMessage): SendingUIMessage => { const metadata = msg.metadata && typeof msg.metadata === "object" ? (msg.metadata as BizUIMetadata) : undefined diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatInterface.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatInterface.tsx index 10bc17588..99aaa37fa 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatInterface.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatInterface.tsx @@ -227,6 +227,7 @@ const ChatInterfaceContent = ({ centerInputOnEmpty }: ChatInterfaceProps) => { }, }) } else { + staticEditor.setEditorState(message) parts.push({ type: "data-rich-text", data: { diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/UserChatMessage.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/UserChatMessage.tsx index 2f65e0468..c8d6488bc 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/UserChatMessage.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/UserChatMessage.tsx @@ -8,7 +8,6 @@ import { useEditingMessageId, useSetEditingMessageId } from "~/modules/ai-chat/a import { useChatActions, useChatStatus } from "~/modules/ai-chat/store/hooks" import type { AIChatContextBlock, BizUIMessage } from "~/modules/ai-chat/store/types" -import type { RichTextPart } from "../../types/ChatSession" import { convertLexicalToMarkdown } from "../../utils/lexical-markdown" import { AIDataBlockPart } from "./AIDataBlockPart" import { AIMessageIdContext } from "./AIMessageIdContext" @@ -68,12 +67,10 @@ export const UserChatMessage: React.FC = React.memo(({ mes const nextMessage = messages[messageIndex]! chatActions.setMessages(messagesToKeep) - const richTextPart = nextMessage.parts.find( - (part) => part.type === "data-rich-text", - ) as RichTextPart + const richTextPart = nextMessage.parts.find((part) => part.type === "data-rich-text") if (richTextPart) { richTextPart.data = { - state: newState, + state: JSON.stringify(newState), text: messageContent, } } diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/hooks/useLoadMessages.ts b/apps/desktop/layer/renderer/src/modules/ai-chat/hooks/useLoadMessages.ts index 6f55885f9..bcbcef8e6 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/hooks/useLoadMessages.ts +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/hooks/useLoadMessages.ts @@ -28,6 +28,7 @@ export const useLoadMessages = ( parts: message.messageParts as any[], role: message.role, metadata: message.metadata as BizUIMetadata, + createdAt: message.createdAt, })) chatActions.setMessages(messagesToSet) onLoadEventCallback(messagesToSet) diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/services/index.ts b/apps/desktop/layer/renderer/src/modules/ai-chat/services/index.ts index 920d1ac5e..3b6ecf90d 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/services/index.ts +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/services/index.ts @@ -39,18 +39,15 @@ class AIPersistServiceStatic { * Convert enhanced database message to BizUIMessage format for compatibility */ private convertToUIMessage(dbMessage: AiChatMessagesModel): BizUIMessage { - // Reconstruct UIMessage from database fields const uiMessage: BizUIMessage = { id: dbMessage.id, role: dbMessage.role, createdAt: dbMessage.createdAt, - parts: [], // AI SDK v5 uses parts array + parts: [], } - // Add parts based on content format and data if (dbMessage.messageParts && dbMessage.messageParts.length > 0) { - // For assistant messages with complex parts (tools, reasoning, etc) - uiMessage.parts = dbMessage.messageParts as BizUIMessagePart[] + uiMessage.parts = dbMessage.messageParts as any[] as BizUIMessagePart[] } return uiMessage diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/store/types.ts b/apps/desktop/layer/renderer/src/modules/ai-chat/store/types.ts index b7ac27879..4a6f1cc77 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/store/types.ts +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/store/types.ts @@ -63,6 +63,6 @@ export type BizUIMessage = UIMessage createdAt: Date } -export type BizUIMessagePart = UIMessagePart +export type BizUIMessagePart = UIMessagePart export type SendingUIMessage = Omit diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/utils/export.ts b/apps/desktop/layer/renderer/src/modules/ai-chat/utils/export.ts index b3925e59b..06585cd75 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/utils/export.ts +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/utils/export.ts @@ -1,5 +1,4 @@ import type { BizUIMessage } from "../store/types" -import type { RichTextPart } from "../types/ChatSession" export const exportChatToMarkdown = (messages: BizUIMessage[], title?: string) => { const date = new Date().toLocaleString() @@ -22,7 +21,7 @@ export const exportChatToMarkdown = (messages: BizUIMessage[], title?: string) = if (part.type === "text") { acc.push(part.text) } else if (part.type === "data-rich-text") { - acc.push((part as RichTextPart).data.text) + acc.push(part.data.text) } return acc }, [] as string[]) @@ -40,7 +39,7 @@ export const exportChatToMarkdown = (messages: BizUIMessage[], title?: string) = if (part.type === "text") { acc.push(part.text) } else if (part.type === "data-rich-text") { - acc.push((part as RichTextPart).data.text) + acc.push(part.data.text) } return acc }, [] as string[]) From 8a450c542181a19b72f31605d6f351c0bfe89d02 Mon Sep 17 00:00:00 2001 From: Innei Date: Tue, 30 Sep 2025 23:43:11 +0800 Subject: [PATCH 029/601] fix(ai): add remote message created prop - Changed the return type of normalizeRemoteMessage to BizUIMessage for consistency with the local chat system. - Added createdAt property to the normalized message to ensure accurate timestamp handling. Signed-off-by: Innei --- .../layer/renderer/src/modules/ai-chat-session/service.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat-session/service.ts b/apps/desktop/layer/renderer/src/modules/ai-chat-session/service.ts index b669d14e5..0235f9d8a 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat-session/service.ts +++ b/apps/desktop/layer/renderer/src/modules/ai-chat-session/service.ts @@ -3,7 +3,7 @@ import type { AIChatMessage, AIChatSession } from "@follow-app/client-sdk" import { followApi } from "../../lib/api-client" import { queryClient } from "../../lib/query-client" import { AIPersistService } from "../ai-chat/services" -import type { BizUIMessage, BizUIMetadata, SendingUIMessage } from "../ai-chat/store/types" +import type { BizUIMessage, BizUIMetadata } from "../ai-chat/store/types" import { aiChatSessionKeys } from "./query" // Hard cap on pagination to prevent excessive API calls while keeping initial sync fast. @@ -77,7 +77,7 @@ class AIChatSessionServiceStatic { /** * Normalize a remote message object into BizUIMessage shape expected by the local chat system. */ - private normalizeRemoteMessage = (msg: AIChatMessage): SendingUIMessage => { + private normalizeRemoteMessage = (msg: AIChatMessage): BizUIMessage => { const metadata = msg.metadata && typeof msg.metadata === "object" ? (msg.metadata as BizUIMetadata) : undefined @@ -90,6 +90,7 @@ class AIChatSessionServiceStatic { role: msg.role satisfies BizUIMessage["role"], parts: normalizedParts, metadata, + createdAt: new Date(msg.createdAt), } } } From f8b58662e52d31c575c33a67a76abd82742521fe Mon Sep 17 00:00:00 2001 From: Innei Date: Tue, 30 Sep 2025 23:46:02 +0800 Subject: [PATCH 030/601] fix: swipe media navigation button color style - Enhanced button styles in SwipeMedia component to use bg-material-medium and border for better visual consistency. - Updated icon classes to use MingCute's left and right line icons for improved clarity. Signed-off-by: Innei --- .../layer/renderer/src/components/ui/media/SwipeMedia.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/desktop/layer/renderer/src/components/ui/media/SwipeMedia.tsx b/apps/desktop/layer/renderer/src/components/ui/media/SwipeMedia.tsx index 3b95b67c8..dfdad36ba 100644 --- a/apps/desktop/layer/renderer/src/components/ui/media/SwipeMedia.tsx +++ b/apps/desktop/layer/renderer/src/components/ui/media/SwipeMedia.tsx @@ -100,21 +100,21 @@ export function SwipeMedia({ {emblaApi?.canScrollPrev() && ( )} {emblaApi?.canScrollNext() && ( )}
    From 984887117b4182f96a3bb84b352b8d80dc8e4d3a Mon Sep 17 00:00:00 2001 From: Innei Date: Wed, 1 Oct 2025 00:01:19 +0800 Subject: [PATCH 031/601] fix(layout): unify top header height - Renamed CSS class from `h-entry-header` to `h-top-header` for clarity and consistency. - Introduced `h-top-header-with-border-b` for enhanced layout in `EntryListHeader`. - Updated `RelativeTime` component to accept a `postfix` prop, allowing for more flexible date formatting. - Adjusted instances of `RelativeTime` to utilize the new `postfix` prop for improved display. Signed-off-by: Innei --- .../modules/ai-chat/components/layouts/ChatHeader.tsx | 2 +- .../src/modules/entry-column/Items/all-item.tsx | 2 +- .../modules/entry-column/layouts/EntryListHeader.tsx | 3 ++- .../components/entry-header/AIEntryHeader.tsx | 2 +- apps/desktop/tailwind.config.ts | 3 ++- .../internal/components/src/ui/datetime/index.tsx | 11 ++++------- 6 files changed, 11 insertions(+), 12 deletions(-) diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatHeader.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatHeader.tsx index 31b77992f..94f24c3b3 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatHeader.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatHeader.tsx @@ -59,7 +59,7 @@ const ChatHeaderLayout = ({ )} data-scrolled-beyond-threshold={isScrolledBeyondThresholdValue} > -
    +
    {isFloating && (
    - {!!displayTime && } + {!!displayTime && }
    ) diff --git a/apps/desktop/layer/renderer/src/modules/entry-column/layouts/EntryListHeader.tsx b/apps/desktop/layer/renderer/src/modules/entry-column/layouts/EntryListHeader.tsx index e4e7b3e28..6de995716 100644 --- a/apps/desktop/layer/renderer/src/modules/entry-column/layouts/EntryListHeader.tsx +++ b/apps/desktop/layer/renderer/src/modules/entry-column/layouts/EntryListHeader.tsx @@ -83,10 +83,11 @@ export const EntryListHeader: FC<{ return (
    diff --git a/apps/desktop/layer/renderer/src/modules/entry-content/components/entry-header/AIEntryHeader.tsx b/apps/desktop/layer/renderer/src/modules/entry-content/components/entry-header/AIEntryHeader.tsx index 8ef302a48..db1271936 100644 --- a/apps/desktop/layer/renderer/src/modules/entry-content/components/entry-header/AIEntryHeader.tsx +++ b/apps/desktop/layer/renderer/src/modules/entry-content/components/entry-header/AIEntryHeader.tsx @@ -11,7 +11,7 @@ function EntryHeaderImpl({ entryId, className, compact }: EntryHeaderProps) { return (
    diff --git a/apps/desktop/tailwind.config.ts b/apps/desktop/tailwind.config.ts index e84be7e84..02758b5ae 100644 --- a/apps/desktop/tailwind.config.ts +++ b/apps/desktop/tailwind.config.ts @@ -48,7 +48,8 @@ export default extendConfig({ screen: "100svh", // button height 2rem (size-8) + sidebar padding top 0.625rem (pt-2.5) x 2 // 2 + 0.625 * 2 = 3.25 - "entry-header": "3.25rem", + "top-header": "3.25rem", + "top-header-with-border-b": "calc(3.25rem + 1px)", }, colors: { sidebar: "hsl(var(--fo-sidebar) / )", diff --git a/packages/internal/components/src/ui/datetime/index.tsx b/packages/internal/components/src/ui/datetime/index.tsx index 279ef7c60..3e7507847 100644 --- a/packages/internal/components/src/ui/datetime/index.tsx +++ b/packages/internal/components/src/ui/datetime/index.tsx @@ -25,7 +25,7 @@ export const RelativeTime: FC<{ date: string | Date displayAbsoluteTimeAfterDay?: number dateFormatTemplate?: string - compact?: boolean + postfix?: string }> = (props) => { const { displayAbsoluteTimeAfterDay = 29, dateFormatTemplate = formatTemplateString } = props const nextDateFormatTemplate = @@ -60,17 +60,14 @@ export const RelativeTime: FC<{ if (formated === relative) { return <>{relative} } + + const resolvedPostfix = props.postfix ?? t("space") + t("words.ago") return ( {/* https://github.com/radix-ui/primitives/issues/2248#issuecomment-2147056904 */} {relative} - {!props.compact && ( - <> - {t("space")} - {t("words.ago")} - - )} + {resolvedPostfix} From ab5f0df64c39c4f4dbfef2fa32a1693f564fb71d Mon Sep 17 00:00:00 2001 From: Innei Date: Sun, 5 Oct 2025 23:42:27 +0800 Subject: [PATCH 032/601] fix(ai): prevent context menu from appearing on message right-click - Added `stopPropagation` to the context menu event on the AIMarkdownStreamingMessage component to prevent the default context menu from appearing, enhancing user experience during message interactions. Signed-off-by: Innei --- .../modules/ai-chat/components/message/AIMarkdownMessage.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIMarkdownMessage.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIMarkdownMessage.tsx index d2a6c763f..50e2a49ab 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIMarkdownMessage.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIMarkdownMessage.tsx @@ -1,4 +1,4 @@ -import { cn } from "@follow/utils" +import { cn, stopPropagation } from "@follow/utils" import { memo, useRef } from "react" import { MarkdownAnimateText } from "./animated/AnimatedMarkdown" @@ -23,7 +23,7 @@ export const AIMarkdownStreamingMessage = memo( const stableStreamingState = useRef(isStreaming) return ( -
    +
    Date: Sun, 5 Oct 2025 23:52:06 +0800 Subject: [PATCH 033/601] feat(ui): enhance EntryPreviewCard and FeedPreviewCard with navigation support - Added `onNavigate` prop to `EntryPreviewCard` and `FeedPreviewCard` components to handle navigation on click. - Updated the header and content sections to wrap clickable elements in anchor tags for improved user interaction. - Refactored click handling to prevent default behavior and trigger navigation functions, enhancing the overall user experience. Signed-off-by: Innei --- .../ui/hover-preview/EntryPreviewCard.tsx | 34 +++++++++++++----- .../ui/hover-preview/FeedPreviewCard.tsx | 16 +++++++-- .../message/animated/AnimatedMarkdown.tsx | 35 ++++++++++--------- 3 files changed, 59 insertions(+), 26 deletions(-) diff --git a/apps/desktop/layer/renderer/src/components/ui/hover-preview/EntryPreviewCard.tsx b/apps/desktop/layer/renderer/src/components/ui/hover-preview/EntryPreviewCard.tsx index 8e4f02c84..0a8b5b1c5 100644 --- a/apps/desktop/layer/renderer/src/components/ui/hover-preview/EntryPreviewCard.tsx +++ b/apps/desktop/layer/renderer/src/components/ui/hover-preview/EntryPreviewCard.tsx @@ -17,12 +17,14 @@ interface EntryPreviewCardProps { entryId: string children: React.ReactNode className?: string + onNavigate?: (entryId: string) => void } export const EntryPreviewCard: React.FC = ({ entryId, children, className, + onNavigate, }) => { // Prefetch entry details on hover usePrefetchEntryDetail(entryId) @@ -57,29 +59,45 @@ export const EntryPreviewCard: React.FC = ({ > {/* Header */} {/* Content */}
    -

    {entry.title}

    + { + e.preventDefault() + onNavigate?.(entryId) + }} + className="contents" + target="_blank" + rel="noopener noreferrer" + > +

    {entry.title}

    - {entry.description && ( -

    {entry.description}

    - )} + {entry.description && ( +

    {entry.description}

    + )} +
    {entry.author && by {entry.author}}
    -
    +
    diff --git a/apps/desktop/layer/renderer/src/components/ui/hover-preview/FeedPreviewCard.tsx b/apps/desktop/layer/renderer/src/components/ui/hover-preview/FeedPreviewCard.tsx index 8921ef5f5..49b8249ab 100644 --- a/apps/desktop/layer/renderer/src/components/ui/hover-preview/FeedPreviewCard.tsx +++ b/apps/desktop/layer/renderer/src/components/ui/hover-preview/FeedPreviewCard.tsx @@ -15,12 +15,15 @@ interface FeedPreviewCardProps { feedId: string children: React.ReactNode className?: string + + onNavigate?: (feedId: string) => void } export const FeedPreviewCard: React.FC = ({ feedId, children, className, + onNavigate, }) => { const feed = useFeedById(feedId, feedIconSelector) @@ -39,7 +42,16 @@ export const FeedPreviewCard: React.FC = ({ className={cn("overflow-hidden", className)} > {/* Header */} -
    -
    + diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/animated/AnimatedMarkdown.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/animated/AnimatedMarkdown.tsx index d56e195ee..22b655621 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/animated/AnimatedMarkdown.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/animated/AnimatedMarkdown.tsx @@ -210,21 +210,15 @@ const InlineFoloReference: React.FC< ) }, [id, children]) - const handleClick = React.useCallback( - (event: React.MouseEvent) => { - event.preventDefault() - event.stopPropagation() + const handleClick = React.useCallback(() => { + if (!targetId) return - if (!targetId) return - - if (type === "entry") { - peekModal(targetId, "modal") - } else { - navigateEntry({ feedId: targetId, entryId: null }) - } - }, - [navigateEntry, peekModal, targetId, type], - ) + if (type === "entry") { + peekModal(targetId, "modal") + } else { + navigateEntry({ feedId: targetId, entryId: null }) + } + }, [navigateEntry, peekModal, targetId, type]) if (!targetId) return null @@ -236,6 +230,7 @@ const InlineFoloReference: React.FC< title={type === "entry" ? `Open entry ${targetId}` : `Open feed ${targetId}`} className={cn( "text-text-secondary hover:text-text mx-[0.15em] inline-flex cursor-pointer items-center align-middle opacity-80 transition-opacity hover:opacity-100", + "-translate-y-0.5", className, )} style={style} @@ -250,9 +245,17 @@ const InlineFoloReference: React.FC< ) if (isEntry) { - return {button} + return ( + + {button} + + ) } else { - return {button} + return ( + + {button} + + ) } } From 6f6b0ac953876f7ef8988eb8a0c0bfcbd8fb19f3 Mon Sep 17 00:00:00 2001 From: Innei Date: Mon, 6 Oct 2025 00:25:32 +0800 Subject: [PATCH 034/601] feat: adjust mask background styling - Updated ChatInterface to improve the gradient mask and backdrop filter for better visual effects. - Refined ChatMoreDropdown by removing unnecessary padding from the DropdownMenuSubContent for a cleaner layout. Signed-off-by: Innei --- .../components/layouts/ChatInterface.tsx | 29 ++++++++++++++----- .../components/layouts/ChatMoreDropdown.tsx | 2 +- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatInterface.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatInterface.tsx index 99aaa37fa..51c42b6a1 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatInterface.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatInterface.tsx @@ -377,13 +377,28 @@ const ChatInterfaceContent = ({ centerInputOnEmpty }: ChatInterfaceProps) => {
    {(!centerInputOnEmpty || hasMessages) && ( -
    +
    +
    + +
    +
    )}
    diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatMoreDropdown.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatMoreDropdown.tsx index b29da573c..2249ca66a 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatMoreDropdown.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatMoreDropdown.tsx @@ -127,7 +127,7 @@ export const ChatMoreDropdown = ({ Chat History - + {loading ? (
    From ba236f6bb942819d361d5481aa7e4f3b71938e4a Mon Sep 17 00:00:00 2001 From: RieN 7z Date: Tue, 7 Oct 2025 01:20:13 +0800 Subject: [PATCH 035/601] feat: add image-only view setting and related UI updates (#4524) * feat: add image-only view setting and related UI updates - Introduced a new setting for image-only view mode. - Updated components to conditionally render based on the new setting. - Added translations for the new feature in multiple languages. * fix: changelog --- apps/desktop/changelog/next.md | 3 +- .../entry-column/Items/picture-item.ai.tsx | 16 ++++-- .../Items/picture-item.legacy.tsx | 15 +++-- .../entry-column/Items/picture-masonry.tsx | 56 +++++++++++-------- .../src/modules/entry-column/grid.tsx | 6 +- .../layouts/buttons/SwitchToMasonryButton.tsx | 13 +++++ .../templates/grid-item-template.tsx | 4 ++ locales/app/en.json | 1 + locales/app/ja.json | 1 + locales/app/zh-CN.json | 1 + locales/app/zh-TW.json | 1 + .../components/src/ui/masonry/contexts.tsx | 4 ++ .../components/src/ui/masonry/index.tsx | 4 +- .../internal/shared/src/settings/interface.ts | 1 + 14 files changed, 88 insertions(+), 38 deletions(-) diff --git a/apps/desktop/changelog/next.md b/apps/desktop/changelog/next.md index fc6b11e21..b3855a7f2 100644 --- a/apps/desktop/changelog/next.md +++ b/apps/desktop/changelog/next.md @@ -9,6 +9,7 @@ - Streamlined chat UI with context pickers for feeds, entries, and more. - Early preview: weโ€™ll iterate quickly with your feedback. - Limited rollout: currently available to a subset of users; weโ€™ll expand testing over time. +- Image Only Mode: Add image only mode for image view (#4423) ## Improvements @@ -18,4 +19,4 @@ ## Thanks -Special thanks to volunteer contributors @ for their valuable contributions +Special thanks to volunteer contributors @rien7 for their valuable contributions diff --git a/apps/desktop/layer/renderer/src/modules/entry-column/Items/picture-item.ai.tsx b/apps/desktop/layer/renderer/src/modules/entry-column/Items/picture-item.ai.tsx index 9f4a7de86..04af51da1 100644 --- a/apps/desktop/layer/renderer/src/modules/entry-column/Items/picture-item.ai.tsx +++ b/apps/desktop/layer/renderer/src/modules/entry-column/Items/picture-item.ai.tsx @@ -11,6 +11,7 @@ import type { PropsWithChildren } from "react" import { memo, use, useEffect, useMemo, useState } from "react" import { useTranslation } from "react-i18next" +import { useUISettingKey } from "~/atoms/settings/ui" import { SwipeMedia } from "~/components/ui/media/SwipeMedia" import { useRouteParamsSelector } from "~/hooks/biz/useRouteParams" import { useImageDimensions } from "~/store/image" @@ -71,6 +72,7 @@ export const PictureWaterFallItem = memo(function PictureWaterFallItem({ const isActive = useRouteParamsSelector(({ entryId }) => entryId === entry?.id) const itemWidth = useMasonryItemWidth() + const isImageOnly = useUISettingKey("pictureViewImageOnly") const [ref, setRef] = useState(null) const intersectionObserver = use(MasonryIntersectionContext) @@ -111,10 +113,11 @@ export const PictureWaterFallItem = memo(function PictureWaterFallItem({ proxySize={proxySize} imgClassName="object-cover" /> - -
    - -
    + {!isImageOnly && ( +
    + +
    + )} ) : (
    @@ -135,6 +138,7 @@ const MasonryItemFixedDimensionWrapper = ( const { url, children } = props const dim = useImageDimensions(url) const itemWidth = useMasonryItemWidth() + const isImageOnly = useUISettingKey("pictureViewImageOnly") const stableRadio = useMemo(() => { return dim ? dim.ratio : 1 @@ -153,9 +157,9 @@ const MasonryItemFixedDimensionWrapper = ( const style = useMemo( () => ({ width: itemWidth, - height: itemWidth / finalRatio + 60, + height: itemWidth / finalRatio + +(!isImageOnly ? 60 : 0), }), - [itemWidth, finalRatio], + [itemWidth, finalRatio, isImageOnly], ) if (!style.height || style.height === Infinity) return null diff --git a/apps/desktop/layer/renderer/src/modules/entry-column/Items/picture-item.legacy.tsx b/apps/desktop/layer/renderer/src/modules/entry-column/Items/picture-item.legacy.tsx index f04de3ba0..003e8070c 100644 --- a/apps/desktop/layer/renderer/src/modules/entry-column/Items/picture-item.legacy.tsx +++ b/apps/desktop/layer/renderer/src/modules/entry-column/Items/picture-item.legacy.tsx @@ -11,6 +11,7 @@ import type { PropsWithChildren } from "react" import { memo, use, useEffect, useMemo, useState } from "react" import { useTranslation } from "react-i18next" +import { useUISettingKey } from "~/atoms/settings/ui" import { SwipeMedia } from "~/components/ui/media/SwipeMedia" import { useRouteParamsSelector } from "~/hooks/biz/useRouteParams" import { EntryContent } from "~/modules/entry-content/components/entry-content" @@ -74,6 +75,7 @@ export const PictureWaterFallItem = memo(function PictureWaterFallItem({ })) const isActive = useRouteParamsSelector(({ entryId }) => entryId === entry?.id) + const isImageOnly = useUISettingKey("pictureViewImageOnly") const entryContent = useMemo(() => , [entryId]) const previewMedia = usePreviewMedia(entryContent) const itemWidth = useMasonryItemWidth() @@ -119,9 +121,11 @@ export const PictureWaterFallItem = memo(function PictureWaterFallItem({ onPreview={previewMedia} /> -
    - -
    + {!isImageOnly && ( +
    + +
    + )} ) : (
    @@ -142,6 +146,7 @@ const MasonryItemFixedDimensionWrapper = ( const { url, children } = props const dim = useImageDimensions(url) const itemWidth = useMasonryItemWidth() + const isImageOnly = useUISettingKey("pictureViewImageOnly") const stableRadio = useMemo(() => { return dim ? dim.ratio : 1 @@ -160,9 +165,9 @@ const MasonryItemFixedDimensionWrapper = ( const style = useMemo( () => ({ width: itemWidth, - height: itemWidth / finalRatio + 60, + height: itemWidth / finalRatio + (!isImageOnly ? 60 : 0), }), - [itemWidth, finalRatio], + [itemWidth, finalRatio, isImageOnly], ) if (!style.height || style.height === Infinity) return null diff --git a/apps/desktop/layer/renderer/src/modules/entry-column/Items/picture-masonry.tsx b/apps/desktop/layer/renderer/src/modules/entry-column/Items/picture-masonry.tsx index cf72e2b8e..e0c226276 100644 --- a/apps/desktop/layer/renderer/src/modules/entry-column/Items/picture-masonry.tsx +++ b/apps/desktop/layer/renderer/src/modules/entry-column/Items/picture-masonry.tsx @@ -1,4 +1,5 @@ import { + MasonryForceRerenderContext, MasonryIntersectionContext, MasonryItemsAspectRatioContext, MasonryItemsAspectRatioSetterContext, @@ -30,6 +31,7 @@ import { import { useEventCallback } from "usehooks-ts" import { useActionLanguage, useGeneralSettingKey } from "~/atoms/settings/general" +import { useUISettingKey } from "~/atoms/settings/ui" import { MediaContainerWidthProvider } from "~/components/ui/media/MediaContainerWidthProvider" import type { StoreImageType } from "~/store/image" import { imageActions } from "~/store/image" @@ -221,6 +223,12 @@ export const PictureMasonry: FC = (props) => { } }, []) + const isImageOnly = useUISettingKey("pictureViewImageOnly") + const [masonryForceRerender, setMasonrtForceRerender] = useState(0) + useEffect(() => { + setMasonrtForceRerender((i) => i + 1) + }, [isImageOnly, setMasonrtForceRerender]) + return (
    {isInitDim && deferIsInitLayout && ( @@ -229,29 +237,31 @@ export const PictureMasonry: FC = (props) => { - - - - {props.Footer ? ( - typeof props.Footer === "function" ? ( -
    - -
    - ) : ( -
    {props.Footer}
    - ) - ) : null} -
    -
    + + + + + {props.Footer ? ( + typeof props.Footer === "function" ? ( +
    + +
    + ) : ( +
    {props.Footer}
    + ) + ) : null} +
    +
    +
    diff --git a/apps/desktop/layer/renderer/src/modules/entry-column/grid.tsx b/apps/desktop/layer/renderer/src/modules/entry-column/grid.tsx index f775802b8..664d8d117 100644 --- a/apps/desktop/layer/renderer/src/modules/entry-column/grid.tsx +++ b/apps/desktop/layer/renderer/src/modules/entry-column/grid.tsx @@ -115,6 +115,8 @@ const VirtualGridImpl: FC< return Array.from({ length: columnCount }).fill(width / columnCount) as number[] }, [containerWidth]) + const isImageOnly = useUISettingKey("pictureViewImageOnly") + // Calculate rows based on entries const rows = useMemo(() => { const itemsPerRow = columns.length @@ -149,7 +151,7 @@ const VirtualGridImpl: FC< const rowVirtualizer = useVirtualizer({ count: rows.length + (hasNextPage ? 1 : 0) + (Footer ? 1 : 0), estimateSize: () => { - return columns[0]! / ratioMap[view] + 58 + return columns[0]! / ratioMap[view] + (!isImageOnly ? 58 : 0) }, overscan: 5, gap: 8, @@ -189,7 +191,7 @@ const VirtualGridImpl: FC< columnVirtualizer.measure() } measureRef.current?.() - }, [columnVirtualizer, measureRef, rowVirtualizer]) + }, [columnVirtualizer, measureRef, rowVirtualizer, isImageOnly]) const virtualItems = rowVirtualizer.getVirtualItems() useEffect(() => { diff --git a/apps/desktop/layer/renderer/src/modules/entry-column/layouts/buttons/SwitchToMasonryButton.tsx b/apps/desktop/layer/renderer/src/modules/entry-column/layouts/buttons/SwitchToMasonryButton.tsx index b568e0f88..08234d8fe 100644 --- a/apps/desktop/layer/renderer/src/modules/entry-column/layouts/buttons/SwitchToMasonryButton.tsx +++ b/apps/desktop/layer/renderer/src/modules/entry-column/layouts/buttons/SwitchToMasonryButton.tsx @@ -2,6 +2,7 @@ import { useMobile } from "@follow/components/hooks/useMobile.js" import { AutoResizeHeight } from "@follow/components/ui/auto-resize-height/index.js" import { ActionButton } from "@follow/components/ui/button/index.js" import { SegmentGroup, SegmentItem } from "@follow/components/ui/segment/index.js" +import { Switch } from "@follow/components/ui/switch/index.js" import { clsx, cn } from "@follow/utils/utils" import { HoverCard, @@ -15,6 +16,7 @@ import { setUISetting, useUISettingKey } from "~/atoms/settings/ui" export const SwitchToMasonryButton = () => { const isMasonry = useUISettingKey("pictureViewMasonry") + const isImageOnly = useUISettingKey("pictureViewImageOnly") const { t } = useTranslation() const isMobile = useMobile() @@ -75,6 +77,17 @@ export const SwitchToMasonryButton = () => { />
    +
    + + { + setUISetting("pictureViewImageOnly", checked) + }} + /> +
    diff --git a/apps/desktop/layer/renderer/src/modules/entry-column/templates/grid-item-template.tsx b/apps/desktop/layer/renderer/src/modules/entry-column/templates/grid-item-template.tsx index c96b73e6a..0df4cb666 100644 --- a/apps/desktop/layer/renderer/src/modules/entry-column/templates/grid-item-template.tsx +++ b/apps/desktop/layer/renderer/src/modules/entry-column/templates/grid-item-template.tsx @@ -6,6 +6,7 @@ import { cn } from "@follow/utils/utils" import dayjs from "dayjs" import { useTranslation } from "react-i18next" +import { useUISettingKey } from "~/atoms/settings/ui" import { useEntryIsRead } from "~/hooks/biz/useAsRead" import { EntryTranslation } from "~/modules/entry-column/translation" import type { FeedIconEntry } from "~/modules/feed/feed-icon" @@ -74,6 +75,9 @@ export const GridItemFooter = ({ const { t } = useTranslation("common") + const isImageOnly = useUISettingKey("pictureViewImageOnly") + if (isImageOnly) return null + if (!entry) return null return (
    diff --git a/locales/app/en.json b/locales/app/en.json index ff9c0c58e..ad20b4d3d 100644 --- a/locales/app/en.json +++ b/locales/app/en.json @@ -174,6 +174,7 @@ "entry_content.web_app_notice": "Maybe web app doesn't support this content type. But you can download the desktop app.", "entry_list.zero_unread": "Zero Unread", "entry_list_header.grid": "Grid", + "entry_list_header.image_only": "Image Only", "entry_list_header.items": "items", "entry_list_header.masonry": "Masonry", "entry_list_header.masonry_column": "Masonry Column", diff --git a/locales/app/ja.json b/locales/app/ja.json index 315a42b77..e3f047a87 100644 --- a/locales/app/ja.json +++ b/locales/app/ja.json @@ -172,6 +172,7 @@ "entry_content.web_app_notice": "ใ“ใฎใ‚ณใƒณใƒ†ใƒณใƒ„ใ‚ฟใ‚คใƒ—ใฏใ‚ฆใ‚งใƒ–ใ‚ขใƒ—ใƒชใงใฏใ‚ตใƒใƒผใƒˆใ•ใ‚Œใฆใ„ใชใ„ใ‹ใ‚‚ใ—ใ‚Œใพใ›ใ‚“ใ€‚ใƒ‡ใ‚นใ‚ฏใƒˆใƒƒใƒ—ใ‚ขใƒ—ใƒชใ‚’ใƒ€ใ‚ฆใƒณใƒญใƒผใƒ‰ใ—ใฆใใ ใ•ใ„ใ€‚", "entry_list.zero_unread": "ๆœช่ชญใ‚ผใƒญ", "entry_list_header.grid": "ใ‚ฐใƒชใƒƒใƒ‰", + "entry_list_header.image_only": "ใ‚คใƒกใƒผใ‚ธใฎใฟ", "entry_list_header.items": "้ …็›ฎ", "entry_list_header.masonry": "ใƒกใ‚คใ‚ฝใƒณใƒชใƒผ", "entry_list_header.masonry_column": "ใƒกใ‚คใ‚ฝใƒณใƒชใƒผๅˆ—", diff --git a/locales/app/zh-CN.json b/locales/app/zh-CN.json index 2be9f3654..847596cc7 100644 --- a/locales/app/zh-CN.json +++ b/locales/app/zh-CN.json @@ -172,6 +172,7 @@ "entry_content.web_app_notice": "็ฝ‘้กต็‰ˆไธๆ”ฏๆŒๅฑ•็คบๆญค็ฑปๅž‹๏ผŒ่ฏทไธ‹่ฝฝๅฎขๆˆท็ซฏๆŸฅ็œ‹ใ€‚", "entry_list.zero_unread": "ๅ…จ้ƒจๅทฒ่ฏป", "entry_list_header.grid": "็ฝ‘ๆ ผๅธƒๅฑ€", + "entry_list_header.image_only": "ไป…ๅ›พ็‰‡", "entry_list_header.items": "ๅ†…ๅฎน", "entry_list_header.masonry": "็€‘ๅธƒๆตๅธƒๅฑ€", "entry_list_header.masonry_column": "ๅธƒๅฑ€ๅˆ—ๆ•ฐ", diff --git a/locales/app/zh-TW.json b/locales/app/zh-TW.json index 221090a65..cd1f0915a 100644 --- a/locales/app/zh-TW.json +++ b/locales/app/zh-TW.json @@ -157,6 +157,7 @@ "entry_content.web_app_notice": "Web ๆ‡‰็”จ็จ‹ๅผไธๆ”ฏๆดๆญค้กžๅž‹ๅ…งๅฎนใ€‚ๆ‚จๅฏไปฅไธ‹่ผ‰ๆกŒ้ขๆ‡‰็”จ็จ‹ๅผใ€‚", "entry_list.zero_unread": "ๅ…จ้ƒจๅทฒ่ฎ€", "entry_list_header.grid": "ๆ ผ็ทšไฝˆๅฑ€", + "entry_list_header.image_only": "ๅƒ…ๅœ–็‰‡", "entry_list_header.items": "ๅ…งๅฎน", "entry_list_header.masonry": "็€‘ๅธƒๅผไฝˆๅฑ€", "entry_list_header.masonry_column": "ไฝˆๅฑ€ๅˆ—ๆ•ธ", diff --git a/packages/internal/components/src/ui/masonry/contexts.tsx b/packages/internal/components/src/ui/masonry/contexts.tsx index 63e2780ab..156beed20 100644 --- a/packages/internal/components/src/ui/masonry/contexts.tsx +++ b/packages/internal/components/src/ui/masonry/contexts.tsx @@ -5,6 +5,10 @@ import { createContext as createContextSelector, useContextSelector } from "use- export const MasonryItemWidthContext = createContext(0) +export const MasonryForceRerenderContext = createContext(0) + +export const useMasonryForceRerender = () => use(MasonryForceRerenderContext) + export const useMasonryItemWidth = () => use(MasonryItemWidthContext) export const MasonryItemsAspectRatioContext = createContextSelector({} as Record) diff --git a/packages/internal/components/src/ui/masonry/index.tsx b/packages/internal/components/src/ui/masonry/index.tsx index 7ebc24fb0..eae422ee2 100644 --- a/packages/internal/components/src/ui/masonry/index.tsx +++ b/packages/internal/components/src/ui/masonry/index.tsx @@ -8,6 +8,7 @@ import { useForceUpdate } from "motion/react" import * as React from "react" import { useScrollViewElement } from "../scroll-area/hooks.js" +import { useMasonryForceRerender } from "./contexts.jsx" /** * A "batteries included" masonry grid which includes all of the implementation details below. This component is the * easiest way to get off and running in your app, before switching to more advanced implementations, if necessary. @@ -20,6 +21,7 @@ export const Masonry = (props: MasonryProps) => { const [scrollTop, setScrollTop] = React.useState(0) const [isScrolling, setIsScrolling] = React.useState(false) const scrollElement = useScrollViewElement() + const forceRerender = useMasonryForceRerender() const fps = props.scrollFps || 12 React.useEffect(() => { @@ -88,7 +90,7 @@ export const Masonry = (props: MasonryProps) => { itemCounter.current = props.items.length } - nextProps.positioner = usePositioner(nextProps, [shrunk && Math.random()]) + nextProps.positioner = usePositioner(nextProps, [shrunk && Math.random(), forceRerender]) nextProps.resizeObserver = useResizeObserver(nextProps.positioner) nextProps.scrollTop = scrollTop diff --git a/packages/internal/shared/src/settings/interface.ts b/packages/internal/shared/src/settings/interface.ts index 465884d05..51eadcdf9 100644 --- a/packages/internal/shared/src/settings/interface.ts +++ b/packages/internal/shared/src/settings/interface.ts @@ -79,6 +79,7 @@ export interface UISettings { // view pictureViewMasonry: boolean + pictureViewImageOnly: boolean wideMode: boolean contentFontSize: number dateFormat: string From 2a4cfc4069a7da7c195c3e337f48c45dd7676575 Mon Sep 17 00:00:00 2001 From: Innei Date: Tue, 7 Oct 2025 01:24:13 +0800 Subject: [PATCH 036/601] fix(pic-view): masonry button icon Signed-off-by: Innei --- .../layouts/buttons/SwitchToMasonryButton.tsx | 91 +++++++++---------- 1 file changed, 43 insertions(+), 48 deletions(-) diff --git a/apps/desktop/layer/renderer/src/modules/entry-column/layouts/buttons/SwitchToMasonryButton.tsx b/apps/desktop/layer/renderer/src/modules/entry-column/layouts/buttons/SwitchToMasonryButton.tsx index 08234d8fe..06b822423 100644 --- a/apps/desktop/layer/renderer/src/modules/entry-column/layouts/buttons/SwitchToMasonryButton.tsx +++ b/apps/desktop/layer/renderer/src/modules/entry-column/layouts/buttons/SwitchToMasonryButton.tsx @@ -1,5 +1,4 @@ import { useMobile } from "@follow/components/hooks/useMobile.js" -import { AutoResizeHeight } from "@follow/components/ui/auto-resize-height/index.js" import { ActionButton } from "@follow/components/ui/button/index.js" import { SegmentGroup, SegmentItem } from "@follow/components/ui/segment/index.js" import { Switch } from "@follow/components/ui/switch/index.js" @@ -39,57 +38,53 @@ export const SwitchToMasonryButton = () => { "data-[state=closed]:slide-out-to-top-5 data-[state=open]:slide-in-from-top-5", "data-[state=closed]:slide-in-from-top-0 data-[state=open]:slide-in-from-top-0", "transition-all duration-200 ease-in-out", - "px-3 py-2", + "p-3", )} > - -
    -
    - - { - setUISetting("pictureViewMasonry", v === "masonry") - }} - > - - - {t("entry_list_header.grid")} -
    - } - /> - - - {t("entry_list_header.masonry")} -
    - } - /> - -
    -
    - - { - setUISetting("pictureViewImageOnly", checked) - }} +
    +
    + + { + setUISetting("pictureViewMasonry", v === "masonry") + }} + > + + + {t("entry_list_header.grid")} +
    + } /> -
    + + + {t("entry_list_header.masonry")} +
    + } + /> +
    - +
    + + { + setUISetting("pictureViewImageOnly", checked) + }} + /> +
    +
    From 160e136f0c2c6ea610db37272cac449c338b366b Mon Sep 17 00:00:00 2001 From: Innei Date: Tue, 7 Oct 2025 13:28:38 +0800 Subject: [PATCH 037/601] fix(chat): refine gradient mask and backdrop styling in ChatInterface - Updated the gradient mask and backdrop filter in ChatInterface for improved visual effects. - Adjusted maskImage properties for better clarity and consistency in the UI. Signed-off-by: Innei --- .../modules/ai-chat/components/layouts/ChatInterface.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatInterface.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatInterface.tsx index 51c42b6a1..b71d5a871 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatInterface.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatInterface.tsx @@ -379,13 +379,11 @@ const ChatInterfaceContent = ({ centerInputOnEmpty }: ChatInterfaceProps) => { {(!centerInputOnEmpty || hasMessages) && (
    From 14c3d9589592488647385784ed3aa7c154c1cd36 Mon Sep 17 00:00:00 2001 From: DIYgod Date: Tue, 7 Oct 2025 17:07:04 +0800 Subject: [PATCH 038/601] fix(mention): update mention feed and date formatting to use self-closing tags --- .../modules/ai-chat/editor/plugins/mention/MentionNode.tsx | 4 ++-- .../ai-chat/editor/plugins/mention/hooks/dateMentionUtils.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/mention/MentionNode.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/mention/MentionNode.tsx index e0dbe2d75..c7fc512f4 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/mention/MentionNode.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/mention/MentionNode.tsx @@ -112,10 +112,10 @@ export class MentionNode extends DecoratorNode { ) { const { view } = getRouteParams() const ids = getCategoryFeedIds(value.slice(ROUTE_FEED_IN_FOLDER.length), view) - return `` + return `` } - return `` + return `` } override decorate(_editor: LexicalEditor): React.JSX.Element { diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/mention/hooks/dateMentionUtils.ts b/apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/mention/hooks/dateMentionUtils.ts index 6ebe4657c..47119d5b9 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/mention/hooks/dateMentionUtils.ts +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/mention/hooks/dateMentionUtils.ts @@ -42,7 +42,7 @@ export const formatRangeValue = (range: DateRange): string => { const endExclusive = range.end.add(1, "day").startOf("day") const endIso = endExclusive.format(MENTION_DATE_VALUE_FORMAT) - return `` + return `` } const DEFAULT_DATE_FORMAT: Intl.DateTimeFormatOptions = { From f44a4c4524555abdb7719d3a9a15aa30e2b302bf Mon Sep 17 00:00:00 2001 From: Innei Date: Tue, 7 Oct 2025 22:13:40 +0800 Subject: [PATCH 039/601] fix(timeline): adjust layout for AIEnhancedTimeline to ensure minimum width - Updated the AIEnhancedTimelineLayout component to set a minimum width of 300px for better layout consistency. - Enhanced the flex properties to improve responsiveness and visual structure. Signed-off-by: Innei --- .../AIEnhancedTimelineLayout.tsx | 7 ++++- .../entry-content/actions/more-actions.tsx | 26 ++++++++++++++++--- .../components/entry-header/AIEntryHeader.tsx | 26 +++++++++++++++---- .../internal/EntryHeaderActionsContainer.tsx | 9 ++++--- .../internal/EntryHeaderBreadcrumb.tsx | 10 +++---- 5 files changed, 59 insertions(+), 19 deletions(-) diff --git a/apps/desktop/layer/renderer/src/modules/app-layout/ai-enhanced-timeline/AIEnhancedTimelineLayout.tsx b/apps/desktop/layer/renderer/src/modules/app-layout/ai-enhanced-timeline/AIEnhancedTimelineLayout.tsx index 2ab04e8df..608f8d8c6 100644 --- a/apps/desktop/layer/renderer/src/modules/app-layout/ai-enhanced-timeline/AIEnhancedTimelineLayout.tsx +++ b/apps/desktop/layer/renderer/src/modules/app-layout/ai-enhanced-timeline/AIEnhancedTimelineLayout.tsx @@ -119,7 +119,12 @@ const AIEnhancedTimelineLayoutImpl = () => { const showCompactTimelineColumn = useGeneralSettingKey("showCompactTimelineInSub") return (
    -
    +
    {/* Entry list - always rendered to prevent animation */} diff --git a/apps/desktop/layer/renderer/src/modules/entry-content/actions/more-actions.tsx b/apps/desktop/layer/renderer/src/modules/entry-content/actions/more-actions.tsx index d7938243e..9312f2cc4 100644 --- a/apps/desktop/layer/renderer/src/modules/entry-content/actions/more-actions.tsx +++ b/apps/desktop/layer/renderer/src/modules/entry-content/actions/more-actions.tsx @@ -26,15 +26,15 @@ import type { FollowCommandId } from "~/modules/command/types" export const MoreActions = ({ entryId, view, - + showMainAction = false, hideCustomizeToolbar = false, }: { entryId: string view: FeedViewType - + showMainAction?: boolean hideCustomizeToolbar?: boolean }) => { - const { moreAction } = useSortedEntryActions({ entryId, view }) + const { moreAction, mainAction } = useSortedEntryActions({ entryId, view }) const actionConfigs = useMemo( () => @@ -81,7 +81,25 @@ export const MoreActions = ({ } /> - + + {showMainAction && ( +
    + {mainAction + .filter((config) => config instanceof MenuItemText) + .map((config) => { + return ( + + ) + })} + +
    + )} + {availableActions.map((config) => { // Handle EntryActionI with sub-menu if (config instanceof EntryActionDropdownItem && config.hasChildren) { diff --git a/apps/desktop/layer/renderer/src/modules/entry-content/components/entry-header/AIEntryHeader.tsx b/apps/desktop/layer/renderer/src/modules/entry-content/components/entry-header/AIEntryHeader.tsx index db1271936..cfa6af6f4 100644 --- a/apps/desktop/layer/renderer/src/modules/entry-content/components/entry-header/AIEntryHeader.tsx +++ b/apps/desktop/layer/renderer/src/modules/entry-content/components/entry-header/AIEntryHeader.tsx @@ -1,4 +1,4 @@ -import { memo } from "react" +import { memo, useLayoutEffect, useRef, useState } from "react" import { useEntryContentScrollToTop } from "../../atoms" import { EntryHeaderRoot } from "./internal/context" @@ -8,16 +8,32 @@ import type { EntryHeaderProps } from "./types" function EntryHeaderImpl({ entryId, className, compact }: EntryHeaderProps) { const isAtTop = useEntryContentScrollToTop() + const headerRef = useRef(null) + const [isSmallWidth, setIsSmallWidth] = useState(false) + useLayoutEffect(() => { + const $header = headerRef.current + if (!$header) return + const handler = () => setIsSmallWidth($header.clientWidth <= 500) + + const observer = new ResizeObserver(handler) + observer.observe($header) + handler() + + return () => { + observer.disconnect() + } + }, [headerRef]) return ( -
    - -
    + +
    ) } diff --git a/apps/desktop/layer/renderer/src/modules/entry-content/components/entry-header/internal/EntryHeaderActionsContainer.tsx b/apps/desktop/layer/renderer/src/modules/entry-content/components/entry-header/internal/EntryHeaderActionsContainer.tsx index 03f1e26c5..5f6972e33 100644 --- a/apps/desktop/layer/renderer/src/modules/entry-content/components/entry-header/internal/EntryHeaderActionsContainer.tsx +++ b/apps/desktop/layer/renderer/src/modules/entry-content/components/entry-header/internal/EntryHeaderActionsContainer.tsx @@ -1,3 +1,4 @@ +import clsx from "clsx" import { memo } from "react" import { useRouteParams } from "~/hooks/biz/useRouteParams" @@ -6,14 +7,14 @@ import { EntryHeaderActions } from "../../../actions/header-actions" import { MoreActions } from "../../../actions/more-actions" import { useEntryHeaderContext } from "./context" -function EntryHeaderActionsContainerImpl() { +function EntryHeaderActionsContainerImpl({ isSmallWidth }: { isSmallWidth: boolean }) { const { entryId } = useEntryHeaderContext() const { view } = useRouteParams() return ( -
    - - +
    + {!isSmallWidth && } +
    ) } diff --git a/apps/desktop/layer/renderer/src/modules/entry-content/components/entry-header/internal/EntryHeaderBreadcrumb.tsx b/apps/desktop/layer/renderer/src/modules/entry-content/components/entry-header/internal/EntryHeaderBreadcrumb.tsx index 60a8dba87..d6bdf72fe 100644 --- a/apps/desktop/layer/renderer/src/modules/entry-content/components/entry-header/internal/EntryHeaderBreadcrumb.tsx +++ b/apps/desktop/layer/renderer/src/modules/entry-content/components/entry-header/internal/EntryHeaderBreadcrumb.tsx @@ -246,7 +246,7 @@ export function EntryHeaderBreadcrumb() { {viewName && ( -
    +
    - {/* Context Bar - Always shown, positioned below the input area */} -
    -
    -
    - onSend(prompt, null)} + {/* Context Bar - only shown in non-onboarding scene, positioned below the input area */} + {scene !== "onboarding" && ( +
    +
    +
    + onSend(prompt, null)} + /> +
    +
    -
    -
    + )}
    ) }) diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatInterface.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatInterface.tsx index b71d5a871..02bdd2142 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatInterface.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatInterface.tsx @@ -412,7 +412,7 @@ export const ChatInterface = (props: ChatInterfaceProps) => ( ) -const Messages: FC<{ contentRef?: RefObject }> = ({ contentRef }) => { +export const Messages: FC<{ contentRef?: RefObject }> = ({ contentRef }) => { const messages = useMessages() const fallbackRef = useRef(null) const messageContainerWidth = useElementWidth(contentRef ?? fallbackRef) diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/UserChatMessage.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/UserChatMessage.tsx index c8d6488bc..db3cc5d2c 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/UserChatMessage.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/UserChatMessage.tsx @@ -5,7 +5,7 @@ import { AnimatePresence, m } from "motion/react" import * as React from "react" import { useEditingMessageId, useSetEditingMessageId } from "~/modules/ai-chat/atoms/session" -import { useChatActions, useChatStatus } from "~/modules/ai-chat/store/hooks" +import { useChatActions, useChatScene, useChatStatus } from "~/modules/ai-chat/store/hooks" import type { AIChatContextBlock, BizUIMessage } from "~/modules/ai-chat/store/types" import { convertLexicalToMarkdown } from "../../utils/lexical-markdown" @@ -91,11 +91,13 @@ export const UserChatMessage: React.FC = React.memo(({ mes chatActions.regenerate({ messageId }) }, [chatActions, messageId]) + const scene = useChatScene() + return (
    {/* Render data-block parts separately, outside the chat bubble */} - {dataBlockParts.length > 0 && ( + {dataBlockParts.length > 0 && scene !== "onboarding" && dataBlockParts.length > 0 && (
    {dataBlockParts.map((part) => { diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/store/chat-core/chat-actions.ts b/apps/desktop/layer/renderer/src/modules/ai-chat/store/chat-core/chat-actions.ts index 0d0a87b62..04fc40716 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/store/chat-core/chat-actions.ts +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/store/chat-core/chat-actions.ts @@ -1,5 +1,6 @@ import { autoBindThis } from "@follow/utils/bind-this" import type { ChatRequestOptions, ChatStatus } from "ai" +import { merge } from "es-toolkit/compat" import { nanoid } from "nanoid" import type { StateCreator } from "zustand" @@ -158,7 +159,13 @@ export class ChatSliceActions { : (message as Parameters[0]) // Use the AI SDK's sendMessage method - const response = await this.chatInstance.sendMessage(messageObj, options) + const finalOptions = merge( + { + body: { scene: this.get().scene }, + }, + options, + ) + const response = await this.chatInstance.sendMessage(messageObj, finalOptions) return response } catch (error) { this.setError(error as Error) @@ -276,4 +283,8 @@ export class ChatSliceActions { throw error } } + + setScene = (scene: ChatSlice["scene"]) => { + this.set((state) => ({ ...state, scene })) + } } diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/store/chat-core/types.ts b/apps/desktop/layer/renderer/src/modules/ai-chat/store/chat-core/types.ts index 3be582f45..2373a5582 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/store/chat-core/types.ts +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/store/chat-core/types.ts @@ -20,4 +20,7 @@ export interface ChatSlice { // Actions chatActions: ChatSliceActions + + // Scene + scene: "general" | "onboarding" | "timeline-summary" } diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/store/hooks.ts b/apps/desktop/layer/renderer/src/modules/ai-chat/store/hooks.ts index e48419ac8..ea94e86dd 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/store/hooks.ts +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/store/hooks.ts @@ -80,3 +80,11 @@ export const useChatError = () => { const store = useAIChatStore() return store((state) => state.error) } + +/** + * Hook to get the chat scene + */ +export const useChatScene = () => { + const store = useAIChatStore() + return store((state) => state.scene) +} diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/store/slices/chat.slice.ts b/apps/desktop/layer/renderer/src/modules/ai-chat/store/slices/chat.slice.ts index 54d0ca56a..aa352c766 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/store/slices/chat.slice.ts +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/store/slices/chat.slice.ts @@ -77,5 +77,6 @@ export const createChatSlice: (options: { currentTitle: undefined, chatInstance, chatActions, + scene: "general", } } diff --git a/apps/desktop/layer/renderer/src/modules/discover/DiscoverFeedCard.tsx b/apps/desktop/layer/renderer/src/modules/discover/DiscoverFeedCard.tsx index 741756910..a81bacd2d 100644 --- a/apps/desktop/layer/renderer/src/modules/discover/DiscoverFeedCard.tsx +++ b/apps/desktop/layer/renderer/src/modules/discover/DiscoverFeedCard.tsx @@ -166,7 +166,7 @@ export const DiscoverFeedCard: FC = memo( }, ) -const SearchResultContent: FC<{ +export const SearchResultContent: FC<{ entry: NonUndefined[number] }> = memo(({ entry }) => { const safeUrl = useFeedSafeUrl(entry.id) diff --git a/apps/desktop/layer/renderer/src/modules/new-user-guide/ai-chat-pane.tsx b/apps/desktop/layer/renderer/src/modules/new-user-guide/ai-chat-pane.tsx new file mode 100644 index 000000000..822cefd5d --- /dev/null +++ b/apps/desktop/layer/renderer/src/modules/new-user-guide/ai-chat-pane.tsx @@ -0,0 +1,526 @@ +import { Logo } from "@follow/components/icons/logo.jsx" +import { AIShortcutButton } from "@follow/components/ui/ai-shortcut-button/index.js" +import { Button } from "@follow/components/ui/button/index.js" +import type { LexicalRichEditorRef } from "@follow/components/ui/lexical-rich-editor/index.js" +import { ScrollArea } from "@follow/components/ui/scroll-area/ScrollArea.js" +import { tracker } from "@follow/tracker" +import { nextFrame } from "@follow/utils" +import { cn } from "@follow/utils/utils" +import { AnimatePresence } from "framer-motion" +import { useSetAtom } from "jotai" +import type { EditorState, LexicalEditor } from "lexical" +import { $getRoot, $getSelection, $isRangeSelection } from "lexical" +import { nanoid } from "nanoid" +import type { RefObject } from "react" +import { Fragment, useEffect, useLayoutEffect, useRef, useState } from "react" +import { useEventCallback } from "usehooks-ts" + +import { useI18n } from "~/hooks/common" +import { ChatInput } from "~/modules/ai-chat/components/layouts/ChatInput" +import { Messages } from "~/modules/ai-chat/components/layouts/ChatInterface" +import { useAttachScrollBeyond } from "~/modules/ai-chat/hooks/useAttachScrollBeyond" +import { useAutoScroll } from "~/modules/ai-chat/hooks/useAutoScroll" +import { + useBlockActions, + useChatActions, + useChatError, + useChatStatus, + useCurrentChatId, + useHasMessages, + useMessages, +} from "~/modules/ai-chat/store/hooks" +import type { AIChatContextBlock, BizUIMessage } from "~/modules/ai-chat/store/types" +import { convertLexicalToMarkdown } from "~/modules/ai-chat/utils/lexical-markdown" + +import { CollapsibleError } from "../ai-chat/components/layouts/CollapsibleError" +import { AIChatWaitingIndicator } from "../ai-chat/components/message/AIChatMessage" +import { stepAtom } from "./store" + +const SUGGESTION_KEYS = [ + "new_user_guide.ai_chat.suggestions.fashion_designer", + "new_user_guide.ai_chat.suggestions.nano_engineering_researcher", + "new_user_guide.ai_chat.suggestions.drug_delivery_student", + "new_user_guide.ai_chat.suggestions.investor_market_news", + "new_user_guide.ai_chat.suggestions.nasa_fan", + "new_user_guide.ai_chat.suggestions.climate_newsletter_writer", + "new_user_guide.ai_chat.suggestions.plant_based_cooking", + "new_user_guide.ai_chat.suggestions.cybersecurity_tracker", + "new_user_guide.ai_chat.suggestions.japan_trip_planner", + "new_user_guide.ai_chat.suggestions.podcast_summary_seeker", + "new_user_guide.ai_chat.suggestions.personal_finance_builder", + "new_user_guide.ai_chat.suggestions.robotics_coach", + "new_user_guide.ai_chat.suggestions.saas_marketing_manager", + "new_user_guide.ai_chat.suggestions.ai_regulation_learner", +] as const + +const SUGGESTION_SAMPLE_SIZE = 5 + +type SuggestionKey = (typeof SUGGESTION_KEYS)[number] + +function pickSuggestionKeys(previous?: readonly SuggestionKey[]): SuggestionKey[] { + const shuffle = (input: readonly SuggestionKey[]) => { + const pool = [...input] as SuggestionKey[] + for (let i = pool.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)) + ;[pool[i], pool[j]] = [pool[j]!, pool[i]!] + } + return pool + } + + if (!previous || previous.length === 0) { + return shuffle(SUGGESTION_KEYS).slice(0, SUGGESTION_SAMPLE_SIZE) + } + + const previousSet = new Set(previous) + const available = SUGGESTION_KEYS.filter((key) => !previousSet.has(key)) + + if (available.length >= SUGGESTION_SAMPLE_SIZE) { + return shuffle(available).slice(0, SUGGESTION_SAMPLE_SIZE) + } + + // When there aren't enough unique suggestions left, attempt to find a fully new batch. + const maxAttempts = 10 + for (let attempt = 0; attempt < maxAttempts; attempt++) { + const candidate = shuffle(SUGGESTION_KEYS).slice(0, SUGGESTION_SAMPLE_SIZE) + if (!candidate.some((key) => previousSet.has(key))) { + return candidate + } + } + + return shuffle(SUGGESTION_KEYS).slice(0, SUGGESTION_SAMPLE_SIZE) +} + +export function AIChatPane() { + return ( +
    + +
    + ) +} + +function AIChatPaneImpl() { + const t = useI18n() + + const setStep = useSetAtom(stepAtom) + + const hasMessages = useHasMessages() + const chatInputRef = useRef(null) + + const appendSuggestionToInput = (suggestion: string) => { + const ref = chatInputRef.current + const editor = ref?.getEditor() + + if (!editor) { + return + } + + editor.focus() + editor.update(() => { + const root = $getRoot() + const currentText = root.getTextContent() + const needsLeadingSpace = currentText.length > 0 && !currentText.endsWith(" ") + const textToInsert = needsLeadingSpace ? ` ${suggestion}` : suggestion + + root.selectEnd() + let selection = $getSelection() + + if (!$isRangeSelection(selection)) { + root.selectEnd() + selection = $getSelection() + } + + if ($isRangeSelection(selection)) { + selection.insertText(textToInsert) + } + }) + } + + return ( +
    +
    + + + +
    + + + {!hasMessages && } + + +
    + +
    +
    + ) +} + +interface WelcomeProps { + onSuggestionClick: (suggestion: string) => void +} + +function Welcome({ onSuggestionClick }: WelcomeProps) { + const t = useI18n() + const [suggestionKeys, setSuggestionKeys] = useState(() => pickSuggestionKeys()) + + const onClickSuggestion = useEventCallback((suggestion: string) => { + onSuggestionClick(suggestion) + }) + + const rerollSuggestions = useEventCallback(() => { + setSuggestionKeys((prev) => pickSuggestionKeys(prev)) + }) + + return ( +
    +
    +
    +

    + {(t.app("new_user_guide.ai_chat.intro") as string).split("\n").map((line) => ( + + {line} +
    +
    + ))} +

    +
    +
    + +
    +
    +

    + {t.app("new_user_guide.ai_chat.you_can_say")} +

    + +
    +
    + {suggestionKeys.map((suggestionKey, index) => { + const suggestionText = t.app(suggestionKey) as string + return ( + onClickSuggestion(suggestionText)} + animationDelay={index * 0.05} + className={cn( + "font-normal text-black shadow-[0_12px_30px_rgba(15,15,15,0.35)] hover:text-black", + gradientByIndex(index), + )} + > + {suggestionText} + + ) + })} +
    +
    + + +
    + ) +} + +// if the chat response has `tool-onboardingGetTrendingFeedsTool`, set the step to pre-finish +function FinishListener() { + const chatMessages = useMessages() + const setStep = useSetAtom(stepAtom) + useEffect(() => { + const hasCalledConfirmTool = chatMessages.some((msg) => + // @ts-expect-error TODO: fix this after version published + msg.parts.some((p) => p.type === "tool-onboardingGetTrendingFeeds"), + ) + if (hasCalledConfirmTool) { + setStep("pre-finish") + } + }, [chatMessages, setStep]) + + return null +} + +const SCROLL_BOTTOM_THRESHOLD = 100 + +interface AIChatInterfaceProps { + inputRef?: RefObject +} + +function AIChatInterface({ inputRef }: AIChatInterfaceProps) { + const hasMessages = useHasMessages() + const status = useChatStatus() + const chatActions = useChatActions() + const error = useChatError() + const t = useI18n() + + useEffect(() => { + if (error) { + console.error("AIChat Error:", error) + } + }, [error]) + + // on init, set the scene to onboarding + useEffect(() => { + chatActions.setScene("onboarding") + + return () => { + // reset the scene to general + chatActions.setScene("general") + } + }, [chatActions]) + + const currentChatId = useCurrentChatId() + + const [scrollAreaRef, setScrollAreaRef] = useState(null) + const [isAtBottom, setIsAtBottom] = useState(true) + const [messageContainerMinHeight, setMessageContainerMinHeight] = useState() + const previousMinHeightRef = useRef(0) + const messagesContentRef = useRef(null) + + useEffect(() => { + setIsAtBottom(true) + setMessageContainerMinHeight(undefined) + previousMinHeightRef.current = 0 + }, [currentChatId]) + + const { resetScrollState } = useAutoScroll(scrollAreaRef, status === "streaming") + + const { handleScroll } = useAttachScrollBeyond() + + useEffect(() => { + const scrollElement = scrollAreaRef + + if (!scrollElement) return + + const handleScroll = () => { + const { scrollTop, scrollHeight, clientHeight } = scrollElement + const distanceFromBottom = scrollHeight - scrollTop - clientHeight + const atBottom = distanceFromBottom <= SCROLL_BOTTOM_THRESHOLD + setIsAtBottom(atBottom) + } + + scrollElement.addEventListener("scroll", handleScroll, { passive: true }) + + handleScroll() + + return () => { + scrollElement.removeEventListener("scroll", handleScroll) + } + }, [scrollAreaRef]) + + const blockActions = useBlockActions() + + const scrollHeightBeforeSendingRef = useRef(0) + const scrollContainerParentRef = useRef(null) + const handleScrollPositioning = useEventCallback(() => { + const $scrollContainerParent = scrollContainerParentRef.current + if (!scrollAreaRef || !$scrollContainerParent) return + + const parentClientHeight = $scrollContainerParent.clientHeight + // Use actual content height captured before send (messages container height), not inflated by minHeight + const currentScrollHeight = scrollHeightBeforeSendingRef.current + + // Calculate new minimum height based on actual content height + // Use previousMinHeightRef which tracks the real content height, not reserved space + const baseHeight = Math.max(previousMinHeightRef.current, currentScrollHeight) + const newMinHeight = baseHeight + parentClientHeight - 250 + + setMessageContainerMinHeight(newMinHeight) + + // Scroll to the end immediately to position user message at top + nextFrame(() => { + scrollAreaRef.scrollTo({ + top: scrollAreaRef.scrollHeight, + behavior: "instant", + }) + }) + }) + + const handleSendMessage = useEventCallback( + (message: string | EditorState, editor: LexicalEditor | null) => { + resetScrollState() + + const blocks = [] as AIChatContextBlock[] + + for (const block of blockActions.getBlocks()) { + if (block.type === "fileAttachment" && block.attachment.serverUrl) { + blocks.push({ + ...block, + attachment: { + id: block.attachment.id, + name: block.attachment.name, + type: block.attachment.type, + size: block.attachment.size, + serverUrl: block.attachment.serverUrl, + }, + }) + } else { + blocks.push(block) + } + } + + const parts: BizUIMessage["parts"] = [ + { + type: "data-block", + data: blocks, + }, + ] + + if (typeof message === "string") { + parts.push({ + type: "text", + text: message, + }) + } else if (editor) { + parts.push({ + type: "data-rich-text", + data: { + state: JSON.stringify(message.toJSON()), + text: convertLexicalToMarkdown(editor), + }, + }) + } + + // Capture actual content height (messages container), not including reserved minHeight + scrollHeightBeforeSendingRef.current = messagesContentRef.current?.scrollHeight ?? 0 + chatActions.sendMessage({ + parts, + role: "user", + id: nanoid(), + }) + tracker.aiChatMessageSent() + + nextFrame(() => { + // Calculate and adjust scroll positioning immediately + handleScrollPositioning() + }) + }, + ) + + const [bottomPanelHeight, setBottomPanelHeight] = useState(0) + const bottomPanelRef = useRef(null) + + useLayoutEffect(() => { + if (!bottomPanelRef.current) { + return + } + setBottomPanelHeight(bottomPanelRef.current.offsetHeight) + + const resizeObserver = new ResizeObserver(() => { + if (!bottomPanelRef.current) { + return + } + setBottomPanelHeight(bottomPanelRef.current.offsetHeight) + }) + resizeObserver.observe(bottomPanelRef.current) + + return () => { + resizeObserver.disconnect() + } + }, []) + + useEffect(() => { + if (status === "submitted") { + resetScrollState() + } + + // When AI response is complete, update the reference height but keep the container height unchanged + // This avoids CLS while ensuring next calculation is based on actual content + if (status === "ready" && scrollAreaRef && messagesContentRef.current) { + // Update the reference to actual content height for next calculation (use messages container) + previousMinHeightRef.current = messagesContentRef.current.scrollHeight + // Keep the current minHeight unchanged to avoid CLS + } + }, [status, resetScrollState, messageContainerMinHeight, scrollAreaRef]) + + const shouldShowScrollToBottom = hasMessages && !isAtBottom + + const messages = useMessages() + const setStep = useSetAtom(stepAtom) + + return ( +
    + +
    + } /> + + {/* if the last message is from ai, show "Next Step" button */} + {messages.length > 0 && messages.at(-1)?.role === "assistant" && status === "ready" && ( +
    + +
    + )} + + {(status === "submitted" || status === "streaming") && } +
    +
    + + {shouldShowScrollToBottom && ( +
    + +
    + )} + +
    + {error && } + +
    +
    + ) +} + +const GRADIENT_CLASSES = [ + "bg-gradient-to-r from-[#ff8a00] to-[#ff5f2d]", + "bg-gradient-to-r from-[#0ccb8b] to-[#00a36c]", + "bg-gradient-to-r from-[#ffd700] to-[#ffa800]", + "bg-gradient-to-r from-[#9b5df5] to-[#7848ff]", + "bg-gradient-to-r from-[#ff5dc5] to-[#7a5cff]", +] + +function gradientByIndex(index: number) { + return GRADIENT_CLASSES[index % GRADIENT_CLASSES.length] +} diff --git a/apps/desktop/layer/renderer/src/modules/new-user-guide/discover-import-step.tsx b/apps/desktop/layer/renderer/src/modules/new-user-guide/discover-import-step.tsx new file mode 100644 index 000000000..2276486e0 --- /dev/null +++ b/apps/desktop/layer/renderer/src/modules/new-user-guide/discover-import-step.tsx @@ -0,0 +1,25 @@ +import { Button } from "@follow/components/ui/button/index.js" +import { useSetAtom } from "jotai" + +import { useI18n } from "~/hooks/common" + +import { DiscoverImport } from "../discover/DiscoverImport" +import { stepAtom } from "./store" + +export function DiscoverImportStep() { + const t = useI18n() + const setStep = useSetAtom(stepAtom) + return ( +
    + + +
    + + + +
    +
    + ) +} diff --git a/apps/desktop/layer/renderer/src/modules/new-user-guide/feeds-selection-list.tsx b/apps/desktop/layer/renderer/src/modules/new-user-guide/feeds-selection-list.tsx new file mode 100644 index 000000000..47a448a1d --- /dev/null +++ b/apps/desktop/layer/renderer/src/modules/new-user-guide/feeds-selection-list.tsx @@ -0,0 +1,207 @@ +import { + Card, + CardContent, + CardDescription, + CardHeader, +} from "@follow/components/ui/card/index.jsx" +import { ScrollArea } from "@follow/components/ui/scroll-area/ScrollArea.js" +import { Tooltip, TooltipContent, TooltipTrigger } from "@follow/components/ui/tooltip/index.js" +import { cn } from "@follow/utils/utils" +import type { PrimitiveAtom } from "jotai" +import { useAtom, useAtomValue, useSetAtom, useStore } from "jotai" +import { AnimatePresence, m } from "motion/react" +import { useEffect, useMemo, useRef } from "react" + +import { useI18n } from "~/hooks/common" + +import { useMessages } from "../ai-chat/store/hooks" +import { SearchResultContent } from "../discover/DiscoverFeedCard" +import { FeedIcon } from "../feed/feed-icon" +import type { FeedSelection } from "./store" +import { feedSelectionAtomsAtom, selectedFeedSelectionAtomsAtom } from "./store" + +type FeedToSelect = Omit + +export function FeedsSelectionList() { + const chatMessages = useMessages() + + const hasFeedsSelection = chatMessages.some((msg) => + // @ts-expect-error TODO: fix this after version published + msg.parts.some((p) => p.type === "tool-onboardingGetTrendingFeeds" && p.output), + ) + + return ( +
    + + {hasFeedsSelection ? : } + +
    + ) +} + +function FeedSelectionOperationScreen() { + const chatMessages = useMessages() + + const feedsToSelect: FeedToSelect[] = useMemo( + () => + // find the last message that has the tool + chatMessages + .findLast((m) => + // @ts-expect-error TODO: fix this after version published + m.parts?.some((p) => p.type === "tool-onboardingGetTrendingFeeds"), + ) + // @ts-expect-error TODO: fix this after version published + ?.parts?.findLast((p) => p.type === "tool-onboardingGetTrendingFeeds")?.output ?? [], + [chatMessages], + ) + + const store = useStore() + const atomList = useAtomValue(feedSelectionAtomsAtom) + const dispatch = useSetAtom(feedSelectionAtomsAtom) + + const lastKeyRef = useRef(null) + + const outputKey = useMemo(() => { + const ids = Array.from(new Set(feedsToSelect.map((f) => String(f.id)))) + ids.sort() + return ids.join("|") + }, [feedsToSelect]) + + const existingIds = useMemo( + () => new Set(atomList.map((a) => String(store.get(a).id))), + [atomList, store], + ) + + useEffect(() => { + if (lastKeyRef.current === outputKey) return + lastKeyRef.current = outputKey + + const seen = new Set(existingIds) + + for (const feed of feedsToSelect) { + const id = String(feed.id) + if (seen.has(id)) continue + seen.add(id) + + dispatch({ + type: "insert", + value: { ...feed, selected: true }, + }) + } + }, [dispatch, feedsToSelect, existingIds, outputKey]) + + const selectedAtoms = useAtomValue(selectedFeedSelectionAtomsAtom) + const items = useMemo( + () => selectedAtoms.map((atom) => ({ atom, id: store.get(atom).id })), + [selectedAtoms, store], + ) + + return ( + +
    + + {items.map(({ atom, id }) => ( + + + + ))} + +
    +
    + ) +} + +function FeedSelectionItem({ feedAtom }: { feedAtom: PrimitiveAtom }) { + const [feed, setFeed] = useAtom(feedAtom) + + const onRemove = () => { + setFeed((prev) => ({ + ...prev, + selected: false, + })) + } + + return ( +
    + {/* remove button */} + + + + + Remove + + + + +
    + +
    +

    {feed.title}

    +

    {feed.url}

    +
    +
    +
    + + + + {feed.description} + + +
    + {feed.entries?.map((entry) => ( + + ))} +
    +
    +
    +
    + ) +} + +function FeedSelectionFirstScreen() { + const t = useI18n() + + return ( +