UNPKG

@activecollab/components

Version:

ActiveCollab Components

147 lines (140 loc) 5.04 kB
import _extends from "@babel/runtime/helpers/esm/extends"; import { useCallback, useEffect, useRef, useState } from "react"; /** * Mock unfurl service for the LinkCard concept. * * A link preview is a LIFECYCLE, not two static layouts. The real service is * server-side (SSRF-guarded, oEmbed → OpenGraph → Twitter Cards → <title>, * cached ~24h with a stored `fetchedAt` and a manual re-unfurl affordance — see * note 52104 §B3). This is a promise-based fake with a configurable delay and a * failure toggle so the story can drive the whole lifecycle without a network. * * The data arrives on a PROGRESSIVE LADDER of three tiers, each carrying more * than the last: * url → just the raw URL (nothing fetched yet) * basic → favicon + title + domain * full → + description, hero image, media kind / duration * The card reserves the next tier's space at every tier, so climbing the ladder * never reflows the card. */ /** The cumulative data shape. Fields fill in as the ladder climbs. */ /** A seed link — what the fake "server" would resolve the URL to at tier full. */ export const MOCK_LINKS = { vimeo: { url: "https://vimeo.com/876631121", faviconColor: "#0F6E56", title: "Brand refresh — motion explorations for the spring launch", domain: "vimeo.com", description: "A reel of motion studies exploring the refreshed brand system: type in motion, the new mark, and transitions for the marketing site.", heroColor: "#1D9E75", kind: "Video", duration: "4:32", playable: true }, history: { url: "https://ww2history.org/timeline", faviconColor: "#8C5A2B", title: "The Second World War • Timeline, key battles, and firsthand accounts", domain: "ww2history.org", description: "An illustrated timeline of the Second World War with primary sources, maps, and firsthand accounts from each theatre of the war.", heroColor: "#B5793B", kind: "Article", playable: false } }; /** Progressive tier data derived from a seed link. */ const tierData = (link, tier) => { if (tier === "url") return { url: link.url }; if (tier === "basic") { return { url: link.url, faviconColor: link.faviconColor, title: link.title, domain: link.domain }; } return _extends({}, link); }; /** * Drives one link through the unfurl lifecycle. Climbs url → basic → full on a * timer, or lands in `error` if `shouldFail`. `retry` / `reunfurl` re-run it; * `markStale` moves a resolved card to the stale state so the re-unfurl * affordance can be shown. */ export const useUnfurl = _ref => { let link = _ref.link, _ref$delayMs = _ref.delayMs, delayMs = _ref$delayMs === void 0 ? 900 : _ref$delayMs, _ref$shouldFail = _ref.shouldFail, shouldFail = _ref$shouldFail === void 0 ? false : _ref$shouldFail; const _useState = useState("resolving"), status = _useState[0], setStatus = _useState[1]; const _useState2 = useState("url"), tier = _useState2[0], setTier = _useState2[1]; const _useState3 = useState(() => tierData(link, "url")), data = _useState3[0], setData = _useState3[1]; const _useState4 = useState(null), fetchedAt = _useState4[0], setFetchedAt = _useState4[1]; const timers = useRef([]); const clearTimers = useCallback(() => { timers.current.forEach(clearTimeout); timers.current = []; }, []); const start = useCallback(() => { clearTimers(); setStatus("resolving"); setTier("url"); setData(tierData(link, "url")); if (shouldFail) { // Fail partway: the favicon + title never arrive; we keep the raw URL. timers.current.push(setTimeout(() => setStatus("error"), delayMs)); return; } timers.current.push(setTimeout(() => { setTier("basic"); setData(tierData(link, "basic")); }, delayMs)); timers.current.push(setTimeout(() => { setTier("full"); setData(tierData(link, "full")); setStatus("resolved"); setFetchedAt(Date.now()); }, delayMs * 2)); }, [clearTimers, link, delayMs, shouldFail]); const markStale = useCallback(() => { setStatus(prev => prev === "resolved" ? "stale" : prev); }, []); // Kick off on mount and whenever the config that defines the run changes. useEffect(() => { start(); return clearTimers; }, [start, clearTimers]); return { status, tier, data, fetchedAt, start, retry: start, reunfurl: start, markStale }; }; /** Human-readable "fetched N ago" for the stale affordance. */ export const formatFetchedAt = fetchedAt => { if (!fetchedAt) return ""; const seconds = Math.max(1, Math.round((Date.now() - fetchedAt) / 1000)); if (seconds < 60) return seconds + "s ago"; const minutes = Math.round(seconds / 60); if (minutes < 60) return minutes + "m ago"; const hours = Math.round(minutes / 60); if (hours < 24) return hours + "h ago"; return Math.round(hours / 24) + "d ago"; }; //# sourceMappingURL=unfurl.js.map