Margin is an open annotation layer for the internet. Powered by the AT Protocol. margin.at
extension web atproto comments

more bug fixes and additions

+97 -48
+2 -1
web/src/api/client.ts
··· 217 217 target: target, 218 218 viewer: raw.viewer || { like: raw.viewerHasLiked ? "true" : undefined }, 219 219 motivation: raw.motivation || "highlighting", 220 + parentUri: (raw as Record<string, unknown>).inReplyTo as string | undefined, 220 221 }; 221 222 } 222 223 ··· 699 700 700 701 export async function getCollections(creator?: string): Promise<Collection[]> { 701 702 try { 702 - const query = creator ? `?creator=${encodeURIComponent(creator)}` : ""; 703 + const query = creator ? `?author=${encodeURIComponent(creator)}` : ""; 703 704 const res = await apiRequest(`/api/collections${query}`); 704 705 if (!res.ok) throw new Error("Failed to fetch collections"); 705 706 const data = await res.json();
+44 -10
web/src/components/common/Card.tsx
··· 83 83 try { 84 84 const hostname = safeUrlHostname(url); 85 85 if (hostname) { 86 + if ( 87 + hostname === "margin.at" || 88 + hostname.endsWith(".margin.at") || 89 + hostname === "semble.so" || 90 + hostname.endsWith(".semble.so") 91 + ) { 92 + window.open(url, "_blank", "noopener,noreferrer"); 93 + return; 94 + } 86 95 const skipped = $preferences.get().externalLinkSkippedHostnames; 87 96 if (skipped.includes(hostname)) { 88 97 window.open(url, "_blank", "noopener,noreferrer"); ··· 101 110 102 111 const timestamp = item.createdAt 103 112 ? formatDistanceToNow(new Date(item.createdAt), { addSuffix: false }) 113 + .replace("less than a minute", "just now") 104 114 .replace("about ", "") 105 115 .replace(" hours", "h") 106 116 .replace(" hour", "h") ··· 229 239 <span className="text-surface-400 dark:text-surface-500 text-sm"> 230 240 {timestamp} 231 241 </span> 232 - {isSemble && ( 233 - <span className="inline-flex items-center gap-1 text-[10px] text-surface-400 dark:text-surface-500 uppercase font-medium tracking-wide"> 234 - · via{" "} 235 - <img 236 - src="/semble-logo.svg" 237 - alt="Semble" 238 - className="h-3 opacity-70" 239 - /> 240 - </span> 241 - )} 242 + {isSemble && 243 + (() => { 244 + const uri = item.uri || ""; 245 + const parts = uri.replace("at://", "").split("/"); 246 + const userHandle = item.author?.handle || parts[0] || ""; 247 + const rkey = parts[2] || ""; 248 + const targetUrl = item.target?.source || item.source || ""; 249 + let sembleUrl = `https://semble.so/profile/${userHandle}`; 250 + if (uri.includes("network.cosmik.collection")) 251 + sembleUrl = `https://semble.so/profile/${userHandle}/collections/${rkey}`; 252 + else if (uri.includes("network.cosmik.card") && targetUrl) 253 + sembleUrl = `https://semble.so/url?id=${encodeURIComponent(targetUrl)}`; 254 + return ( 255 + <span className="relative inline-flex items-center"> 256 + <span className="text-surface-300 dark:text-surface-600"> 257 + · 258 + </span> 259 + <button 260 + onClick={(e) => handleExternalClick(e, sembleUrl)} 261 + className="group/semble relative inline-flex items-center ml-1 cursor-pointer" 262 + > 263 + <img 264 + src="/semble-logo.svg" 265 + alt="Semble" 266 + className="h-3.5" 267 + /> 268 + <span className="pointer-events-none absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-2.5 py-1 rounded-lg bg-surface-800 dark:bg-surface-700 text-white text-[11px] font-medium whitespace-nowrap opacity-0 group-hover/semble:opacity-100 transition-opacity shadow-lg"> 269 + Open in Semble 270 + <span className="absolute top-full left-1/2 -translate-x-1/2 -mt-px border-4 border-transparent border-t-surface-800 dark:border-t-surface-700" /> 271 + </span> 272 + </button> 273 + </span> 274 + ); 275 + })()} 242 276 </div> 243 277 244 278 {pageUrl && !isBookmark && (
+50 -36
web/src/components/modals/ExternalLinkModal.tsx
··· 1 1 import React, { useState } from "react"; 2 2 import { Button } from "../ui"; 3 - import { ExternalLink, AlertTriangle } from "lucide-react"; 3 + import { ExternalLink, Shield } from "lucide-react"; 4 4 import { addSkippedHostname } from "../../store/preferences"; 5 5 6 6 interface ExternalLinkModalProps { ··· 42 42 })(); 43 43 44 44 return ( 45 - <div className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm animate-fade-in"> 46 - <div className="bg-white dark:bg-surface-900 rounded-2xl shadow-2xl max-w-sm w-full animate-scale-in ring-1 ring-black/5 dark:ring-white/10 p-6"> 47 - <div className="flex flex-col items-center text-center"> 48 - <div className="w-12 h-12 bg-yellow-100 dark:bg-yellow-900/30 text-yellow-600 dark:text-yellow-400 rounded-full flex items-center justify-center mb-4"> 49 - <AlertTriangle size={24} /> 45 + <div 46 + className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/40 backdrop-blur-sm animate-fade-in" 47 + onClick={onClose} 48 + > 49 + <div 50 + className="bg-white dark:bg-surface-900 rounded-xl shadow-2xl max-w-md w-full animate-scale-in ring-1 ring-surface-200 dark:ring-surface-700 overflow-hidden" 51 + onClick={(e) => e.stopPropagation()} 52 + > 53 + <div className="px-6 pt-6 pb-4"> 54 + <div className="flex items-start gap-3"> 55 + <div className="w-9 h-9 bg-primary-100 dark:bg-primary-900/30 text-primary-600 dark:text-primary-400 rounded-lg flex items-center justify-center flex-shrink-0 mt-0.5"> 56 + <Shield size={18} /> 57 + </div> 58 + <div className="min-w-0"> 59 + <h2 className="text-base font-semibold text-surface-900 dark:text-white"> 60 + Leaving Margin 61 + </h2> 62 + <p className="text-sm text-surface-500 dark:text-surface-400 mt-1"> 63 + You're about to visit an external site. 64 + </p> 65 + </div> 50 66 </div> 51 67 52 - <h2 className="text-xl font-bold text-surface-900 dark:text-white mb-2"> 53 - You are leaving Margin 54 - </h2> 55 - 56 - <p className="text-surface-500 dark:text-surface-400 text-sm mb-6 leading-relaxed"> 57 - This link will take you to an external website: 58 - <br /> 59 - <span className="font-medium text-sm bg-surface-100 dark:bg-surface-800 text-surface-900 dark:text-surface-100 p-3 rounded-xl mt-3 block break-all border border-surface-200 dark:border-surface-700 shadow-sm"> 68 + <div className="mt-4 flex items-center gap-2 bg-surface-50 dark:bg-surface-800/60 border border-surface-200 dark:border-surface-700 rounded-lg px-3 py-2.5"> 69 + <ExternalLink 70 + size={14} 71 + className="text-surface-400 dark:text-surface-500 flex-shrink-0" 72 + /> 73 + <span className="text-sm text-surface-700 dark:text-surface-300 break-all line-clamp-2"> 60 74 {displayUrl} 61 75 </span> 62 - </p> 76 + </div> 77 + </div> 63 78 64 - <div className="flex items-center gap-2 mb-6 w-full px-1"> 79 + <div className="px-6 pb-5 pt-2 flex flex-col gap-3"> 80 + <label className="flex items-center gap-2 cursor-pointer select-none group"> 65 81 <input 66 82 type="checkbox" 67 - id="dontAskAgain" 68 83 checked={dontAskAgain} 69 84 onChange={(e) => setDontAskAgain(e.target.checked)} 70 - className="rounded border-surface-300 text-primary-600 focus:ring-primary-500 w-4 h-4 cursor-pointer" 85 + className="rounded border-surface-300 dark:border-surface-600 text-primary-600 focus:ring-primary-500 w-3.5 h-3.5 cursor-pointer" 71 86 /> 72 - <label 73 - htmlFor="dontAskAgain" 74 - className="text-sm text-surface-600 dark:text-surface-300 cursor-pointer select-none" 75 - > 76 - Don't ask again for{" "} 77 - <span className="font-medium">{hostname}</span> 78 - </label> 79 - </div> 87 + <span className="text-xs text-surface-500 dark:text-surface-400 group-hover:text-surface-600 dark:group-hover:text-surface-300 transition-colors"> 88 + Always allow links to{" "} 89 + <span className="font-medium text-surface-700 dark:text-surface-200"> 90 + {hostname} 91 + </span> 92 + </span> 93 + </label> 80 94 81 - <div className="flex flex-col gap-3 w-full"> 95 + <div className="flex gap-2"> 82 96 <Button 83 - onClick={handleContinue} 84 - variant="primary" 85 - className="w-full justify-center" 86 - icon={<ExternalLink size={16} />} 97 + onClick={onClose} 98 + variant="ghost" 99 + className="flex-1 justify-center" 87 100 > 88 - Continue to Site 101 + Cancel 89 102 </Button> 90 103 <Button 91 - onClick={onClose} 92 - variant="ghost" 93 - className="w-full justify-center" 104 + onClick={handleContinue} 105 + variant="primary" 106 + className="flex-1 justify-center" 107 + icon={<ExternalLink size={14} />} 94 108 > 95 - Go Back 109 + Open Link 96 110 </Button> 97 111 </div> 98 112 </div>
+1 -1
web/src/views/content/AnnotationDetail.tsx
··· 261 261 value={replyText} 262 262 onChange={(e) => setReplyText(e.target.value)} 263 263 placeholder="Write a reply..." 264 - className="w-full p-0 border-0 focus:ring-0 text-surface-900 dark:text-white placeholder:text-surface-400 dark:placeholder:text-surface-500 resize-none min-h-[40px] appearance-none bg-transparent leading-relaxed" 264 + className="w-full p-3 bg-surface-50 dark:bg-surface-800 border border-surface-200 dark:border-surface-700 rounded-lg text-surface-900 dark:text-white placeholder:text-surface-400 dark:placeholder:text-surface-500 focus:ring-2 focus:ring-primary-500/20 focus:border-primary-500 dark:focus:border-primary-400 outline-none resize-none min-h-[80px]" 265 265 rows={2} 266 266 disabled={posting} 267 267 />