import React, { useState, useRef, useEffect, useCallback } from "react"; import { Copy, ExternalLink, Check, Share2, MoreHorizontal, X, } from "lucide-react"; import { AturiIcon, BlueskyIcon, BlackskyIcon, WitchskyIcon, CatskyIcon, DeerIcon, } from "../common/Icons"; import { analytics } from "../../lib/analytics"; import { useTranslation } from "react-i18next"; const SembleLogo = () => ( Semble ); const BLUESKY_COLOR = "#1185fe"; interface ShareMenuProps { uri: string; text?: string; customUrl?: string; handle?: string; type?: string; url?: string; } export default function ShareMenu({ uri, text, customUrl, handle, type, url, }: ShareMenuProps) { const { t } = useTranslation(); const [isOpen, setIsOpen] = useState(false); const [copied, setCopied] = useState(null); const [isMobile, setIsMobile] = useState(false); const menuRef = useRef(null); const buttonRef = useRef(null); const sheetRef = useRef(null); const dragStartY = useRef(0); const dragCurrentY = useRef(0); const handleTouchStart = useCallback((e: React.TouchEvent) => { dragStartY.current = e.touches[0].clientY; if (sheetRef.current) sheetRef.current.style.transition = "none"; }, []); const handleTouchMove = useCallback((e: React.TouchEvent) => { const delta = e.touches[0].clientY - dragStartY.current; dragCurrentY.current = delta; if (delta > 0 && sheetRef.current) { sheetRef.current.style.transform = `translateY(${delta}px)`; } }, []); const handleTouchEnd = useCallback(() => { if (sheetRef.current) { sheetRef.current.style.transition = "transform 0.3s ease"; if (dragCurrentY.current > 100) { sheetRef.current.style.transform = "translateY(100%)"; setTimeout(() => setIsOpen(false), 300); } else { sheetRef.current.style.transform = "translateY(0)"; } } dragCurrentY.current = 0; }, []); const [menuPosition, setMenuPosition] = useState({ top: 0, left: 0, alignRight: false, }); useEffect(() => { const check = () => setIsMobile(window.innerWidth < 640); check(); window.addEventListener("resize", check); return () => window.removeEventListener("resize", check); }, []); const getShareUrl = () => { if (customUrl) return customUrl; if (!uri) return ""; const origin = typeof window !== "undefined" ? window.location.origin : ""; const uriParts = uri.split("/"); const rkey = uriParts[uriParts.length - 1]; const did = uriParts[2]; const collection = uriParts[3] ?? ""; const marginSegment = collection.startsWith("at.margin.note") ? "note" : collection.startsWith("at.margin.highlight") ? "highlight" : collection.startsWith("at.margin.bookmark") ? "bookmark" : collection.startsWith("at.margin.annotation") ? "annotation" : null; if (marginSegment && handle) { return `${origin}/${handle}/${marginSegment}/${rkey}`; } if (did && collection && rkey) { return `${origin}/at/${did}/${collection}/${rkey}`; } return `${origin}/at/${did}/${rkey}`; }; const shareUrl = getShareUrl(); const isSemble = uri && uri.includes("network.cosmik"); const sembleUrl = (() => { if (!isSemble) return ""; const parts = (uri || "").split("/"); const rkey = parts[parts.length - 1]; const userHandle = handle || (parts.length > 2 ? parts[2] : ""); if (uri.includes("network.cosmik.collection")) return `https://semble.so/profile/${userHandle}/collections/${rkey}`; if (uri.includes("network.cosmik.card") && url) return `https://semble.so/url?id=${encodeURIComponent(url)}`; return `https://semble.so/profile/${userHandle}`; })(); const handleCopy = async (textToCopy: string, key: string) => { try { await navigator.clipboard.writeText(textToCopy); setCopied(key); analytics.capture("item_shared", { method: "copy_link", destination: key, item_type: type, }); setTimeout(() => { setCopied(null); setIsOpen(false); }, 1000); } catch { prompt("Copy this link:", textToCopy); } }; const handleShareToFork = (domain: string) => { const composeText = text ? `${text.substring(0, 200)}...\n\n${shareUrl}` : shareUrl; const composeUrl = `https://${domain}/intent/compose?text=${encodeURIComponent(composeText)}`; analytics.capture("item_shared", { method: "social_app", destination: domain, item_type: type, }); window.open(composeUrl, "_blank"); setIsOpen(false); }; useEffect(() => { const handleClickOutside = (e: MouseEvent) => { if ( menuRef.current && !menuRef.current.contains(e.target as Node) && !buttonRef.current?.contains(e.target as Node) ) { setIsOpen(false); } }; if (isOpen && !isMobile) { document.addEventListener("mousedown", handleClickOutside); window.addEventListener("scroll", () => setIsOpen(false), true); window.addEventListener("resize", () => setIsOpen(false)); } return () => { document.removeEventListener("mousedown", handleClickOutside); window.removeEventListener("scroll", () => setIsOpen(false), true); window.removeEventListener("resize", () => setIsOpen(false)); }; }, [isOpen, isMobile]); const calculatePosition = () => { if (!buttonRef.current) return; const rect = buttonRef.current.getBoundingClientRect(); const menuWidth = 260; const padding = 8; let top = rect.bottom + 8; let left = rect.left; let alignRight = false; if (left + menuWidth > window.innerWidth - padding) { left = rect.right - menuWidth; alignRight = true; } left = Math.max( padding, Math.min(left, window.innerWidth - menuWidth - padding), ); if (top + 300 > window.innerHeight) { top = rect.top - 8; } setMenuPosition({ top, left, alignRight }); }; const toggleMenu = () => { if (!isOpen && !isMobile) calculatePosition(); setIsOpen(!isOpen); }; const renderMenuItem = ( label: string, icon: React.ReactNode, onClick: () => void, isCopied: boolean = false, highlight: boolean = false, ) => ( ); const shareForks = [ { name: "Bluesky", domain: "bsky.app", icon: , }, { name: "Witchsky", domain: "witchsky.app", icon: , }, { name: "Blacksky", domain: "blacksky.community", icon: , }, { name: "Catsky", domain: "catsky.social", icon: }, { name: "Deer", domain: "deer.social", icon: }, ]; const menuContent = (
{isSemble ? ( <>
{t("shareMenu.sembleIntegration")}
{renderMenuItem( t("shareMenu.openOnSemble"), , () => window.open(sembleUrl, "_blank"), false, true, )} {renderMenuItem( t("shareMenu.copySembleLink"), , () => handleCopy(sembleUrl, "semble"), copied === "semble", )}
) : null} {renderMenuItem( t("shareMenu.copyLink"), , () => handleCopy(shareUrl, "link"), copied === "link", )}
{t("shareMenu.shareViaApp")}
{shareForks.map((fork) => ( ))}
{renderMenuItem( t("shareMenu.copyUniversalLink"), , () => handleCopy(uri.replace("at://", "https://aturi.to/"), "aturi"), copied === "aturi", )} {typeof navigator !== "undefined" && navigator.share && renderMenuItem( t("shareMenu.moreOptions"), , () => { navigator .share({ title: "Margin", text, url: shareUrl }) .catch(() => {}); setIsOpen(false); }, )}
); return (
{isOpen && isMobile && ( <>
setIsOpen(false)} />
{t("shareMenu.share", { defaultValue: "Share" })}
{menuContent}
)} {isOpen && !isMobile && (
{menuContent}
)}
); }