👁️
5
fork

Configure Feed

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

more chars in pronouns

+110 -7
+103
.claude/PROFILE.md
··· 1 + # Profile System 2 + 3 + DeckBelcher profiles use ATProto records with richtext bio support and DNS-based handle verification. 4 + 5 + ## Lexicon 6 + 7 + **Collection**: `com.deckbelcher.actor.profile` 8 + **Record key**: `"self"` (singleton per user) 9 + 10 + ```typescript 11 + { 12 + bio?: com.deckbelcher.richtext.Document; // Full multi-block richtext 13 + pronouns?: string; // Max 64 graphemes 14 + createdAt: string; // ISO datetime, required 15 + } 16 + ``` 17 + 18 + **No displayName** - Handles can be custom domains (`@aviva.cool`), making separate display names redundant. 19 + 20 + ## Data Flow 21 + 22 + ### Fetching 23 + 24 + ``` 25 + getProfileQueryOptions(did) 26 + → getProfileRecord(did) 27 + → GET com.atproto.repo.getRecord with rkey "self" 28 + → Returns ProfileRecord | null 29 + ``` 30 + 31 + Missing profiles return `null` (not an error). ATProto returns HTTP 400 for `RecordNotFound`. 32 + 33 + ### Updating 34 + 35 + ``` 36 + useUpdateProfileMutation() 37 + → upsertProfileRecord(agent, record) 38 + → PUT com.atproto.repo.putRecord with rkey "self" 39 + ``` 40 + 41 + Uses upsert since rkey is always `"self"` - creates on first save, updates thereafter. 42 + 43 + ## Components 44 + 45 + ### ProfileLayout 46 + 47 + Shared layout for profile tabs (Decks, Lists). NOT a route.tsx file - that would wrap deck editor pages too. 48 + 49 + ``` 50 + ProfileLayout 51 + ├── ProfileHeader (handle, pronouns, bio, edit button) 52 + ├── Tab navigation (Decks | Lists) 53 + └── {children} (tab content) 54 + ``` 55 + 56 + ### ProfileHeader 57 + 58 + Displays handle with Recursive font (`'MONO' 0.5, 'CASL' 0.3`) and optional external link. 59 + 60 + **Edit mode**: Single "Edit profile" button toggles all fields editable. Bio uses ProseMirror editor with debounced autosave (1500ms). 61 + 62 + ## Handle Links 63 + 64 + External link button appears next to handle if the domain resolves. 65 + 66 + ### DNS Aliveness Check 67 + 68 + Can't do HTTP requests to arbitrary domains (CORS, security). Instead, use Cloudflare DNS-over-HTTPS: 69 + 70 + ```typescript 71 + // Query A, AAAA, CNAME records in parallel 72 + await Promise.any([ 73 + requireDnsRecord(handle, "A"), 74 + requireDnsRecord(handle, "AAAA"), 75 + requireDnsRecord(handle, "CNAME"), 76 + ]); 77 + ``` 78 + 79 + Each `requireDnsRecord` rejects if no records exist, so `Promise.any` resolves on first success. 80 + 81 + **API**: `https://cloudflare-dns.com/dns-query?name={handle}&type={type}` 82 + **Header**: `Accept: application/dns-json` 83 + 84 + Query is prefetched during SSR (non-blocking) and cached for 10 minutes. 85 + 86 + ## Future: Profile Pictures 87 + 88 + ATProto supports blob storage for images: 89 + 90 + 1. **Client-side compress** - Max 1MB, jpeg/png/gif 91 + 2. **Upload blob**: `com.atproto.repo.uploadBlob` → returns blob ref 92 + 3. **Store ref in profile**: 93 + ```typescript 94 + avatar?: { 95 + $type: "blob", 96 + ref: { $link: "bafyrei..." }, 97 + mimeType: "image/jpeg", 98 + size: 12345 99 + } 100 + ``` 101 + 4. **Serve from PDS**: `https://{pds}/xrpc/com.atproto.sync.getBlob?did={did}&cid={cid}` 102 + 103 + Lexicon would add: `avatar?: at.Blob`
+3 -3
lexicons/com/deckbelcher/actor/profile.json
··· 15 15 }, 16 16 "pronouns": { 17 17 "type": "string", 18 - "maxLength": 200, 19 - "maxGraphemes": 20, 20 - "description": "Free-form pronouns text." 18 + "maxLength": 256, 19 + "maxGraphemes": 64, 20 + "description": "Free-form pronouns text, can include brief explanation." 21 21 }, 22 22 "createdAt": { 23 23 "type": "string",
+1 -1
src/components/profile/ProfileHeader.tsx
··· 121 121 value={editedPronouns} 122 122 onChange={(e) => setEditedPronouns(e.target.value)} 123 123 placeholder="e.g. she/her, they/them" 124 - maxLength={20} 124 + maxLength={64} 125 125 className="px-3 py-2 w-full max-w-xs bg-gray-100 dark:bg-slate-800 border border-gray-300 dark:border-slate-700 rounded-lg text-gray-900 dark:text-white focus:outline-none focus:border-cyan-500" 126 126 /> 127 127 </div>
+3 -3
typelex/main.tsp
··· 14 14 /** Profile bio/description as a rich text document. */ 15 15 bio?: com.deckbelcher.richtext.Document; 16 16 17 - /** Free-form pronouns text. */ 18 - @maxGraphemes(20) 19 - @maxLength(200) 17 + /** Free-form pronouns text, can include brief explanation. */ 18 + @maxGraphemes(64) 19 + @maxLength(256) 20 20 pronouns?: string; 21 21 22 22 /** Timestamp when the profile was created. */