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 00000000..707edc60 Binary files /dev/null and b/src/assets/images/LinkatLogo.jpg differ diff --git a/src/assets/images/frontpageLogo.jpg b/src/assets/images/frontpageLogo.jpg new file mode 100644 index 00000000..14988b7d Binary files /dev/null and b/src/assets/images/frontpageLogo.jpg differ diff --git a/src/assets/images/smokeSignalLogo.png b/src/assets/images/smokeSignalLogo.png new file mode 100644 index 00000000..7750b9f0 Binary files /dev/null and b/src/assets/images/smokeSignalLogo.png differ diff --git a/src/assets/images/whtwndLogo.jpg b/src/assets/images/whtwndLogo.jpg new file mode 100644 index 00000000..58e14ec2 Binary files /dev/null and b/src/assets/images/whtwndLogo.jpg differ 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 +}