Revert "Merge pull request #4460 from RSSNext/feat/remove-all-view"

This reverts commit fdf076f28b, reversing
changes made to 3776d82d97.
This commit is contained in:
DIYgod 2025-09-09 16:18:29 +08:00
parent b07e2de01e
commit 280bc2a45e
No known key found for this signature in database
18 changed files with 989 additions and 49 deletions

View File

@ -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
}

View File

@ -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(() => <EntryContent entryId={entryId} noMedia compact />, [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<HTMLDivElement>(null)
const hoverTimerRef = useRef<ReturnType<typeof setTimeout> | 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 ? (
<span
key={`keyword-${index}-${part}`}
className={cn(type === "highlight" && randomStyle.highlight.className)}
style={{
textDecorationSkipInk: "none",
}}
>
{part}
</span>
) : (
<span
key={`text-${index}-${part}`}
className={cn(type === "highlight" && "text-transparent")}
>
{part}
</span>
)
})}
</>
)
return (
<div className="relative">
{renderTitle({ type: "normal" })}
<div className="absolute inset-0 z-0">{renderTitle({ type: "highlight" })}</div>
</div>
)
}, [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 (
<div className="group" ref={ref}>
{/* Hero */}
<div
className={cn(
"relative flex max-h-[35em] flex-col overflow-hidden rounded-lg",
"before:group-hover:bg-theme-item-hover before:pointer-events-none before:absolute before:inset-0 before:z-10 before:transition-colors before:duration-200",
randomStyle.card.card,
)}
>
{/* Icon */}
{!mediaCover && (
<div
className={cn(
"absolute left-4 top-4 z-[1] flex items-center justify-center text-2xl",
randomStyle.card.icon,
)}
>
{icon}
</div>
)}
{/* Common views */}
{(view === FeedViewType.Articles ||
view === FeedViewType.Notifications ||
view === FeedViewType.SocialMedia ||
view === FeedViewType.Audios) && (
<>
{mediaCover ? (
<Media
src={mediaCover.url}
type={mediaCover.type}
previewImageUrl={mediaCover.preview_image_url}
className="min-h-[6em] w-full overflow-hidden"
mediaContainerClassName="size-full min-h-[6em] object-cover"
videoClassName="size-full min-h-[6em] object-cover"
loading="lazy"
proxy={{
width: 600,
height: 0,
}}
blurhash={mediaCover.blurhash || undefined}
style={{
aspectRatio: aspectRatio ?? 1,
}}
/>
) : (
<div className="flex min-h-[6em] flex-col items-center justify-center overflow-hidden px-5 py-16 text-xl font-medium leading-snug text-black/80">
<div className="line-clamp-6 max-w-full break-words">{titleWithKeyword}</div>
</div>
)}
</>
)}
{/* Pictures */}
{view === FeedViewType.Pictures && (
<div className="relative flex gap-2 overflow-x-auto">
{entryMedia ? (
<SwipeMedia
media={entryMedia}
className={cn(
"aspect-square",
"w-full shrink-0 rounded-md [&_img]:rounded-md",
isActive && "rounded-b-none",
)}
imgClassName="object-cover"
onPreview={previewMedia}
/>
) : (
<div className="flex min-h-[6em] flex-col items-center justify-center overflow-hidden px-4 py-20 text-[1.5rem] font-normal leading-[1.2]">
<div className="line-clamp-6 max-w-full break-words">{titleWithKeyword}</div>
</div>
)}
</div>
)}
{/* Videos */}
{view === FeedViewType.Videos && (
<div className="cursor-card w-full">
<div className="relative overflow-x-auto">
{mediaCover ? (
<Media
key={mediaCover.url}
src={mediaCover.url}
type={mediaCover.type}
previewImageUrl={mediaCover.preview_image_url}
className="min-h-[6em] w-full overflow-hidden"
mediaContainerClassName="size-full min-h-[6em] object-cover"
videoClassName="size-full min-h-[6em] object-cover"
loading="lazy"
proxy={{
width: mediaCover.width ?? 640,
height: mediaCover.height ?? 360,
}}
blurhash={mediaCover.blurhash || undefined}
style={{
aspectRatio: aspectRatio ?? 16 / 9,
}}
showFallback={true}
/>
) : (
<div className="flex min-h-[6em] flex-col items-center justify-center overflow-hidden px-4 py-20 text-[1.5rem] font-normal leading-[1.2]">
<div className="line-clamp-6 max-w-full break-words">{titleWithKeyword}</div>
</div>
)}
{miniIframeSrc && showPreview && (
<div className="pointer-events-none absolute inset-0">
<ViewTag
src={miniIframeSrc}
className={cn("pointer-events-none size-full min-h-[6em] object-cover")}
/>
</div>
)}
{!!entry.duration && (
<div className="absolute bottom-2 right-2 rounded-md bg-black/50 px-1 py-0.5 text-xs font-medium text-white">
{entry.duration}
</div>
)}
</div>
</div>
)}
</div>
{/* Footer */}
<div className={cn("relative px-1 pb-4 text-sm")}>
<div className="flex items-center">
<div
className={cn(
"bg-accent mr-1 size-1.5 shrink-0 self-center rounded-full duration-200",
asRead && "mr-0 w-0",
)}
/>
<div className={cn("relative mb-1 mt-1.5 flex w-full items-center gap-1 font-medium")}>
<EntryTranslation
source={entry.title}
target={translation?.title}
className="line-clamp-2"
inline={false}
/>
{isInCollection && (
<div className="h-0 shrink-0 -translate-y-2">
<StarIcon />
</div>
)}
</div>
</div>
<div className="flex min-w-0 items-center justify-between text-[13px]">
<div className="flex min-w-0 items-center gap-1">
<FeedIcon
fallback
noMargin
className="flex"
feed={feeds!}
entry={entry.iconEntry}
size={18}
/>
<span className={cn("text-text-secondary min-w-0 truncate pl-1")}>
<FeedTitle feed={feeds} />
</span>
</div>
<span className={"text-text-secondary ml-2 min-w-0 shrink-0"}>
{dayjs.duration(dayjs(entry.publishedAt).diff(dayjs(), "minute"), "minute").humanize()}
{t("space")}
{t("words.ago")}
</span>
</div>
</div>
</div>
)
}
AllItem.wrapperClassName = "hover:bg-transparent"
// function AllArticleItem({ entryId, entryPreview, translation }: UniversalItemProps) {
// return <ListItem entryId={entryId} entryPreview={entryPreview} translation={translation} />
// }
// 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],
),
)
}

