From 0c98b3d5ae05309f4079cf7899d34b0244f3e4d2 Mon Sep 17 00:00:00 2001 From: Pouria Delfanazari Date: Fri, 22 Nov 2024 17:05:53 -0800 Subject: [PATCH] Add "Atmosphere" tab Supports frontpage, linkat, and white wind --- lexicons/frontpage/post.json | 33 ++ lexicons/linkat/board.json | 42 +++ lexicons/whiteWind/defs.json | 63 ++++ lexicons/whiteWind/entry.json | 54 ++++ package-lock.json | 159 ++++++++- package.json | 6 +- src/app/api/auth/identity/actions.ts | 3 +- .../[handle]/(content)/atmosphere/loading.tsx | 5 + .../[handle]/(content)/atmosphere/page.tsx | 20 ++ .../user/[handle]/(content)/error.tsx | 6 +- src/assets/images/LinkatLogo.jpg | Bin 0 -> 2293 bytes src/assets/images/frontpageLogo.jpg | Bin 0 -> 4216 bytes src/assets/images/smokeSignalLogo.png | Bin 0 -> 15833 bytes src/assets/images/whtwndLogo.jpg | Bin 0 -> 5037 bytes .../navigational/profileTabs/ProfileTabs.tsx | 5 + src/components/navigational/tabs/TabItem.tsx | 6 +- .../atmosphere/AtmosphereContainer.tsx | 169 ++++++++++ .../AtmosphereContainerSkeleton.tsx | 19 ++ .../AtmosphereNotFoundContainer.tsx | 66 ++++ .../atmosphere/FrontpageContainer.tsx | 55 ++++ src/containers/atmosphere/LinkatContainer.tsx | 52 +++ .../atmosphere/WhiteWindContainer.tsx | 52 +++ src/lib/api/atmosphere/record.ts | 22 ++ src/lib/api/bsky/identity/did.ts | 11 + .../bsky/identity/{index.ts => service.ts} | 12 - src/lib/utils/image.ts | 23 +- types/atmosphere/index.ts | 301 ++++++++++++++++++ types/atmosphere/lexicons.ts | 210 ++++++++++++ types/atmosphere/types/blue/linkat/board.ts | 43 +++ .../atmosphere/types/com/whtwnd/blog/defs.ts | 78 +++++ .../atmosphere/types/com/whtwnd/blog/entry.ts | 35 ++ .../types/fyi/unravel/frontpage/post.ts | 30 ++ types/atmosphere/util.ts | 13 + 33 files changed, 1557 insertions(+), 36 deletions(-) create mode 100644 lexicons/frontpage/post.json create mode 100644 lexicons/linkat/board.json create mode 100644 lexicons/whiteWind/defs.json create mode 100644 lexicons/whiteWind/entry.json create mode 100644 src/app/dashboard/user/[handle]/(content)/atmosphere/loading.tsx create mode 100644 src/app/dashboard/user/[handle]/(content)/atmosphere/page.tsx create mode 100644 src/assets/images/LinkatLogo.jpg create mode 100644 src/assets/images/frontpageLogo.jpg create mode 100644 src/assets/images/smokeSignalLogo.png create mode 100644 src/assets/images/whtwndLogo.jpg create mode 100644 src/containers/atmosphere/AtmosphereContainer.tsx create mode 100644 src/containers/atmosphere/AtmosphereContainerSkeleton.tsx create mode 100644 src/containers/atmosphere/AtmosphereNotFoundContainer.tsx create mode 100644 src/containers/atmosphere/FrontpageContainer.tsx create mode 100644 src/containers/atmosphere/LinkatContainer.tsx create mode 100644 src/containers/atmosphere/WhiteWindContainer.tsx create mode 100644 src/lib/api/atmosphere/record.ts create mode 100644 src/lib/api/bsky/identity/did.ts rename src/lib/api/bsky/identity/{index.ts => service.ts} (60%) create mode 100644 types/atmosphere/index.ts create mode 100644 types/atmosphere/lexicons.ts create mode 100644 types/atmosphere/types/blue/linkat/board.ts create mode 100644 types/atmosphere/types/com/whtwnd/blog/defs.ts create mode 100644 types/atmosphere/types/com/whtwnd/blog/entry.ts create mode 100644 types/atmosphere/types/fyi/unravel/frontpage/post.ts create mode 100644 types/atmosphere/util.ts diff --git a/lexicons/frontpage/post.json b/lexicons/frontpage/post.json new file mode 100644 index 00000000..857d5d7a --- /dev/null +++ b/lexicons/frontpage/post.json @@ -0,0 +1,33 @@ +{ + "lexicon": 1, + "id": "fyi.unravel.frontpage.post", + "defs": { + "main": { + "type": "record", + "description": "Record containing a Frontpage post.", + "key": "tid", + "record": { + "type": "object", + "required": ["title", "url", "createdAt"], + "properties": { + "title": { + "type": "string", + "maxLength": 3000, + "maxGraphemes": 300, + "description": "The title of the post." + }, + "url": { + "type": "string", + "format": "uri", + "description": "The URL of the post." + }, + "createdAt": { + "type": "string", + "format": "datetime", + "description": "Client-declared timestamp when this post was originally created." + } + } + } + } + } +} diff --git a/lexicons/linkat/board.json b/lexicons/linkat/board.json new file mode 100644 index 00000000..5d768825 --- /dev/null +++ b/lexicons/linkat/board.json @@ -0,0 +1,42 @@ +{ + "lexicon": 1, + "id": "blue.linkat.board", + "defs": { + "main": { + "type": "record", + "description": "Record containing a cards of your profile.", + "key": "literal:self", + "record": { + "type": "object", + "required": ["cards"], + "properties": { + "cards": { + "type": "array", + "description": "List of cards in the board.", + "items": { + "type": "ref", + "ref": "#card" + } + } + } + } + }, + "card": { + "type": "object", + "properties": { + "url": { + "type": "string", + "description": "URL of the link" + }, + "text": { + "type": "string", + "description": "Text of the card" + }, + "emoji": { + "type": "string", + "description": "Emoji of the card" + } + } + } + } +} diff --git a/lexicons/whiteWind/defs.json b/lexicons/whiteWind/defs.json new file mode 100644 index 00000000..f939a63d --- /dev/null +++ b/lexicons/whiteWind/defs.json @@ -0,0 +1,63 @@ +{ + "lexicon": 1, + "id": "com.whtwnd.blog.defs", + "defs": { + "blogEntry": { + "type": "object", + "required": ["content"], + "properties": { + "content": { + "type": "string", + "maxLength": 100000 + }, + "createdAt": { + "type": "string", + "format": "datetime" + } + } + }, + "comment": { + "type": "object", + "required": ["content", "entryUri"], + "properties": { + "content": { + "type": "string", + "maxLength": 1000 + }, + "entryUri": { + "type": "string", + "format": "at-uri" + } + } + }, + "ogp": { + "type": "object", + "required": ["url"], + "properties": { + "url": { + "type": "string", + "format": "uri" + }, + "width": { + "type": "integer" + }, + "height": { + "type": "integer" + } + } + }, + "blobMetadata": { + "type": "object", + "required": ["blobref"], + "properties": { + "blobref": { + "type": "blob", + "accept": ["*/*"] + }, + "name": { + "type": "string" + } + } + } + } +} diff --git a/lexicons/whiteWind/entry.json b/lexicons/whiteWind/entry.json new file mode 100644 index 00000000..9af8a9ca --- /dev/null +++ b/lexicons/whiteWind/entry.json @@ -0,0 +1,54 @@ +{ + "lexicon": 1, + "id": "com.whtwnd.blog.entry", + "defs": { + "main": { + "type": "record", + "description": "A declaration of a post.", + "key": "tid", + "record": { + "type": "object", + "required": ["content"], + "properties": { + "content": { + "type": "string", + "maxLength": 100000 + }, + "createdAt": { + "type": "string", + "format": "datetime" + }, + "title": { + "type": "string", + "maxLength": 1000 + }, + "ogp": { + "type": "ref", + "ref": "com.whtwnd.blog.defs#ogp" + }, + "theme": { + "type": "string", + "enum": ["github-light"] + }, + "blobs": { + "type": "array", + "items": { + "type": "ref", + "ref": "com.whtwnd.blog.defs#blobMetadata" + } + }, + "isDraft": { + "type": "boolean", + "description": "(DEPRECATED) Marks this entry as draft to tell AppViews not to show it to anyone except for the author" + }, + "visibility": { + "type": "string", + "enum": ["public", "url", "author"], + "default": "public", + "description": "Tells the visibility of the article to AppView." + } + } + } + } + } +} diff --git a/package-lock.json b/package-lock.json index 7ba5cd30..080cc9b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,8 @@ "dependencies": { "@atproto/api": "^0.13.11", "@atproto/identity": "^0.4.3", + "@atproto/lex-cli": "^0.5.2", + "@atproto/xrpc": "^0.6.4", "@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-dropdown-menu": "^2.1.2", "@radix-ui/react-hover-card": "^1.1.2", @@ -35,6 +37,7 @@ "browser-image-compression": "^2.0.2", "emoji-picker-react": "^4.12.0", "jwt-decode": "^4.0.0", + "multiformats": "^13.3.1", "next": "14.2.14", "next-auth": "^4.24.5", "next-themes": "^0.3.0", @@ -114,6 +117,11 @@ "zod": "^3.23.8" } }, + "node_modules/@atproto/api/node_modules/multiformats": { + "version": "9.9.0", + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz", + "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==" + }, "node_modules/@atproto/common-web": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/@atproto/common-web/-/common-web-0.3.1.tgz", @@ -125,6 +133,11 @@ "zod": "^3.23.8" } }, + "node_modules/@atproto/common-web/node_modules/multiformats": { + "version": "9.9.0", + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz", + "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==" + }, "node_modules/@atproto/crypto": { "version": "0.4.2", "resolved": "https://registry.npmjs.org/@atproto/crypto/-/crypto-0.4.2.tgz", @@ -145,29 +158,60 @@ "axios": "^0.27.2" } }, + "node_modules/@atproto/lex-cli": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@atproto/lex-cli/-/lex-cli-0.5.2.tgz", + "integrity": "sha512-fM/FR/FpOMUOpwir7odZNiJhY3at0gMDGZpkLJeWFDaYyChtwqCGd6x7J4S6w/mqtCDcx1bzGTrnsLkJjtuNfg==", + "dependencies": { + "@atproto/lexicon": "^0.4.3", + "@atproto/syntax": "^0.3.1", + "chalk": "^4.1.2", + "commander": "^9.4.0", + "prettier": "^3.2.5", + "ts-morph": "^16.0.0", + "yesno": "^0.4.0", + "zod": "^3.23.8" + }, + "bin": { + "lex": "dist/index.js" + } + }, + "node_modules/@atproto/lex-cli/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "engines": { + "node": "^12.20.0 || >=14" + } + }, "node_modules/@atproto/lexicon": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@atproto/lexicon/-/lexicon-0.4.2.tgz", - "integrity": "sha512-CXoOkhcdF3XVUnR2oNgCs2ljWfo/8zUjxL5RIhJW/UNLp/FSl+KpF8Jm5fbk8Y/XXVPGRAsv9OYfxyU/14N/pw==", + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@atproto/lexicon/-/lexicon-0.4.3.tgz", + "integrity": "sha512-lFVZXe1S1pJP0dcxvJuHP3r/a+EAIBwwU7jUK+r8iLhIja+ml6NmYv8KeFHmIJATh03spEQ9s02duDmFVdCoXg==", "dependencies": { "@atproto/common-web": "^0.3.1", - "@atproto/syntax": "^0.3.0", + "@atproto/syntax": "^0.3.1", "iso-datestring-validator": "^2.2.2", "multiformats": "^9.9.0", "zod": "^3.23.8" } }, + "node_modules/@atproto/lexicon/node_modules/multiformats": { + "version": "9.9.0", + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz", + "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==" + }, "node_modules/@atproto/syntax": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.3.0.tgz", - "integrity": "sha512-Weq0ZBxffGHDXHl9U7BQc2BFJi/e23AL+k+i5+D9hUq/bzT4yjGsrCejkjq0xt82xXDjmhhvQSZ0LqxyZ5woxA==" + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.3.1.tgz", + "integrity": "sha512-fzW0Mg1QUOVCWUD3RgEsDt6d1OZ6DdFmbKcDdbzUfh0t4rhtRAC05KbZYmxuMPWDAiJ4BbbQ5dkAc/mNypMXkw==" }, "node_modules/@atproto/xrpc": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/@atproto/xrpc/-/xrpc-0.6.3.tgz", - "integrity": "sha512-S3tRvOdA9amPkKLll3rc4vphlDitLrkN5TwWh5Tu/jzk7mnobVVE3akYgICV9XCNHKjWM+IAPxFFI2qi+VW6nQ==", + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@atproto/xrpc/-/xrpc-0.6.4.tgz", + "integrity": "sha512-9ZAJ8nsXTqC4XFyS0E1Wlg7bAvonhXQNQ3Ocs1L1LIwFLXvsw/4fNpIHXxvXvqTCVeyHLbImOnE9UiO1c/qIYA==", "dependencies": { - "@atproto/lexicon": "^0.4.2", + "@atproto/lexicon": "^0.4.3", "zod": "^3.23.8" } }, @@ -2904,6 +2948,36 @@ "@tiptap/pm": "^2.7.0" } }, + "node_modules/@ts-morph/common": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.17.0.tgz", + "integrity": "sha512-RMSSvSfs9kb0VzkvQ2NWobwnj7TxCA9vI/IjR9bDHqgAyVbu2T0DN4wiKVqomyDWqO7dPr/tErSfq7urQ1Q37g==", + "dependencies": { + "fast-glob": "^3.2.11", + "minimatch": "^5.1.0", + "mkdirp": "^1.0.4", + "path-browserify": "^1.0.1" + } + }, + "node_modules/@ts-morph/common/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@ts-morph/common/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@types/cookie": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.5.4.tgz", @@ -3907,7 +3981,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -3980,6 +4053,11 @@ } ] }, + "node_modules/code-block-writer": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-11.0.3.tgz", + "integrity": "sha512-NiujjUFB4SwScJq2bwbYUtXbZhBSlY6vYzm++3Q6oC+U+injTqfPYFK8wS9COOmb2lueqp0ZRB4nK1VYeHgNyw==" + }, "node_modules/code-red": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/code-red/-/code-red-1.0.4.tgz", @@ -5505,7 +5583,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, "engines": { "node": ">=8" } @@ -6412,6 +6489,17 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/mri": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", @@ -6439,9 +6527,9 @@ "devOptional": true }, "node_modules/multiformats": { - "version": "9.9.0", - "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz", - "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==" + "version": "13.3.1", + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-13.3.1.tgz", + "integrity": "sha512-QxowxTNwJ3r5RMctoGA5p13w5RbRT2QDkoM+yFlqfLiioBp78nhDjnRLvmSBI9+KAqN4VdgOVWM9c0CHd86m3g==" }, "node_modules/mz": { "version": "2.7.0", @@ -9726,6 +9814,11 @@ "node": ">=6" } }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==" + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -10000,6 +10093,20 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/pretty-format": { "version": "3.8.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz", @@ -11101,7 +11208,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -11346,6 +11452,15 @@ "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==" }, + "node_modules/ts-morph": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-16.0.0.tgz", + "integrity": "sha512-jGNF0GVpFj0orFw55LTsQxVYEUOCWBAbR5Ls7fTYE5pQsbW18ssTb/6UXx/GYAEjS+DQTp8VoTw0vqYMiaaQuw==", + "dependencies": { + "@ts-morph/common": "~0.17.0", + "code-block-writer": "^11.0.3" + } + }, "node_modules/tsconfig-paths": { "version": "3.15.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", @@ -11486,6 +11601,11 @@ "multiformats": "^9.4.2" } }, + "node_modules/uint8arrays/node_modules/multiformats": { + "version": "9.9.0", + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz", + "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==" + }, "node_modules/unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", @@ -11958,6 +12078,11 @@ "node": ">= 14" } }, + "node_modules/yesno": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/yesno/-/yesno-0.4.0.tgz", + "integrity": "sha512-tdBxmHvbXPBKYIg81bMCB7bVeDmHkRzk5rVJyYYXurwKkHq/MCd8rz4HSJUP7hW0H2NlXiq8IFiWvYKEHhlotA==" + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index a08bd9de..87dd8a69 100644 --- a/package.json +++ b/package.json @@ -6,11 +6,14 @@ "dev": "next dev --turbo", "build": "next build", "start": "next start", - "lint": "next lint" + "lint": "next lint", + "gen-api": "lex gen-api ./types/atmosphere ./lexicons/**/*.json" }, "dependencies": { "@atproto/api": "^0.13.11", "@atproto/identity": "^0.4.3", + "@atproto/lex-cli": "^0.5.2", + "@atproto/xrpc": "^0.6.4", "@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-dropdown-menu": "^2.1.2", "@radix-ui/react-hover-card": "^1.1.2", @@ -36,6 +39,7 @@ "browser-image-compression": "^2.0.2", "emoji-picker-react": "^4.12.0", "jwt-decode": "^4.0.0", + "multiformats": "^13.3.1", "next": "14.2.14", "next-auth": "^4.24.5", "next-themes": "^0.3.0", diff --git a/src/app/api/auth/identity/actions.ts b/src/app/api/auth/identity/actions.ts index 8126aa78..360a70e1 100644 --- a/src/app/api/auth/identity/actions.ts +++ b/src/app/api/auth/identity/actions.ts @@ -1,6 +1,7 @@ "use server"; -import { getDidFromHandle, getPDS } from "@/lib/api/bsky/identity"; +import { getDidFromHandle } from "@/lib/api/bsky/identity/did"; +import { getPDS } from "@/lib/api/bsky/identity/service"; export async function getService(handle: string) { const userDID = await getDidFromHandle(handle); diff --git a/src/app/dashboard/user/[handle]/(content)/atmosphere/loading.tsx b/src/app/dashboard/user/[handle]/(content)/atmosphere/loading.tsx new file mode 100644 index 00000000..6122330e --- /dev/null +++ b/src/app/dashboard/user/[handle]/(content)/atmosphere/loading.tsx @@ -0,0 +1,5 @@ +import AtmosphereContainerSkeleton from "@/containers/atmosphere/AtmosphereContainerSkeleton"; + +export default function Loading() { + return ; +} diff --git a/src/app/dashboard/user/[handle]/(content)/atmosphere/page.tsx b/src/app/dashboard/user/[handle]/(content)/atmosphere/page.tsx new file mode 100644 index 00000000..8d0de0a6 --- /dev/null +++ b/src/app/dashboard/user/[handle]/(content)/atmosphere/page.tsx @@ -0,0 +1,20 @@ +"use client"; + +import AtmosphereContainer from "@/containers/atmosphere/AtmosphereContainer"; +import AtmosphereContainerSkeleton from "@/containers/atmosphere/AtmosphereContainerSkeleton"; +import useProfile from "@/lib/hooks/bsky/actor/useProfile"; + +interface Props { + params: { + handle: string; + }; +} + +export default function Page(props: Props) { + const { params } = props; + const { data: profile, isFetching } = useProfile(params.handle); + + if (isFetching || !profile) return ; + + return ; +} diff --git a/src/app/dashboard/user/[handle]/(content)/error.tsx b/src/app/dashboard/user/[handle]/(content)/error.tsx index 76cd69f4..40e8d5d7 100644 --- a/src/app/dashboard/user/[handle]/(content)/error.tsx +++ b/src/app/dashboard/user/[handle]/(content)/error.tsx @@ -4,6 +4,10 @@ import FeedAlert from "@/components/feedback/feedAlert/FeedAlert"; export default function Error() { return ( - + ); } diff --git a/src/assets/images/LinkatLogo.jpg b/src/assets/images/LinkatLogo.jpg new file mode 100644 index 0000000000000000000000000000000000000000..707edc6012cf97d9f4f6541435c0a6593004214a GIT binary patch literal 2293 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 literal 0 HcmV?d00001 diff --git a/src/assets/images/smokeSignalLogo.png b/src/assets/images/smokeSignalLogo.png new file mode 100644 index 0000000000000000000000000000000000000000..7750b9f0f2bb453daf4b4e48b4d5b88f1ebccea7 GIT binary patch literal 15833 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 literal 0 HcmV?d00001 diff --git a/src/components/navigational/profileTabs/ProfileTabs.tsx b/src/components/navigational/profileTabs/ProfileTabs.tsx index 3554be91..182da689 100644 --- a/src/components/navigational/profileTabs/ProfileTabs.tsx +++ b/src/components/navigational/profileTabs/ProfileTabs.tsx @@ -40,6 +40,11 @@ export default function ProfileTabs() { path={`${basePath}/lists`} isActive={pathname === `${basePath}/lists`} /> + ); diff --git a/src/components/navigational/tabs/TabItem.tsx b/src/components/navigational/tabs/TabItem.tsx index 57df4f3a..f6a28960 100644 --- a/src/components/navigational/tabs/TabItem.tsx +++ b/src/components/navigational/tabs/TabItem.tsx @@ -4,13 +4,14 @@ import Link from "next/link"; interface Props { label: string; path?: string | Url; + as?: string | Url; isActive: boolean; asButton?: boolean; onClick?: () => void; } export default function TabItem(props: Props) { - const { label, path, isActive, asButton, onClick } = props; + const { label, path, as, isActive, asButton, onClick } = props; if (asButton) { return ( @@ -32,13 +33,14 @@ export default function TabItem(props: Props) { return ( {label} diff --git a/src/containers/atmosphere/AtmosphereContainer.tsx b/src/containers/atmosphere/AtmosphereContainer.tsx new file mode 100644 index 00000000..95fff931 --- /dev/null +++ b/src/containers/atmosphere/AtmosphereContainer.tsx @@ -0,0 +1,169 @@ +"use client"; + +import * as ScrollArea from "@radix-ui/react-scroll-area"; +import Button from "@/components/actions/button/Button"; +import { getATRecords } from "@/lib/api/atmosphere/record"; +import { createAgent } from "@/lib/api/bsky/agent"; +import { getPDS } from "@/lib/api/bsky/identity/service"; +import { getAtmosphereServiceLogo } from "@/lib/utils/image"; +import { useQuery } from "@tanstack/react-query"; +import Image from "next/image"; +import { useLayoutEffect, useState } from "react"; +import LinkatContainer from "./LinkatContainer"; +import WhiteWindContainer from "./WhiteWindContainer"; +import FrontpageContainer from "./FrontpageContainer"; +import AtmosphereNotFoundContainer from "./AtmosphereNotFoundContainer"; +import AtmosphereContainerSkeleton from "./AtmosphereContainerSkeleton"; +import FeedAlert from "@/components/feedback/feedAlert/FeedAlert"; + +interface Props { + handle: string; + did: string; +} + +export default function AtmosphereContainer(props: Props) { + const { handle, did } = props; + const [service, setService] = useState(); + + const { + data: collections, + isFetching: isFetchingCollections, + error, + } = useQuery({ + queryKey: ["atmosphere collections", did], + + queryFn: async () => { + const pds = await getPDS(did); + const agent = createAgent(pds); + + const frontpageRecords = await getATRecords( + did, + "fyi.unravel.frontpage.post", + agent + ); + const linkatRecords = await getATRecords(did, "blue.linkat.board", agent); + const whtwndRecords = await getATRecords( + did, + "com.whtwnd.blog.entry", + agent + ); + + return [ + { name: "Frontpage", records: frontpageRecords.records }, + { name: "Linkat", records: linkatRecords.records }, + { name: "White Wind", records: whtwndRecords.records }, + ]; + }, + }); + + useLayoutEffect(() => { + if (collections && collections.some((c) => c.records.length > 0)) { + setService(collections.find((c) => c.records.length > 0)?.name); + } + }, [collections]); + + const hasCollection = + collections && collections.some((c) => c.records.length > 0); + + if (isFetchingCollections) return ; + + if (error) + return ( + + ); + + if (!hasCollection) { + return ; + } + + const collectionMap = collections.reduce( + (acc, collection) => { + if (collection.records.length > 0) { + acc[collection.name] = collection.records; + } + return acc; + }, + {} as Record + ); + + const renderContainer = () => { + switch (service) { + case "Frontpage": + return ( + collectionMap.Frontpage && ( + + ) + ); + case "Linkat": + return ( + collectionMap.Linkat && ( + + ) + ); + case "White Wind": + return ( + collectionMap["White Wind"] && ( + + ) + ); + default: + return null; + } + }; + + return ( +
+
+ {hasCollection && ( + +
+ {collections.map((c) => ( + <> + {c.records.length !== 0 && ( + + )} + + ))} +
+ + + + +
+ )} +
+ + {renderContainer()} +
+ ); +} diff --git a/src/containers/atmosphere/AtmosphereContainerSkeleton.tsx b/src/containers/atmosphere/AtmosphereContainerSkeleton.tsx new file mode 100644 index 00000000..eff4dc40 --- /dev/null +++ b/src/containers/atmosphere/AtmosphereContainerSkeleton.tsx @@ -0,0 +1,19 @@ +function Skeleton() { + return ( +
+ ); +} + +export default function AtmosphereContainerSkeleton() { + return ( +
+ + + + + + + +
+ ); +} diff --git a/src/containers/atmosphere/AtmosphereNotFoundContainer.tsx b/src/containers/atmosphere/AtmosphereNotFoundContainer.tsx new file mode 100644 index 00000000..fc6a4057 --- /dev/null +++ b/src/containers/atmosphere/AtmosphereNotFoundContainer.tsx @@ -0,0 +1,66 @@ +import Link from "next/link"; +import Alert from "@/components/feedback/alert/Alert"; +import Avatar from "@/components/dataDisplay/avatar/Avatar"; +import WhiteWindLogo from "@/assets/images/whtwndLogo.jpg"; +import LinkatLogo from "@/assets/images/LinkatLogo.jpg"; +import FrontpageLogo from "@/assets/images/frontpageLogo.jpg"; + +interface Props { + url: string; + title: string; + description: string; + logo: string; +} + +function AtmosphereItem(props: Props) { + const { url, title, description, logo } = props; + + return ( + +
+ +
+ {title} + {description} +
+
+ + ); +} + +export default function AtmosphereNotFoundContainer() { + return ( +
+ +

+ Ouranos currently supports the following apps in the atmosphere: +

+
    +
  • + +
  • +
  • + +
  • +
  • + +
  • +
+
+ ); +} diff --git a/src/containers/atmosphere/FrontpageContainer.tsx b/src/containers/atmosphere/FrontpageContainer.tsx new file mode 100644 index 00000000..7783c05a --- /dev/null +++ b/src/containers/atmosphere/FrontpageContainer.tsx @@ -0,0 +1,55 @@ +import Alert from "@/components/feedback/alert/Alert"; +import { FyiUnravelFrontpagePost } from "../../../types/atmosphere/index"; +import { Record } from "@atproto/api/dist/client/types/com/atproto/repo/listRecords"; +import Link from "next/link"; +import { getFormattedDate } from "@/lib/utils/time"; +import { AtUri } from "@atproto/api"; +import { getHostname } from "@/lib/utils/text"; + +interface Props { + records: Record[]; + handle: string; +} + +export default function FrontpageContainer(props: Props) { + const { records, handle } = props; + + if (!records.every((item) => FyiUnravelFrontpagePost.isRecord(item?.value))) { + return ; + } + + return ( +
+ <> + {records.map((item) => ( + <> + {FyiUnravelFrontpagePost.isRecord(item.value) && ( + + {item.value.url && ( + + {getHostname(item.value.url)} + + )} + {item.value.title && ( +

+ {item.value.title} +

+ )} + {item.value.createdAt && ( + + )} + + )} + + ))} + +
+ ); +} diff --git a/src/containers/atmosphere/LinkatContainer.tsx b/src/containers/atmosphere/LinkatContainer.tsx new file mode 100644 index 00000000..c27f3389 --- /dev/null +++ b/src/containers/atmosphere/LinkatContainer.tsx @@ -0,0 +1,52 @@ +import Alert from "@/components/feedback/alert/Alert"; +import { BlueLinkatBoard } from "../../../types/atmosphere/index"; +import { Record } from "@atproto/api/dist/client/types/com/atproto/repo/listRecords"; +import Link from "next/link"; +import { BiLink } from "react-icons/bi"; + +interface Props { + records: Record[]; +} + +export default function LinkatContainer(props: Props) { + const { records } = props; + + if (!records.every((item) => BlueLinkatBoard.isRecord(item?.value))) { + return ; + } + + return ( +
+ <> + {records.map((item) => ( + <> + {BlueLinkatBoard.isRecord(item.value) && + item.value.cards.map((card, i) => ( +
+ {card.url ? ( + + + {card.emoji && {card.emoji}} + {card.text && {card.text}} + + ) : ( +
+ {card.emoji && {card.emoji}} + {card.text && {card.text}} +
+ )} +
+ ))} + + ))} + +
+ ); +} diff --git a/src/containers/atmosphere/WhiteWindContainer.tsx b/src/containers/atmosphere/WhiteWindContainer.tsx new file mode 100644 index 00000000..d0a605cf --- /dev/null +++ b/src/containers/atmosphere/WhiteWindContainer.tsx @@ -0,0 +1,52 @@ +import Alert from "@/components/feedback/alert/Alert"; +import { ComWhtwndBlogEntry } from "../../../types/atmosphere/index"; +import { Record } from "@atproto/api/dist/client/types/com/atproto/repo/listRecords"; +import { getFormattedDate } from "@/lib/utils/time"; +import Link from "next/link"; + +interface Props { + records: Record[]; + handle: string; +} + +export default function WhiteWindContainer(props: Props) { + const { records, handle } = props; + + if (!records.every((item) => ComWhtwndBlogEntry.isRecord(item?.value))) { + return ; + } + + return ( +
+ {records.map((post) => ( + <> + {ComWhtwndBlogEntry.isRecord(post.value) && + post.value.visibility === "public" && ( + + {post.value.title && ( +

+ {post.value.title} +

+ )} + {post.value.createdAt && ( + + )} + {post.value.content && ( +

+ {post.value.content} +

+ )} + + )} + + ))} +
+ ); +} diff --git a/src/lib/api/atmosphere/record.ts b/src/lib/api/atmosphere/record.ts new file mode 100644 index 00000000..f7647993 --- /dev/null +++ b/src/lib/api/atmosphere/record.ts @@ -0,0 +1,22 @@ +import AtpAgent from "@atproto/api"; +import { getAgentFromServer } from "../bsky/agent"; + +export const getATRecords = async ( + did: string, + collection: string, + agent: AtpAgent +) => { + if (!agent) agent = await getAgentFromServer(); + + const result = await agent.com.atproto.repo.listRecords({ + repo: did, + collection: collection, + limit: 10, + }); + + if (!result.success) { + throw new Error(`Could not get records from collection '${collection}'`); + } + + return result.data; +}; diff --git a/src/lib/api/bsky/identity/did.ts b/src/lib/api/bsky/identity/did.ts new file mode 100644 index 00000000..8ad2d0c7 --- /dev/null +++ b/src/lib/api/bsky/identity/did.ts @@ -0,0 +1,11 @@ +import { HandleResolver } from "@atproto/identity"; + +export const getDidFromHandle = async (handle: string) => { + const handleResolver = new HandleResolver({}); + const did = await handleResolver.resolve(handle); + if (!did) { + throw new Error("Could not get DID"); + } + + return did; +}; diff --git a/src/lib/api/bsky/identity/index.ts b/src/lib/api/bsky/identity/service.ts similarity index 60% rename from src/lib/api/bsky/identity/index.ts rename to src/lib/api/bsky/identity/service.ts index fe10cbaa..8e9a5f68 100644 --- a/src/lib/api/bsky/identity/index.ts +++ b/src/lib/api/bsky/identity/service.ts @@ -1,15 +1,3 @@ -import { HandleResolver } from "@atproto/identity"; - -export const getDidFromHandle = async (handle: string) => { - const handleResolver = new HandleResolver({}); - const did = await handleResolver.resolve(handle); - if (!did) { - throw new Error("Could not get DID"); - } - - return did; -}; - export const getPDS = async (did: string) => { const res = await fetch(`https://plc.directory/${did}`); if (!res.ok) throw new Error("PDS not found"); diff --git a/src/lib/utils/image.ts b/src/lib/utils/image.ts index fdb58fc4..bd702d0e 100644 --- a/src/lib/utils/image.ts +++ b/src/lib/utils/image.ts @@ -1,4 +1,8 @@ import imageCompression from "browser-image-compression"; +import SmokeSignalLogo from "@/assets/images/smokeSignalLogo.png"; +import WhiteWindLogo from "@/assets/images/whtwndLogo.jpg"; +import LinkatLogo from "@/assets/images/LinkatLogo.jpg"; +import FrontpageLogo from "@/assets/images/frontpageLogo.jpg"; export function getAvatarSize(size?: AvatarSize): number[] { switch (size) { @@ -18,7 +22,7 @@ export function getAvatarSize(size?: AvatarSize): number[] { export async function compressImage(image: UploadImage) { try { const compressed = await imageCompression(image, { - maxSizeMB: 0.95, + maxSizeMB: 0.95, maxWidthOrHeight: 3000, fileType: "image/jpeg", initialQuality: 0.9, @@ -27,4 +31,19 @@ export async function compressImage(image: UploadImage) { } catch (error) { throw new Error("Could not compress image"); } -} \ No newline at end of file +} + +export const getAtmosphereServiceLogo = (name: string) => { + switch (name) { + case "Smoke Signal": + return SmokeSignalLogo.src; + case "White Wind": + return WhiteWindLogo.src; + case "Linkat": + return LinkatLogo.src; + case "Frontpage": + return FrontpageLogo; + default: + return ""; + } +}; diff --git a/types/atmosphere/index.ts b/types/atmosphere/index.ts new file mode 100644 index 00000000..59dc84d1 --- /dev/null +++ b/types/atmosphere/index.ts @@ -0,0 +1,301 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { XrpcClient, FetchHandler, FetchHandlerOptions } from '@atproto/xrpc' +import { schemas } from './lexicons' +import { CID } from 'multiformats/cid' +import * as FyiUnravelFrontpagePost from './types/fyi/unravel/frontpage/post' +import * as BlueLinkatBoard from './types/blue/linkat/board' +import * as ComWhtwndBlogDefs from './types/com/whtwnd/blog/defs' +import * as ComWhtwndBlogEntry from './types/com/whtwnd/blog/entry' +import { ComAtprotoRepoCreateRecord, ComAtprotoRepoDeleteRecord, ComAtprotoRepoGetRecord, ComAtprotoRepoListRecords } from '@atproto/api' + +export * as FyiUnravelFrontpagePost from './types/fyi/unravel/frontpage/post' +export * as BlueLinkatBoard from './types/blue/linkat/board' +export * as ComWhtwndBlogDefs from './types/com/whtwnd/blog/defs' +export * as ComWhtwndBlogEntry from './types/com/whtwnd/blog/entry' + +export class AtpBaseClient extends XrpcClient { + fyi: FyiNS + blue: BlueNS + com: ComNS + + constructor(options: FetchHandler | FetchHandlerOptions) { + super(options, schemas) + this.fyi = new FyiNS(this) + this.blue = new BlueNS(this) + this.com = new ComNS(this) + } + + /** @deprecated use `this` instead */ + get xrpc(): XrpcClient { + return this + } +} + +export class FyiNS { + _client: XrpcClient + unravel: FyiUnravelNS + + constructor(client: XrpcClient) { + this._client = client + this.unravel = new FyiUnravelNS(client) + } +} + +export class FyiUnravelNS { + _client: XrpcClient + frontpage: FyiUnravelFrontpageNS + + constructor(client: XrpcClient) { + this._client = client + this.frontpage = new FyiUnravelFrontpageNS(client) + } +} + +export class FyiUnravelFrontpageNS { + _client: XrpcClient + post: PostRecord + + constructor(client: XrpcClient) { + this._client = client + this.post = new PostRecord(client) + } +} + +export class PostRecord { + _client: XrpcClient + + constructor(client: XrpcClient) { + this._client = client + } + + async list( + params: Omit, + ): Promise<{ + cursor?: string + records: { uri: string; value: FyiUnravelFrontpagePost.Record }[] + }> { + const res = await this._client.call('com.atproto.repo.listRecords', { + collection: 'fyi.unravel.frontpage.post', + ...params, + }) + return res.data + } + + async get( + params: Omit, + ): Promise<{ + uri: string + cid: string + value: FyiUnravelFrontpagePost.Record + }> { + const res = await this._client.call('com.atproto.repo.getRecord', { + collection: 'fyi.unravel.frontpage.post', + ...params, + }) + return res.data + } + + async create( + params: Omit< + ComAtprotoRepoCreateRecord.InputSchema, + 'collection' | 'record' + >, + record: FyiUnravelFrontpagePost.Record, + headers?: Record, + ): Promise<{ uri: string; cid: string }> { + record.$type = 'fyi.unravel.frontpage.post' + const res = await this._client.call( + 'com.atproto.repo.createRecord', + undefined, + { collection: 'fyi.unravel.frontpage.post', ...params, record }, + { encoding: 'application/json', headers }, + ) + return res.data + } + + async delete( + params: Omit, + headers?: Record, + ): Promise { + await this._client.call( + 'com.atproto.repo.deleteRecord', + undefined, + { collection: 'fyi.unravel.frontpage.post', ...params }, + { headers }, + ) + } +} + +export class BlueNS { + _client: XrpcClient + linkat: BlueLinkatNS + + constructor(client: XrpcClient) { + this._client = client + this.linkat = new BlueLinkatNS(client) + } +} + +export class BlueLinkatNS { + _client: XrpcClient + board: BoardRecord + + constructor(client: XrpcClient) { + this._client = client + this.board = new BoardRecord(client) + } +} + +export class BoardRecord { + _client: XrpcClient + + constructor(client: XrpcClient) { + this._client = client + } + + async list( + params: Omit, + ): Promise<{ + cursor?: string + records: { uri: string; value: BlueLinkatBoard.Record }[] + }> { + const res = await this._client.call('com.atproto.repo.listRecords', { + collection: 'blue.linkat.board', + ...params, + }) + return res.data + } + + async get( + params: Omit, + ): Promise<{ uri: string; cid: string; value: BlueLinkatBoard.Record }> { + const res = await this._client.call('com.atproto.repo.getRecord', { + collection: 'blue.linkat.board', + ...params, + }) + return res.data + } + + async create( + params: Omit< + ComAtprotoRepoCreateRecord.InputSchema, + 'collection' | 'record' + >, + record: BlueLinkatBoard.Record, + headers?: Record, + ): Promise<{ uri: string; cid: string }> { + record.$type = 'blue.linkat.board' + const res = await this._client.call( + 'com.atproto.repo.createRecord', + undefined, + { collection: 'blue.linkat.board', rkey: 'self', ...params, record }, + { encoding: 'application/json', headers }, + ) + return res.data + } + + async delete( + params: Omit, + headers?: Record, + ): Promise { + await this._client.call( + 'com.atproto.repo.deleteRecord', + undefined, + { collection: 'blue.linkat.board', ...params }, + { headers }, + ) + } +} + +export class ComNS { + _client: XrpcClient + whtwnd: ComWhtwndNS + + constructor(client: XrpcClient) { + this._client = client + this.whtwnd = new ComWhtwndNS(client) + } +} + +export class ComWhtwndNS { + _client: XrpcClient + blog: ComWhtwndBlogNS + + constructor(client: XrpcClient) { + this._client = client + this.blog = new ComWhtwndBlogNS(client) + } +} + +export class ComWhtwndBlogNS { + _client: XrpcClient + entry: EntryRecord + + constructor(client: XrpcClient) { + this._client = client + this.entry = new EntryRecord(client) + } +} + +export class EntryRecord { + _client: XrpcClient + + constructor(client: XrpcClient) { + this._client = client + } + + async list( + params: Omit, + ): Promise<{ + cursor?: string + records: { uri: string; value: ComWhtwndBlogEntry.Record }[] + }> { + const res = await this._client.call('com.atproto.repo.listRecords', { + collection: 'com.whtwnd.blog.entry', + ...params, + }) + return res.data + } + + async get( + params: Omit, + ): Promise<{ uri: string; cid: string; value: ComWhtwndBlogEntry.Record }> { + const res = await this._client.call('com.atproto.repo.getRecord', { + collection: 'com.whtwnd.blog.entry', + ...params, + }) + return res.data + } + + async create( + params: Omit< + ComAtprotoRepoCreateRecord.InputSchema, + 'collection' | 'record' + >, + record: ComWhtwndBlogEntry.Record, + headers?: Record, + ): Promise<{ uri: string; cid: string }> { + record.$type = 'com.whtwnd.blog.entry' + const res = await this._client.call( + 'com.atproto.repo.createRecord', + undefined, + { collection: 'com.whtwnd.blog.entry', ...params, record }, + { encoding: 'application/json', headers }, + ) + return res.data + } + + async delete( + params: Omit, + headers?: Record, + ): Promise { + await this._client.call( + 'com.atproto.repo.deleteRecord', + undefined, + { collection: 'com.whtwnd.blog.entry', ...params }, + { headers }, + ) + } +} diff --git a/types/atmosphere/lexicons.ts b/types/atmosphere/lexicons.ts new file mode 100644 index 00000000..322d0929 --- /dev/null +++ b/types/atmosphere/lexicons.ts @@ -0,0 +1,210 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { LexiconDoc, Lexicons } from '@atproto/lexicon' + +export const schemaDict = { + FyiUnravelFrontpagePost: { + lexicon: 1, + id: 'fyi.unravel.frontpage.post', + defs: { + main: { + type: 'record', + description: 'Record containing a Frontpage post.', + key: 'tid', + record: { + type: 'object', + required: ['title', 'url', 'createdAt'], + properties: { + title: { + type: 'string', + maxLength: 3000, + maxGraphemes: 300, + description: 'The title of the post.', + }, + url: { + type: 'string', + format: 'uri', + description: 'The URL of the post.', + }, + createdAt: { + type: 'string', + format: 'datetime', + description: + 'Client-declared timestamp when this post was originally created.', + }, + }, + }, + }, + }, + }, + BlueLinkatBoard: { + lexicon: 1, + id: 'blue.linkat.board', + defs: { + main: { + type: 'record', + description: 'Record containing a cards of your profile.', + key: 'literal:self', + record: { + type: 'object', + required: ['cards'], + properties: { + cards: { + type: 'array', + description: 'List of cards in the board.', + items: { + type: 'ref', + ref: 'lex:blue.linkat.board#card', + }, + }, + }, + }, + }, + card: { + type: 'object', + properties: { + url: { + type: 'string', + description: 'URL of the link', + }, + text: { + type: 'string', + description: 'Text of the card', + }, + emoji: { + type: 'string', + description: 'Emoji of the card', + }, + }, + }, + }, + }, + ComWhtwndBlogDefs: { + lexicon: 1, + id: 'com.whtwnd.blog.defs', + defs: { + blogEntry: { + type: 'object', + required: ['content'], + properties: { + content: { + type: 'string', + maxLength: 100000, + }, + createdAt: { + type: 'string', + format: 'datetime', + }, + }, + }, + comment: { + type: 'object', + required: ['content', 'entryUri'], + properties: { + content: { + type: 'string', + maxLength: 1000, + }, + entryUri: { + type: 'string', + format: 'at-uri', + }, + }, + }, + ogp: { + type: 'object', + required: ['url'], + properties: { + url: { + type: 'string', + format: 'uri', + }, + width: { + type: 'integer', + }, + height: { + type: 'integer', + }, + }, + }, + blobMetadata: { + type: 'object', + required: ['blobref'], + properties: { + blobref: { + type: 'blob', + accept: ['*/*'], + }, + name: { + type: 'string', + }, + }, + }, + }, + }, + ComWhtwndBlogEntry: { + lexicon: 1, + id: 'com.whtwnd.blog.entry', + defs: { + main: { + type: 'record', + description: 'A declaration of a post.', + key: 'tid', + record: { + type: 'object', + required: ['content'], + properties: { + content: { + type: 'string', + maxLength: 100000, + }, + createdAt: { + type: 'string', + format: 'datetime', + }, + title: { + type: 'string', + maxLength: 1000, + }, + ogp: { + type: 'ref', + ref: 'lex:com.whtwnd.blog.defs#ogp', + }, + theme: { + type: 'string', + enum: ['github-light'], + }, + blobs: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:com.whtwnd.blog.defs#blobMetadata', + }, + }, + isDraft: { + type: 'boolean', + description: + '(DEPRECATED) Marks this entry as draft to tell AppViews not to show it to anyone except for the author', + }, + visibility: { + type: 'string', + enum: ['public', 'url', 'author'], + default: 'public', + description: 'Tells the visibility of the article to AppView.', + }, + }, + }, + }, + }, + }, +} as const satisfies Record + +export const schemas = Object.values(schemaDict) +export const lexicons: Lexicons = new Lexicons(schemas) +export const ids = { + FyiUnravelFrontpagePost: 'fyi.unravel.frontpage.post', + BlueLinkatBoard: 'blue.linkat.board', + ComWhtwndBlogDefs: 'com.whtwnd.blog.defs', + ComWhtwndBlogEntry: 'com.whtwnd.blog.entry', +} diff --git a/types/atmosphere/types/blue/linkat/board.ts b/types/atmosphere/types/blue/linkat/board.ts new file mode 100644 index 00000000..a9025610 --- /dev/null +++ b/types/atmosphere/types/blue/linkat/board.ts @@ -0,0 +1,43 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { isObj, hasProp } from '../../../util' +import { lexicons } from '../../../lexicons' +import { CID } from 'multiformats/cid' + +export interface Record { + /** List of cards in the board. */ + cards: Card[] + [k: string]: unknown +} + +export function isRecord(v: unknown): v is Record { + return ( + isObj(v) && + hasProp(v, '$type') && + (v.$type === 'blue.linkat.board#main' || v.$type === 'blue.linkat.board') + ) +} + +export function validateRecord(v: unknown): ValidationResult { + return lexicons.validate('blue.linkat.board#main', v) +} + +export interface Card { + /** URL of the link */ + url?: string + /** Text of the card */ + text?: string + /** Emoji of the card */ + emoji?: string + [k: string]: unknown +} + +export function isCard(v: unknown): v is Card { + return isObj(v) && hasProp(v, '$type') && v.$type === 'blue.linkat.board#card' +} + +export function validateCard(v: unknown): ValidationResult { + return lexicons.validate('blue.linkat.board#card', v) +} diff --git a/types/atmosphere/types/com/whtwnd/blog/defs.ts b/types/atmosphere/types/com/whtwnd/blog/defs.ts new file mode 100644 index 00000000..51d06015 --- /dev/null +++ b/types/atmosphere/types/com/whtwnd/blog/defs.ts @@ -0,0 +1,78 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { isObj, hasProp } from '../../../../util' +import { lexicons } from '../../../../lexicons' +import { CID } from 'multiformats/cid' + +export interface BlogEntry { + content: string + createdAt?: string + [k: string]: unknown +} + +export function isBlogEntry(v: unknown): v is BlogEntry { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'com.whtwnd.blog.defs#blogEntry' + ) +} + +export function validateBlogEntry(v: unknown): ValidationResult { + return lexicons.validate('com.whtwnd.blog.defs#blogEntry', v) +} + +export interface Comment { + content: string + entryUri: string + [k: string]: unknown +} + +export function isComment(v: unknown): v is Comment { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'com.whtwnd.blog.defs#comment' + ) +} + +export function validateComment(v: unknown): ValidationResult { + return lexicons.validate('com.whtwnd.blog.defs#comment', v) +} + +export interface Ogp { + url: string + width?: number + height?: number + [k: string]: unknown +} + +export function isOgp(v: unknown): v is Ogp { + return ( + isObj(v) && hasProp(v, '$type') && v.$type === 'com.whtwnd.blog.defs#ogp' + ) +} + +export function validateOgp(v: unknown): ValidationResult { + return lexicons.validate('com.whtwnd.blog.defs#ogp', v) +} + +export interface BlobMetadata { + blobref: BlobRef + name?: string + [k: string]: unknown +} + +export function isBlobMetadata(v: unknown): v is BlobMetadata { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'com.whtwnd.blog.defs#blobMetadata' + ) +} + +export function validateBlobMetadata(v: unknown): ValidationResult { + return lexicons.validate('com.whtwnd.blog.defs#blobMetadata', v) +} diff --git a/types/atmosphere/types/com/whtwnd/blog/entry.ts b/types/atmosphere/types/com/whtwnd/blog/entry.ts new file mode 100644 index 00000000..a55fceb9 --- /dev/null +++ b/types/atmosphere/types/com/whtwnd/blog/entry.ts @@ -0,0 +1,35 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { isObj, hasProp } from '../../../../util' +import { lexicons } from '../../../../lexicons' +import { CID } from 'multiformats/cid' +import * as ComWhtwndBlogDefs from './defs' + +export interface Record { + content: string + createdAt?: string + title?: string + ogp?: ComWhtwndBlogDefs.Ogp + theme?: 'github-light' + blobs?: ComWhtwndBlogDefs.BlobMetadata[] + /** (DEPRECATED) Marks this entry as draft to tell AppViews not to show it to anyone except for the author */ + isDraft?: boolean + /** Tells the visibility of the article to AppView. */ + visibility: 'public' | 'url' | 'author' + [k: string]: unknown +} + +export function isRecord(v: unknown): v is Record { + return ( + isObj(v) && + hasProp(v, '$type') && + (v.$type === 'com.whtwnd.blog.entry#main' || + v.$type === 'com.whtwnd.blog.entry') + ) +} + +export function validateRecord(v: unknown): ValidationResult { + return lexicons.validate('com.whtwnd.blog.entry#main', v) +} diff --git a/types/atmosphere/types/fyi/unravel/frontpage/post.ts b/types/atmosphere/types/fyi/unravel/frontpage/post.ts new file mode 100644 index 00000000..1d5f08cd --- /dev/null +++ b/types/atmosphere/types/fyi/unravel/frontpage/post.ts @@ -0,0 +1,30 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { isObj, hasProp } from '../../../../util' +import { lexicons } from '../../../../lexicons' +import { CID } from 'multiformats/cid' + +export interface Record { + /** The title of the post. */ + title: string + /** The URL of the post. */ + url: string + /** Client-declared timestamp when this post was originally created. */ + createdAt: string + [k: string]: unknown +} + +export function isRecord(v: unknown): v is Record { + return ( + isObj(v) && + hasProp(v, '$type') && + (v.$type === 'fyi.unravel.frontpage.post#main' || + v.$type === 'fyi.unravel.frontpage.post') + ) +} + +export function validateRecord(v: unknown): ValidationResult { + return lexicons.validate('fyi.unravel.frontpage.post#main', v) +} diff --git a/types/atmosphere/util.ts b/types/atmosphere/util.ts new file mode 100644 index 00000000..d7e70440 --- /dev/null +++ b/types/atmosphere/util.ts @@ -0,0 +1,13 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +export function isObj(v: unknown): v is Record { + return typeof v === 'object' && v !== null +} + +export function hasProp( + data: object, + prop: K, +): data is Record { + return prop in data +}