Revert "Merge pull request #4460 from RSSNext/feat/remove-all-view"
This reverts commitfdf076f28b, reversing changes made to3776d82d97.
This commit is contained in:
parent
b07e2de01e
commit
280bc2a45e
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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}`} />
|
||||
|
|
|
|||
|
|
@ -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}`)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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" />,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 ||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Reference in New Issue