diff --git a/apps/desktop/layer/renderer/src/hooks/biz/useTimelineList.ts b/apps/desktop/layer/renderer/src/hooks/biz/useTimelineList.ts index 6437d5adf..61bb8bf5c 100644 --- a/apps/desktop/layer/renderer/src/hooks/biz/useTimelineList.ts +++ b/apps/desktop/layer/renderer/src/hooks/biz/useTimelineList.ts @@ -1,10 +1,22 @@ +import { FeedViewType } from "@follow/constants" import { useViewWithSubscription } from "@follow/store/subscription/hooks" import { useMemo } from "react" +import { useFeature } from "~/hooks/biz/useFeature" + export const useTimelineList = () => { const views = useViewWithSubscription() - const viewsIds = useMemo(() => views.map((view) => `view-${view}`), [views]) + // because the All view is highly tied to the AI + // so we need to filter it out if the AI is not enabled + const aiEnabled = useFeature("ai") + + const filteredViews = useMemo( + () => (aiEnabled ? views : views.filter((v) => v !== FeedViewType.All)), + [views, aiEnabled], + ) + + const viewsIds = useMemo(() => filteredViews.map((view) => `view-${view}`), [filteredViews]) return viewsIds } diff --git a/apps/desktop/layer/renderer/src/modules/entry-column/Items/all-item.tsx b/apps/desktop/layer/renderer/src/modules/entry-column/Items/all-item.tsx new file mode 100644 index 000000000..de308c1db --- /dev/null +++ b/apps/desktop/layer/renderer/src/modules/entry-column/Items/all-item.tsx @@ -0,0 +1,491 @@ +import { views } from "@follow/constants" +import { IN_ELECTRON } from "@follow/shared/constants" +import { useIsEntryStarred } from "@follow/store/collection/hooks" +import { useEntry } from "@follow/store/entry/hooks" +import { useEntryStore } from "@follow/store/entry/store" +import { useFeedById } from "@follow/store/feed/hooks" +import { cn, formatDuration, transformVideoUrl } from "@follow/utils" +import { FeedViewType } from "@follow-app/client-sdk" +import { useHover } from "@use-gesture/react" +import dayjs from "dayjs" +import { useCallback, useMemo, useRef, useState } from "react" +import { useTranslation } from "react-i18next" + +import { usePreviewMedia } from "~/components/ui/media/hooks" +import { Media } from "~/components/ui/media/Media" +import { SwipeMedia } from "~/components/ui/media/SwipeMedia" +import { useEntryIsRead } from "~/hooks/biz/useAsRead" +import { useRouteParamsSelector } from "~/hooks/biz/useRouteParams" +import { EntryContent } from "~/modules/entry-content/components/entry-content" +import type { FeedIconEntry } from "~/modules/feed/feed-icon" +import { FeedIcon } from "~/modules/feed/feed-icon" +import { FeedTitle } from "~/modules/feed/feed-title" + +import { StarIcon } from "../star-icon" +import { EntryTranslation } from "../translation" +import type { EntryListItemFC } from "../types" + +const cardStylePresets = [ + { + card: "bg-[#7C6E63] text-white", + icon: "text-[#B7ADA4]", + }, + { + card: "bg-[#D7FED3] text-black", + icon: "text-[#92E58A]", + }, + { + card: "bg-[#FDFFDA] text-black", + icon: "text-[#F9EFAA]", + }, + { + card: "bg-[#FFE5EE] text-black", + icon: "text-[#FDD1E2]", + }, + { + card: "bg-[#CFF0FF] text-black", + icon: "text-[#A7D6F2]", + }, + { + card: "bg-[#ECE7FB] text-black", + icon: "text-[#DFCFF0]", + }, +] + +const highlightStyle = [ + { + type: "underline", + className: "underline decoration-blue-500 decoration-4 underline-offset-1", + }, + { + type: "underline", + className: "underline decoration-yellow-500 decoration-4 underline-offset-1", + }, + { + type: "underline", + className: "underline decoration-green-500 decoration-4 underline-offset-1", + }, + { + type: "underline", + className: "underline decoration-orange-500 decoration-4 underline-offset-1", + }, + // { + // type: "underline-wavy", + // className: + // "underline decoration-blue-500 decoration-4 underline-offset-1 decoration-wavy", + // }, + // { + // type: "underline-wavy", + // className: + // "underline decoration-yellow-500 decoration-4 underline-offset-1 decoration-wavy", + // }, + // { + // type: "underline-wavy", + // className: + // "underline decoration-green-500 decoration-4 underline-offset-1 decoration-wavy", + // }, + // { + // type: "underline-wavy", + // className: + // "underline decoration-orange-500 decoration-4 underline-offset-1 decoration-wavy", + // }, + { + type: "full", + className: "bg-blue-200", + }, + { + type: "full", + className: "bg-yellow-200", + }, + { + type: "full", + className: "bg-green-200", + }, + { + type: "full", + className: "bg-orange-200", + }, +] + +const ViewTag = IN_ELECTRON ? "webview" : "iframe" + +export const AllItem: EntryListItemFC = ({ entryId, entryPreview, translation }) => { + const view = useViewTypeByEntryId(entryId) + const entry = useEntry(entryId, (state) => { + /// keep-sorted + const { + attachments, + authorAvatar, + content, + description, + extra, + feedId, + id, + publishedAt, + read, + title, + url, + } = state + + const media = state.media || [] + const photo = media.find((a) => a.type === "photo") + const firstPhotoUrl = photo?.url + const iconEntry: FeedIconEntry = { + firstPhotoUrl, + authorAvatar, + } + + const { duration_in_seconds } = + attachments?.find((attachment) => attachment.duration_in_seconds) ?? {} + const seconds = duration_in_seconds + ? Number.parseInt(duration_in_seconds.toString()) + : undefined + const duration = formatDuration(seconds) + + return { + attachments, + duration, + content, + extra, + feedId, + iconEntry, + id, + media, + publishedAt, + read, + title, + description, + url, + } + }) + + const isInCollection = useIsEntryStarred(entryId) + + const feeds = useFeedById(entry?.feedId) + + const asRead = useEntryIsRead(entry) + + const { t } = useTranslation("common") + + const icon = useMemo(() => views.find((v) => v.view === view)?.icon, [view]) + + const entryMedia = useMemo( + () => entry?.media || entryPreview?.entries?.media || [], + [entry, entryPreview], + ) + + const randomStyle = useMemo(() => { + // Use a hash of entryId to get a consistent index for card style + // djb2 hash + let hash = 5381 + for (let i = 0, len = entryId.length; i < len; ++i) { + hash = (hash << 5) + hash + entryId.codePointAt(i)! + } + + const hashShift = (hash >>> 0) + 1 + + const cardIndex = hashShift % cardStylePresets.length + const highlightIndex = hashShift % highlightStyle.length + + return { + card: cardStylePresets[cardIndex]!, + highlight: highlightStyle[highlightIndex]!, + } + }, [entryId]) + + const isActive = useRouteParamsSelector(({ entryId }) => entryId === entry?.id) + + const entryContent = useMemo(() => , [entryId]) + const previewMedia = usePreviewMedia(entryContent) + + const [miniIframeSrc] = useMemo( + () => [ + transformVideoUrl({ + url: entry?.url ?? "", + mini: true, + isIframe: !IN_ELECTRON, + attachments: entry?.attachments, + }), + transformVideoUrl({ + url: entry?.url ?? "", + isIframe: !IN_ELECTRON, + attachments: entry?.attachments, + }), + ], + [entry?.attachments, entry?.url], + ) + + const ref = useRef(null) + const hoverTimerRef = useRef | null>(null) + const [showPreview, setShowPreview] = useState(false) + useHover( + (event) => { + const hovered = event.active + if (hovered) { + if (hoverTimerRef.current) { + clearTimeout(hoverTimerRef.current) + } + hoverTimerRef.current = setTimeout(() => { + setShowPreview(true) + }, 500) + } else { + setShowPreview(false) + if (hoverTimerRef.current) { + clearTimeout(hoverTimerRef.current) + hoverTimerRef.current = null + } + } + }, + { target: ref }, + ) + + const title = entry?.title || entry?.description || entry?.content + const titleKeyword = entry?.extra?.title_keyword?.toLowerCase().trim() || "" + + const titleWithKeyword = useMemo(() => { + if (!title || !titleKeyword) return title + + const regex = new RegExp(`(${titleKeyword.replaceAll(/[.*+?^${}()|[\]\\]/g, "\\$&")})`, "gi") + + const renderTitle = ({ type }: { type: "highlight" | "normal" }) => ( + <> + {title.split(regex).map((part, index) => { + // Check if this part matches the keyword (case-insensitive) + const normalizedPart = part.toLowerCase().trim() + const normalizedKeyword = titleKeyword.trim() + const isKeyword = normalizedPart === normalizedKeyword && part.trim() !== "" + + return isKeyword ? ( + + {part} + + ) : ( + + {part} + + ) + })} + + ) + + return ( +
+ {renderTitle({ type: "normal" })} +
{renderTitle({ type: "highlight" })}
+
+ ) + }, [randomStyle.highlight.className, titleKeyword, title]) + + if (!entry) return null + + const mediaCover = entryMedia?.[0] ?? null + + const mediaCoverHeight = mediaCover?.height + const mediaCoverWidth = mediaCover?.width + + const aspectRatio = + mediaCoverHeight && mediaCoverWidth ? mediaCoverWidth / mediaCoverHeight : undefined + + return ( +
+ {/* Hero */} +
+ {/* Icon */} + {!mediaCover && ( +
+ {icon} +
+ )} + + {/* Common views */} + {(view === FeedViewType.Articles || + view === FeedViewType.Notifications || + view === FeedViewType.SocialMedia || + view === FeedViewType.Audios) && ( + <> + {mediaCover ? ( + + ) : ( +
+
{titleWithKeyword}
+
+ )} + + )} + + {/* Pictures */} + {view === FeedViewType.Pictures && ( +
+ {entryMedia ? ( + + ) : ( +
+
{titleWithKeyword}
+
+ )} +
+ )} + + {/* Videos */} + {view === FeedViewType.Videos && ( +
+
+ {mediaCover ? ( + + ) : ( +
+
{titleWithKeyword}
+
+ )} + {miniIframeSrc && showPreview && ( +
+ +
+ )} + {!!entry.duration && ( +
+ {entry.duration} +
+ )} +
+
+ )} +
+ + {/* Footer */} +
+
+
+
+ + + {isInCollection && ( +
+ +
+ )} +
+
+
+
+ + + + +
+ + + {dayjs.duration(dayjs(entry.publishedAt).diff(dayjs(), "minute"), "minute").humanize()} + {t("space")} + {t("words.ago")} + +
+
+
+ ) +} + +AllItem.wrapperClassName = "hover:bg-transparent" + +// function AllArticleItem({ entryId, entryPreview, translation }: UniversalItemProps) { +// return +// } + +// Determine the most appropriate view type for an entry +function useViewTypeByEntryId(entryId: string): FeedViewType { + return useEntryStore( + useCallback( + (state) => { + const certain = Object.entries(state.entryIdByView).find(([_, entryIds]) => + entryIds.has(entryId), + )?.[0] as FeedViewType | undefined + const fallback = FeedViewType.Articles + return Number(certain ?? fallback) + }, + [entryId], + ), + ) +} diff --git a/apps/desktop/layer/renderer/src/modules/entry-column/Items/all-masonry.tsx b/apps/desktop/layer/renderer/src/modules/entry-column/Items/all-masonry.tsx new file mode 100644 index 000000000..d9c7839dc --- /dev/null +++ b/apps/desktop/layer/renderer/src/modules/entry-column/Items/all-masonry.tsx @@ -0,0 +1,365 @@ +import { Masonry } from "@follow/components/ui/masonry/index.js" +import { useScrollViewElement } from "@follow/components/ui/scroll-area/hooks.js" +import { Skeleton } from "@follow/components/ui/skeleton/index.jsx" +import { FeedViewType } from "@follow/constants" +import { useRefValue } from "@follow/hooks" +import { getEntry } from "@follow/store/entry/getter" +import { clsx } from "@follow/utils/utils" +import { ErrorBoundary } from "@sentry/react" +import type { RenderComponentProps } from "masonic" +import { useInfiniteLoader } from "masonic" +import type { FC, ReactNode } from "react" +import { startTransition, useCallback, useEffect, useMemo, useRef, useState } from "react" + +import { useGeneralSettingKey } from "~/atoms/settings/general" + +import { EntryColumnShortcutHandler } from "../EntryColumnShortcutHandler" +import { batchMarkRead } from "../hooks/useEntryMarkReadHandler" +import { EntryItem } from "../item" + +interface AllMasonryProps { + data: string[] + hasNextPage: boolean + endReached: () => void + Footer?: FC | ReactNode + refetch: () => void +} + +const GUTTER = 16 +const COLUMN_WIDTH = 230 +const OVERSCAN = 2 + +interface MasonryItem { + entryId: string +} + +export const AllMasonry: FC = ({ + data, + hasNextPage, + endReached, + Footer, + refetch, +}) => { + const scrollElement = useScrollViewElement() + const [containerRef, setContainerRef] = useState(null) + const [width, setWidth] = useState(0) + const [isLoadingMore, setIsLoadingMore] = useState(false) + const prevDataLengthRef = useRef(data.length) + + // Convert entry IDs to masonry items with stable references + const items = useMemo( + () => data.filter(Boolean).map((entryId) => ({ entryId })), + [data], + ) + + // Handle loading state when new data arrives + useEffect(() => { + if (data.length > prevDataLengthRef.current) { + setIsLoadingMore(false) + } + prevDataLengthRef.current = data.length + }, [data.length]) + + // Force remount when errors happen + const [forceRemountCounter, setForceRemountCounter] = useState(0) + const masonryKey = useMemo(() => `masonry-${forceRemountCounter}`, [forceRemountCounter]) + + // Handle container resize + useEffect(() => { + if (!containerRef) return + + const resizeObserver = new ResizeObserver((entries) => { + const [first] = entries + if (first) { + startTransition(() => { + setWidth(first.contentRect.width) + }) + } + }) + + resizeObserver.observe(containerRef) + return () => resizeObserver.disconnect() + }, [containerRef]) + + const columnCount = useMemo(() => { + if (!width) return 1 + return Math.max(1, Math.round(width / COLUMN_WIDTH)) + }, [width]) + + const columnWidth = useMemo(() => { + if (!width) return COLUMN_WIDTH + const totalGutter = (columnCount - 1) * GUTTER + return Math.floor((width - totalGutter) / columnCount) + }, [width, columnCount]) + + const maybeLoadMore = useInfiniteLoader( + useCallback(() => { + if (hasNextPage && !isLoadingMore) { + setIsLoadingMore(true) + endReached() + } + }, [hasNextPage, endReached, isLoadingMore]), + { + isItemLoaded: (index, items) => index < items.length && Boolean(items[index]), + minimumBatchSize: 24, + threshold: 6, + }, + ) + + const currentRange = useRef<{ start: number; end: number } | undefined>(undefined) + const handleRender = useCallback( + (startIndex: number, stopIndex: number, items: MasonryItem[]) => { + currentRange.current = { start: startIndex, end: stopIndex } + return maybeLoadMore(startIndex, stopIndex, items as any[]) + }, + [maybeLoadMore], + ) + + // Mark as read functionality + const renderMarkRead = useGeneralSettingKey("renderMarkUnread") + const scrollMarkRead = useGeneralSettingKey("scrollMarkUnread") + const dataRef = useRefValue(data) + + useEffect(() => { + if (!renderMarkRead && !scrollMarkRead) return + if (!scrollElement) return + + const observer = new IntersectionObserver( + (entries) => { + if (renderMarkRead) { + const visibleEntryIds: string[] = [] + entries.forEach( + ({ isIntersecting, intersectionRatio, boundingClientRect, rootBounds, target }) => { + if ( + isIntersecting && + intersectionRatio >= 0.8 && + boundingClientRect.top >= (rootBounds?.top ?? 0) + ) { + const { entryId } = (target as HTMLElement).dataset + if (entryId) visibleEntryIds.push(entryId) + } + }, + ) + if (visibleEntryIds.length > 0) { + batchMarkRead(visibleEntryIds) + } + } + + if (scrollMarkRead) { + let minIndex = Number.MAX_SAFE_INTEGER + entries.forEach(({ isIntersecting, boundingClientRect, target }) => { + if (!isIntersecting && boundingClientRect.top < 0) { + const { index: datasetIndex } = (target as HTMLElement).dataset + const parsedIndex = Number.parseInt(datasetIndex || "0") + if (parsedIndex > 0 && parsedIndex <= (currentRange.current?.end ?? 0)) { + minIndex = Math.min(minIndex, parsedIndex) + } + } + }) + if (minIndex !== Number.MAX_SAFE_INTEGER) { + batchMarkRead(dataRef.current.slice(0, minIndex)) + } + } + }, + { + root: scrollElement, + rootMargin: "0px", + threshold: [0, 0.8, 1], + }, + ) + + return () => observer.disconnect() + }, [scrollElement, renderMarkRead, scrollMarkRead, dataRef]) + + const handleScrollTo = useCallback( + (index: number) => { + if (!scrollElement) return + + const findTarget = (): HTMLElement | null => { + const byIndex = containerRef?.querySelector(`[data-index="${index}"]`) + if (byIndex) return byIndex + const id = dataRef.current[index] + if (!id) return null + return containerRef?.querySelector(`[data-entry-id="${id}"]`) ?? null + } + + const scrollToEl = (el: HTMLElement) => { + const scRect = scrollElement.getBoundingClientRect() + const elRect = el.getBoundingClientRect() + const centerOffset = (scrollElement.clientHeight - elRect.height) / 2 + const targetTop = elRect.top - scRect.top + scrollElement.scrollTop - centerOffset + const nextTop = Math.max(0, Math.round(targetTop)) + if (Math.abs(nextTop - scrollElement.scrollTop) > 2) { + scrollElement.scrollTo({ top: nextTop, behavior: "auto" }) + } + } + + const el = findTarget() + if (el) { + scrollToEl(el) + } else { + // Try once more on the next frame in case virtualization just mounted it + requestAnimationFrame(() => { + const el2 = findTarget() + if (el2) scrollToEl(el2) + }) + } + }, + [containerRef, dataRef, scrollElement], + ) + + if (!width) { + return ( +
+ +
+ ) + } + + return ( +
+ { + setForceRemountCounter((prev) => prev + 1) + }} + /> +
+ {Footer &&
{typeof Footer === "function" ?
: Footer}
} + {(hasNextPage || isLoadingMore) && } +
+ + +
+ ) +} + +const itemKey = (item: MasonryItem, index: number) => { + if (!item || !item.entryId) { + console.warn("Missing item or entryId at index:", index) + return `fallback-${index}` + } + return item.entryId +} + +const MasonryItemRender: React.ComponentType> = ({ + data, + index, +}) => { + if (!data || !data.entryId) return + + const entry = getEntry(data.entryId) + if (!entry) return + + return ( +
+ +
+ ) +} + +const MasonryWrapper: FC<{ + items: MasonryItem[] + columnGutter: number + columnWidth: number + columnCount: number + overscanBy: number + render: React.ComponentType> + onRender: (startIndex: number, stopIndex: number, items: MasonryItem[]) => void + itemKey: (item: MasonryItem, index: number) => string + itemHeightEstimate?: number + onError?: () => void +}> = (props) => { + const [errorKey, setErrorKey] = useState(0) + const [hasError, setHasError] = useState(false) + + useEffect(() => { + if (hasError) { + const timer = setTimeout(() => setHasError(false), 100) + return () => clearTimeout(timer) + } + }, [hasError]) + + return ( + { + console.error("Masonry error caught:", errorData.error) + setHasError(true) + props.onError?.() + return ( +
+ +
+ ) + }} + beforeCapture={() => { + setErrorKey((prev) => prev + 1) + }} + > + {!hasError && ( + + )} +
+ ) +} + +// Loading skeleton component +const LoadingSkeleton: FC<{ count?: number }> = ({ count = 1 }) => { + const keys = useMemo(() => Array.from({ length: count }), [count]) + return ( + <> + {keys.map((_, index) => ( +
+
+ + + +
+
+ ))} + + ) +} + +const SkeletonGrid: FC<{ columnCount: number }> = ({ columnCount }) => { + const keys = useMemo(() => Array.from({ length: columnCount * 2 }, () => null), [columnCount]) + return ( +
+ {keys.map((_, index) => ( + + ))} +
+ ) +} diff --git a/apps/desktop/layer/renderer/src/modules/entry-column/Items/getItemComponentByView.ts b/apps/desktop/layer/renderer/src/modules/entry-column/Items/getItemComponentByView.ts index ce1e4f35a..d54efe740 100644 --- a/apps/desktop/layer/renderer/src/modules/entry-column/Items/getItemComponentByView.ts +++ b/apps/desktop/layer/renderer/src/modules/entry-column/Items/getItemComponentByView.ts @@ -1,5 +1,6 @@ import { FeedViewType } from "@follow/constants" +import { AllItem } from "./all-item" import { ArticleItem } from "./article-item" import { AudioItem } from "./audio-item" import { NotificationItem } from "./notification-item" @@ -8,6 +9,7 @@ import { SocialMediaItem } from "./social-media-item" import { VideoItem } from "./video-item" const ItemMap = { + [FeedViewType.All]: AllItem, [FeedViewType.Articles]: ArticleItem, [FeedViewType.SocialMedia]: SocialMediaItem, [FeedViewType.Pictures]: PictureItem, 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 a4ad29aed..100177231 100644 --- a/apps/desktop/layer/renderer/src/modules/entry-column/grid.tsx +++ b/apps/desktop/layer/renderer/src/modules/entry-column/grid.tsx @@ -21,15 +21,29 @@ import { MediaContainerWidthProvider } from "~/components/ui/media/MediaContaine import { EntryItemSkeleton } from "./EntryItemSkeleton" import { EntryItem } from "./item" +import { AllMasonry } from "./Items/all-masonry" import { PictureMasonry } from "./Items/picture-masonry" import type { EntryListProps } from "./list" export const EntryColumnGrid: FC = (props) => { - const { entriesIds, feedId, hasNextPage, view, fetchNextPage } = props + const { entriesIds, feedId, hasNextPage, view, fetchNextPage, refetch } = props const isMobile = useMobile() const masonry = useUISettingKey("pictureViewMasonry") || isMobile + if (view === FeedViewType.All) { + return ( + + ) + } + if (masonry && view === FeedViewType.Pictures) { return ( { {visible.map((timelineId, index) => ( diff --git a/apps/desktop/layer/renderer/src/pages/(main)/index.sync.tsx b/apps/desktop/layer/renderer/src/pages/(main)/index.sync.tsx index a2b1bc274..c4ae0deb5 100644 --- a/apps/desktop/layer/renderer/src/pages/(main)/index.sync.tsx +++ b/apps/desktop/layer/renderer/src/pages/(main)/index.sync.tsx @@ -2,13 +2,14 @@ import { FeedViewType } from "@follow/constants" import { redirect } from "react-router" import { ROUTE_ENTRY_PENDING, ROUTE_FEED_PENDING } from "~/constants" +import { getFeature } from "~/hooks/biz/useFeature" export function Component() { return null } export const loader = () => { - return redirect( - `/timeline/view-${FeedViewType.Articles}/${ROUTE_FEED_PENDING}/${ROUTE_ENTRY_PENDING}`, - ) + const aiEnabled = getFeature("ai") + const view = aiEnabled ? FeedViewType.All : FeedViewType.Articles + return redirect(`/timeline/view-${view}/${ROUTE_FEED_PENDING}/${ROUTE_ENTRY_PENDING}`) } diff --git a/apps/mobile/src/constants/views.tsx b/apps/mobile/src/constants/views.tsx index 6ea73d0ac..1ae73488b 100644 --- a/apps/mobile/src/constants/views.tsx +++ b/apps/mobile/src/constants/views.tsx @@ -11,7 +11,7 @@ import { VideoCuteFiIcon } from "../icons/video_cute_fi" interface ViewDefinitionExtended { icon: React.FC<{ color?: string; height?: number; width?: number }> } - +// @ts-expect-error FIXME const extendMap: Record = { [FeedViewType.Articles]: { icon: PaperCuteFiIcon, diff --git a/apps/mobile/src/modules/screen/TimelineViewSelector.tsx b/apps/mobile/src/modules/screen/TimelineViewSelector.tsx index 5c4d3109e..49950e8f8 100644 --- a/apps/mobile/src/modules/screen/TimelineViewSelector.tsx +++ b/apps/mobile/src/modules/screen/TimelineViewSelector.tsx @@ -1,3 +1,4 @@ +import { FeedViewType } from "@follow/constants" import { useViewWithSubscription } from "@follow/store/subscription/hooks" import { useUnreadByView } from "@follow/store/unread/hooks" import { cn } from "@follow/utils" @@ -43,19 +44,21 @@ export function TimelineViewSelector() { contentContainerClassName="flex-row gap-3 items-center px-3" showsHorizontalScrollIndicator={false} > - {activeViews.map((v, index) => { - const view = views.find((view) => view.view === v) - if (!view) return null - return ( - - ) - })} + {activeViews + .filter((v) => v !== FeedViewType.All) + .map((v, index) => { + const view = views.find((view) => view.view === v) + if (!view) return null + return ( + + ) + })} ) diff --git a/packages/internal/constants/src/tabs.tsx b/packages/internal/constants/src/tabs.tsx index 020112ea2..1a6ebb20f 100644 --- a/packages/internal/constants/src/tabs.tsx +++ b/packages/internal/constants/src/tabs.tsx @@ -4,6 +4,7 @@ import { FeedViewType } from "./enums" export interface ViewDefinition { name: + | "feed_view_type.all" | "feed_view_type.articles" | "feed_view_type.audios" | "feed_view_type.notifications" @@ -22,6 +23,18 @@ export interface ViewDefinition { switchable: boolean } export const views: ViewDefinition[] = [ + { + name: "feed_view_type.all", + icon: , + className: "text-folo", + peerClassName: "peer-checked:text-folo dark:peer-checked:text-folo", + translation: "title,description,content", + view: FeedViewType.All, + gridMode: true, + wideMode: true, + activeColor: "#FF5C00", + switchable: false, + }, { name: "feed_view_type.articles", icon: , diff --git a/packages/internal/store/src/modules/entry/store.ts b/packages/internal/store/src/modules/entry/store.ts index c185694a3..c1e3636c3 100644 --- a/packages/internal/store/src/modules/entry/store.ts +++ b/packages/internal/store/src/modules/entry/store.ts @@ -46,6 +46,7 @@ interface EntryState { const defaultState: EntryState = { data: {}, entryIdByView: { + [FeedViewType.All]: new Set(), [FeedViewType.Articles]: new Set(), [FeedViewType.Audios]: new Set(), [FeedViewType.Notifications]: new Set(), @@ -96,8 +97,11 @@ class EntryActions implements Hydratable, Resetable { (hidePrivateSubscriptionsInTimeline && subscription?.isPrivate) || subscription?.hideFromTimeline - if (!ignore && typeof subscription?.view === "number") { - draft.entryIdByView[subscription.view].add(entryId) + if (!ignore) { + if (typeof subscription?.view === "number") { + draft.entryIdByView[subscription.view].add(entryId) + } + draft.entryIdByView[FeedViewType.All].add(entryId) } // lists @@ -107,8 +111,11 @@ class EntryActions implements Hydratable, Resetable { (hidePrivateSubscriptionsInTimeline && subscription?.isPrivate) || subscription?.hideFromTimeline - if (!ignore && typeof subscription?.view === "number") { - draft.entryIdByView[subscription.view].add(entryId) + if (!ignore) { + if (typeof subscription?.view === "number") { + draft.entryIdByView[subscription.view].add(entryId) + } + draft.entryIdByView[FeedViewType.All].add(entryId) } } } @@ -437,6 +444,7 @@ class EntryActions implements Hydratable, Resetable { delete draft.data[entryId] draft.entryIdSet.delete(entryId) draft.entryIdByInbox[entry.inboxHandle!]?.delete(entryId) + draft.entryIdByView[FeedViewType.All].delete(entryId) }) } diff --git a/packages/internal/store/src/modules/subscription/hooks.ts b/packages/internal/store/src/modules/subscription/hooks.ts index 8f009cf30..95e0cf4bf 100644 --- a/packages/internal/store/src/modules/subscription/hooks.ts +++ b/packages/internal/store/src/modules/subscription/hooks.ts @@ -262,6 +262,7 @@ export const useViewWithSubscription = () => return views .filter((view) => { if ( + view.view === FeedViewType.All || view.view === FeedViewType.Articles || view.view === FeedViewType.SocialMedia || view.view === FeedViewType.Pictures || diff --git a/packages/internal/store/src/modules/subscription/selectors.ts b/packages/internal/store/src/modules/subscription/selectors.ts index 59c552414..97ce5df57 100644 --- a/packages/internal/store/src/modules/subscription/selectors.ts +++ b/packages/internal/store/src/modules/subscription/selectors.ts @@ -1,4 +1,4 @@ -import type { FeedViewType } from "@follow/constants" +import { FeedViewType } from "@follow/constants" import { FEED_COLLECTION_LIST, ROUTE_FEED_IN_FOLDER } from "../../constants/app" import type { SubscriptionState } from "./store" @@ -21,7 +21,7 @@ export const folderFeedsByFeedIdSelector = for (const feedId in state.data) { const subscription = state.data[feedId]! if ( - subscription.view === view && + (subscription.view === view || view === FeedViewType.All) && (subscription.category ? subscription.category === folderName : getDefaultCategory(subscription) === folderName) diff --git a/packages/internal/store/src/modules/subscription/store.ts b/packages/internal/store/src/modules/subscription/store.ts index 98ca1910e..c71cf24f6 100644 --- a/packages/internal/store/src/modules/subscription/store.ts +++ b/packages/internal/store/src/modules/subscription/store.ts @@ -48,6 +48,7 @@ export interface SubscriptionState { } const emptyDataSetByView: Record> = { + [FeedViewType.All]: new Set(), [FeedViewType.Articles]: new Set(), [FeedViewType.Audios]: new Set(), [FeedViewType.Notifications]: new Set(), @@ -56,6 +57,7 @@ const emptyDataSetByView: Record> = { [FeedViewType.Videos]: new Set(), } const emptyCategoryOpenStateByView: Record> = { + [FeedViewType.All]: {}, [FeedViewType.Articles]: {}, [FeedViewType.Audios]: {}, [FeedViewType.Notifications]: {}, @@ -97,12 +99,14 @@ class SubscriptionActions implements Hydratable, Resetable { if (subscription.feedId && subscription.type === "feed") { draft.feedIdByView[subscription.view].add(subscription.feedId) + draft.feedIdByView[FeedViewType.All].add(subscription.feedId) if (subscription.category) { draft.categories[subscription.view].add(subscription.category) } } if (subscription.listId && subscription.type === "list") { draft.listIdByView[subscription.view].add(subscription.listId) + draft.listIdByView[FeedViewType.All].add(subscription.listId) } } }) @@ -323,12 +327,15 @@ class SubscriptionSyncService { draft.subscriptionIdSet.delete(getSubscriptionDBId(subscription)) if (subscription.feedId) { draft.feedIdByView[subscription.view].delete(subscription.feedId) + draft.feedIdByView[FeedViewType.All].delete(subscription.feedId) } if (subscription.listId) { draft.listIdByView[subscription.view].delete(subscription.listId) + draft.listIdByView[FeedViewType.All].delete(subscription.listId) } if (subscription.category) { draft.categories[subscription.view].delete(subscription.category) + draft.categories[FeedViewType.All].delete(subscription.category) } delete draft.data[id] } @@ -354,12 +361,15 @@ class SubscriptionSyncService { draft.subscriptionIdSet.add(getSubscriptionDBId(subscription)) if (subscription.feedId) { draft.feedIdByView[subscription.view].add(subscription.feedId) + draft.feedIdByView[FeedViewType.All].add(subscription.feedId) } if (subscription.listId) { draft.listIdByView[subscription.view].add(subscription.listId) + draft.listIdByView[FeedViewType.All].add(subscription.listId) } if (subscription.category) { draft.categories[subscription.view].add(subscription.category) + draft.categories[FeedViewType.All].add(subscription.category) } } }) diff --git a/packages/internal/store/src/modules/unread/store.ts b/packages/internal/store/src/modules/unread/store.ts index 8f31662ab..d0274afd7 100644 --- a/packages/internal/store/src/modules/unread/store.ts +++ b/packages/internal/store/src/modules/unread/store.ts @@ -1,4 +1,4 @@ -import type { FeedViewType } from "@follow/constants" +import { FeedViewType } from "@follow/constants" import type { UnreadSchema } from "@follow/database/schemas/types" import { EntryService } from "@follow/database/services/entry" import { UnreadService } from "@follow/database/services/unread" @@ -129,11 +129,14 @@ class UnreadSyncService { }) { const request = async () => { const args: MarkAllAsReadRequest = { - view, + view: view === FeedViewType.All ? undefined : view, excludePrivate, ...filter, ...time, } + if (view === FeedViewType.All) { + delete args.view + } const res = await api().reads.markAllAsRead(args) return res.data.read @@ -162,7 +165,7 @@ class UnreadSyncService { async markViewAsRead(view: FeedViewType, excludePrivate: boolean) { await this.markBatchAsRead({ - view, + view: view === FeedViewType.All ? undefined : view, excludePrivate, }) } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5eeacdc34..ba2257ed3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,8 +7,8 @@ settings: catalogs: default: '@follow-app/client-sdk': - specifier: 0.3.55 - version: 0.3.55 + specifier: 0.3.53 + version: 0.3.53 tailwindcss-uikit-colors: specifier: 1.0.0 version: 1.0.0 @@ -350,7 +350,7 @@ importers: version: 4.3.0 '@follow-app/client-sdk': specifier: 'catalog:' - version: 0.3.55(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(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.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(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.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(kysely@0.28.2)(pg@8.16.3) + version: 0.3.53(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(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.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(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.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(kysely@0.28.2)(pg@8.16.3) '@follow-app/readability': specifier: workspace:* version: link:../../../../packages/readability @@ -468,7 +468,7 @@ importers: version: 3.0.2(electron@37.2.0) '@follow-app/client-sdk': specifier: 'catalog:' - version: 0.3.55(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)(pg@8.16.3) + version: 0.3.53(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)(pg@8.16.3) '@follow/database': specifier: workspace:* version: link:../../../../packages/internal/database @@ -1682,7 +1682,7 @@ importers: dependencies: '@follow-app/client-sdk': specifier: 'catalog:' - version: 0.3.55(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)(pg@8.16.3) + version: 0.3.53(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)(pg@8.16.3) '@follow/configs': specifier: workspace:* version: link:../../configs @@ -1694,7 +1694,7 @@ importers: dependencies: '@follow-app/client-sdk': specifier: 'catalog:' - version: 0.3.55(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)(pg@8.16.3) + version: 0.3.53(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)(pg@8.16.3) '@follow/constants': specifier: workspace:* version: link:../constants @@ -1766,7 +1766,7 @@ importers: dependencies: '@follow-app/client-sdk': specifier: 'catalog:' - version: 0.3.55(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)(pg@8.16.3) + version: 0.3.53(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)(pg@8.16.3) '@follow/constants': specifier: workspace:* version: link:../constants @@ -1830,7 +1830,7 @@ importers: dependencies: '@follow-app/client-sdk': specifier: 'catalog:' - version: 0.3.55(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)(pg@8.16.3) + version: 0.3.53(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)(pg@8.16.3) '@follow/configs': specifier: workspace:* version: link:../../configs @@ -4060,14 +4060,14 @@ packages: '@floating-ui/utils@0.2.10': resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} - '@follow-app/client-sdk@0.3.55': - resolution: {integrity: sha512-E3Edh5j6Eeg45PS+2ZXjrNyIWp4UBhxFDT1rk7V+kOsWJ+hG9TAk8+wDnK5FI4H8di1Sl7wfqcRc1fmCZsYapQ==} + '@follow-app/client-sdk@0.3.53': + resolution: {integrity: sha512-GW4I4do/tkeY2d8ry75SJBQQiSPyuq9qbUKUr1fZO+bfzAmBvPW8zDjdVGCaNHU6dV0RvreWbJB8rw6rKYym4w==} '@folo-services/ai-tools@0.2.36': resolution: {integrity: sha512-MNgXELZklcwteBjyC92lp2xgVm1ydDRkX+5dl4ZiYYqVi9spbD3GcckurbiQJ9ANF/7PW71nnJSIo7/9y8qaWg==} - '@folo-services/constants@0.1.32': - resolution: {integrity: sha512-Kn1Kkf9HUFHxgX5MjbFljJplrgLCjlh5SDTaiE7pyunXdM2HmrYu8FSl6jSAPTdNbYgXprl3eGQqU+tPUooFKg==} + '@folo-services/constants@0.1.31': + resolution: {integrity: sha512-DBcxfN1FRg24JmxM26G07y3lvCAdCG6gPAyGJZx5yBFL83dNYHJeN1/pvXcTqDlfM3or3+0Oq22FWF7381sjxQ==} '@folo-services/drizzle@0.1.25': resolution: {integrity: sha512-FByV3IWHAV3qh5G9eIl6dUzUGfWakk+VyjOBBseVQ5InpZiCZyMAKh5MXsvR1L4Uew/cX+rHey6dNfo80BYB7Q==} @@ -18994,9 +18994,9 @@ snapshots: '@floating-ui/utils@0.2.10': {} - '@follow-app/client-sdk@0.3.55(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(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.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(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.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(kysely@0.28.2)(pg@8.16.3)': + '@follow-app/client-sdk@0.3.53(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(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.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(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.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(kysely@0.28.2)(pg@8.16.3)': dependencies: - '@folo-services/constants': 0.1.32 + '@folo-services/constants': 0.1.31 '@folo-services/drizzle': 0.1.26(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(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.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(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.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(kysely@0.28.2) '@folo-services/exceptions': 0.1.16 '@folo-services/shared': 0.0.21(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(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.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(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.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)(utf-8-validate@6.0.5))(react@19.0.0))(kysely@0.28.2)(pg@8.16.3) @@ -19033,9 +19033,9 @@ snapshots: - sql.js - sqlite3 - '@follow-app/client-sdk@0.3.55(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)(pg@8.16.3)': + '@follow-app/client-sdk@0.3.53(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)(pg@8.16.3)': dependencies: - '@folo-services/constants': 0.1.32 + '@folo-services/constants': 0.1.31 '@folo-services/drizzle': 0.1.26(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2) '@folo-services/exceptions': 0.1.16 '@folo-services/shared': 0.0.21(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)(pg@8.16.3) @@ -19111,7 +19111,7 @@ snapshots: - sql.js - sqlite3 - '@folo-services/constants@0.1.32': + '@folo-services/constants@0.1.31': dependencies: zod: 3.25.76 @@ -21321,7 +21321,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: @@ -28676,6 +28676,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 @@ -28861,7 +28877,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 c9d9734be..64daafd5d 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -9,7 +9,7 @@ packages: catalog: typescript: 5.8.3 - "@follow-app/client-sdk": 0.3.55 + "@follow-app/client-sdk": 0.3.53 tailwindcss-uikit-colors: 1.0.0 onlyBuiltDependencies: