(READ ONLY) Margin is an open annotation layer for the internet. Powered by the AT Protocol. margin.at
extension web atproto comments
98
fork

Configure Feed

Select the types of activity you want to include in your feed.

more bug fixes and additions

scanash00 a226bc2e ab4b436d

+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 />