a tool for shared writing and social publishing

Compare changes

Choose any two refs to compare.

+2360 -2188
-15
.claude/settings.local.json
··· 1 - { 2 - "permissions": { 3 - "allow": [ 4 - "mcp__acp__Edit", 5 - "mcp__acp__Write", 6 - "mcp__acp__Bash", 7 - "mcp__primitive__say_hello", 8 - "mcp__primitive__pending_delegations", 9 - "mcp__primitive__claim_delegation", 10 - "mcp__primitive__tasks_update", 11 - "mcp__primitive__contexts_update", 12 - "mcp__primitive__contexts_list" 13 - ] 14 - } 15 - }
+304
.claude/skills/lexicons/SKILL.md
··· 1 + --- 2 + name: lexicons 3 + description: Reference for the AT Protocol lexicon system. Use when working with lexicons, adding new record types, or modifying AT Protocol schemas. 4 + user-invocable: false 5 + --- 6 + 7 + # Lexicon System 8 + 9 + ## Overview 10 + 11 + Lexicons define the schema for AT Protocol records. This project has two namespaces: 12 + - **`pub.leaflet.*`** - Leaflet-specific lexicons (documents, publications, blocks, etc.) 13 + - **`site.standard.*`** - Standard site lexicons for interoperability 14 + 15 + The lexicons are defined as TypeScript in `lexicons/src/`, built to JSON in `lexicons/pub/leaflet/` and `lexicons/site/standard/`, and TypeScript types are generated in `lexicons/api/`. 16 + 17 + ## Key Files 18 + 19 + - **`lexicons/src/*.ts`** - Source definitions for `pub.leaflet.*` lexicons 20 + - **`lexicons/site/standard/**/*.json`** - JSON definitions for `site.standard.*` lexicons (manually maintained) 21 + - **`lexicons/build.ts`** - Builds TypeScript sources to JSON 22 + - **`lexicons/api/`** - Generated TypeScript types and client 23 + - **`package.json`** - Contains `lexgen` script 24 + 25 + ## Running Lexicon Generation 26 + 27 + ```bash 28 + npm run lexgen 29 + ``` 30 + 31 + This runs: 32 + 1. `tsx ./lexicons/build.ts` - Builds `pub.leaflet.*` JSON from TypeScript 33 + 2. `lex gen-api` - Generates TypeScript types from all JSON lexicons 34 + 3. `tsx ./lexicons/fix-extensions.ts` - Fixes import extensions 35 + 36 + ## Adding a New pub.leaflet Lexicon 37 + 38 + ### 1. Create the Source Definition 39 + 40 + Create a file in `lexicons/src/` (e.g., `lexicons/src/myLexicon.ts`): 41 + 42 + ```typescript 43 + import { LexiconDoc } from "@atproto/lexicon"; 44 + 45 + export const PubLeafletMyLexicon: LexiconDoc = { 46 + lexicon: 1, 47 + id: "pub.leaflet.myLexicon", 48 + defs: { 49 + main: { 50 + type: "record", // or "object" for non-record types 51 + key: "tid", 52 + record: { 53 + type: "object", 54 + required: ["field1"], 55 + properties: { 56 + field1: { type: "string", maxLength: 1000 }, 57 + field2: { type: "integer", minimum: 0 }, 58 + optionalRef: { type: "ref", ref: "other.lexicon#def" }, 59 + }, 60 + }, 61 + }, 62 + // Additional defs for sub-objects 63 + subType: { 64 + type: "object", 65 + properties: { 66 + nested: { type: "string" }, 67 + }, 68 + }, 69 + }, 70 + }; 71 + ``` 72 + 73 + ### 2. Add to Build 74 + 75 + Update `lexicons/build.ts`: 76 + 77 + ```typescript 78 + import { PubLeafletMyLexicon } from "./src/myLexicon"; 79 + 80 + const lexicons = [ 81 + // ... existing lexicons 82 + PubLeafletMyLexicon, 83 + ]; 84 + ``` 85 + 86 + ### 3. Update lexgen Command (if needed) 87 + 88 + If your lexicon is at the top level of `pub/leaflet/` (not in a subdirectory), add it to the `lexgen` script in `package.json`: 89 + 90 + ```json 91 + "lexgen": "tsx ./lexicons/build.ts && lex gen-api ./lexicons/api ./lexicons/pub/leaflet/document.json ./lexicons/pub/leaflet/myLexicon.json ./lexicons/pub/leaflet/*/* ..." 92 + ``` 93 + 94 + Note: Files in subdirectories (`pub/leaflet/*/*`) are automatically included. 95 + 96 + ### 4. Add to authFullPermissions (for record types) 97 + 98 + If your lexicon is a record type that users should be able to create/update/delete, add it to the `authFullPermissions` permission set in `lexicons/src/authFullPermissions.ts`: 99 + 100 + ```typescript 101 + import { PubLeafletMyLexicon } from "./myLexicon"; 102 + 103 + // In the permissions collection array: 104 + collection: [ 105 + // ... existing lexicons 106 + PubLeafletMyLexicon.id, 107 + ], 108 + ``` 109 + 110 + ### 5. Regenerate Types 111 + 112 + ```bash 113 + npm run lexgen 114 + ``` 115 + 116 + ### 6. Use the Generated Types 117 + 118 + ```typescript 119 + import { PubLeafletMyLexicon } from "lexicons/api"; 120 + 121 + // Type for the record 122 + type MyRecord = PubLeafletMyLexicon.Record; 123 + 124 + // Validation 125 + const result = PubLeafletMyLexicon.validateRecord(data); 126 + if (result.success) { 127 + // result.value is typed 128 + } 129 + 130 + // Type guard 131 + if (PubLeafletMyLexicon.isRecord(data)) { 132 + // data is typed as Record 133 + } 134 + ``` 135 + 136 + ## Adding a New site.standard Lexicon 137 + 138 + ### 1. Create the JSON Definition 139 + 140 + Create a file in `lexicons/site/standard/` (e.g., `lexicons/site/standard/myType.json`): 141 + 142 + ```json 143 + { 144 + "lexicon": 1, 145 + "id": "site.standard.myType", 146 + "defs": { 147 + "main": { 148 + "type": "record", 149 + "key": "tid", 150 + "record": { 151 + "type": "object", 152 + "required": ["field1"], 153 + "properties": { 154 + "field1": { 155 + "type": "string", 156 + "maxLength": 1000 157 + } 158 + } 159 + } 160 + } 161 + } 162 + } 163 + ``` 164 + 165 + ### 2. Regenerate Types 166 + 167 + ```bash 168 + npm run lexgen 169 + ``` 170 + 171 + The `site/*/* site/*/*/*` globs in the lexgen command automatically pick up new files. 172 + 173 + ## Common Lexicon Patterns 174 + 175 + ### Referencing Other Lexicons 176 + 177 + ```typescript 178 + // Reference another lexicon's main def 179 + { type: "ref", ref: "pub.leaflet.publication" } 180 + 181 + // Reference a specific def within a lexicon 182 + { type: "ref", ref: "pub.leaflet.publication#theme" } 183 + 184 + // Reference within the same lexicon 185 + { type: "ref", ref: "#myDef" } 186 + ``` 187 + 188 + ### Union Types 189 + 190 + ```typescript 191 + { 192 + type: "union", 193 + refs: [ 194 + "pub.leaflet.pages.linearDocument", 195 + "pub.leaflet.pages.canvas", 196 + ], 197 + } 198 + 199 + // Open union (allows unknown types) 200 + { 201 + type: "union", 202 + closed: false, // default is true 203 + refs: ["pub.leaflet.content"], 204 + } 205 + ``` 206 + 207 + ### Blob Types (for images/files) 208 + 209 + ```typescript 210 + { 211 + type: "blob", 212 + accept: ["image/*"], // or specific types like ["image/png", "image/jpeg"] 213 + maxSize: 1000000, // bytes 214 + } 215 + ``` 216 + 217 + ### Color Types 218 + 219 + The project has color types defined: 220 + - `pub.leaflet.theme.color#rgb` / `#rgba` 221 + - `site.standard.theme.color#rgb` / `#rgba` 222 + 223 + ```typescript 224 + // In lexicons/src/theme.ts 225 + export const ColorUnion = { 226 + type: "union", 227 + refs: [ 228 + "pub.leaflet.theme.color#rgba", 229 + "pub.leaflet.theme.color#rgb", 230 + ], 231 + }; 232 + ``` 233 + 234 + ## Normalization Between Formats 235 + 236 + Use `lexicons/src/normalize.ts` to convert between `pub.leaflet` and `site.standard` formats: 237 + 238 + ```typescript 239 + import { 240 + normalizeDocument, 241 + normalizePublication, 242 + isLeafletDocument, 243 + isStandardDocument, 244 + getDocumentPages, 245 + } from "lexicons/src/normalize"; 246 + 247 + // Normalize a document from either format 248 + const normalized = normalizeDocument(record); 249 + if (normalized) { 250 + // normalized is always in site.standard.document format 251 + console.log(normalized.title, normalized.site); 252 + 253 + // Get pages if content is pub.leaflet.content 254 + const pages = getDocumentPages(normalized); 255 + } 256 + 257 + // Normalize a publication 258 + const pub = normalizePublication(record); 259 + if (pub) { 260 + console.log(pub.name, pub.url); 261 + } 262 + ``` 263 + 264 + ## Handling in Appview (Firehose Consumer) 265 + 266 + When processing records from the firehose in `appview/index.ts`: 267 + 268 + ```typescript 269 + import { ids } from "lexicons/api/lexicons"; 270 + import { PubLeafletMyLexicon } from "lexicons/api"; 271 + 272 + // In filterCollections: 273 + filterCollections: [ 274 + ids.PubLeafletMyLexicon, 275 + // ... 276 + ], 277 + 278 + // In handleEvent: 279 + if (evt.collection === ids.PubLeafletMyLexicon) { 280 + if (evt.event === "create" || evt.event === "update") { 281 + let record = PubLeafletMyLexicon.validateRecord(evt.record); 282 + if (!record.success) return; 283 + 284 + // Store in database 285 + await supabase.from("my_table").upsert({ 286 + uri: evt.uri.toString(), 287 + data: record.value as Json, 288 + }); 289 + } 290 + if (evt.event === "delete") { 291 + await supabase.from("my_table").delete().eq("uri", evt.uri.toString()); 292 + } 293 + } 294 + ``` 295 + 296 + ## Publishing Lexicons 297 + 298 + To publish lexicons to an AT Protocol PDS: 299 + 300 + ```bash 301 + npm run publish-lexicons 302 + ``` 303 + 304 + This runs `lexicons/publish.ts` which publishes lexicons to the configured PDS.
+298
.claude/skills/lexicons.md
··· 1 + # Lexicon System 2 + 3 + ## Overview 4 + 5 + Lexicons define the schema for AT Protocol records. This project has two namespaces: 6 + - **`pub.leaflet.*`** - Leaflet-specific lexicons (documents, publications, blocks, etc.) 7 + - **`site.standard.*`** - Standard site lexicons for interoperability 8 + 9 + The lexicons are defined as TypeScript in `lexicons/src/`, built to JSON in `lexicons/pub/leaflet/` and `lexicons/site/standard/`, and TypeScript types are generated in `lexicons/api/`. 10 + 11 + ## Key Files 12 + 13 + - **`lexicons/src/*.ts`** - Source definitions for `pub.leaflet.*` lexicons 14 + - **`lexicons/site/standard/**/*.json`** - JSON definitions for `site.standard.*` lexicons (manually maintained) 15 + - **`lexicons/build.ts`** - Builds TypeScript sources to JSON 16 + - **`lexicons/api/`** - Generated TypeScript types and client 17 + - **`package.json`** - Contains `lexgen` script 18 + 19 + ## Running Lexicon Generation 20 + 21 + ```bash 22 + npm run lexgen 23 + ``` 24 + 25 + This runs: 26 + 1. `tsx ./lexicons/build.ts` - Builds `pub.leaflet.*` JSON from TypeScript 27 + 2. `lex gen-api` - Generates TypeScript types from all JSON lexicons 28 + 3. `tsx ./lexicons/fix-extensions.ts` - Fixes import extensions 29 + 30 + ## Adding a New pub.leaflet Lexicon 31 + 32 + ### 1. Create the Source Definition 33 + 34 + Create a file in `lexicons/src/` (e.g., `lexicons/src/myLexicon.ts`): 35 + 36 + ```typescript 37 + import { LexiconDoc } from "@atproto/lexicon"; 38 + 39 + export const PubLeafletMyLexicon: LexiconDoc = { 40 + lexicon: 1, 41 + id: "pub.leaflet.myLexicon", 42 + defs: { 43 + main: { 44 + type: "record", // or "object" for non-record types 45 + key: "tid", 46 + record: { 47 + type: "object", 48 + required: ["field1"], 49 + properties: { 50 + field1: { type: "string", maxLength: 1000 }, 51 + field2: { type: "integer", minimum: 0 }, 52 + optionalRef: { type: "ref", ref: "other.lexicon#def" }, 53 + }, 54 + }, 55 + }, 56 + // Additional defs for sub-objects 57 + subType: { 58 + type: "object", 59 + properties: { 60 + nested: { type: "string" }, 61 + }, 62 + }, 63 + }, 64 + }; 65 + ``` 66 + 67 + ### 2. Add to Build 68 + 69 + Update `lexicons/build.ts`: 70 + 71 + ```typescript 72 + import { PubLeafletMyLexicon } from "./src/myLexicon"; 73 + 74 + const lexicons = [ 75 + // ... existing lexicons 76 + PubLeafletMyLexicon, 77 + ]; 78 + ``` 79 + 80 + ### 3. Update lexgen Command (if needed) 81 + 82 + If your lexicon is at the top level of `pub/leaflet/` (not in a subdirectory), add it to the `lexgen` script in `package.json`: 83 + 84 + ```json 85 + "lexgen": "tsx ./lexicons/build.ts && lex gen-api ./lexicons/api ./lexicons/pub/leaflet/document.json ./lexicons/pub/leaflet/myLexicon.json ./lexicons/pub/leaflet/*/* ..." 86 + ``` 87 + 88 + Note: Files in subdirectories (`pub/leaflet/*/*`) are automatically included. 89 + 90 + ### 4. Add to authFullPermissions (for record types) 91 + 92 + If your lexicon is a record type that users should be able to create/update/delete, add it to the `authFullPermissions` permission set in `lexicons/src/authFullPermissions.ts`: 93 + 94 + ```typescript 95 + import { PubLeafletMyLexicon } from "./myLexicon"; 96 + 97 + // In the permissions collection array: 98 + collection: [ 99 + // ... existing lexicons 100 + PubLeafletMyLexicon.id, 101 + ], 102 + ``` 103 + 104 + ### 5. Regenerate Types 105 + 106 + ```bash 107 + npm run lexgen 108 + ``` 109 + 110 + ### 6. Use the Generated Types 111 + 112 + ```typescript 113 + import { PubLeafletMyLexicon } from "lexicons/api"; 114 + 115 + // Type for the record 116 + type MyRecord = PubLeafletMyLexicon.Record; 117 + 118 + // Validation 119 + const result = PubLeafletMyLexicon.validateRecord(data); 120 + if (result.success) { 121 + // result.value is typed 122 + } 123 + 124 + // Type guard 125 + if (PubLeafletMyLexicon.isRecord(data)) { 126 + // data is typed as Record 127 + } 128 + ``` 129 + 130 + ## Adding a New site.standard Lexicon 131 + 132 + ### 1. Create the JSON Definition 133 + 134 + Create a file in `lexicons/site/standard/` (e.g., `lexicons/site/standard/myType.json`): 135 + 136 + ```json 137 + { 138 + "lexicon": 1, 139 + "id": "site.standard.myType", 140 + "defs": { 141 + "main": { 142 + "type": "record", 143 + "key": "tid", 144 + "record": { 145 + "type": "object", 146 + "required": ["field1"], 147 + "properties": { 148 + "field1": { 149 + "type": "string", 150 + "maxLength": 1000 151 + } 152 + } 153 + } 154 + } 155 + } 156 + } 157 + ``` 158 + 159 + ### 2. Regenerate Types 160 + 161 + ```bash 162 + npm run lexgen 163 + ``` 164 + 165 + The `site/*/* site/*/*/*` globs in the lexgen command automatically pick up new files. 166 + 167 + ## Common Lexicon Patterns 168 + 169 + ### Referencing Other Lexicons 170 + 171 + ```typescript 172 + // Reference another lexicon's main def 173 + { type: "ref", ref: "pub.leaflet.publication" } 174 + 175 + // Reference a specific def within a lexicon 176 + { type: "ref", ref: "pub.leaflet.publication#theme" } 177 + 178 + // Reference within the same lexicon 179 + { type: "ref", ref: "#myDef" } 180 + ``` 181 + 182 + ### Union Types 183 + 184 + ```typescript 185 + { 186 + type: "union", 187 + refs: [ 188 + "pub.leaflet.pages.linearDocument", 189 + "pub.leaflet.pages.canvas", 190 + ], 191 + } 192 + 193 + // Open union (allows unknown types) 194 + { 195 + type: "union", 196 + closed: false, // default is true 197 + refs: ["pub.leaflet.content"], 198 + } 199 + ``` 200 + 201 + ### Blob Types (for images/files) 202 + 203 + ```typescript 204 + { 205 + type: "blob", 206 + accept: ["image/*"], // or specific types like ["image/png", "image/jpeg"] 207 + maxSize: 1000000, // bytes 208 + } 209 + ``` 210 + 211 + ### Color Types 212 + 213 + The project has color types defined: 214 + - `pub.leaflet.theme.color#rgb` / `#rgba` 215 + - `site.standard.theme.color#rgb` / `#rgba` 216 + 217 + ```typescript 218 + // In lexicons/src/theme.ts 219 + export const ColorUnion = { 220 + type: "union", 221 + refs: [ 222 + "pub.leaflet.theme.color#rgba", 223 + "pub.leaflet.theme.color#rgb", 224 + ], 225 + }; 226 + ``` 227 + 228 + ## Normalization Between Formats 229 + 230 + Use `lexicons/src/normalize.ts` to convert between `pub.leaflet` and `site.standard` formats: 231 + 232 + ```typescript 233 + import { 234 + normalizeDocument, 235 + normalizePublication, 236 + isLeafletDocument, 237 + isStandardDocument, 238 + getDocumentPages, 239 + } from "lexicons/src/normalize"; 240 + 241 + // Normalize a document from either format 242 + const normalized = normalizeDocument(record); 243 + if (normalized) { 244 + // normalized is always in site.standard.document format 245 + console.log(normalized.title, normalized.site); 246 + 247 + // Get pages if content is pub.leaflet.content 248 + const pages = getDocumentPages(normalized); 249 + } 250 + 251 + // Normalize a publication 252 + const pub = normalizePublication(record); 253 + if (pub) { 254 + console.log(pub.name, pub.url); 255 + } 256 + ``` 257 + 258 + ## Handling in Appview (Firehose Consumer) 259 + 260 + When processing records from the firehose in `appview/index.ts`: 261 + 262 + ```typescript 263 + import { ids } from "lexicons/api/lexicons"; 264 + import { PubLeafletMyLexicon } from "lexicons/api"; 265 + 266 + // In filterCollections: 267 + filterCollections: [ 268 + ids.PubLeafletMyLexicon, 269 + // ... 270 + ], 271 + 272 + // In handleEvent: 273 + if (evt.collection === ids.PubLeafletMyLexicon) { 274 + if (evt.event === "create" || evt.event === "update") { 275 + let record = PubLeafletMyLexicon.validateRecord(evt.record); 276 + if (!record.success) return; 277 + 278 + // Store in database 279 + await supabase.from("my_table").upsert({ 280 + uri: evt.uri.toString(), 281 + data: record.value as Json, 282 + }); 283 + } 284 + if (evt.event === "delete") { 285 + await supabase.from("my_table").delete().eq("uri", evt.uri.toString()); 286 + } 287 + } 288 + ``` 289 + 290 + ## Publishing Lexicons 291 + 292 + To publish lexicons to an AT Protocol PDS: 293 + 294 + ```bash 295 + npm run publish-lexicons 296 + ``` 297 + 298 + This runs `lexicons/publish.ts` which publishes lexicons to the configured PDS.
+150
.claude/skills/notifications/SKILL.md
··· 1 + --- 2 + name: notifications 3 + description: Reference for the notification system. Use when adding new notification types or modifying notification handling. 4 + user-invocable: false 5 + --- 6 + 7 + # Notification System 8 + 9 + ## Overview 10 + 11 + Notifications are stored in the database and hydrated with related data before being rendered. The system supports multiple notification types (comments, subscriptions, etc.) that are processed in parallel. 12 + 13 + ## Key Files 14 + 15 + - **`src/notifications.ts`** - Core notification types and hydration logic 16 + - **`app/(home-pages)/notifications/NotificationList.tsx`** - Renders all notification types 17 + - **`app/(home-pages)/notifications/Notification.tsx`** - Base notification component 18 + - Individual notification components (e.g., `CommentNotification.tsx`, `FollowNotification.tsx`) 19 + 20 + ## Adding a New Notification Type 21 + 22 + ### 1. Update Notification Data Types (`src/notifications.ts`) 23 + 24 + Add your type to the `NotificationData` union: 25 + 26 + ```typescript 27 + export type NotificationData = 28 + | { type: "comment"; comment_uri: string; parent_uri?: string } 29 + | { type: "subscribe"; subscription_uri: string } 30 + | { type: "your_type"; your_field: string }; // Add here 31 + ``` 32 + 33 + Add to the `HydratedNotification` union: 34 + 35 + ```typescript 36 + export type HydratedNotification = 37 + | HydratedCommentNotification 38 + | HydratedSubscribeNotification 39 + | HydratedYourNotification; // Add here 40 + ``` 41 + 42 + ### 2. Create Hydration Function (`src/notifications.ts`) 43 + 44 + ```typescript 45 + export type HydratedYourNotification = Awaited< 46 + ReturnType<typeof hydrateYourNotifications> 47 + >[0]; 48 + 49 + async function hydrateYourNotifications(notifications: NotificationRow[]) { 50 + const yourNotifications = notifications.filter( 51 + (n): n is NotificationRow & { data: ExtractNotificationType<"your_type"> } => 52 + (n.data as NotificationData)?.type === "your_type", 53 + ); 54 + 55 + if (yourNotifications.length === 0) return []; 56 + 57 + // Fetch related data with joins 58 + const { data } = await supabaseServerClient 59 + .from("your_table") 60 + .select("*, related_table(*)") 61 + .in("uri", yourNotifications.map((n) => n.data.your_field)); 62 + 63 + return yourNotifications.map((notification) => ({ 64 + id: notification.id, 65 + recipient: notification.recipient, 66 + created_at: notification.created_at, 67 + type: "your_type" as const, 68 + your_field: notification.data.your_field, 69 + yourData: data?.find((d) => d.uri === notification.data.your_field)!, 70 + })); 71 + } 72 + ``` 73 + 74 + Add to `hydrateNotifications` parallel array: 75 + 76 + ```typescript 77 + const [commentNotifications, subscribeNotifications, yourNotifications] = await Promise.all([ 78 + hydrateCommentNotifications(notifications), 79 + hydrateSubscribeNotifications(notifications), 80 + hydrateYourNotifications(notifications), // Add here 81 + ]); 82 + 83 + const allHydrated = [...commentNotifications, ...subscribeNotifications, ...yourNotifications]; 84 + ``` 85 + 86 + ### 3. Trigger the Notification (in your action file) 87 + 88 + ```typescript 89 + import { Notification, pingIdentityToUpdateNotification } from "src/notifications"; 90 + import { v7 } from "uuid"; 91 + 92 + // When the event occurs: 93 + const recipient = /* determine who should receive it */; 94 + if (recipient !== currentUser) { 95 + const notification: Notification = { 96 + id: v7(), 97 + recipient, 98 + data: { 99 + type: "your_type", 100 + your_field: "value", 101 + }, 102 + }; 103 + await supabaseServerClient.from("notifications").insert(notification); 104 + await pingIdentityToUpdateNotification(recipient); 105 + } 106 + ``` 107 + 108 + ### 4. Create Notification Component 109 + 110 + Create a new component (e.g., `YourNotification.tsx`): 111 + 112 + ```typescript 113 + import { HydratedYourNotification } from "src/notifications"; 114 + import { Notification } from "./Notification"; 115 + 116 + export const YourNotification = (props: HydratedYourNotification) => { 117 + // Extract data from props.yourData 118 + 119 + return ( 120 + <Notification 121 + timestamp={props.created_at} 122 + href={/* link to relevant page */} 123 + icon={/* icon or avatar */} 124 + actionText={<>Message to display</>} 125 + content={/* optional additional content */} 126 + /> 127 + ); 128 + }; 129 + ``` 130 + 131 + ### 5. Update NotificationList (`NotificationList.tsx`) 132 + 133 + Import and render your notification type: 134 + 135 + ```typescript 136 + import { YourNotification } from "./YourNotification"; 137 + 138 + // In the map function: 139 + if (n.type === "your_type") { 140 + return <YourNotification key={n.id} {...n} />; 141 + } 142 + ``` 143 + 144 + ## Example: Subscribe Notifications 145 + 146 + See the implementation in: 147 + - `src/notifications.ts:88-125` - Hydration logic 148 + - `app/lish/subscribeToPublication.ts:55-68` - Trigger 149 + - `app/(home-pages)/notifications/FollowNotification.tsx` - Component 150 + - `app/(home-pages)/notifications/NotificationList.tsx:40-42` - Rendering
+85
.claude/skills/spec/SKILL.md
··· 1 + --- 2 + name: spec 3 + description: Draft design documents and specs with research-informed questioning 4 + disable-model-invocation: true 5 + --- 6 + 7 + # Spec Writing 8 + 9 + ## When to Use 10 + 11 + Use this skill when the user explicitly requests a spec, design document, or similar planning artifact. Do not auto-trigger for routine tasks. 12 + 13 + ## Process 14 + 15 + ### 1. Initial Scoping 16 + 17 + When starting a spec: 18 + - Ask the user for a brief description of what they want to build/change 19 + - Reason about which question categories matter for this particular workโ€”derive these from the problem, not a checklist 20 + 21 + ### 2. Interrogation Loop 22 + 23 + Conduct open-ended questioning until the designer signals completion: 24 + 25 + **Research-informed questions**: Before asking about an area, do just-in-time codebase research. Find relevant files, understand current patterns, identify constraints. Reference specific files and functions in questions when it adds clarity. 26 + 27 + **Adaptive scope**: When the designer marks something as out of scope, update your mental model and don't revisit. However, flag when you believe there are gaps that could cause problems: 28 + - "This approach assumes Xโ€”is that intentional?" 29 + - "The current implementation of Y in `src/foo.ts:42` handles Z differently. Should we align or diverge?" 30 + 31 + **Question style**: 32 + - Ask one focused question at a time, or a small related cluster 33 + - Ground questions in what you've learned from the codebase 34 + - Avoid hypotheticals that don't apply to this codebase 35 + - When presenting options, describe tradeoffs without recommending unless asked 36 + 37 + ### 3. Spec Writing 38 + 39 + When the designer indicates readiness, produce the spec document. 40 + 41 + ## Output 42 + 43 + Save to: `/specs/YYYY-MM-DD-short-name.md` 44 + 45 + ### Required Elements 46 + 47 + **Title and status** (draft | approved | implemented) 48 + 49 + **Goal**: What this achieves and why. 1-3 sentences. 50 + 51 + **Design**: The substance of what will be built/changed. For each significant component or concern: 52 + - Describe the approach 53 + - State key decisions with their rationale inline 54 + - Reference specific files, functions, and types where relevant 55 + - Structure subsections to fit the problemโ€”no fixed format 56 + 57 + **Implementation**: Ordered steps that an agent or developer can execute. Each step should: 58 + - Be concrete and actionable 59 + - Reference specific files/functions to modify or create 60 + - Be sequenced correctly (dependencies before dependents) 61 + 62 + ### Optional 63 + 64 + - Background context (only if necessary for understanding) 65 + - Open questions (only if unresolved items remain) 66 + 67 + Add other sections if the problem demands it. 68 + 69 + ## Writing Style 70 + 71 + - No filler, hedging, or preamble 72 + - No "This document describes..." or "In this spec we will..." 73 + - Start sections with substance, not meta-commentary 74 + - Use precise technical language 75 + - Keep decisions and rationale tightโ€”one sentence each when possible 76 + - Code references use `file/path.ts:lineNumber` or `functionName` in backticks 77 + - Prefer concrete over abstract; specific over general 78 + 79 + ## Status Lifecycle 80 + 81 + - **draft**: Work in progress. Should not be merged to main. 82 + - **approved**: Designer has signed off. Ready for implementation. 83 + - **implemented**: Work is complete. Spec is now historical record. 84 + 85 + Update status in the document as it progresses. Specs are point-in-time snapshotsโ€”do not update content after implementation begins except to change status.
-22
actions/publishToPublication.ts
··· 78 78 cover_image, 79 79 entitiesToDelete, 80 80 publishedAt, 81 - postPreferences, 82 81 }: { 83 82 root_entity: string; 84 83 publication_uri?: string; ··· 89 88 cover_image?: string | null; 90 89 entitiesToDelete?: string[]; 91 90 publishedAt?: string; 92 - postPreferences?: { 93 - showComments?: boolean; 94 - showMentions?: boolean; 95 - showRecommends?: boolean; 96 - } | null; 97 91 }): Promise<PublishResult> { 98 92 let identity = await getIdentityData(); 99 93 if (!identity || !identity.atp_did) { ··· 181 175 }; 182 176 } 183 177 184 - // Resolve preferences: explicit param > draft DB value 185 - const preferences = postPreferences ?? draft?.preferences; 186 - 187 178 // Extract theme for standalone documents (not for publications) 188 179 let theme: PubLeafletPublication.Theme | undefined; 189 180 if (!publication_uri) { ··· 254 245 ...(coverImageBlob && { coverImage: coverImageBlob }), 255 246 // Include theme for standalone documents (not for publication documents) 256 247 ...(!publication_uri && theme && { theme }), 257 - ...(preferences && { 258 - preferences: { 259 - $type: "pub.leaflet.publication#preferences" as const, 260 - ...preferences, 261 - }, 262 - }), 263 248 content: { 264 249 $type: "pub.leaflet.content" as const, 265 250 pages: pagesArray, ··· 272 257 author: credentialSession.did!, 273 258 ...(publication_uri && { publication: publication_uri }), 274 259 ...(theme && { theme }), 275 - ...(preferences && { 276 - preferences: { 277 - $type: "pub.leaflet.publication#preferences" as const, 278 - ...preferences, 279 - }, 280 - }), 281 260 title: title || "Untitled", 282 261 description: description || "", 283 262 ...(tags !== undefined && { tags }), ··· 300 279 await supabaseServerClient.from("documents").upsert({ 301 280 uri: result.uri, 302 281 data: record as unknown as Json, 303 - indexed: true, 304 282 }); 305 283 306 284 if (publication_uri) {
+5 -2
app/(home-pages)/notifications/BskyPostEmbedNotification.tsx
··· 2 2 import { ContentLayout, Notification } from "./Notification"; 3 3 import { HydratedBskyPostEmbedNotification } from "src/notifications"; 4 4 import { AtUri } from "@atproto/api"; 5 - import { getDocumentURL } from "app/lish/createPub/getPublicationURL"; 6 5 7 6 export const BskyPostEmbedNotification = ( 8 7 props: HydratedBskyPostEmbedNotification, ··· 12 11 13 12 if (!docRecord) return null; 14 13 15 - const href = getDocumentURL(docRecord, props.document.uri, pubRecord); 14 + const docUri = new AtUri(props.document.uri); 15 + const rkey = docUri.rkey; 16 + const did = docUri.host; 17 + 18 + const href = pubRecord ? `${pubRecord.url}/${rkey}` : `/p/${did}/${rkey}`; 16 19 17 20 const embedder = props.documentCreatorHandle 18 21 ? `@${props.documentCreatorHandle}`
+6 -4
app/(home-pages)/notifications/CommentMentionNotification.tsx
··· 8 8 Notification, 9 9 } from "./Notification"; 10 10 import { AtUri } from "@atproto/api"; 11 - import { getDocumentURL } from "app/lish/createPub/getPublicationURL"; 12 11 13 12 export const CommentMentionNotification = ( 14 13 props: HydratedCommentMentionNotification, ··· 20 19 const profileRecord = props.commentData.bsky_profiles 21 20 ?.record as AppBskyActorProfile.Record; 22 21 const pubRecord = props.normalizedPublication; 22 + const docUri = new AtUri(props.commentData.documents?.uri!); 23 + const rkey = docUri.rkey; 24 + const did = docUri.host; 23 25 24 - const href = 25 - getDocumentURL(docRecord, props.commentData.documents?.uri!, pubRecord) + 26 - "?interactionDrawer=comments"; 26 + const href = pubRecord 27 + ? `${pubRecord.url}/${rkey}?interactionDrawer=comments` 28 + : `/p/${did}/${rkey}?interactionDrawer=comments`; 27 29 28 30 const commenter = props.commenterHandle 29 31 ? `@${props.commenterHandle}`
+6 -4
app/(home-pages)/notifications/CommentNotication.tsx
··· 10 10 Notification, 11 11 } from "./Notification"; 12 12 import { AtUri } from "@atproto/api"; 13 - import { getDocumentURL } from "app/lish/createPub/getPublicationURL"; 14 13 15 14 export const CommentNotification = (props: HydratedCommentNotification) => { 16 15 const docRecord = props.normalizedDocument; ··· 25 24 props.commentData.bsky_profiles?.handle || 26 25 "Someone"; 27 26 const pubRecord = props.normalizedPublication; 27 + const docUri = new AtUri(props.commentData.documents?.uri!); 28 + const rkey = docUri.rkey; 29 + const did = docUri.host; 28 30 29 - const href = 30 - getDocumentURL(docRecord, props.commentData.documents?.uri!, pubRecord) + 31 - "?interactionDrawer=comments"; 31 + const href = pubRecord 32 + ? `${pubRecord.url}/${rkey}?interactionDrawer=comments` 33 + : `/p/${did}/${rkey}?interactionDrawer=comments`; 32 34 33 35 return ( 34 36 <Notification
+7 -2
app/(home-pages)/notifications/MentionNotification.tsx
··· 2 2 import { ContentLayout, Notification } from "./Notification"; 3 3 import { HydratedMentionNotification } from "src/notifications"; 4 4 import { AtUri } from "@atproto/api"; 5 - import { getDocumentURL } from "app/lish/createPub/getPublicationURL"; 6 5 7 6 export const MentionNotification = (props: HydratedMentionNotification) => { 8 7 const docRecord = props.normalizedDocument; ··· 10 9 11 10 if (!docRecord) return null; 12 11 13 - const href = getDocumentURL(docRecord, props.document.uri, pubRecord); 12 + const docUri = new AtUri(props.document.uri); 13 + const rkey = docUri.rkey; 14 + const did = docUri.host; 15 + 16 + const href = pubRecord 17 + ? `${pubRecord.url}/${rkey}` 18 + : `/p/${did}/${rkey}`; 14 19 15 20 let actionText: React.ReactNode; 16 21 let mentionedItemName: string | undefined;
-4
app/(home-pages)/notifications/NotificationList.tsx
··· 11 11 import { BskyPostEmbedNotification } from "./BskyPostEmbedNotification"; 12 12 import { MentionNotification } from "./MentionNotification"; 13 13 import { CommentMentionNotification } from "./CommentMentionNotification"; 14 - import { RecommendNotification } from "./RecommendNotification"; 15 14 16 15 export function NotificationList({ 17 16 notifications, ··· 58 57 } 59 58 if (n.type === "comment_mention") { 60 59 return <CommentMentionNotification key={n.id} {...n} />; 61 - } 62 - if (n.type === "recommend") { 63 - return <RecommendNotification key={n.id} {...n} />; 64 60 } 65 61 })} 66 62 </div>
+6 -2
app/(home-pages)/notifications/QuoteNotification.tsx
··· 3 3 import { HydratedQuoteNotification } from "src/notifications"; 4 4 import { AtUri } from "@atproto/api"; 5 5 import { Avatar } from "components/Avatar"; 6 - import { getDocumentURL } from "app/lish/createPub/getPublicationURL"; 7 6 8 7 export const QuoteNotification = (props: HydratedQuoteNotification) => { 9 8 const postView = props.bskyPost.post_view as any; ··· 14 13 15 14 if (!docRecord) return null; 16 15 16 + const docUri = new AtUri(props.document.uri); 17 + const rkey = docUri.rkey; 18 + const did = docUri.host; 17 19 const postText = postView.record?.text || ""; 18 20 19 - const href = getDocumentURL(docRecord, props.document.uri, pubRecord); 21 + const href = pubRecord 22 + ? `${pubRecord.url}/${rkey}` 23 + : `/p/${did}/${rkey}`; 20 24 21 25 return ( 22 26 <Notification
-45
app/(home-pages)/notifications/RecommendNotification.tsx
··· 1 - import { ContentLayout, Notification } from "./Notification"; 2 - import { HydratedRecommendNotification } from "src/notifications"; 3 - import { blobRefToSrc } from "src/utils/blobRefToSrc"; 4 - import { AppBskyActorProfile } from "lexicons/api"; 5 - import { Avatar } from "components/Avatar"; 6 - import { AtUri } from "@atproto/api"; 7 - import { RecommendTinyFilled } from "components/Icons/RecommendTiny"; 8 - import { getDocumentURL } from "app/lish/createPub/getPublicationURL"; 9 - 10 - export const RecommendNotification = ( 11 - props: HydratedRecommendNotification, 12 - ) => { 13 - const profileRecord = props.recommendData?.identities?.bsky_profiles 14 - ?.record as AppBskyActorProfile.Record; 15 - const displayName = 16 - profileRecord?.displayName || 17 - props.recommendData?.identities?.bsky_profiles?.handle || 18 - "Someone"; 19 - const docRecord = props.normalizedDocument; 20 - const pubRecord = props.normalizedPublication; 21 - const avatarSrc = 22 - profileRecord?.avatar?.ref && 23 - blobRefToSrc( 24 - profileRecord.avatar.ref, 25 - props.recommendData?.recommender_did || "", 26 - ); 27 - 28 - if (!docRecord) return null; 29 - 30 - const href = getDocumentURL(docRecord, props.document.uri, pubRecord); 31 - 32 - return ( 33 - <Notification 34 - timestamp={props.created_at} 35 - href={href} 36 - icon={<RecommendTinyFilled />} 37 - actionText={<>{displayName} recommended your post</>} 38 - content={ 39 - <ContentLayout postTitle={docRecord.title} pubRecord={pubRecord}> 40 - {null} 41 - </ContentLayout> 42 - } 43 - /> 44 - ); 45 - };
+6 -4
app/(home-pages)/notifications/ReplyNotification.tsx
··· 10 10 import { PubLeafletComment } from "lexicons/api"; 11 11 import { AppBskyActorProfile, AtUri } from "@atproto/api"; 12 12 import { blobRefToSrc } from "src/utils/blobRefToSrc"; 13 - import { getDocumentURL } from "app/lish/createPub/getPublicationURL"; 14 13 15 14 export const ReplyNotification = (props: HydratedCommentNotification) => { 16 15 const docRecord = props.normalizedDocument; ··· 33 32 props.parentData?.bsky_profiles?.handle || 34 33 "Someone"; 35 34 35 + const docUri = new AtUri(props.commentData.documents?.uri!); 36 + const rkey = docUri.rkey; 37 + const did = docUri.host; 36 38 const pubRecord = props.normalizedPublication; 37 39 38 - const href = 39 - getDocumentURL(docRecord, props.commentData.documents?.uri!, pubRecord) + 40 - "?interactionDrawer=comments"; 40 + const href = pubRecord 41 + ? `${pubRecord.url}/${rkey}?interactionDrawer=comments` 42 + : `/p/${did}/${rkey}?interactionDrawer=comments`; 41 43 42 44 return ( 43 45 <Notification
+12 -15
app/(home-pages)/p/[didOrHandle]/getProfilePosts.ts
··· 26 26 `*, 27 27 comments_on_documents(count), 28 28 document_mentions_in_bsky(count), 29 - recommends_on_documents(count), 30 29 documents_in_publications(publications(*))`, 31 30 ) 32 31 .like("uri", `at://${did}/%`) ··· 40 39 ); 41 40 } 42 41 43 - let [{ data: rawDocs }, { data: rawPubs }, { data: profile }] = 44 - await Promise.all([ 45 - query, 46 - supabaseServerClient 47 - .from("publications") 48 - .select("*") 49 - .eq("identity_did", did), 50 - supabaseServerClient 51 - .from("bsky_profiles") 52 - .select("handle") 53 - .eq("did", did) 54 - .single(), 55 - ]); 42 + let [{ data: rawDocs }, { data: rawPubs }, { data: profile }] = await Promise.all([ 43 + query, 44 + supabaseServerClient 45 + .from("publications") 46 + .select("*") 47 + .eq("identity_did", did), 48 + supabaseServerClient 49 + .from("bsky_profiles") 50 + .select("handle") 51 + .eq("did", did) 52 + .single(), 53 + ]); 56 54 57 55 // Deduplicate records that may exist under both pub.leaflet and site.standard namespaces 58 56 const docs = deduplicateByUriOrdered(rawDocs || []); ··· 84 82 sort_date: doc.sort_date, 85 83 comments_on_documents: doc.comments_on_documents, 86 84 document_mentions_in_bsky: doc.document_mentions_in_bsky, 87 - recommends_on_documents: doc.recommends_on_documents, 88 85 }, 89 86 }; 90 87
-3
app/(home-pages)/reader/getReaderFeed.ts
··· 32 32 `*, 33 33 comments_on_documents(count), 34 34 document_mentions_in_bsky(count), 35 - recommends_on_documents(count), 36 35 documents_in_publications!inner(publications!inner(*, publication_subscriptions!inner(*)))`, 37 36 ) 38 37 .eq( ··· 77 76 documents: { 78 77 comments_on_documents: post.comments_on_documents, 79 78 document_mentions_in_bsky: post.document_mentions_in_bsky, 80 - recommends_on_documents: post.recommends_on_documents, 81 79 data: normalizedData, 82 80 uri: post.uri, 83 81 sort_date: post.sort_date, ··· 114 112 sort_date: string; 115 113 comments_on_documents: { count: number }[] | undefined; 116 114 document_mentions_in_bsky: { count: number }[] | undefined; 117 - recommends_on_documents: { count: number }[] | undefined; 118 115 }; 119 116 };
-2
app/(home-pages)/tag/[tag]/getDocumentsByTag.ts
··· 21 21 `*, 22 22 comments_on_documents(count), 23 23 document_mentions_in_bsky(count), 24 - recommends_on_documents(count), 25 24 documents_in_publications(publications(*))`, 26 25 ) 27 26 .contains("data->tags", `["${tag}"]`) ··· 68 67 documents: { 69 68 comments_on_documents: doc.comments_on_documents, 70 69 document_mentions_in_bsky: doc.document_mentions_in_bsky, 71 - recommends_on_documents: doc.recommends_on_documents, 72 70 data: normalizedData, 73 71 uri: doc.uri, 74 72 sort_date: doc.sort_date,
-2
app/[leaflet_id]/Footer.tsx
··· 14 14 import { useIdentityData } from "components/IdentityProvider"; 15 15 import { useEntity } from "src/replicache"; 16 16 import { block } from "sharp"; 17 - import { PostSettings } from "components/PostSettings"; 18 17 19 18 export function hasBlockToolbar(blockType: string | null | undefined) { 20 19 return ( ··· 65 64 66 65 <PublishButton entityID={props.entityID} /> 67 66 <ShareOptions /> 68 - <PostSettings /> 69 67 <ThemePopover entityID={props.entityID} /> 70 68 </ActionFooter> 71 69 ) : (
-2
app/[leaflet_id]/Sidebar.tsx
··· 8 8 import { ShareOptions } from "app/[leaflet_id]/actions/ShareOptions"; 9 9 import { ThemePopover } from "components/ThemeManager/ThemeSetter"; 10 10 import { PublishButton } from "./actions/PublishButton"; 11 - import { PostSettings } from "components/PostSettings"; 12 11 import { Watermark } from "components/Watermark"; 13 12 import { BackToPubButton } from "./actions/BackToPubButton"; 14 13 import { useIdentityData } from "components/IdentityProvider"; ··· 31 30 <Sidebar> 32 31 <PublishButton entityID={rootEntity} /> 33 32 <ShareOptions /> 34 - <PostSettings /> 35 33 <ThemePopover entityID={rootEntity} /> 36 34 <HelpButton /> 37 35 <hr className="text-border" />
-10
app/[leaflet_id]/actions/PublishButton.tsx
··· 96 96 tx.get<string | null>("publication_cover_image"), 97 97 ); 98 98 99 - // Get post preferences from Replicache state 100 - let postPreferences = useSubscribe(rep, (tx) => 101 - tx.get<{ 102 - showComments?: boolean; 103 - showMentions?: boolean; 104 - showRecommends?: boolean; 105 - } | null>("post_preferences"), 106 - ); 107 - 108 99 // Get local published at from Replicache (session-only state, not persisted to DB) 109 100 let publishedAt = useLocalPublishedAt((s) => 110 101 pub?.doc ? s[pub?.doc] : undefined, ··· 127 118 tags: currentTags, 128 119 cover_image: coverImage, 129 120 publishedAt: publishedAt?.toISOString(), 130 - postPreferences, 131 121 }); 132 122 setIsLoading(false); 133 123 mutate();
+7 -9
app/[leaflet_id]/actions/ShareOptions/index.tsx
··· 14 14 useLeafletPublicationData, 15 15 } from "components/PageSWRDataProvider"; 16 16 import { ShareSmall } from "components/Icons/ShareSmall"; 17 - import { getPublicationURL, getDocumentURL } from "app/lish/createPub/getPublicationURL"; 17 + import { getPublicationURL } from "app/lish/createPub/getPublicationURL"; 18 18 import { AtUri } from "@atproto/syntax"; 19 19 import { useIsMobile } from "src/hooks/isMobile"; 20 20 ··· 89 89 let { permission_token } = useReplicache(); 90 90 let { data: pub, normalizedDocument } = useLeafletPublicationData(); 91 91 92 - let postLink = 93 - pub?.documents && normalizedDocument 94 - ? getDocumentURL( 95 - normalizedDocument, 96 - pub.documents.uri, 97 - pub?.publications || null, 98 - ) 99 - : null; 92 + let docURI = pub?.documents ? new AtUri(pub?.documents.uri) : null; 93 + let postLink = !docURI 94 + ? null 95 + : pub?.publications 96 + ? `${getPublicationURL(pub.publications)}/${docURI.rkey}` 97 + : `p/${docURI.host}/${docURI.rkey}`; 100 98 let publishLink = useReadOnlyShareLink(); 101 99 let [collabLink, setCollabLink] = useState<null | string>(null); 102 100 useEffect(() => {
-10
app/[leaflet_id]/publish/PublishPost.tsx
··· 91 91 tx.get<string | null>("publication_cover_image"), 92 92 ); 93 93 94 - // Get post preferences from Replicache state 95 - let postPreferences = useSubscribe(rep, (tx) => 96 - tx.get<{ 97 - showComments?: boolean; 98 - showMentions?: boolean; 99 - showRecommends?: boolean; 100 - } | null>("post_preferences"), 101 - ); 102 - 103 94 // Use Replicache tags only when we have a draft 104 95 const currentTags = props.hasDraft 105 96 ? Array.isArray(replicacheTags) ··· 133 124 cover_image: replicacheCoverImage, 134 125 entitiesToDelete: props.entitiesToDelete, 135 126 publishedAt: localPublishedAt?.toISOString() || new Date().toISOString(), 136 - postPreferences, 137 127 }); 138 128 139 129 if (!result.success) {
-6
app/api/inngest/client.ts
··· 51 51 documentUris?: string[]; 52 52 }; 53 53 }; 54 - "appview/sync-document-metadata": { 55 - data: { 56 - document_uri: string; 57 - bsky_post_uri?: string; 58 - }; 59 - }; 60 54 "user/write-records-to-pds": { 61 55 data: { 62 56 did: string;
+3 -12
app/api/inngest/functions/index_post_mention.ts
··· 3 3 import { AtpAgent, AtUri } from "@atproto/api"; 4 4 import { Json } from "supabase/database.types"; 5 5 import { ids } from "lexicons/api/lexicons"; 6 - import { 7 - Notification, 8 - pingIdentityToUpdateNotification, 9 - } from "src/notifications"; 6 + import { Notification, pingIdentityToUpdateNotification } from "src/notifications"; 10 7 import { v7 } from "uuid"; 11 8 import { idResolver } from "app/(home-pages)/reader/idResolver"; 12 9 import { documentUriFilter } from "src/utils/uriHelpers"; ··· 63 60 let { data: pub, error } = await supabaseServerClient 64 61 .from("publications") 65 62 .select("*") 66 - .or( 67 - `record->>base_path.eq.${url.host},record->>url.eq.https://${url.host}`, 68 - ) 69 - .order("uri", { ascending: false }) 70 - .limit(1) 63 + .or(`record->>base_path.eq.${url.host},record->>url.eq.https://${url.host}`) 71 64 .single(); 72 65 73 66 if (!pub) { ··· 87 80 const docData = docDataArr?.[0]; 88 81 89 82 if (!docData) { 90 - return { 91 - message: `No document found for publication ${url.host}/${path[0]}`, 92 - }; 83 + return { message: `No document found for publication ${url.host}/${path[0]}` }; 93 84 } 94 85 95 86 documentUri = docData.uri;
-71
app/api/inngest/functions/sync_document_metadata.ts
··· 1 - import { inngest } from "../client"; 2 - import { supabaseServerClient } from "supabase/serverClient"; 3 - import { AtpAgent, AtUri } from "@atproto/api"; 4 - import { idResolver } from "app/(home-pages)/reader/idResolver"; 5 - 6 - const TOTAL_ITERATIONS = 144; // 36 hours at 15-minute intervals 7 - 8 - export const sync_document_metadata = inngest.createFunction( 9 - { 10 - id: "sync_document_metadata", 11 - idempotency: "event.data.document_uri", 12 - }, 13 - { event: "appview/sync-document-metadata" }, 14 - async ({ event, step }) => { 15 - const { document_uri, bsky_post_uri } = event.data; 16 - 17 - const did = new AtUri(document_uri).host; 18 - 19 - const handleResult = await step.run("resolve-handle", async () => { 20 - const doc = await idResolver.did.resolve(did); 21 - const handle = doc?.alsoKnownAs 22 - ?.find((a) => a.startsWith("at://")) 23 - ?.replace("at://", ""); 24 - if (!doc) return null; 25 - const isBridgy = !!doc?.service?.find( 26 - (s) => 27 - typeof s.serviceEndpoint === "string" && 28 - s.serviceEndpoint.includes("atproto.brid.gy"), 29 - ); 30 - return { handle: handle ?? null, isBridgy, doc }; 31 - }); 32 - if (!handleResult) return { error: "No Handle" }; 33 - 34 - await step.run("set-indexed", async () => { 35 - return await supabaseServerClient 36 - .from("documents") 37 - .update({ indexed: !handleResult.isBridgy }) 38 - .eq("uri", document_uri) 39 - .select(); 40 - }); 41 - 42 - if (!bsky_post_uri || handleResult.isBridgy) { 43 - return { handle: handleResult.handle }; 44 - } 45 - 46 - const agent = new AtpAgent({ service: "https://public.api.bsky.app" }); 47 - 48 - const fetchAndUpdate = async () => { 49 - const res = await agent.app.bsky.feed.getPosts({ 50 - uris: [bsky_post_uri], 51 - }); 52 - const post = res.data.posts[0]; 53 - if (!post) return 0; 54 - const likeCount = post.likeCount ?? 0; 55 - await supabaseServerClient 56 - .from("documents") 57 - .update({ bsky_like_count: likeCount }) 58 - .eq("uri", document_uri); 59 - return likeCount; 60 - }; 61 - 62 - let likeCount = await step.run("sync-0", fetchAndUpdate); 63 - 64 - for (let i = 1; i < TOTAL_ITERATIONS; i++) { 65 - await step.sleep(`wait-${i}`, "15m"); 66 - likeCount = await step.run(`sync-${i}`, fetchAndUpdate); 67 - } 68 - 69 - return { likeCount, handle: handleResult.handle }; 70 - }, 71 - );
-2
app/api/inngest/route.tsx
··· 13 13 check_oauth_session, 14 14 } from "./functions/cleanup_expired_oauth_sessions"; 15 15 import { write_records_to_pds } from "./functions/write_records_to_pds"; 16 - import { sync_document_metadata } from "./functions/sync_document_metadata"; 17 16 18 17 export const { GET, POST, PUT } = serve({ 19 18 client: inngest, ··· 29 28 cleanup_expired_oauth_sessions, 30 29 check_oauth_session, 31 30 write_records_to_pds, 32 - sync_document_metadata, 33 31 ], 34 32 });
+1 -2
app/api/oauth/[route]/oauth-metadata.ts
··· 7 7 ? "http://localhost:3000" 8 8 : "https://leaflet.pub"; 9 9 10 - const scope = 11 - "atproto transition:generic transition:email include:pub.leaflet.authFullPermissions include:site.standard.authFull include:app.bsky.authCreatePosts include:app.bsky.authViewAll?aud=did:web:api.bsky.app%23bsky_appview blob:*/*"; 10 + const scope = "atproto transition:generic transition:email"; 12 11 const localconfig: OAuthClientMetadataInput = { 13 12 client_id: `http://localhost/?redirect_uri=${encodeURI(`http://127.0.0.1:3000/api/oauth/callback`)}&scope=${encodeURIComponent(scope)}`, 14 13 client_name: `Leaflet`,
+6 -9
app/api/oauth/[route]/route.ts
··· 42 42 const ac = new AbortController(); 43 43 44 44 const url = await client.authorize(handle || "https://bsky.social", { 45 - scope: 46 - "atproto transition:email include:pub.leaflet.authFullPermissions include:site.standard.authFull include:app.bsky.authCreatePosts include:app.bsky.authViewAll?aud=did:web:api.bsky.app%23bsky_appview blob:*/*", 45 + scope: "atproto transition:generic transition:email", 47 46 signal: ac.signal, 48 47 state: JSON.stringify(state), 49 48 }); ··· 90 89 // Trigger migration if identity needs it 91 90 const metadata = identity?.metadata as Record<string, unknown> | null; 92 91 if (metadata?.needsStandardSiteMigration) { 93 - if (process.env.NODE_ENV === "production") 94 - await inngest.send({ 95 - name: "user/migrate-to-standard", 96 - data: { did: session.did }, 97 - }); 92 + await inngest.send({ 93 + name: "user/migrate-to-standard", 94 + data: { did: session.did }, 95 + }); 98 96 } 99 97 100 98 let { data: token } = await supabaseServerClient ··· 106 104 }) 107 105 .select() 108 106 .single(); 109 - console.log({ token }); 107 + 110 108 if (token) await setAuthToken(token.id); 111 109 112 110 // Process successful authentication here ··· 115 113 console.log("User authenticated as:", session.did); 116 114 return handleAction(s.action, redirectPath); 117 115 } catch (e) { 118 - console.log(e); 119 116 redirect(redirectPath); 120 117 } 121 118 }
+1 -5
app/api/rpc/[command]/get_publication_data.ts
··· 40 40 documents_in_publications(documents( 41 41 *, 42 42 comments_on_documents(count), 43 - document_mentions_in_bsky(count), 44 - recommends_on_documents(count) 43 + document_mentions_in_bsky(count) 45 44 )), 46 45 publication_subscriptions(*, identities(bsky_profiles(*))), 47 46 publication_domains(*), ··· 86 85 indexed_at: dip.documents.indexed_at, 87 86 sort_date: dip.documents.sort_date, 88 87 data: dip.documents.data, 89 - bsky_like_count: dip.documents.bsky_like_count, 90 88 commentsCount: dip.documents.comments_on_documents[0]?.count || 0, 91 89 mentionsCount: dip.documents.document_mentions_in_bsky[0]?.count || 0, 92 - recommendsCount: 93 - dip.documents.recommends_on_documents?.[0]?.count || 0, 94 90 }; 95 91 }) 96 92 .filter((d): d is NonNullable<typeof d> => d !== null);
-40
app/api/rpc/[command]/get_user_recommendations.ts
··· 1 - import { z } from "zod"; 2 - import { makeRoute } from "../lib"; 3 - import type { Env } from "./route"; 4 - import { getIdentityData } from "actions/getIdentityData"; 5 - 6 - export type GetUserRecommendationsReturnType = Awaited< 7 - ReturnType<(typeof get_user_recommendations)["handler"]> 8 - >; 9 - 10 - export const get_user_recommendations = makeRoute({ 11 - route: "get_user_recommendations", 12 - input: z.object({ 13 - documentUris: z.array(z.string()), 14 - }), 15 - handler: async ({ documentUris }, { supabase }: Pick<Env, "supabase">) => { 16 - const identity = await getIdentityData(); 17 - const currentUserDid = identity?.atp_did; 18 - 19 - if (!currentUserDid || documentUris.length === 0) { 20 - return { 21 - result: {} as Record<string, boolean>, 22 - }; 23 - } 24 - 25 - const { data: recommendations } = await supabase 26 - .from("recommends_on_documents") 27 - .select("document") 28 - .eq("recommender_did", currentUserDid) 29 - .in("document", documentUris); 30 - 31 - const recommendedSet = new Set(recommendations?.map((r) => r.document)); 32 - 33 - const result: Record<string, boolean> = {}; 34 - for (const uri of documentUris) { 35 - result[uri] = recommendedSet.has(uri); 36 - } 37 - 38 - return { result }; 39 - }, 40 - });
-7
app/api/rpc/[command]/pull.ts
··· 9 9 import type { Attribute } from "src/replicache/attributes"; 10 10 import { makeRoute } from "../lib"; 11 11 import type { Env } from "./route"; 12 - import type { Json } from "supabase/database.types"; 13 12 14 13 // First define the sub-types for V0 and V1 requests 15 14 const pullRequestV0 = z.object({ ··· 76 75 title: string; 77 76 tags: string[]; 78 77 cover_image: string | null; 79 - preferences: Json | null; 80 78 }[]; 81 79 let pub_patch = publication_data?.[0] 82 80 ? [ ··· 99 97 op: "put", 100 98 key: "publication_cover_image", 101 99 value: publication_data[0].cover_image || null, 102 - }, 103 - { 104 - op: "put", 105 - key: "post_preferences", 106 - value: publication_data[0].preferences || null, 107 100 }, 108 101 ] 109 102 : [];
-2
app/api/rpc/[command]/route.ts
··· 14 14 import { search_publication_names } from "./search_publication_names"; 15 15 import { search_publication_documents } from "./search_publication_documents"; 16 16 import { get_profile_data } from "./get_profile_data"; 17 - import { get_user_recommendations } from "./get_user_recommendations"; 18 17 19 18 let supabase = createClient<Database>( 20 19 process.env.NEXT_PUBLIC_SUPABASE_API_URL as string, ··· 42 41 search_publication_names, 43 42 search_publication_documents, 44 43 get_profile_data, 45 - get_user_recommendations, 46 44 ]; 47 45 export async function POST( 48 46 req: Request,
+5 -7
app/api/rpc/[command]/search_publication_documents.ts
··· 2 2 import { z } from "zod"; 3 3 import { makeRoute } from "../lib"; 4 4 import type { Env } from "./route"; 5 - import { getDocumentURL } from "app/lish/createPub/getPublicationURL"; 6 - import { normalizeDocumentRecord } from "src/utils/normalizeRecords"; 5 + import { getPublicationURL } from "app/lish/createPub/getPublicationURL"; 7 6 8 7 export type SearchPublicationDocumentsReturnType = Awaited< 9 8 ReturnType<(typeof search_publication_documents)["handler"]> ··· 38 37 } 39 38 40 39 const result = documents.map((d) => { 41 - const normalizedDoc = normalizeDocumentRecord(d.documents.data, d.documents.uri); 40 + const docUri = new AtUri(d.documents.uri); 41 + const pubUrl = getPublicationURL(d.publications); 42 42 43 43 return { 44 44 uri: d.documents.uri, 45 - title: normalizedDoc?.title || (d.documents.data as { title?: string })?.title || "Untitled", 46 - url: normalizedDoc 47 - ? getDocumentURL(normalizedDoc, d.documents.uri, d.publications) 48 - : `${d.documents.uri}`, 45 + title: (d.documents.data as { title?: string })?.title || "Untitled", 46 + url: `${pubUrl}/${docUri.rkey}`, 49 47 }; 50 48 }); 51 49
+1 -3
app/lish/Subscribe.tsx
··· 87 87 return ( 88 88 <Popover 89 89 trigger={ 90 - <div className="text-accent-contrast text-sm w-fit"> 91 - Manage Subscription 92 - </div> 90 + <div className="text-accent-contrast text-sm">Manage Subscription</div> 93 91 } 94 92 > 95 93 <div className="max-w-sm flex flex-col gap-1">
+68
app/lish/[did]/[publication]/UpgradeModal.tsx
··· 1 + import { ButtonPrimary } from "components/Buttons"; 2 + import { Modal } from "components/Modal"; 3 + import { useState } from "react"; 4 + 5 + export const UpgradeContent = () => { 6 + let [cadence, setCadence] = useState<"year" | "month">("year"); 7 + return ( 8 + <div className="flex flex-col justify-center text-center sm:gap-4 gap-2 w-full"> 9 + <h2>Get Leaflet Pro!</h2> 10 + <div className="flex sm:flex-row flex-col sm:gap-8 gap-4 items-stretch w-full"> 11 + <div className="text-secondary sm:py-6 py-4 px-4"> 12 + <div className="font-bold text-primary">Analytics</div> 13 + <div className="">Views per Post</div> 14 + <div className="">Subscriber Counts</div> 15 + <div className="">Top Referrers</div> 16 + <hr className="my-4 border-border-light" /> 17 + <div className="font-bold text-primary">Coming ASAP</div> 18 + <div className="">Emails</div> 19 + <div className="">Paid Membership</div> 20 + </div> 21 + <div className="sm:w-64 w-full accent-container flex justify-center items-center"> 22 + <div className="flex flex-col justify-center text-center py-6"> 23 + <div className="flex gap-2 mb-3 p-1 bg-accent-contrast rounded-lg text-sm"> 24 + <button 25 + className={`px-1 rounded-md ${cadence === "year" ? "bg-bg-page font-bold text-accent-contrast" : "bg-transparent text-bg-page"}`} 26 + onClick={() => setCadence("year")} 27 + > 28 + Yearly 29 + </button> 30 + <button 31 + className={`px-1 rounded-md ${cadence === "month" ? "bg-bg-page font-bold text-accent-contrast" : "bg-transparent text-bg-page"}`} 32 + onClick={() => setCadence("month")} 33 + > 34 + Monthly 35 + </button> 36 + </div> 37 + <div className="flex gap-1 items-baseline justify-center"> 38 + <div className="text-2xl font-bold leading-tight"> 39 + {cadence === "year" ? "$120" : "$12"} 40 + </div> 41 + <div className="text-secondary pb-4"> 42 + {cadence === "year" ? "/year" : "/month"} 43 + </div> 44 + </div> 45 + <ButtonPrimary fullWidth className="mx-auto"> 46 + Get it! 47 + </ButtonPrimary> 48 + </div> 49 + </div> 50 + </div> 51 + </div> 52 + ); 53 + }; 54 + 55 + export const UpgradeModal = (props: { 56 + trigger: React.ReactNode; 57 + asChild?: boolean; 58 + }) => { 59 + return ( 60 + <Modal 61 + asChild={props.asChild} 62 + className="sm:w-fit w-[90vw]" 63 + trigger={<div className="sm:w-full">{props.trigger}</div>} 64 + > 65 + <UpgradeContent /> 66 + </Modal> 67 + ); 68 + };
-6
app/lish/[did]/[publication]/[rkey]/CanvasPage.tsx
··· 71 71 preferences={preferences} 72 72 commentsCount={getCommentCount(document.comments_on_documents, pageId)} 73 73 quotesCount={getQuoteCount(document.quotesAndMentions, pageId)} 74 - recommendsCount={document.recommendsCount} 75 74 /> 76 75 <CanvasContent 77 76 blocks={blocks} ··· 206 205 preferences: { 207 206 showComments?: boolean; 208 207 showMentions?: boolean; 209 - showRecommends?: boolean; 210 208 showPrevNext?: boolean; 211 209 }; 212 210 quotesCount: number | undefined; 213 211 commentsCount: number | undefined; 214 - recommendsCount: number; 215 212 }) => { 216 213 let isMobile = useIsMobile(); 217 214 return ( ··· 219 216 <Interactions 220 217 quotesCount={props.quotesCount || 0} 221 218 commentsCount={props.commentsCount || 0} 222 - recommendsCount={props.recommendsCount} 223 219 showComments={props.preferences.showComments !== false} 224 220 showMentions={props.preferences.showMentions !== false} 225 - showRecommends={props.preferences.showRecommends !== false} 226 221 pageId={props.pageId} 227 222 /> 228 223 {!props.isSubpage && ( ··· 238 233 data={props.data} 239 234 profile={props.profile} 240 235 preferences={props.preferences} 241 - isCanvas 242 236 /> 243 237 </Popover> 244 238 </>
+1 -2
app/lish/[did]/[publication]/[rkey]/DocumentPageRenderer.tsx
··· 21 21 } from "src/utils/normalizeRecords"; 22 22 import { DocumentProvider } from "contexts/DocumentContext"; 23 23 import { LeafletContentProvider } from "contexts/LeafletContentContext"; 24 - import { mergePreferences } from "src/utils/mergePreferences"; 25 24 26 25 export async function DocumentPageRenderer({ 27 26 did, ··· 134 133 <LeafletLayout> 135 134 <PostPages 136 135 document_uri={document.uri} 137 - preferences={mergePreferences(record?.preferences, pubRecord?.preferences)} 136 + preferences={pubRecord?.preferences || {}} 138 137 pubRecord={pubRecord} 139 138 profile={JSON.parse(JSON.stringify(profile.data))} 140 139 document={document}
+41 -83
app/lish/[did]/[publication]/[rkey]/Interactions/Interactions.tsx
··· 18 18 import { ManageSubscription, SubscribeWithBluesky } from "app/lish/Subscribe"; 19 19 import { EditTiny } from "components/Icons/EditTiny"; 20 20 import { getPublicationURL } from "app/lish/createPub/getPublicationURL"; 21 - import { RecommendButton } from "components/RecommendButton"; 22 - import { ButtonSecondary } from "components/Buttons"; 23 - import { Separator } from "components/Layout"; 24 21 25 22 export type InteractionState = { 26 23 drawerOpen: undefined | boolean; ··· 108 105 export const Interactions = (props: { 109 106 quotesCount: number; 110 107 commentsCount: number; 111 - recommendsCount: number; 112 108 className?: string; 113 109 showComments: boolean; 114 110 showMentions: boolean; 115 - showRecommends: boolean; 116 111 pageId?: string; 117 112 }) => { 118 - const { 119 - uri: document_uri, 120 - quotesAndMentions, 121 - normalizedDocument, 122 - } = useDocument(); 113 + const { uri: document_uri, quotesAndMentions, normalizedDocument } = useDocument(); 123 114 let { identity } = useIdentityData(); 124 115 125 116 let { drawerOpen, drawer, pageId } = useInteractionState(document_uri); ··· 133 124 const tags = normalizedDocument.tags; 134 125 const tagCount = tags?.length || 0; 135 126 136 - let interactionsAvailable = 137 - props.showComments || props.showMentions || props.showRecommends; 138 - 139 127 return ( 140 - <div 141 - className={`flex gap-[10px] text-tertiary text-sm item-center ${props.className}`} 142 - > 143 - {props.showRecommends === false ? null : ( 144 - <RecommendButton 145 - documentUri={document_uri} 146 - recommendsCount={props.recommendsCount} 147 - /> 148 - )} 128 + <div className={`flex gap-2 text-tertiary text-sm ${props.className}`}> 129 + {tagCount > 0 && <TagPopover tags={tags} tagCount={tagCount} />} 149 130 150 - {/*MENTIONS BUTTON*/} 151 131 {props.quotesCount === 0 || props.showMentions === false ? null : ( 152 132 <button 153 - className="flex w-fit gap-1 items-center" 133 + className="flex w-fit gap-2 items-center" 154 134 onClick={() => { 155 135 if (!drawerOpen || drawer !== "quotes") 156 136 openInteractionDrawer("quotes", document_uri, props.pageId); ··· 163 143 <QuoteTiny aria-hidden /> {props.quotesCount} 164 144 </button> 165 145 )} 166 - {/*COMMENT BUTTON*/} 167 146 {props.showComments === false ? null : ( 168 147 <button 169 - className="flex gap-1 items-center w-fit" 148 + className="flex gap-2 items-center w-fit" 170 149 onClick={() => { 171 150 if (!drawerOpen || drawer !== "comments" || pageId !== props.pageId) 172 151 openInteractionDrawer("comments", document_uri, props.pageId); ··· 177 156 <CommentTiny aria-hidden /> {props.commentsCount} 178 157 </button> 179 158 )} 180 - 181 - {tagCount > 0 && ( 182 - <> 183 - {interactionsAvailable && <Separator classname="h-4!" />} 184 - <TagPopover tags={tags} tagCount={tagCount} /> 185 - </> 186 - )} 187 159 </div> 188 160 ); 189 161 }; ··· 191 163 export const ExpandedInteractions = (props: { 192 164 quotesCount: number; 193 165 commentsCount: number; 194 - recommendsCount: number; 195 166 className?: string; 196 167 showComments: boolean; 197 168 showMentions: boolean; 198 - showRecommends: boolean; 199 169 pageId?: string; 200 170 }) => { 201 - const { 202 - uri: document_uri, 203 - quotesAndMentions, 204 - normalizedDocument, 205 - publication, 206 - leafletId, 207 - } = useDocument(); 171 + const { uri: document_uri, quotesAndMentions, normalizedDocument, publication, leafletId } = useDocument(); 208 172 let { identity } = useIdentityData(); 209 173 210 174 let { drawerOpen, drawer, pageId } = useInteractionState(document_uri); ··· 218 182 const tags = normalizedDocument.tags; 219 183 const tagCount = tags?.length || 0; 220 184 221 - let noInteractions = 222 - !props.showComments && !props.showMentions && !props.showRecommends; 185 + let noInteractions = !props.showComments && !props.showMentions; 223 186 224 187 let subscribed = 225 188 identity?.atp_did && ··· 228 191 (s) => s.identity === identity.atp_did, 229 192 ); 230 193 194 + let isAuthor = 195 + identity && 196 + identity.atp_did === publication?.identity_did && 197 + leafletId; 198 + 231 199 return ( 232 200 <div 233 201 className={`text-tertiary px-3 sm:px-4 flex flex-col ${props.className}`} ··· 246 214 {noInteractions ? ( 247 215 <div /> 248 216 ) : ( 249 - <div className="flex flex-col gap-2 just"> 250 - <div className="flex gap-2 sm:flex-row flex-col"> 251 - {props.showRecommends === false ? null : ( 252 - <RecommendButton 253 - documentUri={document_uri} 254 - recommendsCount={props.recommendsCount} 255 - expanded 256 - /> 257 - )} 217 + <> 218 + <div className="flex gap-2"> 258 219 {props.quotesCount === 0 || !props.showMentions ? null : ( 259 - <ButtonSecondary 220 + <button 221 + className="flex w-fit gap-2 items-center px-1 py-0.5 border border-border-light rounded-lg trasparent-outline selected-outline" 260 222 onClick={() => { 261 223 if (!drawerOpen || drawer !== "quotes") 262 224 openInteractionDrawer( ··· 271 233 onTouchStart={handleQuotePrefetch} 272 234 aria-label="Post quotes" 273 235 > 274 - <QuoteTiny aria-hidden /> {props.quotesCount} 275 - <Separator classname="h-4! text-accent-contrast!" /> 276 - Mention{props.quotesCount > 1 ? "s" : ""} 277 - </ButtonSecondary> 236 + <QuoteTiny aria-hidden /> {props.quotesCount}{" "} 237 + <span 238 + aria-hidden 239 + >{`Mention${props.quotesCount === 1 ? "" : "s"}`}</span> 240 + </button> 278 241 )} 279 242 {!props.showComments ? null : ( 280 - <ButtonSecondary 243 + <button 244 + className="flex gap-2 items-center w-fit px-1 py-0.5 border border-border-light rounded-lg trasparent-outline selected-outline" 281 245 onClick={() => { 282 246 if ( 283 247 !drawerOpen || ··· 295 259 aria-label="Post comments" 296 260 > 297 261 <CommentTiny aria-hidden />{" "} 298 - {props.commentsCount > 0 && ( 299 - <> 300 - {props.commentsCount} 301 - <Separator classname="h-4! text-accent-contrast!" /> 302 - </> 262 + {props.commentsCount > 0 ? ( 263 + <span aria-hidden> 264 + {`${props.commentsCount} Comment${props.commentsCount === 1 ? "" : "s"}`} 265 + </span> 266 + ) : ( 267 + "Comment" 303 268 )} 304 - Comment{props.commentsCount > 1 ? "s" : ""} 305 - </ButtonSecondary> 269 + </button> 306 270 )} 307 271 </div> 308 - {subscribed && publication && ( 309 - <ManageSubscription 310 - base_url={getPublicationURL(publication)} 311 - pub_uri={publication.uri} 312 - subscribers={publication.publication_subscriptions} 313 - /> 314 - )} 315 - </div> 272 + </> 316 273 )} 317 274 318 275 <EditButton publication={publication} leafletId={leafletId} /> 276 + {subscribed && publication && ( 277 + <ManageSubscription 278 + base_url={getPublicationURL(publication)} 279 + pub_uri={publication.uri} 280 + subscribers={publication.publication_subscriptions} 281 + /> 282 + )} 319 283 </div> 320 284 </div> 321 285 ); ··· 349 313 </div> 350 314 ); 351 315 }; 352 - export function getQuoteCount( 353 - quotesAndMentions: { uri: string; link?: string }[], 354 - pageId?: string, 355 - ) { 316 + export function getQuoteCount(quotesAndMentions: { uri: string; link?: string }[], pageId?: string) { 356 317 return getQuoteCountFromArray(quotesAndMentions, pageId); 357 318 } 358 319 ··· 377 338 } 378 339 } 379 340 380 - export function getCommentCount( 381 - comments: CommentOnDocument[], 382 - pageId?: string, 383 - ) { 341 + export function getCommentCount(comments: CommentOnDocument[], pageId?: string) { 384 342 if (pageId) 385 343 return comments.filter( 386 344 (c) => (c.record as PubLeafletComment.Record)?.onPage === pageId, ··· 404 362 return ( 405 363 <a 406 364 href={`https://leaflet.pub/${props.leafletId}`} 407 - className="flex gap-2 items-center hover:!no-underline selected-outline px-2 py-0.5 bg-accent-1 text-accent-2 font-bold w-fit rounded-md !border-accent-1 !outline-accent-1 h-fit" 365 + className="flex gap-2 items-center hover:!no-underline selected-outline px-2 py-0.5 bg-accent-1 text-accent-2 font-bold w-fit rounded-lg !border-accent-1 !outline-accent-1" 408 366 > 409 367 <EditTiny /> Edit Post 410 368 </a>
-156
app/lish/[did]/[publication]/[rkey]/Interactions/recommendAction.ts
··· 1 - "use server"; 2 - 3 - import { AtpBaseClient, PubLeafletInteractionsRecommend } from "lexicons/api"; 4 - import { getIdentityData } from "actions/getIdentityData"; 5 - import { restoreOAuthSession, OAuthSessionError } from "src/atproto-oauth"; 6 - import { TID } from "@atproto/common"; 7 - import { AtUri, Un$Typed } from "@atproto/api"; 8 - import { supabaseServerClient } from "supabase/serverClient"; 9 - import { Json } from "supabase/database.types"; 10 - import { v7 } from "uuid"; 11 - import { 12 - Notification, 13 - pingIdentityToUpdateNotification, 14 - } from "src/notifications"; 15 - 16 - type RecommendResult = 17 - | { success: true; uri: string } 18 - | { 19 - success: false; 20 - error: OAuthSessionError | { type: string; message: string }; 21 - }; 22 - 23 - export async function recommendAction(args: { 24 - document: string; 25 - }): Promise<RecommendResult> { 26 - console.log("recommend action..."); 27 - let identity = await getIdentityData(); 28 - if (!identity || !identity.atp_did) { 29 - return { 30 - success: false, 31 - error: { 32 - type: "oauth_session_expired", 33 - message: "Not authenticated", 34 - did: "", 35 - }, 36 - }; 37 - } 38 - 39 - const sessionResult = await restoreOAuthSession(identity.atp_did); 40 - if (!sessionResult.ok) { 41 - return { success: false, error: sessionResult.error }; 42 - } 43 - let credentialSession = sessionResult.value; 44 - let agent = new AtpBaseClient( 45 - credentialSession.fetchHandler.bind(credentialSession), 46 - ); 47 - 48 - let record: Un$Typed<PubLeafletInteractionsRecommend.Record> = { 49 - subject: args.document, 50 - createdAt: new Date().toISOString(), 51 - }; 52 - 53 - let rkey = TID.nextStr(); 54 - let uri = AtUri.make( 55 - credentialSession.did!, 56 - "pub.leaflet.interactions.recommend", 57 - rkey, 58 - ); 59 - 60 - await agent.pub.leaflet.interactions.recommend.create( 61 - { rkey, repo: credentialSession.did! }, 62 - record, 63 - ); 64 - 65 - let res = await supabaseServerClient.from("recommends_on_documents").upsert({ 66 - uri: uri.toString(), 67 - document: args.document, 68 - recommender_did: credentialSession.did!, 69 - record: { 70 - $type: "pub.leaflet.interactions.recommend", 71 - ...record, 72 - } as unknown as Json, 73 - }); 74 - console.log(res); 75 - 76 - // Notify the document owner 77 - let documentOwner = new AtUri(args.document).host; 78 - if (documentOwner !== credentialSession.did) { 79 - let notification: Notification = { 80 - id: v7(), 81 - recipient: documentOwner, 82 - data: { 83 - type: "recommend", 84 - document_uri: args.document, 85 - recommend_uri: uri.toString(), 86 - }, 87 - }; 88 - await supabaseServerClient.from("notifications").insert(notification); 89 - await pingIdentityToUpdateNotification(documentOwner); 90 - } 91 - 92 - return { 93 - success: true, 94 - uri: uri.toString(), 95 - }; 96 - } 97 - 98 - export async function unrecommendAction(args: { 99 - document: string; 100 - }): Promise<RecommendResult> { 101 - let identity = await getIdentityData(); 102 - if (!identity || !identity.atp_did) { 103 - return { 104 - success: false, 105 - error: { 106 - type: "oauth_session_expired", 107 - message: "Not authenticated", 108 - did: "", 109 - }, 110 - }; 111 - } 112 - 113 - const sessionResult = await restoreOAuthSession(identity.atp_did); 114 - if (!sessionResult.ok) { 115 - return { success: false, error: sessionResult.error }; 116 - } 117 - let credentialSession = sessionResult.value; 118 - let agent = new AtpBaseClient( 119 - credentialSession.fetchHandler.bind(credentialSession), 120 - ); 121 - 122 - // Find the existing recommend record 123 - const { data: existingRecommend } = await supabaseServerClient 124 - .from("recommends_on_documents") 125 - .select("uri") 126 - .eq("document", args.document) 127 - .eq("recommender_did", credentialSession.did!) 128 - .single(); 129 - 130 - if (!existingRecommend) { 131 - return { 132 - success: false, 133 - error: { 134 - type: "not_found", 135 - message: "Recommend not found", 136 - }, 137 - }; 138 - } 139 - 140 - let uri = new AtUri(existingRecommend.uri); 141 - 142 - await agent.pub.leaflet.interactions.recommend.delete({ 143 - rkey: uri.rkey, 144 - repo: credentialSession.did!, 145 - }); 146 - 147 - await supabaseServerClient 148 - .from("recommends_on_documents") 149 - .delete() 150 - .eq("uri", existingRecommend.uri); 151 - 152 - return { 153 - success: true, 154 - uri: existingRecommend.uri, 155 - }; 156 - }
+1 -5
app/lish/[did]/[publication]/[rkey]/LinearDocumentPage.tsx
··· 87 87 pageId={pageId} 88 88 showComments={preferences.showComments !== false} 89 89 showMentions={preferences.showMentions !== false} 90 - showRecommends={preferences.showRecommends !== false} 91 - commentsCount={ 92 - getCommentCount(document.comments_on_documents, pageId) || 0 93 - } 90 + commentsCount={getCommentCount(document.comments_on_documents, pageId) || 0} 94 91 quotesCount={getQuoteCount(document.quotesAndMentions, pageId) || 0} 95 - recommendsCount={document.recommendsCount} 96 92 /> 97 93 {!hasPageBackground && <div className={`spacer h-8 w-full`} />} 98 94 </PageWrapper>
+1 -2
app/lish/[did]/[publication]/[rkey]/PostContent.tsx
··· 32 32 import { PublishedPollBlock } from "./Blocks/PublishedPollBlock"; 33 33 import { PollData } from "./fetchPollData"; 34 34 import { ButtonPrimary } from "components/Buttons"; 35 - import { PostNotAvailable } from "components/Blocks/BlueskyPostBlock/BlueskyEmbed"; 36 35 37 36 export function PostContent({ 38 37 blocks, ··· 171 170 case PubLeafletBlocksBskyPost.isMain(b.block): { 172 171 let uri = b.block.postRef.uri; 173 172 let post = bskyPostData.find((p) => p.uri === uri); 174 - if (!post) return <PostNotAvailable />; 173 + if (!post) return <div>no prefetched post rip</div>; 175 174 return ( 176 175 <PubBlueskyPostBlock 177 176 post={post}
+7 -20
app/lish/[did]/[publication]/[rkey]/PostHeader/PostHeader.tsx
··· 18 18 export function PostHeader(props: { 19 19 data: PostPageData; 20 20 profile: ProfileViewDetailed; 21 - preferences: { 22 - showComments?: boolean; 23 - showMentions?: boolean; 24 - showRecommends?: boolean; 25 - }; 26 - isCanvas?: boolean; 21 + preferences: { showComments?: boolean; showMentions?: boolean }; 27 22 }) { 28 23 let { identity } = useIdentityData(); 29 24 let document = props.data; ··· 89 84 </> 90 85 ) : null} 91 86 </div> 92 - {!props.isCanvas && ( 93 - <Interactions 94 - showComments={props.preferences.showComments !== false} 95 - showMentions={props.preferences.showMentions !== false} 96 - showRecommends={props.preferences.showRecommends !== false} 97 - quotesCount={ 98 - getQuoteCount(document?.quotesAndMentions || []) || 0 99 - } 100 - commentsCount={ 101 - getCommentCount(document?.comments_on_documents || []) || 0 102 - } 103 - recommendsCount={document?.recommendsCount || 0} 104 - /> 105 - )} 87 + <Interactions 88 + showComments={props.preferences.showComments !== false} 89 + showMentions={props.preferences.showMentions !== false} 90 + quotesCount={getQuoteCount(document?.quotesAndMentions || []) || 0} 91 + commentsCount={getCommentCount(document?.comments_on_documents || []) || 0} 92 + /> 106 93 </> 107 94 } 108 95 />
+4 -6
app/lish/[did]/[publication]/[rkey]/PostPages.tsx
··· 170 170 preferences: { 171 171 showComments?: boolean; 172 172 showMentions?: boolean; 173 - showRecommends?: boolean; 174 173 showPrevNext?: boolean; 175 174 }; 176 175 pubRecord?: NormalizedPublication | null; ··· 234 233 preferences: { 235 234 showComments?: boolean; 236 235 showMentions?: boolean; 237 - showRecommends?: boolean; 238 236 showPrevNext?: boolean; 239 237 }; 240 238 pollData: PollData[]; ··· 295 293 showPageBackground={pubRecord?.theme?.showPageBackground} 296 294 document_uri={document.uri} 297 295 comments={ 298 - preferences.showComments === false 296 + pubRecord?.preferences?.showComments === false 299 297 ? [] 300 298 : document.comments_on_documents 301 299 } 302 300 quotesAndMentions={ 303 - preferences.showMentions === false 301 + pubRecord?.preferences?.showMentions === false 304 302 ? [] 305 303 : quotesAndMentions 306 304 } ··· 387 385 pageId={page.id} 388 386 document_uri={document.uri} 389 387 comments={ 390 - preferences.showComments === false 388 + pubRecord?.preferences?.showComments === false 391 389 ? [] 392 390 : document.comments_on_documents 393 391 } 394 392 quotesAndMentions={ 395 - preferences.showMentions === false 393 + pubRecord?.preferences?.showMentions === false 396 394 ? [] 397 395 : quotesAndMentions 398 396 }
+17 -38
app/lish/[did]/[publication]/[rkey]/getPostPageData.ts
··· 8 8 } from "src/utils/normalizeRecords"; 9 9 import { PubLeafletPublication, SiteStandardPublication } from "lexicons/api"; 10 10 import { documentUriFilter } from "src/utils/uriHelpers"; 11 - import { getDocumentURL } from "app/lish/createPub/getPublicationURL"; 12 11 13 12 export async function getPostPageData(did: string, rkey: string) { 14 13 let { data: documents } = await supabaseServerClient ··· 23 22 publication_subscriptions(*)) 24 23 ), 25 24 document_mentions_in_bsky(*), 26 - leaflets_in_publications(*), 27 - recommends_on_documents(count) 25 + leaflets_in_publications(*) 28 26 `, 29 27 ) 30 28 .or(documentUriFilter(did, rkey)) ··· 35 33 if (!document) return null; 36 34 37 35 // Normalize the document record - this is the primary way consumers should access document data 38 - const normalizedDocument = normalizeDocumentRecord( 39 - document.data, 40 - document.uri, 41 - ); 36 + const normalizedDocument = normalizeDocumentRecord(document.data, document.uri); 42 37 if (!normalizedDocument) return null; 43 38 44 39 // Normalize the publication record - this is the primary way consumers should access publication data 45 40 const normalizedPublication = normalizePublicationRecord( 46 - document.documents_in_publications[0]?.publications?.record, 41 + document.documents_in_publications[0]?.publications?.record 47 42 ); 48 43 49 44 // Fetch constellation backlinks for mentions 50 - const postUrl = getDocumentURL(normalizedDocument, document.uri, normalizedPublication); 51 - // Constellation needs an absolute URL 52 - const absolutePostUrl = postUrl.startsWith("/") 53 - ? `https://leaflet.pub${postUrl}` 54 - : postUrl; 55 - const constellationBacklinks = await getConstellationBacklinks(absolutePostUrl); 45 + let aturi = new AtUri(document.uri); 46 + const postUrl = normalizedPublication 47 + ? `${normalizedPublication.url}/${aturi.rkey}` 48 + : `https://leaflet.pub/p/${aturi.host}/${aturi.rkey}`; 49 + const constellationBacklinks = await getConstellationBacklinks(postUrl); 56 50 57 51 // Deduplicate constellation backlinks (same post could appear in both links and embeds) 58 52 const uniqueBacklinks = Array.from( ··· 89 83 // Filter and sort documents by publishedAt 90 84 const sortedDocs = allDocs 91 85 .map((dip) => { 92 - const normalizedData = normalizeDocumentRecord( 93 - dip?.documents?.data, 94 - dip?.documents?.uri, 95 - ); 86 + const normalizedData = normalizeDocumentRecord(dip?.documents?.data, dip?.documents?.uri); 96 87 return { 97 88 uri: dip?.documents?.uri, 98 89 title: normalizedData?.title, ··· 107 98 ); 108 99 109 100 // Find current document index 110 - const currentIndex = sortedDocs.findIndex( 111 - (doc) => doc.uri === document.uri, 112 - ); 101 + const currentIndex = sortedDocs.findIndex((doc) => doc.uri === document.uri); 113 102 114 103 if (currentIndex !== -1) { 115 104 prevNext = { ··· 133 122 134 123 // Build explicit publication context for consumers 135 124 const rawPub = document.documents_in_publications[0]?.publications; 136 - const publication = rawPub 137 - ? { 138 - uri: rawPub.uri, 139 - name: rawPub.name, 140 - identity_did: rawPub.identity_did, 141 - record: rawPub.record as 142 - | PubLeafletPublication.Record 143 - | SiteStandardPublication.Record 144 - | null, 145 - publication_subscriptions: rawPub.publication_subscriptions || [], 146 - } 147 - : null; 148 - 149 - // Get recommends count from the aggregated query result 150 - const recommendsCount = document.recommends_on_documents?.[0]?.count ?? 0; 125 + const publication = rawPub ? { 126 + uri: rawPub.uri, 127 + name: rawPub.name, 128 + identity_did: rawPub.identity_did, 129 + record: rawPub.record as PubLeafletPublication.Record | SiteStandardPublication.Record | null, 130 + publication_subscriptions: rawPub.publication_subscriptions || [], 131 + } : null; 151 132 152 133 return { 153 134 ...document, ··· 162 143 comments: document.comments_on_documents, 163 144 mentions: document.document_mentions_in_bsky, 164 145 leafletId: document.leaflets_in_publications[0]?.leaflet || null, 165 - // Recommends data 166 - recommendsCount, 167 146 }; 168 147 } 169 148
+45
app/lish/[did]/[publication]/dashboard/Actions.tsx
··· 10 10 import { useSmoker } from "components/Toast"; 11 11 import { useIsMobile } from "src/hooks/isMobile"; 12 12 import { SpeedyLink } from "components/SpeedyLink"; 13 + import { ButtonSecondary, ButtonTertiary } from "components/Buttons"; 14 + import { UpgradeModal } from "../UpgradeModal"; 15 + import { LeafletPro } from "components/Icons/LeafletPro"; 13 16 14 17 export const Actions = (props: { publication: string }) => { 15 18 return ( 16 19 <> 17 20 <NewDraftActionButton publication={props.publication} /> 21 + <MobileUpgrade /> 22 + 18 23 <PublicationShareButton /> 19 24 <PublicationSettingsButton publication={props.publication} /> 25 + <DesktopUpgrade /> 20 26 </> 21 27 ); 22 28 }; ··· 76 82 </Menu> 77 83 ); 78 84 } 85 + 86 + const MobileUpgrade = () => { 87 + return ( 88 + <UpgradeModal 89 + asChild 90 + trigger={ 91 + <ActionButton 92 + label="Upgrade to Leaflet Pro" 93 + icon={<LeafletPro />} 94 + className={`sm:hidden block bg-[var(--accent-light)]!`} 95 + style={{ backgroundColor: "var(--accent-light) important!" }} 96 + /> 97 + } 98 + /> 99 + ); 100 + }; 101 + 102 + const DesktopUpgrade = () => { 103 + return ( 104 + <div 105 + style={{ backgroundColor: "var(--accent-light)" }} 106 + className=" rounded-md mt-2 pt-2 pb-3 px-3 sm:block hidden" 107 + > 108 + <h4 className="text-accent-contrast text-sm">Get Leaflet Pro</h4> 109 + <div className="text-xs text-secondary mb-2"> 110 + <strong>Analytics!</strong> Emails and membership soon. 111 + </div> 112 + <UpgradeModal 113 + asChild 114 + trigger={ 115 + <ButtonSecondary fullWidth compact className="text-sm!"> 116 + Learn more 117 + </ButtonSecondary> 118 + } 119 + /> 120 + <ButtonTertiary className="mx-auto text-sm">Dismiss</ButtonTertiary> 121 + </div> 122 + ); 123 + };
+9
app/lish/[did]/[publication]/dashboard/PublicationAnalytics.tsx
··· 1 + import { UpgradeContent } from "../UpgradeModal"; 2 + 3 + export const PublicationAnalytics = () => { 4 + return ( 5 + <div className="sm:mx-auto pt-4 s"> 6 + <UpgradeContent /> 7 + </div> 8 + ); 9 + };
+6 -1
app/lish/[did]/[publication]/dashboard/PublicationDashboard.tsx
··· 12 12 } from "components/PageLayouts/DashboardLayout"; 13 13 import { useDebouncedEffect } from "src/hooks/useDebouncedEffect"; 14 14 import { type NormalizedPublication } from "src/utils/normalizeRecords"; 15 + import { PublicationAnalytics } from "./PublicationAnalytics"; 15 16 16 17 export default function PublicationDashboard({ 17 18 publication, ··· 55 56 /> 56 57 ), 57 58 }, 58 - Published: { 59 + Posts: { 59 60 content: ( 60 61 <PublishedPostsList 61 62 searchValue={debouncedSearchValue} ··· 70 71 showPageBackground={!!record.theme?.showPageBackground} 71 72 /> 72 73 ), 74 + controls: null, 75 + }, 76 + Analytics: { 77 + content: <PublicationAnalytics />, 73 78 controls: null, 74 79 }, 75 80 }}
+11 -15
app/lish/[did]/[publication]/dashboard/PublishedPostsLists.tsx
··· 9 9 } from "./PublicationSWRProvider"; 10 10 import { Fragment } from "react"; 11 11 import { useParams } from "next/navigation"; 12 - import { getPublicationURL, getDocumentURL } from "app/lish/createPub/getPublicationURL"; 12 + import { getPublicationURL } from "app/lish/createPub/getPublicationURL"; 13 13 import { SpeedyLink } from "components/SpeedyLink"; 14 14 import { InteractionPreview } from "components/InteractionsPreview"; 15 15 import { useLocalizedDate } from "src/hooks/useLocalizedDate"; ··· 60 60 61 61 function PublishedPostItem(props: { 62 62 doc: PublishedDocument; 63 - publication: NonNullable< 64 - NonNullable<ReturnType<typeof usePublicationData>["data"]>["publication"] 65 - >; 63 + publication: NonNullable<NonNullable<ReturnType<typeof usePublicationData>["data"]>["publication"]>; 66 64 pubRecord: ReturnType<typeof useNormalizedPublicationRecord>; 67 65 showPageBackground: boolean; 68 66 }) { ··· 71 69 const leaflet = publication.leaflets_in_publications.find( 72 70 (l) => l.doc === doc.uri, 73 71 ); 74 - const docUrl = getDocumentURL(doc.record, doc.uri, publication); 75 72 76 73 return ( 77 74 <Fragment> ··· 88 85 <a 89 86 className="hover:no-underline!" 90 87 target="_blank" 91 - href={docUrl} 88 + href={`${getPublicationURL(publication)}/${uri.rkey}`} 92 89 > 93 90 <h3 className="text-primary grow leading-snug"> 94 91 {doc.record.title} ··· 97 94 <div className="flex justify-start align-top flex-row gap-1"> 98 95 {leaflet && leaflet.permission_tokens && ( 99 96 <> 100 - <SpeedyLink className="pt-[6px]" href={`/${leaflet.leaflet}`}> 97 + <SpeedyLink 98 + className="pt-[6px]" 99 + href={`/${leaflet.leaflet}`} 100 + > 101 101 <EditTiny /> 102 102 </SpeedyLink> 103 103 ··· 113 113 indexed_at: doc.indexed_at, 114 114 sort_date: doc.sort_date, 115 115 data: doc.data, 116 - bsky_like_count: doc.bsky_like_count ?? 0, 117 - indexed: true, 118 - recommend_count: doc.recommendsCount ?? 0, 119 116 }, 120 117 }, 121 118 ], ··· 132 129 </div> 133 130 134 131 {doc.record.description ? ( 135 - <p className="italic text-secondary">{doc.record.description}</p> 132 + <p className="italic text-secondary"> 133 + {doc.record.description} 134 + </p> 136 135 ) : null} 137 136 <div className="text-sm text-tertiary flex gap-3 justify-between sm:justify-start items-center pt-3"> 138 137 {doc.record.publishedAt ? ( ··· 141 140 <InteractionPreview 142 141 quotesCount={doc.mentionsCount} 143 142 commentsCount={doc.commentsCount} 144 - recommendsCount={doc.recommendsCount} 145 - documentUri={doc.uri} 146 143 tags={doc.record.tags || []} 147 144 showComments={pubRecord?.preferences?.showComments !== false} 148 145 showMentions={pubRecord?.preferences?.showMentions !== false} 149 - showRecommends={pubRecord?.preferences?.showRecommends !== false} 150 - postUrl={docUrl} 146 + postUrl={`${getPublicationURL(publication)}/${uri.rkey}`} 151 147 /> 152 148 </div> 153 149 </div>
+1 -21
app/lish/[did]/[publication]/dashboard/settings/PostOptions.tsx
··· 29 29 ? true 30 30 : record.preferences.showMentions, 31 31 ); 32 - let [showRecommends, setShowRecommends] = useState( 33 - record?.preferences?.showRecommends === undefined 34 - ? true 35 - : record.preferences.showRecommends, 36 - ); 37 32 let [showPrevNext, setShowPrevNext] = useState( 38 33 record?.preferences?.showPrevNext === undefined 39 34 ? true ··· 58 53 showComments: showComments, 59 54 showMentions: showMentions, 60 55 showPrevNext: showPrevNext, 61 - showRecommends: showRecommends, 62 56 }, 63 57 }); 64 58 toast({ type: "success", content: <strong>Posts Updated!</strong> }); ··· 105 99 <div className="flex flex-col justify-start"> 106 100 <div className="font-bold">Show Mentions</div> 107 101 <div className="text-tertiary text-sm leading-tight"> 108 - Display a list of Bluesky mentions about your post 109 - </div> 110 - </div> 111 - </Toggle> 112 - 113 - <Toggle 114 - toggle={showRecommends} 115 - onToggle={() => { 116 - setShowRecommends(!showRecommends); 117 - }} 118 - > 119 - <div className="flex flex-col justify-start"> 120 - <div className="font-bold">Show Recommends</div> 121 - <div className="text-tertiary text-sm leading-tight"> 122 - Allow readers to recommend/like your post 102 + Display a list of posts on Bluesky that mention your post 123 103 </div> 124 104 </div> 125 105 </Toggle>
+37 -46
app/lish/[did]/[publication]/generateFeed.ts
··· 11 11 hasLeafletContent, 12 12 } from "src/utils/normalizeRecords"; 13 13 import { publicationNameOrUriFilter } from "src/utils/uriHelpers"; 14 - import { getDocumentURL } from "app/lish/createPub/getPublicationURL"; 15 14 16 15 export async function generateFeed( 17 16 did: string, ··· 53 52 }, 54 53 }); 55 54 56 - let docs = publication.documents_in_publications.sort((a, b) => { 57 - const dateA = a.documents?.sort_date 58 - ? new Date(a.documents.sort_date).getTime() 59 - : 0; 60 - const dateB = b.documents?.sort_date 61 - ? new Date(b.documents.sort_date).getTime() 62 - : 0; 63 - return dateB - dateA; // Sort in descending order (newest first) 64 - }); 65 - for (const doc of docs) { 66 - if (!doc.documents) continue; 67 - const record = normalizeDocumentRecord( 68 - doc.documents?.data, 69 - doc.documents?.uri, 70 - ); 71 - const uri = new AtUri(doc.documents?.uri); 72 - const rkey = uri.rkey; 73 - if (!record) continue; 55 + await Promise.all( 56 + publication.documents_in_publications.map(async (doc) => { 57 + if (!doc.documents) return; 58 + const record = normalizeDocumentRecord( 59 + doc.documents?.data, 60 + doc.documents?.uri, 61 + ); 62 + const uri = new AtUri(doc.documents?.uri); 63 + const rkey = uri.rkey; 64 + if (!record) return; 74 65 75 - let blocks: PubLeafletPagesLinearDocument.Block[] = []; 76 - if (hasLeafletContent(record) && record.content.pages[0]) { 77 - const firstPage = record.content.pages[0]; 78 - if (PubLeafletPagesLinearDocument.isMain(firstPage)) { 79 - blocks = firstPage.blocks || []; 66 + let blocks: PubLeafletPagesLinearDocument.Block[] = []; 67 + if (hasLeafletContent(record) && record.content.pages[0]) { 68 + const firstPage = record.content.pages[0]; 69 + if (PubLeafletPagesLinearDocument.isMain(firstPage)) { 70 + blocks = firstPage.blocks || []; 71 + } 80 72 } 81 - } 82 - const stream = await renderToReadableStream( 83 - createElement(StaticPostContent, { blocks, did: uri.host }), 84 - ); 85 - const reader = stream.getReader(); 86 - const chunks = []; 73 + const stream = await renderToReadableStream( 74 + createElement(StaticPostContent, { blocks, did: uri.host }), 75 + ); 76 + const reader = stream.getReader(); 77 + const chunks = []; 87 78 88 - let done, value; 89 - while (!done) { 90 - ({ done, value } = await reader.read()); 91 - if (value) { 92 - chunks.push(new TextDecoder().decode(value)); 79 + let done, value; 80 + while (!done) { 81 + ({ done, value } = await reader.read()); 82 + if (value) { 83 + chunks.push(new TextDecoder().decode(value)); 84 + } 93 85 } 94 - } 95 86 96 - const docUrl = getDocumentURL(record, doc.documents.uri, pubRecord); 97 - feed.addItem({ 98 - title: record.title, 99 - description: record.description, 100 - date: record.publishedAt ? new Date(record.publishedAt) : new Date(), 101 - id: docUrl, 102 - link: docUrl, 103 - content: chunks.join(""), 104 - }); 105 - } 87 + feed.addItem({ 88 + title: record.title, 89 + description: record.description, 90 + date: record.publishedAt ? new Date(record.publishedAt) : new Date(), 91 + id: `${pubRecord.url}/${rkey}`, 92 + link: `${pubRecord.url}/${rkey}`, 93 + content: chunks.join(""), 94 + }); 95 + }), 96 + ); 106 97 107 98 return feed; 108 99 }
+11 -18
app/lish/[did]/[publication]/page.tsx
··· 1 1 import { supabaseServerClient } from "supabase/serverClient"; 2 2 import { AtUri } from "@atproto/syntax"; 3 - import { getPublicationURL, getDocumentURL } from "app/lish/createPub/getPublicationURL"; 3 + import { getPublicationURL } from "app/lish/createPub/getPublicationURL"; 4 4 import { BskyAgent } from "@atproto/api"; 5 5 import { publicationNameOrUriFilter } from "src/utils/uriHelpers"; 6 6 import { SubscribeWithBluesky } from "app/lish/Subscribe"; ··· 38 38 documents_in_publications(documents( 39 39 *, 40 40 comments_on_documents(count), 41 - document_mentions_in_bsky(count), 42 - recommends_on_documents(count) 41 + document_mentions_in_bsky(count) 43 42 )) 44 43 `, 45 44 ) ··· 120 119 }) 121 120 .map((doc) => { 122 121 if (!doc.documents) return null; 123 - const doc_record = normalizeDocumentRecord( 124 - doc.documents.data, 125 - ); 122 + const doc_record = normalizeDocumentRecord(doc.documents.data); 126 123 if (!doc_record) return null; 127 124 let uri = new AtUri(doc.documents.uri); 128 125 let quotes = ··· 131 128 record?.preferences?.showComments === false 132 129 ? 0 133 130 : doc.documents.comments_on_documents[0].count || 0; 134 - let recommends = 135 - doc.documents.recommends_on_documents?.[0]?.count || 0; 136 131 let tags = doc_record.tags || []; 137 132 138 - const docUrl = getDocumentURL(doc_record, doc.documents.uri, publication); 139 133 return ( 140 134 <React.Fragment key={doc.documents?.uri}> 141 135 <div className="flex w-full grow flex-col "> 142 136 <SpeedyLink 143 - href={docUrl} 137 + href={`${getPublicationURL(publication)}/${uri.rkey}`} 144 138 className="publishedPost hover:no-underline! flex flex-col" 145 139 > 146 140 <h3 className="text-primary">{doc_record.title}</h3> ··· 149 143 </p> 150 144 </SpeedyLink> 151 145 152 - <div className="justify-between w-full text-sm text-tertiary flex gap-1 flex-wrap pt-2 items-center"> 146 + <div className="text-sm text-tertiary flex gap-1 flex-wrap pt-2 items-center"> 153 147 <p className="text-sm text-tertiary "> 154 148 {doc_record.publishedAt && ( 155 149 <LocalizedDate ··· 162 156 /> 163 157 )}{" "} 164 158 </p> 165 - 159 + {comments > 0 || quotes > 0 || tags.length > 0 ? ( 160 + <Separator classname="h-4! mx-1" /> 161 + ) : ( 162 + "" 163 + )} 166 164 <InteractionPreview 167 165 quotesCount={quotes} 168 166 commentsCount={comments} 169 - recommendsCount={recommends} 170 - documentUri={doc.documents.uri} 171 167 tags={tags} 172 - postUrl={docUrl} 168 + postUrl={`${getPublicationURL(publication)}/${uri.rkey}`} 173 169 showComments={ 174 170 record?.preferences?.showComments !== false 175 171 } 176 172 showMentions={ 177 173 record?.preferences?.showMentions !== false 178 - } 179 - showRecommends={ 180 - record?.preferences?.showRecommends !== false 181 174 } 182 175 /> 183 176 </div>
-1
app/lish/createPub/CreatePubForm.tsx
··· 58 58 showComments: true, 59 59 showMentions: true, 60 60 showPrevNext: true, 61 - showRecommends: true, 62 61 }, 63 62 }); 64 63
+2 -1
app/lish/createPub/UpdatePubForm.tsx
··· 88 88 showComments: showComments, 89 89 showMentions: showMentions, 90 90 showPrevNext: showPrevNext, 91 - showRecommends: record?.preferences?.showRecommends ?? true, 92 91 }, 93 92 }); 94 93 toast({ type: "success", content: "Updated!" }); ··· 195 194 </p> 196 195 </div> 197 196 </Toggle> 197 + 198 + 198 199 </div> 199 200 </form> 200 201 );
+9 -23
app/lish/createPub/createPublication.ts
··· 5 5 PubLeafletPublication, 6 6 SiteStandardPublication, 7 7 } from "lexicons/api"; 8 - import { restoreOAuthSession, OAuthSessionError } from "src/atproto-oauth"; 8 + import { 9 + restoreOAuthSession, 10 + OAuthSessionError, 11 + } from "src/atproto-oauth"; 9 12 import { getIdentityData } from "actions/getIdentityData"; 10 13 import { supabaseServerClient } from "supabase/serverClient"; 11 14 import { Json } from "supabase/database.types"; ··· 73 76 74 77 // Build record based on publication type 75 78 let record: SiteStandardPublication.Record | PubLeafletPublication.Record; 76 - let iconBlob: 77 - | Awaited< 78 - ReturnType<typeof agent.com.atproto.repo.uploadBlob> 79 - >["data"]["blob"] 80 - | undefined; 79 + let iconBlob: Awaited<ReturnType<typeof agent.com.atproto.repo.uploadBlob>>["data"]["blob"] | undefined; 81 80 82 81 // Upload the icon if provided 83 82 if (iconFile && iconFile.size > 0) { ··· 98 97 ...(iconBlob && { icon: iconBlob }), 99 98 basicTheme: { 100 99 $type: "site.standard.theme.basic", 101 - background: { 102 - $type: "site.standard.theme.color#rgb", 103 - ...PubThemeDefaultsRGB.background, 104 - }, 105 - foreground: { 106 - $type: "site.standard.theme.color#rgb", 107 - ...PubThemeDefaultsRGB.foreground, 108 - }, 109 - accent: { 110 - $type: "site.standard.theme.color#rgb", 111 - ...PubThemeDefaultsRGB.accent, 112 - }, 113 - accentForeground: { 114 - $type: "site.standard.theme.color#rgb", 115 - ...PubThemeDefaultsRGB.accentForeground, 116 - }, 100 + background: { $type: "site.standard.theme.color#rgb", ...PubThemeDefaultsRGB.background }, 101 + foreground: { $type: "site.standard.theme.color#rgb", ...PubThemeDefaultsRGB.foreground }, 102 + accent: { $type: "site.standard.theme.color#rgb", ...PubThemeDefaultsRGB.accent }, 103 + accentForeground: { $type: "site.standard.theme.color#rgb", ...PubThemeDefaultsRGB.accentForeground }, 117 104 }, 118 105 preferences: { 119 106 showInDiscover: preferences.showInDiscover, 120 107 showComments: preferences.showComments, 121 108 showMentions: preferences.showMentions, 122 109 showPrevNext: preferences.showPrevNext, 123 - showRecommends: preferences.showRecommends, 124 110 }, 125 111 } satisfies SiteStandardPublication.Record; 126 112 } else {
-49
app/lish/createPub/getPublicationURL.ts
··· 5 5 import { 6 6 normalizePublicationRecord, 7 7 isLeafletPublication, 8 - hasLeafletContent, 9 - type NormalizedDocument, 10 8 type NormalizedPublication, 11 9 } from "src/utils/normalizeRecords"; 12 10 ··· 46 44 const name = aturi.rkey || normalized?.name; 47 45 return `/lish/${aturi.host}/${encodeURIComponent(name || "")}`; 48 46 } 49 - 50 - /** 51 - * Gets the full URL for a document. 52 - * Always appends the document's path property. 53 - * For non-leaflet documents (content.$type !== "pub.leaflet.content"), 54 - * always uses the full publication site URL, not internal /lish/ URLs. 55 - */ 56 - export function getDocumentURL( 57 - doc: NormalizedDocument, 58 - docUri: string, 59 - publication?: PublicationInput | NormalizedPublication | null, 60 - ): string { 61 - let path = doc.path || "/" + new AtUri(docUri).rkey; 62 - if (path[0] !== "/") path = "/" + path; 63 - const aturi = new AtUri(docUri); 64 - 65 - const isNormalized = 66 - !!publication && 67 - (publication as NormalizedPublication).$type === 68 - "site.standard.publication"; 69 - const normPub = isNormalized 70 - ? (publication as NormalizedPublication) 71 - : publication 72 - ? normalizePublicationRecord((publication as PublicationInput).record) 73 - : null; 74 - const pubInput = isNormalized 75 - ? null 76 - : (publication as PublicationInput | null); 77 - 78 - // Non-leaflet documents always use the full publication site URL 79 - if (doc.content && !hasLeafletContent(doc) && normPub?.url) { 80 - return normPub.url + path; 81 - } 82 - 83 - // For leaflet documents, use getPublicationURL (may return /lish/ internal paths) 84 - if (pubInput) { 85 - return getPublicationURL(pubInput) + path; 86 - } 87 - 88 - // When we only have a normalized publication, use its URL directly 89 - if (normPub?.url) { 90 - return normPub.url + path; 91 - } 92 - 93 - // Standalone document fallback 94 - return `/p/${aturi.host}${path}`; 95 - }
+98 -167
app/lish/createPub/updatePublication.ts
··· 77 77 } 78 78 79 79 const aturi = new AtUri(existingPub.uri); 80 - const publicationType = getPublicationType( 81 - aturi.collection, 82 - ) as PublicationType; 80 + const publicationType = getPublicationType(aturi.collection) as PublicationType; 83 81 84 82 // Normalize existing record 85 83 const normalizedPub = normalizePublicationRecord(existingPub.record); ··· 130 128 } 131 129 132 130 /** Merges override with existing value, respecting explicit undefined */ 133 - function resolveField<T>( 134 - override: T | undefined, 135 - existing: T | undefined, 136 - hasOverride: boolean, 137 - ): T | undefined { 131 + function resolveField<T>(override: T | undefined, existing: T | undefined, hasOverride: boolean): T | undefined { 138 132 return hasOverride ? override : existing; 139 133 } 140 134 ··· 152 146 return { 153 147 $type: "pub.leaflet.publication", 154 148 name: overrides.name ?? normalizedPub?.name ?? "", 155 - description: resolveField( 156 - overrides.description, 157 - normalizedPub?.description, 158 - "description" in overrides, 159 - ), 160 - icon: resolveField( 161 - overrides.icon, 162 - normalizedPub?.icon, 163 - "icon" in overrides, 164 - ), 165 - theme: resolveField( 166 - overrides.theme, 167 - normalizedPub?.theme, 168 - "theme" in overrides, 169 - ), 149 + description: resolveField(overrides.description, normalizedPub?.description, "description" in overrides), 150 + icon: resolveField(overrides.icon, normalizedPub?.icon, "icon" in overrides), 151 + theme: resolveField(overrides.theme, normalizedPub?.theme, "theme" in overrides), 170 152 base_path: overrides.basePath ?? existingBasePath, 171 - preferences: preferences 172 - ? { 173 - $type: "pub.leaflet.publication#preferences", 174 - showInDiscover: preferences.showInDiscover, 175 - showComments: preferences.showComments, 176 - showMentions: preferences.showMentions, 177 - showPrevNext: preferences.showPrevNext, 178 - showRecommends: preferences.showRecommends, 179 - } 180 - : undefined, 153 + preferences: preferences ? { 154 + $type: "pub.leaflet.publication#preferences", 155 + showInDiscover: preferences.showInDiscover, 156 + showComments: preferences.showComments, 157 + showMentions: preferences.showMentions, 158 + showPrevNext: preferences.showPrevNext, 159 + } : undefined, 181 160 }; 182 161 } 183 162 ··· 196 175 return { 197 176 $type: "site.standard.publication", 198 177 name: overrides.name ?? normalizedPub?.name ?? "", 199 - description: resolveField( 200 - overrides.description, 201 - normalizedPub?.description, 202 - "description" in overrides, 203 - ), 204 - icon: resolveField( 205 - overrides.icon, 206 - normalizedPub?.icon, 207 - "icon" in overrides, 208 - ), 209 - theme: resolveField( 210 - overrides.theme, 211 - normalizedPub?.theme, 212 - "theme" in overrides, 213 - ), 214 - basicTheme: resolveField( 215 - overrides.basicTheme, 216 - normalizedPub?.basicTheme, 217 - "basicTheme" in overrides, 218 - ), 178 + description: resolveField(overrides.description, normalizedPub?.description, "description" in overrides), 179 + icon: resolveField(overrides.icon, normalizedPub?.icon, "icon" in overrides), 180 + theme: resolveField(overrides.theme, normalizedPub?.theme, "theme" in overrides), 181 + basicTheme: resolveField(overrides.basicTheme, normalizedPub?.basicTheme, "basicTheme" in overrides), 219 182 url: basePath ? `https://${basePath}` : normalizedPub?.url || "", 220 - preferences: preferences 221 - ? { 222 - showInDiscover: preferences.showInDiscover, 223 - showComments: preferences.showComments, 224 - showMentions: preferences.showMentions, 225 - showPrevNext: preferences.showPrevNext, 226 - showRecommends: preferences.showRecommends, 227 - } 228 - : undefined, 183 + preferences: preferences ? { 184 + showInDiscover: preferences.showInDiscover, 185 + showComments: preferences.showComments, 186 + showMentions: preferences.showMentions, 187 + showPrevNext: preferences.showPrevNext, 188 + } : undefined, 229 189 }; 230 190 } 231 191 ··· 257 217 iconFile?: File | null; 258 218 preferences?: Omit<PubLeafletPublication.Preferences, "$type">; 259 219 }): Promise<UpdatePublicationResult> { 260 - return withPublicationUpdate( 261 - uri, 262 - async ({ normalizedPub, existingBasePath, publicationType, agent }) => { 263 - // Upload icon if provided 264 - let iconBlob = normalizedPub?.icon; 265 - if (iconFile && iconFile.size > 0) { 266 - const buffer = await iconFile.arrayBuffer(); 267 - const uploadResult = await agent.com.atproto.repo.uploadBlob( 268 - new Uint8Array(buffer), 269 - { encoding: iconFile.type }, 270 - ); 271 - if (uploadResult.data.blob) { 272 - iconBlob = uploadResult.data.blob; 273 - } 220 + return withPublicationUpdate(uri, async ({ normalizedPub, existingBasePath, publicationType, agent }) => { 221 + // Upload icon if provided 222 + let iconBlob = normalizedPub?.icon; 223 + if (iconFile && iconFile.size > 0) { 224 + const buffer = await iconFile.arrayBuffer(); 225 + const uploadResult = await agent.com.atproto.repo.uploadBlob( 226 + new Uint8Array(buffer), 227 + { encoding: iconFile.type }, 228 + ); 229 + if (uploadResult.data.blob) { 230 + iconBlob = uploadResult.data.blob; 274 231 } 232 + } 275 233 276 - return buildRecord(normalizedPub, existingBasePath, publicationType, { 277 - name, 278 - description, 279 - icon: iconBlob, 280 - preferences, 281 - }); 282 - }, 283 - ); 234 + return buildRecord(normalizedPub, existingBasePath, publicationType, { 235 + name, 236 + description, 237 + icon: iconBlob, 238 + preferences, 239 + }); 240 + }); 284 241 } 285 242 286 243 export async function updatePublicationBasePath({ ··· 290 247 uri: string; 291 248 base_path: string; 292 249 }): Promise<UpdatePublicationResult> { 293 - return withPublicationUpdate( 294 - uri, 295 - async ({ normalizedPub, existingBasePath, publicationType }) => { 296 - return buildRecord(normalizedPub, existingBasePath, publicationType, { 297 - basePath: base_path, 298 - }); 299 - }, 300 - ); 250 + return withPublicationUpdate(uri, async ({ normalizedPub, existingBasePath, publicationType }) => { 251 + return buildRecord(normalizedPub, existingBasePath, publicationType, { 252 + basePath: base_path, 253 + }); 254 + }); 301 255 } 302 256 303 257 type Color = ··· 321 275 accentText: Color; 322 276 }; 323 277 }): Promise<UpdatePublicationResult> { 324 - return withPublicationUpdate( 325 - uri, 326 - async ({ normalizedPub, existingBasePath, publicationType, agent }) => { 327 - // Build theme object 328 - const themeData = { 329 - $type: "pub.leaflet.publication#theme" as const, 330 - backgroundImage: theme.backgroundImage 331 - ? { 332 - $type: "pub.leaflet.theme.backgroundImage", 333 - image: ( 334 - await agent.com.atproto.repo.uploadBlob( 335 - new Uint8Array(await theme.backgroundImage.arrayBuffer()), 336 - { encoding: theme.backgroundImage.type }, 337 - ) 338 - )?.data.blob, 339 - width: theme.backgroundRepeat || undefined, 340 - repeat: !!theme.backgroundRepeat, 341 - } 342 - : theme.backgroundImage === null 343 - ? undefined 344 - : normalizedPub?.theme?.backgroundImage, 345 - backgroundColor: theme.backgroundColor 346 - ? { 347 - ...theme.backgroundColor, 348 - } 349 - : undefined, 350 - pageWidth: theme.pageWidth, 351 - primary: { 352 - ...theme.primary, 353 - }, 354 - pageBackground: { 355 - ...theme.pageBackground, 356 - }, 357 - showPageBackground: theme.showPageBackground, 358 - accentBackground: { 359 - ...theme.accentBackground, 360 - }, 361 - accentText: { 362 - ...theme.accentText, 363 - }, 364 - }; 278 + return withPublicationUpdate(uri, async ({ normalizedPub, existingBasePath, publicationType, agent }) => { 279 + // Build theme object 280 + const themeData = { 281 + $type: "pub.leaflet.publication#theme" as const, 282 + backgroundImage: theme.backgroundImage 283 + ? { 284 + $type: "pub.leaflet.theme.backgroundImage", 285 + image: ( 286 + await agent.com.atproto.repo.uploadBlob( 287 + new Uint8Array(await theme.backgroundImage.arrayBuffer()), 288 + { encoding: theme.backgroundImage.type }, 289 + ) 290 + )?.data.blob, 291 + width: theme.backgroundRepeat || undefined, 292 + repeat: !!theme.backgroundRepeat, 293 + } 294 + : theme.backgroundImage === null 295 + ? undefined 296 + : normalizedPub?.theme?.backgroundImage, 297 + backgroundColor: theme.backgroundColor 298 + ? { 299 + ...theme.backgroundColor, 300 + } 301 + : undefined, 302 + pageWidth: theme.pageWidth, 303 + primary: { 304 + ...theme.primary, 305 + }, 306 + pageBackground: { 307 + ...theme.pageBackground, 308 + }, 309 + showPageBackground: theme.showPageBackground, 310 + accentBackground: { 311 + ...theme.accentBackground, 312 + }, 313 + accentText: { 314 + ...theme.accentText, 315 + }, 316 + }; 365 317 366 - // Derive basicTheme from the theme colors for site.standard.publication 367 - const basicTheme: NormalizedPublication["basicTheme"] = { 368 - $type: "site.standard.theme.basic", 369 - background: { 370 - $type: "site.standard.theme.color#rgb", 371 - r: theme.backgroundColor.r, 372 - g: theme.backgroundColor.g, 373 - b: theme.backgroundColor.b, 374 - }, 375 - foreground: { 376 - $type: "site.standard.theme.color#rgb", 377 - r: theme.primary.r, 378 - g: theme.primary.g, 379 - b: theme.primary.b, 380 - }, 381 - accent: { 382 - $type: "site.standard.theme.color#rgb", 383 - r: theme.accentBackground.r, 384 - g: theme.accentBackground.g, 385 - b: theme.accentBackground.b, 386 - }, 387 - accentForeground: { 388 - $type: "site.standard.theme.color#rgb", 389 - r: theme.accentText.r, 390 - g: theme.accentText.g, 391 - b: theme.accentText.b, 392 - }, 393 - }; 318 + // Derive basicTheme from the theme colors for site.standard.publication 319 + const basicTheme: NormalizedPublication["basicTheme"] = { 320 + $type: "site.standard.theme.basic", 321 + background: { $type: "site.standard.theme.color#rgb", r: theme.backgroundColor.r, g: theme.backgroundColor.g, b: theme.backgroundColor.b }, 322 + foreground: { $type: "site.standard.theme.color#rgb", r: theme.primary.r, g: theme.primary.g, b: theme.primary.b }, 323 + accent: { $type: "site.standard.theme.color#rgb", r: theme.accentBackground.r, g: theme.accentBackground.g, b: theme.accentBackground.b }, 324 + accentForeground: { $type: "site.standard.theme.color#rgb", r: theme.accentText.r, g: theme.accentText.g, b: theme.accentText.b }, 325 + }; 394 326 395 - return buildRecord(normalizedPub, existingBasePath, publicationType, { 396 - theme: themeData, 397 - basicTheme, 398 - }); 399 - }, 400 - ); 327 + return buildRecord(normalizedPub, existingBasePath, publicationType, { 328 + theme: themeData, 329 + basicTheme, 330 + }); 331 + }); 401 332 }
+13 -10
app/lish/subscribeToPublication.ts
··· 3 3 import { AtpBaseClient } from "lexicons/api"; 4 4 import { AppBskyActorDefs, Agent as BskyAgent } from "@atproto/api"; 5 5 import { getIdentityData } from "actions/getIdentityData"; 6 - import { restoreOAuthSession, OAuthSessionError } from "src/atproto-oauth"; 6 + import { 7 + restoreOAuthSession, 8 + OAuthSessionError, 9 + } from "src/atproto-oauth"; 7 10 import { TID } from "@atproto/common"; 8 11 import { supabaseServerClient } from "supabase/serverClient"; 9 12 import { revalidatePath } from "next/cache"; ··· 76 79 } 77 80 78 81 let bsky = new BskyAgent(credentialSession); 79 - let [profile, resolveDid] = await Promise.all([ 82 + let [prefs, profile, resolveDid] = await Promise.all([ 83 + bsky.app.bsky.actor.getPreferences(), 80 84 bsky.app.bsky.actor.profile 81 85 .get({ 82 86 repo: credentialSession.did!, ··· 92 96 handle: resolveDid?.alsoKnownAs?.[0]?.slice(5), 93 97 }); 94 98 } 99 + let savedFeeds = prefs.data.preferences.find( 100 + (pref) => pref.$type === "app.bsky.actor.defs#savedFeedsPrefV2", 101 + ) as AppBskyActorDefs.SavedFeedsPrefV2; 95 102 revalidatePath("/lish/[did]/[publication]", "layout"); 96 103 return { 97 104 success: true, 98 - hasFeed: true, 105 + hasFeed: !!savedFeeds.items.find((feed) => feed.value === leafletFeedURI), 99 106 }; 100 107 } 101 108 ··· 104 111 | { success: false; error: OAuthSessionError }; 105 112 106 113 export async function unsubscribeToPublication( 107 - publication: string, 114 + publication: string 108 115 ): Promise<UnsubscribeResult> { 109 116 let identity = await getIdentityData(); 110 117 if (!identity || !identity.atp_did) { ··· 137 144 // Delete from both collections (old and new schema) - one or both may exist 138 145 let rkey = new AtUri(existingSubscription.uri).rkey; 139 146 await Promise.all([ 140 - agent.pub.leaflet.graph.subscription 141 - .delete({ repo: credentialSession.did!, rkey }) 142 - .catch(() => {}), 143 - agent.site.standard.graph.subscription 144 - .delete({ repo: credentialSession.did!, rkey }) 145 - .catch(() => {}), 147 + agent.pub.leaflet.graph.subscription.delete({ repo: credentialSession.did!, rkey }).catch(() => {}), 148 + agent.site.standard.graph.subscription.delete({ repo: credentialSession.did!, rkey }).catch(() => {}), 146 149 ]); 147 150 148 151 await supabaseServerClient
+1 -18
appview/index.ts
··· 109 109 data: record.value as Json, 110 110 }); 111 111 if (docResult.error) console.log(docResult.error); 112 - await inngest.send({ 113 - name: "appview/sync-document-metadata", 114 - data: { 115 - document_uri: evt.uri.toString(), 116 - bsky_post_uri: record.value.postRef?.uri, 117 - }, 118 - }); 119 112 if (record.value.publication) { 120 113 let publicationURI = new AtUri(record.value.publication); 121 114 ··· 276 269 data: record.value as Json, 277 270 }); 278 271 if (docResult.error) console.log(docResult.error); 279 - await inngest.send({ 280 - name: "appview/sync-document-metadata", 281 - data: { 282 - document_uri: evt.uri.toString(), 283 - bsky_post_uri: record.value.bskyPostRef?.uri, 284 - }, 285 - }); 286 272 287 273 // site.standard.document uses "site" field to reference the publication 288 274 // For documents in publications, site is an AT-URI (at://did:plc:xxx/site.standard.publication/rkey) ··· 392 378 393 379 // Now validate the record since we know it contains our quote param 394 380 let record = AppBskyFeedPost.validateRecord(evt.record); 395 - if (!record.success) { 396 - console.log(record.error); 397 - return; 398 - } 381 + if (!record.success) return; 399 382 400 383 let embed: string | null = null; 401 384 if (
+5 -8
components/ActionBar/ActionButton.tsx
··· 7 7 8 8 type ButtonProps = Omit<JSX.IntrinsicElements["button"], "content">; 9 9 10 - export const ActionButton = forwardRef< 11 - HTMLButtonElement, 12 - ButtonProps & { 10 + export const ActionButton = ( 11 + _props: ButtonProps & { 13 12 id?: string; 14 13 icon: React.ReactNode; 15 14 label: React.ReactNode; ··· 20 19 subtext?: string; 21 20 labelOnMobile?: boolean; 22 21 z?: boolean; 23 - } 24 - >((_props, ref) => { 22 + }, 23 + ) => { 25 24 let { 26 25 id, 27 26 icon, ··· 51 50 return ( 52 51 <button 53 52 {...buttonProps} 54 - ref={ref} 55 53 className={` 56 54 actionButton relative font-bold 57 55 rounded-md border ··· 83 81 </div> 84 82 </button> 85 83 ); 86 - }); 87 - ActionButton.displayName = "ActionButton"; 84 + };
-1
components/Blocks/Block.tsx
··· 419 419 if (focusedEntity?.entityType === "page") return; 420 420 421 421 if (isMultiselected) return; 422 - if (!entity_set.permissions.write) return null; 423 422 424 423 return ( 425 424 <div
+23 -19
components/Blocks/TextBlock/mountProsemirror.ts
··· 80 80 handlePaste, 81 81 handleClickOn: (_view, _pos, node, _nodePos, _event, direct) => { 82 82 if (!direct) return; 83 - 84 - // Check for didMention inline nodes 85 - if (node?.type === schema.nodes.didMention) { 86 - window.open( 87 - didToBlueskyUrl(node.attrs.did), 88 - "_blank", 89 - "noopener,noreferrer", 90 - ); 91 - return; 92 - } 93 - 94 - // Check for atMention inline nodes 95 - if (node?.type === schema.nodes.atMention) { 96 - const url = atUriToUrl(node.attrs.atURI); 97 - window.open(url, "_blank", "noopener,noreferrer"); 98 - return; 99 - } 100 83 if (node.nodeSize - 2 <= _pos) return; 101 84 102 85 // Check for marks at the clicked position ··· 104 87 const nodeAt2 = node.nodeAt(Math.max(_pos - 2, 0)); 105 88 106 89 // Check for link marks 107 - let linkMark = 108 - nodeAt1?.marks.find((f) => f.type === schema.marks.link) || 90 + let linkMark = nodeAt1?.marks.find((f) => f.type === schema.marks.link) || 109 91 nodeAt2?.marks.find((f) => f.type === schema.marks.link); 110 92 if (linkMark) { 111 93 window.open(linkMark.attrs.href, "_blank"); 94 + return; 95 + } 96 + 97 + // Check for didMention inline nodes 98 + if (nodeAt1?.type === schema.nodes.didMention) { 99 + window.open(didToBlueskyUrl(nodeAt1.attrs.did), "_blank", "noopener,noreferrer"); 100 + return; 101 + } 102 + if (nodeAt2?.type === schema.nodes.didMention) { 103 + window.open(didToBlueskyUrl(nodeAt2.attrs.did), "_blank", "noopener,noreferrer"); 104 + return; 105 + } 106 + 107 + // Check for atMention inline nodes 108 + if (nodeAt1?.type === schema.nodes.atMention) { 109 + const url = atUriToUrl(nodeAt1.attrs.atURI); 110 + window.open(url, "_blank", "noopener,noreferrer"); 111 + return; 112 + } 113 + if (nodeAt2?.type === schema.nodes.atMention) { 114 + const url = atUriToUrl(nodeAt2.attrs.atURI); 115 + window.open(url, "_blank", "noopener,noreferrer"); 112 116 return; 113 117 } 114 118 },
+1 -20
components/Blocks/TextBlock/useHandlePaste.ts
··· 396 396 ]); 397 397 } 398 398 399 - if (child.tagName === "DIV" && child.getAttribute("data-bluesky-post")) { 400 - let postData = child.getAttribute("data-bluesky-post"); 401 - if (postData) { 402 - rep.mutate.assertFact([ 403 - { 404 - entity: entityID, 405 - attribute: "block/type", 406 - data: { type: "block-type-union", value: "bluesky-post" }, 407 - }, 408 - { 409 - entity: entityID, 410 - attribute: "block/bluesky-post", 411 - data: { type: "bluesky-post", value: JSON.parse(postData) }, 412 - }, 413 - ]); 414 - } 415 - } 416 - 417 399 if (child.tagName === "DIV" && child.getAttribute("data-entityid")) { 418 400 let oldEntityID = child.getAttribute("data-entityid") as string; 419 401 let factsData = child.getAttribute("data-facts"); ··· 611 593 "HR", 612 594 ].includes(elementNode.tagName) || 613 595 elementNode.getAttribute("data-entityid") || 614 - elementNode.getAttribute("data-tex") || 615 - elementNode.getAttribute("data-bluesky-post") 596 + elementNode.getAttribute("data-tex") 616 597 ) { 617 598 htmlBlocks.push(elementNode); 618 599 } else {
+5 -33
components/Canvas.tsx
··· 19 19 import { Separator } from "./Layout"; 20 20 import { CommentTiny } from "./Icons/CommentTiny"; 21 21 import { QuoteTiny } from "./Icons/QuoteTiny"; 22 - import { AddTags, PublicationMetadata } from "./Pages/PublicationMetadata"; 22 + import { PublicationMetadata } from "./Pages/PublicationMetadata"; 23 23 import { useLeafletPublicationData } from "./PageSWRDataProvider"; 24 24 import { useHandleCanvasDrop } from "./Blocks/useHandleCanvasDrop"; 25 25 import { useBlockMouseHandlers } from "./Blocks/useBlockMouseHandlers"; 26 - import { RecommendTinyEmpty } from "./Icons/RecommendTiny"; 27 - import { useSubscribe } from "src/replicache/useSubscribe"; 28 - import { mergePreferences } from "src/utils/mergePreferences"; 29 26 30 27 export function Canvas(props: { 31 28 entityID: string; ··· 166 163 167 164 const CanvasMetadata = (props: { isSubpage: boolean | undefined }) => { 168 165 let { data: pub, normalizedPublication } = useLeafletPublicationData(); 169 - let { rep } = useReplicache(); 170 - let postPreferences = useSubscribe(rep, (tx) => 171 - tx.get<{ 172 - showComments?: boolean; 173 - showMentions?: boolean; 174 - showRecommends?: boolean; 175 - } | null>("post_preferences"), 176 - ); 177 166 if (!pub || !pub.publications) return null; 178 167 179 168 if (!normalizedPublication) return null; 180 - let merged = mergePreferences( 181 - postPreferences || undefined, 182 - normalizedPublication.preferences, 183 - ); 184 - let showComments = merged.showComments !== false; 185 - let showMentions = merged.showMentions !== false; 186 - let showRecommends = merged.showRecommends !== false; 169 + let showComments = normalizedPublication.preferences?.showComments !== false; 170 + let showMentions = normalizedPublication.preferences?.showMentions !== false; 187 171 188 172 return ( 189 173 <div className="flex flex-row gap-3 items-center absolute top-6 right-3 sm:top-4 sm:right-4 bg-bg-page border-border-light rounded-md px-2 py-1 h-fit z-20"> 190 - {showRecommends && ( 191 - <div className="flex gap-1 text-tertiary items-center"> 192 - <RecommendTinyEmpty className="text-border" /> โ€” 193 - </div> 194 - )} 195 174 {showComments && ( 196 175 <div className="flex gap-1 text-tertiary items-center"> 197 176 <CommentTiny className="text-border" /> โ€” 198 177 </div> 199 178 )} 200 - {showMentions && ( 179 + {showComments && ( 201 180 <div className="flex gap-1 text-tertiary items-center"> 202 181 <QuoteTiny className="text-border" /> โ€” 203 182 </div> 204 183 )} 205 184 206 - {showMentions !== false || 207 - showComments !== false || 208 - showRecommends === false ? ( 209 - <Separator classname="h-4!" /> 210 - ) : null} 211 - <AddTags /> 212 - 213 185 {!props.isSubpage && ( 214 186 <> 215 187 <Separator classname="h-5" /> ··· 219 191 className="flex flex-col gap-2 p-0! max-w-sm w-[1000px]" 220 192 trigger={<InfoSmall />} 221 193 > 222 - <PublicationMetadata noInteractions /> 194 + <PublicationMetadata /> 223 195 </Popover> 224 196 </> 225 197 )}
+19
components/Icons/LeafletPro.tsx
··· 1 + import { Props } from "./Props"; 2 + 3 + export const LeafletPro = (props: Props) => { 4 + return ( 5 + <svg 6 + width="24" 7 + height="24" 8 + viewBox="0 0 24 24" 9 + fill="none" 10 + xmlns="http://www.w3.org/2000/svg" 11 + {...props} 12 + > 13 + <path 14 + d="M18.7746 14.4153C18.9173 13.0548 19.8101 13.0677 19.9572 14.4153C20.1043 15.7627 20.5438 16.6149 20.7453 16.8352C20.9468 17.0556 21.3114 17.3121 22.55 17.5921C23.7885 17.8721 23.7581 18.6935 22.55 18.8069C21.3418 18.9203 21.0958 19.2801 20.7453 19.6634C20.3949 20.0467 20.1218 20.841 19.9611 22.1653C19.8004 23.4897 18.9308 23.4787 18.7785 22.1653C18.6262 20.8518 18.0988 19.8781 17.9025 19.6634C17.7062 19.4487 17.0699 18.9126 16.0138 18.8069C14.9583 18.7009 14.9723 17.7682 16.0148 17.5921C17.0571 17.4158 17.5148 17.1033 17.7599 16.8352C18.0052 16.567 18.6318 15.7753 18.7746 14.4153ZM17.633 4.65261C18.2545 4.6359 18.2445 4.90445 18.2345 5.17605C18.2282 5.34951 18.2219 5.52489 18.38 5.6282C18.536 5.73003 18.9291 5.69449 19.38 5.65359C20.1026 5.58804 20.9756 5.50855 21.2687 5.95925C21.5262 6.35603 21.1984 6.75118 20.8839 7.12917C20.6178 7.44909 20.3617 7.75719 20.4787 8.04519C20.5743 8.28028 20.7705 8.4134 20.963 8.54323C21.278 8.75563 21.5826 8.96083 21.4191 9.59695C21.2462 10.2694 20.2065 10.4516 19.2316 10.6223C18.4149 10.7654 17.6432 10.9003 17.466 11.3089C17.3549 11.5652 17.5398 11.6849 17.7462 11.8186C17.9928 11.9783 18.2711 12.1588 18.1154 12.6175C17.7505 13.6907 16.5315 13.6907 15.3879 13.6907C14.4064 13.6907 13.4797 13.691 13.1955 14.3694C13.0315 14.7611 13.2797 14.8307 13.5519 14.9065C13.8852 14.9994 14.2551 15.1028 13.9494 15.8196C13.3634 17.1923 11.177 17.2049 9.17594 17.2161C8.4422 17.2202 7.73292 17.2243 7.13687 17.2952C7.069 17.3034 7.0084 17.3423 6.97281 17.4007C6.42436 18.3006 6.03616 19.2911 5.62125 20.3469C5.53013 20.5788 5.43753 20.8141 5.34195 21.052C5.21548 21.3665 4.8577 21.5191 4.54312 21.3928C4.22842 21.2664 4.07592 20.9087 4.2023 20.594C4.67305 19.422 5.2159 18.3441 5.80973 17.3548C6.77895 15.5699 8.68971 13.3798 10.7101 11.7346C12.5078 10.238 14.2248 9.11793 16.2228 8.26784C16.4728 8.16148 16.3987 7.86852 16.1379 7.94363C14.3657 8.45574 12.7987 9.4717 11.175 10.4954C9.40901 11.6087 7.92945 13.1146 7.01871 14.2093C6.85813 14.4023 6.50624 14.2433 6.5568 13.9973C6.80973 12.767 7.06835 11.6016 7.37809 10.7522L7.28238 10.7639C6.44178 10.8798 6.2203 11.0676 5.92105 11.3948L5.79605 11.554C5.51348 11.9708 5.29182 12.7065 5.15445 13.8382C5.0073 15.0507 4.25168 15.1172 4.03531 14.0637L4.00016 13.8382C3.8514 12.5557 3.33638 11.6044 3.14469 11.3948C2.97667 11.2113 2.48011 10.9103 1.66812 10.7688L1.30094 10.719C0.334043 10.6222 0.28533 9.81565 1.12223 9.57351L1.30094 9.53249C2.19153 9.38197 2.64469 9.16494 2.90445 8.93777L3.00504 8.84011C3.22962 8.59435 3.78116 7.71784 3.96402 6.51491L3.99625 6.2698C4.13585 4.94235 5.00674 4.95483 5.15055 6.2698C5.29411 7.58551 5.72422 8.62466 5.92105 8.84011C6.11788 9.05521 6.47376 9.25917 7.68277 9.53249C7.7981 9.55857 7.90142 9.59161 7.9943 9.62624C8.53604 9.05675 8.72654 9.22459 8.96695 9.43581C9.16999 9.6142 9.40867 9.82341 9.92203 9.6448C10.2576 9.52781 10.6911 8.76684 11.175 7.91823C11.8825 6.67737 12.6974 5.2484 13.4699 5.36745C14.4017 5.51106 14.3441 6.02636 14.3 6.42312C14.269 6.70142 14.2454 6.92081 14.5734 6.91238C14.9073 6.90341 15.2324 6.51136 15.6095 6.05593C16.1307 5.42651 16.7515 4.67636 17.633 4.65261ZM4.53238 8.17214C4.26911 8.81037 3.95672 9.28141 3.74332 9.51491C3.53024 9.74791 3.26162 9.93768 2.92594 10.096C3.26002 10.2368 3.56731 10.4174 3.79215 10.6282L3.88297 10.72L3.965 10.8176C4.15719 11.0641 4.3536 11.4381 4.51578 11.8401C4.52382 11.8601 4.53025 11.8812 4.53824 11.9016C4.68793 11.4542 4.89034 11.0398 5.18277 10.72C5.37157 10.5136 5.61429 10.266 6.01383 10.0764C5.65755 9.92268 5.39363 9.74547 5.18277 9.51491C5.02724 9.3448 4.91159 9.12468 4.83414 8.9612C4.74357 8.76996 4.65178 8.53982 4.56656 8.28347C4.55456 8.24733 4.54426 8.20969 4.53238 8.17214ZM6.55289 1.39773C6.63053 0.657978 7.11646 0.664932 7.19644 1.39773C7.27641 2.1306 7.51556 2.70953 7.62516 2.82937C7.73481 2.94909 7.93362 3.06297 8.6066 3.21511C9.27969 3.36744 9.26321 3.8135 8.6066 3.87527C7.94957 3.93694 7.81576 4.0438 7.62516 4.25222C7.43459 4.46067 7.28579 4.89241 7.1984 5.61257C7.11099 6.33285 6.63765 6.32689 6.55484 5.61257C6.4721 4.89889 6.18624 4.36957 6.07926 4.25222C5.97249 4.13547 5.62622 3.93275 5.05191 3.87527C4.47787 3.8177 4.4852 3.31103 5.05191 3.21511C5.61867 3.11933 5.86777 2.97509 6.00113 2.82937C6.13445 2.68358 6.47528 2.13758 6.55289 1.39773Z" 15 + fill="currentColor" 16 + /> 17 + </svg> 18 + ); 19 + };
-37
components/Icons/RecommendTiny.tsx
··· 1 - import { Props } from "./Props"; 2 - 3 - export const RecommendTinyFilled = (props: Props) => { 4 - return ( 5 - <svg 6 - width="16" 7 - height="16" 8 - viewBox="0 0 16 16" 9 - fill="none" 10 - xmlns="http://www.w3.org/2000/svg" 11 - {...props} 12 - > 13 - <path 14 - d="M13.8218 8.85542C13.9838 8.63176 14.2964 8.58118 14.5201 8.74312C14.7433 8.90516 14.7932 9.21786 14.6314 9.44136C12.9671 11.7399 10.7811 13.1142 9.07472 14.0947C8.83547 14.2321 8.52981 14.1491 8.3921 13.9101C8.25463 13.6707 8.33728 13.365 8.57667 13.2275C10.2589 12.2608 12.2881 10.9736 13.8218 8.85542ZM9.09327 2.90525C10.0113 2.2003 11.4161 2.21431 12.2886 2.61521C13.0365 2.95905 13.6929 3.5946 14.0044 4.62106C14.2614 5.46809 14.2169 6.28576 14.0044 7.17867C13.4531 9.49467 10.1475 11.7776 8.22413 12.8828C8.15152 12.9245 8.05431 12.9453 7.97315 12.9453C7.89219 12.9453 7.80343 12.9243 7.73096 12.8828C5.80749 11.7776 2.50174 9.49385 1.95065 7.1777C1.7383 6.28491 1.69376 5.46798 1.95065 4.62106C2.26221 3.59471 2.91764 2.95906 3.66551 2.61521C4.53812 2.21415 5.94374 2.19992 6.86181 2.90525C7.4145 3.32999 7.72613 3.72603 7.97315 4.14939C8.22018 3.72604 8.5406 3.32998 9.09327 2.90525ZM4.55418 3.84079C4.44015 3.58958 4.14441 3.47805 3.89305 3.59177C2.93793 4.0246 2.4787 5.35564 2.85105 6.64059C2.9282 6.90532 3.20525 7.05713 3.47019 6.98043C3.73523 6.9035 3.88869 6.62638 3.81199 6.36129C3.52801 5.38087 3.94973 4.66317 4.30516 4.50192C4.55654 4.38789 4.6681 4.09224 4.55418 3.84079Z" 15 - fill="currentColor" 16 - /> 17 - </svg> 18 - ); 19 - }; 20 - 21 - export const RecommendTinyEmpty = (props: Props) => { 22 - return ( 23 - <svg 24 - width="16" 25 - height="16" 26 - viewBox="0 0 16 16" 27 - fill="none" 28 - xmlns="http://www.w3.org/2000/svg" 29 - {...props} 30 - > 31 - <path 32 - d="M13.8215 8.85505C13.9834 8.63149 14.2961 8.58084 14.5197 8.74275C14.7432 8.90468 14.7928 9.21739 14.631 9.44099C12.9668 11.7395 10.7808 13.1138 9.0744 14.0943C8.83501 14.2318 8.52937 14.1491 8.39178 13.9097C8.25431 13.6703 8.33696 13.3647 8.57635 13.2271C10.2586 12.2605 12.2878 10.9733 13.8215 8.85505ZM4.12127 2.44392C5.05035 2.20462 6.17272 2.3143 7.04412 3.04744C7.33889 3.29547 7.62399 3.64884 7.85369 3.96833C7.89451 4.02512 7.93345 4.08237 7.97186 4.13826C8.22436 3.76381 8.53885 3.3457 8.86248 3.06501C9.80388 2.24888 11.1891 2.16939 12.1564 2.56501C12.9693 2.89763 13.663 3.49593 14.0002 4.60701C14.267 5.48669 14.2598 6.26139 14.0461 7.15974C13.7527 8.39225 12.7396 9.53682 11.6691 10.4703C10.5802 11.4198 9.3429 12.2265 8.47772 12.7681C8.47247 12.7714 8.46646 12.7748 8.46111 12.7779C8.43136 12.795 8.369 12.8315 8.30096 12.8619C8.2405 12.8889 8.11991 12.937 7.97576 12.9371C7.82229 12.9372 7.7007 12.8832 7.63885 12.8521C7.6045 12.8349 7.57372 12.8176 7.55291 12.8052C7.52605 12.7893 7.52018 12.7855 7.50701 12.7779C7.50235 12.7752 7.49792 12.7719 7.49334 12.7691C6.59506 12.2129 5.35778 11.3987 4.27654 10.4439C3.21273 9.50447 2.21958 8.35999 1.92693 7.13044C1.71321 6.23218 1.70502 5.4352 1.97186 4.55525C2.31285 3.43128 3.22341 2.67532 4.12127 2.44392ZM6.40057 3.81306C5.82954 3.33259 5.06002 3.23404 4.37029 3.41169C3.79433 3.56026 3.16381 4.07131 2.92889 4.84529C2.72085 5.53135 2.72051 6.14631 2.89959 6.899C3.11654 7.81042 3.90364 8.77988 4.93865 9.69392C5.94258 10.5805 7.10507 11.3507 7.98358 11.8961C8.83657 11.3611 9.99989 10.5988 11.0119 9.71638C12.0571 8.8049 12.8571 7.83679 13.0734 6.9283C13.2523 6.17606 13.2512 5.58411 13.0431 4.89802C12.8044 4.11102 12.3496 3.72381 11.7775 3.48982C11.1013 3.21328 10.1298 3.29025 9.51776 3.82087C9.10331 4.18037 8.63998 4.9218 8.40545 5.3238C8.3158 5.4772 8.1515 5.57185 7.97381 5.57185C7.79617 5.57171 7.63172 5.47723 7.54217 5.3238C7.43363 5.13777 7.25216 4.84479 7.04119 4.55134C6.82572 4.25167 6.5988 3.97993 6.40057 3.81306Z" 33 - fill="currentColor" 34 - /> 35 - </svg> 36 - ); 37 - };
+15 -21
components/InteractionsPreview.tsx
··· 7 7 import { Popover } from "./Popover"; 8 8 import { TagTiny } from "./Icons/TagTiny"; 9 9 import { SpeedyLink } from "./SpeedyLink"; 10 - import { RecommendButton } from "./RecommendButton"; 11 10 12 11 export const InteractionPreview = (props: { 13 12 quotesCount: number; 14 13 commentsCount: number; 15 - recommendsCount: number; 16 - documentUri: string; 17 14 tags?: string[]; 18 15 postUrl: string; 19 16 showComments: boolean; 20 17 showMentions: boolean; 21 - showRecommends: boolean; 22 18 23 19 share?: boolean; 24 20 }) => { 25 21 let smoker = useSmoker(); 26 22 let interactionsAvailable = 27 23 (props.quotesCount > 0 && props.showMentions) || 28 - (props.showComments !== false && props.commentsCount > 0) || 29 - (props.showRecommends !== false && props.recommendsCount > 0); 24 + (props.showComments !== false && props.commentsCount > 0); 30 25 31 26 const tagsCount = props.tags?.length || 0; 32 27 33 28 return ( 34 - <div className={`flex gap-2 text-tertiary text-sm items-center`}> 35 - {props.showRecommends === false ? null : ( 36 - <RecommendButton 37 - documentUri={props.documentUri} 38 - recommendsCount={props.recommendsCount} 39 - /> 29 + <div 30 + className={`flex gap-2 text-tertiary text-sm items-center self-start`} 31 + > 32 + {tagsCount === 0 ? null : ( 33 + <> 34 + <TagPopover tags={props.tags!} /> 35 + {interactionsAvailable || props.share ? ( 36 + <Separator classname="h-4!" /> 37 + ) : null} 38 + </> 40 39 )} 41 40 42 41 {!props.showMentions || props.quotesCount === 0 ? null : ( ··· 57 56 <CommentTiny /> {props.commentsCount} 58 57 </SpeedyLink> 59 58 )} 60 - {tagsCount === 0 ? null : ( 61 - <> 62 - {interactionsAvailable ? <Separator classname="h-4!" /> : null} 63 - <TagPopover tags={props.tags!} /> 64 - </> 65 - )} 59 + {interactionsAvailable && props.share ? ( 60 + <Separator classname="h-4! !min-h-0" /> 61 + ) : null} 66 62 {props.share && ( 67 63 <> 68 - <Separator classname="h-4!" /> 69 - 70 64 <button 71 65 id={`copy-post-link-${props.postUrl}`} 72 66 className="flex gap-1 items-center hover:text-accent-contrast relative" ··· 77 71 let mouseY = e.clientY; 78 72 79 73 if (!props.postUrl) return; 80 - navigator.clipboard.writeText(props.postUrl); 74 + navigator.clipboard.writeText(`leaflet.pub${props.postUrl}`); 81 75 82 76 smoker({ 83 77 text: <strong>Copied Link!</strong>,
+11 -5
components/Modal.tsx
··· 1 1 import * as Dialog from "@radix-ui/react-dialog"; 2 2 import React from "react"; 3 + import { CloseTiny } from "./Icons/CloseTiny"; 3 4 4 5 export const Modal = ({ 5 6 className, ··· 22 23 <Dialog.Root open={open} onOpenChange={onOpenChange}> 23 24 <Dialog.Trigger asChild={asChild}>{trigger}</Dialog.Trigger> 24 25 <Dialog.Portal> 25 - <Dialog.Overlay className="fixed inset-0 bg-primary data-[state=open]:animate-overlayShow opacity-60" /> 26 + <Dialog.Overlay className="fixed z-10 inset-0 bg-primary data-[state=open]:animate-overlayShow opacity-60" /> 26 27 <Dialog.Content 27 28 className={` 28 29 z-20 fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 29 - overflow-y-scroll no-scrollbar w-max max-w-screen h-fit max-h-screen p-3 30 + overflow-y-scroll no-scrollbar w-max max-w-screen h-fit max-h-screen p-3 flex flex-col 31 + 30 32 `} 31 33 > 34 + <Dialog.Close className="bg-bg-page rounded-full -mb-3 mr-2 z-10 w-fit p-1 place-self-end border border-border-light text-tertiary"> 35 + <CloseTiny /> 36 + </Dialog.Close> 32 37 <div 33 38 className={` 34 39 opaque-container p-3 35 - flex flex-col gap-1 40 + flex flex-col gap-1 rounded-lg! 36 41 ${className}`} 37 42 > 38 - {title && ( 43 + {title ? ( 39 44 <Dialog.Title> 40 45 <h3>{title}</h3> 41 46 </Dialog.Title> 47 + ) : ( 48 + <Dialog.Title /> 42 49 )} 43 50 <Dialog.Description>{children}</Dialog.Description> 44 51 </div> 45 - <Dialog.Close /> 46 52 </Dialog.Content> 47 53 </Dialog.Portal> 48 54 </Dialog.Root>
+7 -21
components/PageSWRDataProvider.tsx
··· 8 8 import type { GetLeafletDataReturnType } from "app/api/rpc/[command]/get_leaflet_data"; 9 9 import { createContext, useContext, useMemo } from "react"; 10 10 import { getPublicationMetadataFromLeafletData } from "src/utils/getPublicationMetadataFromLeafletData"; 11 - import { getPublicationURL, getDocumentURL } from "app/lish/createPub/getPublicationURL"; 11 + import { getPublicationURL } from "app/lish/createPub/getPublicationURL"; 12 12 import { AtUri } from "@atproto/syntax"; 13 13 import { 14 14 normalizeDocumentRecord, ··· 119 119 // Compute the full post URL for sharing 120 120 let postShareLink: string | undefined; 121 121 if (publishedInPublication?.publications && publishedInPublication.documents) { 122 - const normalizedDoc = normalizeDocumentRecord( 123 - publishedInPublication.documents.data, 124 - publishedInPublication.documents.uri, 125 - ); 126 - if (normalizedDoc) { 127 - postShareLink = getDocumentURL( 128 - normalizedDoc, 129 - publishedInPublication.documents.uri, 130 - publishedInPublication.publications, 131 - ); 132 - } 122 + // Published in a publication - use publication URL + document rkey 123 + const docUri = new AtUri(publishedInPublication.documents.uri); 124 + postShareLink = `${getPublicationURL(publishedInPublication.publications)}/${docUri.rkey}`; 133 125 } else if (publishedStandalone?.document) { 134 - const normalizedDoc = publishedStandalone.documents 135 - ? normalizeDocumentRecord(publishedStandalone.documents.data, publishedStandalone.document) 136 - : null; 137 - if (normalizedDoc) { 138 - postShareLink = getDocumentURL(normalizedDoc, publishedStandalone.document); 139 - } else { 140 - const docUri = new AtUri(publishedStandalone.document); 141 - postShareLink = `/p/${docUri.host}/${docUri.rkey}`; 142 - } 126 + // Standalone published post - use /p/{did}/{rkey} format 127 + const docUri = new AtUri(publishedStandalone.document); 128 + postShareLink = `/p/${docUri.host}/${docUri.rkey}`; 143 129 } 144 130 145 131 return {
+25 -54
components/Pages/PublicationMetadata.tsx
··· 20 20 import { useIdentityData } from "components/IdentityProvider"; 21 21 import { PostHeaderLayout } from "app/lish/[did]/[publication]/[rkey]/PostHeader/PostHeader"; 22 22 import { Backdater } from "./Backdater"; 23 - import { RecommendTinyEmpty } from "components/Icons/RecommendTiny"; 24 - import { mergePreferences } from "src/utils/mergePreferences"; 25 23 26 - export const PublicationMetadata = (props: { noInteractions?: boolean }) => { 24 + export const PublicationMetadata = () => { 27 25 let { rep } = useReplicache(); 28 - let { 29 - data: pub, 30 - normalizedDocument, 31 - normalizedPublication, 32 - } = useLeafletPublicationData(); 26 + let { data: pub, normalizedDocument, normalizedPublication } = useLeafletPublicationData(); 33 27 let { identity } = useIdentityData(); 34 28 let title = useSubscribe(rep, (tx) => tx.get<string>("publication_title")); 35 29 let description = useSubscribe(rep, (tx) => 36 30 tx.get<string>("publication_description"), 37 - ); 38 - let postPreferences = useSubscribe(rep, (tx) => 39 - tx.get<{ 40 - showComments?: boolean; 41 - showMentions?: boolean; 42 - showRecommends?: boolean; 43 - } | null>("post_preferences"), 44 - ); 45 - let merged = mergePreferences( 46 - postPreferences || undefined, 47 - normalizedPublication?.preferences, 48 31 ); 49 32 let publishedAt = normalizedDocument?.publishedAt; 50 33 ··· 131 114 ) : ( 132 115 <p>Draft</p> 133 116 )} 134 - {!props.noInteractions && ( 135 - <div className="flex gap-2 text-border items-center"> 136 - {merged.showRecommends !== false && ( 137 - <div className="flex gap-1 items-center"> 138 - <RecommendTinyEmpty />โ€” 139 - </div> 140 - )} 141 - 142 - {merged.showMentions !== false && ( 143 - <div className="flex gap-1 items-center"> 144 - <QuoteTiny />โ€” 145 - </div> 146 - )} 147 - {merged.showComments !== false && ( 148 - <div className="flex gap-1 items-center"> 149 - <CommentTiny />โ€” 150 - </div> 151 - )} 152 - {tags && ( 153 - <> 154 - {merged.showRecommends !== false || 155 - merged.showMentions !== false || 156 - merged.showComments !== false ? ( 157 - <Separator classname="h-4!" /> 158 - ) : null} 159 - <AddTags /> 160 - </> 161 - )} 162 - </div> 163 - )} 117 + <div className="flex gap-2 text-border items-center"> 118 + {tags && ( 119 + <> 120 + <AddTags /> 121 + {normalizedPublication?.preferences?.showMentions !== false || 122 + normalizedPublication?.preferences?.showComments !== false ? ( 123 + <Separator classname="h-4!" /> 124 + ) : null} 125 + </> 126 + )} 127 + {normalizedPublication?.preferences?.showMentions !== false && ( 128 + <div className="flex gap-1 items-center"> 129 + <QuoteTiny />โ€” 130 + </div> 131 + )} 132 + {normalizedPublication?.preferences?.showComments !== false && ( 133 + <div className="flex gap-1 items-center"> 134 + <CommentTiny />โ€” 135 + </div> 136 + )} 137 + </div> 164 138 </> 165 139 } 166 140 /> ··· 264 238 ); 265 239 }; 266 240 267 - export const AddTags = () => { 241 + const AddTags = () => { 268 242 let { data: pub, normalizedDocument } = useLeafletPublicationData(); 269 243 let { rep } = useReplicache(); 270 244 ··· 277 251 let tags: string[] = []; 278 252 if (Array.isArray(replicacheTags)) { 279 253 tags = replicacheTags; 280 - } else if ( 281 - normalizedDocument?.tags && 282 - Array.isArray(normalizedDocument.tags) 283 - ) { 254 + } else if (normalizedDocument?.tags && Array.isArray(normalizedDocument.tags)) { 284 255 tags = normalizedDocument.tags as string[]; 285 256 } 286 257
+7 -15
components/PostListing.tsx
··· 17 17 import Link from "next/link"; 18 18 import { InteractionPreview } from "./InteractionsPreview"; 19 19 import { useLocalizedDate } from "src/hooks/useLocalizedDate"; 20 - import { mergePreferences } from "src/utils/mergePreferences"; 21 - import { getDocumentURL } from "app/lish/createPub/getPublicationURL"; 22 20 23 21 export const PostListing = (props: Post) => { 24 22 let pubRecord = props.publication?.pubRecord as ··· 50 48 ? pubRecord?.theme?.showPageBackground 51 49 : postRecord.theme?.showPageBackground ?? true; 52 50 53 - let mergedPrefs = mergePreferences(postRecord?.preferences, pubRecord?.preferences); 54 - 55 51 let quotes = props.documents.document_mentions_in_bsky?.[0]?.count || 0; 56 52 let comments = 57 - mergedPrefs.showComments === false 53 + pubRecord?.preferences?.showComments === false 58 54 ? 0 59 55 : props.documents.comments_on_documents?.[0]?.count || 0; 60 - let recommends = props.documents.recommends_on_documents?.[0]?.count || 0; 61 56 let tags = (postRecord?.tags as string[] | undefined) || []; 62 57 63 58 // For standalone posts, link directly to the document 64 - let postHref = getDocumentURL(postRecord, props.documents.uri, pubRecord); 59 + let postHref = props.publication 60 + ? `${props.publication.href}/${postUri.rkey}` 61 + : `/p/${postUri.host}/${postUri.rkey}`; 65 62 66 63 return ( 67 64 <BaseThemeProvider {...theme} local> ··· 91 88 > 92 89 <h3 className="text-primary truncate">{postRecord.title}</h3> 93 90 94 - <p className="text-secondary italic line-clamp-3"> 95 - {postRecord.description} 96 - </p> 91 + <p className="text-secondary italic">{postRecord.description}</p> 97 92 <div className="flex flex-col-reverse md:flex-row md gap-2 text-sm text-tertiary items-center justify-start pt-1.5 md:pt-3 w-full"> 98 93 {props.publication && pubRecord && ( 99 94 <PubInfo ··· 108 103 postUrl={postHref} 109 104 quotesCount={quotes} 110 105 commentsCount={comments} 111 - recommendsCount={recommends} 112 - documentUri={props.documents.uri} 113 106 tags={tags} 114 - showComments={mergedPrefs.showComments !== false} 115 - showMentions={mergedPrefs.showMentions !== false} 116 - showRecommends={mergedPrefs.showRecommends !== false} 107 + showComments={pubRecord?.preferences?.showComments !== false} 108 + showMentions={pubRecord?.preferences?.showMentions !== false} 117 109 share 118 110 /> 119 111 </div>
-96
components/PostSettings.tsx
··· 1 - "use client"; 2 - 3 - import { ActionButton } from "components/ActionBar/ActionButton"; 4 - import { SettingsSmall } from "components/Icons/SettingsSmall"; 5 - import { Toggle } from "components/Toggle"; 6 - import { Popover } from "components/Popover"; 7 - import { useLeafletPublicationData } from "components/PageSWRDataProvider"; 8 - import { useReplicache } from "src/replicache"; 9 - import { useSubscribe } from "src/replicache/useSubscribe"; 10 - 11 - type PostPreferences = { 12 - showComments?: boolean; 13 - showMentions?: boolean; 14 - showRecommends?: boolean; 15 - }; 16 - 17 - export function PostSettings() { 18 - let { data: pub, normalizedPublication } = useLeafletPublicationData(); 19 - let { rep } = useReplicache(); 20 - 21 - let postPreferences = useSubscribe(rep, (tx) => 22 - tx.get<PostPreferences | null>("post_preferences"), 23 - ); 24 - 25 - if (!pub || !pub.publications) return null; 26 - 27 - let pubPrefs = normalizedPublication?.preferences; 28 - 29 - let showComments = 30 - postPreferences?.showComments ?? pubPrefs?.showComments ?? true; 31 - let showMentions = 32 - postPreferences?.showMentions ?? pubPrefs?.showMentions ?? true; 33 - let showRecommends = 34 - postPreferences?.showRecommends ?? pubPrefs?.showRecommends ?? true; 35 - 36 - const updatePreference = ( 37 - field: keyof PostPreferences, 38 - value: boolean, 39 - ) => { 40 - let current: PostPreferences = postPreferences || {}; 41 - rep?.mutate.updatePublicationDraft({ 42 - preferences: { ...current, [field]: value }, 43 - }); 44 - }; 45 - 46 - return ( 47 - <Popover 48 - asChild 49 - side="right" 50 - align="start" 51 - className="max-w-xs w-[1000px]" 52 - trigger={ 53 - <ActionButton 54 - icon={<SettingsSmall />} 55 - label="Settings" 56 - /> 57 - } 58 - > 59 - <div className="text-primary flex flex-col"> 60 - <div className="flex justify-between font-bold text-secondary bg-border-light -mx-3 -mt-2 px-3 py-2 mb-1"> 61 - This Post Settings 62 - </div> 63 - <div className="flex flex-col gap-2"> 64 - <Toggle 65 - toggle={showComments} 66 - onToggle={() => updatePreference("showComments", !showComments)} 67 - > 68 - <div className="font-bold">Show Comments</div> 69 - </Toggle> 70 - <Toggle 71 - toggle={showMentions} 72 - onToggle={() => updatePreference("showMentions", !showMentions)} 73 - > 74 - <div className="flex flex-col justify-start"> 75 - <div className="font-bold">Show Mentions</div> 76 - <div className="text-tertiary text-sm leading-tight"> 77 - Display a list of Bluesky mentions about your post 78 - </div> 79 - </div> 80 - </Toggle> 81 - <Toggle 82 - toggle={showRecommends} 83 - onToggle={() => updatePreference("showRecommends", !showRecommends)} 84 - > 85 - <div className="flex flex-col justify-start"> 86 - <div className="font-bold">Show Recommends</div> 87 - <div className="text-tertiary text-sm leading-tight"> 88 - Allow readers to recommend/like your post 89 - </div> 90 - </div> 91 - </Toggle> 92 - </div> 93 - </div> 94 - </Popover> 95 - ); 96 - }
-173
components/RecommendButton.tsx
··· 1 - "use client"; 2 - 3 - import { useState } from "react"; 4 - import useSWR, { mutate } from "swr"; 5 - import { create, windowScheduler } from "@yornaath/batshit"; 6 - import { RecommendTinyEmpty, RecommendTinyFilled } from "./Icons/RecommendTiny"; 7 - import { 8 - recommendAction, 9 - unrecommendAction, 10 - } from "app/lish/[did]/[publication]/[rkey]/Interactions/recommendAction"; 11 - import { callRPC } from "app/api/rpc/client"; 12 - import { useSmoker, useToaster } from "./Toast"; 13 - import { OAuthErrorMessage, isOAuthSessionError } from "./OAuthError"; 14 - import { ButtonSecondary } from "./Buttons"; 15 - import { Separator } from "./Layout"; 16 - 17 - // Create a batcher for recommendation checks 18 - // Batches requests made within 10ms window 19 - const recommendationBatcher = create({ 20 - fetcher: async (documentUris: string[]) => { 21 - const response = await callRPC("get_user_recommendations", { 22 - documentUris, 23 - }); 24 - return response.result; 25 - }, 26 - resolver: (results, documentUri) => results[documentUri] ?? false, 27 - scheduler: windowScheduler(10), 28 - }); 29 - 30 - const getRecommendationKey = (documentUri: string) => 31 - `recommendation:${documentUri}`; 32 - 33 - function useUserRecommendation(documentUri: string) { 34 - const { data: hasRecommended, isLoading } = useSWR( 35 - getRecommendationKey(documentUri), 36 - () => recommendationBatcher.fetch(documentUri), 37 - ); 38 - 39 - return { 40 - hasRecommended: hasRecommended ?? false, 41 - isLoading, 42 - }; 43 - } 44 - 45 - function mutateRecommendation(documentUri: string, hasRecommended: boolean) { 46 - mutate(getRecommendationKey(documentUri), hasRecommended, { 47 - revalidate: false, 48 - }); 49 - } 50 - 51 - /** 52 - * RecommendButton that fetches the user's recommendation status asynchronously. 53 - * Uses SWR with batched requests for efficient fetching when many buttons are rendered. 54 - */ 55 - export function RecommendButton(props: { 56 - documentUri: string; 57 - recommendsCount: number; 58 - className?: string; 59 - expanded?: boolean; 60 - }) { 61 - const { hasRecommended, isLoading } = useUserRecommendation( 62 - props.documentUri, 63 - ); 64 - const [count, setCount] = useState(props.recommendsCount); 65 - const [isPending, setIsPending] = useState(false); 66 - const [optimisticRecommended, setOptimisticRecommended] = useState< 67 - boolean | null 68 - >(null); 69 - const toaster = useToaster(); 70 - const smoker = useSmoker(); 71 - 72 - // Use optimistic state if set, otherwise use fetched state 73 - const displayRecommended = 74 - optimisticRecommended !== null ? optimisticRecommended : hasRecommended; 75 - 76 - const handleClick = async (e: React.MouseEvent) => { 77 - if (isPending || isLoading) return; 78 - 79 - const currentlyRecommended = displayRecommended; 80 - setIsPending(true); 81 - setOptimisticRecommended(!currentlyRecommended); 82 - setCount((c) => (currentlyRecommended ? c - 1 : c + 1)); 83 - 84 - if (!currentlyRecommended) { 85 - smoker({ 86 - position: { 87 - x: e.clientX, 88 - y: e.clientY - 16, 89 - }, 90 - text: <div className="text-xs">Recc'd!</div>, 91 - }); 92 - } 93 - 94 - const result = currentlyRecommended 95 - ? await unrecommendAction({ document: props.documentUri }) 96 - : await recommendAction({ document: props.documentUri }); 97 - if (!result.success) { 98 - // Revert optimistic update 99 - setOptimisticRecommended(null); 100 - setCount((c) => (currentlyRecommended ? c + 1 : c - 1)); 101 - setIsPending(false); 102 - 103 - toaster({ 104 - content: isOAuthSessionError(result.error) ? ( 105 - <OAuthErrorMessage error={result.error} /> 106 - ) : ( 107 - "oh no! error!" 108 - ), 109 - type: "error", 110 - }); 111 - return; 112 - } 113 - 114 - // Update the SWR cache to match the new state 115 - mutateRecommendation(props.documentUri, !currentlyRecommended); 116 - setOptimisticRecommended(null); 117 - setIsPending(false); 118 - }; 119 - 120 - if (props.expanded) 121 - return ( 122 - <ButtonSecondary 123 - onClick={(e) => { 124 - e.preventDefault(); 125 - e.stopPropagation(); 126 - handleClick(e); 127 - }} 128 - > 129 - {displayRecommended ? ( 130 - <RecommendTinyFilled className="text-accent-contrast" /> 131 - ) : ( 132 - <RecommendTinyEmpty /> 133 - )} 134 - <div className="flex gap-2 items-center"> 135 - {count > 0 && ( 136 - <> 137 - <span 138 - className={`${displayRecommended && "text-accent-contrast"}`} 139 - > 140 - {count} 141 - </span> 142 - <Separator classname="h-4! text-accent-contrast!" /> 143 - </> 144 - )} 145 - {displayRecommended ? "Recommended!" : "Recommend"} 146 - </div> 147 - </ButtonSecondary> 148 - ); 149 - 150 - return ( 151 - <button 152 - onClick={(e) => { 153 - e.preventDefault(); 154 - e.stopPropagation(); 155 - handleClick(e); 156 - }} 157 - disabled={isPending || isLoading} 158 - className={`recommendButton relative flex gap-1 items-center hover:text-accent-contrast ${props.className || ""}`} 159 - aria-label={displayRecommended ? "Remove recommend" : "Recommend"} 160 - > 161 - {displayRecommended ? ( 162 - <RecommendTinyFilled className="text-accent-contrast" /> 163 - ) : ( 164 - <RecommendTinyEmpty /> 165 - )} 166 - {count > 0 && ( 167 - <span className={`${displayRecommended && "text-accent-contrast"}`}> 168 - {count} 169 - </span> 170 - )} 171 - </button> 172 - ); 173 - }
-1
contexts/DocumentContext.tsx
··· 21 21 | "comments" 22 22 | "mentions" 23 23 | "leafletId" 24 - | "recommendsCount" 25 24 >; 26 25 27 26 const DocumentContext = createContext<DocumentContextValue | null>(null);
+1 -2
drizzle/schema.ts
··· 1 - import { pgTable, pgEnum, text, jsonb, foreignKey, timestamp, boolean, uuid, index, bigint, unique, uniqueIndex, smallint, primaryKey, integer } from "drizzle-orm/pg-core" 1 + import { pgTable, pgEnum, text, jsonb, foreignKey, timestamp, boolean, uuid, index, bigint, unique, uniqueIndex, smallint, primaryKey } from "drizzle-orm/pg-core" 2 2 import { sql } from "drizzle-orm" 3 3 4 4 export const aal_level = pgEnum("aal_level", ['aal1', 'aal2', 'aal3']) ··· 225 225 uri: text("uri").primaryKey().notNull(), 226 226 data: jsonb("data").notNull(), 227 227 indexed_at: timestamp("indexed_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), 228 - bsky_like_count: integer("bsky_like_count").default(0).notNull(), 229 228 }); 230 229 231 230 export const atp_poll_votes = pgTable("atp_poll_votes", {
-1
feeds/index.ts
··· 115 115 ); 116 116 } 117 117 query = query 118 - .eq("indexed", true) 119 118 .or("data->postRef.not.is.null,data->bskyPostRef.not.is.null") 120 119 .order("sort_date", { ascending: false }) 121 120 .order("uri", { ascending: false })
-17
lexicons/api/lexicons.ts
··· 1469 1469 type: 'ref', 1470 1470 ref: 'lex:pub.leaflet.publication#theme', 1471 1471 }, 1472 - preferences: { 1473 - type: 'ref', 1474 - ref: 'lex:pub.leaflet.publication#preferences', 1475 - }, 1476 1472 tags: { 1477 1473 type: 'array', 1478 1474 items: { ··· 1872 1868 type: 'boolean', 1873 1869 default: true, 1874 1870 }, 1875 - showRecommends: { 1876 - type: 'boolean', 1877 - default: true, 1878 - }, 1879 1871 }, 1880 1872 }, 1881 1873 theme: { ··· 2202 2194 maxLength: 5000, 2203 2195 type: 'string', 2204 2196 }, 2205 - preferences: { 2206 - type: 'union', 2207 - refs: ['lex:pub.leaflet.publication#preferences'], 2208 - closed: false, 2209 - }, 2210 2197 updatedAt: { 2211 2198 format: 'datetime', 2212 2199 type: 'string', ··· 2301 2288 }, 2302 2289 showPrevNext: { 2303 2290 default: false, 2304 - type: 'boolean', 2305 - }, 2306 - showRecommends: { 2307 - default: true, 2308 2291 type: 'boolean', 2309 2292 }, 2310 2293 },
-1
lexicons/api/types/pub/leaflet/document.ts
··· 23 23 publication?: string 24 24 author: string 25 25 theme?: PubLeafletPublication.Theme 26 - preferences?: PubLeafletPublication.Preferences 27 26 tags?: string[] 28 27 coverImage?: BlobRef 29 28 pages: (
-1
lexicons/api/types/pub/leaflet/publication.ts
··· 39 39 showComments: boolean 40 40 showMentions: boolean 41 41 showPrevNext: boolean 42 - showRecommends: boolean 43 42 } 44 43 45 44 const hashPreferences = 'preferences'
-1
lexicons/api/types/site/standard/document.ts
··· 28 28 textContent?: string 29 29 theme?: PubLeafletPublication.Theme 30 30 title: string 31 - preferences?: $Typed<PubLeafletPublication.Preferences> | { $type: string } 32 31 updatedAt?: string 33 32 [k: string]: unknown 34 33 }
-1
lexicons/api/types/site/standard/publication.ts
··· 40 40 showComments: boolean 41 41 showMentions: boolean 42 42 showPrevNext: boolean 43 - showRecommends: boolean 44 43 } 45 44 46 45 const hashPreferences = 'preferences'
-4
lexicons/pub/leaflet/document.json
··· 46 46 "type": "ref", 47 47 "ref": "pub.leaflet.publication#theme" 48 48 }, 49 - "preferences": { 50 - "type": "ref", 51 - "ref": "pub.leaflet.publication#preferences" 52 - }, 53 49 "tags": { 54 50 "type": "array", 55 51 "items": {
-4
lexicons/pub/leaflet/publication.json
··· 59 59 "showPrevNext": { 60 60 "type": "boolean", 61 61 "default": true 62 - }, 63 - "showRecommends": { 64 - "type": "boolean", 65 - "default": true 66 62 } 67 63 } 68 64 },
-5
lexicons/site/standard/document.json
··· 57 57 "maxLength": 5000, 58 58 "type": "string" 59 59 }, 60 - "preferences": { 61 - "type": "union", 62 - "refs": ["pub.leaflet.publication#preferences"], 63 - "closed": false 64 - }, 65 60 "updatedAt": { 66 61 "format": "datetime", 67 62 "type": "string"
-4
lexicons/site/standard/publication.json
··· 58 58 "showPrevNext": { 59 59 "default": false, 60 60 "type": "boolean" 61 - }, 62 - "showRecommends": { 63 - "default": true, 64 - "type": "boolean" 65 61 } 66 62 }, 67 63 "type": "object"
-4
lexicons/src/document.ts
··· 23 23 publication: { type: "string", format: "at-uri" }, 24 24 author: { type: "string", format: "at-identifier" }, 25 25 theme: { type: "ref", ref: "pub.leaflet.publication#theme" }, 26 - preferences: { 27 - type: "ref", 28 - ref: "pub.leaflet.publication#preferences", 29 - }, 30 26 tags: { type: "array", items: { type: "string", maxLength: 50 } }, 31 27 coverImage: { 32 28 type: "blob",
+11 -28
lexicons/src/normalize.ts
··· 28 28 export type NormalizedDocument = SiteStandardDocument.Record & { 29 29 // Keep the original theme for components that need leaflet-specific styling 30 30 theme?: PubLeafletPublication.Theme; 31 - preferences?: SiteStandardPublication.Preferences; 32 31 }; 33 32 34 33 // Normalized publication type - uses the generated site.standard.publication type ··· 51 50 * Checks if the record is a pub.leaflet.document 52 51 */ 53 52 export function isLeafletDocument( 54 - record: unknown, 53 + record: unknown 55 54 ): record is PubLeafletDocument.Record { 56 55 if (!record || typeof record !== "object") return false; 57 56 const r = record as Record<string, unknown>; ··· 66 65 * Checks if the record is a site.standard.document 67 66 */ 68 67 export function isStandardDocument( 69 - record: unknown, 68 + record: unknown 70 69 ): record is SiteStandardDocument.Record { 71 70 if (!record || typeof record !== "object") return false; 72 71 const r = record as Record<string, unknown>; ··· 77 76 * Checks if the record is a pub.leaflet.publication 78 77 */ 79 78 export function isLeafletPublication( 80 - record: unknown, 79 + record: unknown 81 80 ): record is PubLeafletPublication.Record { 82 81 if (!record || typeof record !== "object") return false; 83 82 const r = record as Record<string, unknown>; ··· 92 91 * Checks if the record is a site.standard.publication 93 92 */ 94 93 export function isStandardPublication( 95 - record: unknown, 94 + record: unknown 96 95 ): record is SiteStandardPublication.Record { 97 96 if (!record || typeof record !== "object") return false; 98 97 const r = record as Record<string, unknown>; ··· 107 106 | $Typed<PubLeafletThemeColor.Rgba> 108 107 | $Typed<PubLeafletThemeColor.Rgb> 109 108 | { $type: string } 110 - | undefined, 109 + | undefined 111 110 ): { r: number; g: number; b: number } | undefined { 112 111 if (!color || typeof color !== "object") return undefined; 113 112 const c = color as Record<string, unknown>; ··· 125 124 * Converts a pub.leaflet theme to a site.standard.theme.basic format 126 125 */ 127 126 export function leafletThemeToBasicTheme( 128 - theme: PubLeafletPublication.Theme | undefined, 127 + theme: PubLeafletPublication.Theme | undefined 129 128 ): SiteStandardThemeBasic.Main | undefined { 130 129 if (!theme) return undefined; 131 130 132 131 const background = extractRgb(theme.backgroundColor); 133 - const accent = 134 - extractRgb(theme.accentBackground) || extractRgb(theme.primary); 132 + const accent = extractRgb(theme.accentBackground) || extractRgb(theme.primary); 135 133 const accentForeground = extractRgb(theme.accentText); 136 134 137 135 // If we don't have the required colors, return undefined ··· 162 160 * @param uri - Optional document URI, used to extract the rkey for the path field when normalizing pub.leaflet records 163 161 * @returns A normalized document in site.standard format, or null if invalid/unrecognized 164 162 */ 165 - export function normalizeDocument( 166 - record: unknown, 167 - uri?: string, 168 - ): NormalizedDocument | null { 163 + export function normalizeDocument(record: unknown, uri?: string): NormalizedDocument | null { 169 164 if (!record || typeof record !== "object") return null; 170 165 171 166 // Pass through site.standard records directly (theme is already in correct format if present) 172 167 if (isStandardDocument(record)) { 173 - const preferences = record.preferences as 174 - | SiteStandardPublication.Preferences 175 - | undefined; 176 168 return { 177 169 ...record, 178 170 theme: record.theme, 179 - preferences, 180 171 } as NormalizedDocument; 181 172 } 182 173 ··· 203 194 } 204 195 : undefined; 205 196 206 - // Extract preferences if present (available after lexicon rebuild) 207 - const leafletPrefs = (record as Record<string, unknown>) 208 - .preferences as SiteStandardPublication.Preferences | undefined; 209 - 210 197 return { 211 198 $type: "site.standard.document", 212 199 title: record.title, ··· 219 206 bskyPostRef: record.postRef, 220 207 content, 221 208 theme: record.theme, 222 - preferences: leafletPrefs 223 - ? { ...leafletPrefs, $type: "site.standard.publication#preferences" as const } 224 - : undefined, 225 209 }; 226 210 } 227 211 ··· 235 219 * @returns A normalized publication in site.standard format, or null if invalid/unrecognized 236 220 */ 237 221 export function normalizePublication( 238 - record: unknown, 222 + record: unknown 239 223 ): NormalizedPublication | null { 240 224 if (!record || typeof record !== "object") return null; 241 225 ··· 284 268 showComments: record.preferences.showComments, 285 269 showMentions: record.preferences.showMentions, 286 270 showPrevNext: record.preferences.showPrevNext, 287 - showRecommends: record.preferences.showRecommends, 288 271 } 289 272 : undefined; 290 273 ··· 307 290 * Type guard to check if a normalized document has leaflet content 308 291 */ 309 292 export function hasLeafletContent( 310 - doc: NormalizedDocument, 293 + doc: NormalizedDocument 311 294 ): doc is NormalizedDocument & { 312 295 content: $Typed<PubLeafletContent.Main>; 313 296 } { ··· 321 304 * Gets the pages array from a normalized document, handling both formats 322 305 */ 323 306 export function getDocumentPages( 324 - doc: NormalizedDocument, 307 + doc: NormalizedDocument 325 308 ): PubLeafletContent.Main["pages"] | undefined { 326 309 if (!doc.content) return undefined; 327 310
-1
lexicons/src/publication.ts
··· 29 29 showComments: { type: "boolean", default: true }, 30 30 showMentions: { type: "boolean", default: true }, 31 31 showPrevNext: { type: "boolean", default: true }, 32 - showRecommends: { type: "boolean", default: true }, 33 32 }, 34 33 }, 35 34 theme: {
+681 -423
package-lock.json
··· 12 12 "@atproto/api": "^0.16.9", 13 13 "@atproto/common": "^0.4.8", 14 14 "@atproto/identity": "^0.4.6", 15 - "@atproto/lexicon": "^0.6.1", 15 + "@atproto/lexicon": "^0.5.1", 16 16 "@atproto/oauth-client-node": "^0.3.8", 17 17 "@atproto/sync": "^0.1.34", 18 18 "@atproto/syntax": "^0.3.3", 19 + "@atproto/tap": "^0.1.1", 19 20 "@atproto/xrpc": "^0.7.5", 20 21 "@atproto/xrpc-server": "^0.9.5", 21 22 "@hono/node-server": "^1.14.3", ··· 37 38 "@vercel/analytics": "^1.5.0", 38 39 "@vercel/functions": "^2.2.12", 39 40 "@vercel/sdk": "^1.11.4", 40 - "@yornaath/batshit": "^0.14.0", 41 41 "babel-plugin-react-compiler": "^19.1.0-rc.1", 42 42 "base64-js": "^1.5.1", 43 43 "colorjs.io": "^0.5.2", ··· 236 236 } 237 237 }, 238 238 "node_modules/@atproto/api/node_modules/@atproto/syntax": { 239 - "version": "0.4.3", 240 - "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.4.3.tgz", 241 - "integrity": "sha512-YoZUz40YAJr5nPwvCDWgodEOlt5IftZqPJvA0JDWjuZKD8yXddTwSzXSaKQAzGOpuM+/A3uXRtPzJJqlScc+iA==", 242 - "license": "MIT", 243 - "dependencies": { 244 - "tslib": "^2.8.1" 245 - } 239 + "version": "0.4.1", 240 + "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.4.1.tgz", 241 + "integrity": "sha512-CJdImtLAiFO+0z3BWTtxwk6aY5w4t8orHTMVJgkf++QRJWTxPbIFko/0hrkADB7n2EruDxDSeAgfUGehpH6ngw==", 242 + "license": "MIT" 246 243 }, 247 244 "node_modules/@atproto/api/node_modules/multiformats": { 248 245 "version": "9.9.0", ··· 268 265 } 269 266 }, 270 267 "node_modules/@atproto/common-web": { 271 - "version": "0.4.15", 272 - "resolved": "https://registry.npmjs.org/@atproto/common-web/-/common-web-0.4.15.tgz", 273 - "integrity": "sha512-A4l9gyqUNez8CjZp/Trypz/D3WIQsNj8dN05WR6+RoBbvwc9JhWjKPrm+WoVYc/F16RPdXHLkE3BEJlGIyYIiA==", 268 + "version": "0.4.10", 269 + "resolved": "https://registry.npmjs.org/@atproto/common-web/-/common-web-0.4.10.tgz", 270 + "integrity": "sha512-TLDZSgSKzT8ZgOrBrTGK87J1CXve9TEuY6NVVUBRkOMzRRtQzpFb9/ih5WVS/hnaWVvE30CfuyaetRoma+WKNw==", 274 271 "license": "MIT", 275 272 "dependencies": { 276 - "@atproto/lex-data": "^0.0.10", 277 - "@atproto/lex-json": "^0.0.10", 278 - "@atproto/syntax": "^0.4.3", 273 + "@atproto/lex-data": "0.0.6", 274 + "@atproto/lex-json": "0.0.6", 279 275 "zod": "^3.23.8" 280 276 } 281 - }, 282 - "node_modules/@atproto/common-web/node_modules/@atproto/lex-data": { 283 - "version": "0.0.10", 284 - "resolved": "https://registry.npmjs.org/@atproto/lex-data/-/lex-data-0.0.10.tgz", 285 - "integrity": "sha512-FDbcy8VIUVzS9Mi1F8SMxbkL/jOUmRRpqbeM1xB4A0fMxeZJTxf6naAbFt4gYF3quu/+TPJGmio6/7cav05FqQ==", 286 - "license": "MIT", 287 - "dependencies": { 288 - "multiformats": "^9.9.0", 289 - "tslib": "^2.8.1", 290 - "uint8arrays": "3.0.0", 291 - "unicode-segmenter": "^0.14.0" 292 - } 293 - }, 294 - "node_modules/@atproto/common-web/node_modules/@atproto/lex-json": { 295 - "version": "0.0.10", 296 - "resolved": "https://registry.npmjs.org/@atproto/lex-json/-/lex-json-0.0.10.tgz", 297 - "integrity": "sha512-L6MyXU17C5ODMeob8myQ2F3xvgCTvJUtM0ew8qSApnN//iDasB/FDGgd7ty4UVNmx4NQ/rtvz8xV94YpG6kneQ==", 298 - "license": "MIT", 299 - "dependencies": { 300 - "@atproto/lex-data": "^0.0.10", 301 - "tslib": "^2.8.1" 302 - } 303 - }, 304 - "node_modules/@atproto/common-web/node_modules/@atproto/syntax": { 305 - "version": "0.4.3", 306 - "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.4.3.tgz", 307 - "integrity": "sha512-YoZUz40YAJr5nPwvCDWgodEOlt5IftZqPJvA0JDWjuZKD8yXddTwSzXSaKQAzGOpuM+/A3uXRtPzJJqlScc+iA==", 308 - "license": "MIT", 309 - "dependencies": { 310 - "tslib": "^2.8.1" 311 - } 312 - }, 313 - "node_modules/@atproto/common-web/node_modules/multiformats": { 314 - "version": "9.9.0", 315 - "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz", 316 - "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==", 317 - "license": "(Apache-2.0 AND MIT)" 318 277 }, 319 278 "node_modules/@atproto/common/node_modules/multiformats": { 320 279 "version": "9.9.0", ··· 395 354 "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==", 396 355 "license": "(Apache-2.0 AND MIT)" 397 356 }, 357 + "node_modules/@atproto/lex": { 358 + "version": "0.0.9", 359 + "resolved": "https://registry.npmjs.org/@atproto/lex/-/lex-0.0.9.tgz", 360 + "integrity": "sha512-o6gauf1lz0iyzJR0rqSj4VHOrO+Nt8+/iPb0KPojw1ieXk13zOSTSxotAoDzO/dP6y8Ey5jxwuCQGuzab/4XnQ==", 361 + "license": "MIT", 362 + "dependencies": { 363 + "@atproto/lex-builder": "0.0.9", 364 + "@atproto/lex-client": "0.0.7", 365 + "@atproto/lex-data": "0.0.6", 366 + "@atproto/lex-installer": "0.0.9", 367 + "@atproto/lex-json": "0.0.6", 368 + "@atproto/lex-schema": "0.0.7", 369 + "tslib": "^2.8.1", 370 + "yargs": "^17.0.0" 371 + }, 372 + "bin": { 373 + "lex": "bin/lex", 374 + "ts-lex": "bin/lex" 375 + } 376 + }, 377 + "node_modules/@atproto/lex-builder": { 378 + "version": "0.0.9", 379 + "resolved": "https://registry.npmjs.org/@atproto/lex-builder/-/lex-builder-0.0.9.tgz", 380 + "integrity": "sha512-buOFk1JpuW3twI7To7f/67zQQ1NulLHf/oasH/kTOPUAd0dNyeAa13t9eRSVGbwi0BcZYxRxBm0QzPmdLKyuyw==", 381 + "license": "MIT", 382 + "dependencies": { 383 + "@atproto/lex-document": "0.0.8", 384 + "@atproto/lex-schema": "0.0.7", 385 + "prettier": "^3.2.5", 386 + "ts-morph": "^27.0.0", 387 + "tslib": "^2.8.1" 388 + } 389 + }, 390 + "node_modules/@atproto/lex-builder/node_modules/@ts-morph/common": { 391 + "version": "0.28.1", 392 + "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.28.1.tgz", 393 + "integrity": "sha512-W74iWf7ILp1ZKNYXY5qbddNaml7e9Sedv5lvU1V8lftlitkc9Pq1A+jlH23ltDgWYeZFFEqGCD1Ies9hqu3O+g==", 394 + "license": "MIT", 395 + "dependencies": { 396 + "minimatch": "^10.0.1", 397 + "path-browserify": "^1.0.1", 398 + "tinyglobby": "^0.2.14" 399 + } 400 + }, 401 + "node_modules/@atproto/lex-builder/node_modules/minimatch": { 402 + "version": "10.1.1", 403 + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", 404 + "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", 405 + "license": "BlueOak-1.0.0", 406 + "dependencies": { 407 + "@isaacs/brace-expansion": "^5.0.0" 408 + }, 409 + "engines": { 410 + "node": "20 || >=22" 411 + }, 412 + "funding": { 413 + "url": "https://github.com/sponsors/isaacs" 414 + } 415 + }, 416 + "node_modules/@atproto/lex-builder/node_modules/ts-morph": { 417 + "version": "27.0.2", 418 + "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-27.0.2.tgz", 419 + "integrity": "sha512-fhUhgeljcrdZ+9DZND1De1029PrE+cMkIP7ooqkLRTrRLTqcki2AstsyJm0vRNbTbVCNJ0idGlbBrfqc7/nA8w==", 420 + "license": "MIT", 421 + "dependencies": { 422 + "@ts-morph/common": "~0.28.1", 423 + "code-block-writer": "^13.0.3" 424 + } 425 + }, 426 + "node_modules/@atproto/lex-cbor": { 427 + "version": "0.0.6", 428 + "resolved": "https://registry.npmjs.org/@atproto/lex-cbor/-/lex-cbor-0.0.6.tgz", 429 + "integrity": "sha512-lee2T00owDy3I1plRHuURT6f98NIpYZZr2wXa5pJZz5JzefZ+nv8gJ2V70C2f+jmSG+5S9NTIy4uJw94vaHf4A==", 430 + "license": "MIT", 431 + "dependencies": { 432 + "@atproto/lex-data": "0.0.6", 433 + "multiformats": "^9.9.0", 434 + "tslib": "^2.8.1" 435 + } 436 + }, 437 + "node_modules/@atproto/lex-cbor/node_modules/multiformats": { 438 + "version": "9.9.0", 439 + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz", 440 + "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==", 441 + "license": "(Apache-2.0 AND MIT)" 442 + }, 398 443 "node_modules/@atproto/lex-cli": { 399 444 "version": "0.9.5", 400 445 "resolved": "https://registry.npmjs.org/@atproto/lex-cli/-/lex-cli-0.9.5.tgz", ··· 419 464 } 420 465 }, 421 466 "node_modules/@atproto/lex-cli/node_modules/@atproto/syntax": { 422 - "version": "0.4.3", 423 - "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.4.3.tgz", 424 - "integrity": "sha512-YoZUz40YAJr5nPwvCDWgodEOlt5IftZqPJvA0JDWjuZKD8yXddTwSzXSaKQAzGOpuM+/A3uXRtPzJJqlScc+iA==", 467 + "version": "0.4.1", 468 + "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.4.1.tgz", 469 + "integrity": "sha512-CJdImtLAiFO+0z3BWTtxwk6aY5w4t8orHTMVJgkf++QRJWTxPbIFko/0hrkADB7n2EruDxDSeAgfUGehpH6ngw==", 425 470 "dev": true, 471 + "license": "MIT" 472 + }, 473 + "node_modules/@atproto/lex-client": { 474 + "version": "0.0.7", 475 + "resolved": "https://registry.npmjs.org/@atproto/lex-client/-/lex-client-0.0.7.tgz", 476 + "integrity": "sha512-ofUz3yXJ0nN/M9aqqF2ZUL/4D1wWT1P4popCfV3OEDsDrtWofMflYPFz1IWuyPa2e83paaEHRhaw3bZEhgXH1w==", 426 477 "license": "MIT", 427 478 "dependencies": { 479 + "@atproto/lex-data": "0.0.6", 480 + "@atproto/lex-json": "0.0.6", 481 + "@atproto/lex-schema": "0.0.7", 428 482 "tslib": "^2.8.1" 429 483 } 430 484 }, 431 - "node_modules/@atproto/lexicon": { 432 - "version": "0.6.1", 433 - "resolved": "https://registry.npmjs.org/@atproto/lexicon/-/lexicon-0.6.1.tgz", 434 - "integrity": "sha512-/vI1kVlY50Si+5MXpvOucelnYwb0UJ6Qto5mCp+7Q5C+Jtp+SoSykAPVvjVtTnQUH2vrKOFOwpb3C375vSKzXw==", 485 + "node_modules/@atproto/lex-data": { 486 + "version": "0.0.6", 487 + "resolved": "https://registry.npmjs.org/@atproto/lex-data/-/lex-data-0.0.6.tgz", 488 + "integrity": "sha512-MBNB4ghRJQzuXK1zlUPljpPbQcF1LZ5dzxy274KqPt4p3uPuRw0mHjgcCoWzRUNBQC685WMQR4IN9DHtsnG57A==", 435 489 "license": "MIT", 436 490 "dependencies": { 437 - "@atproto/common-web": "^0.4.13", 438 - "@atproto/syntax": "^0.4.3", 439 - "iso-datestring-validator": "^2.2.2", 491 + "@atproto/syntax": "0.4.2", 440 492 "multiformats": "^9.9.0", 493 + "tslib": "^2.8.1", 494 + "uint8arrays": "3.0.0", 495 + "unicode-segmenter": "^0.14.0" 496 + } 497 + }, 498 + "node_modules/@atproto/lex-data/node_modules/@atproto/syntax": { 499 + "version": "0.4.2", 500 + "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.4.2.tgz", 501 + "integrity": "sha512-X9XSRPinBy/0VQ677j8VXlBsYSsUXaiqxWVpGGxJYsAhugdQRb0jqaVKJFtm6RskeNkV6y9xclSUi9UYG/COrA==", 502 + "license": "MIT" 503 + }, 504 + "node_modules/@atproto/lex-data/node_modules/multiformats": { 505 + "version": "9.9.0", 506 + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz", 507 + "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==", 508 + "license": "(Apache-2.0 AND MIT)" 509 + }, 510 + "node_modules/@atproto/lex-document": { 511 + "version": "0.0.8", 512 + "resolved": "https://registry.npmjs.org/@atproto/lex-document/-/lex-document-0.0.8.tgz", 513 + "integrity": "sha512-p3l5h96Hx0vxUwbO/eas6x5h2vU0JVN1a/ktX4k3PlK9YLXfWMFsv+RdVwVZom8o0irHwlcyh1D/cY0PyUojDA==", 514 + "license": "MIT", 515 + "dependencies": { 516 + "@atproto/lex-schema": "0.0.7", 517 + "core-js": "^3", 518 + "tslib": "^2.8.1" 519 + } 520 + }, 521 + "node_modules/@atproto/lex-installer": { 522 + "version": "0.0.9", 523 + "resolved": "https://registry.npmjs.org/@atproto/lex-installer/-/lex-installer-0.0.9.tgz", 524 + "integrity": "sha512-zEeIeSaSCb3j+zNsqqMY7+X5FO6fxy/MafaCEj42KsXQHNcobuygZsnG/0fxMj/kMvhjrNUCp/w9PyOMwx4hQg==", 525 + "license": "MIT", 526 + "dependencies": { 527 + "@atproto/lex-builder": "0.0.9", 528 + "@atproto/lex-cbor": "0.0.6", 529 + "@atproto/lex-data": "0.0.6", 530 + "@atproto/lex-document": "0.0.8", 531 + "@atproto/lex-resolver": "0.0.8", 532 + "@atproto/lex-schema": "0.0.7", 533 + "@atproto/syntax": "0.4.2", 534 + "tslib": "^2.8.1" 535 + } 536 + }, 537 + "node_modules/@atproto/lex-installer/node_modules/@atproto/syntax": { 538 + "version": "0.4.2", 539 + "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.4.2.tgz", 540 + "integrity": "sha512-X9XSRPinBy/0VQ677j8VXlBsYSsUXaiqxWVpGGxJYsAhugdQRb0jqaVKJFtm6RskeNkV6y9xclSUi9UYG/COrA==", 541 + "license": "MIT" 542 + }, 543 + "node_modules/@atproto/lex-json": { 544 + "version": "0.0.6", 545 + "resolved": "https://registry.npmjs.org/@atproto/lex-json/-/lex-json-0.0.6.tgz", 546 + "integrity": "sha512-EILnN5cditPvf+PCNjXt7reMuzjugxAL1fpSzmzJbEMGMUwxOf5pPWxRsaA/M3Boip4NQZ+6DVrPOGUMlnqceg==", 547 + "license": "MIT", 548 + "dependencies": { 549 + "@atproto/lex-data": "0.0.6", 550 + "tslib": "^2.8.1" 551 + } 552 + }, 553 + "node_modules/@atproto/lex-resolver": { 554 + "version": "0.0.8", 555 + "resolved": "https://registry.npmjs.org/@atproto/lex-resolver/-/lex-resolver-0.0.8.tgz", 556 + "integrity": "sha512-4hXT560+k5BIttouuhXOr+UkhAuFvvkJaVdqYb8vx2Ez7eHPiZ+yWkUK6FKpyGsx2whHkJzgleEA6DNWtdDlWA==", 557 + "license": "MIT", 558 + "dependencies": { 559 + "@atproto-labs/did-resolver": "0.2.5", 560 + "@atproto/crypto": "0.4.5", 561 + "@atproto/lex-client": "0.0.7", 562 + "@atproto/lex-data": "0.0.6", 563 + "@atproto/lex-document": "0.0.8", 564 + "@atproto/lex-schema": "0.0.7", 565 + "@atproto/repo": "0.8.12", 566 + "@atproto/syntax": "0.4.2", 567 + "tslib": "^2.8.1" 568 + } 569 + }, 570 + "node_modules/@atproto/lex-resolver/node_modules/@atproto-labs/did-resolver": { 571 + "version": "0.2.5", 572 + "resolved": "https://registry.npmjs.org/@atproto-labs/did-resolver/-/did-resolver-0.2.5.tgz", 573 + "integrity": "sha512-he7EC6OMSifNs01a4RT9mta/yYitoKDzlK9ty2TFV5Uj/+HpB4vYMRdIDFrRW0Hcsehy90E2t/dw0t7361MEKQ==", 574 + "license": "MIT", 575 + "dependencies": { 576 + "@atproto-labs/fetch": "0.2.3", 577 + "@atproto-labs/pipe": "0.1.1", 578 + "@atproto-labs/simple-store": "0.3.0", 579 + "@atproto-labs/simple-store-memory": "0.1.4", 580 + "@atproto/did": "0.2.4", 441 581 "zod": "^3.23.8" 442 582 } 443 583 }, 444 - "node_modules/@atproto/lexicon/node_modules/@atproto/syntax": { 445 - "version": "0.4.3", 446 - "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.4.3.tgz", 447 - "integrity": "sha512-YoZUz40YAJr5nPwvCDWgodEOlt5IftZqPJvA0JDWjuZKD8yXddTwSzXSaKQAzGOpuM+/A3uXRtPzJJqlScc+iA==", 584 + "node_modules/@atproto/lex-resolver/node_modules/@atproto/did": { 585 + "version": "0.2.4", 586 + "resolved": "https://registry.npmjs.org/@atproto/did/-/did-0.2.4.tgz", 587 + "integrity": "sha512-nxNiCgXeo7pfjojq9fpfZxCO0X0xUipNVKW+AHNZwQKiUDt6zYL0VXEfm8HBUwQOCmKvj2pRRSM1Cur+tUWk3g==", 448 588 "license": "MIT", 449 589 "dependencies": { 590 + "zod": "^3.23.8" 591 + } 592 + }, 593 + "node_modules/@atproto/lex-resolver/node_modules/@atproto/syntax": { 594 + "version": "0.4.2", 595 + "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.4.2.tgz", 596 + "integrity": "sha512-X9XSRPinBy/0VQ677j8VXlBsYSsUXaiqxWVpGGxJYsAhugdQRb0jqaVKJFtm6RskeNkV6y9xclSUi9UYG/COrA==", 597 + "license": "MIT" 598 + }, 599 + "node_modules/@atproto/lex-schema": { 600 + "version": "0.0.7", 601 + "resolved": "https://registry.npmjs.org/@atproto/lex-schema/-/lex-schema-0.0.7.tgz", 602 + "integrity": "sha512-/7HkTUsnP1rlzmVE6nnY0kl/hydL/W8V29V8BhFwdAvdDKpYcdRgzzsMe38LAt+ZOjHknRCZDIKGsbQMSbJErw==", 603 + "license": "MIT", 604 + "dependencies": { 605 + "@atproto/lex-data": "0.0.6", 606 + "@atproto/syntax": "0.4.2", 450 607 "tslib": "^2.8.1" 451 608 } 609 + }, 610 + "node_modules/@atproto/lex-schema/node_modules/@atproto/syntax": { 611 + "version": "0.4.2", 612 + "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.4.2.tgz", 613 + "integrity": "sha512-X9XSRPinBy/0VQ677j8VXlBsYSsUXaiqxWVpGGxJYsAhugdQRb0jqaVKJFtm6RskeNkV6y9xclSUi9UYG/COrA==", 614 + "license": "MIT" 615 + }, 616 + "node_modules/@atproto/lexicon": { 617 + "version": "0.5.1", 618 + "resolved": "https://registry.npmjs.org/@atproto/lexicon/-/lexicon-0.5.1.tgz", 619 + "integrity": "sha512-y8AEtYmfgVl4fqFxqXAeGvhesiGkxiy3CWoJIfsFDDdTlZUC8DFnZrYhcqkIop3OlCkkljvpSJi1hbeC1tbi8A==", 620 + "license": "MIT", 621 + "dependencies": { 622 + "@atproto/common-web": "^0.4.3", 623 + "@atproto/syntax": "^0.4.1", 624 + "iso-datestring-validator": "^2.2.2", 625 + "multiformats": "^9.9.0", 626 + "zod": "^3.23.8" 627 + } 628 + }, 629 + "node_modules/@atproto/lexicon/node_modules/@atproto/syntax": { 630 + "version": "0.4.1", 631 + "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.4.1.tgz", 632 + "integrity": "sha512-CJdImtLAiFO+0z3BWTtxwk6aY5w4t8orHTMVJgkf++QRJWTxPbIFko/0hrkADB7n2EruDxDSeAgfUGehpH6ngw==", 633 + "license": "MIT" 452 634 }, 453 635 "node_modules/@atproto/lexicon/node_modules/multiformats": { 454 636 "version": "9.9.0", ··· 533 715 } 534 716 }, 535 717 "node_modules/@atproto/repo/node_modules/@atproto/common": { 536 - "version": "0.5.10", 537 - "resolved": "https://registry.npmjs.org/@atproto/common/-/common-0.5.10.tgz", 538 - "integrity": "sha512-A1+4W3JmjZIgmtJFLJBAaoVruZhRL0ANtyjZ91aJR4rjHcZuaQ+v4IFR1UcE6yyTATacLdBk6ADy8OtxXzq14g==", 718 + "version": "0.5.6", 719 + "resolved": "https://registry.npmjs.org/@atproto/common/-/common-0.5.6.tgz", 720 + "integrity": "sha512-rbWoZwHQNP8jcwjCREVecchw8aaoM5A1NCONyb9PVDWOJLRLCzojYMeIS8IbFqXo6NyIByOGddupADkkLeVBGQ==", 539 721 "license": "MIT", 540 722 "dependencies": { 541 - "@atproto/common-web": "^0.4.15", 542 - "@atproto/lex-cbor": "^0.0.10", 543 - "@atproto/lex-data": "^0.0.10", 723 + "@atproto/common-web": "^0.4.10", 724 + "@atproto/lex-cbor": "0.0.6", 725 + "@atproto/lex-data": "0.0.6", 544 726 "iso-datestring-validator": "^2.2.2", 545 727 "multiformats": "^9.9.0", 546 728 "pino": "^8.21.0" ··· 549 731 "node": ">=18.7.0" 550 732 } 551 733 }, 552 - "node_modules/@atproto/repo/node_modules/@atproto/lex-cbor": { 553 - "version": "0.0.10", 554 - "resolved": "https://registry.npmjs.org/@atproto/lex-cbor/-/lex-cbor-0.0.10.tgz", 555 - "integrity": "sha512-5RtV90iIhRNCXXvvETd3KlraV8XGAAAgOmiszUb+l8GySDU/sGk7AlVvArFfXnj/S/GXJq8DP6IaUxCw/sPASA==", 734 + "node_modules/@atproto/repo/node_modules/@atproto/lexicon": { 735 + "version": "0.6.0", 736 + "resolved": "https://registry.npmjs.org/@atproto/lexicon/-/lexicon-0.6.0.tgz", 737 + "integrity": "sha512-5veb8aD+J5M0qszLJ+73KSFsFrJBgAY/nM1TSAJvGY7fNc9ZAT+PSUlmIyrdye9YznAZ07yktalls/TwNV7cHQ==", 556 738 "license": "MIT", 557 739 "dependencies": { 558 - "@atproto/lex-data": "^0.0.10", 559 - "tslib": "^2.8.1" 740 + "@atproto/common-web": "^0.4.7", 741 + "@atproto/syntax": "^0.4.2", 742 + "iso-datestring-validator": "^2.2.2", 743 + "multiformats": "^9.9.0", 744 + "zod": "^3.23.8" 560 745 } 561 746 }, 562 - "node_modules/@atproto/repo/node_modules/@atproto/lex-data": { 563 - "version": "0.0.10", 564 - "resolved": "https://registry.npmjs.org/@atproto/lex-data/-/lex-data-0.0.10.tgz", 565 - "integrity": "sha512-FDbcy8VIUVzS9Mi1F8SMxbkL/jOUmRRpqbeM1xB4A0fMxeZJTxf6naAbFt4gYF3quu/+TPJGmio6/7cav05FqQ==", 566 - "license": "MIT", 567 - "dependencies": { 568 - "multiformats": "^9.9.0", 569 - "tslib": "^2.8.1", 570 - "uint8arrays": "3.0.0", 571 - "unicode-segmenter": "^0.14.0" 572 - } 747 + "node_modules/@atproto/repo/node_modules/@atproto/syntax": { 748 + "version": "0.4.2", 749 + "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.4.2.tgz", 750 + "integrity": "sha512-X9XSRPinBy/0VQ677j8VXlBsYSsUXaiqxWVpGGxJYsAhugdQRb0jqaVKJFtm6RskeNkV6y9xclSUi9UYG/COrA==", 751 + "license": "MIT" 573 752 }, 574 753 "node_modules/@atproto/repo/node_modules/multiformats": { 575 754 "version": "9.9.0", ··· 598 777 } 599 778 }, 600 779 "node_modules/@atproto/sync/node_modules/@atproto/syntax": { 601 - "version": "0.4.3", 602 - "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.4.3.tgz", 603 - "integrity": "sha512-YoZUz40YAJr5nPwvCDWgodEOlt5IftZqPJvA0JDWjuZKD8yXddTwSzXSaKQAzGOpuM+/A3uXRtPzJJqlScc+iA==", 604 - "license": "MIT", 605 - "dependencies": { 606 - "tslib": "^2.8.1" 607 - } 780 + "version": "0.4.1", 781 + "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.4.1.tgz", 782 + "integrity": "sha512-CJdImtLAiFO+0z3BWTtxwk6aY5w4t8orHTMVJgkf++QRJWTxPbIFko/0hrkADB7n2EruDxDSeAgfUGehpH6ngw==", 783 + "license": "MIT" 608 784 }, 609 785 "node_modules/@atproto/sync/node_modules/multiformats": { 610 786 "version": "9.9.0", ··· 617 793 "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.3.4.tgz", 618 794 "integrity": "sha512-8CNmi5DipOLaVeSMPggMe7FCksVag0aO6XZy9WflbduTKM4dFZVCs4686UeMLfGRXX+X966XgwECHoLYrovMMg==", 619 795 "license": "MIT" 796 + }, 797 + "node_modules/@atproto/tap": { 798 + "version": "0.1.1", 799 + "resolved": "https://registry.npmjs.org/@atproto/tap/-/tap-0.1.1.tgz", 800 + "integrity": "sha512-gW4NzLOxj74TzaDOVzzzt5kl2PdC0r75XkIpYpI5xobwCfsc/DmVtwpuSw1fW9gr4Vzk2Q90S9UE4ifAFl2gyA==", 801 + "license": "MIT", 802 + "dependencies": { 803 + "@atproto/common": "^0.5.6", 804 + "@atproto/lex": "^0.0.9", 805 + "@atproto/syntax": "^0.4.2", 806 + "@atproto/ws-client": "^0.0.4", 807 + "ws": "^8.12.0", 808 + "zod": "^3.23.8" 809 + }, 810 + "engines": { 811 + "node": ">=18.7.0" 812 + } 813 + }, 814 + "node_modules/@atproto/tap/node_modules/@atproto/common": { 815 + "version": "0.5.6", 816 + "resolved": "https://registry.npmjs.org/@atproto/common/-/common-0.5.6.tgz", 817 + "integrity": "sha512-rbWoZwHQNP8jcwjCREVecchw8aaoM5A1NCONyb9PVDWOJLRLCzojYMeIS8IbFqXo6NyIByOGddupADkkLeVBGQ==", 818 + "license": "MIT", 819 + "dependencies": { 820 + "@atproto/common-web": "^0.4.10", 821 + "@atproto/lex-cbor": "0.0.6", 822 + "@atproto/lex-data": "0.0.6", 823 + "iso-datestring-validator": "^2.2.2", 824 + "multiformats": "^9.9.0", 825 + "pino": "^8.21.0" 826 + }, 827 + "engines": { 828 + "node": ">=18.7.0" 829 + } 830 + }, 831 + "node_modules/@atproto/tap/node_modules/@atproto/syntax": { 832 + "version": "0.4.2", 833 + "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.4.2.tgz", 834 + "integrity": "sha512-X9XSRPinBy/0VQ677j8VXlBsYSsUXaiqxWVpGGxJYsAhugdQRb0jqaVKJFtm6RskeNkV6y9xclSUi9UYG/COrA==", 835 + "license": "MIT" 836 + }, 837 + "node_modules/@atproto/tap/node_modules/multiformats": { 838 + "version": "9.9.0", 839 + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz", 840 + "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==", 841 + "license": "(Apache-2.0 AND MIT)" 842 + }, 843 + "node_modules/@atproto/ws-client": { 844 + "version": "0.0.4", 845 + "resolved": "https://registry.npmjs.org/@atproto/ws-client/-/ws-client-0.0.4.tgz", 846 + "integrity": "sha512-dox1XIymuC7/ZRhUqKezIGgooZS45C6vHCfu0PnWjfvsLCK2kAlnvX4IBkA/WpcoijDhQ9ejChnFbo/sLmgvAg==", 847 + "license": "MIT", 848 + "dependencies": { 849 + "@atproto/common": "^0.5.3", 850 + "ws": "^8.12.0" 851 + }, 852 + "engines": { 853 + "node": ">=18.7.0" 854 + } 855 + }, 856 + "node_modules/@atproto/ws-client/node_modules/@atproto/common": { 857 + "version": "0.5.6", 858 + "resolved": "https://registry.npmjs.org/@atproto/common/-/common-0.5.6.tgz", 859 + "integrity": "sha512-rbWoZwHQNP8jcwjCREVecchw8aaoM5A1NCONyb9PVDWOJLRLCzojYMeIS8IbFqXo6NyIByOGddupADkkLeVBGQ==", 860 + "license": "MIT", 861 + "dependencies": { 862 + "@atproto/common-web": "^0.4.10", 863 + "@atproto/lex-cbor": "0.0.6", 864 + "@atproto/lex-data": "0.0.6", 865 + "iso-datestring-validator": "^2.2.2", 866 + "multiformats": "^9.9.0", 867 + "pino": "^8.21.0" 868 + }, 869 + "engines": { 870 + "node": ">=18.7.0" 871 + } 872 + }, 873 + "node_modules/@atproto/ws-client/node_modules/multiformats": { 874 + "version": "9.9.0", 875 + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz", 876 + "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==", 877 + "license": "(Apache-2.0 AND MIT)" 620 878 }, 621 879 "node_modules/@atproto/xrpc": { 622 880 "version": "0.7.5", ··· 1456 1714 } 1457 1715 }, 1458 1716 "node_modules/@esbuild/aix-ppc64": { 1459 - "version": "0.25.12", 1460 - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", 1461 - "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", 1717 + "version": "0.25.4", 1718 + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz", 1719 + "integrity": "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==", 1462 1720 "cpu": [ 1463 1721 "ppc64" 1464 1722 ], 1465 1723 "dev": true, 1466 - "license": "MIT", 1467 1724 "optional": true, 1468 1725 "os": [ 1469 1726 "aix" ··· 1473 1730 } 1474 1731 }, 1475 1732 "node_modules/@esbuild/android-arm": { 1476 - "version": "0.25.12", 1477 - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", 1478 - "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", 1733 + "version": "0.25.4", 1734 + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.4.tgz", 1735 + "integrity": "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==", 1479 1736 "cpu": [ 1480 1737 "arm" 1481 1738 ], 1482 1739 "dev": true, 1483 - "license": "MIT", 1484 1740 "optional": true, 1485 1741 "os": [ 1486 1742 "android" ··· 1490 1746 } 1491 1747 }, 1492 1748 "node_modules/@esbuild/android-arm64": { 1493 - "version": "0.25.12", 1494 - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", 1495 - "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", 1749 + "version": "0.25.4", 1750 + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.4.tgz", 1751 + "integrity": "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==", 1496 1752 "cpu": [ 1497 1753 "arm64" 1498 1754 ], 1499 1755 "dev": true, 1500 - "license": "MIT", 1501 1756 "optional": true, 1502 1757 "os": [ 1503 1758 "android" ··· 1507 1762 } 1508 1763 }, 1509 1764 "node_modules/@esbuild/android-x64": { 1510 - "version": "0.25.12", 1511 - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", 1512 - "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", 1765 + "version": "0.25.4", 1766 + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.4.tgz", 1767 + "integrity": "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==", 1513 1768 "cpu": [ 1514 1769 "x64" 1515 1770 ], 1516 1771 "dev": true, 1517 - "license": "MIT", 1518 1772 "optional": true, 1519 1773 "os": [ 1520 1774 "android" ··· 1523 1777 "node": ">=18" 1524 1778 } 1525 1779 }, 1526 - "node_modules/@esbuild/darwin-arm64": { 1527 - "version": "0.25.12", 1528 - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", 1529 - "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", 1530 - "cpu": [ 1531 - "arm64" 1532 - ], 1533 - "dev": true, 1534 - "license": "MIT", 1535 - "optional": true, 1536 - "os": [ 1537 - "darwin" 1538 - ], 1539 - "engines": { 1540 - "node": ">=18" 1541 - } 1542 - }, 1543 1780 "node_modules/@esbuild/darwin-x64": { 1544 - "version": "0.25.12", 1545 - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", 1546 - "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", 1781 + "version": "0.25.4", 1782 + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.4.tgz", 1783 + "integrity": "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==", 1547 1784 "cpu": [ 1548 1785 "x64" 1549 1786 ], 1550 1787 "dev": true, 1551 - "license": "MIT", 1552 1788 "optional": true, 1553 1789 "os": [ 1554 1790 "darwin" ··· 1558 1794 } 1559 1795 }, 1560 1796 "node_modules/@esbuild/freebsd-arm64": { 1561 - "version": "0.25.12", 1562 - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", 1563 - "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", 1797 + "version": "0.25.4", 1798 + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.4.tgz", 1799 + "integrity": "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==", 1564 1800 "cpu": [ 1565 1801 "arm64" 1566 1802 ], 1567 1803 "dev": true, 1568 - "license": "MIT", 1569 1804 "optional": true, 1570 1805 "os": [ 1571 1806 "freebsd" ··· 1575 1810 } 1576 1811 }, 1577 1812 "node_modules/@esbuild/freebsd-x64": { 1578 - "version": "0.25.12", 1579 - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", 1580 - "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", 1813 + "version": "0.25.4", 1814 + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.4.tgz", 1815 + "integrity": "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==", 1581 1816 "cpu": [ 1582 1817 "x64" 1583 1818 ], 1584 1819 "dev": true, 1585 - "license": "MIT", 1586 1820 "optional": true, 1587 1821 "os": [ 1588 1822 "freebsd" ··· 1592 1826 } 1593 1827 }, 1594 1828 "node_modules/@esbuild/linux-arm": { 1595 - "version": "0.25.12", 1596 - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", 1597 - "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", 1829 + "version": "0.25.4", 1830 + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.4.tgz", 1831 + "integrity": "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==", 1598 1832 "cpu": [ 1599 1833 "arm" 1600 1834 ], 1601 1835 "dev": true, 1602 - "license": "MIT", 1603 1836 "optional": true, 1604 1837 "os": [ 1605 1838 "linux" ··· 1609 1842 } 1610 1843 }, 1611 1844 "node_modules/@esbuild/linux-arm64": { 1612 - "version": "0.25.12", 1613 - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", 1614 - "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", 1845 + "version": "0.25.4", 1846 + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.4.tgz", 1847 + "integrity": "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==", 1615 1848 "cpu": [ 1616 1849 "arm64" 1617 1850 ], 1618 1851 "dev": true, 1619 - "license": "MIT", 1620 1852 "optional": true, 1621 1853 "os": [ 1622 1854 "linux" ··· 1626 1858 } 1627 1859 }, 1628 1860 "node_modules/@esbuild/linux-ia32": { 1629 - "version": "0.25.12", 1630 - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", 1631 - "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", 1861 + "version": "0.25.4", 1862 + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.4.tgz", 1863 + "integrity": "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==", 1632 1864 "cpu": [ 1633 1865 "ia32" 1634 1866 ], 1635 1867 "dev": true, 1636 - "license": "MIT", 1637 1868 "optional": true, 1638 1869 "os": [ 1639 1870 "linux" ··· 1643 1874 } 1644 1875 }, 1645 1876 "node_modules/@esbuild/linux-loong64": { 1646 - "version": "0.25.12", 1647 - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", 1648 - "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", 1877 + "version": "0.25.4", 1878 + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.4.tgz", 1879 + "integrity": "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==", 1649 1880 "cpu": [ 1650 1881 "loong64" 1651 1882 ], 1652 1883 "dev": true, 1653 - "license": "MIT", 1654 1884 "optional": true, 1655 1885 "os": [ 1656 1886 "linux" ··· 1660 1890 } 1661 1891 }, 1662 1892 "node_modules/@esbuild/linux-mips64el": { 1663 - "version": "0.25.12", 1664 - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", 1665 - "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", 1893 + "version": "0.25.4", 1894 + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.4.tgz", 1895 + "integrity": "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==", 1666 1896 "cpu": [ 1667 1897 "mips64el" 1668 1898 ], 1669 1899 "dev": true, 1670 - "license": "MIT", 1671 1900 "optional": true, 1672 1901 "os": [ 1673 1902 "linux" ··· 1677 1906 } 1678 1907 }, 1679 1908 "node_modules/@esbuild/linux-ppc64": { 1680 - "version": "0.25.12", 1681 - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", 1682 - "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", 1909 + "version": "0.25.4", 1910 + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.4.tgz", 1911 + "integrity": "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==", 1683 1912 "cpu": [ 1684 1913 "ppc64" 1685 1914 ], 1686 1915 "dev": true, 1687 - "license": "MIT", 1688 1916 "optional": true, 1689 1917 "os": [ 1690 1918 "linux" ··· 1694 1922 } 1695 1923 }, 1696 1924 "node_modules/@esbuild/linux-riscv64": { 1697 - "version": "0.25.12", 1698 - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", 1699 - "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", 1925 + "version": "0.25.4", 1926 + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.4.tgz", 1927 + "integrity": "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==", 1700 1928 "cpu": [ 1701 1929 "riscv64" 1702 1930 ], 1703 1931 "dev": true, 1704 - "license": "MIT", 1705 1932 "optional": true, 1706 1933 "os": [ 1707 1934 "linux" ··· 1711 1938 } 1712 1939 }, 1713 1940 "node_modules/@esbuild/linux-s390x": { 1714 - "version": "0.25.12", 1715 - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", 1716 - "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", 1941 + "version": "0.25.4", 1942 + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.4.tgz", 1943 + "integrity": "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==", 1717 1944 "cpu": [ 1718 1945 "s390x" 1719 1946 ], 1720 1947 "dev": true, 1721 - "license": "MIT", 1722 1948 "optional": true, 1723 1949 "os": [ 1724 1950 "linux" ··· 1728 1954 } 1729 1955 }, 1730 1956 "node_modules/@esbuild/linux-x64": { 1731 - "version": "0.25.12", 1732 - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", 1733 - "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", 1957 + "version": "0.25.4", 1958 + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.4.tgz", 1959 + "integrity": "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==", 1734 1960 "cpu": [ 1735 1961 "x64" 1736 1962 ], ··· 1745 1971 } 1746 1972 }, 1747 1973 "node_modules/@esbuild/netbsd-arm64": { 1748 - "version": "0.25.12", 1749 - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", 1750 - "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", 1974 + "version": "0.25.4", 1975 + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.4.tgz", 1976 + "integrity": "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==", 1751 1977 "cpu": [ 1752 1978 "arm64" 1753 1979 ], 1754 1980 "dev": true, 1755 - "license": "MIT", 1756 1981 "optional": true, 1757 1982 "os": [ 1758 1983 "netbsd" ··· 1762 1987 } 1763 1988 }, 1764 1989 "node_modules/@esbuild/netbsd-x64": { 1765 - "version": "0.25.12", 1766 - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", 1767 - "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", 1990 + "version": "0.25.4", 1991 + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.4.tgz", 1992 + "integrity": "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==", 1768 1993 "cpu": [ 1769 1994 "x64" 1770 1995 ], 1771 1996 "dev": true, 1772 - "license": "MIT", 1773 1997 "optional": true, 1774 1998 "os": [ 1775 1999 "netbsd" ··· 1779 2003 } 1780 2004 }, 1781 2005 "node_modules/@esbuild/openbsd-arm64": { 1782 - "version": "0.25.12", 1783 - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", 1784 - "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", 2006 + "version": "0.25.4", 2007 + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.4.tgz", 2008 + "integrity": "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==", 1785 2009 "cpu": [ 1786 2010 "arm64" 1787 2011 ], 1788 2012 "dev": true, 1789 - "license": "MIT", 1790 2013 "optional": true, 1791 2014 "os": [ 1792 2015 "openbsd" ··· 1796 2019 } 1797 2020 }, 1798 2021 "node_modules/@esbuild/openbsd-x64": { 1799 - "version": "0.25.12", 1800 - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", 1801 - "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", 2022 + "version": "0.25.4", 2023 + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.4.tgz", 2024 + "integrity": "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==", 1802 2025 "cpu": [ 1803 2026 "x64" 1804 2027 ], 1805 2028 "dev": true, 1806 - "license": "MIT", 1807 2029 "optional": true, 1808 2030 "os": [ 1809 2031 "openbsd" ··· 1812 2034 "node": ">=18" 1813 2035 } 1814 2036 }, 1815 - "node_modules/@esbuild/openharmony-arm64": { 1816 - "version": "0.25.12", 1817 - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", 1818 - "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", 1819 - "cpu": [ 1820 - "arm64" 1821 - ], 1822 - "dev": true, 1823 - "license": "MIT", 1824 - "optional": true, 1825 - "os": [ 1826 - "openharmony" 1827 - ], 1828 - "engines": { 1829 - "node": ">=18" 1830 - } 1831 - }, 1832 2037 "node_modules/@esbuild/sunos-x64": { 1833 - "version": "0.25.12", 1834 - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", 1835 - "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", 2038 + "version": "0.25.4", 2039 + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.4.tgz", 2040 + "integrity": "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==", 1836 2041 "cpu": [ 1837 2042 "x64" 1838 2043 ], 1839 2044 "dev": true, 1840 - "license": "MIT", 1841 2045 "optional": true, 1842 2046 "os": [ 1843 2047 "sunos" ··· 1847 2051 } 1848 2052 }, 1849 2053 "node_modules/@esbuild/win32-arm64": { 1850 - "version": "0.25.12", 1851 - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", 1852 - "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", 2054 + "version": "0.25.4", 2055 + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.4.tgz", 2056 + "integrity": "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==", 1853 2057 "cpu": [ 1854 2058 "arm64" 1855 2059 ], 1856 2060 "dev": true, 1857 - "license": "MIT", 1858 2061 "optional": true, 1859 2062 "os": [ 1860 2063 "win32" ··· 1864 2067 } 1865 2068 }, 1866 2069 "node_modules/@esbuild/win32-ia32": { 1867 - "version": "0.25.12", 1868 - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", 1869 - "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", 2070 + "version": "0.25.4", 2071 + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.4.tgz", 2072 + "integrity": "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==", 1870 2073 "cpu": [ 1871 2074 "ia32" 1872 2075 ], 1873 2076 "dev": true, 1874 - "license": "MIT", 1875 2077 "optional": true, 1876 2078 "os": [ 1877 2079 "win32" ··· 1881 2083 } 1882 2084 }, 1883 2085 "node_modules/@esbuild/win32-x64": { 1884 - "version": "0.25.12", 1885 - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", 1886 - "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", 2086 + "version": "0.25.4", 2087 + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz", 2088 + "integrity": "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==", 1887 2089 "cpu": [ 1888 2090 "x64" 1889 2091 ], 1890 2092 "dev": true, 1891 - "license": "MIT", 1892 2093 "optional": true, 1893 2094 "os": [ 1894 2095 "win32" ··· 2693 2894 "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==", 2694 2895 "license": "(Apache-2.0 AND MIT)" 2695 2896 }, 2897 + "node_modules/@isaacs/balanced-match": { 2898 + "version": "4.0.1", 2899 + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", 2900 + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", 2901 + "license": "MIT", 2902 + "engines": { 2903 + "node": "20 || >=22" 2904 + } 2905 + }, 2906 + "node_modules/@isaacs/brace-expansion": { 2907 + "version": "5.0.0", 2908 + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", 2909 + "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", 2910 + "license": "MIT", 2911 + "dependencies": { 2912 + "@isaacs/balanced-match": "^4.0.1" 2913 + }, 2914 + "engines": { 2915 + "node": "20 || >=22" 2916 + } 2917 + }, 2696 2918 "node_modules/@isaacs/fs-minipass": { 2697 2919 "version": "4.0.1", 2698 2920 "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", ··· 2792 3014 } 2793 3015 }, 2794 3016 "node_modules/@mdx-js/loader/node_modules/source-map": { 2795 - "version": "0.7.6", 2796 - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", 2797 - "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", 2798 - "license": "BSD-3-Clause", 3017 + "version": "0.7.4", 3018 + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", 3019 + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", 2799 3020 "engines": { 2800 - "node": ">= 12" 3021 + "node": ">= 8" 2801 3022 } 2802 3023 }, 2803 3024 "node_modules/@mdx-js/mdx": { ··· 2844 3065 } 2845 3066 }, 2846 3067 "node_modules/@mdx-js/mdx/node_modules/source-map": { 2847 - "version": "0.7.6", 2848 - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", 2849 - "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", 2850 - "license": "BSD-3-Clause", 3068 + "version": "0.7.4", 3069 + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", 3070 + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", 2851 3071 "engines": { 2852 - "node": ">= 12" 3072 + "node": ">= 8" 2853 3073 } 2854 3074 }, 2855 3075 "node_modules/@mdx-js/react": { ··· 2940 3160 } 2941 3161 }, 2942 3162 "node_modules/@next/mdx/node_modules/source-map": { 2943 - "version": "0.7.6", 2944 - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", 2945 - "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", 2946 - "license": "BSD-3-Clause", 3163 + "version": "0.7.4", 3164 + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", 3165 + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", 2947 3166 "engines": { 2948 - "node": ">= 12" 3167 + "node": ">= 8" 2949 3168 } 2950 3169 }, 2951 3170 "node_modules/@next/swc-darwin-arm64": { ··· 7330 7549 } 7331 7550 }, 7332 7551 "node_modules/@tailwindcss/node/node_modules/magic-string": { 7333 - "version": "0.30.21", 7334 - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", 7335 - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", 7552 + "version": "0.30.19", 7553 + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz", 7554 + "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==", 7336 7555 "dev": true, 7337 7556 "license": "MIT", 7338 7557 "dependencies": { ··· 7586 7805 } 7587 7806 }, 7588 7807 "node_modules/@tailwindcss/oxide/node_modules/tar": { 7589 - "version": "7.5.7", 7590 - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.7.tgz", 7591 - "integrity": "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==", 7808 + "version": "7.5.1", 7809 + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.1.tgz", 7810 + "integrity": "sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==", 7592 7811 "dev": true, 7593 - "license": "BlueOak-1.0.0", 7812 + "license": "ISC", 7594 7813 "dependencies": { 7595 7814 "@isaacs/fs-minipass": "^4.0.0", 7596 7815 "chownr": "^3.0.0", ··· 7923 8142 } 7924 8143 }, 7925 8144 "node_modules/@types/unist": { 7926 - "version": "3.0.3", 7927 - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", 7928 - "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", 7929 - "license": "MIT" 8145 + "version": "3.0.2", 8146 + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", 8147 + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" 7930 8148 }, 7931 8149 "node_modules/@types/uuid": { 7932 8150 "version": "10.0.0", ··· 8282 8500 } 8283 8501 } 8284 8502 }, 8285 - "node_modules/@yornaath/batshit": { 8286 - "version": "0.14.0", 8287 - "resolved": "https://registry.npmjs.org/@yornaath/batshit/-/batshit-0.14.0.tgz", 8288 - "integrity": "sha512-0I+xMi5JoRs3+qVXXhk2AmsEl43MwrG+L+VW+nqw/qQqMFtgRPszLaxhJCfsBKnjfJ0gJzTI1Q9Q9+y903HyHQ==", 8289 - "license": "MIT", 8290 - "dependencies": { 8291 - "@yornaath/batshit-devtools": "^1.7.1" 8292 - } 8293 - }, 8294 - "node_modules/@yornaath/batshit-devtools": { 8295 - "version": "1.7.1", 8296 - "resolved": "https://registry.npmjs.org/@yornaath/batshit-devtools/-/batshit-devtools-1.7.1.tgz", 8297 - "integrity": "sha512-AyttV1Njj5ug+XqEWY1smV45dTWMlWKtj1B8jcFYgBKUFyUlF/qEhD+iP1E5UaRYW6hQRYD9T2WNDwFTrOMWzQ==", 8298 - "license": "MIT" 8299 - }, 8300 8503 "node_modules/abort-controller": { 8301 8504 "version": "3.0.0", 8302 8505 "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", ··· 8360 8563 } 8361 8564 }, 8362 8565 "node_modules/agent-base": { 8363 - "version": "7.1.4", 8364 - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", 8365 - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", 8366 - "license": "MIT", 8566 + "version": "7.1.1", 8567 + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", 8568 + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", 8569 + "dependencies": { 8570 + "debug": "^4.3.4" 8571 + }, 8367 8572 "engines": { 8368 8573 "node": ">= 14" 8369 8574 } ··· 8842 9047 "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", 8843 9048 "license": "MIT" 8844 9049 }, 9050 + "node_modules/body-parser/node_modules/qs": { 9051 + "version": "6.13.0", 9052 + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", 9053 + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", 9054 + "license": "BSD-3-Clause", 9055 + "dependencies": { 9056 + "side-channel": "^1.0.6" 9057 + }, 9058 + "engines": { 9059 + "node": ">=0.6" 9060 + }, 9061 + "funding": { 9062 + "url": "https://github.com/sponsors/ljharb" 9063 + } 9064 + }, 8845 9065 "node_modules/brace-expansion": { 8846 - "version": "1.1.12", 8847 - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", 8848 - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", 9066 + "version": "1.1.11", 9067 + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 9068 + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 8849 9069 "dev": true, 8850 - "license": "MIT", 8851 9070 "dependencies": { 8852 9071 "balanced-match": "^1.0.0", 8853 9072 "concat-map": "0.0.1" ··· 9251 9470 "version": "13.0.3", 9252 9471 "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.3.tgz", 9253 9472 "integrity": "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==", 9254 - "dev": true, 9255 9473 "license": "MIT" 9256 9474 }, 9257 9475 "node_modules/collapse-white-space": { ··· 9359 9577 "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 9360 9578 "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", 9361 9579 "license": "MIT" 9580 + }, 9581 + "node_modules/core-js": { 9582 + "version": "3.47.0", 9583 + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.47.0.tgz", 9584 + "integrity": "sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg==", 9585 + "hasInstallScript": true, 9586 + "license": "MIT", 9587 + "funding": { 9588 + "type": "opencollective", 9589 + "url": "https://opencollective.com/core-js" 9590 + } 9362 9591 }, 9363 9592 "node_modules/crelt": { 9364 9593 "version": "1.0.6", ··· 9519 9748 "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==" 9520 9749 }, 9521 9750 "node_modules/debug": { 9522 - "version": "4.4.3", 9523 - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", 9524 - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", 9751 + "version": "4.4.1", 9752 + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", 9753 + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", 9525 9754 "license": "MIT", 9526 9755 "dependencies": { 9527 9756 "ms": "^2.1.3" ··· 9673 9902 }, 9674 9903 "engines": { 9675 9904 "node": "*" 9676 - } 9677 - }, 9678 - "node_modules/doctrine": { 9679 - "version": "2.1.0", 9680 - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", 9681 - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", 9682 - "dev": true, 9683 - "license": "Apache-2.0", 9684 - "dependencies": { 9685 - "esutils": "^2.0.2" 9686 - }, 9687 - "engines": { 9688 - "node": ">=0.10.0" 9689 9905 } 9690 9906 }, 9691 9907 "node_modules/dreamopt": { ··· 10584 10800 } 10585 10801 }, 10586 10802 "node_modules/esbuild": { 10587 - "version": "0.25.12", 10588 - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", 10589 - "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", 10803 + "version": "0.25.4", 10804 + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz", 10805 + "integrity": "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==", 10590 10806 "dev": true, 10591 10807 "hasInstallScript": true, 10592 10808 "license": "MIT", ··· 10597 10813 "node": ">=18" 10598 10814 }, 10599 10815 "optionalDependencies": { 10600 - "@esbuild/aix-ppc64": "0.25.12", 10601 - "@esbuild/android-arm": "0.25.12", 10602 - "@esbuild/android-arm64": "0.25.12", 10603 - "@esbuild/android-x64": "0.25.12", 10604 - "@esbuild/darwin-arm64": "0.25.12", 10605 - "@esbuild/darwin-x64": "0.25.12", 10606 - "@esbuild/freebsd-arm64": "0.25.12", 10607 - "@esbuild/freebsd-x64": "0.25.12", 10608 - "@esbuild/linux-arm": "0.25.12", 10609 - "@esbuild/linux-arm64": "0.25.12", 10610 - "@esbuild/linux-ia32": "0.25.12", 10611 - "@esbuild/linux-loong64": "0.25.12", 10612 - "@esbuild/linux-mips64el": "0.25.12", 10613 - "@esbuild/linux-ppc64": "0.25.12", 10614 - "@esbuild/linux-riscv64": "0.25.12", 10615 - "@esbuild/linux-s390x": "0.25.12", 10616 - "@esbuild/linux-x64": "0.25.12", 10617 - "@esbuild/netbsd-arm64": "0.25.12", 10618 - "@esbuild/netbsd-x64": "0.25.12", 10619 - "@esbuild/openbsd-arm64": "0.25.12", 10620 - "@esbuild/openbsd-x64": "0.25.12", 10621 - "@esbuild/openharmony-arm64": "0.25.12", 10622 - "@esbuild/sunos-x64": "0.25.12", 10623 - "@esbuild/win32-arm64": "0.25.12", 10624 - "@esbuild/win32-ia32": "0.25.12", 10625 - "@esbuild/win32-x64": "0.25.12" 10816 + "@esbuild/aix-ppc64": "0.25.4", 10817 + "@esbuild/android-arm": "0.25.4", 10818 + "@esbuild/android-arm64": "0.25.4", 10819 + "@esbuild/android-x64": "0.25.4", 10820 + "@esbuild/darwin-arm64": "0.25.4", 10821 + "@esbuild/darwin-x64": "0.25.4", 10822 + "@esbuild/freebsd-arm64": "0.25.4", 10823 + "@esbuild/freebsd-x64": "0.25.4", 10824 + "@esbuild/linux-arm": "0.25.4", 10825 + "@esbuild/linux-arm64": "0.25.4", 10826 + "@esbuild/linux-ia32": "0.25.4", 10827 + "@esbuild/linux-loong64": "0.25.4", 10828 + "@esbuild/linux-mips64el": "0.25.4", 10829 + "@esbuild/linux-ppc64": "0.25.4", 10830 + "@esbuild/linux-riscv64": "0.25.4", 10831 + "@esbuild/linux-s390x": "0.25.4", 10832 + "@esbuild/linux-x64": "0.25.4", 10833 + "@esbuild/netbsd-arm64": "0.25.4", 10834 + "@esbuild/netbsd-x64": "0.25.4", 10835 + "@esbuild/openbsd-arm64": "0.25.4", 10836 + "@esbuild/openbsd-x64": "0.25.4", 10837 + "@esbuild/sunos-x64": "0.25.4", 10838 + "@esbuild/win32-arm64": "0.25.4", 10839 + "@esbuild/win32-ia32": "0.25.4", 10840 + "@esbuild/win32-x64": "0.25.4" 10626 10841 } 10627 10842 }, 10628 10843 "node_modules/esbuild-register": { ··· 10635 10850 }, 10636 10851 "peerDependencies": { 10637 10852 "esbuild": ">=0.12 <1" 10853 + } 10854 + }, 10855 + "node_modules/esbuild/node_modules/@esbuild/darwin-arm64": { 10856 + "version": "0.25.4", 10857 + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.4.tgz", 10858 + "integrity": "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==", 10859 + "cpu": [ 10860 + "arm64" 10861 + ], 10862 + "dev": true, 10863 + "optional": true, 10864 + "os": [ 10865 + "darwin" 10866 + ], 10867 + "engines": { 10868 + "node": ">=18" 10638 10869 } 10639 10870 }, 10640 10871 "node_modules/escalade": { ··· 10872 11103 "ms": "^2.1.1" 10873 11104 } 10874 11105 }, 11106 + "node_modules/eslint-plugin-import/node_modules/doctrine": { 11107 + "version": "2.1.0", 11108 + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", 11109 + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", 11110 + "dev": true, 11111 + "dependencies": { 11112 + "esutils": "^2.0.2" 11113 + }, 11114 + "engines": { 11115 + "node": ">=0.10.0" 11116 + } 11117 + }, 10875 11118 "node_modules/eslint-plugin-import/node_modules/semver": { 10876 11119 "version": "6.3.1", 10877 11120 "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", ··· 10963 11206 "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" 10964 11207 } 10965 11208 }, 11209 + "node_modules/eslint-plugin-react-hooks/node_modules/zod": { 11210 + "version": "4.1.12", 11211 + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz", 11212 + "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", 11213 + "dev": true, 11214 + "funding": { 11215 + "url": "https://github.com/sponsors/colinhacks" 11216 + } 11217 + }, 10966 11218 "node_modules/eslint-plugin-react-hooks/node_modules/zod-validation-error": { 10967 11219 "version": "4.0.2", 10968 11220 "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", ··· 10975 11227 "zod": "^3.25.0 || ^4.0.0" 10976 11228 } 10977 11229 }, 11230 + "node_modules/eslint-plugin-react/node_modules/doctrine": { 11231 + "version": "2.1.0", 11232 + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", 11233 + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", 11234 + "dev": true, 11235 + "license": "Apache-2.0", 11236 + "dependencies": { 11237 + "esutils": "^2.0.2" 11238 + }, 11239 + "engines": { 11240 + "node": ">=0.10.0" 11241 + } 11242 + }, 10978 11243 "node_modules/eslint-plugin-react/node_modules/resolve": { 10979 11244 "version": "2.0.0-next.5", 10980 11245 "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", ··· 11168 11433 } 11169 11434 }, 11170 11435 "node_modules/estree-util-to-js/node_modules/source-map": { 11171 - "version": "0.7.6", 11172 - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", 11173 - "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", 11174 - "license": "BSD-3-Clause", 11436 + "version": "0.7.4", 11437 + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", 11438 + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", 11175 11439 "engines": { 11176 - "node": ">= 12" 11440 + "node": ">= 8" 11177 11441 } 11178 11442 }, 11179 11443 "node_modules/estree-util-visit": { ··· 11335 11599 "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", 11336 11600 "license": "MIT" 11337 11601 }, 11602 + "node_modules/express/node_modules/qs": { 11603 + "version": "6.13.0", 11604 + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", 11605 + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", 11606 + "license": "BSD-3-Clause", 11607 + "dependencies": { 11608 + "side-channel": "^1.0.6" 11609 + }, 11610 + "engines": { 11611 + "node": ">=0.6" 11612 + }, 11613 + "funding": { 11614 + "url": "https://github.com/sponsors/ljharb" 11615 + } 11616 + }, 11338 11617 "node_modules/ext": { 11339 11618 "version": "1.7.0", 11340 11619 "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", ··· 11356 11635 "dev": true 11357 11636 }, 11358 11637 "node_modules/fast-glob": { 11359 - "version": "3.3.3", 11360 - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", 11361 - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", 11638 + "version": "3.3.2", 11639 + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", 11640 + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", 11362 11641 "dev": true, 11363 - "license": "MIT", 11364 11642 "dependencies": { 11365 11643 "@nodelib/fs.stat": "^2.0.2", 11366 11644 "@nodelib/fs.walk": "^1.2.3", 11367 11645 "glob-parent": "^5.1.2", 11368 11646 "merge2": "^1.3.0", 11369 - "micromatch": "^4.0.8" 11647 + "micromatch": "^4.0.4" 11370 11648 }, 11371 11649 "engines": { 11372 11650 "node": ">=8.6.0" ··· 11650 11928 "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", 11651 11929 "dev": true 11652 11930 }, 11653 - "node_modules/fsevents": { 11654 - "version": "2.3.3", 11655 - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", 11656 - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", 11657 - "dev": true, 11658 - "hasInstallScript": true, 11659 - "license": "MIT", 11660 - "optional": true, 11661 - "os": [ 11662 - "darwin" 11663 - ], 11664 - "engines": { 11665 - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 11666 - } 11667 - }, 11668 11931 "node_modules/function-bind": { 11669 11932 "version": "1.1.2", 11670 11933 "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", ··· 11915 12178 "dev": true 11916 12179 }, 11917 12180 "node_modules/glob/node_modules/brace-expansion": { 11918 - "version": "2.0.2", 11919 - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", 11920 - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", 12181 + "version": "2.0.1", 12182 + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", 12183 + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", 11921 12184 "dev": true, 11922 - "license": "MIT", 11923 12185 "dependencies": { 11924 12186 "balanced-match": "^1.0.0" 11925 12187 } ··· 12459 12721 } 12460 12722 }, 12461 12723 "node_modules/https-proxy-agent": { 12462 - "version": "7.0.6", 12463 - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", 12464 - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", 12465 - "license": "MIT", 12724 + "version": "7.0.4", 12725 + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", 12726 + "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", 12466 12727 "dependencies": { 12467 - "agent-base": "^7.1.2", 12728 + "agent-base": "^7.0.2", 12468 12729 "debug": "4" 12469 12730 }, 12470 12731 "engines": { ··· 12504 12765 "license": "BSD-3-Clause" 12505 12766 }, 12506 12767 "node_modules/ignore": { 12507 - "version": "5.3.2", 12508 - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", 12509 - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", 12768 + "version": "5.3.1", 12769 + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", 12770 + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", 12510 12771 "dev": true, 12511 - "license": "MIT", 12512 12772 "engines": { 12513 12773 "node": ">= 4" 12514 12774 } ··· 12577 12837 "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 12578 12838 }, 12579 12839 "node_modules/inline-style-parser": { 12580 - "version": "0.2.7", 12581 - "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", 12582 - "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", 12583 - "license": "MIT" 12840 + "version": "0.2.4", 12841 + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz", 12842 + "integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==" 12584 12843 }, 12585 12844 "node_modules/inngest": { 12586 12845 "version": "3.40.1", ··· 12741 13000 } 12742 13001 }, 12743 13002 "node_modules/ipaddr.js": { 12744 - "version": "2.3.0", 12745 - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.3.0.tgz", 12746 - "integrity": "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==", 13003 + "version": "2.2.0", 13004 + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", 13005 + "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", 12747 13006 "license": "MIT", 12748 13007 "engines": { 12749 13008 "node": ">= 10" ··· 13914 14173 } 13915 14174 }, 13916 14175 "node_modules/lru-cache": { 13917 - "version": "10.4.3", 13918 - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", 13919 - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", 13920 - "license": "ISC" 14176 + "version": "10.2.2", 14177 + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", 14178 + "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", 14179 + "engines": { 14180 + "node": "14 || >=16.14" 14181 + } 13921 14182 }, 13922 14183 "node_modules/lru-queue": { 13923 14184 "version": "0.1.0", ··· 15021 15282 ] 15022 15283 }, 15023 15284 "node_modules/micromatch": { 15024 - "version": "4.0.8", 15025 - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", 15026 - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", 15285 + "version": "4.0.7", 15286 + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", 15287 + "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", 15027 15288 "dev": true, 15028 - "license": "MIT", 15029 15289 "dependencies": { 15030 15290 "braces": "^3.0.3", 15031 15291 "picomatch": "^2.3.1" ··· 15092 15352 } 15093 15353 }, 15094 15354 "node_modules/miniflare/node_modules/undici": { 15095 - "version": "5.29.0", 15096 - "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", 15097 - "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", 15355 + "version": "5.28.4", 15356 + "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz", 15357 + "integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==", 15098 15358 "dev": true, 15099 - "license": "MIT", 15100 15359 "dependencies": { 15101 15360 "@fastify/busboy": "^2.0.0" 15102 15361 }, ··· 15190 15449 "license": "MIT" 15191 15450 }, 15192 15451 "node_modules/multiformats": { 15193 - "version": "13.4.2", 15194 - "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-13.4.2.tgz", 15195 - "integrity": "sha512-eh6eHCrRi1+POZ3dA+Dq1C6jhP1GNtr9CRINMb67OKzqW9I5DUuZM/3jLPlzhgpGeiNUlEGEbkCYChXMCc/8DQ==", 15452 + "version": "13.3.2", 15453 + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-13.3.2.tgz", 15454 + "integrity": "sha512-qbB0CQDt3QKfiAzZ5ZYjLFOs+zW43vA4uyM8g27PeEuXZybUOFyjrVdP93HPBHMoglibwfkdVwbzfUq8qGcH6g==", 15196 15455 "license": "Apache-2.0 OR MIT" 15197 15456 }, 15198 15457 "node_modules/mustache": { ··· 15752 16011 "version": "1.0.1", 15753 16012 "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", 15754 16013 "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", 15755 - "dev": true, 15756 16014 "license": "MIT" 15757 16015 }, 15758 16016 "node_modules/path-exists": { ··· 15779 16037 "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" 15780 16038 }, 15781 16039 "node_modules/path-to-regexp": { 15782 - "version": "6.3.0", 15783 - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", 15784 - "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", 15785 - "dev": true, 15786 - "license": "MIT" 16040 + "version": "6.2.2", 16041 + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.2.tgz", 16042 + "integrity": "sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==", 16043 + "dev": true 15787 16044 }, 15788 16045 "node_modules/pg": { 15789 16046 "version": "8.16.3", ··· 16035 16292 "version": "3.2.5", 16036 16293 "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", 16037 16294 "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", 16038 - "dev": true, 16039 16295 "bin": { 16040 16296 "prettier": "bin/prettier.cjs" 16041 16297 }, ··· 16254 16510 "prosemirror-view": "^1.37.2" 16255 16511 } 16256 16512 }, 16513 + "node_modules/prosemirror-tables/node_modules/prosemirror-view": { 16514 + "version": "1.39.2", 16515 + "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.39.2.tgz", 16516 + "integrity": "sha512-BmOkml0QWNob165gyUxXi5K5CVUgVPpqMEAAml/qzgKn9boLUWVPzQ6LtzXw8Cn1GtRQX4ELumPxqtLTDaAKtg==", 16517 + "license": "MIT", 16518 + "peer": true, 16519 + "dependencies": { 16520 + "prosemirror-model": "^1.20.0", 16521 + "prosemirror-state": "^1.0.0", 16522 + "prosemirror-transform": "^1.1.0" 16523 + } 16524 + }, 16257 16525 "node_modules/prosemirror-trailing-node": { 16258 16526 "version": "3.0.0", 16259 16527 "resolved": "https://registry.npmjs.org/prosemirror-trailing-node/-/prosemirror-trailing-node-3.0.0.tgz", ··· 16280 16548 } 16281 16549 }, 16282 16550 "node_modules/prosemirror-view": { 16283 - "version": "1.41.5", 16284 - "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.5.tgz", 16285 - "integrity": "sha512-UDQbIPnDrjE8tqUBbPmCOZgtd75htE6W3r0JCmY9bL6W1iemDM37MZEKC49d+tdQ0v/CKx4gjxLoLsfkD2NiZA==", 16551 + "version": "1.37.1", 16552 + "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.37.1.tgz", 16553 + "integrity": "sha512-MEAnjOdXU1InxEmhjgmEzQAikaS6lF3hD64MveTPpjOGNTl87iRLA1HupC/DEV6YuK7m4Q9DHFNTjwIVtqz5NA==", 16286 16554 "license": "MIT", 16287 16555 "dependencies": { 16288 16556 "prosemirror-model": "^1.20.0", ··· 16361 16629 } 16362 16630 }, 16363 16631 "node_modules/qs": { 16364 - "version": "6.13.0", 16365 - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", 16366 - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", 16632 + "version": "6.13.1", 16633 + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.1.tgz", 16634 + "integrity": "sha512-EJPeIn0CYrGu+hli1xilKAPXODtJ12T0sP63Ijx2/khC2JtuaN3JyNIpvmnkmaEtha9ocbG4A4cMcr+TvqvwQg==", 16367 16635 "license": "BSD-3-Clause", 16368 16636 "dependencies": { 16369 16637 "side-channel": "^1.0.6" ··· 17103 17371 } 17104 17372 }, 17105 17373 "node_modules/resolve": { 17106 - "version": "1.22.11", 17107 - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", 17108 - "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", 17109 - "license": "MIT", 17374 + "version": "1.22.8", 17375 + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", 17376 + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", 17110 17377 "dependencies": { 17111 - "is-core-module": "^2.16.1", 17378 + "is-core-module": "^2.13.0", 17112 17379 "path-parse": "^1.0.7", 17113 17380 "supports-preserve-symlinks-flag": "^1.0.0" 17114 17381 }, 17115 17382 "bin": { 17116 17383 "resolve": "bin/resolve" 17117 - }, 17118 - "engines": { 17119 - "node": ">= 0.4" 17120 17384 }, 17121 17385 "funding": { 17122 17386 "url": "https://github.com/sponsors/ljharb" ··· 17340 17604 } 17341 17605 }, 17342 17606 "node_modules/semver": { 17343 - "version": "7.7.3", 17344 - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", 17345 - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", 17607 + "version": "7.7.2", 17608 + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", 17609 + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", 17346 17610 "license": "ISC", 17347 17611 "bin": { 17348 17612 "semver": "bin/semver.js" ··· 17971 18235 } 17972 18236 }, 17973 18237 "node_modules/style-to-object": { 17974 - "version": "1.0.14", 17975 - "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", 17976 - "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", 17977 - "license": "MIT", 18238 + "version": "1.0.8", 18239 + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.8.tgz", 18240 + "integrity": "sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g==", 17978 18241 "dependencies": { 17979 - "inline-style-parser": "0.2.7" 18242 + "inline-style-parser": "0.2.4" 17980 18243 } 17981 18244 }, 17982 18245 "node_modules/styled-jsx": { ··· 18132 18395 "version": "0.2.15", 18133 18396 "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", 18134 18397 "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", 18135 - "dev": true, 18136 18398 "license": "MIT", 18137 18399 "dependencies": { 18138 18400 "fdir": "^6.5.0", ··· 18149 18411 "version": "6.5.0", 18150 18412 "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", 18151 18413 "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", 18152 - "dev": true, 18153 18414 "license": "MIT", 18154 18415 "engines": { 18155 18416 "node": ">=12.0.0" ··· 18167 18428 "version": "4.0.3", 18168 18429 "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", 18169 18430 "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", 18170 - "dev": true, 18171 18431 "license": "MIT", 18172 18432 "engines": { 18173 18433 "node": ">=12" ··· 18545 18805 } 18546 18806 }, 18547 18807 "node_modules/undici": { 18548 - "version": "6.23.0", 18549 - "resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz", 18550 - "integrity": "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==", 18808 + "version": "6.21.3", 18809 + "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz", 18810 + "integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==", 18551 18811 "license": "MIT", 18552 18812 "engines": { 18553 18813 "node": ">=18.17" ··· 19545 19805 } 19546 19806 }, 19547 19807 "node_modules/ws": { 19548 - "version": "8.19.0", 19549 - "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", 19550 - "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", 19551 - "license": "MIT", 19808 + "version": "8.17.0", 19809 + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz", 19810 + "integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==", 19552 19811 "engines": { 19553 19812 "node": ">=10.0.0" 19554 19813 }, ··· 19736 19995 } 19737 19996 }, 19738 19997 "node_modules/zod": { 19739 - "version": "3.25.76", 19740 - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", 19741 - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", 19742 - "license": "MIT", 19998 + "version": "3.23.8", 19999 + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", 20000 + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", 19743 20001 "funding": { 19744 20002 "url": "https://github.com/sponsors/colinhacks" 19745 20003 }
+3 -4
package.json
··· 23 23 "@atproto/api": "^0.16.9", 24 24 "@atproto/common": "^0.4.8", 25 25 "@atproto/identity": "^0.4.6", 26 - "@atproto/lexicon": "^0.6.1", 26 + "@atproto/lexicon": "^0.5.1", 27 27 "@atproto/oauth-client-node": "^0.3.8", 28 28 "@atproto/sync": "^0.1.34", 29 29 "@atproto/syntax": "^0.3.3", 30 + "@atproto/tap": "^0.1.1", 30 31 "@atproto/xrpc": "^0.7.5", 31 32 "@atproto/xrpc-server": "^0.9.5", 32 33 "@hono/node-server": "^1.14.3", ··· 48 49 "@vercel/analytics": "^1.5.0", 49 50 "@vercel/functions": "^2.2.12", 50 51 "@vercel/sdk": "^1.11.4", 51 - "@yornaath/batshit": "^0.14.0", 52 52 "babel-plugin-react-compiler": "^19.1.0-rc.1", 53 53 "base64-js": "^1.5.1", 54 54 "colorjs.io": "^0.5.2", ··· 124 124 "ajv": "^8.17.1", 125 125 "whatwg-url": "^14.0.0", 126 126 "@types/react": "19.2.6", 127 - "@types/react-dom": "19.2.3", 128 - "@atproto/lexicon": "^0.6.1" 127 + "@types/react-dom": "19.2.3" 129 128 } 130 129 }
+244
specs/2026-02-03-pro-tier.md
··· 1 + # Pro Tier Subscription System 2 + 3 + **Status**: draft 4 + 5 + ## Goal 6 + 7 + Add a paid Pro tier to Leaflet using Stripe for billing, with an entitlements-based architecture that decouples feature access from subscription state. 8 + 9 + ## Design 10 + 11 + ### Data Model 12 + 13 + Two new tables separate Stripe subscription state from feature access: 14 + 15 + **`user_subscriptions`** โ€” Stripe sync state 16 + - `identity_id` (UUID, FK to identities, PK) 17 + - `stripe_customer_id` (text, unique) 18 + - `stripe_subscription_id` (text, unique, nullable) 19 + - `plan` (text) โ€” Price ID from `stripe/products.ts`, e.g., `pro_monthly_v1_usd` 20 + - `status` (text) โ€” mirrors Stripe: `trialing`, `active`, `past_due`, `canceled`, `unpaid` 21 + - `current_period_end` (timestamp) 22 + - `created_at`, `updated_at` 23 + 24 + **`user_entitlements`** โ€” Feature access grants 25 + - `identity_id` (UUID, FK to identities) 26 + - `entitlement_key` (text) โ€” e.g., `analytics` 27 + - `granted_at` (timestamp) 28 + - `expires_at` (timestamp, nullable) โ€” null means permanent 29 + - `source` (text) โ€” provenance, e.g., `stripe:sub_xxx`, `manual:admin`, `promo:launch2026` 30 + - `metadata` (jsonb, nullable) โ€” for limits or additional config 31 + - Primary key: `(identity_id, entitlement_key)` โ€” one entitlement per key per user 32 + 33 + The unique constraint on `(identity_id, entitlement_key)` means writes are upserts. The most recent write wins; `source` tracks provenance. 34 + 35 + ### SKU โ†’ Entitlements Mapping 36 + 37 + Entitlements for each Stripe Product are stored in Stripe's product metadata, not locally. Example product metadata: 38 + 39 + ```json 40 + { 41 + "entitlements": "{\"analytics\": true}" 42 + } 43 + ``` 44 + 45 + This keeps Stripe as the source of truth for what each SKU grants. 46 + 47 + ### Stripe Product Sync 48 + 49 + Products and prices are defined in code and synced to Stripe via a GitHub Action. This provides version control, reproducibility, and enforces immutability. 50 + 51 + **Directory structure:** 52 + ``` 53 + stripe/ 54 + โ”œโ”€โ”€ products.ts # Product/price definitions with entitlement metadata 55 + โ””โ”€โ”€ sync.ts # Script that ensures Stripe matches definitions 56 + ``` 57 + 58 + **Product definition format** (`stripe/products.ts`): 59 + ```typescript 60 + export const products = [ 61 + { 62 + id: "pro_monthly_v1", 63 + name: "Leaflet Pro (Monthly)", 64 + prices: [ 65 + { 66 + id: "pro_monthly_v1_usd", 67 + currency: "usd", 68 + unit_amount: 900, // $9.00 69 + recurring: { interval: "month" }, 70 + }, 71 + ], 72 + metadata: { 73 + entitlements: JSON.stringify({ publication_analytics: true }), 74 + }, 75 + }, 76 + ]; 77 + ``` 78 + 79 + **Sync script behavior** (`stripe/sync.ts`): 80 + 1. Fetch existing products and prices by `id` (`stripe.products.retrieve(id)`, `stripe.prices.retrieve(id)`) 81 + 2. For each definition: 82 + - If missing in Stripe โ†’ create it with the custom `id` 83 + - If exists and matches โ†’ no-op 84 + - If exists but differs โ†’ update it to match the definition 85 + 3. Never deletes products/prices 86 + 87 + Both products and prices use custom `id` for idempotent matching (set at creation). Same definitions apply to both test and live modeโ€”the script targets whichever mode the API key belongs to. 88 + 89 + **GitHub Action** (`.github/workflows/stripe-sync.yml`): 90 + - Triggers on push to `stripe/` directory 91 + - Runs sync against test mode automatically 92 + - Live mode sync requires manual workflow dispatch with approval 93 + 94 + **Versioning/grandfathering:** 95 + To grandfather existing subscribers on different terms, create a new product with a new `id` (e.g., `pro_monthly_v2`). Existing subscribers stay on v1. New subscribers get v2. For additive changes (new entitlements, metadata updates), just update the existing product definition. 96 + 97 + ### Stripe Sync Strategy 98 + 99 + **Webhook-driven** with **optimistic updates**: 100 + 101 + 1. **Optimistic**: After successful Checkout Session completion on the client, immediately call a server action to write `user_subscriptions` and `user_entitlements` based on the session data. User gets instant access. 102 + 103 + 2. **Durable**: Stripe webhooks confirm and reconcile state. Handles edge cases (payment failures, disputes, subscription updates from Stripe dashboard). 104 + 105 + Webhooks to handle: 106 + - `checkout.session.completed` โ€” initial subscription created 107 + - `customer.subscription.created` โ€” backup for subscription creation 108 + - `customer.subscription.updated` โ€” plan changes, renewals, status changes 109 + - `customer.subscription.deleted` โ€” subscription ended 110 + - `invoice.payment_failed` โ€” payment issues 111 + - `customer.subscription.trial_will_end` โ€” trial ending reminder (optional, for notifications) 112 + 113 + ### Entitlement Lifecycle 114 + 115 + **Grant flow** (subscription activates or trial starts): 116 + 1. Webhook receives event with subscription and product data 117 + 2. Parse `entitlements` from product metadata 118 + 3. Upsert `user_entitlements` rows with `expires_at` = `current_period_end`, `source` = `stripe:{subscription_id}` 119 + 120 + **Renewal flow** (subscription renews): 121 + 1. `customer.subscription.updated` webhook fires 122 + 2. Update `expires_at` on all entitlements with matching `source` 123 + 124 + **Cancellation flow** (user cancels but period remains): 125 + 1. `customer.subscription.updated` with `cancel_at_period_end: true` 126 + 2. Update `user_subscriptions.status` 127 + 3. Entitlements remain valid until `expires_at` (already set to period end) 128 + 129 + **Expiration flow** (subscription ends): 130 + 1. `customer.subscription.deleted` webhook fires 131 + 2. No action needed on `user_entitlements` โ€” they naturally expire via `expires_at` 132 + 3. Update `user_subscriptions.status` to `canceled` 133 + 134 + Soft expiration via `expires_at` preserves audit history and handles the canceled-but-paid-through-period case without additional webhooks. 135 + 136 + ### Trials 137 + 138 + Stripe-managed trials: 139 + 1. Trial starts โ†’ subscription created with `status: trialing` 140 + 2. Entitlements granted with `expires_at` = trial end date 141 + 3. Trial converts โ†’ `expires_at` updated to subscription period end 142 + 4. Trial lapses โ†’ entitlements expire naturally, no action needed 143 + 144 + ### Entitlement Checks 145 + 146 + Extend `getIdentityData()` to join against `user_entitlements` and return active entitlements: 147 + 148 + ```typescript 149 + type IdentityData = { 150 + id: string; 151 + email: string; 152 + atp_did: string; 153 + // ... existing fields 154 + entitlements: Record<string, { 155 + granted_at: string; 156 + expires_at: string | null; 157 + source: string; 158 + metadata: Record<string, unknown> | null; 159 + }>; 160 + }; 161 + ``` 162 + 163 + Query filters to `expires_at IS NULL OR expires_at > NOW()`. The `useIdentityData()` hook exposes the same shape client-side. 164 + 165 + Feature gating in server actions: 166 + 167 + ```typescript 168 + export async function getAnalyticsData() { 169 + const identity = await getIdentityData(); 170 + if (!identity?.entitlements.analytics) { 171 + return err("Pro subscription required"); 172 + } 173 + // ... fetch analytics 174 + } 175 + ``` 176 + 177 + ### Initial Pro Features 178 + 179 + Single entitlement at launch: 180 + - `publication_analytics` (boolean) โ€” access to analytics features for 181 + publications 182 + 183 + ### Webhook Endpoint 184 + 185 + New API route at `app/api/webhooks/stripe/route.ts`: 186 + - Verify Stripe signature using `STRIPE_WEBHOOK_SECRET` 187 + - Dispatch to Inngest functions for processing (keeps webhook response fast, enables retries) 188 + 189 + ### Inngest Functions 190 + 191 + New functions in `app/api/inngest/functions/`: 192 + - `stripe/handle-checkout-completed` โ€” process successful checkout 193 + - `stripe/handle-subscription-updated` โ€” sync subscription changes to entitlements 194 + - `stripe/handle-subscription-deleted` โ€” update subscription status 195 + 196 + Events to add to Inngest client: 197 + ```typescript 198 + "stripe/checkout.session.completed": { data: { sessionId: string } } 199 + "stripe/customer.subscription.updated": { data: { subscriptionId: string } } 200 + "stripe/customer.subscription.deleted": { data: { subscriptionId: string } } 201 + ``` 202 + 203 + ### Environment Variables 204 + 205 + ``` 206 + STRIPE_SECRET_KEY=sk_... 207 + STRIPE_WEBHOOK_SECRET=whsec_... 208 + ``` 209 + 210 + Price IDs are defined in `stripe/products.ts` and imported directly โ€” no env var needed. 211 + 212 + ## Open Questions 213 + 214 + - **UI**: Where upgrade prompts appear, pricing page design, checkout flow UX โ€” to be designed separately 215 + - **Multiple plans**: Current design supports one subscription per user. If we add team plans or multiple concurrent subscriptions, `user_subscriptions` would need to become one-to-many 216 + 217 + ## Implementation 218 + 219 + 1. **Add Stripe packages**: Install `stripe` npm package 220 + 221 + 2. **Database migration**: Create `user_subscriptions` and `user_entitlements` tables with indexes on `identity_id` and `expires_at` 222 + 223 + 3. **Drizzle schema**: Add table definitions to `drizzle/schema.ts` 224 + 225 + 4. **Environment setup**: Add Stripe env vars to `.env.local` and deployment config 226 + 227 + 5. **Stripe webhook endpoint**: Create `app/api/webhooks/stripe/route.ts` with signature verification, dispatch events to Inngest 228 + 229 + 6. **Inngest events and functions**: Add event types to `app/api/inngest/client.ts`, create handler functions for checkout completed, subscription updated, subscription deleted 230 + 231 + 7. **Extend getIdentityData**: Join against `user_entitlements`, filter expired, return entitlements object 232 + 233 + 8. **Extend useIdentityData**: Ensure client-side hook receives entitlements from server 234 + 235 + 9. **Optimistic update action**: Create `actions/subscriptions/activateSubscription.ts` for immediate access after checkout 236 + 237 + 10. **Stripe sync infrastructure**: 238 + - Create `stripe/products.ts` with Pro product definitions and entitlement metadata 239 + - Create `stripe/sync.ts` script using Stripe Node SDK to reconcile definitions with Stripe 240 + - Add `.github/workflows/stripe-sync.yml` for automated test mode sync on push, manual live mode sync 241 + 242 + 11. **Initial product sync**: Run sync script against both test and live mode to create Pro products 243 + 244 + 12. **Gate analytics**: Add entitlement check to analytics server actions
+4 -59
src/notifications.ts
··· 27 27 | { type: "mention"; document_uri: string; mention_type: "document"; mentioned_uri: string } 28 28 | { type: "comment_mention"; comment_uri: string; mention_type: "did" } 29 29 | { type: "comment_mention"; comment_uri: string; mention_type: "publication"; mentioned_uri: string } 30 - | { type: "comment_mention"; comment_uri: string; mention_type: "document"; mentioned_uri: string } 31 - | { type: "recommend"; document_uri: string; recommend_uri: string }; 30 + | { type: "comment_mention"; comment_uri: string; mention_type: "document"; mentioned_uri: string }; 32 31 33 32 export type HydratedNotification = 34 33 | HydratedCommentNotification ··· 36 35 | HydratedQuoteNotification 37 36 | HydratedBskyPostEmbedNotification 38 37 | HydratedMentionNotification 39 - | HydratedCommentMentionNotification 40 - | HydratedRecommendNotification; 38 + | HydratedCommentMentionNotification; 41 39 export async function hydrateNotifications( 42 40 notifications: NotificationRow[], 43 41 ): Promise<Array<HydratedNotification>> { 44 42 // Call all hydrators in parallel 45 - const [commentNotifications, subscribeNotifications, quoteNotifications, bskyPostEmbedNotifications, mentionNotifications, commentMentionNotifications, recommendNotifications] = await Promise.all([ 43 + const [commentNotifications, subscribeNotifications, quoteNotifications, bskyPostEmbedNotifications, mentionNotifications, commentMentionNotifications] = await Promise.all([ 46 44 hydrateCommentNotifications(notifications), 47 45 hydrateSubscribeNotifications(notifications), 48 46 hydrateQuoteNotifications(notifications), 49 47 hydrateBskyPostEmbedNotifications(notifications), 50 48 hydrateMentionNotifications(notifications), 51 49 hydrateCommentMentionNotifications(notifications), 52 - hydrateRecommendNotifications(notifications), 53 50 ]); 54 51 55 52 // Combine all hydrated notifications 56 - const allHydrated = [...commentNotifications, ...subscribeNotifications, ...quoteNotifications, ...bskyPostEmbedNotifications, ...mentionNotifications, ...commentMentionNotifications, ...recommendNotifications]; 53 + const allHydrated = [...commentNotifications, ...subscribeNotifications, ...quoteNotifications, ...bskyPostEmbedNotifications, ...mentionNotifications, ...commentMentionNotifications]; 57 54 58 55 // Sort by created_at to maintain order 59 56 allHydrated.sort( ··· 517 514 ), 518 515 normalizedMentionedPublication: normalizePublicationRecord(mentionedPublication?.record), 519 516 normalizedMentionedDocument: normalizeDocumentRecord(mentionedDoc?.data, mentionedDoc?.uri), 520 - }; 521 - }) 522 - .filter((n) => n !== null); 523 - } 524 - 525 - export type HydratedRecommendNotification = Awaited< 526 - ReturnType<typeof hydrateRecommendNotifications> 527 - >[0]; 528 - 529 - async function hydrateRecommendNotifications(notifications: NotificationRow[]) { 530 - const recommendNotifications = notifications.filter( 531 - (n): n is NotificationRow & { data: ExtractNotificationType<"recommend"> } => 532 - (n.data as NotificationData)?.type === "recommend", 533 - ); 534 - 535 - if (recommendNotifications.length === 0) { 536 - return []; 537 - } 538 - 539 - // Fetch recommend data from the database 540 - const recommendUris = recommendNotifications.map((n) => n.data.recommend_uri); 541 - const documentUris = recommendNotifications.map((n) => n.data.document_uri); 542 - 543 - const [{ data: recommends }, { data: documents }] = await Promise.all([ 544 - supabaseServerClient 545 - .from("recommends_on_documents") 546 - .select("*, identities(bsky_profiles(*))") 547 - .in("uri", recommendUris), 548 - supabaseServerClient 549 - .from("documents") 550 - .select("*, documents_in_publications(publications(*))") 551 - .in("uri", documentUris), 552 - ]); 553 - 554 - return recommendNotifications 555 - .map((notification) => { 556 - const recommendData = recommends?.find((r) => r.uri === notification.data.recommend_uri); 557 - const document = documents?.find((d) => d.uri === notification.data.document_uri); 558 - if (!recommendData || !document) return null; 559 - return { 560 - id: notification.id, 561 - recipient: notification.recipient, 562 - created_at: notification.created_at, 563 - type: "recommend" as const, 564 - recommend_uri: notification.data.recommend_uri, 565 - document_uri: notification.data.document_uri, 566 - recommendData, 567 - document, 568 - normalizedDocument: normalizeDocumentRecord(document.data, document.uri), 569 - normalizedPublication: normalizePublicationRecord( 570 - document.documents_in_publications[0]?.publications?.record, 571 - ), 572 517 }; 573 518 }) 574 519 .filter((n) => n !== null);
-14
src/replicache/cachedServerMutationContext.ts
··· 155 155 }); 156 156 }, 157 157 async retractFact(factID) { 158 - let cachedFact = writeCache.find( 159 - (f) => f.type === "put" && f.fact.id === factID, 160 - ); 161 - let entity: string | undefined; 162 - if (cachedFact && cachedFact.type === "put") { 163 - entity = cachedFact.fact.entity; 164 - } else { 165 - let [row] = await tx 166 - .select({ entity: facts.entity }) 167 - .from(facts) 168 - .where(driz.eq(facts.id, factID)); 169 - entity = row?.entity; 170 - } 171 - if (!entity || !(await this.checkPermission(entity))) return; 172 158 writeCache = writeCache.filter((f) => f.fact.id !== factID); 173 159 writeCache.push({ type: "del", fact: { id: factID } }); 174 160 },
-13
src/replicache/mutations.ts
··· 659 659 tags?: string[]; 660 660 cover_image?: string | null; 661 661 localPublishedAt?: string | null; 662 - preferences?: { 663 - showComments?: boolean; 664 - showMentions?: boolean; 665 - showRecommends?: boolean; 666 - } | null; 667 662 }> = async (args, ctx) => { 668 663 await ctx.runOnServer(async (serverCtx) => { 669 664 console.log("updating"); ··· 672 667 title?: string; 673 668 tags?: string[]; 674 669 cover_image?: string | null; 675 - preferences?: { 676 - showComments?: boolean; 677 - showMentions?: boolean; 678 - showRecommends?: boolean; 679 - } | null; 680 670 } = {}; 681 671 if (args.description !== undefined) updates.description = args.description; 682 672 if (args.title !== undefined) updates.title = args.title; 683 673 if (args.tags !== undefined) updates.tags = args.tags; 684 674 if (args.cover_image !== undefined) updates.cover_image = args.cover_image; 685 - if (args.preferences !== undefined) updates.preferences = args.preferences; 686 675 687 676 if (Object.keys(updates).length > 0) { 688 677 // First try to update leaflets_in_publications (for publications) ··· 711 700 await tx.set("publication_cover_image", args.cover_image); 712 701 if (args.localPublishedAt !== undefined) 713 702 await tx.set("publication_local_published_at", args.localPublishedAt); 714 - if (args.preferences !== undefined) 715 - await tx.set("post_preferences", args.preferences); 716 703 }); 717 704 }; 718 705
+1 -10
src/utils/getBlocksAsHTML.tsx
··· 79 79 mailbox: async () => null, 80 80 poll: async () => null, 81 81 embed: async () => null, 82 - "bluesky-post": async (b, tx) => { 83 - let [post] = await scanIndex(tx).eav(b.value, "block/bluesky-post"); 84 - if (!post) return null; 85 - return ( 86 - <div 87 - data-type="bluesky-post" 88 - data-bluesky-post={JSON.stringify(post.data.value)} 89 - /> 90 - ); 91 - }, 82 + "bluesky-post": async () => null, 92 83 math: async (b, tx, a) => { 93 84 let [math] = await scanIndex(tx).eav(b.value, "block/math"); 94 85 const html = Katex.renderToString(math?.data.value || "", {
+5 -2
src/utils/mentionUtils.ts
··· 19 19 const uri = new AtUri(atUri); 20 20 21 21 if (isPublicationCollection(uri.collection)) { 22 - return `/lish/uri/${encodeURIComponent(atUri)}`; 22 + // Publication URL: /lish/{did}/{rkey} 23 + return `/lish/${uri.host}/${uri.rkey}`; 23 24 } else if (isDocumentCollection(uri.collection)) { 25 + // Document URL - we need to resolve this via the API 26 + // For now, create a redirect route that will handle it 24 27 return `/lish/uri/${encodeURIComponent(atUri)}`; 25 28 } 26 29 ··· 39 42 export function handleMentionClick( 40 43 e: MouseEvent | React.MouseEvent, 41 44 type: "did" | "at-uri", 42 - value: string, 45 + value: string 43 46 ) { 44 47 e.preventDefault(); 45 48 e.stopPropagation();
-24
src/utils/mergePreferences.ts
··· 1 - type PreferencesInput = { 2 - showComments?: boolean; 3 - showMentions?: boolean; 4 - showRecommends?: boolean; 5 - showPrevNext?: boolean; 6 - } | null; 7 - 8 - export function mergePreferences( 9 - documentPrefs?: PreferencesInput, 10 - publicationPrefs?: PreferencesInput, 11 - ): { 12 - showComments?: boolean; 13 - showMentions?: boolean; 14 - showRecommends?: boolean; 15 - showPrevNext?: boolean; 16 - } { 17 - return { 18 - showComments: documentPrefs?.showComments ?? publicationPrefs?.showComments, 19 - showMentions: documentPrefs?.showMentions ?? publicationPrefs?.showMentions, 20 - showRecommends: 21 - documentPrefs?.showRecommends ?? publicationPrefs?.showRecommends, 22 - showPrevNext: publicationPrefs?.showPrevNext, 23 - }; 24 - }
-17
supabase/database.types.ts
··· 335 335 } 336 336 documents: { 337 337 Row: { 338 - bsky_like_count: number 339 338 data: Json 340 - indexed: boolean 341 339 indexed_at: string 342 - recommend_count: number 343 340 sort_date: string 344 341 uri: string 345 342 } 346 343 Insert: { 347 - bsky_like_count?: number 348 344 data: Json 349 - indexed?: boolean 350 345 indexed_at?: string 351 - recommend_count?: number 352 - sort_date?: string 353 346 uri: string 354 347 } 355 348 Update: { 356 - bsky_like_count?: number 357 349 data?: Json 358 - indexed?: boolean 359 350 indexed_at?: string 360 - recommend_count?: number 361 - sort_date?: string 362 351 uri?: string 363 352 } 364 353 Relationships: [] ··· 600 589 description: string 601 590 doc: string | null 602 591 leaflet: string 603 - preferences: Json | null 604 592 publication: string 605 593 tags: string[] | null 606 594 title: string ··· 611 599 description?: string 612 600 doc?: string | null 613 601 leaflet: string 614 - preferences?: Json | null 615 602 publication: string 616 603 tags?: string[] | null 617 604 title?: string ··· 622 609 description?: string 623 610 doc?: string | null 624 611 leaflet?: string 625 - preferences?: Json | null 626 612 publication?: string 627 613 tags?: string[] | null 628 614 title?: string ··· 659 645 description: string 660 646 document: string 661 647 leaflet: string 662 - preferences: Json | null 663 648 tags: string[] | null 664 649 title: string 665 650 } ··· 670 655 description?: string 671 656 document: string 672 657 leaflet: string 673 - preferences?: Json | null 674 658 tags?: string[] | null 675 659 title?: string 676 660 } ··· 681 665 description?: string 682 666 document?: string 683 667 leaflet?: string 684 - preferences?: Json | null 685 668 tags?: string[] | null 686 669 title?: string 687 670 }
-2
supabase/migrations/20260208000000_add_preferences_to_drafts.sql
··· 1 - ALTER TABLE leaflets_in_publications ADD COLUMN preferences jsonb; 2 - ALTER TABLE leaflets_to_documents ADD COLUMN preferences jsonb;
-1
supabase/migrations/20260209000000_add_bsky_like_count.sql
··· 1 - ALTER TABLE documents ADD COLUMN bsky_like_count integer NOT NULL DEFAULT 0;
-23
supabase/migrations/20260210000000_add_recommend_count.sql
··· 1 - ALTER TABLE documents ADD COLUMN recommend_count integer NOT NULL DEFAULT 0; 2 - 3 - UPDATE documents d 4 - SET recommend_count = ( 5 - SELECT COUNT(*) FROM recommends_on_documents r WHERE r.document = d.uri 6 - ); 7 - 8 - CREATE OR REPLACE FUNCTION update_recommend_count() RETURNS trigger AS $$ 9 - BEGIN 10 - IF TG_OP = 'INSERT' THEN 11 - UPDATE documents SET recommend_count = recommend_count + 1 12 - WHERE uri = NEW.document; 13 - ELSIF TG_OP = 'DELETE' THEN 14 - UPDATE documents SET recommend_count = recommend_count - 1 15 - WHERE uri = OLD.document; 16 - END IF; 17 - RETURN NULL; 18 - END; 19 - $$ LANGUAGE plpgsql; 20 - 21 - CREATE TRIGGER trg_recommend_count 22 - AFTER INSERT OR DELETE ON recommends_on_documents 23 - FOR EACH ROW EXECUTE FUNCTION update_recommend_count();
-6
supabase/migrations/20260210100000_add_indexed_column_and_ranking_index.sql
··· 1 - ALTER TABLE documents ADD COLUMN indexed boolean NOT NULL DEFAULT true; 2 - 3 - CREATE INDEX idx_documents_ranking 4 - ON documents (sort_date DESC) 5 - INCLUDE (uri, bsky_like_count, recommend_count) 6 - WHERE indexed = true;
-1
supabase/migrations/20260211000000_indexed_default_false.sql
··· 1 - ALTER TABLE documents ALTER COLUMN indexed SET DEFAULT false;