diff --git a/api/README.md b/api/README.md index c2aeecf..beeeef0 100644 --- a/api/README.md +++ b/api/README.md @@ -11,7 +11,7 @@ https://api.atmo.watch/lexicons https://api.atmo.watch/status ``` -Collection reads, profiles, and `watch.atmo.getCursor` are anonymous. Review responses include materialized `likesCount` and `commentsCount`; likes count distinct actors, while comments count records whose `subjectUri` is the review. Like and comment endpoints can also filter by subject. `watch.atmo.notifyOfUpdate` uses AT Protocol service auth with audience `did:web:api.atmo.watch`. +Collection reads, profiles, and `watch.atmo.getCursor` are anonymous. Profile responses include both Bluesky and Popfeed profiles when available. Review responses include materialized `likesCount` and `commentsCount`. List responses include `itemsCount`, `likesCount`, and `commentsCount`, plus optional relation hydration. Likes count distinct actors, while comments count records whose `subjectUri` is the review or list. List items can be filtered by list URI, media identifiers, work type, list type, status, and date. `watch.atmo.notifyOfUpdate` uses AT Protocol service auth with audience `did:web:api.atmo.watch`. ## Initial Cloudflare setup diff --git a/api/lex.config.js b/api/lex.config.js index a7d9c5e..0bc3545 100644 --- a/api/lex.config.js +++ b/api/lex.config.js @@ -21,6 +21,7 @@ export default defineLexiconConfig({ "app.bsky.actor.profile", "com.atproto.label.defs", "com.atproto.repo.strongRef", + "social.popfeed.actor.profile", "social.popfeed.feed.comment", "social.popfeed.feed.like", "social.popfeed.feed.list", diff --git a/api/lexicons/generated/watch/atmo/comment/getRecord.json b/api/lexicons/generated/watch/atmo/comment/getRecord.json index 4e8a729..f7e9183 100644 --- a/api/lexicons/generated/watch/atmo/comment/getRecord.json +++ b/api/lexicons/generated/watch/atmo/comment/getRecord.json @@ -94,8 +94,11 @@ "format": "cid" }, "value": { - "type": "ref", - "ref": "app.bsky.actor.profile#main" + "type": "union", + "refs": [ + "app.bsky.actor.profile#main", + "social.popfeed.actor.profile#main" + ] }, "collection": { "type": "string", diff --git a/api/lexicons/generated/watch/atmo/comment/listRecords.json b/api/lexicons/generated/watch/atmo/comment/listRecords.json index 362b124..1fe1372 100644 --- a/api/lexicons/generated/watch/atmo/comment/listRecords.json +++ b/api/lexicons/generated/watch/atmo/comment/listRecords.json @@ -148,8 +148,11 @@ "format": "cid" }, "value": { - "type": "ref", - "ref": "app.bsky.actor.profile#main" + "type": "union", + "refs": [ + "app.bsky.actor.profile#main", + "social.popfeed.actor.profile#main" + ] }, "collection": { "type": "string", diff --git a/api/lexicons/generated/watch/atmo/getProfile.json b/api/lexicons/generated/watch/atmo/getProfile.json index ebd042a..081509e 100644 --- a/api/lexicons/generated/watch/atmo/getProfile.json +++ b/api/lexicons/generated/watch/atmo/getProfile.json @@ -57,8 +57,11 @@ "format": "cid" }, "value": { - "type": "ref", - "ref": "app.bsky.actor.profile#main" + "type": "union", + "refs": [ + "app.bsky.actor.profile#main", + "social.popfeed.actor.profile#main" + ] }, "collection": { "type": "string", diff --git a/api/lexicons/generated/watch/atmo/like/getRecord.json b/api/lexicons/generated/watch/atmo/like/getRecord.json index e649081..6dd8311 100644 --- a/api/lexicons/generated/watch/atmo/like/getRecord.json +++ b/api/lexicons/generated/watch/atmo/like/getRecord.json @@ -94,8 +94,11 @@ "format": "cid" }, "value": { - "type": "ref", - "ref": "app.bsky.actor.profile#main" + "type": "union", + "refs": [ + "app.bsky.actor.profile#main", + "social.popfeed.actor.profile#main" + ] }, "collection": { "type": "string", diff --git a/api/lexicons/generated/watch/atmo/like/listRecords.json b/api/lexicons/generated/watch/atmo/like/listRecords.json index 2d3f82c..220b134 100644 --- a/api/lexicons/generated/watch/atmo/like/listRecords.json +++ b/api/lexicons/generated/watch/atmo/like/listRecords.json @@ -143,8 +143,11 @@ "format": "cid" }, "value": { - "type": "ref", - "ref": "app.bsky.actor.profile#main" + "type": "union", + "refs": [ + "app.bsky.actor.profile#main", + "social.popfeed.actor.profile#main" + ] }, "collection": { "type": "string", diff --git a/api/lexicons/generated/watch/atmo/list/getRecord.json b/api/lexicons/generated/watch/atmo/list/getRecord.json index 8c0a4f6..4b03196 100644 --- a/api/lexicons/generated/watch/atmo/list/getRecord.json +++ b/api/lexicons/generated/watch/atmo/list/getRecord.json @@ -19,6 +19,24 @@ "profiles": { "type": "boolean", "description": "Include indexed profile and identity information" + }, + "hydrateComments": { + "type": "integer", + "minimum": 1, + "maximum": 50, + "description": "Number of comments records to embed" + }, + "hydrateItems": { + "type": "integer", + "minimum": 1, + "maximum": 50, + "description": "Number of items records to embed" + }, + "hydrateLikes": { + "type": "integer", + "minimum": 1, + "maximum": 50, + "description": "Number of likes records to embed" } } }, @@ -61,6 +79,39 @@ "time_us": { "type": "integer" }, + "commentsCount": { + "type": "integer", + "description": "Total comments count" + }, + "comments": { + "type": "array", + "items": { + "type": "ref", + "ref": "#hydrateCommentsRecord" + } + }, + "itemsCount": { + "type": "integer", + "description": "Total items count" + }, + "items": { + "type": "array", + "items": { + "type": "ref", + "ref": "#hydrateItemsRecord" + } + }, + "likesCount": { + "type": "integer", + "description": "Total likes count" + }, + "likes": { + "type": "array", + "items": { + "type": "ref", + "ref": "#hydrateLikesRecord" + } + }, "profiles": { "type": "array", "items": { @@ -72,6 +123,123 @@ } } }, + "hydrateCommentsRecord": { + "type": "object", + "required": [ + "uri", + "value", + "did", + "collection", + "rkey", + "time_us" + ], + "properties": { + "uri": { + "type": "string", + "format": "at-uri" + }, + "cid": { + "type": "string", + "format": "cid" + }, + "value": { + "type": "ref", + "ref": "social.popfeed.feed.comment#main" + }, + "did": { + "type": "string", + "format": "did" + }, + "collection": { + "type": "string", + "format": "nsid" + }, + "rkey": { + "type": "string" + }, + "time_us": { + "type": "integer" + } + } + }, + "hydrateItemsRecord": { + "type": "object", + "required": [ + "uri", + "value", + "did", + "collection", + "rkey", + "time_us" + ], + "properties": { + "uri": { + "type": "string", + "format": "at-uri" + }, + "cid": { + "type": "string", + "format": "cid" + }, + "value": { + "type": "ref", + "ref": "social.popfeed.feed.listItem#main" + }, + "did": { + "type": "string", + "format": "did" + }, + "collection": { + "type": "string", + "format": "nsid" + }, + "rkey": { + "type": "string" + }, + "time_us": { + "type": "integer" + } + } + }, + "hydrateLikesRecord": { + "type": "object", + "required": [ + "uri", + "value", + "did", + "collection", + "rkey", + "time_us" + ], + "properties": { + "uri": { + "type": "string", + "format": "at-uri" + }, + "cid": { + "type": "string", + "format": "cid" + }, + "value": { + "type": "ref", + "ref": "social.popfeed.feed.like#main" + }, + "did": { + "type": "string", + "format": "did" + }, + "collection": { + "type": "string", + "format": "nsid" + }, + "rkey": { + "type": "string" + }, + "time_us": { + "type": "integer" + } + } + }, "profileEntry": { "type": "object", "required": [ @@ -94,8 +262,11 @@ "format": "cid" }, "value": { - "type": "ref", - "ref": "app.bsky.actor.profile#main" + "type": "union", + "refs": [ + "app.bsky.actor.profile#main", + "social.popfeed.actor.profile#main" + ] }, "collection": { "type": "string", diff --git a/api/lexicons/generated/watch/atmo/list/listRecords.json b/api/lexicons/generated/watch/atmo/list/listRecords.json index 25a287b..73d8953 100644 --- a/api/lexicons/generated/watch/atmo/list/listRecords.json +++ b/api/lexicons/generated/watch/atmo/list/listRecords.json @@ -25,6 +25,67 @@ "profiles": { "type": "boolean", "description": "Include indexed profile and identity information" + }, + "createdAtMin": { + "type": "string", + "description": "Minimum value for createdAt" + }, + "createdAtMax": { + "type": "string", + "description": "Maximum value for createdAt" + }, + "listType": { + "type": "string", + "description": "Filter by listType" + }, + "commentsCountMin": { + "type": "integer", + "description": "Minimum total comments count" + }, + "hydrateComments": { + "type": "integer", + "minimum": 1, + "maximum": 50, + "description": "Number of comments records to embed" + }, + "itemsCountMin": { + "type": "integer", + "description": "Minimum total items count" + }, + "hydrateItems": { + "type": "integer", + "minimum": 1, + "maximum": 50, + "description": "Number of items records to embed" + }, + "likesCountMin": { + "type": "integer", + "description": "Minimum total likes count" + }, + "hydrateLikes": { + "type": "integer", + "minimum": 1, + "maximum": 50, + "description": "Number of likes records to embed" + }, + "sort": { + "type": "string", + "knownValues": [ + "commentsCount", + "createdAt", + "itemsCount", + "likesCount", + "listType" + ], + "description": "Field to sort by (default: time_us)" + }, + "order": { + "type": "string", + "knownValues": [ + "asc", + "desc" + ], + "description": "Sort direction" } } }, @@ -92,6 +153,156 @@ "rkey": { "type": "string" }, + "time_us": { + "type": "integer" + }, + "commentsCount": { + "type": "integer", + "description": "Total comments count" + }, + "comments": { + "type": "array", + "items": { + "type": "ref", + "ref": "#hydrateCommentsRecord" + } + }, + "itemsCount": { + "type": "integer", + "description": "Total items count" + }, + "items": { + "type": "array", + "items": { + "type": "ref", + "ref": "#hydrateItemsRecord" + } + }, + "likesCount": { + "type": "integer", + "description": "Total likes count" + }, + "likes": { + "type": "array", + "items": { + "type": "ref", + "ref": "#hydrateLikesRecord" + } + } + } + }, + "hydrateCommentsRecord": { + "type": "object", + "required": [ + "uri", + "value", + "did", + "collection", + "rkey", + "time_us" + ], + "properties": { + "uri": { + "type": "string", + "format": "at-uri" + }, + "cid": { + "type": "string", + "format": "cid" + }, + "value": { + "type": "ref", + "ref": "social.popfeed.feed.comment#main" + }, + "did": { + "type": "string", + "format": "did" + }, + "collection": { + "type": "string", + "format": "nsid" + }, + "rkey": { + "type": "string" + }, + "time_us": { + "type": "integer" + } + } + }, + "hydrateItemsRecord": { + "type": "object", + "required": [ + "uri", + "value", + "did", + "collection", + "rkey", + "time_us" + ], + "properties": { + "uri": { + "type": "string", + "format": "at-uri" + }, + "cid": { + "type": "string", + "format": "cid" + }, + "value": { + "type": "ref", + "ref": "social.popfeed.feed.listItem#main" + }, + "did": { + "type": "string", + "format": "did" + }, + "collection": { + "type": "string", + "format": "nsid" + }, + "rkey": { + "type": "string" + }, + "time_us": { + "type": "integer" + } + } + }, + "hydrateLikesRecord": { + "type": "object", + "required": [ + "uri", + "value", + "did", + "collection", + "rkey", + "time_us" + ], + "properties": { + "uri": { + "type": "string", + "format": "at-uri" + }, + "cid": { + "type": "string", + "format": "cid" + }, + "value": { + "type": "ref", + "ref": "social.popfeed.feed.like#main" + }, + "did": { + "type": "string", + "format": "did" + }, + "collection": { + "type": "string", + "format": "nsid" + }, + "rkey": { + "type": "string" + }, "time_us": { "type": "integer" } @@ -119,8 +330,11 @@ "format": "cid" }, "value": { - "type": "ref", - "ref": "app.bsky.actor.profile#main" + "type": "union", + "refs": [ + "app.bsky.actor.profile#main", + "social.popfeed.actor.profile#main" + ] }, "collection": { "type": "string", diff --git a/api/lexicons/generated/watch/atmo/listItem/getRecord.json b/api/lexicons/generated/watch/atmo/listItem/getRecord.json index 044245a..9e19cbf 100644 --- a/api/lexicons/generated/watch/atmo/listItem/getRecord.json +++ b/api/lexicons/generated/watch/atmo/listItem/getRecord.json @@ -19,6 +19,10 @@ "profiles": { "type": "boolean", "description": "Include indexed profile and identity information" + }, + "hydrateList": { + "type": "boolean", + "description": "Embed the referenced list record" } } }, @@ -61,6 +65,10 @@ "time_us": { "type": "integer" }, + "list": { + "type": "ref", + "ref": "#refListRecord" + }, "profiles": { "type": "array", "items": { @@ -72,6 +80,45 @@ } } }, + "refListRecord": { + "type": "object", + "required": [ + "uri", + "value", + "did", + "collection", + "rkey", + "time_us" + ], + "properties": { + "uri": { + "type": "string", + "format": "at-uri" + }, + "cid": { + "type": "string", + "format": "cid" + }, + "value": { + "type": "ref", + "ref": "social.popfeed.feed.list#main" + }, + "did": { + "type": "string", + "format": "did" + }, + "collection": { + "type": "string", + "format": "nsid" + }, + "rkey": { + "type": "string" + }, + "time_us": { + "type": "integer" + } + } + }, "profileEntry": { "type": "object", "required": [ @@ -94,8 +141,11 @@ "format": "cid" }, "value": { - "type": "ref", - "ref": "app.bsky.actor.profile#main" + "type": "union", + "refs": [ + "app.bsky.actor.profile#main", + "social.popfeed.actor.profile#main" + ] }, "collection": { "type": "string", diff --git a/api/lexicons/generated/watch/atmo/listItem/listRecords.json b/api/lexicons/generated/watch/atmo/listItem/listRecords.json index 540a92e..dd894fd 100644 --- a/api/lexicons/generated/watch/atmo/listItem/listRecords.json +++ b/api/lexicons/generated/watch/atmo/listItem/listRecords.json @@ -25,6 +25,63 @@ "profiles": { "type": "boolean", "description": "Include indexed profile and identity information" + }, + "addedAtMin": { + "type": "string", + "description": "Minimum value for addedAt" + }, + "addedAtMax": { + "type": "string", + "description": "Maximum value for addedAt" + }, + "creativeWorkType": { + "type": "string", + "description": "Filter by creativeWorkType" + }, + "identifiersTmdbId": { + "type": "string", + "description": "Filter by identifiers.tmdbId" + }, + "identifiersTmdbTvSeriesId": { + "type": "string", + "description": "Filter by identifiers.tmdbTvSeriesId" + }, + "listType": { + "type": "string", + "description": "Filter by listType" + }, + "listUri": { + "type": "string", + "description": "Filter by listUri" + }, + "status": { + "type": "string", + "description": "Filter by status" + }, + "hydrateList": { + "type": "boolean", + "description": "Embed the referenced list record" + }, + "sort": { + "type": "string", + "knownValues": [ + "addedAt", + "creativeWorkType", + "identifiersTmdbId", + "identifiersTmdbTvSeriesId", + "listType", + "listUri", + "status" + ], + "description": "Field to sort by (default: time_us)" + }, + "order": { + "type": "string", + "knownValues": [ + "asc", + "desc" + ], + "description": "Sort direction" } } }, @@ -92,6 +149,49 @@ "rkey": { "type": "string" }, + "time_us": { + "type": "integer" + }, + "list": { + "type": "ref", + "ref": "#refListRecord" + } + } + }, + "refListRecord": { + "type": "object", + "required": [ + "uri", + "value", + "did", + "collection", + "rkey", + "time_us" + ], + "properties": { + "uri": { + "type": "string", + "format": "at-uri" + }, + "cid": { + "type": "string", + "format": "cid" + }, + "value": { + "type": "ref", + "ref": "social.popfeed.feed.list#main" + }, + "did": { + "type": "string", + "format": "did" + }, + "collection": { + "type": "string", + "format": "nsid" + }, + "rkey": { + "type": "string" + }, "time_us": { "type": "integer" } @@ -119,8 +219,11 @@ "format": "cid" }, "value": { - "type": "ref", - "ref": "app.bsky.actor.profile#main" + "type": "union", + "refs": [ + "app.bsky.actor.profile#main", + "social.popfeed.actor.profile#main" + ] }, "collection": { "type": "string", diff --git a/api/lexicons/generated/watch/atmo/review/getRecord.json b/api/lexicons/generated/watch/atmo/review/getRecord.json index 0218242..05f60d5 100644 --- a/api/lexicons/generated/watch/atmo/review/getRecord.json +++ b/api/lexicons/generated/watch/atmo/review/getRecord.json @@ -206,8 +206,11 @@ "format": "cid" }, "value": { - "type": "ref", - "ref": "app.bsky.actor.profile#main" + "type": "union", + "refs": [ + "app.bsky.actor.profile#main", + "social.popfeed.actor.profile#main" + ] }, "collection": { "type": "string", diff --git a/api/lexicons/generated/watch/atmo/review/listRecords.json b/api/lexicons/generated/watch/atmo/review/listRecords.json index fdceadc..c1d7ac8 100644 --- a/api/lexicons/generated/watch/atmo/review/listRecords.json +++ b/api/lexicons/generated/watch/atmo/review/listRecords.json @@ -265,8 +265,11 @@ "format": "cid" }, "value": { - "type": "ref", - "ref": "app.bsky.actor.profile#main" + "type": "union", + "refs": [ + "app.bsky.actor.profile#main", + "social.popfeed.actor.profile#main" + ] }, "collection": { "type": "string", diff --git a/api/src/contrail.config.ts b/api/src/contrail.config.ts index 51b2487..65c6cac 100644 --- a/api/src/contrail.config.ts +++ b/api/src/contrail.config.ts @@ -2,7 +2,10 @@ import type { ContrailConfig } from '@atmo-dev/contrail'; export const config: ContrailConfig = { namespace: 'watch.atmo', - profiles: ['app.bsky.actor.profile'], + profiles: [ + { collection: 'app.bsky.actor.profile', shortName: 'profile' }, + { collection: 'social.popfeed.actor.profile', shortName: 'popfeedProfile' } + ], jetstreams: ['wss://jetstream1.us-east.bsky.network'], orderedSource: { source: 'jetstream', @@ -41,10 +44,44 @@ export const config: ContrailConfig = { } }, list: { - collection: 'social.popfeed.feed.list' + collection: 'social.popfeed.feed.list', + queryable: { + listType: {}, + createdAt: { type: 'range' } + }, + relations: { + items: { + collection: 'listItem', + field: 'listUri' + }, + likes: { + collection: 'like', + field: 'subjectUri', + countDistinct: 'did' + }, + comments: { + collection: 'comment', + field: 'subjectUri' + } + } }, listItem: { - collection: 'social.popfeed.feed.listItem' + collection: 'social.popfeed.feed.listItem', + queryable: { + listUri: {}, + listType: {}, + creativeWorkType: {}, + 'identifiers.tmdbId': {}, + 'identifiers.tmdbTvSeriesId': {}, + status: {}, + addedAt: { type: 'range' } + }, + references: { + list: { + collection: 'list', + field: 'listUri' + } + } }, comment: { collection: 'social.popfeed.feed.comment', diff --git a/api/wrangler.jsonc b/api/wrangler.jsonc index d618631..5df669d 100644 --- a/api/wrangler.jsonc +++ b/api/wrangler.jsonc @@ -16,8 +16,7 @@ { "binding": "DB", "database_name": "atmo-watch-contrail-g20260815", - // Replace this after creating the production D1 database. - "database_id": "00000000-0000-0000-0000-000000000000" + "database_id": "cb7e6da0-d1ff-4e0d-8543-ca13c941460f" } ], "triggers": { diff --git a/contrail.lock.json b/contrail.lock.json index e1652cc..9cb4ecb 100644 --- a/contrail.lock.json +++ b/contrail.lock.json @@ -1,10 +1,10 @@ { "format": "contrail.provider-lock", "version": 1, - "endpoint": "http://127.0.0.1:8787", + "endpoint": "https://api.atmo.watch", "namespace": "watch.atmo", - "contractDigest": "sha256:cb70003452bdb5fe4baf2c5c41f932a52ebf74ddc6d03ed3d833d0e34cc452ad", - "lexiconDigest": "sha256:fc63a6640eaff6cb71b35045fbb5c8ec3f8b178a64bae46c9512bc0bc0aeb73b", + "contractDigest": "sha256:13d95e3935100499b0d7d30deaa3daebff2150c6bdb118dc33015235897fb126", + "lexiconDigest": "sha256:f4f7b4ddbb358fc1c701329b0bdde8f085bb17c238197c91f8a2a62233343afd", "methods": [ "watch.atmo.comment.getRecord", "watch.atmo.comment.listRecords", @@ -21,13 +21,22 @@ ], "collections": [ "app.bsky.actor.profile", + "social.popfeed.actor.profile", "social.popfeed.feed.comment", "social.popfeed.feed.like", "social.popfeed.feed.list", "social.popfeed.feed.listItem", "social.popfeed.feed.review" ], - "serviceAuth": null, - "lexiconRoot": "src/lib/contrail/lexicons", - "allowInsecureHttp": true + "serviceAuth": { + "type": "atproto-service-auth", + "audience": "did:web:api.atmo.watch", + "methods": [ + { + "id": "watch.atmo.notifyOfUpdate", + "type": "procedure" + } + ] + }, + "lexiconRoot": "src/lib/contrail/lexicons/api.atmo.watch" } diff --git a/lex.config.js b/lex.config.js index 643c7ff..1ccbd00 100644 --- a/lex.config.js +++ b/lex.config.js @@ -1,11 +1,12 @@ // Generated by `contrail connect`. Re-run the command to update; do not edit. export default { contrail: { - endpoint: "http://127.0.0.1:8787", - serviceDid: null, - scope: null, + endpoint: "https://api.atmo.watch", + serviceDid: "did:web:api.atmo.watch", + scope: "rpc?lxm=*&aud=did:web:api.atmo.watch", collections: [ "app.bsky.actor.profile", + "social.popfeed.actor.profile", "social.popfeed.feed.comment", "social.popfeed.feed.like", "social.popfeed.feed.list", diff --git a/src/lib/components/AccountMenuItem.svelte b/src/lib/components/AccountMenuItem.svelte index b56b21a..bee6805 100644 --- a/src/lib/components/AccountMenuItem.svelte +++ b/src/lib/components/AccountMenuItem.svelte @@ -1,13 +1,13 @@ @@ -29,44 +53,37 @@ - {#if data.lists.length > 0} -
-

