"use client" import { useIsMobile } from "@hooks/use-mobile" import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api" import { colors } from "@repo/ui/colors" import { Sparkles } from "lucide-react" import { Masonry, useInfiniteLoader } from "masonic" import { memo, useCallback, useMemo, useState } from "react" import type { z } from "zod" import { analytics } from "@/lib/analytics" import { useDeleteDocument } from "@lib/queries" import { useProject } from "@/stores" import { MemoryDetail } from "./memories-utils/memory-detail" import { TweetCard } from "./content-cards/tweet" import { WebsiteCard } from "./content-cards/website" import { NoteCard } from "./content-cards/note" import { GoogleDocsCard } from "./content-cards/google-docs" import type { Tweet } from "react-tweet/api" type DocumentsResponse = z.infer type DocumentWithMemories = DocumentsResponse["documents"][0] interface MasonryMemoryListProps { children?: React.ReactNode documents: DocumentWithMemories[] isLoading: boolean isLoadingMore: boolean error: Error | null totalLoaded: number hasMore: boolean loadMoreDocuments: () => Promise } const DocumentCard = memo( ({ index: _index, data: document, width, onOpenDetails, onDelete, }: { index: number data: DocumentWithMemories & { ogImage?: string } width: number onOpenDetails: (document: DocumentWithMemories) => void onDelete: (document: DocumentWithMemories) => void }) => { const activeMemories = document.memoryEntries.filter((m) => !m.isForgotten) const forgottenMemories = document.memoryEntries.filter( (m) => m.isForgotten, ) if ( document.url?.includes("https://docs.googleapis.com/v1/documents") || document.url?.includes("docs.google.com/document") || document.type === "google_doc" ) { return ( onDelete(document)} /> ) } if ( document.url?.includes("x.com/") && document.metadata?.sm_internal_twitter_metadata ) { return ( onDelete(document)} /> ) } // Check if this is a website document saved from the Chrome extension const websiteUrl = (document.metadata?.website_url as string | undefined) || (document.url?.includes("https://") ? document.url : undefined) if (websiteUrl) { return ( onOpenDetails(document)} onDelete={() => onDelete(document)} /> ) } return ( ) }, ) DocumentCard.displayName = "DocumentCard" export const MasonryMemoryList = ({ children, documents, isLoading, isLoadingMore, error, hasMore, loadMoreDocuments, }: MasonryMemoryListProps) => { const [selectedSpace, _] = useState("all") const [selectedDocument, setSelectedDocument] = useState(null) const [isDetailOpen, setIsDetailOpen] = useState(false) const isMobile = useIsMobile() const { selectedProject } = useProject() const deleteDocumentMutation = useDeleteDocument(selectedProject) const handleDeleteDocument = useCallback( (document: DocumentWithMemories) => { deleteDocumentMutation.mutate(document.id) }, [deleteDocumentMutation], ) // Filter documents based on selected space const filteredDocuments = useMemo(() => { if (!documents) return [] if (selectedSpace === "all") { return documents } return documents .map((doc) => ({ ...doc, memoryEntries: doc.memoryEntries.filter( (memory) => (memory.spaceContainerTag ?? memory.spaceId) === selectedSpace, ), })) .filter((doc) => doc.memoryEntries.length > 0) }, [documents, selectedSpace]) const handleOpenDetails = useCallback((document: DocumentWithMemories) => { analytics.memoryDetailOpened() setSelectedDocument(document) setIsDetailOpen(true) }, []) const handleCloseDetails = useCallback(() => { setIsDetailOpen(false) setTimeout(() => setSelectedDocument(null), 300) }, []) // Infinite loading with Masonic const maybeLoadMore = useInfiniteLoader( async (_startIndex, _stopIndex, _currentItems) => { if (hasMore && !isLoadingMore) { await loadMoreDocuments() } }, { isItemLoaded: (index, items) => !!items[index], minimumBatchSize: 10, threshold: 5, }, ) const renderDocumentCard = useCallback( ({ index, data, width, }: { index: number data: DocumentWithMemories width: number }) => ( ), [handleOpenDetails, handleDeleteDocument], ) return ( <>
{error ? (
Error loading documents: {error.message}
) : isLoading ? (
{Array.from({ length: 8 }, (_, i) => ({ id: `skeleton-${Math.random()}-${i}`, height: 100 + (i % 3), })).map((item) => (
))}
) : filteredDocuments.length === 0 && !isLoading ? (
{children}
) : (
d.id).join(",")}`} items={filteredDocuments} render={renderDocumentCard} columnGutter={16} rowGutter={16} columnWidth={280} maxColumnCount={isMobile ? 1 : undefined} itemHeightEstimate={200} overscanBy={3} onRender={maybeLoadMore} className="px-4" /> {isLoadingMore && (
Loading more memories...
)}
)}
) }