diff --git a/src/alex_frontend/core/apps/Modules/AppModules/blinks/SingleTokenView.tsx b/src/alex_frontend/core/apps/Modules/AppModules/blinks/SingleTokenView.tsx deleted file mode 100644 index 5b6a24f5e..000000000 --- a/src/alex_frontend/core/apps/Modules/AppModules/blinks/SingleTokenView.tsx +++ /dev/null @@ -1,446 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { useParams } from '@tanstack/react-router'; -import ContentRenderer from '../safeRender/ContentRenderer'; -import { ContentCard } from './../contentGrid/Card'; -import { Dialog, DialogContent, DialogTitle } from './../../../../lib/components/dialog'; -import { useSelector, useDispatch } from 'react-redux'; -import { RootState, AppDispatch } from './../../../../store'; -import { toast } from "sonner"; -// import { withdraw_nft } from "@/features/nft/withdraw"; // Keep commented if not used -import { Principal } from '@dfinity/principal'; -import { ALEX } from '../../../../../../declarations/ALEX'; -import { LBRY } from '../../../../../../declarations/LBRY'; -import { nft_manager } from '../../../../../../declarations/nft_manager'; -import { updateNftBalances, setNFTs } from '../../shared/state/nftData/nftDataSlice'; -import { fetchTransactionById } from '../../LibModules/arweaveSearch/api/directArweaveClient'; -import { ContentService } from '../../LibModules/contentDisplay/services/contentService'; -import { setContentData } from '../../shared/state/transactions/transactionSlice'; -import { Transaction } from '../../shared/types/queries'; -import { Badge } from "./../../../../lib/components/badge"; -import { Copy, Check, Link, Calendar, Info } from "lucide-react"; -import { copyToClipboard } from './../contentGrid/utils/clipboard'; -import { getNftOwnerInfo, UserInfo } from '../../shared/utils/nftOwner'; -import { convertE8sToToken, formatPrincipal, formatBalance } from '../../shared/utils/tokenUtils'; -import { createTokenAdapter, determineTokenType, TokenType } from '../../shared/adapters/TokenAdapter'; -import { ShelvesPreloader } from '../shared/components/ShelvesPreloader'; - -const NFT_MANAGER_PRINCIPAL = "5sh5r-gyaaa-aaaap-qkmra-cai"; - -function SingleTokenView() { - const { tokenId } = useParams({ from: "/nft/$tokenId" }); - const [isLoading, setIsLoading] = useState(true); - const [transaction, setTransaction] = useState(null); - const [contentUrls, setContentUrls] = useState(null); - const [showModal, setShowModal] = useState(false); - const [copiedPrincipal, setCopiedPrincipal] = useState(false); - const [copiedLink, setCopiedLink] = useState(false); - const [ownerInfo, setOwnerInfo] = useState(null); - const [showDetails, setShowDetails] = useState(false); - const [copiedId, setCopiedId] = useState(false); - const dispatch = useDispatch(); - - const contentData = useSelector((state: RootState) => state.transactions.contentData); - const nft = useSelector((state: RootState) => - tokenId ? state.nftData.nfts[tokenId] : undefined - ); - const nftBalances = useSelector((state: RootState) => - tokenId ? state.nftData.nfts[tokenId]?.balances : undefined - ); - const nftPrincipal = nft?.principal; - const nftCollection = nft?.collection; - - const { user } = useSelector((state: RootState) => state.auth); - - const toggleDetails = () => { - setShowDetails(prev => !prev); - }; - - const handleCopyId = async () => { - if (!tokenId) return; - const copied = await copyToClipboard(tokenId); - if (copied) { - setCopiedId(true); - setTimeout(() => setCopiedId(false), 2000); - toast.success('Copied ID to clipboard'); - } else { - toast.error('Failed to copy ID'); - } - }; - - useEffect(() => { - let mounted = true; - - async function loadNFTData() { - if (!tokenId) return; - - try { - setIsLoading(true); - let currentNftData = nft; - let arweaveId = nft?.arweaveId; - - if (!currentNftData || !arweaveId) { - console.log('NFT data or Arweave ID missing in Redux store, fetching directly...', tokenId); - const tokenType = determineTokenType(tokenId); - const tokenAdapter = createTokenAdapter(tokenType); - const nftId = BigInt(tokenId); - - const fetchedNftDataBase = await tokenAdapter.tokenToNFTData(nftId, ''); - arweaveId = fetchedNftDataBase.arweaveId; - - if (!arweaveId) { - throw new Error('Unable to fetch Arweave ID for NFT'); - } - - currentNftData = { - ...fetchedNftDataBase, - principal: currentNftData?.principal || '' - }; - - // Avoid dispatching within the primary data loading part if possible to prevent loops - } - - if (!arweaveId) { - console.error('Critical: Arweave ID not found for tokenId:', tokenId); - toast.error('Unable to fetch NFT metadata link.'); - setIsLoading(false); - return; - } - - const txData = await fetchTransactionById(arweaveId); - - if (!txData) { - console.error('Transaction not found for arweaveId:', arweaveId); - toast.error('Transaction data not found.'); - setIsLoading(false); - return; - } - - const content = await ContentService.loadContent(txData); - const urls = await ContentService.getContentUrls(txData, content); - - if (mounted) { - setTransaction(txData); - setContentUrls(urls); - - dispatch(setContentData({ - id: txData.id, - content: { ...content, urls } - })); - } - - } catch (error) { - console.error('Failed to load NFT core data:', error); - if (mounted) toast.error('Failed to load NFT data'); - } finally { - if (mounted) { - setIsLoading(false); - } - } - } - - loadNFTData(); - - return () => { - mounted = false; - }; - }, [tokenId, dispatch]); - - useEffect(() => { - let mounted = true; - async function loadOwnerInfo() { - if (!tokenId) return; - if (!ownerInfo || (nftPrincipal && ownerInfo.principal !== nftPrincipal)) { - try { - const info = await getNftOwnerInfo(tokenId); - if (mounted) { - setOwnerInfo(info); - if (nft && info?.principal && nft.principal !== info.principal) { - dispatch(setNFTs({ [tokenId]: { ...nft, principal: info.principal } })); - } - } - } catch (error) { - if (mounted) console.error('Failed to load owner info:', error); - } - } - } - loadOwnerInfo(); - return () => { mounted = false; }; - }, [tokenId, nftPrincipal, ownerInfo, dispatch, nft]); - - useEffect(() => { - let mounted = true; - async function loadBalances() { - if (!tokenId || !nftCollection) return; - - if (!nftBalances || Object.keys(nftBalances).length === 0) { - console.log("Fetching balances for", tokenId); - try { - const nftId = BigInt(tokenId); - const subaccount = await nft_manager.to_nft_subaccount(nftId); - const balanceParams = { - owner: Principal.fromText(NFT_MANAGER_PRINCIPAL), - subaccount: [Array.from(subaccount)] as [number[]] - }; - - const [alexBalance, lbryBalance] = await Promise.all([ - ALEX.icrc1_balance_of(balanceParams), - LBRY.icrc1_balance_of(balanceParams) - ]); - - if (mounted) { - const alex = convertE8sToToken(alexBalance); - const lbry = convertE8sToToken(lbryBalance); - - dispatch(updateNftBalances({ - tokenId, - alex, - lbry, - collection: nftCollection - })); - } - } catch (error) { - if (mounted) console.error('Failed to load NFT balances:', error); - } - } - } - loadBalances(); - return () => { mounted = false; }; - }, [tokenId, nftCollection, nftBalances, dispatch]); - - const handleRenderError = (transactionId: string) => { - ContentService.clearTransaction(transactionId); - }; - - if (!tokenId) { - console.log('No tokenId provided'); - return
Invalid token ID
; - } - - if (isLoading || !transaction || !contentUrls) { - console.log('Still loading or no transaction/contentUrls:', { isLoading, transaction: !!transaction, contentUrls: !!contentUrls }); - return
Loading...
; - } - - const currentContent = transaction ? contentData[transaction.id] : null; - - if (!currentContent) { - console.error('Content not found in Redux for transaction:', { - transactionId: transaction.id, - availableContentIds: Object.keys(contentData), - nftData: nft - }); - } - - const collectionType = nftCollection || 'NFT'; - - let ownerPrincipalForActions: Principal | undefined; - try { - const ownerString = ownerInfo?.principal || nftPrincipal; - ownerPrincipalForActions = ownerString ? Principal.fromText(ownerString) : undefined; - } catch (e) { - console.error("Invalid owner principal format for actions:", ownerInfo?.principal || nftPrincipal, e); - ownerPrincipalForActions = undefined; - } - - const isOwned = !!(user?.principal && ownerPrincipalForActions && user.principal === ownerPrincipalForActions.toText()); - - const handleCopyPrincipal = async (e: React.MouseEvent) => { - e.stopPropagation(); - const principalToCopy = ownerInfo?.principal || nftPrincipal; - if (!principalToCopy) return; - - const copied = await copyToClipboard(principalToCopy); - if (copied) { - setCopiedPrincipal(true); - setTimeout(() => setCopiedPrincipal(false), 2000); - toast.success('Copied principal to clipboard'); - } else { - toast.error('Failed to copy principal'); - } - }; - - const handleCopyLink = async (e: React.MouseEvent) => { - e.stopPropagation(); - if (!tokenId) return; - - const publicUrl = process.env.PUBLIC_URL || ''; - const lbryUrl = process.env.NODE_ENV === 'development' - ? `http://localhost:8080/nft/${tokenId}` - : `${publicUrl}/nft/${tokenId}`; - const copied = await copyToClipboard(lbryUrl); - if (copied) { - setCopiedLink(true); - setTimeout(() => setCopiedLink(false), 2000); - toast.success('Copied link to clipboard'); - } else { - toast.error('Failed to copy link'); - } - }; - - const CustomFooter = () => ( -
-
- - {copiedLink ? : } - - {(ownerInfo?.principal || nftPrincipal) && ( - - {formatPrincipal(ownerInfo?.principal || nftPrincipal || '')} - {copiedPrincipal ? : } - - )} - {ownerInfo?.username && ( - - @{ownerInfo.username} - - )} - - {collectionType} - - - ALEX: {formatBalance(nftBalances?.alex?.toString())} - - - LBRY: {formatBalance(nftBalances?.lbry?.toString())} - -
-
- ); - - const formatId = (id: string) => { - if (!id) return ''; - if (id.length <= 8) return id; - return `${id.substring(0, 4)}...${id.substring(id.length - 4)}`; - }; - - return ( -
- - -
- setShowModal(true)} - owner={transaction.owner} - predictions={undefined} - footer={} - initialContentType="Nft" - > - {currentContent && contentUrls ? ( - - ) : ( -
- Content not available... -
- )} -
- - {showDetails && ( -
-
-
-
-
- - ID -
-
- {formatId(tokenId || '')} - {copiedId ? ( - - ) : ( - - )} -
-
- - {nft?.arweaveId && ( -
-
- - Arweave ID -
-
- {formatId(nft.arweaveId)} -
-
- )} - -
-
- - Type -
- - {collectionType} - -
- -
-
- - Balances -
-
- - ALEX: {formatBalance(nftBalances?.alex?.toString())} - - - LBRY: {formatBalance(nftBalances?.lbry?.toString())} - -
-
-
-
-
- )} -
- - !open && setShowModal(false)}> - - Content Viewer -
-
- {currentContent && transaction && contentUrls && ( - - )} -
-
-
-
-
- ); -} - -export default SingleTokenView; \ No newline at end of file diff --git a/src/alex_frontend/core/apps/Modules/AppModules/contentGrid/Card.tsx b/src/alex_frontend/core/apps/Modules/AppModules/contentGrid/Card.tsx deleted file mode 100644 index 101afa259..000000000 --- a/src/alex_frontend/core/apps/Modules/AppModules/contentGrid/Card.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import React from "react"; -import { Card, CardContent } from "../../../../lib/components/card"; -import { AspectRatio } from "../../../../lib/components/aspect-ratio"; -import { UnifiedCardActions } from "./../../shared/components/UnifiedCardActions/UnifiedCardActions"; -import { useContentCardState } from "./hooks/useContentCardState"; - -interface ContentCardProps { - children: React.ReactNode; - onClick?: () => void; - id?: string; // Arweave ID or NFT Nat ID string - owner?: string; // Arweave owner string (kept for potential future use, but not displayed here) - predictions?: any; - footer?: React.ReactNode; - component?: string; - isFromAssetCanister?: boolean; - parentShelfId?: string; - itemId?: number; - currentShelfId?: string; - initialContentType?: 'Arweave' | 'Nft'; // Specifies the *context* this card is rendered in -} - -export function ContentCard({ - children, - onClick, - id, // Arweave ID or NFT Nat ID string - owner, // Keep owner prop, but don't use it directly here - predictions, - footer, // Keep footer prop - component, - isFromAssetCanister, - parentShelfId, - itemId, - currentShelfId, - initialContentType = 'Arweave' // Default to Arweave context -}: ContentCardProps) { - - // --- Use the Custom Hook --- - const { - finalContentId, - finalContentType, - isOwnedByUser, - ownerPrincipal, - isSafeForMinting - } = useContentCardState({ id, initialContentType, predictions }); - - // --- Rendering --- - return ( - <> - - {/* Action Button - Using updated bookmark design */} - {finalContentId && ( - - )} - - {/* Main content area - Apply onClick here if needed */} - - -
- {/* Children now include the hover overlay (TransactionDetails) internally */} - {children} -
- {/* UnifiedCardActions moved outside */} -
-
- - {/* Footer area (unchanged) */} - {footer && ( -
- {footer} -
- )} -
- - ); -} \ No newline at end of file diff --git a/src/alex_frontend/core/apps/Modules/AppModules/contentGrid/Grid.tsx b/src/alex_frontend/core/apps/Modules/AppModules/contentGrid/Grid.tsx deleted file mode 100644 index 5d2d68eef..000000000 --- a/src/alex_frontend/core/apps/Modules/AppModules/contentGrid/Grid.tsx +++ /dev/null @@ -1,246 +0,0 @@ -import React, { useState, useCallback } from "react"; -import { useDispatch, useSelector } from "react-redux"; -import { RootState, AppDispatch } from "@/store"; -import { toast } from "sonner"; -import { clearTransactionContent } from "@/apps/Modules/shared/state/transactions/transactionSlice"; -import ContentRenderer from '@/apps/Modules/AppModules/safeRender/ContentRenderer'; -import { useSortedTransactions } from '@/apps/Modules/shared/state/content/contentSortUtils'; -import { Button } from "@/lib/components/button"; -import { withdraw_nft } from "@/features/nft/withdraw"; -import { TooltipProvider } from "@/lib/components/tooltip"; -import { Loader2 } from 'lucide-react'; -import { ContentCard } from "@/apps/Modules/AppModules/contentGrid/Card"; -import { hasWithdrawableBalance } from '@/apps/Modules/shared/utils/tokenUtils'; -import type { Transaction } from '../../shared/types/queries'; -import { TokenType } from '@/apps/Modules/shared/adapters/TokenAdapter'; -import { ShelvesPreloader } from "../shared/components/ShelvesPreloader"; -import { MainContentDisplayModal } from '@/apps/Modules/shared/components/MainContentDisplayModal/MainContentDisplayModal'; -import { AttachedDetailsPanel } from '@/apps/Modules/shared/components/AttachedDetailsPanel/AttachedDetailsPanel'; -import { useNftManager } from "@/hooks/actors"; - -const useAppDispatch = () => useDispatch(); - -export interface ContentGridProps { - children: React.ReactNode; -} - -type ContentGridComponent = React.FC & { - Item: typeof ContentCard; -}; - -export const ContentGrid: ContentGridComponent = Object.assign( - ({ children }: ContentGridProps) => { - return ( -
- - {children} -
- ); - }, - { Item: ContentCard } -); - -const mapCollectionToBackend = (collection: TokenType): 'icrc7' | 'icrc7_scion' => { - return collection === 'NFT' ? 'icrc7' : 'icrc7_scion'; -}; - -export type GridDataSource = 'transactions'; - -interface GridProps { - dataSource?: GridDataSource; -} - -// Define a type for the selected content state (Transaction should be enough) -interface SelectedContentState { - transaction: Transaction; - // Removed contentType, as it's derived from transaction -} - -const Grid = ({ dataSource }: GridProps = {}) => { - const { actor } = useNftManager(); - const dispatch = useAppDispatch(); - - const contentData = useSelector((state: RootState) => state.transactions.contentData); - const transactions = useSelector((state: RootState) => state.transactions.transactions); // Ensure this is used or remove - const { nfts, arweaveToNftId } = useSelector((state: RootState) => state.nftData); - const { user } = useSelector((state: RootState) => state.auth); - const { predictions } = useSelector((state: RootState) => state.arweave); - - const sortedTransactions = useSortedTransactions(); - - const [selectedContent, setSelectedContent] = useState(null); - const [isMainModalOpen, setIsMainModalOpen] = useState(false); - const [isDetailsPanelOpen, setIsDetailsPanelOpen] = useState(false); - const [withdrawingStates, setWithdrawingStates] = useState>({}); - - const handleRenderError = useCallback((transactionId: string) => { - dispatch(clearTransactionContent(transactionId)); - }, [dispatch]); - - const handleWithdraw = useCallback(async (transactionId: string) => { - try { - setWithdrawingStates(prev => ({ ...prev, [transactionId]: true })); - const nftId = arweaveToNftId[transactionId]; - if (!nftId) { - throw new Error("Could not find NFT ID for this content"); - } - - const nftData = nfts[nftId]; - if (!nftData) { - throw new Error("Could not find NFT data for this content"); - } - - if (!actor) { - throw new Error("Could not find NFT manager actor"); - } - - if(!user) { - throw new Error("You must be authenticated to withdraw NFT funds"); - } - - const collection = nftData.collection as TokenType | undefined; - if (!collection || (collection !== 'NFT' && collection !== 'SBT')) { - throw new Error(`Invalid or missing collection type on NFT data: ${collection}`); - } - const backendCollection = mapCollectionToBackend(collection); - - const [lbryBlock, alexBlock] = await withdraw_nft(actor, nftId, backendCollection); - if (lbryBlock === null && alexBlock === null) { - toast.info("No funds were available to withdraw"); - } else { - let message = "Successfully withdrew"; - if (lbryBlock !== null) message += " LBRY"; - if (alexBlock !== null) message += (lbryBlock !== null ? " and" : "") + " ALEX"; - toast.success(message); - } - } catch (error) { - console.error("Error withdrawing funds:", error); - toast.error(error instanceof Error ? error.message : "An unexpected error occurred"); - } finally { - setWithdrawingStates(prev => ({ ...prev, [transactionId]: false })); - } - }, [arweaveToNftId, nfts, user, actor]); - - const handleOpenMainModal = useCallback((transaction: Transaction) => { - setSelectedContent({ transaction }); - setIsMainModalOpen(true); - setIsDetailsPanelOpen(false); // Ensure details panel is closed when a new item is opened - }, []); - - const handleCloseMainModal = useCallback(() => { - setIsMainModalOpen(false); - setIsDetailsPanelOpen(false); // Also close details panel when main modal is closed - setSelectedContent(null); - }, []); - - const handleToggleDetailsPanel = useCallback(() => { - setIsDetailsPanelOpen(prev => !prev); - }, []); - - const handleCloseDetailsPanel = useCallback(() => { - setIsDetailsPanelOpen(false); - }, []); - - return ( - - <> - - {sortedTransactions.map((transaction: Transaction) => { - const cardContent = contentData[transaction.id]; - // Removed contentType as it's derived within MainContentDisplayModal if needed or not used. - - const currentPredictions = predictions[transaction.id]; - const nftId = arweaveToNftId[transaction.id]; - const nftData = nftId ? nfts[nftId] : undefined; - const isOwned = !!(user && nftData?.principal === user.principal); - const canWithdraw = isOwned && nftData && hasWithdrawableBalance( - nftData.balances?.alex, - nftData.balances?.lbry - ); - const detectedContentType = nftData ? 'Nft' : 'Arweave'; - - return ( - handleOpenMainModal(transaction)} // Updated onClick - id={transaction.id} - owner={transaction.owner} - predictions={currentPredictions} - isFromAssetCanister={(transaction.assetUrl && transaction?.assetUrl !== "") ? true : false} - initialContentType={detectedContentType} - > -
- - {predictions[transaction.id]?.isPorn && ( -
-
- Content Filtered -
-
- )} - - {isOwned && canWithdraw && ( - - )} -
-
- ); - })} -
- - {selectedContent && selectedContent.transaction && ( - <> - - - - )} - -
- ); -}; - -export default Grid; \ No newline at end of file diff --git a/src/alex_frontend/core/apps/Modules/AppModules/contentGrid/components/TransactionDetails.tsx b/src/alex_frontend/core/apps/Modules/AppModules/contentGrid/components/TransactionDetails.tsx deleted file mode 100644 index 2bf6a6cc6..000000000 --- a/src/alex_frontend/core/apps/Modules/AppModules/contentGrid/components/TransactionDetails.tsx +++ /dev/null @@ -1,370 +0,0 @@ -import React, { JSX } from "react"; -import { Transaction } from "@/apps/Modules/shared/types/queries"; -import { Copy, Check, Link, Database, User, Search, Flag, X, ChevronDown, ChevronUp } from 'lucide-react'; -import { toast } from "sonner"; -import { - Card, - CardContent, - CardHeader, -} from "@/lib/components/card"; -import { ScrollArea } from "@/lib/components/scroll-area"; -import { Badge } from "@/lib/components/badge"; -import { Separator } from "@/lib/components/separator"; -import { useSelector, useDispatch } from 'react-redux'; -import { RootState } from '@/store'; -import { useNftData, NftDataResult } from '@/apps/Modules/shared/hooks/getNftData'; -import { formatId, handleCopy } from "../utils/formatters"; -import { formatPrincipal, formatBalance } from '@/apps/Modules/shared/utils/tokenUtils'; -import { setSearchState } from "@/apps/Modules/shared/state/arweave/arweaveSlice"; -import { Button } from "@/lib/components/button"; -import { Progress } from "@/lib/components/progress"; -import { PredictionResults } from "@/apps/Modules/shared/state/arweave/arweaveSlice"; - -const truncateMiddle = (str: string, startChars: number = 4, endChars: number = 4) => { - if (str.length <= startChars + endChars + 3) return str; - return `${str.slice(0, startChars)}...${str.slice(-endChars)}`; -}; - -interface TransactionDetailsProps { - transaction: Transaction; - predictions?: PredictionResults; -} - -const TransactionDetails: React.FC = ({ - transaction, - predictions -}) => { - const dispatch = useDispatch(); - - // NFT data related hooks and state - const { nfts, arweaveToNftId } = useSelector((state: RootState) => state.nftData); - const { getNftData } = useNftData(); - const [copiedPrincipal, setCopiedPrincipal] = React.useState(false); - const [copiedLink, setCopiedLink] = React.useState(false); - const [copiedTokenId, setCopiedTokenId] = React.useState(false); - const [copiedOwner, setCopiedOwner] = React.useState(false); - const [copiedTxId, setCopiedTxId] = React.useState(false); - const [searchTriggered, setSearchTriggered] = React.useState(false); - const [nftDataResult, setNftDataResult] = React.useState(null); - const [loading, setLoading] = React.useState(true); - - // Get the specific NFT data (including balances) from Redux - const tokenId = arweaveToNftId[transaction.id]; - const nftDataFromStore = useSelector((state: RootState) => tokenId ? state.nftData.nfts[tokenId] : undefined); - - // Fetch basic NFT data (principal, collection) when component mounts or transaction changes - React.useEffect(() => { - let isMounted = true; - const fetchBasicNftData = async () => { - if (transaction.id) { - setLoading(true); - try { - // Fetch basic data but rely on Redux for balances - const data = await getNftData(transaction.id); - if (isMounted) { - setNftDataResult(data); - } - } catch (err) { - console.error("Error fetching NFT data:", err); - } finally { - if (isMounted) { - setLoading(false); - } - } - } - }; - fetchBasicNftData(); - return () => { isMounted = false; }; // Cleanup function - }, [transaction.id, getNftData]); - - const copyToClipboard = async (text: string, label: string) => { - try { - await navigator.clipboard.writeText(text); - toast.success(`Copied ${label} to clipboard`); - return true; - } catch (err) { - toast.error('Failed to copy to clipboard'); - return false; - } - }; - - // NFT copy handlers - const handleCopyPrincipal = async (e: React.MouseEvent) => { - e.stopPropagation(); - if (!nftDataResult?.principal) return; - const success = await copyToClipboard(nftDataResult.principal, 'Principal'); - if (success) { - setCopiedPrincipal(true); - setTimeout(() => setCopiedPrincipal(false), 2000); - } - }; - - const handleCopyLink = async (e: React.MouseEvent) => { - e.stopPropagation(); - if (!tokenId) return; - - const publicUrl = process.env.PUBLIC_URL || ''; - const lbryUrl = process.env.NODE_ENV === 'development' - ? `http://localhost:8080/nft/${tokenId}` - : `${publicUrl}/nft/${tokenId}`; - - const success = await copyToClipboard(lbryUrl, 'NFT link'); - if (success) { - setCopiedLink(true); - setTimeout(() => setCopiedLink(false), 2000); - } - }; - - const handleCopyTokenId = async (e: React.MouseEvent) => { - e.stopPropagation(); - if (!tokenId) return; - - const success = await copyToClipboard(tokenId, 'Token ID'); - if (success) { - setCopiedTokenId(true); - setTimeout(() => setCopiedTokenId(false), 2000); - } - }; - - // Transaction ID Copy Handler - const handleCopyTxId = async (e: React.MouseEvent) => { - e.stopPropagation(); - const success = await copyToClipboard(transaction.id, 'Transaction ID'); - if (success) { - setCopiedTxId(true); - setTimeout(() => setCopiedTxId(false), 2000); - } - }; - - // Arweave Owner Click Handler - const handleOwnerClick = (e: React.MouseEvent) => { - if (!transaction.owner) return; - - e.stopPropagation(); - - handleCopy(e, transaction.owner, setCopiedOwner, () => { - dispatch(setSearchState({ ownerFilter: transaction.owner })); - setSearchTriggered(true); - setTimeout(() => setSearchTriggered(false), 2000); - }); - }; - - const isFromAssetCanister = transaction.assetUrl && transaction.assetUrl !== ""; - // Use principal from local state or store, and check tokenId for NFT data presence - const hasNftData = tokenId || nftDataResult?.principal || nftDataFromStore?.principal; - const principalToDisplay = nftDataResult?.principal || nftDataFromStore?.principal; - const collectionToDisplay = nftDataResult?.collection || nftDataFromStore?.collection; - const balancesToDisplay = nftDataFromStore?.balances; // Get balances from Redux store - const orderIndexToDisplay = nftDataResult?.orderIndex ?? nftDataFromStore?.orderIndex; - const rarityPercentageToDisplay = nftDataFromStore?.rarityPercentage; // Get rarity from Redux store - - // Helper function to format rarity percentage - const formatRarityPercentage = (rarity: number | undefined): string => { - if (rarity === undefined || rarity === null || rarity < 0) { // Check for -1 as "not ranked" - return "Not Ranked"; - } - const percentage = (rarity / 100).toFixed(2); - return `${percentage}% Rarity`; - }; - - // This function now directly returns the details content JSX - // The old hover wrapper div is removed. - const renderDetailsContent = (): JSX.Element => ( - - {/* ICP NFT Data Section */} - {hasNftData && ( -
- ICP Info -
- - {isFromAssetCanister ? "ICP" : "AR"} - - - {/* Link badge */} - {tokenId && ( - - {copiedLink ? ( - - ) : ( - - )} - - )} - - {/* Collection badge */} - {collectionToDisplay && collectionToDisplay !== 'No Collection' && ( - - - {collectionToDisplay} - - )} - - {/* Principal badge */} - {principalToDisplay && ( - - - {formatPrincipal ? formatPrincipal(principalToDisplay) : truncateMiddle(principalToDisplay)} - {copiedPrincipal ? ( - - ) : ( - - )} - - )} - - {/* Token ID badge */} - {tokenId && ( - - - {formatId(tokenId)} - {copiedTokenId ? ( - - ) : ( - - )} - - )} - - {/* Order Index */} - {orderIndexToDisplay !== undefined && ( - - #{orderIndexToDisplay} - - )} -
- - {/* Balance and Rarity badges - Use balances from Redux store with better styling */} - {(balancesToDisplay || (collectionToDisplay === 'NFT' && rarityPercentageToDisplay !== undefined)) && ( -
- {/* ALEX Badge */} - {balancesToDisplay && ( - - - {formatBalance(balancesToDisplay.alex)} ALEX - - - )} - {/* LBRY Badge */} - {balancesToDisplay && ( - - - {formatBalance(balancesToDisplay.lbry)} LBRY - - - )} - {/* Rarity Percentage Badge - only for NFTs */} - {collectionToDisplay === 'NFT' && rarityPercentageToDisplay !== undefined && ( - - - {formatRarityPercentage(rarityPercentageToDisplay)} - - )} -
- )} - - -
- )} - - {/* Arweave Transaction Details Section */} -
- Arweave Info -
-
- Transaction ID -
- {transaction.id} - {copiedTxId ? ( - - ) : ( - - )} -
-
- - {/* Arweave Owner field in proper section with search functionality */} - {transaction.owner && ( -
- Owner -
- - - {formatId(transaction.owner)} - - {copiedOwner ? ( - - ) : ( - - )} -
-
- )} - - {transaction.data && ( -
- Size - {(transaction.data.size / 1024).toFixed(2)} KB -
- )} - {transaction.block && ( -
- Date - - {new Date(transaction.block.timestamp * 1000).toLocaleString('en-US', { - timeZone: 'UTC' - })} UTC - -
- )} -
- - - -
- Tags -
- {transaction.tags.map((tag, index) => ( - copyToClipboard(`${tag.name}: ${tag.value}`, 'Tag')} - > - {truncateMiddle(tag.name, 4, 2)}: {truncateMiddle(tag.value, 4, 2)} - - - ))} -
-
-
-
- ); - - // The component now directly returns the result of renderDetailsContent() - // The hover div and ScrollArea/Card wrappers are removed from here. - return renderDetailsContent(); -}; - -export default TransactionDetails; \ No newline at end of file diff --git a/src/alex_frontend/core/apps/Modules/AppModules/contentGrid/hooks/useContentCardState.ts b/src/alex_frontend/core/apps/Modules/AppModules/contentGrid/hooks/useContentCardState.ts deleted file mode 100644 index 752714737..000000000 --- a/src/alex_frontend/core/apps/Modules/AppModules/contentGrid/hooks/useContentCardState.ts +++ /dev/null @@ -1,203 +0,0 @@ -import { useMemo } from 'react'; -import { useSelector } from 'react-redux'; -import { Principal } from '@dfinity/principal'; -import { RootState } from '@/store'; -import { fileTypeCategories } from '@/apps/Modules/shared/types/files'; -import type { Transaction } from '@/apps/Modules/shared/types/queries'; // Ensure Transaction type is imported - -// Define the shape of the hook's return value -interface ContentCardState { - finalContentId: string | undefined; - finalContentType: 'Nft' | 'Arweave'; - isItemLikable: boolean; - isOwnedByUser: boolean; - ownerPrincipal: Principal | undefined; - isSafeForMinting: boolean; -} - -// Define the props the hook needs -interface UseContentCardStateProps { - id?: string; // Arweave ID or NFT Nat ID string (depending on context) - initialContentType?: 'Arweave' | 'Nft'; - predictions?: any; // Consider defining a more specific type if possible -} - -/** - * Custom hook to manage the derived state for the ContentCard component. - * Encapsulates logic for determining content ID, type, likability, ownership, and mint safety. - */ -export const useContentCardState = ({ - id, - initialContentType = 'Arweave', // Default to Arweave context - predictions, -}: UseContentCardStateProps): ContentCardState => { - // --- Selectors --- - const { user } = useSelector((state: RootState) => state.auth); - const currentUserPrincipal = user?.principal; - const arweaveToNftId = useSelector((state: RootState) => state.nftData.arweaveToNftId); - const nfts = useSelector((state: RootState) => state.nftData.nfts); - // Ensure transactions state is accessed correctly and cast appropriately - const transactions = useSelector((state: RootState) => state.transactions.transactions as Transaction[]); - - // --- Memoized Derived State --- - - // Find the corresponding Arweave transaction if the initial context is NFT - // and we need to check its original Content-Type tag - const arweaveTransactionForNft = useMemo(() => { - if (initialContentType === 'Nft' && id) { - // Find the Arweave ID associated with this NFT Nat ID - const arweaveId = Object.keys(arweaveToNftId).find(key => arweaveToNftId[key] === id); - return transactions.find(t => t.id === arweaveId); - } - return undefined; - }, [id, initialContentType, arweaveToNftId, transactions]); - - const { nftNatId, nftData } = useMemo(() => { - let natId: string | undefined; - let data: typeof nfts[string] | undefined; - - if (initialContentType === 'Nft') { - natId = id; // In NFT context, id is the Nat ID string - data = natId ? nfts[natId] : undefined; - } else { // Arweave context - natId = id ? arweaveToNftId[id] : undefined; // Look up Nat ID from Arweave ID - data = natId ? nfts[natId] : undefined; - } - return { nftNatId: natId, nftData: data }; - }, [id, initialContentType, arweaveToNftId, nfts]); - - const ownerPrincipal = useMemo(() => { - // NFT data takes precedence if available - if (nftData?.principal) { - try { - return Principal.fromText(nftData.principal); - } catch (e) { - console.error("Invalid principal format in nftData:", nftData.principal, e); - return undefined; - } - } - // Fallback for Arweave context if no NFT exists yet (owner from transaction) - if (initialContentType === 'Arweave') { - const transaction = transactions.find(t => t.id === id); - if (transaction?.owner) { - try { - // Arweave tx owner might be an address, not principal. Handle carefully. - // Assuming owner field in Transaction *is* intended to be a Principal string - // If it's an Arweave address, this will fail. Adapt if needed. - // return Principal.fromText(transaction.owner); - // For now, let's assume we only care about NFT owner principal - return undefined; // Or adapt if Arweave owner principal is needed - } catch (e) { - console.error("Invalid principal format in transaction owner:", transaction.owner, e); - return undefined; - } - } - } - return undefined; - }, [nftData, initialContentType, id, transactions]); - - const isOwnedByUser = useMemo(() => { - // Ownership is determined *only* by the NFT data, regardless of context - return !!(nftData && currentUserPrincipal && nftData.principal === currentUserPrincipal); - }, [nftData, currentUserPrincipal]); - - const isMediaContent = useMemo(() => { - // Use the direct transaction if Arweave context, or the looked-up one if NFT context - const relevantTransaction = initialContentType === 'Arweave' - ? transactions.find(t => t.id === id) - : arweaveTransactionForNft; - - const contentTypeTag = relevantTransaction?.tags?.find(tag => tag.name === "Content-Type")?.value; - if (!contentTypeTag) return false; - // Ensure fileTypeCategories comparison is correct - return [...fileTypeCategories.images, ...fileTypeCategories.video].includes(contentTypeTag); - }, [id, initialContentType, transactions, arweaveTransactionForNft]); - - // --- Determine Final Props for UnifiedCardActions --- - const { finalContentId, finalContentType, isItemLikable, isSafeForMinting } = useMemo(() => { - let determinedContentId: string | undefined = id; // Default to Arweave ID or incoming NFT ID - let determinedContentType: 'Nft' | 'Arweave' = initialContentType; // Start with the initial context - let determinedLikability: boolean = false; - let safeForMinting: boolean = true; // Calculation happens here - - if (initialContentType === 'Nft') { - determinedContentType = 'Nft'; - determinedContentId = id; // Use the incoming NFT Nat ID string - // Likability for existing NFTs (usually means creating an SBT if not owned) - determinedLikability = !isOwnedByUser; - // Safety is not relevant for minting *from* an existing NFT context - safeForMinting = true; - - } else { // Arweave Context (initialContentType === 'Arweave') - determinedContentType = 'Arweave'; - determinedContentId = id; // Always use Arweave ID for Arweave context - - // Likability/Mintability Check - if (id && !isOwnedByUser) { - if (!isMediaContent) { - determinedLikability = true; // Non-media is likable/mintable if not owned - safeForMinting = true; // Non-media is always considered safe for minting - } else { - // Media requires prediction check - if (predictions && predictions.isPorn === true) { - // Explicitly marked as NSFW - not safe for minting - determinedLikability = false; - safeForMinting = false; - } else { - // Safe if predictions are not loaded yet, loading, or explicitly safe - // This allows action buttons to show while predictions are loading - determinedLikability = true; - safeForMinting = true; - } - } - } else { - // Already owned (based on NFT lookup) or no ID - not likable/mintable from Arweave context - determinedLikability = false; - // If owned, it's implicitly "safe" because it already exists as NFT, but minting isn't the action. - // If no ID, safety isn't applicable. - safeForMinting = true; // Set true if owned or no ID, as minting won't happen anyway - } - } - - // If an NFT already exists for this Arweave ID, override the content type to Nft - // and use the Nat ID for actions, unless the user explicitly owns it (then it's still 'Nft') - // Keep Arweave ID if we need it for minting (likability). - if (initialContentType === 'Arweave' && nftNatId) { - // An NFT exists for this Arweave item. - // The *actionable* ID should be the NFT Nat ID for adding to shelf etc. - // But the *likability* and *safety* checks were based on the Arweave context. - determinedContentId = nftNatId; // Use NFT ID for actions like add-to-shelf - determinedContentType = 'Nft'; // Treat it as an NFT now - // Keep determinedLikability and safeForMinting from Arweave context checks above. - // If it exists as NFT, safeforMinting should be true. Likability depends on ownership. - safeForMinting = true; // If NFT exists, it passed safety check implicitly or wasn't media - determinedLikability = !isOwnedByUser; // Can 'like' (create SBT) if not owned - - } - - - return { - finalContentId: determinedContentId, - finalContentType: determinedContentType, - isItemLikable: determinedLikability, - isSafeForMinting: safeForMinting, - }; - }, [id, initialContentType, nftNatId, isOwnedByUser, isMediaContent, predictions]); - - // --- Memoized return value for the entire hook --- - return useMemo(() => ({ - finalContentId, - finalContentType, - isItemLikable, - isOwnedByUser, - ownerPrincipal, - isSafeForMinting, - }), [ - finalContentId, - finalContentType, - isItemLikable, - isOwnedByUser, - ownerPrincipal, - isSafeForMinting, - ]); -}; \ No newline at end of file diff --git a/src/alex_frontend/core/apps/Modules/AppModules/contentGrid/utils/clipboard.ts b/src/alex_frontend/core/apps/Modules/AppModules/contentGrid/utils/clipboard.ts deleted file mode 100644 index f8a2aaaab..000000000 --- a/src/alex_frontend/core/apps/Modules/AppModules/contentGrid/utils/clipboard.ts +++ /dev/null @@ -1,9 +0,0 @@ -export async function copyToClipboard(text: string): Promise { - try { - await navigator.clipboard.writeText(text); - return true; - } catch (err) { - console.error('Failed to copy:', err); - return false; - } -} \ No newline at end of file diff --git a/src/alex_frontend/core/apps/Modules/AppModules/contentGrid/utils/formatters.ts b/src/alex_frontend/core/apps/Modules/AppModules/contentGrid/utils/formatters.ts deleted file mode 100644 index cacf2577d..000000000 --- a/src/alex_frontend/core/apps/Modules/AppModules/contentGrid/utils/formatters.ts +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Formats an ID by showing only the first 3 and last 2 characters separated by ellipsis. - * @param id - The ID to format - * @param defaultValue - Value to return if the ID is empty or null - * @returns Formatted ID string - */ -import { copyToClipboard } from "./clipboard"; - -export function formatId(id: string | null | undefined, defaultValue: string = 'N/A'): string { - if (!id) return defaultValue; - - // If the ID is too short to be meaningfully formatted - if (id.length <= 5) return id; - - return `${id.slice(0, 3)}...${id.slice(-2)}`; -} - -/** - * Universal copy handler that manages the copying process and copy state - * @param e - The mouse event - * @param textToCopy - The text to copy to clipboard - * @param setCopiedState - State setter function to update copied state - * @param callback - Optional callback function to execute after successful copy - * @returns Promise - Whether the copy was successful - */ -export async function handleCopy( - e: React.MouseEvent, - textToCopy: string | undefined | null, - setCopiedState: React.Dispatch>, - callback?: () => void -): Promise { - e.stopPropagation(); - - if (!textToCopy) return false; - - try { - const success = await copyToClipboard(textToCopy); - - if (success) { - setCopiedState(true); - setTimeout(() => setCopiedState(false), 2000); - - // Execute callback if provided - if (callback) { - callback(); - } - } - - return success; - } catch (error) { - console.error("Copy failed:", error); - return false; - } -} \ No newline at end of file diff --git a/src/alex_frontend/core/apps/Modules/AppModules/safeRender/ContentFetcher.tsx b/src/alex_frontend/core/apps/Modules/AppModules/safeRender/ContentFetcher.tsx deleted file mode 100644 index a03313558..000000000 --- a/src/alex_frontend/core/apps/Modules/AppModules/safeRender/ContentFetcher.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import React, { useRef, useEffect } from 'react'; - -interface ContentFetcherProps { - contentUrl: string; - contentType: string; - imageObjectUrl: string | null; - onLoad: (element: HTMLImageElement | HTMLVideoElement, thumbnailUrl?: string) => void; - onError: () => void; -} - -const ContentFetcher: React.FC = ({ - contentUrl, - contentType, - imageObjectUrl, - onLoad, - onError, -}) => { - const contentRef = useRef(null); - - useEffect(() => { - if (contentRef.current) { - const handleLoad = () => { - if (contentRef.current) { - if (contentType.startsWith('video/')) { - const video = contentRef.current as HTMLVideoElement; - // Seek to first frame - video.currentTime = 0; - // Wait for seek to complete - video.addEventListener('seeked', () => { - const canvas = document.createElement('canvas'); - canvas.width = video.videoWidth; - canvas.height = video.videoHeight; - const ctx = canvas.getContext('2d'); - if (ctx) { - ctx.drawImage(video, 0, 0, canvas.width, canvas.height); - // Convert to blob and create thumbnail URL - canvas.toBlob((blob) => { - if (blob) { - const thumbnailUrl = URL.createObjectURL(blob); - onLoad(video, thumbnailUrl); - } - }, 'image/jpeg'); - } - }, { once: true }); - } else { - onLoad(contentRef.current); - } - } - }; - - const currentContent = contentRef.current; - currentContent.addEventListener('load', handleLoad); - currentContent.addEventListener('loadedmetadata', handleLoad); // For videos - - return () => { - currentContent.removeEventListener('load', handleLoad); - currentContent.removeEventListener('loadedmetadata', handleLoad); - }; - } - }, [contentRef.current, onLoad, contentType]); - - return ( - <> - {contentType.startsWith('image/') && imageObjectUrl && ( - } - src={imageObjectUrl} - alt="Content for validation" - onError={onError} - style={{ display: 'none' }} - /> - )} - {contentType.startsWith('video/') && ( - - )} - - ); -}; - -export default ContentFetcher; diff --git a/src/alex_frontend/core/apps/Modules/AppModules/safeRender/ContentRenderer.tsx b/src/alex_frontend/core/apps/Modules/AppModules/safeRender/ContentRenderer.tsx deleted file mode 100644 index 0ea43b551..000000000 --- a/src/alex_frontend/core/apps/Modules/AppModules/safeRender/ContentRenderer.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import React from 'react'; -import { File } from 'lucide-react'; -import ContentValidator from './ContentValidator'; -import SandboxRenderer from './SandboxRenderer'; -import { Transaction } from "@/apps/Modules/shared/types/queries"; -import { ContentUrlInfo } from './types'; -import { useSelector } from 'react-redux'; -import { RootState } from '@/store'; - -const getContentType = (transaction: Transaction): string => { - return transaction.tags.find(tag => tag.name === "Content-Type")?.value || 'application/octet-stream'; -}; - -interface ContentRendererProps { - transaction: Transaction; - content: any; - inModal?: boolean; - contentUrls: ContentUrlInfo; - handleRenderError: (id: string) => void; -} - -const ContentRenderer: React.FC = ({ - transaction, - content, - inModal = false, - contentUrls, - handleRenderError, -}) => { - const contentType = getContentType(transaction); - const predictions = useSelector((state: RootState) => state.arweave.predictions[transaction.id]); - const shouldShowBlur = predictions && predictions.isPorn === true; - - // If no content, show skeleton-like UI - if (!content) { - return ( -
- -
-
- ); - } - - return ( -
- - - {shouldShowBlur && ( -
-
- Content Filtered -
-
- )} -
- ); -}; - -export default React.memo(ContentRenderer); \ No newline at end of file diff --git a/src/alex_frontend/core/apps/Modules/AppModules/safeRender/ContentTypeMap.tsx b/src/alex_frontend/core/apps/Modules/AppModules/safeRender/ContentTypeMap.tsx deleted file mode 100644 index 98a4ed065..000000000 --- a/src/alex_frontend/core/apps/Modules/AppModules/safeRender/ContentTypeMap.tsx +++ /dev/null @@ -1,472 +0,0 @@ -import React, { useEffect, useRef, useState, Suspense, JSX } from 'react'; -import DOMPurify from 'dompurify'; -import { BookOpen, File, Play, Music } from 'lucide-react'; -import { AspectRatio } from "@/lib/components/aspect-ratio"; -import { Skeleton } from "@/lib/components/skeleton"; -import { getFileIcon } from './fileIcons'; -import { Transaction } from "@/apps/Modules/shared/types/queries"; -import { ContentUrlInfo } from './types'; -import ReactMarkdown from 'react-markdown'; -import { useTheme } from "@/providers/ThemeProvider"; -import { getCover } from '@/utils/epub'; - -interface ContentTypeMapProps { - transaction: Transaction; - content: any; - inModal?: boolean; - contentUrls: ContentUrlInfo; - handleRenderError: (id: string) => void; -} - -type ContentRenderer = () => JSX.Element; - -interface ContentMap { - [key: string]: ContentRenderer; -} - -interface TextBasedContentMap { - [key: string]: boolean; -} - -const formatJSON = (content: string) => { - try { - const parsed = JSON.parse(content); - return JSON.stringify(parsed, null, 2); - } catch { - return content; - } -}; -const generateVideoThumbnail = (videoUrl: string, callback: (thumbnail: string) => void) => { - const video = document.createElement('video'); - video.src = videoUrl; - video.crossOrigin = "anonymous"; - video.onloadeddata = () => { - video.currentTime = 2; - }; - video.onseeked = () => { - const canvas = document.createElement('canvas'); - canvas.width = video.videoWidth; - canvas.height = video.videoHeight; - const ctx = canvas.getContext('2d'); - if (ctx) { - ctx.drawImage(video, 0, 0, canvas.width, canvas.height); - const thumbnailUrl = canvas.toDataURL('image/png'); - callback(thumbnailUrl); - } - }; -}; - -// Lazy load Reader and ReaderProvider -const Reader = React.lazy(() => import('@/features/reader').then(module => ({ default: module.Reader }))); -const ReaderProvider = React.lazy(() => import('@/features/reader/lib/providers/ReaderProvider').then(module => ({ default: module.ReaderProvider }))); - -export const ContentTypeMap: React.FC = ({ - transaction, - content, - inModal = false, - contentUrls, - handleRenderError, -}) => { - const iframeRef = useRef(null); - const { theme } = useTheme(); - const contentType = transaction.tags.find(tag => tag.name === "Content-Type")?.value || 'application/octet-stream'; - const { fullUrl, coverUrl, thumbnailUrl } = contentUrls; - const [generatedThumbnail, setGeneratedThumbnail] = useState(null); - const [isVideoThumbLoading, setIsVideoThumbLoading] = useState(false); - const [isImageLoading, setIsImageLoading] = useState(true); - - const [cover, setCover] = useState(null); - const [coverLoading, setCoverLoading] = useState(false); - - useEffect(() => { - if(!fullUrl) return; - - if(contentType.startsWith('application/epub') && !cover && !coverLoading){ - setCoverLoading(true); - getCover(fullUrl).then(setCover).finally(()=>{setCoverLoading(false)}) - } - - if (contentType.includes("video/") && !thumbnailUrl) { - setIsVideoThumbLoading(true); - generateVideoThumbnail(fullUrl, (thumb) => { - setGeneratedThumbnail(thumb); - setIsVideoThumbLoading(false); - }); - } - }, [fullUrl, contentType, thumbnailUrl]); - - const commonProps = { - className: `${inModal ? 'w-full h-full sm:object-cover xs:object-fill rounded-xl' : 'absolute inset-0 w-full h-full object-cover'}`, - onError: () => handleRenderError(transaction.id), - }; - - const renderTextBasedContent = () => ( -
- {inModal ? ( -
- {contentType === "application/json" ? formatJSON(content?.textContent) : content?.textContent} -
- ) : ( - -
-
-
- {contentType === "application/json" ? formatJSON(content?.textContent) : content?.textContent} -
-
-
-
- - )} -
- ); - - const BookView = ()=>{ - if (inModal) { - return ( -
}> - -
- -
-
- - ); - } - - if(coverLoading) return ( -
- Loading Please Wait.. -
- ) - - if(!cover) return ( -
- Preview Not Available -
- ) - - return ( -
- - Book cover - -
- ); - } - - const contentMap: ContentMap = { - "audio/": () => ( -
- -
-
- -
- {contentType.split('/')[1].toUpperCase()} -
-
-
-
-
- ), - "video/": () => ( -
- {inModal ? ( -
-
- ) : ( -
- {isVideoThumbLoading ? ( -
- -
- ) : thumbnailUrl || generatedThumbnail ? ( - <> - - Video thumbnail - -
-
- -
-
- - ) : ( -
- -
- )} -
- )} -
- ), - "image/": () => ( - inModal ? ( -
- Content handleRenderError(transaction.id)} - /> -
- ) : ( - - Content { - setIsImageLoading(false); - handleRenderError(transaction.id); - }} - onLoad={() => setIsImageLoading(false)} - /> - - ) - ), - "text/html": () => ( -
-