lists

-
- {#each data.lists as list (list.uri)} - - {/each} -
-
- {/if} - - {#if ratings.length > 0} -
-

ratings

-
- {#each ratings as review (review.uri)} - - {/each} -
-
- {/if} - - {#if writtenReviews.length > 0} -
-

reviews

-
- {#each writtenReviews as review (review.uri)} - - {/each} -
-
- {/if} + {#if profileTabs.length > 0} +
+

@{data.profile.handle}'s activity

+ - {#if data.reviews.length === 0} -
-

reviews

-

No movie or TV reviews yet.

+ {#if selectedSection === 'reviews' && writtenReviews.length > 0} +
+ {#each writtenReviews as review (review.uri)} + + {/each} +
+ {:else if selectedSection === 'ratings' && ratings.length > 0} +
+ {#each ratings as review (review.uri)} + + {/each} +
+ {:else if selectedSection === 'lists' && data.lists.length > 0} +
+ {#each data.lists as list (list.uri)} + + {/each} +
+ {/if}
+ {:else} +

No reviews, ratings, or lists yet.

{/if} diff --git a/src/routes/profile/[actor]/list/[rkey]/+page.svelte b/src/routes/profile/[actor]/list/[rkey]/+page.svelte index 4877098..3110188 100644 --- a/src/routes/profile/[actor]/list/[rkey]/+page.svelte +++ b/src/routes/profile/[actor]/list/[rkey]/+page.svelte @@ -7,12 +7,6 @@ let { data }: { data: PageData } = $props(); let profileUrl = $derived(resolve('/profile/[actor]', { actor: data.list.author.did })); - let listUrl = $derived( - resolve('/profile/[actor]/list/[rkey]', { - actor: data.list.author.did, - rkey: data.list.rkey - }) - ); let handle = $derived(data.list.author.handle.replace(/^@/, '')); const dateFormatter = new Intl.DateTimeFormat('en', { @@ -73,7 +67,10 @@