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

Configure Feed

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

disable external link warning setting and fix trending tags

scanash00 0c101253 bf514eec

+131 -26
+13 -4
backend/internal/api/preferences.go
··· 1 1 package api 2 2 3 3 import ( 4 + "context" 4 5 "encoding/json" 5 6 "fmt" 6 7 "net/http" ··· 25 26 ExternalLinkSkippedHostnames []string `json:"externalLinkSkippedHostnames"` 26 27 SubscribedLabelers []LabelerSubscription `json:"subscribedLabelers"` 27 28 LabelPreferences []LabelPreference `json:"labelPreferences"` 29 + DisableExternalLinkWarning bool `json:"disableExternalLinkWarning"` 28 30 } 29 31 30 32 func (h *Handler) GetPreferences(w http.ResponseWriter, r *http.Request) { ··· 65 67 labelPrefs = []LabelPreference{} 66 68 } 67 69 70 + disableWarning := false 71 + if prefs != nil && prefs.DisableExternalLinkWarning != nil { 72 + disableWarning = *prefs.DisableExternalLinkWarning 73 + } 74 + 68 75 w.Header().Set("Content-Type", "application/json") 69 76 json.NewEncoder(w).Encode(PreferencesResponse{ 70 77 ExternalLinkSkippedHostnames: hostnames, 71 78 SubscribedLabelers: labelers, 72 79 LabelPreferences: labelPrefs, 80 + DisableExternalLinkWarning: disableWarning, 73 81 }) 74 82 } 75 83 ··· 103 111 }) 104 112 } 105 113 106 - record := xrpc.NewPreferencesRecord(input.ExternalLinkSkippedHostnames, xrpcLabelers, xrpcLabelPrefs) 114 + record := xrpc.NewPreferencesRecord(input.ExternalLinkSkippedHostnames, xrpcLabelers, xrpcLabelPrefs, &input.DisableExternalLinkWarning) 107 115 if err := record.Validate(); err != nil { 108 116 http.Error(w, fmt.Sprintf("Invalid record: %v", err), http.StatusBadRequest) 109 117 return 110 118 } 111 119 112 120 err = h.refresher.ExecuteWithAutoRefresh(r, session, func(client *xrpc.Client, did string) error { 113 - _, err := client.PutRecord(r.Context(), did, xrpc.CollectionPreferences, "self", record) 121 + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 122 + defer cancel() 123 + _, err := client.PutRecord(ctx, did, xrpc.CollectionPreferences, "self", record) 114 124 return err 115 125 }) 116 126 117 127 if err != nil { 118 128 fmt.Printf("[UpdatePreferences] PDS write failed: %v\n", err) 119 - http.Error(w, fmt.Sprintf("Failed to update preferences: %v", err), http.StatusInternalServerError) 120 - return 121 129 } 122 130 123 131 createdAt, _ := time.Parse(time.RFC3339, record.CreatedAt) ··· 144 152 ExternalLinkSkippedHostnames: &hostnamesStr, 145 153 SubscribedLabelers: subscribedLabelersPtr, 146 154 LabelPreferences: labelPrefsPtr, 155 + DisableExternalLinkWarning: &input.DisableExternalLinkWarning, 147 156 CreatedAt: createdAt, 148 157 IndexedAt: time.Now(), 149 158 })
+11 -5
backend/internal/db/db.go
··· 151 151 ExternalLinkSkippedHostnames *string `json:"externalLinkSkippedHostnames,omitempty"` 152 152 SubscribedLabelers *string `json:"subscribedLabelers,omitempty"` 153 153 LabelPreferences *string `json:"labelPreferences,omitempty"` 154 + DisableExternalLinkWarning *bool `json:"disableExternalLinkWarning,omitempty"` 154 155 CreatedAt time.Time `json:"createdAt"` 155 156 IndexedAt time.Time `json:"indexedAt"` 156 157 CID *string `json:"cid,omitempty"` ··· 422 423 uri TEXT PRIMARY KEY, 423 424 author_did TEXT NOT NULL, 424 425 external_link_skipped_hostnames TEXT, 426 + subscribed_labelers TEXT, 427 + label_preferences TEXT, 428 + disable_external_link_warning BOOLEAN, 425 429 created_at ` + dateType + ` NOT NULL, 426 430 indexed_at ` + dateType + ` NOT NULL, 427 431 cid TEXT ··· 618 622 619 623 func (db *DB) GetPreferences(did string) (*Preferences, error) { 620 624 var p Preferences 621 - err := db.QueryRow("SELECT uri, author_did, external_link_skipped_hostnames, subscribed_labelers, label_preferences, created_at, indexed_at, cid FROM preferences WHERE author_did = $1", did).Scan( 622 - &p.URI, &p.AuthorDID, &p.ExternalLinkSkippedHostnames, &p.SubscribedLabelers, &p.LabelPreferences, &p.CreatedAt, &p.IndexedAt, &p.CID, 625 + err := db.QueryRow("SELECT uri, author_did, external_link_skipped_hostnames, subscribed_labelers, label_preferences, disable_external_link_warning, created_at, indexed_at, cid FROM preferences WHERE author_did = $1", did).Scan( 626 + &p.URI, &p.AuthorDID, &p.ExternalLinkSkippedHostnames, &p.SubscribedLabelers, &p.LabelPreferences, &p.DisableExternalLinkWarning, &p.CreatedAt, &p.IndexedAt, &p.CID, 623 627 ) 624 628 if err == sql.ErrNoRows { 625 629 return nil, nil ··· 632 636 633 637 func (db *DB) UpsertPreferences(p *Preferences) error { 634 638 query := ` 635 - INSERT INTO preferences (uri, author_did, external_link_skipped_hostnames, subscribed_labelers, label_preferences, created_at, indexed_at, cid) 636 - VALUES ($1, $2, $3, $4, $5, $6, $7, $8) 639 + INSERT INTO preferences (uri, author_did, external_link_skipped_hostnames, subscribed_labelers, label_preferences, disable_external_link_warning, created_at, indexed_at, cid) 640 + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) 637 641 ON CONFLICT(uri) DO UPDATE SET 638 642 external_link_skipped_hostnames = EXCLUDED.external_link_skipped_hostnames, 639 643 subscribed_labelers = EXCLUDED.subscribed_labelers, 640 644 label_preferences = EXCLUDED.label_preferences, 645 + disable_external_link_warning = EXCLUDED.disable_external_link_warning, 641 646 indexed_at = EXCLUDED.indexed_at, 642 647 cid = EXCLUDED.cid 643 648 ` 644 - _, err := db.Exec(db.Rebind(query), p.URI, p.AuthorDID, p.ExternalLinkSkippedHostnames, p.SubscribedLabelers, p.LabelPreferences, p.CreatedAt, p.IndexedAt, p.CID) 649 + _, err := db.Exec(db.Rebind(query), p.URI, p.AuthorDID, p.ExternalLinkSkippedHostnames, p.SubscribedLabelers, p.LabelPreferences, p.DisableExternalLinkWarning, p.CreatedAt, p.IndexedAt, p.CID) 645 650 return err 646 651 } 647 652 ··· 729 734 730 735 db.Exec(`ALTER TABLE preferences ADD COLUMN subscribed_labelers TEXT`) 731 736 db.Exec(`ALTER TABLE preferences ADD COLUMN label_preferences TEXT`) 737 + db.Exec(`ALTER TABLE preferences ADD COLUMN disable_external_link_warning BOOLEAN`) 732 738 } 733 739 734 740 func (db *DB) migrateModeration(dateType string) {
+4 -4
backend/internal/db/tags.go
··· 18 18 AND tags_json != '[]' 19 19 AND created_at > NOW() - INTERVAL '7 days' 20 20 GROUP BY tag 21 - HAVING count > 2 22 - ORDER BY count DESC 23 - LIMIT ? 21 + HAVING COUNT(*) > 2 22 + ORDER BY COUNT(*) DESC 23 + LIMIT $1 24 24 ` 25 - rows, err := db.Query(db.Rebind(query), limit) 25 + rows, err := db.Query(query, limit) 26 26 if err != nil { 27 27 return nil, err 28 28 }
+3 -1
backend/internal/xrpc/records.go
··· 478 478 ExternalLinkSkippedHostnames []string `json:"externalLinkSkippedHostnames,omitempty"` 479 479 SubscribedLabelers []LabelerSubscription `json:"subscribedLabelers,omitempty"` 480 480 LabelPreferences []LabelPreference `json:"labelPreferences,omitempty"` 481 + DisableExternalLinkWarning *bool `json:"disableExternalLinkWarning,omitempty"` 481 482 CreatedAt string `json:"createdAt"` 482 483 } 483 484 ··· 499 500 return nil 500 501 } 501 502 502 - func NewPreferencesRecord(skippedHostnames []string, labelers interface{}, labelPrefs interface{}) *PreferencesRecord { 503 + func NewPreferencesRecord(skippedHostnames []string, labelers interface{}, labelPrefs interface{}, disableExternalLinkWarning *bool) *PreferencesRecord { 503 504 record := &PreferencesRecord{ 504 505 Type: CollectionPreferences, 505 506 ExternalLinkSkippedHostnames: skippedHostnames, 507 + DisableExternalLinkWarning: disableExternalLinkWarning, 506 508 CreatedAt: time.Now().UTC().Format(time.RFC3339), 507 509 } 508 510
+4
lexicons/at/margin/preferences.json
··· 40 40 "createdAt": { 41 41 "type": "string", 42 42 "format": "datetime" 43 + }, 44 + "disableExternalLinkWarning": { 45 + "type": "boolean", 46 + "description": "If true, do not show the confirmation modal when opening external links." 43 47 } 44 48 } 45 49 }
+2
web/src/api/client.ts
··· 1007 1007 externalLinkSkippedHostnames?: string[]; 1008 1008 subscribedLabelers?: LabelerSubscription[]; 1009 1009 labelPreferences?: LabelPreference[]; 1010 + disableExternalLinkWarning?: boolean; 1010 1011 } 1011 1012 1012 1013 export async function getPreferences(): Promise<PreferencesResponse> { ··· 1026 1027 externalLinkSkippedHostnames?: string[]; 1027 1028 subscribedLabelers?: LabelerSubscription[]; 1028 1029 labelPreferences?: LabelPreference[]; 1030 + disableExternalLinkWarning?: boolean; 1029 1031 }): Promise<boolean> { 1030 1032 try { 1031 1033 const res = await apiRequest("/api/preferences", {
+6
web/src/components/common/Card.tsx
··· 227 227 window.open(url, "_blank", "noopener,noreferrer"); 228 228 return; 229 229 } 230 + 231 + if ($preferences.get().disableExternalLinkWarning) { 232 + window.open(url, "_blank", "noopener,noreferrer"); 233 + return; 234 + } 235 + 230 236 const skipped = $preferences.get().externalLinkSkippedHostnames; 231 237 if (skipped.includes(hostname)) { 232 238 window.open(url, "_blank", "noopener,noreferrer");
+6
web/src/components/common/RichText.tsx
··· 78 78 window.open(url, "_blank", "noopener,noreferrer"); 79 79 return; 80 80 } 81 + 82 + if (preferences.disableExternalLinkWarning) { 83 + window.open(url, "_blank", "noopener,noreferrer"); 84 + return; 85 + } 86 + 81 87 const skipped = preferences.externalLinkSkippedHostnames || []; 82 88 if (skipped.includes(hostname)) { 83 89 window.open(url, "_blank", "noopener,noreferrer");
+39
web/src/components/ui/Switch.tsx
··· 1 + import * as React from "react"; 2 + 3 + interface SwitchProps { 4 + checked: boolean; 5 + onCheckedChange: (checked: boolean) => void; 6 + disabled?: boolean; 7 + className?: string; 8 + } 9 + 10 + export function Switch({ 11 + checked, 12 + onCheckedChange, 13 + disabled = false, 14 + className = "", 15 + }: SwitchProps) { 16 + return ( 17 + <button 18 + type="button" 19 + role="switch" 20 + aria-checked={checked} 21 + disabled={disabled} 22 + onClick={() => !disabled && onCheckedChange(!checked)} 23 + className={` 24 + relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 dark:focus:ring-offset-surface-900 25 + ${checked ? "bg-primary-600" : "bg-surface-300 dark:bg-surface-700"} 26 + ${disabled ? "opacity-50 cursor-not-allowed" : "cursor-pointer"} 27 + ${className} 28 + `} 29 + > 30 + <span className="sr-only">Use setting</span> 31 + <span 32 + className={` 33 + inline-block h-4 w-4 transform rounded-full bg-white transition-transform 34 + ${checked ? "translate-x-6" : "translate-x-1"} 35 + `} 36 + /> 37 + </button> 38 + ); 39 + }
+1
web/src/components/ui/index.ts
··· 6 6 export { default as EmptyState } from "./EmptyState"; 7 7 export { default as Badge, CountBadge } from "./Badge"; 8 8 export { default as LayoutToggle } from "./LayoutToggle"; 9 + export * from "./Switch";
+15
web/src/store/preferences.ts
··· 10 10 externalLinkSkippedHostnames: string[]; 11 11 subscribedLabelers: LabelerSubscription[]; 12 12 labelPreferences: LabelPreference[]; 13 + disableExternalLinkWarning: boolean; 13 14 } 14 15 15 16 export const $preferences = atom<Preferences>({ 16 17 externalLinkSkippedHostnames: [], 17 18 subscribedLabelers: [], 18 19 labelPreferences: [], 20 + disableExternalLinkWarning: false, 19 21 }); 20 22 21 23 export async function loadPreferences() { ··· 24 26 externalLinkSkippedHostnames: prefs.externalLinkSkippedHostnames || [], 25 27 subscribedLabelers: prefs.subscribedLabelers || [], 26 28 labelPreferences: prefs.labelPreferences || [], 29 + disableExternalLinkWarning: !!prefs.disableExternalLinkWarning, 27 30 }); 28 31 } 29 32 ··· 92 95 ); 93 96 return pref?.visibility || "warn"; 94 97 } 98 + 99 + export async function setDisableExternalLinkWarning(disabled: boolean) { 100 + const current = $preferences.get(); 101 + if (current.disableExternalLinkWarning === disabled) return; 102 + 103 + const updated = { 104 + ...current, 105 + disableExternalLinkWarning: disabled, 106 + }; 107 + $preferences.set(updated); 108 + await updatePreferences(updated); 109 + }
+27 -12
web/src/views/core/Settings.tsx
··· 9 9 removeLabeler, 10 10 setLabelVisibility, 11 11 getLabelVisibility, 12 + setDisableExternalLinkWarning, 12 13 } from "../../store/preferences"; 13 14 import { 14 15 getAPIKeys, ··· 54 55 Input, 55 56 Skeleton, 56 57 EmptyState, 58 + Switch, 57 59 } from "../../components/ui"; 58 60 import { AppleIcon } from "../../components/common/Icons"; 59 61 import { Link } from "react-router-dom"; ··· 174 176 <button 175 177 key={opt.value} 176 178 onClick={() => setTheme(opt.value)} 177 - className={`flex-1 flex flex-col items-center gap-2 p-4 rounded-xl border-2 transition-all ${ 178 - theme === opt.value 179 + className={`flex-1 flex flex-col items-center gap-2 p-4 rounded-xl border-2 transition-all ${theme === opt.value 179 180 ? "border-primary-500 bg-primary-50 dark:bg-primary-900/20" 180 181 : "border-surface-200 dark:border-surface-700 hover:border-surface-300 dark:hover:border-surface-600" 181 - }`} 182 + }`} 182 183 > 183 184 <opt.icon 184 185 size={24} ··· 196 197 </button> 197 198 ))} 198 199 </div> 200 + 201 + <div className="mt-6 flex items-center justify-between"> 202 + <div> 203 + <h3 className="text-sm font-medium text-surface-900 dark:text-white"> 204 + Disable external link warning 205 + </h3> 206 + <p className="text-sm text-surface-500 dark:text-surface-400"> 207 + Don't ask for confirmation when opening external links 208 + </p> 209 + </div> 210 + <Switch 211 + checked={$preferences.get().disableExternalLinkWarning} 212 + onCheckedChange={setDisableExternalLinkWarning} 213 + /> 214 + </div> 199 215 </section> 200 216 201 217 <section className="card p-5"> ··· 203 219 API Keys 204 220 </h2> 205 221 <p className="text-sm text-surface-400 dark:text-surface-500 mb-5"> 206 - For the browser extension and other apps 222 + For the iOS shortcut and other apps 207 223 </p> 208 224 209 225 <form onSubmit={handleCreate} className="flex gap-2 mb-5"> ··· 211 227 <Input 212 228 value={newKeyName} 213 229 onChange={(e) => setNewKeyName(e.target.value)} 214 - placeholder="Key name, e.g. Chrome Extension" 230 + placeholder="Key name, e.g. iOS Shortcut" 215 231 /> 216 232 </div> 217 233 <Button ··· 558 574 label: string; 559 575 icon: typeof Eye; 560 576 }[] = [ 561 - { value: "warn", label: "Warn", icon: EyeOff }, 562 - { value: "hide", label: "Hide", icon: XCircle }, 563 - { value: "ignore", label: "Ignore", icon: Eye }, 564 - ]; 577 + { value: "warn", label: "Warn", icon: EyeOff }, 578 + { value: "hide", label: "Hide", icon: XCircle }, 579 + { value: "ignore", label: "Ignore", icon: Eye }, 580 + ]; 565 581 return ( 566 582 <div 567 583 key={label} ··· 581 597 opt.value, 582 598 ) 583 599 } 584 - className={`px-2.5 py-1 text-xs font-medium rounded-lg transition-all flex items-center gap-1 ${ 585 - current === opt.value 600 + className={`px-2.5 py-1 text-xs font-medium rounded-lg transition-all flex items-center gap-1 ${current === opt.value 586 601 ? opt.value === "hide" 587 602 ? "bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-400" 588 603 : opt.value === "warn" 589 604 ? "bg-amber-100 dark:bg-amber-900/30 text-amber-700 dark:text-amber-400" 590 605 : "bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400" 591 606 : "text-surface-400 dark:text-surface-500 hover:bg-surface-200 dark:hover:bg-surface-700" 592 - }`} 607 + }`} 593 608 > 594 609 <opt.icon size={12} /> 595 610 {opt.label}