View File

@ -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<AllMasonryProps> = ({
data,
hasNextPage,
endReached,
Footer,
refetch,
}) => {
const scrollElement = useScrollViewElement()
const [containerRef, setContainerRef] = useState<HTMLDivElement | null>(null)
const [width, setWidth] = useState<number>(0)
const [isLoadingMore, setIsLoadingMore] = useState(false)
const prevDataLengthRef = useRef(data.length)
// Convert entry IDs to masonry items with stable references
const items = useMemo<MasonryItem[]>(
() => 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<HTMLElement>(`[data-index="${index}"]`)
if (byIndex) return byIndex
const id = dataRef.current[index]
if (!id) return null
return containerRef?.querySelector<HTMLElement>(`[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 (
<div ref={setContainerRef} className="mx-4 pt-4">
<LoadingSkeleton />
</div>
)
}
return (
<div ref={setContainerRef} className="mx-4 pb-8 pt-4">
<MasonryWrapper
key={masonryKey}
items={items}
columnGutter={GUTTER}
columnWidth={columnWidth}
columnCount={columnCount}
overscanBy={OVERSCAN}
render={MasonryItemRender}
onRender={handleRender}
itemKey={itemKey}
itemHeightEstimate={200}
onError={() => {
setForceRemountCounter((prev) => prev + 1)
}}
/>
<div className="mt-8">
{Footer && <div className="mb-4">{typeof Footer === "function" ? <Footer /> : Footer}</div>}
{(hasNextPage || isLoadingMore) && <SkeletonGrid columnCount={columnCount} />}
</div>
<EntryColumnShortcutHandler
refetch={refetch}
data={dataRef.current}
handleScrollTo={handleScrollTo}
/>
</div>
)
}
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<RenderComponentProps<MasonryItem>> = ({
data,
index,
}) => {
if (!data || !data.entryId) return <LoadingSkeleton count={1} />
const entry = getEntry(data.entryId)
if (!entry) return <LoadingSkeleton count={1} />
return (
<div
data-entry-id={data.entryId}
data-index={index}
className={clsx("bg-background rounded-lg")}
>
<EntryItem entryId={data.entryId} view={FeedViewType.All} />
</div>
)
}
const MasonryWrapper: FC<{
items: MasonryItem[]
columnGutter: number
columnWidth: number
columnCount: number
overscanBy: number
render: React.ComponentType<RenderComponentProps<MasonryItem>>
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 (
<ErrorBoundary
key={errorKey}
fallback={(errorData) => {
console.error("Masonry error caught:", errorData.error)
setHasError(true)
props.onError?.()
return (
<div className="flex items-center justify-center py-8">
<LoadingSkeleton count={6} />
</div>
)
}}
beforeCapture={() => {
setErrorKey((prev) => prev + 1)
}}
>
{!hasError && (
<Masonry
items={props.items}
columnGutter={props.columnGutter}
columnWidth={props.columnWidth}
columnCount={props.columnCount}
overscanBy={props.overscanBy}
render={props.render}
onRender={props.onRender}
itemKey={props.itemKey}
itemHeightEstimate={props.itemHeightEstimate}
/>
)}
</ErrorBoundary>
)
}
// Loading skeleton component
const LoadingSkeleton: FC<{ count?: number }> = ({ count = 1 }) => {
const keys = useMemo(() => Array.from({ length: count }), [count])
return (
<>
{keys.map((_, index) => (
<div
// eslint-disable-next-line @eslint-react/no-array-index-key
key={index}
className="border-material-ultra-thick bg-background overflow-hidden rounded-lg border"
>
<div className="space-y-3 p-4">
<Skeleton className="h-4 w-3/4" />
<Skeleton className="h-3 w-1/2" />
<Skeleton className="h-32 w-full" />
</div>
</div>
))}
</>
)
}
const SkeletonGrid: FC<{ columnCount: number }> = ({ columnCount }) => {
const keys = useMemo(() => Array.from({ length: columnCount * 2 }, () => null), [columnCount])
return (
<div className="mb-4 grid gap-4" style={{ gridTemplateColumns: `repeat(${columnCount}, 1fr)` }}>
{keys.map((_, index) => (
<LoadingSkeleton count={1} key={index} />
))}
</div>
)
}

View File

@ -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,

View File

@ -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<EntryListProps> = (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 (
<AllMasonry
key={feedId}
hasNextPage={hasNextPage}
endReached={fetchNextPage}
data={entriesIds}
Footer={props.Footer}
refetch={refetch}
/>
)
}
if (masonry && view === FeedViewType.Pictures) {
return (
<PictureMasonry

View File

@ -63,6 +63,7 @@ export const EntryListHeader: FC<{
const feed = useFeedById(feedId)
const titleStyleBasedView = {
[FeedViewType.All]: "pl-5",
[FeedViewType.Articles]: "pl-7",
[FeedViewType.Pictures]: "pl-7",
[FeedViewType.Videos]: "pl-7",

View File

@ -280,7 +280,7 @@ const TabsRow: FC = () => {
<SubscriptionTabButton
shortcut="BackQuote"
key={first}
timelineId={`${ROUTE_TIMELINE_OF_VIEW}${FeedViewType.Articles}`}
timelineId={`${ROUTE_TIMELINE_OF_VIEW}${FeedViewType.All}`}
/>
{visible.map((timelineId, index) => (
<SubscriptionTabButton key={timelineId} timelineId={timelineId} shortcut={`${index + 1}`} />

View File

@ -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}`)
}

View File

@ -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, ViewDefinitionExtended> = {
[FeedViewType.Articles]: {
icon: PaperCuteFiIcon,

View File

@ -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 (
<ViewItem
key={view.name}
index={index}
view={view}
scrollViewRef={scrollViewRef}
isActive={selectedFeed?.type === "view" && selectedFeed.viewId === view.view}
/>
)
})}
{activeViews
.filter((v) => v !== FeedViewType.All)
.map((v, index) => {
const view = views.find((view) => view.view === v)
if (!view) return null
return (
<ViewItem
key={view.name}
index={index}
view={view}
scrollViewRef={scrollViewRef}
isActive={selectedFeed?.type === "view" && selectedFeed.viewId === view.view}
/>
)
})}
</ScrollView>
</View>
)

View File

@ -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: <i className="i-mgc-bubble-cute-fi" />,
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: <i className="i-mgc-paper-cute-fi" />,

View File

@ -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)
})
}

View File

@ -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 ||

View File

@ -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)

View File

@ -48,6 +48,7 @@ export interface SubscriptionState {
}
const emptyDataSetByView: Record<FeedViewType, Set<FeedId>> = {
[FeedViewType.All]: new Set(),
[FeedViewType.Articles]: new Set(),
[FeedViewType.Audios]: new Set(),
[FeedViewType.Notifications]: new Set(),
@ -56,6 +57,7 @@ const emptyDataSetByView: Record<FeedViewType, Set<FeedId>> = {
[FeedViewType.Videos]: new Set(),
}
const emptyCategoryOpenStateByView: Record<FeedViewType, Record<string, boolean>> = {
[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)
}
}
})

View File

@ -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,
})
}

View File

@ -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

View File

@ -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: