Write on the margins of the internet. Powered by the AT Protocol. margin.at
extension web atproto comments
at ui-refactor 341 lines 19 kB view raw
1import { useState, useRef, useEffect } from "react"; 2import { Copy, ExternalLink, Check } from "lucide-react"; 3import { BlueskyIcon, AturiIcon } from "./Icons"; 4 5const BLUESKY_COLOR = "#1185fe"; 6 7const WitchskyIcon = () => ( 8 <svg fill="none" viewBox="0 0 512 512" width="18" height="18"> 9 <path 10 fill="#ee5346" 11 d="M374.473 57.7173C367.666 50.7995 357.119 49.1209 348.441 53.1659C347.173 53.7567 342.223 56.0864 334.796 59.8613C326.32 64.1696 314.568 70.3869 301.394 78.0596C275.444 93.1728 242.399 114.83 218.408 139.477C185.983 172.786 158.719 225.503 140.029 267.661C130.506 289.144 122.878 308.661 117.629 322.81C116.301 326.389 115.124 329.63 114.104 332.478C87.1783 336.42 64.534 341.641 47.5078 348.101C37.6493 351.84 28.3222 356.491 21.0573 362.538C13.8818 368.511 6.00003 378.262 6.00003 391.822C6.00014 403.222 11.8738 411.777 17.4566 417.235C23.0009 422.655 29.9593 426.793 36.871 430.062C50.8097 436.653 69.5275 441.988 90.8362 446.249C133.828 454.846 192.21 460 256.001 460C319.79 460 378.172 454.846 421.164 446.249C442.472 441.988 461.19 436.653 475.129 430.062C482.041 426.793 488.999 422.655 494.543 417.235C500.039 411.862 505.817 403.489 505.996 392.353L506 391.822L505.995 391.188C505.754 377.959 498.012 368.417 490.945 362.534C483.679 356.485 474.35 351.835 464.491 348.095C446.749 341.366 422.906 335.982 394.476 331.987C393.6 330.57 392.633 328.995 391.595 327.273C386.477 318.777 379.633 306.842 372.737 293.115C358.503 264.781 345.757 232.098 344.756 206.636C343.87 184.121 351.638 154.087 360.819 127.789C365.27 115.041 369.795 103.877 373.207 95.9072C374.909 91.9309 376.325 88.7712 377.302 86.6328C377.79 85.5645 378.167 84.7524 378.416 84.2224C378.54 83.9579 378.632 83.7635 378.69 83.643C378.718 83.5829 378.739 83.5411 378.75 83.5181C378.753 83.5108 378.756 83.5049 378.757 83.5015C382.909 74.8634 381.196 64.5488 374.473 57.7173Z" 12 /> 13 </svg> 14); 15 16const BlackskyIcon = () => ( 17 <svg viewBox="0 0 285 285" width="18" height="18"> 18 <path 19 fill="#f9faf9" 20 d="M148.846 144.562C148.846 159.75 161.158 172.062 176.346 172.062H207.012V185.865H176.346C161.158 185.865 148.846 198.177 148.846 213.365V243.045H136.029V213.365C136.029 198.177 123.717 185.865 108.529 185.865H77.8633V172.062H108.529C123.717 172.062 136.029 159.75 136.029 144.562V113.896H148.846V144.562Z" 21 /> 22 <path 23 fill="#f9faf9" 24 d="M170.946 31.8766C160.207 42.616 160.207 60.0281 170.946 70.7675L192.631 92.4516L182.871 102.212L161.186 80.5275C150.447 69.7881 133.035 69.7881 122.296 80.5275L101.309 101.514L92.2456 92.4509L113.232 71.4642C123.972 60.7248 123.972 43.3128 113.232 32.5733L91.5488 10.8899L101.309 1.12988L122.993 22.814C133.732 33.5533 151.144 33.5534 161.884 22.814L183.568 1.12988L192.631 10.1925L170.946 31.8766Z" 25 /> 26 <path 27 fill="#f9faf9" 28 d="M79.0525 75.3259C75.1216 89.9962 83.8276 105.076 98.498 109.006L128.119 116.943L124.547 130.275L94.9267 122.338C80.2564 118.407 65.1772 127.113 61.2463 141.784L53.5643 170.453L41.1837 167.136L48.8654 138.467C52.7963 123.797 44.0902 108.718 29.4199 104.787L-0.201172 96.8497L3.37124 83.5173L32.9923 91.4542C47.6626 95.3851 62.7419 86.679 66.6728 72.0088L74.6098 42.3877L86.9895 45.7048L79.0525 75.3259Z" 29 /> 30 <path 31 fill="#f9faf9" 32 d="M218.413 71.4229C222.344 86.093 237.423 94.7992 252.094 90.8683L281.715 82.9313L285.287 96.2628L255.666 104.2C240.995 108.131 232.29 123.21 236.22 137.88L243.902 166.55L231.522 169.867L223.841 141.198C219.91 126.528 204.831 117.822 190.16 121.753L160.539 129.69L156.967 116.357L186.588 108.42C201.258 104.49 209.964 89.4103 206.033 74.74L198.096 45.1189L210.476 41.8018L218.413 71.4229Z" 33 /> 34 </svg> 35); 36 37const CatskyIcon = () => ( 38 <svg fill="none" viewBox="0 0 67.733328 67.733329" width="18" height="18"> 39 <path 40 fill="#cba7f7" 41 d="m 7.4595521,49.230487 -1.826355,1.186314 -0.00581,0.0064 c -0.6050542,0.41651 -1.129182,0.831427 -1.5159445,1.197382 -0.193382,0.182977 -0.3509469,0.347606 -0.4862911,0.535791 -0.067671,0.0941 -0.1322972,0.188188 -0.1933507,0.352343 -0.061048,0.164157 -0.1411268,0.500074 0.025624,0.844456 l 0.099589,0.200339 c 0.1666616,0.344173 0.4472046,0.428734 0.5969419,0.447854 0.1497358,0.01912 0.2507411,0.0024 0.352923,-0.02039 0.204367,-0.04555 0.4017284,-0.126033 0.6313049,-0.234117 0.4549828,-0.214229 1.0166476,-0.545006 1.6155328,-0.956275 l 0.014617,-0.01049 2.0855152,-1.357536 C 8.3399261,50.711052 7.8735929,49.979321 7.4596148,49.230532 Z" 42 /> 43 <path 44 fill="#cba7f7" 45 d="m 60.225246,49.199041 c -0.421632,0.744138 -0.895843,1.47112 -1.418104,2.178115 l 2.170542,1.413443 c 0.598885,0.411268 1.160549,0.742047 1.615532,0.956276 0.229578,0.108104 0.426937,0.188564 0.631304,0.234116 0.102186,0.02278 0.2061,0.03951 0.355838,0.02039 0.148897,-0.01901 0.427619,-0.104957 0.594612,-0.444358 l 0.0029,-0.0035 0.09667,-0.20034 h 0.0029 c 0.166756,-0.34438 0.08667,-0.680303 0.02562,-0.844455 -0.06104,-0.164158 -0.125675,-0.258251 -0.193352,-0.352343 -0.135356,-0.188186 -0.293491,-0.352814 -0.486873,-0.535792 -0.386891,-0.366 -0.911016,-0.780916 -1.516073,-1.197426 l -0.0082,-0.007 z" 46 /> 47 <path 48 fill="#cba7f7" 49 d="m 62.374822,42.996075 c -0.123437,0.919418 -0.330922,1.827482 -0.614997,2.71973 h 2.864745 c 0.698786,0 1.328766,-0.04848 1.817036,-0.1351 0.244137,-0.04331 0.449793,-0.09051 0.645864,-0.172979 0.09803,-0.04122 0.194035,-0.08458 0.315651,-0.190439 0.121618,-0.105868 0.330211,-0.348705 0.330211,-0.746032 v -0.233536 c 0,-0.397326 -0.208544,-0.637282 -0.330211,-0.743122 -0.121662,-0.105838 -0.217613,-0.152159 -0.315651,-0.193351 -0.196079,-0.08238 -0.401748,-0.129732 -0.645864,-0.17296 -0.488229,-0.08645 -1.118333,-0.132208 -1.817036,-0.132208 z" 50 /> 51 <path 52 fill="#cba7f7" 53 d="m 3.1074004,42.996075 c -0.6987018,0 -1.3264778,0.04576 -1.8147079,0.132208 -0.2441143,0.04324 -0.44978339,0.09059 -0.64586203,0.17296 -0.0980369,0.04118 -0.19398758,0.08751 -0.31565316,0.193351 C 0.20951466,43.600432 0.0015501,43.84039 0.0015501,44.237717 v 0.233535 c 0,0.397326 0.20800926,0.640175 0.32962721,0.746034 0.12161784,0.105867 0.21761904,0.149206 0.31565316,0.190437 0.19606972,0.08246 0.40172683,0.129657 0.64586203,0.172979 0.4882704,0.08663 1.1159226,0.1351 1.8147079,0.1351 H 5.9517617 C 5.6756425,44.822849 5.4740706,43.914705 5.3542351,42.996072 Z" 54 /> 55 <path 56 fill="#cba7f7" 57 d="m 64.667084,33.5073 c -0.430203,0 -0.690808,0.160181 -1.103618,0.372726 -0.41281,0.212535 -0.895004,0.507161 -1.40529,0.858434 l -0.84038,0.578305 c 0.360074,0.820951 0.644317,1.675211 0.844456,2.560741 l 1.136813,-0.78214 c 0.605058,-0.41651 1.12918,-0.834919 1.515944,-1.200875 0.193382,-0.182976 0.350947,-0.347609 0.486291,-0.535795 0.06767,-0.0941 0.132313,-0.188185 0.193351,-0.352341 0.06104,-0.164157 0.141126,-0.497171 -0.02562,-0.841544 L 65.369444,33.96156 C 65.163418,33.537073 64.829889,33.5073 64.669999,33.5073 Z" 58 /> 59 <path 60 fill="#cba7f7" 61 d="m 3.0648864,33.5073 c -0.1600423,3.64e-4 -0.4969719,0.0355 -0.7000249,0.45426 l -0.099589,0.203251 c -0.16676,0.344375 -0.089013,0.677388 -0.027951,0.841544 0.061047,0.164157 0.1285982,0.258248 0.1962636,0.352341 0.1353547,0.188186 0.2899962,0.352819 0.4833782,0.535795 0.386764,0.365956 0.9138003,0.784365 1.518856,1.200875 l 1.1478766,0.78971 c 0.2068,-0.879769 0.5000939,-1.727856 0.8706646,-2.542104 v -5.81e-4 L 5.5761273,34.73846 C 5.065553,34.38699 4.5814871,34.09259 4.1685053,33.880026 3.7555236,33.667462 3.4962107,33.506322 3.0648893,33.5073 Z" 62 /> 63 <path 64 fill="#cba7f7" 65 d="m 34.206496,25.930929 c -7.358038,0 -14.087814,1.669555 -18.851571,4.452678 -4.763758,2.783122 -7.4049994,6.472247 -7.4049994,10.665932 0,4.229683 2.6374854,8.946766 7.2694834,12.60017 4.631996,3.653402 11.153152,6.176813 18.420538,6.176813 7.267388,0 13.908863,-2.52485 18.657979,-6.185354 4.749117,-3.660501 7.485285,-8.390746 7.485285,-12.591629 0,-4.236884 -2.494219,-7.904081 -7.079874,-10.67732 -4.585655,-2.773237 -11.1388,-4.44129 -18.496841,-4.44129 z" 66 /> 67 <path 68 fill="#cba7f7" 69 d="m 51.797573,6.1189692 c -0.02945,-7.175e-4 -0.05836,4.17e-5 -0.08736,5.831e-4 -0.143066,0.00254 -0.278681,0.00746 -0.419898,0.094338 -0.483586,0.2975835 -0.980437,0.9277726 -1.446058,1.5345809 -1.170891,1.5259255 -2.372514,3.8701448 -4.229269,7.0095668 -0.839492,1.419423 -2.308256,4.55051 -3.891486,8.089307 4.831393,0.745951 9.148869,2.222975 12.643546,4.336427 2.130458,1.288425 3.976812,2.848736 5.416167,4.643344 C 58.614334,27.483611 57.260351,22.206768 56.421696,19.015263 55.149066,14.172268 54.241403,10.340754 53.185389,8.0524745 52.815225,7.2503647 52.540611,6.4969378 52.052073,6.1836069 51.974407,6.1337905 51.885945,6.1211124 51.79757,6.1189646 Z" 70 /> 71 <path 72 fill="#cba7f7" 73 d="m 15.935563,6.1189692 c -0.08837,0.00223 -0.176832,0.014766 -0.254502,0.064642 -0.48854,0.3133308 -0.763154,1.0667562 -1.13332,1.8688677 -1.056011,2.2882791 -1.963673,6.1197931 -3.236303,10.9627891 -0.85539,3.255187 -2.247014,8.680054 -3.4314032,13.071013 1.5346704,-1.910372 3.5390122,-3.56005 5.8517882,-4.91124 3.456591,-2.019439 7.668347,-3.458497 12.320324,-4.231015 C 24.452511,19.365796 22.96466,16.190327 22.117564,14.758042 20.260808,11.61862 19.059771,9.2744012 17.888878,7.7484762 17.423256,7.1416679 16.926404,6.5114787 16.442819,6.2138951 16.301603,6.127059 16.165987,6.1222115 16.02292,6.1195569 c -0.02901,-5.429e-4 -0.0579,-0.0013 -0.08734,-5.847e-4 z" 74 /> 75 </svg> 76); 77 78const DeerIcon = () => ( 79 <svg fill="none" viewBox="0 0 512 512" width="18" height="18"> 80 <path 81 fill="#739f7c" 82 d="m 149.96484,186.56641 46.09766,152.95898 c 0,0 -6.30222,-9.61174 -15.60547,-17.47656 -8.87322,-7.50128 -28.4082,-4.04492 -28.4082,-4.04492 0,0 6.14721,39.88867 15.53125,44.39843 10.71251,5.1482 22.19726,0.16993 22.19726,0.16993 0,0 11.7613,-4.87282 22.82032,31.82421 5.26534,17.47196 15.33258,50.877 20.9707,69.58594 2.16717,7.1913 8.83789,7.25781 8.83789,7.25781 0,0 6.67072,-0.0665 8.83789,-7.25781 5.63812,-18.70894 15.70536,-52.11398 20.9707,-69.58594 11.05902,-36.69703 22.82032,-31.82421 22.82032,-31.82421 0,0 11.48475,4.97827 22.19726,-0.16993 9.38404,-4.50976 15.5332,-44.39843 15.5332,-44.39843 0,0 -19.53693,-3.45636 -28.41015,4.04492 -9.30325,7.86482 -15.60547,17.47656 -15.60547,17.47656 l 46.09766,-152.95898 -49.32618,83.84179 -20.34375,-31.1914 6.35547,54.96875 -23.1582,39.36132 c 0,0 -2.97595,5.06226 -5.94336,4.68946 -0.009,-0.001 -0.0169,0.003 -0.0254,0.01 -0.008,-0.007 -0.0167,-0.0109 -0.0254,-0.01 -2.96741,0.3728 -5.94336,-4.68946 -5.94336,-4.68946 l -23.1582,-39.36132 6.35547,-54.96875 -20.34375,31.1914 z" 83 transform="matrix(2.6921023,0,0,1.7145911,-396.58283,-308.01527)" 84 /> 85 </svg> 86); 87 88const BLUESKY_FORKS = [ 89 { 90 name: "Bluesky", 91 domain: "bsky.app", 92 Icon: () => <BlueskyIcon size={18} color={BLUESKY_COLOR} />, 93 }, 94 { name: "Witchsky", domain: "witchsky.app", Icon: WitchskyIcon }, 95 { name: "Blacksky", domain: "blacksky.community", Icon: BlackskyIcon }, 96 { name: "Catsky", domain: "catsky.social", Icon: CatskyIcon }, 97 { name: "Deer", domain: "deer.social", Icon: DeerIcon }, 98]; 99 100export default function ShareMenu({ uri, text, customUrl, handle, type, url }) { 101 const [isOpen, setIsOpen] = useState(false); 102 const [copied, setCopied] = useState(false); 103 const [copiedAturi, setCopiedAturi] = useState(false); 104 const menuRef = useRef(null); 105 106 const getShareUrl = () => { 107 if (customUrl) return customUrl; 108 if (!uri) return ""; 109 110 const uriParts = uri.split("/"); 111 const rkey = uriParts[uriParts.length - 1]; 112 const did = uriParts[2]; 113 114 if (uri.includes("network.cosmik.card")) { 115 return `${window.location.origin}/at/${did}/${rkey}`; 116 } 117 118 if (handle && type) { 119 return `${window.location.origin}/${handle}/${type.toLowerCase()}/${rkey}`; 120 } 121 122 return `${window.location.origin}/at/${did}/${rkey}`; 123 }; 124 125 const shareUrl = getShareUrl(); 126 127 useEffect(() => { 128 const handleClickOutside = (e) => { 129 if (menuRef.current && !menuRef.current.contains(e.target)) { 130 setIsOpen(false); 131 } 132 }; 133 134 const card = menuRef.current?.closest(".card"); 135 if (card) { 136 if (isOpen) { 137 card.style.zIndex = "50"; 138 } else { 139 card.style.zIndex = ""; 140 } 141 } 142 143 if (isOpen) { 144 document.addEventListener("mousedown", handleClickOutside); 145 } 146 return () => document.removeEventListener("mousedown", handleClickOutside); 147 }, [isOpen]); 148 149 const handleShareToFork = (domain) => { 150 const composeText = text 151 ? `${text.substring(0, 200)}...\n\n${shareUrl}` 152 : shareUrl; 153 const composeUrl = `https://${domain}/intent/compose?text=${encodeURIComponent(composeText)}`; 154 window.open(composeUrl, "_blank"); 155 setIsOpen(false); 156 }; 157 158 const handleCopy = async () => { 159 try { 160 await navigator.clipboard.writeText(shareUrl); 161 setCopied(true); 162 setTimeout(() => { 163 setCopied(false); 164 setIsOpen(false); 165 }, 1500); 166 } catch { 167 prompt("Copy this link:", shareUrl); 168 } 169 }; 170 171 const handleCopyAturi = async () => { 172 const aturiUrl = uri ? uri.replace("at://", "https://aturi.to/") : ""; 173 if (!aturiUrl) return; 174 175 try { 176 await navigator.clipboard.writeText(aturiUrl); 177 setCopiedAturi(true); 178 setTimeout(() => { 179 setCopiedAturi(false); 180 setIsOpen(false); 181 }, 1500); 182 } catch { 183 prompt("Copy this link:", aturiUrl); 184 } 185 }; 186 187 const handleSystemShare = async () => { 188 if (navigator.share) { 189 try { 190 await navigator.share({ 191 title: "Margin Annotation", 192 text: text?.substring(0, 100), 193 url: shareUrl, 194 }); 195 } catch { 196 /* ignore */ 197 } 198 } 199 setIsOpen(false); 200 }; 201 202 const isSemble = uri && uri.includes("network.cosmik"); 203 const sembleUrl = (() => { 204 if (!isSemble) return ""; 205 const parts = uri.split("/"); 206 const rkey = parts[parts.length - 1]; 207 const userHandle = handle || (parts.length > 2 ? parts[2] : ""); 208 209 if (uri.includes("network.cosmik.collection")) { 210 return `https://semble.so/profile/${userHandle}/collections/${rkey}`; 211 } 212 213 if (uri.includes("network.cosmik.card") && url) { 214 return `https://semble.so/url?id=${encodeURIComponent(url)}`; 215 } 216 217 return `https://semble.so/profile/${userHandle}`; 218 })(); 219 220 const handleCopySemble = async () => { 221 try { 222 await navigator.clipboard.writeText(sembleUrl); 223 setCopied(true); 224 setTimeout(() => { 225 setCopied(false); 226 setIsOpen(false); 227 }, 1500); 228 } catch { 229 prompt("Copy this link:", sembleUrl); 230 } 231 }; 232 233 return ( 234 <div className="share-menu-container" ref={menuRef}> 235 <button 236 className="annotation-action" 237 onClick={() => setIsOpen(!isOpen)} 238 title="Share" 239 > 240 <svg 241 width="18" 242 height="18" 243 viewBox="0 0 24 24" 244 fill="none" 245 stroke="currentColor" 246 strokeWidth="2" 247 strokeLinecap="round" 248 strokeLinejoin="round" 249 > 250 <circle cx="18" cy="5" r="3" /> 251 <circle cx="6" cy="12" r="3" /> 252 <circle cx="18" cy="19" r="3" /> 253 <line x1="8.59" y1="13.51" x2="15.42" y2="17.49" /> 254 <line x1="15.41" y1="6.51" x2="8.59" y2="10.49" /> 255 </svg> 256 </button> 257 258 {isOpen && ( 259 <div className="share-menu"> 260 {isSemble ? ( 261 <> 262 <div className="share-menu-section"> 263 <div 264 className="share-menu-label" 265 style={{ display: "flex", alignItems: "center", gap: "6px" }} 266 > 267 <img 268 src="/semble-logo.svg" 269 alt="" 270 style={{ width: "12px", height: "12px" }} 271 /> 272 Semble 273 </div> 274 <a 275 href={sembleUrl} 276 target="_blank" 277 rel="noopener noreferrer" 278 className="share-menu-item" 279 style={{ textDecoration: "none" }} 280 > 281 <ExternalLink size={16} /> 282 <span>Open on Semble</span> 283 </a> 284 <button className="share-menu-item" onClick={handleCopySemble}> 285 {copied ? <Check size={16} /> : <Copy size={16} />} 286 <span>{copied ? "Copied!" : "Copy Semble Link"}</span> 287 </button> 288 </div> 289 <div className="share-menu-divider" /> 290 <button 291 className="share-menu-item" 292 onClick={handleCopyAturi} 293 title="Copy Universal URL" 294 > 295 {copiedAturi ? <Check size={16} /> : <AturiIcon size={16} />} 296 <span>{copiedAturi ? "Copied!" : "Copy Universal URL"}</span> 297 </button> 298 </> 299 ) : ( 300 <> 301 <div className="share-menu-section"> 302 <div className="share-menu-label">Share to</div> 303 {BLUESKY_FORKS.map((fork) => ( 304 <button 305 key={fork.domain} 306 className="share-menu-item" 307 onClick={() => handleShareToFork(fork.domain)} 308 > 309 <span className="share-menu-icon"> 310 <fork.Icon /> 311 </span> 312 <span>{fork.name}</span> 313 </button> 314 ))} 315 </div> 316 <div className="share-menu-divider" /> 317 <button className="share-menu-item" onClick={handleCopy}> 318 {copied ? <Check size={16} /> : <Copy size={16} />} 319 <span>{copied ? "Copied!" : "Copy Link"}</span> 320 </button> 321 <button 322 className="share-menu-item" 323 onClick={handleCopyAturi} 324 title="Copy a universal link atproto link (via aturi.to)" 325 > 326 {copiedAturi ? <Check size={16} /> : <AturiIcon size={16} />} 327 <span>{copiedAturi ? "Copied!" : "Copy Universal Link"}</span> 328 </button> 329 {navigator.share && ( 330 <button className="share-menu-item" onClick={handleSystemShare}> 331 <ExternalLink size={16} /> 332 <span>More...</span> 333 </button> 334 )} 335 </> 336 )} 337 </div> 338 )} 339 </div> 340 ); 341}