diff --git a/package-lock.json b/package-lock.json index 65a85d2a..08cf16a1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@atproto/api": "^0.16.9", "@atproto/common": "^0.4.8", "@atproto/identity": "^0.4.6", - "@atproto/lexicon": "^0.5.1", + "@atproto/lexicon": "^0.6.1", "@atproto/oauth-client-node": "^0.3.8", "@atproto/sync": "^0.1.34", "@atproto/syntax": "^0.3.3", @@ -236,10 +236,13 @@ } }, "node_modules/@atproto/api/node_modules/@atproto/syntax": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.4.1.tgz", - "integrity": "sha512-CJdImtLAiFO+0z3BWTtxwk6aY5w4t8orHTMVJgkf++QRJWTxPbIFko/0hrkADB7n2EruDxDSeAgfUGehpH6ngw==", - "license": "MIT" + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.4.3.tgz", + "integrity": "sha512-YoZUz40YAJr5nPwvCDWgodEOlt5IftZqPJvA0JDWjuZKD8yXddTwSzXSaKQAzGOpuM+/A3uXRtPzJJqlScc+iA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } }, "node_modules/@atproto/api/node_modules/multiformats": { "version": "9.9.0", @@ -265,16 +268,54 @@ } }, "node_modules/@atproto/common-web": { - "version": "0.4.10", - "resolved": "https://registry.npmjs.org/@atproto/common-web/-/common-web-0.4.10.tgz", - "integrity": "sha512-TLDZSgSKzT8ZgOrBrTGK87J1CXve9TEuY6NVVUBRkOMzRRtQzpFb9/ih5WVS/hnaWVvE30CfuyaetRoma+WKNw==", + "version": "0.4.15", + "resolved": "https://registry.npmjs.org/@atproto/common-web/-/common-web-0.4.15.tgz", + "integrity": "sha512-A4l9gyqUNez8CjZp/Trypz/D3WIQsNj8dN05WR6+RoBbvwc9JhWjKPrm+WoVYc/F16RPdXHLkE3BEJlGIyYIiA==", "license": "MIT", "dependencies": { - "@atproto/lex-data": "0.0.6", - "@atproto/lex-json": "0.0.6", + "@atproto/lex-data": "^0.0.10", + "@atproto/lex-json": "^0.0.10", + "@atproto/syntax": "^0.4.3", "zod": "^3.23.8" } }, + "node_modules/@atproto/common-web/node_modules/@atproto/lex-data": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/@atproto/lex-data/-/lex-data-0.0.10.tgz", + "integrity": "sha512-FDbcy8VIUVzS9Mi1F8SMxbkL/jOUmRRpqbeM1xB4A0fMxeZJTxf6naAbFt4gYF3quu/+TPJGmio6/7cav05FqQ==", + "license": "MIT", + "dependencies": { + "multiformats": "^9.9.0", + "tslib": "^2.8.1", + "uint8arrays": "3.0.0", + "unicode-segmenter": "^0.14.0" + } + }, + "node_modules/@atproto/common-web/node_modules/@atproto/lex-json": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/@atproto/lex-json/-/lex-json-0.0.10.tgz", + "integrity": "sha512-L6MyXU17C5ODMeob8myQ2F3xvgCTvJUtM0ew8qSApnN//iDasB/FDGgd7ty4UVNmx4NQ/rtvz8xV94YpG6kneQ==", + "license": "MIT", + "dependencies": { + "@atproto/lex-data": "^0.0.10", + "tslib": "^2.8.1" + } + }, + "node_modules/@atproto/common-web/node_modules/@atproto/syntax": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.4.3.tgz", + "integrity": "sha512-YoZUz40YAJr5nPwvCDWgodEOlt5IftZqPJvA0JDWjuZKD8yXddTwSzXSaKQAzGOpuM+/A3uXRtPzJJqlScc+iA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } + }, + "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==", + "license": "(Apache-2.0 AND MIT)" + }, "node_modules/@atproto/common/node_modules/multiformats": { "version": "9.9.0", "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz", @@ -464,11 +505,14 @@ } }, "node_modules/@atproto/lex-cli/node_modules/@atproto/syntax": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.4.1.tgz", - "integrity": "sha512-CJdImtLAiFO+0z3BWTtxwk6aY5w4t8orHTMVJgkf++QRJWTxPbIFko/0hrkADB7n2EruDxDSeAgfUGehpH6ngw==", + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.4.3.tgz", + "integrity": "sha512-YoZUz40YAJr5nPwvCDWgodEOlt5IftZqPJvA0JDWjuZKD8yXddTwSzXSaKQAzGOpuM+/A3uXRtPzJJqlScc+iA==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } }, "node_modules/@atproto/lex-client": { "version": "0.0.7", @@ -614,23 +658,26 @@ "license": "MIT" }, "node_modules/@atproto/lexicon": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/@atproto/lexicon/-/lexicon-0.5.1.tgz", - "integrity": "sha512-y8AEtYmfgVl4fqFxqXAeGvhesiGkxiy3CWoJIfsFDDdTlZUC8DFnZrYhcqkIop3OlCkkljvpSJi1hbeC1tbi8A==", + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@atproto/lexicon/-/lexicon-0.6.1.tgz", + "integrity": "sha512-/vI1kVlY50Si+5MXpvOucelnYwb0UJ6Qto5mCp+7Q5C+Jtp+SoSykAPVvjVtTnQUH2vrKOFOwpb3C375vSKzXw==", "license": "MIT", "dependencies": { - "@atproto/common-web": "^0.4.3", - "@atproto/syntax": "^0.4.1", + "@atproto/common-web": "^0.4.13", + "@atproto/syntax": "^0.4.3", "iso-datestring-validator": "^2.2.2", "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "node_modules/@atproto/lexicon/node_modules/@atproto/syntax": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.4.1.tgz", - "integrity": "sha512-CJdImtLAiFO+0z3BWTtxwk6aY5w4t8orHTMVJgkf++QRJWTxPbIFko/0hrkADB7n2EruDxDSeAgfUGehpH6ngw==", - "license": "MIT" + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.4.3.tgz", + "integrity": "sha512-YoZUz40YAJr5nPwvCDWgodEOlt5IftZqPJvA0JDWjuZKD8yXddTwSzXSaKQAzGOpuM+/A3uXRtPzJJqlScc+iA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } }, "node_modules/@atproto/lexicon/node_modules/multiformats": { "version": "9.9.0", @@ -715,14 +762,14 @@ } }, "node_modules/@atproto/repo/node_modules/@atproto/common": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/@atproto/common/-/common-0.5.6.tgz", - "integrity": "sha512-rbWoZwHQNP8jcwjCREVecchw8aaoM5A1NCONyb9PVDWOJLRLCzojYMeIS8IbFqXo6NyIByOGddupADkkLeVBGQ==", + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/@atproto/common/-/common-0.5.10.tgz", + "integrity": "sha512-A1+4W3JmjZIgmtJFLJBAaoVruZhRL0ANtyjZ91aJR4rjHcZuaQ+v4IFR1UcE6yyTATacLdBk6ADy8OtxXzq14g==", "license": "MIT", "dependencies": { - "@atproto/common-web": "^0.4.10", - "@atproto/lex-cbor": "0.0.6", - "@atproto/lex-data": "0.0.6", + "@atproto/common-web": "^0.4.15", + "@atproto/lex-cbor": "^0.0.10", + "@atproto/lex-data": "^0.0.10", "iso-datestring-validator": "^2.2.2", "multiformats": "^9.9.0", "pino": "^8.21.0" @@ -731,24 +778,27 @@ "node": ">=18.7.0" } }, - "node_modules/@atproto/repo/node_modules/@atproto/lexicon": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@atproto/lexicon/-/lexicon-0.6.0.tgz", - "integrity": "sha512-5veb8aD+J5M0qszLJ+73KSFsFrJBgAY/nM1TSAJvGY7fNc9ZAT+PSUlmIyrdye9YznAZ07yktalls/TwNV7cHQ==", + "node_modules/@atproto/repo/node_modules/@atproto/lex-cbor": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/@atproto/lex-cbor/-/lex-cbor-0.0.10.tgz", + "integrity": "sha512-5RtV90iIhRNCXXvvETd3KlraV8XGAAAgOmiszUb+l8GySDU/sGk7AlVvArFfXnj/S/GXJq8DP6IaUxCw/sPASA==", "license": "MIT", "dependencies": { - "@atproto/common-web": "^0.4.7", - "@atproto/syntax": "^0.4.2", - "iso-datestring-validator": "^2.2.2", - "multiformats": "^9.9.0", - "zod": "^3.23.8" + "@atproto/lex-data": "^0.0.10", + "tslib": "^2.8.1" } }, - "node_modules/@atproto/repo/node_modules/@atproto/syntax": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.4.2.tgz", - "integrity": "sha512-X9XSRPinBy/0VQ677j8VXlBsYSsUXaiqxWVpGGxJYsAhugdQRb0jqaVKJFtm6RskeNkV6y9xclSUi9UYG/COrA==", - "license": "MIT" + "node_modules/@atproto/repo/node_modules/@atproto/lex-data": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/@atproto/lex-data/-/lex-data-0.0.10.tgz", + "integrity": "sha512-FDbcy8VIUVzS9Mi1F8SMxbkL/jOUmRRpqbeM1xB4A0fMxeZJTxf6naAbFt4gYF3quu/+TPJGmio6/7cav05FqQ==", + "license": "MIT", + "dependencies": { + "multiformats": "^9.9.0", + "tslib": "^2.8.1", + "uint8arrays": "3.0.0", + "unicode-segmenter": "^0.14.0" + } }, "node_modules/@atproto/repo/node_modules/multiformats": { "version": "9.9.0", @@ -777,10 +827,13 @@ } }, "node_modules/@atproto/sync/node_modules/@atproto/syntax": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.4.1.tgz", - "integrity": "sha512-CJdImtLAiFO+0z3BWTtxwk6aY5w4t8orHTMVJgkf++QRJWTxPbIFko/0hrkADB7n2EruDxDSeAgfUGehpH6ngw==", - "license": "MIT" + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.4.3.tgz", + "integrity": "sha512-YoZUz40YAJr5nPwvCDWgodEOlt5IftZqPJvA0JDWjuZKD8yXddTwSzXSaKQAzGOpuM+/A3uXRtPzJJqlScc+iA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } }, "node_modules/@atproto/sync/node_modules/multiformats": { "version": "9.9.0", @@ -812,14 +865,14 @@ } }, "node_modules/@atproto/tap/node_modules/@atproto/common": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/@atproto/common/-/common-0.5.6.tgz", - "integrity": "sha512-rbWoZwHQNP8jcwjCREVecchw8aaoM5A1NCONyb9PVDWOJLRLCzojYMeIS8IbFqXo6NyIByOGddupADkkLeVBGQ==", + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/@atproto/common/-/common-0.5.10.tgz", + "integrity": "sha512-A1+4W3JmjZIgmtJFLJBAaoVruZhRL0ANtyjZ91aJR4rjHcZuaQ+v4IFR1UcE6yyTATacLdBk6ADy8OtxXzq14g==", "license": "MIT", "dependencies": { - "@atproto/common-web": "^0.4.10", - "@atproto/lex-cbor": "0.0.6", - "@atproto/lex-data": "0.0.6", + "@atproto/common-web": "^0.4.15", + "@atproto/lex-cbor": "^0.0.10", + "@atproto/lex-data": "^0.0.10", "iso-datestring-validator": "^2.2.2", "multiformats": "^9.9.0", "pino": "^8.21.0" @@ -828,11 +881,36 @@ "node": ">=18.7.0" } }, + "node_modules/@atproto/tap/node_modules/@atproto/lex-cbor": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/@atproto/lex-cbor/-/lex-cbor-0.0.10.tgz", + "integrity": "sha512-5RtV90iIhRNCXXvvETd3KlraV8XGAAAgOmiszUb+l8GySDU/sGk7AlVvArFfXnj/S/GXJq8DP6IaUxCw/sPASA==", + "license": "MIT", + "dependencies": { + "@atproto/lex-data": "^0.0.10", + "tslib": "^2.8.1" + } + }, + "node_modules/@atproto/tap/node_modules/@atproto/lex-data": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/@atproto/lex-data/-/lex-data-0.0.10.tgz", + "integrity": "sha512-FDbcy8VIUVzS9Mi1F8SMxbkL/jOUmRRpqbeM1xB4A0fMxeZJTxf6naAbFt4gYF3quu/+TPJGmio6/7cav05FqQ==", + "license": "MIT", + "dependencies": { + "multiformats": "^9.9.0", + "tslib": "^2.8.1", + "uint8arrays": "3.0.0", + "unicode-segmenter": "^0.14.0" + } + }, "node_modules/@atproto/tap/node_modules/@atproto/syntax": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.4.2.tgz", - "integrity": "sha512-X9XSRPinBy/0VQ677j8VXlBsYSsUXaiqxWVpGGxJYsAhugdQRb0jqaVKJFtm6RskeNkV6y9xclSUi9UYG/COrA==", - "license": "MIT" + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.4.3.tgz", + "integrity": "sha512-YoZUz40YAJr5nPwvCDWgodEOlt5IftZqPJvA0JDWjuZKD8yXddTwSzXSaKQAzGOpuM+/A3uXRtPzJJqlScc+iA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } }, "node_modules/@atproto/tap/node_modules/multiformats": { "version": "9.9.0", @@ -854,14 +932,14 @@ } }, "node_modules/@atproto/ws-client/node_modules/@atproto/common": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/@atproto/common/-/common-0.5.6.tgz", - "integrity": "sha512-rbWoZwHQNP8jcwjCREVecchw8aaoM5A1NCONyb9PVDWOJLRLCzojYMeIS8IbFqXo6NyIByOGddupADkkLeVBGQ==", + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/@atproto/common/-/common-0.5.10.tgz", + "integrity": "sha512-A1+4W3JmjZIgmtJFLJBAaoVruZhRL0ANtyjZ91aJR4rjHcZuaQ+v4IFR1UcE6yyTATacLdBk6ADy8OtxXzq14g==", "license": "MIT", "dependencies": { - "@atproto/common-web": "^0.4.10", - "@atproto/lex-cbor": "0.0.6", - "@atproto/lex-data": "0.0.6", + "@atproto/common-web": "^0.4.15", + "@atproto/lex-cbor": "^0.0.10", + "@atproto/lex-data": "^0.0.10", "iso-datestring-validator": "^2.2.2", "multiformats": "^9.9.0", "pino": "^8.21.0" @@ -870,6 +948,28 @@ "node": ">=18.7.0" } }, + "node_modules/@atproto/ws-client/node_modules/@atproto/lex-cbor": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/@atproto/lex-cbor/-/lex-cbor-0.0.10.tgz", + "integrity": "sha512-5RtV90iIhRNCXXvvETd3KlraV8XGAAAgOmiszUb+l8GySDU/sGk7AlVvArFfXnj/S/GXJq8DP6IaUxCw/sPASA==", + "license": "MIT", + "dependencies": { + "@atproto/lex-data": "^0.0.10", + "tslib": "^2.8.1" + } + }, + "node_modules/@atproto/ws-client/node_modules/@atproto/lex-data": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/@atproto/lex-data/-/lex-data-0.0.10.tgz", + "integrity": "sha512-FDbcy8VIUVzS9Mi1F8SMxbkL/jOUmRRpqbeM1xB4A0fMxeZJTxf6naAbFt4gYF3quu/+TPJGmio6/7cav05FqQ==", + "license": "MIT", + "dependencies": { + "multiformats": "^9.9.0", + "tslib": "^2.8.1", + "uint8arrays": "3.0.0", + "unicode-segmenter": "^0.14.0" + } + }, "node_modules/@atproto/ws-client/node_modules/multiformats": { "version": "9.9.0", "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz", @@ -1714,13 +1814,14 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz", - "integrity": "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", "cpu": [ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "aix" @@ -1730,13 +1831,14 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.4.tgz", - "integrity": "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -1746,13 +1848,14 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.4.tgz", - "integrity": "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -1762,13 +1865,14 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.4.tgz", - "integrity": "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -1777,14 +1881,32 @@ "node": ">=18" } }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.4.tgz", - "integrity": "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -1794,13 +1916,14 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.4.tgz", - "integrity": "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" @@ -1810,13 +1933,14 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.4.tgz", - "integrity": "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" @@ -1826,13 +1950,14 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.4.tgz", - "integrity": "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -1842,13 +1967,14 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.4.tgz", - "integrity": "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -1858,13 +1984,14 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.4.tgz", - "integrity": "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -1874,13 +2001,14 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.4.tgz", - "integrity": "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", "cpu": [ "loong64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -1890,13 +2018,14 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.4.tgz", - "integrity": "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", "cpu": [ "mips64el" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -1906,13 +2035,14 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.4.tgz", - "integrity": "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", "cpu": [ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -1922,13 +2052,14 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.4.tgz", - "integrity": "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", "cpu": [ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -1938,13 +2069,14 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.4.tgz", - "integrity": "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", "cpu": [ "s390x" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -1954,9 +2086,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.4.tgz", - "integrity": "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", "cpu": [ "x64" ], @@ -1971,13 +2103,14 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.4.tgz", - "integrity": "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "netbsd" @@ -1987,13 +2120,14 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.4.tgz", - "integrity": "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "netbsd" @@ -2003,13 +2137,14 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.4.tgz", - "integrity": "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "openbsd" @@ -2019,13 +2154,14 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.4.tgz", - "integrity": "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "openbsd" @@ -2034,14 +2170,32 @@ "node": ">=18" } }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.4.tgz", - "integrity": "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "sunos" @@ -2051,13 +2205,14 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.4.tgz", - "integrity": "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -2067,13 +2222,14 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.4.tgz", - "integrity": "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -2083,13 +2239,14 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz", - "integrity": "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -3014,11 +3171,12 @@ } }, "node_modules/@mdx-js/loader/node_modules/source-map": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", - "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "license": "BSD-3-Clause", "engines": { - "node": ">= 8" + "node": ">= 12" } }, "node_modules/@mdx-js/mdx": { @@ -3065,11 +3223,12 @@ } }, "node_modules/@mdx-js/mdx/node_modules/source-map": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", - "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "license": "BSD-3-Clause", "engines": { - "node": ">= 8" + "node": ">= 12" } }, "node_modules/@mdx-js/react": { @@ -3160,11 +3319,12 @@ } }, "node_modules/@next/mdx/node_modules/source-map": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", - "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "license": "BSD-3-Clause", "engines": { - "node": ">= 8" + "node": ">= 12" } }, "node_modules/@next/swc-darwin-arm64": { @@ -7549,9 +7709,9 @@ } }, "node_modules/@tailwindcss/node/node_modules/magic-string": { - "version": "0.30.19", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz", - "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==", + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7805,11 +7965,11 @@ } }, "node_modules/@tailwindcss/oxide/node_modules/tar": { - "version": "7.5.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.1.tgz", - "integrity": "sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==", + "version": "7.5.7", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.7.tgz", + "integrity": "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", @@ -8142,9 +8302,10 @@ } }, "node_modules/@types/unist": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", - "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" }, "node_modules/@types/uuid": { "version": "10.0.0", @@ -8563,12 +8724,10 @@ } }, "node_modules/agent-base": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", - "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", - "dependencies": { - "debug": "^4.3.4" - }, + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", "engines": { "node": ">= 14" } @@ -9047,26 +9206,12 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, - "node_modules/body-parser/node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.0.6" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -9748,9 +9893,9 @@ "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==" }, "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -9904,6 +10049,19 @@ "node": "*" } }, + "node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/dreamopt": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/dreamopt/-/dreamopt-0.8.0.tgz", @@ -10800,9 +10958,9 @@ } }, "node_modules/esbuild": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz", - "integrity": "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -10813,31 +10971,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.4", - "@esbuild/android-arm": "0.25.4", - "@esbuild/android-arm64": "0.25.4", - "@esbuild/android-x64": "0.25.4", - "@esbuild/darwin-arm64": "0.25.4", - "@esbuild/darwin-x64": "0.25.4", - "@esbuild/freebsd-arm64": "0.25.4", - "@esbuild/freebsd-x64": "0.25.4", - "@esbuild/linux-arm": "0.25.4", - "@esbuild/linux-arm64": "0.25.4", - "@esbuild/linux-ia32": "0.25.4", - "@esbuild/linux-loong64": "0.25.4", - "@esbuild/linux-mips64el": "0.25.4", - "@esbuild/linux-ppc64": "0.25.4", - "@esbuild/linux-riscv64": "0.25.4", - "@esbuild/linux-s390x": "0.25.4", - "@esbuild/linux-x64": "0.25.4", - "@esbuild/netbsd-arm64": "0.25.4", - "@esbuild/netbsd-x64": "0.25.4", - "@esbuild/openbsd-arm64": "0.25.4", - "@esbuild/openbsd-x64": "0.25.4", - "@esbuild/sunos-x64": "0.25.4", - "@esbuild/win32-arm64": "0.25.4", - "@esbuild/win32-ia32": "0.25.4", - "@esbuild/win32-x64": "0.25.4" + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" } }, "node_modules/esbuild-register": { @@ -10852,22 +11011,6 @@ "esbuild": ">=0.12 <1" } }, - "node_modules/esbuild/node_modules/@esbuild/darwin-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.4.tgz", - "integrity": "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -11103,18 +11246,6 @@ "ms": "^2.1.1" } }, - "node_modules/eslint-plugin-import/node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/eslint-plugin-import/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -11206,15 +11337,6 @@ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, - "node_modules/eslint-plugin-react-hooks/node_modules/zod": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz", - "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, "node_modules/eslint-plugin-react-hooks/node_modules/zod-validation-error": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", @@ -11227,19 +11349,6 @@ "zod": "^3.25.0 || ^4.0.0" } }, - "node_modules/eslint-plugin-react/node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/eslint-plugin-react/node_modules/resolve": { "version": "2.0.0-next.5", "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", @@ -11433,11 +11542,12 @@ } }, "node_modules/estree-util-to-js/node_modules/source-map": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", - "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "license": "BSD-3-Clause", "engines": { - "node": ">= 8" + "node": ">= 12" } }, "node_modules/estree-util-visit": { @@ -11599,21 +11709,6 @@ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", "license": "MIT" }, - "node_modules/express/node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.0.6" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/ext": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", @@ -11635,16 +11730,17 @@ "dev": true }, "node_modules/fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", - "micromatch": "^4.0.4" + "micromatch": "^4.0.8" }, "engines": { "node": ">=8.6.0" @@ -11928,6 +12024,21 @@ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -12178,10 +12289,11 @@ "dev": true }, "node_modules/glob/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==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -12721,11 +12833,12 @@ } }, "node_modules/https-proxy-agent": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", - "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", "dependencies": { - "agent-base": "^7.0.2", + "agent-base": "^7.1.2", "debug": "4" }, "engines": { @@ -12765,10 +12878,11 @@ "license": "BSD-3-Clause" }, "node_modules/ignore": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", - "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, + "license": "MIT", "engines": { "node": ">= 4" } @@ -12837,9 +12951,10 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "node_modules/inline-style-parser": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz", - "integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==" + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", + "license": "MIT" }, "node_modules/inngest": { "version": "3.40.1", @@ -13000,9 +13115,9 @@ } }, "node_modules/ipaddr.js": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", - "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.3.0.tgz", + "integrity": "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==", "license": "MIT", "engines": { "node": ">= 10" @@ -14173,12 +14288,10 @@ } }, "node_modules/lru-cache": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", - "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", - "engines": { - "node": "14 || >=16.14" - } + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" }, "node_modules/lru-queue": { "version": "0.1.0", @@ -15282,10 +15395,11 @@ ] }, "node_modules/micromatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", - "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, + "license": "MIT", "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" @@ -15352,10 +15466,11 @@ } }, "node_modules/miniflare/node_modules/undici": { - "version": "5.28.4", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz", - "integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==", + "version": "5.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", + "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", "dev": true, + "license": "MIT", "dependencies": { "@fastify/busboy": "^2.0.0" }, @@ -15449,9 +15564,9 @@ "license": "MIT" }, "node_modules/multiformats": { - "version": "13.3.2", - "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-13.3.2.tgz", - "integrity": "sha512-qbB0CQDt3QKfiAzZ5ZYjLFOs+zW43vA4uyM8g27PeEuXZybUOFyjrVdP93HPBHMoglibwfkdVwbzfUq8qGcH6g==", + "version": "13.4.2", + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-13.4.2.tgz", + "integrity": "sha512-eh6eHCrRi1+POZ3dA+Dq1C6jhP1GNtr9CRINMb67OKzqW9I5DUuZM/3jLPlzhgpGeiNUlEGEbkCYChXMCc/8DQ==", "license": "Apache-2.0 OR MIT" }, "node_modules/mustache": { @@ -16037,10 +16152,11 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, "node_modules/path-to-regexp": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.2.tgz", - "integrity": "sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==", - "dev": true + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true, + "license": "MIT" }, "node_modules/pg": { "version": "8.16.3", @@ -16510,18 +16626,6 @@ "prosemirror-view": "^1.37.2" } }, - "node_modules/prosemirror-tables/node_modules/prosemirror-view": { - "version": "1.39.2", - "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.39.2.tgz", - "integrity": "sha512-BmOkml0QWNob165gyUxXi5K5CVUgVPpqMEAAml/qzgKn9boLUWVPzQ6LtzXw8Cn1GtRQX4ELumPxqtLTDaAKtg==", - "license": "MIT", - "peer": true, - "dependencies": { - "prosemirror-model": "^1.20.0", - "prosemirror-state": "^1.0.0", - "prosemirror-transform": "^1.1.0" - } - }, "node_modules/prosemirror-trailing-node": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/prosemirror-trailing-node/-/prosemirror-trailing-node-3.0.0.tgz", @@ -16548,9 +16652,9 @@ } }, "node_modules/prosemirror-view": { - "version": "1.37.1", - "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.37.1.tgz", - "integrity": "sha512-MEAnjOdXU1InxEmhjgmEzQAikaS6lF3hD64MveTPpjOGNTl87iRLA1HupC/DEV6YuK7m4Q9DHFNTjwIVtqz5NA==", + "version": "1.41.5", + "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.5.tgz", + "integrity": "sha512-UDQbIPnDrjE8tqUBbPmCOZgtd75htE6W3r0JCmY9bL6W1iemDM37MZEKC49d+tdQ0v/CKx4gjxLoLsfkD2NiZA==", "license": "MIT", "dependencies": { "prosemirror-model": "^1.20.0", @@ -16629,9 +16733,9 @@ } }, "node_modules/qs": { - "version": "6.13.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.1.tgz", - "integrity": "sha512-EJPeIn0CYrGu+hli1xilKAPXODtJ12T0sP63Ijx2/khC2JtuaN3JyNIpvmnkmaEtha9ocbG4A4cMcr+TvqvwQg==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.0.6" @@ -17371,17 +17475,21 @@ } }, "node_modules/resolve": { - "version": "1.22.8", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "license": "MIT", "dependencies": { - "is-core-module": "^2.13.0", + "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -17604,9 +17712,9 @@ } }, "node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -18235,11 +18343,12 @@ } }, "node_modules/style-to-object": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.8.tgz", - "integrity": "sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g==", + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", + "license": "MIT", "dependencies": { - "inline-style-parser": "0.2.4" + "inline-style-parser": "0.2.7" } }, "node_modules/styled-jsx": { @@ -18805,9 +18914,9 @@ } }, "node_modules/undici": { - "version": "6.21.3", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz", - "integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==", + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz", + "integrity": "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==", "license": "MIT", "engines": { "node": ">=18.17" @@ -19805,9 +19914,10 @@ } }, "node_modules/ws": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz", - "integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==", + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "license": "MIT", "engines": { "node": ">=10.0.0" }, @@ -19995,9 +20105,10 @@ } }, "node_modules/zod": { - "version": "3.23.8", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", - "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index e3f13bed..8c589ee7 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "@atproto/api": "^0.16.9", "@atproto/common": "^0.4.8", "@atproto/identity": "^0.4.6", - "@atproto/lexicon": "^0.5.1", + "@atproto/lexicon": "^0.6.1", "@atproto/oauth-client-node": "^0.3.8", "@atproto/sync": "^0.1.34", "@atproto/syntax": "^0.3.3", @@ -124,6 +124,7 @@ "ajv": "^8.17.1", "whatwg-url": "^14.0.0", "@types/react": "19.2.6", - "@types/react-dom": "19.2.3" + "@types/react-dom": "19.2.3", + "@atproto/lexicon": "^0.6.1" } } -- 2.51.2 From 90fee99c82c5c837d27a83b829b10befbe1a70d9 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Tue, 3 Feb 2026 08:30:06 +0000 Subject: [PATCH 02/51] properly open atmentions in editor and resolve pubs --- .../Blocks/TextBlock/mountProsemirror.ts | 42 +++++++++---------- src/utils/mentionUtils.ts | 7 +--- 2 files changed, 21 insertions(+), 28 deletions(-) diff --git a/components/Blocks/TextBlock/mountProsemirror.ts b/components/Blocks/TextBlock/mountProsemirror.ts index 965f8a43..ff646a80 100644 --- a/components/Blocks/TextBlock/mountProsemirror.ts +++ b/components/Blocks/TextBlock/mountProsemirror.ts @@ -80,6 +80,23 @@ export function useMountProsemirror({ handlePaste, handleClickOn: (_view, _pos, node, _nodePos, _event, direct) => { if (!direct) return; + + // Check for didMention inline nodes + if (node?.type === schema.nodes.didMention) { + window.open( + didToBlueskyUrl(node.attrs.did), + "_blank", + "noopener,noreferrer", + ); + return; + } + + // Check for atMention inline nodes + if (node?.type === schema.nodes.atMention) { + const url = atUriToUrl(node.attrs.atURI); + window.open(url, "_blank", "noopener,noreferrer"); + return; + } if (node.nodeSize - 2 <= _pos) return; // Check for marks at the clicked position @@ -87,34 +104,13 @@ export function useMountProsemirror({ const nodeAt2 = node.nodeAt(Math.max(_pos - 2, 0)); // Check for link marks - let linkMark = nodeAt1?.marks.find((f) => f.type === schema.marks.link) || + let linkMark = + nodeAt1?.marks.find((f) => f.type === schema.marks.link) || nodeAt2?.marks.find((f) => f.type === schema.marks.link); if (linkMark) { window.open(linkMark.attrs.href, "_blank"); return; } - - // Check for didMention inline nodes - if (nodeAt1?.type === schema.nodes.didMention) { - window.open(didToBlueskyUrl(nodeAt1.attrs.did), "_blank", "noopener,noreferrer"); - return; - } - if (nodeAt2?.type === schema.nodes.didMention) { - window.open(didToBlueskyUrl(nodeAt2.attrs.did), "_blank", "noopener,noreferrer"); - return; - } - - // Check for atMention inline nodes - if (nodeAt1?.type === schema.nodes.atMention) { - const url = atUriToUrl(nodeAt1.attrs.atURI); - window.open(url, "_blank", "noopener,noreferrer"); - return; - } - if (nodeAt2?.type === schema.nodes.atMention) { - const url = atUriToUrl(nodeAt2.attrs.atURI); - window.open(url, "_blank", "noopener,noreferrer"); - return; - } }, dispatchTransaction, }, diff --git a/src/utils/mentionUtils.ts b/src/utils/mentionUtils.ts index 706928e0..8e97482f 100644 --- a/src/utils/mentionUtils.ts +++ b/src/utils/mentionUtils.ts @@ -19,11 +19,8 @@ export function atUriToUrl(atUri: string): string { const uri = new AtUri(atUri); if (isPublicationCollection(uri.collection)) { - // Publication URL: /lish/{did}/{rkey} - return `/lish/${uri.host}/${uri.rkey}`; + return `/lish/uri/${encodeURIComponent(atUri)}`; } else if (isDocumentCollection(uri.collection)) { - // Document URL - we need to resolve this via the API - // For now, create a redirect route that will handle it return `/lish/uri/${encodeURIComponent(atUri)}`; } @@ -42,7 +39,7 @@ export function atUriToUrl(atUri: string): string { export function handleMentionClick( e: MouseEvent | React.MouseEvent, type: "did" | "at-uri", - value: string + value: string, ) { e.preventDefault(); e.stopPropagation(); -- 2.51.2 From 81a61895f85c9b5b93c9fb26a2e44386d6b40d03 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Tue, 3 Feb 2026 10:10:45 +0000 Subject: [PATCH 03/51] log bsky post ingest errors --- appview/index.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/appview/index.ts b/appview/index.ts index 3588b659..139ea9fd 100644 --- a/appview/index.ts +++ b/appview/index.ts @@ -378,7 +378,10 @@ async function handleEvent(evt: Event) { // Now validate the record since we know it contains our quote param let record = AppBskyFeedPost.validateRecord(evt.record); - if (!record.success) return; + if (!record.success) { + console.log(record.error); + return; + } let embed: string | null = null; if ( -- 2.51.2 From 4ba387abfdb0fe2db2a7712ec9f122beaf14a374 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Tue, 3 Feb 2026 10:18:17 +0000 Subject: [PATCH 04/51] fix bsky post indexing function --- app/api/inngest/functions/index_post_mention.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/app/api/inngest/functions/index_post_mention.ts b/app/api/inngest/functions/index_post_mention.ts index 98ebef6d..80ff24f9 100644 --- a/app/api/inngest/functions/index_post_mention.ts +++ b/app/api/inngest/functions/index_post_mention.ts @@ -3,7 +3,10 @@ import { inngest } from "../client"; import { AtpAgent, AtUri } from "@atproto/api"; import { Json } from "supabase/database.types"; import { ids } from "lexicons/api/lexicons"; -import { Notification, pingIdentityToUpdateNotification } from "src/notifications"; +import { + Notification, + pingIdentityToUpdateNotification, +} from "src/notifications"; import { v7 } from "uuid"; import { idResolver } from "app/(home-pages)/reader/idResolver"; import { documentUriFilter } from "src/utils/uriHelpers"; @@ -60,7 +63,11 @@ export const index_post_mention = inngest.createFunction( let { data: pub, error } = await supabaseServerClient .from("publications") .select("*") - .or(`record->>base_path.eq.${url.host},record->>url.eq.https://${url.host}`) + .or( + `record->>base_path.eq.${url.host},record->>url.eq.https://${url.host}`, + ) + .order("uri", { ascending: false }) + .limit(1) .single(); if (!pub) { @@ -80,7 +87,9 @@ export const index_post_mention = inngest.createFunction( const docData = docDataArr?.[0]; if (!docData) { - return { message: `No document found for publication ${url.host}/${path[0]}` }; + return { + message: `No document found for publication ${url.host}/${path[0]}`, + }; } documentUri = docData.uri; -- 2.51.2 From dcb147ea4cfc390746728c1cfdb7be09f1c62ebc Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Tue, 3 Feb 2026 23:24:49 +0000 Subject: [PATCH 05/51] show proper error for not found bsky posts --- app/lish/[did]/[publication]/[rkey]/PostContent.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/lish/[did]/[publication]/[rkey]/PostContent.tsx b/app/lish/[did]/[publication]/[rkey]/PostContent.tsx index 8e53ba17..37e7980b 100644 --- a/app/lish/[did]/[publication]/[rkey]/PostContent.tsx +++ b/app/lish/[did]/[publication]/[rkey]/PostContent.tsx @@ -32,6 +32,7 @@ import { PublishedPageLinkBlock } from "./Blocks/PublishedPageBlock"; import { PublishedPollBlock } from "./Blocks/PublishedPollBlock"; import { PollData } from "./fetchPollData"; import { ButtonPrimary } from "components/Buttons"; +import { PostNotAvailable } from "components/Blocks/BlueskyPostBlock/BlueskyEmbed"; export function PostContent({ blocks, @@ -170,7 +171,7 @@ export let Block = ({ case PubLeafletBlocksBskyPost.isMain(b.block): { let uri = b.block.postRef.uri; let post = bskyPostData.find((p) => p.uri === uri); - if (!post) return
no prefetched post rip
; + if (!post) return ; return ( Date: Wed, 4 Feb 2026 00:25:11 -0500 Subject: [PATCH 06/51] fwd refs on action Button --- components/ActionBar/ActionButton.tsx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/components/ActionBar/ActionButton.tsx b/components/ActionBar/ActionButton.tsx index 17dac49e..d093d8ff 100644 --- a/components/ActionBar/ActionButton.tsx +++ b/components/ActionBar/ActionButton.tsx @@ -7,8 +7,9 @@ import { PopoverOpenContext } from "components/Popover/PopoverContext"; type ButtonProps = Omit; -export const ActionButton = ( - _props: ButtonProps & { +export const ActionButton = forwardRef< + HTMLButtonElement, + ButtonProps & { id?: string; icon: React.ReactNode; label: React.ReactNode; @@ -19,8 +20,8 @@ export const ActionButton = ( subtext?: string; labelOnMobile?: boolean; z?: boolean; - }, -) => { + } +>((_props, ref) => { let { id, icon, @@ -50,6 +51,7 @@ export const ActionButton = ( return ( ); -}; +}); +ActionButton.displayName = "ActionButton"; -- 2.51.2 From 0af3b2656e7a10ef647e1e1793c59a02c4ff8828 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Wed, 4 Feb 2026 16:56:45 +0000 Subject: [PATCH 07/51] only push inngest event in production --- app/api/oauth/[route]/route.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/app/api/oauth/[route]/route.ts b/app/api/oauth/[route]/route.ts index 903ce627..33b37f07 100644 --- a/app/api/oauth/[route]/route.ts +++ b/app/api/oauth/[route]/route.ts @@ -89,10 +89,11 @@ export async function GET( // Trigger migration if identity needs it const metadata = identity?.metadata as Record | null; if (metadata?.needsStandardSiteMigration) { - await inngest.send({ - name: "user/migrate-to-standard", - data: { did: session.did }, - }); + if (process.env.NODE_ENV === "production") + await inngest.send({ + name: "user/migrate-to-standard", + data: { did: session.did }, + }); } let { data: token } = await supabaseServerClient -- 2.51.2 From 9b47b512a937e3df720964e3f125393b79e66872 Mon Sep 17 00:00:00 2001 From: celine Date: Sat, 7 Feb 2026 04:23:32 -1000 Subject: [PATCH 08/51] Feature/recommend (#264) * just like a bunch of stuff sorry * cleaned up naming * refactor the bskypostcontent to use booleans for reply and quote rather than functions, include its own reply line, some renaming of variables to be more consistant * styling the grandparents in the threadviewer * styling the quotepage * adjustments to make everything more consistent * use BskyPostContent in the leaflet as well * fixing the width of thread and quote pages so they don't get too wide * make the reply line emcompass it's replies as well * little tweaks to the embed block * fixed a bunch of small things * init the recommend button, some stuff is broken tho * oops forgot the new files * add local like data fetching w/ batcher * adjusting interaction button styles for consistency * add user preference to show/hide recommends * made some chnages to canvas layout to better match * `add a little smoker to the recommend acion to keep it cute * copy fixes * spacing fixes --------- Co-authored-by: Jared Pereira --- .claude/settings.local.json | 15 + .claude/skills/lexicons.md | 298 ------------------ .../p/[didOrHandle]/getProfilePosts.ts | 27 +- app/(home-pages)/reader/getReaderFeed.ts | 3 + .../tag/[tag]/getDocumentsByTag.ts | 2 + app/api/oauth/[route]/route.ts | 3 +- app/api/rpc/[command]/get_publication_data.ts | 5 +- .../rpc/[command]/get_user_recommendations.ts | 40 +++ app/api/rpc/[command]/route.ts | 2 + app/lish/Subscribe.tsx | 4 +- .../[did]/[publication]/[rkey]/CanvasPage.tsx | 6 + .../[rkey]/Interactions/Interactions.tsx | 124 +++++--- .../[rkey]/Interactions/recommendAction.ts | 135 ++++++++ .../[rkey]/LinearDocumentPage.tsx | 6 +- .../[rkey]/PostHeader/PostHeader.tsx | 27 +- .../[did]/[publication]/[rkey]/PostPages.tsx | 2 + .../[publication]/[rkey]/getPostPageData.ts | 43 ++- .../dashboard/PublishedPostsLists.tsx | 16 +- .../dashboard/settings/PostOptions.tsx | 22 +- app/lish/[did]/[publication]/page.tsx | 22 +- app/lish/createPub/CreatePubForm.tsx | 1 + app/lish/createPub/UpdatePubForm.tsx | 3 +- app/lish/createPub/createPublication.ts | 32 +- app/lish/createPub/updatePublication.ts | 265 ++++++++++------ components/Canvas.tsx | 21 +- components/Icons/RecommendTiny.tsx | 37 +++ components/InteractionsPreview.tsx | 34 +- components/Pages/PublicationMetadata.tsx | 68 ++-- components/PostListing.tsx | 10 +- components/RecommendButton.tsx | 173 ++++++++++ contexts/DocumentContext.tsx | 1 + lexicons/api/types/pub/leaflet/publication.ts | 83 ++--- .../api/types/site/standard/publication.ts | 61 ++-- lexicons/pub/leaflet/publication.json | 4 + lexicons/site/standard/publication.json | 4 + lexicons/src/normalize.ts | 27 +- lexicons/src/publication.ts | 1 + package-lock.json | 16 + package.json | 1 + 39 files changed, 1023 insertions(+), 621 deletions(-) create mode 100644 .claude/settings.local.json delete mode 100644 .claude/skills/lexicons.md create mode 100644 app/api/rpc/[command]/get_user_recommendations.ts create mode 100644 app/lish/[did]/[publication]/[rkey]/Interactions/recommendAction.ts create mode 100644 components/Icons/RecommendTiny.tsx create mode 100644 components/RecommendButton.tsx diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 00000000..9371392c --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,15 @@ +{ + "permissions": { + "allow": [ + "mcp__acp__Edit", + "mcp__acp__Write", + "mcp__acp__Bash", + "mcp__primitive__say_hello", + "mcp__primitive__pending_delegations", + "mcp__primitive__claim_delegation", + "mcp__primitive__tasks_update", + "mcp__primitive__contexts_update", + "mcp__primitive__contexts_list" + ] + } +} diff --git a/.claude/skills/lexicons.md b/.claude/skills/lexicons.md deleted file mode 100644 index bc14e81b..00000000 --- a/.claude/skills/lexicons.md +++ /dev/null @@ -1,298 +0,0 @@ -# Lexicon System - -## Overview - -Lexicons define the schema for AT Protocol records. This project has two namespaces: -- **`pub.leaflet.*`** - Leaflet-specific lexicons (documents, publications, blocks, etc.) -- **`site.standard.*`** - Standard site lexicons for interoperability - -The lexicons are defined as TypeScript in `lexicons/src/`, built to JSON in `lexicons/pub/leaflet/` and `lexicons/site/standard/`, and TypeScript types are generated in `lexicons/api/`. - -## Key Files - -- **`lexicons/src/*.ts`** - Source definitions for `pub.leaflet.*` lexicons -- **`lexicons/site/standard/**/*.json`** - JSON definitions for `site.standard.*` lexicons (manually maintained) -- **`lexicons/build.ts`** - Builds TypeScript sources to JSON -- **`lexicons/api/`** - Generated TypeScript types and client -- **`package.json`** - Contains `lexgen` script - -## Running Lexicon Generation - -```bash -npm run lexgen -``` - -This runs: -1. `tsx ./lexicons/build.ts` - Builds `pub.leaflet.*` JSON from TypeScript -2. `lex gen-api` - Generates TypeScript types from all JSON lexicons -3. `tsx ./lexicons/fix-extensions.ts` - Fixes import extensions - -## Adding a New pub.leaflet Lexicon - -### 1. Create the Source Definition - -Create a file in `lexicons/src/` (e.g., `lexicons/src/myLexicon.ts`): - -```typescript -import { LexiconDoc } from "@atproto/lexicon"; - -export const PubLeafletMyLexicon: LexiconDoc = { - lexicon: 1, - id: "pub.leaflet.myLexicon", - defs: { - main: { - type: "record", // or "object" for non-record types - key: "tid", - record: { - type: "object", - required: ["field1"], - properties: { - field1: { type: "string", maxLength: 1000 }, - field2: { type: "integer", minimum: 0 }, - optionalRef: { type: "ref", ref: "other.lexicon#def" }, - }, - }, - }, - // Additional defs for sub-objects - subType: { - type: "object", - properties: { - nested: { type: "string" }, - }, - }, - }, -}; -``` - -### 2. Add to Build - -Update `lexicons/build.ts`: - -```typescript -import { PubLeafletMyLexicon } from "./src/myLexicon"; - -const lexicons = [ - // ... existing lexicons - PubLeafletMyLexicon, -]; -``` - -### 3. Update lexgen Command (if needed) - -If your lexicon is at the top level of `pub/leaflet/` (not in a subdirectory), add it to the `lexgen` script in `package.json`: - -```json -"lexgen": "tsx ./lexicons/build.ts && lex gen-api ./lexicons/api ./lexicons/pub/leaflet/document.json ./lexicons/pub/leaflet/myLexicon.json ./lexicons/pub/leaflet/*/* ..." -``` - -Note: Files in subdirectories (`pub/leaflet/*/*`) are automatically included. - -### 4. Add to authFullPermissions (for record types) - -If your lexicon is a record type that users should be able to create/update/delete, add it to the `authFullPermissions` permission set in `lexicons/src/authFullPermissions.ts`: - -```typescript -import { PubLeafletMyLexicon } from "./myLexicon"; - -// In the permissions collection array: -collection: [ - // ... existing lexicons - PubLeafletMyLexicon.id, -], -``` - -### 5. Regenerate Types - -```bash -npm run lexgen -``` - -### 6. Use the Generated Types - -```typescript -import { PubLeafletMyLexicon } from "lexicons/api"; - -// Type for the record -type MyRecord = PubLeafletMyLexicon.Record; - -// Validation -const result = PubLeafletMyLexicon.validateRecord(data); -if (result.success) { - // result.value is typed -} - -// Type guard -if (PubLeafletMyLexicon.isRecord(data)) { - // data is typed as Record -} -``` - -## Adding a New site.standard Lexicon - -### 1. Create the JSON Definition - -Create a file in `lexicons/site/standard/` (e.g., `lexicons/site/standard/myType.json`): - -```json -{ - "lexicon": 1, - "id": "site.standard.myType", - "defs": { - "main": { - "type": "record", - "key": "tid", - "record": { - "type": "object", - "required": ["field1"], - "properties": { - "field1": { - "type": "string", - "maxLength": 1000 - } - } - } - } - } -} -``` - -### 2. Regenerate Types - -```bash -npm run lexgen -``` - -The `site/*/* site/*/*/*` globs in the lexgen command automatically pick up new files. - -## Common Lexicon Patterns - -### Referencing Other Lexicons - -```typescript -// Reference another lexicon's main def -{ type: "ref", ref: "pub.leaflet.publication" } - -// Reference a specific def within a lexicon -{ type: "ref", ref: "pub.leaflet.publication#theme" } - -// Reference within the same lexicon -{ type: "ref", ref: "#myDef" } -``` - -### Union Types - -```typescript -{ - type: "union", - refs: [ - "pub.leaflet.pages.linearDocument", - "pub.leaflet.pages.canvas", - ], -} - -// Open union (allows unknown types) -{ - type: "union", - closed: false, // default is true - refs: ["pub.leaflet.content"], -} -``` - -### Blob Types (for images/files) - -```typescript -{ - type: "blob", - accept: ["image/*"], // or specific types like ["image/png", "image/jpeg"] - maxSize: 1000000, // bytes -} -``` - -### Color Types - -The project has color types defined: -- `pub.leaflet.theme.color#rgb` / `#rgba` -- `site.standard.theme.color#rgb` / `#rgba` - -```typescript -// In lexicons/src/theme.ts -export const ColorUnion = { - type: "union", - refs: [ - "pub.leaflet.theme.color#rgba", - "pub.leaflet.theme.color#rgb", - ], -}; -``` - -## Normalization Between Formats - -Use `lexicons/src/normalize.ts` to convert between `pub.leaflet` and `site.standard` formats: - -```typescript -import { - normalizeDocument, - normalizePublication, - isLeafletDocument, - isStandardDocument, - getDocumentPages, -} from "lexicons/src/normalize"; - -// Normalize a document from either format -const normalized = normalizeDocument(record); -if (normalized) { - // normalized is always in site.standard.document format - console.log(normalized.title, normalized.site); - - // Get pages if content is pub.leaflet.content - const pages = getDocumentPages(normalized); -} - -// Normalize a publication -const pub = normalizePublication(record); -if (pub) { - console.log(pub.name, pub.url); -} -``` - -## Handling in Appview (Firehose Consumer) - -When processing records from the firehose in `appview/index.ts`: - -```typescript -import { ids } from "lexicons/api/lexicons"; -import { PubLeafletMyLexicon } from "lexicons/api"; - -// In filterCollections: -filterCollections: [ - ids.PubLeafletMyLexicon, - // ... -], - -// In handleEvent: -if (evt.collection === ids.PubLeafletMyLexicon) { - if (evt.event === "create" || evt.event === "update") { - let record = PubLeafletMyLexicon.validateRecord(evt.record); - if (!record.success) return; - - // Store in database - await supabase.from("my_table").upsert({ - uri: evt.uri.toString(), - data: record.value as Json, - }); - } - if (evt.event === "delete") { - await supabase.from("my_table").delete().eq("uri", evt.uri.toString()); - } -} -``` - -## Publishing Lexicons - -To publish lexicons to an AT Protocol PDS: - -```bash -npm run publish-lexicons -``` - -This runs `lexicons/publish.ts` which publishes lexicons to the configured PDS. diff --git a/app/(home-pages)/p/[didOrHandle]/getProfilePosts.ts b/app/(home-pages)/p/[didOrHandle]/getProfilePosts.ts index 131b821f..92ca1bf6 100644 --- a/app/(home-pages)/p/[didOrHandle]/getProfilePosts.ts +++ b/app/(home-pages)/p/[didOrHandle]/getProfilePosts.ts @@ -26,6 +26,7 @@ export async function getProfilePosts( `*, comments_on_documents(count), document_mentions_in_bsky(count), + recommends_on_documents(count), documents_in_publications(publications(*))`, ) .like("uri", `at://${did}/%`) @@ -39,18 +40,19 @@ export async function getProfilePosts( ); } - let [{ data: rawDocs }, { data: rawPubs }, { data: profile }] = await Promise.all([ - query, - supabaseServerClient - .from("publications") - .select("*") - .eq("identity_did", did), - supabaseServerClient - .from("bsky_profiles") - .select("handle") - .eq("did", did) - .single(), - ]); + let [{ data: rawDocs }, { data: rawPubs }, { data: profile }] = + await Promise.all([ + query, + supabaseServerClient + .from("publications") + .select("*") + .eq("identity_did", did), + supabaseServerClient + .from("bsky_profiles") + .select("handle") + .eq("did", did) + .single(), + ]); // Deduplicate records that may exist under both pub.leaflet and site.standard namespaces const docs = deduplicateByUriOrdered(rawDocs || []); @@ -82,6 +84,7 @@ export async function getProfilePosts( sort_date: doc.sort_date, comments_on_documents: doc.comments_on_documents, document_mentions_in_bsky: doc.document_mentions_in_bsky, + recommends_on_documents: doc.recommends_on_documents, }, }; diff --git a/app/(home-pages)/reader/getReaderFeed.ts b/app/(home-pages)/reader/getReaderFeed.ts index d3996eb6..4ff16591 100644 --- a/app/(home-pages)/reader/getReaderFeed.ts +++ b/app/(home-pages)/reader/getReaderFeed.ts @@ -32,6 +32,7 @@ export async function getReaderFeed( `*, comments_on_documents(count), document_mentions_in_bsky(count), + recommends_on_documents(count), documents_in_publications!inner(publications!inner(*, publication_subscriptions!inner(*)))`, ) .eq( @@ -76,6 +77,7 @@ export async function getReaderFeed( documents: { comments_on_documents: post.comments_on_documents, document_mentions_in_bsky: post.document_mentions_in_bsky, + recommends_on_documents: post.recommends_on_documents, data: normalizedData, uri: post.uri, sort_date: post.sort_date, @@ -112,5 +114,6 @@ export type Post = { sort_date: string; comments_on_documents: { count: number }[] | undefined; document_mentions_in_bsky: { count: number }[] | undefined; + recommends_on_documents: { count: number }[] | undefined; }; }; diff --git a/app/(home-pages)/tag/[tag]/getDocumentsByTag.ts b/app/(home-pages)/tag/[tag]/getDocumentsByTag.ts index ebaffbe0..bb6394c9 100644 --- a/app/(home-pages)/tag/[tag]/getDocumentsByTag.ts +++ b/app/(home-pages)/tag/[tag]/getDocumentsByTag.ts @@ -21,6 +21,7 @@ export async function getDocumentsByTag( `*, comments_on_documents(count), document_mentions_in_bsky(count), + recommends_on_documents(count), documents_in_publications(publications(*))`, ) .contains("data->tags", `["${tag}"]`) @@ -67,6 +68,7 @@ export async function getDocumentsByTag( documents: { comments_on_documents: doc.comments_on_documents, document_mentions_in_bsky: doc.document_mentions_in_bsky, + recommends_on_documents: doc.recommends_on_documents, data: normalizedData, uri: doc.uri, sort_date: doc.sort_date, diff --git a/app/api/oauth/[route]/route.ts b/app/api/oauth/[route]/route.ts index 33b37f07..db2ab8f4 100644 --- a/app/api/oauth/[route]/route.ts +++ b/app/api/oauth/[route]/route.ts @@ -105,7 +105,7 @@ export async function GET( }) .select() .single(); - + console.log({ token }); if (token) await setAuthToken(token.id); // Process successful authentication here @@ -114,6 +114,7 @@ export async function GET( console.log("User authenticated as:", session.did); return handleAction(s.action, redirectPath); } catch (e) { + console.log(e); redirect(redirectPath); } } diff --git a/app/api/rpc/[command]/get_publication_data.ts b/app/api/rpc/[command]/get_publication_data.ts index b33b9203..09cc91f4 100644 --- a/app/api/rpc/[command]/get_publication_data.ts +++ b/app/api/rpc/[command]/get_publication_data.ts @@ -40,7 +40,8 @@ export const get_publication_data = makeRoute({ documents_in_publications(documents( *, comments_on_documents(count), - document_mentions_in_bsky(count) + document_mentions_in_bsky(count), + recommends_on_documents(count) )), publication_subscriptions(*, identities(bsky_profiles(*))), publication_domains(*), @@ -87,6 +88,8 @@ export const get_publication_data = makeRoute({ data: dip.documents.data, commentsCount: dip.documents.comments_on_documents[0]?.count || 0, mentionsCount: dip.documents.document_mentions_in_bsky[0]?.count || 0, + recommendsCount: + dip.documents.recommends_on_documents?.[0]?.count || 0, }; }) .filter((d): d is NonNullable => d !== null); diff --git a/app/api/rpc/[command]/get_user_recommendations.ts b/app/api/rpc/[command]/get_user_recommendations.ts new file mode 100644 index 00000000..1c608695 --- /dev/null +++ b/app/api/rpc/[command]/get_user_recommendations.ts @@ -0,0 +1,40 @@ +import { z } from "zod"; +import { makeRoute } from "../lib"; +import type { Env } from "./route"; +import { getIdentityData } from "actions/getIdentityData"; + +export type GetUserRecommendationsReturnType = Awaited< + ReturnType<(typeof get_user_recommendations)["handler"]> +>; + +export const get_user_recommendations = makeRoute({ + route: "get_user_recommendations", + input: z.object({ + documentUris: z.array(z.string()), + }), + handler: async ({ documentUris }, { supabase }: Pick) => { + const identity = await getIdentityData(); + const currentUserDid = identity?.atp_did; + + if (!currentUserDid || documentUris.length === 0) { + return { + result: {} as Record, + }; + } + + const { data: recommendations } = await supabase + .from("recommends_on_documents") + .select("document") + .eq("recommender_did", currentUserDid) + .in("document", documentUris); + + const recommendedSet = new Set(recommendations?.map((r) => r.document)); + + const result: Record = {}; + for (const uri of documentUris) { + result[uri] = recommendedSet.has(uri); + } + + return { result }; + }, +}); diff --git a/app/api/rpc/[command]/route.ts b/app/api/rpc/[command]/route.ts index df7a865b..c6ba128e 100644 --- a/app/api/rpc/[command]/route.ts +++ b/app/api/rpc/[command]/route.ts @@ -14,6 +14,7 @@ import { get_publication_data } from "./get_publication_data"; import { search_publication_names } from "./search_publication_names"; import { search_publication_documents } from "./search_publication_documents"; import { get_profile_data } from "./get_profile_data"; +import { get_user_recommendations } from "./get_user_recommendations"; let supabase = createClient( process.env.NEXT_PUBLIC_SUPABASE_API_URL as string, @@ -41,6 +42,7 @@ let Routes = [ search_publication_names, search_publication_documents, get_profile_data, + get_user_recommendations, ]; export async function POST( req: Request, diff --git a/app/lish/Subscribe.tsx b/app/lish/Subscribe.tsx index 4cfd5326..eb2e1411 100644 --- a/app/lish/Subscribe.tsx +++ b/app/lish/Subscribe.tsx @@ -87,7 +87,9 @@ export const ManageSubscription = (props: { return ( Manage Subscription +
+ Manage Subscription +
} >
diff --git a/app/lish/[did]/[publication]/[rkey]/CanvasPage.tsx b/app/lish/[did]/[publication]/[rkey]/CanvasPage.tsx index 9b3a1058..74e3763b 100644 --- a/app/lish/[did]/[publication]/[rkey]/CanvasPage.tsx +++ b/app/lish/[did]/[publication]/[rkey]/CanvasPage.tsx @@ -71,6 +71,7 @@ export function CanvasPage({ preferences={preferences} commentsCount={getCommentCount(document.comments_on_documents, pageId)} quotesCount={getQuoteCount(document.quotesAndMentions, pageId)} + recommendsCount={document.recommendsCount} /> { let isMobile = useIsMobile(); return ( @@ -216,8 +219,10 @@ const CanvasMetadata = (props: { {!props.isSubpage && ( @@ -233,6 +238,7 @@ const CanvasMetadata = (props: { data={props.data} profile={props.profile} preferences={props.preferences} + isCanvas /> diff --git a/app/lish/[did]/[publication]/[rkey]/Interactions/Interactions.tsx b/app/lish/[did]/[publication]/[rkey]/Interactions/Interactions.tsx index 9dcc4421..36713943 100644 --- a/app/lish/[did]/[publication]/[rkey]/Interactions/Interactions.tsx +++ b/app/lish/[did]/[publication]/[rkey]/Interactions/Interactions.tsx @@ -18,6 +18,9 @@ import { useIdentityData } from "components/IdentityProvider"; import { ManageSubscription, SubscribeWithBluesky } from "app/lish/Subscribe"; import { EditTiny } from "components/Icons/EditTiny"; import { getPublicationURL } from "app/lish/createPub/getPublicationURL"; +import { RecommendButton } from "components/RecommendButton"; +import { ButtonSecondary } from "components/Buttons"; +import { Separator } from "components/Layout"; export type InteractionState = { drawerOpen: undefined | boolean; @@ -105,12 +108,18 @@ export function openInteractionDrawer( export const Interactions = (props: { quotesCount: number; commentsCount: number; + recommendsCount: number; className?: string; showComments: boolean; showMentions: boolean; + showRecommends: boolean; pageId?: string; }) => { - const { uri: document_uri, quotesAndMentions, normalizedDocument } = useDocument(); + const { + uri: document_uri, + quotesAndMentions, + normalizedDocument, + } = useDocument(); let { identity } = useIdentityData(); let { drawerOpen, drawer, pageId } = useInteractionState(document_uri); @@ -124,13 +133,24 @@ export const Interactions = (props: { const tags = normalizedDocument.tags; const tagCount = tags?.length || 0; + let interactionsAvailable = + props.showComments || props.showMentions || props.showRecommends; + return ( -
- {tagCount > 0 && } +
+ {props.showRecommends === false ? null : ( + + )} + {/*MENTIONS BUTTON*/} {props.quotesCount === 0 || props.showMentions === false ? null : ( )} + {/*COMMENT BUTTON*/} {props.showComments === false ? null : ( )} + + {tagCount > 0 && ( + <> + {interactionsAvailable && } + + + )}
); }; @@ -163,12 +191,20 @@ export const Interactions = (props: { export const ExpandedInteractions = (props: { quotesCount: number; commentsCount: number; + recommendsCount: number; className?: string; showComments: boolean; showMentions: boolean; + showRecommends: boolean; pageId?: string; }) => { - const { uri: document_uri, quotesAndMentions, normalizedDocument, publication, leafletId } = useDocument(); + const { + uri: document_uri, + quotesAndMentions, + normalizedDocument, + publication, + leafletId, + } = useDocument(); let { identity } = useIdentityData(); let { drawerOpen, drawer, pageId } = useInteractionState(document_uri); @@ -182,7 +218,8 @@ export const ExpandedInteractions = (props: { const tags = normalizedDocument.tags; const tagCount = tags?.length || 0; - let noInteractions = !props.showComments && !props.showMentions; + let noInteractions = + !props.showComments && !props.showMentions && !props.showRecommends; let subscribed = identity?.atp_did && @@ -191,11 +228,6 @@ export const ExpandedInteractions = (props: { (s) => s.identity === identity.atp_did, ); - let isAuthor = - identity && - identity.atp_did === publication?.identity_did && - leafletId; - return (
) : ( - <> -
+
+
+ {props.showRecommends === false ? null : ( + + )} {props.quotesCount === 0 || !props.showMentions ? null : ( - + {props.quotesCount} + + Mention{props.quotesCount > 1 ? "s" : ""} + )} {!props.showComments ? null : ( - + Comment{props.commentsCount > 1 ? "s" : ""} + )}
- + {subscribed && publication && ( + + )} +
)} - {subscribed && publication && ( - - )}
); @@ -313,7 +349,10 @@ const TagList = (props: { className?: string; tags: string[] | undefined }) => {
); }; -export function getQuoteCount(quotesAndMentions: { uri: string; link?: string }[], pageId?: string) { +export function getQuoteCount( + quotesAndMentions: { uri: string; link?: string }[], + pageId?: string, +) { return getQuoteCountFromArray(quotesAndMentions, pageId); } @@ -338,7 +377,10 @@ export function getQuoteCountFromArray( } } -export function getCommentCount(comments: CommentOnDocument[], pageId?: string) { +export function getCommentCount( + comments: CommentOnDocument[], + pageId?: string, +) { if (pageId) return comments.filter( (c) => (c.record as PubLeafletComment.Record)?.onPage === pageId, @@ -362,7 +404,7 @@ const EditButton = (props: { return ( Edit Post diff --git a/app/lish/[did]/[publication]/[rkey]/Interactions/recommendAction.ts b/app/lish/[did]/[publication]/[rkey]/Interactions/recommendAction.ts new file mode 100644 index 00000000..d0295f26 --- /dev/null +++ b/app/lish/[did]/[publication]/[rkey]/Interactions/recommendAction.ts @@ -0,0 +1,135 @@ +"use server"; + +import { AtpBaseClient, PubLeafletInteractionsRecommend } from "lexicons/api"; +import { getIdentityData } from "actions/getIdentityData"; +import { restoreOAuthSession, OAuthSessionError } from "src/atproto-oauth"; +import { TID } from "@atproto/common"; +import { AtUri, Un$Typed } from "@atproto/api"; +import { supabaseServerClient } from "supabase/serverClient"; +import { Json } from "supabase/database.types"; + +type RecommendResult = + | { success: true; uri: string } + | { + success: false; + error: OAuthSessionError | { type: string; message: string }; + }; + +export async function recommendAction(args: { + document: string; +}): Promise { + console.log("recommend action..."); + let identity = await getIdentityData(); + if (!identity || !identity.atp_did) { + return { + success: false, + error: { + type: "oauth_session_expired", + message: "Not authenticated", + did: "", + }, + }; + } + + const sessionResult = await restoreOAuthSession(identity.atp_did); + if (!sessionResult.ok) { + return { success: false, error: sessionResult.error }; + } + let credentialSession = sessionResult.value; + let agent = new AtpBaseClient( + credentialSession.fetchHandler.bind(credentialSession), + ); + + let record: Un$Typed = { + subject: args.document, + createdAt: new Date().toISOString(), + }; + + let rkey = TID.nextStr(); + let uri = AtUri.make( + credentialSession.did!, + "pub.leaflet.interactions.recommend", + rkey, + ); + + await agent.pub.leaflet.interactions.recommend.create( + { rkey, repo: credentialSession.did! }, + record, + ); + + let res = await supabaseServerClient.from("recommends_on_documents").upsert({ + uri: uri.toString(), + document: args.document, + recommender_did: credentialSession.did!, + record: { + $type: "pub.leaflet.interactions.recommend", + ...record, + } as unknown as Json, + }); + console.log(res); + + return { + success: true, + uri: uri.toString(), + }; +} + +export async function unrecommendAction(args: { + document: string; +}): Promise { + let identity = await getIdentityData(); + if (!identity || !identity.atp_did) { + return { + success: false, + error: { + type: "oauth_session_expired", + message: "Not authenticated", + did: "", + }, + }; + } + + const sessionResult = await restoreOAuthSession(identity.atp_did); + if (!sessionResult.ok) { + return { success: false, error: sessionResult.error }; + } + let credentialSession = sessionResult.value; + let agent = new AtpBaseClient( + credentialSession.fetchHandler.bind(credentialSession), + ); + + // Find the existing recommend record + const { data: existingRecommend } = await supabaseServerClient + .from("recommends_on_documents") + .select("uri") + .eq("document", args.document) + .eq("recommender_did", credentialSession.did!) + .single(); + + if (!existingRecommend) { + return { + success: false, + error: { + type: "not_found", + message: "Recommend not found", + }, + }; + } + + let uri = new AtUri(existingRecommend.uri); + + await agent.pub.leaflet.interactions.recommend.delete({ + rkey: uri.rkey, + repo: credentialSession.did!, + }); + + await supabaseServerClient + .from("recommends_on_documents") + .delete() + .eq("uri", existingRecommend.uri); + + return { + success: true, + uri: existingRecommend.uri, + }; +} diff --git a/app/lish/[did]/[publication]/[rkey]/LinearDocumentPage.tsx b/app/lish/[did]/[publication]/[rkey]/LinearDocumentPage.tsx index 1046025a..04b9d7d9 100644 --- a/app/lish/[did]/[publication]/[rkey]/LinearDocumentPage.tsx +++ b/app/lish/[did]/[publication]/[rkey]/LinearDocumentPage.tsx @@ -87,8 +87,12 @@ export function LinearDocumentPage({ pageId={pageId} showComments={preferences.showComments !== false} showMentions={preferences.showMentions !== false} - commentsCount={getCommentCount(document.comments_on_documents, pageId) || 0} + showRecommends={preferences.showRecommends !== false} + commentsCount={ + getCommentCount(document.comments_on_documents, pageId) || 0 + } quotesCount={getQuoteCount(document.quotesAndMentions, pageId) || 0} + recommendsCount={document.recommendsCount} /> {!hasPageBackground &&
} diff --git a/app/lish/[did]/[publication]/[rkey]/PostHeader/PostHeader.tsx b/app/lish/[did]/[publication]/[rkey]/PostHeader/PostHeader.tsx index 86216159..1159683a 100644 --- a/app/lish/[did]/[publication]/[rkey]/PostHeader/PostHeader.tsx +++ b/app/lish/[did]/[publication]/[rkey]/PostHeader/PostHeader.tsx @@ -18,7 +18,12 @@ import { ProfilePopover } from "components/ProfilePopover"; export function PostHeader(props: { data: PostPageData; profile: ProfileViewDetailed; - preferences: { showComments?: boolean; showMentions?: boolean }; + preferences: { + showComments?: boolean; + showMentions?: boolean; + showRecommends?: boolean; + }; + isCanvas?: boolean; }) { let { identity } = useIdentityData(); let document = props.data; @@ -84,12 +89,20 @@ export function PostHeader(props: { ) : null}
- + {!props.isCanvas && ( + + )} } /> diff --git a/app/lish/[did]/[publication]/[rkey]/PostPages.tsx b/app/lish/[did]/[publication]/[rkey]/PostPages.tsx index 234f2041..9b2a8a67 100644 --- a/app/lish/[did]/[publication]/[rkey]/PostPages.tsx +++ b/app/lish/[did]/[publication]/[rkey]/PostPages.tsx @@ -170,6 +170,7 @@ export type SharedPageProps = { preferences: { showComments?: boolean; showMentions?: boolean; + showRecommends?: boolean; showPrevNext?: boolean; }; pubRecord?: NormalizedPublication | null; @@ -233,6 +234,7 @@ export function PostPages({ preferences: { showComments?: boolean; showMentions?: boolean; + showRecommends?: boolean; showPrevNext?: boolean; }; pollData: PollData[]; diff --git a/app/lish/[did]/[publication]/[rkey]/getPostPageData.ts b/app/lish/[did]/[publication]/[rkey]/getPostPageData.ts index 9f7f44a2..7f16418a 100644 --- a/app/lish/[did]/[publication]/[rkey]/getPostPageData.ts +++ b/app/lish/[did]/[publication]/[rkey]/getPostPageData.ts @@ -22,7 +22,8 @@ export async function getPostPageData(did: string, rkey: string) { publication_subscriptions(*)) ), document_mentions_in_bsky(*), - leaflets_in_publications(*) + leaflets_in_publications(*), + recommends_on_documents(count) `, ) .or(documentUriFilter(did, rkey)) @@ -33,12 +34,15 @@ export async function getPostPageData(did: string, rkey: string) { if (!document) return null; // Normalize the document record - this is the primary way consumers should access document data - const normalizedDocument = normalizeDocumentRecord(document.data, document.uri); + const normalizedDocument = normalizeDocumentRecord( + document.data, + document.uri, + ); if (!normalizedDocument) return null; // Normalize the publication record - this is the primary way consumers should access publication data const normalizedPublication = normalizePublicationRecord( - document.documents_in_publications[0]?.publications?.record + document.documents_in_publications[0]?.publications?.record, ); // Fetch constellation backlinks for mentions @@ -83,7 +87,10 @@ export async function getPostPageData(did: string, rkey: string) { // Filter and sort documents by publishedAt const sortedDocs = allDocs .map((dip) => { - const normalizedData = normalizeDocumentRecord(dip?.documents?.data, dip?.documents?.uri); + const normalizedData = normalizeDocumentRecord( + dip?.documents?.data, + dip?.documents?.uri, + ); return { uri: dip?.documents?.uri, title: normalizedData?.title, @@ -98,7 +105,9 @@ export async function getPostPageData(did: string, rkey: string) { ); // Find current document index - const currentIndex = sortedDocs.findIndex((doc) => doc.uri === document.uri); + const currentIndex = sortedDocs.findIndex( + (doc) => doc.uri === document.uri, + ); if (currentIndex !== -1) { prevNext = { @@ -122,13 +131,21 @@ export async function getPostPageData(did: string, rkey: string) { // Build explicit publication context for consumers const rawPub = document.documents_in_publications[0]?.publications; - const publication = rawPub ? { - uri: rawPub.uri, - name: rawPub.name, - identity_did: rawPub.identity_did, - record: rawPub.record as PubLeafletPublication.Record | SiteStandardPublication.Record | null, - publication_subscriptions: rawPub.publication_subscriptions || [], - } : null; + const publication = rawPub + ? { + uri: rawPub.uri, + name: rawPub.name, + identity_did: rawPub.identity_did, + record: rawPub.record as + | PubLeafletPublication.Record + | SiteStandardPublication.Record + | null, + publication_subscriptions: rawPub.publication_subscriptions || [], + } + : null; + + // Get recommends count from the aggregated query result + const recommendsCount = document.recommends_on_documents?.[0]?.count ?? 0; return { ...document, @@ -143,6 +160,8 @@ export async function getPostPageData(did: string, rkey: string) { comments: document.comments_on_documents, mentions: document.document_mentions_in_bsky, leafletId: document.leaflets_in_publications[0]?.leaflet || null, + // Recommends data + recommendsCount, }; } diff --git a/app/lish/[did]/[publication]/dashboard/PublishedPostsLists.tsx b/app/lish/[did]/[publication]/dashboard/PublishedPostsLists.tsx index b8a24406..a7e4c630 100644 --- a/app/lish/[did]/[publication]/dashboard/PublishedPostsLists.tsx +++ b/app/lish/[did]/[publication]/dashboard/PublishedPostsLists.tsx @@ -60,7 +60,9 @@ export function PublishedPostsList(props: { function PublishedPostItem(props: { doc: PublishedDocument; - publication: NonNullable["data"]>["publication"]>; + publication: NonNullable< + NonNullable["data"]>["publication"] + >; pubRecord: ReturnType; showPageBackground: boolean; }) { @@ -94,10 +96,7 @@ function PublishedPostItem(props: {
{leaflet && leaflet.permission_tokens && ( <> - + @@ -129,9 +128,7 @@ function PublishedPostItem(props: {
{doc.record.description ? ( -

- {doc.record.description} -

+

{doc.record.description}

) : null}
{doc.record.publishedAt ? ( @@ -140,9 +137,12 @@ function PublishedPostItem(props: {
diff --git a/app/lish/[did]/[publication]/dashboard/settings/PostOptions.tsx b/app/lish/[did]/[publication]/dashboard/settings/PostOptions.tsx index 076568ef..70ae3d75 100644 --- a/app/lish/[did]/[publication]/dashboard/settings/PostOptions.tsx +++ b/app/lish/[did]/[publication]/dashboard/settings/PostOptions.tsx @@ -29,6 +29,11 @@ export const PostOptions = (props: { ? true : record.preferences.showMentions, ); + let [showRecommends, setShowRecommends] = useState( + record?.preferences?.showRecommends === undefined + ? true + : record.preferences.showRecommends, + ); let [showPrevNext, setShowPrevNext] = useState( record?.preferences?.showPrevNext === undefined ? true @@ -53,6 +58,7 @@ export const PostOptions = (props: { showComments: showComments, showMentions: showMentions, showPrevNext: showPrevNext, + showRecommends: showRecommends, }, }); toast({ type: "success", content: Posts Updated! }); @@ -99,7 +105,21 @@ export const PostOptions = (props: {
Show Mentions
- Display a list of posts on Bluesky that mention your post + Display a list Bluesky mentions about your post +
+
+ + + { + setShowRecommends(!showRecommends); + }} + > +
+
Show Recommends
+
+ Allow readers to recommend/like your post
diff --git a/app/lish/[did]/[publication]/page.tsx b/app/lish/[did]/[publication]/page.tsx index 0a51e1b4..457f38b2 100644 --- a/app/lish/[did]/[publication]/page.tsx +++ b/app/lish/[did]/[publication]/page.tsx @@ -38,7 +38,8 @@ export default async function Publication(props: { documents_in_publications(documents( *, comments_on_documents(count), - document_mentions_in_bsky(count) + document_mentions_in_bsky(count), + recommends_on_documents(count) )) `, ) @@ -119,7 +120,9 @@ export default async function Publication(props: { }) .map((doc) => { if (!doc.documents) return null; - const doc_record = normalizeDocumentRecord(doc.documents.data); + const doc_record = normalizeDocumentRecord( + doc.documents.data, + ); if (!doc_record) return null; let uri = new AtUri(doc.documents.uri); let quotes = @@ -128,6 +131,8 @@ export default async function Publication(props: { record?.preferences?.showComments === false ? 0 : doc.documents.comments_on_documents[0].count || 0; + let recommends = + doc.documents.recommends_on_documents?.[0]?.count || 0; let tags = doc_record.tags || []; return ( @@ -143,7 +148,7 @@ export default async function Publication(props: {

-
+

{doc_record.publishedAt && ( )}{" "}

- {comments > 0 || quotes > 0 || tags.length > 0 ? ( - - ) : ( - "" - )} +
diff --git a/app/lish/createPub/CreatePubForm.tsx b/app/lish/createPub/CreatePubForm.tsx index 2dbe924e..749b4397 100644 --- a/app/lish/createPub/CreatePubForm.tsx +++ b/app/lish/createPub/CreatePubForm.tsx @@ -58,6 +58,7 @@ export const CreatePubForm = () => { showComments: true, showMentions: true, showPrevNext: true, + showRecommends: true, }, }); diff --git a/app/lish/createPub/UpdatePubForm.tsx b/app/lish/createPub/UpdatePubForm.tsx index 3672c5f8..f9844796 100644 --- a/app/lish/createPub/UpdatePubForm.tsx +++ b/app/lish/createPub/UpdatePubForm.tsx @@ -88,6 +88,7 @@ export const EditPubForm = (props: { showComments: showComments, showMentions: showMentions, showPrevNext: showPrevNext, + showRecommends: record?.preferences?.showRecommends ?? true, }, }); toast({ type: "success", content: "Updated!" }); @@ -194,8 +195,6 @@ export const EditPubForm = (props: {

- - ); diff --git a/app/lish/createPub/createPublication.ts b/app/lish/createPub/createPublication.ts index a705a0e6..5adcc7ee 100644 --- a/app/lish/createPub/createPublication.ts +++ b/app/lish/createPub/createPublication.ts @@ -5,10 +5,7 @@ import { PubLeafletPublication, SiteStandardPublication, } from "lexicons/api"; -import { - restoreOAuthSession, - OAuthSessionError, -} from "src/atproto-oauth"; +import { restoreOAuthSession, OAuthSessionError } from "src/atproto-oauth"; import { getIdentityData } from "actions/getIdentityData"; import { supabaseServerClient } from "supabase/serverClient"; import { Json } from "supabase/database.types"; @@ -76,7 +73,11 @@ export async function createPublication({ // Build record based on publication type let record: SiteStandardPublication.Record | PubLeafletPublication.Record; - let iconBlob: Awaited>["data"]["blob"] | undefined; + let iconBlob: + | Awaited< + ReturnType + >["data"]["blob"] + | undefined; // Upload the icon if provided if (iconFile && iconFile.size > 0) { @@ -97,16 +98,29 @@ export async function createPublication({ ...(iconBlob && { icon: iconBlob }), basicTheme: { $type: "site.standard.theme.basic", - background: { $type: "site.standard.theme.color#rgb", ...PubThemeDefaultsRGB.background }, - foreground: { $type: "site.standard.theme.color#rgb", ...PubThemeDefaultsRGB.foreground }, - accent: { $type: "site.standard.theme.color#rgb", ...PubThemeDefaultsRGB.accent }, - accentForeground: { $type: "site.standard.theme.color#rgb", ...PubThemeDefaultsRGB.accentForeground }, + background: { + $type: "site.standard.theme.color#rgb", + ...PubThemeDefaultsRGB.background, + }, + foreground: { + $type: "site.standard.theme.color#rgb", + ...PubThemeDefaultsRGB.foreground, + }, + accent: { + $type: "site.standard.theme.color#rgb", + ...PubThemeDefaultsRGB.accent, + }, + accentForeground: { + $type: "site.standard.theme.color#rgb", + ...PubThemeDefaultsRGB.accentForeground, + }, }, preferences: { showInDiscover: preferences.showInDiscover, showComments: preferences.showComments, showMentions: preferences.showMentions, showPrevNext: preferences.showPrevNext, + showRecommends: preferences.showRecommends, }, } satisfies SiteStandardPublication.Record; } else { diff --git a/app/lish/createPub/updatePublication.ts b/app/lish/createPub/updatePublication.ts index a9bd3e56..350b3f88 100644 --- a/app/lish/createPub/updatePublication.ts +++ b/app/lish/createPub/updatePublication.ts @@ -77,7 +77,9 @@ async function withPublicationUpdate( } const aturi = new AtUri(existingPub.uri); - const publicationType = getPublicationType(aturi.collection) as PublicationType; + const publicationType = getPublicationType( + aturi.collection, + ) as PublicationType; // Normalize existing record const normalizedPub = normalizePublicationRecord(existingPub.record); @@ -128,7 +130,11 @@ interface RecordOverrides { } /** Merges override with existing value, respecting explicit undefined */ -function resolveField(override: T | undefined, existing: T | undefined, hasOverride: boolean): T | undefined { +function resolveField( + override: T | undefined, + existing: T | undefined, + hasOverride: boolean, +): T | undefined { return hasOverride ? override : existing; } @@ -146,17 +152,32 @@ function buildLeafletRecord( return { $type: "pub.leaflet.publication", name: overrides.name ?? normalizedPub?.name ?? "", - description: resolveField(overrides.description, normalizedPub?.description, "description" in overrides), - icon: resolveField(overrides.icon, normalizedPub?.icon, "icon" in overrides), - theme: resolveField(overrides.theme, normalizedPub?.theme, "theme" in overrides), + description: resolveField( + overrides.description, + normalizedPub?.description, + "description" in overrides, + ), + icon: resolveField( + overrides.icon, + normalizedPub?.icon, + "icon" in overrides, + ), + theme: resolveField( + overrides.theme, + normalizedPub?.theme, + "theme" in overrides, + ), base_path: overrides.basePath ?? existingBasePath, - preferences: preferences ? { - $type: "pub.leaflet.publication#preferences", - showInDiscover: preferences.showInDiscover, - showComments: preferences.showComments, - showMentions: preferences.showMentions, - showPrevNext: preferences.showPrevNext, - } : undefined, + preferences: preferences + ? { + $type: "pub.leaflet.publication#preferences", + showInDiscover: preferences.showInDiscover, + showComments: preferences.showComments, + showMentions: preferences.showMentions, + showPrevNext: preferences.showPrevNext, + showRecommends: preferences.showRecommends, + } + : undefined, }; } @@ -175,17 +196,36 @@ function buildStandardRecord( return { $type: "site.standard.publication", name: overrides.name ?? normalizedPub?.name ?? "", - description: resolveField(overrides.description, normalizedPub?.description, "description" in overrides), - icon: resolveField(overrides.icon, normalizedPub?.icon, "icon" in overrides), - theme: resolveField(overrides.theme, normalizedPub?.theme, "theme" in overrides), - basicTheme: resolveField(overrides.basicTheme, normalizedPub?.basicTheme, "basicTheme" in overrides), + description: resolveField( + overrides.description, + normalizedPub?.description, + "description" in overrides, + ), + icon: resolveField( + overrides.icon, + normalizedPub?.icon, + "icon" in overrides, + ), + theme: resolveField( + overrides.theme, + normalizedPub?.theme, + "theme" in overrides, + ), + basicTheme: resolveField( + overrides.basicTheme, + normalizedPub?.basicTheme, + "basicTheme" in overrides, + ), url: basePath ? `https://${basePath}` : normalizedPub?.url || "", - preferences: preferences ? { - showInDiscover: preferences.showInDiscover, - showComments: preferences.showComments, - showMentions: preferences.showMentions, - showPrevNext: preferences.showPrevNext, - } : undefined, + preferences: preferences + ? { + showInDiscover: preferences.showInDiscover, + showComments: preferences.showComments, + showMentions: preferences.showMentions, + showPrevNext: preferences.showPrevNext, + showRecommends: preferences.showRecommends, + } + : undefined, }; } @@ -217,27 +257,30 @@ export async function updatePublication({ iconFile?: File | null; preferences?: Omit; }): Promise { - return withPublicationUpdate(uri, async ({ normalizedPub, existingBasePath, publicationType, agent }) => { - // Upload icon if provided - let iconBlob = normalizedPub?.icon; - if (iconFile && iconFile.size > 0) { - const buffer = await iconFile.arrayBuffer(); - const uploadResult = await agent.com.atproto.repo.uploadBlob( - new Uint8Array(buffer), - { encoding: iconFile.type }, - ); - if (uploadResult.data.blob) { - iconBlob = uploadResult.data.blob; + return withPublicationUpdate( + uri, + async ({ normalizedPub, existingBasePath, publicationType, agent }) => { + // Upload icon if provided + let iconBlob = normalizedPub?.icon; + if (iconFile && iconFile.size > 0) { + const buffer = await iconFile.arrayBuffer(); + const uploadResult = await agent.com.atproto.repo.uploadBlob( + new Uint8Array(buffer), + { encoding: iconFile.type }, + ); + if (uploadResult.data.blob) { + iconBlob = uploadResult.data.blob; + } } - } - return buildRecord(normalizedPub, existingBasePath, publicationType, { - name, - description, - icon: iconBlob, - preferences, - }); - }); + return buildRecord(normalizedPub, existingBasePath, publicationType, { + name, + description, + icon: iconBlob, + preferences, + }); + }, + ); } export async function updatePublicationBasePath({ @@ -247,11 +290,14 @@ export async function updatePublicationBasePath({ uri: string; base_path: string; }): Promise { - return withPublicationUpdate(uri, async ({ normalizedPub, existingBasePath, publicationType }) => { - return buildRecord(normalizedPub, existingBasePath, publicationType, { - basePath: base_path, - }); - }); + return withPublicationUpdate( + uri, + async ({ normalizedPub, existingBasePath, publicationType }) => { + return buildRecord(normalizedPub, existingBasePath, publicationType, { + basePath: base_path, + }); + }, + ); } type Color = @@ -275,58 +321,81 @@ export async function updatePublicationTheme({ accentText: Color; }; }): Promise { - return withPublicationUpdate(uri, async ({ normalizedPub, existingBasePath, publicationType, agent }) => { - // Build theme object - const themeData = { - $type: "pub.leaflet.publication#theme" as const, - backgroundImage: theme.backgroundImage - ? { - $type: "pub.leaflet.theme.backgroundImage", - image: ( - await agent.com.atproto.repo.uploadBlob( - new Uint8Array(await theme.backgroundImage.arrayBuffer()), - { encoding: theme.backgroundImage.type }, - ) - )?.data.blob, - width: theme.backgroundRepeat || undefined, - repeat: !!theme.backgroundRepeat, - } - : theme.backgroundImage === null - ? undefined - : normalizedPub?.theme?.backgroundImage, - backgroundColor: theme.backgroundColor - ? { - ...theme.backgroundColor, - } - : undefined, - pageWidth: theme.pageWidth, - primary: { - ...theme.primary, - }, - pageBackground: { - ...theme.pageBackground, - }, - showPageBackground: theme.showPageBackground, - accentBackground: { - ...theme.accentBackground, - }, - accentText: { - ...theme.accentText, - }, - }; + return withPublicationUpdate( + uri, + async ({ normalizedPub, existingBasePath, publicationType, agent }) => { + // Build theme object + const themeData = { + $type: "pub.leaflet.publication#theme" as const, + backgroundImage: theme.backgroundImage + ? { + $type: "pub.leaflet.theme.backgroundImage", + image: ( + await agent.com.atproto.repo.uploadBlob( + new Uint8Array(await theme.backgroundImage.arrayBuffer()), + { encoding: theme.backgroundImage.type }, + ) + )?.data.blob, + width: theme.backgroundRepeat || undefined, + repeat: !!theme.backgroundRepeat, + } + : theme.backgroundImage === null + ? undefined + : normalizedPub?.theme?.backgroundImage, + backgroundColor: theme.backgroundColor + ? { + ...theme.backgroundColor, + } + : undefined, + pageWidth: theme.pageWidth, + primary: { + ...theme.primary, + }, + pageBackground: { + ...theme.pageBackground, + }, + showPageBackground: theme.showPageBackground, + accentBackground: { + ...theme.accentBackground, + }, + accentText: { + ...theme.accentText, + }, + }; - // Derive basicTheme from the theme colors for site.standard.publication - const basicTheme: NormalizedPublication["basicTheme"] = { - $type: "site.standard.theme.basic", - background: { $type: "site.standard.theme.color#rgb", r: theme.backgroundColor.r, g: theme.backgroundColor.g, b: theme.backgroundColor.b }, - foreground: { $type: "site.standard.theme.color#rgb", r: theme.primary.r, g: theme.primary.g, b: theme.primary.b }, - accent: { $type: "site.standard.theme.color#rgb", r: theme.accentBackground.r, g: theme.accentBackground.g, b: theme.accentBackground.b }, - accentForeground: { $type: "site.standard.theme.color#rgb", r: theme.accentText.r, g: theme.accentText.g, b: theme.accentText.b }, - }; + // Derive basicTheme from the theme colors for site.standard.publication + const basicTheme: NormalizedPublication["basicTheme"] = { + $type: "site.standard.theme.basic", + background: { + $type: "site.standard.theme.color#rgb", + r: theme.backgroundColor.r, + g: theme.backgroundColor.g, + b: theme.backgroundColor.b, + }, + foreground: { + $type: "site.standard.theme.color#rgb", + r: theme.primary.r, + g: theme.primary.g, + b: theme.primary.b, + }, + accent: { + $type: "site.standard.theme.color#rgb", + r: theme.accentBackground.r, + g: theme.accentBackground.g, + b: theme.accentBackground.b, + }, + accentForeground: { + $type: "site.standard.theme.color#rgb", + r: theme.accentText.r, + g: theme.accentText.g, + b: theme.accentText.b, + }, + }; - return buildRecord(normalizedPub, existingBasePath, publicationType, { - theme: themeData, - basicTheme, - }); - }); + return buildRecord(normalizedPub, existingBasePath, publicationType, { + theme: themeData, + basicTheme, + }); + }, + ); } diff --git a/components/Canvas.tsx b/components/Canvas.tsx index 0faa3302..118c163e 100644 --- a/components/Canvas.tsx +++ b/components/Canvas.tsx @@ -19,10 +19,11 @@ import { Popover } from "./Popover"; import { Separator } from "./Layout"; import { CommentTiny } from "./Icons/CommentTiny"; import { QuoteTiny } from "./Icons/QuoteTiny"; -import { PublicationMetadata } from "./Pages/PublicationMetadata"; +import { AddTags, PublicationMetadata } from "./Pages/PublicationMetadata"; import { useLeafletPublicationData } from "./PageSWRDataProvider"; import { useHandleCanvasDrop } from "./Blocks/useHandleCanvasDrop"; import { useBlockMouseHandlers } from "./Blocks/useBlockMouseHandlers"; +import { RecommendTinyEmpty } from "./Icons/RecommendTiny"; export function Canvas(props: { entityID: string; @@ -168,20 +169,34 @@ const CanvasMetadata = (props: { isSubpage: boolean | undefined }) => { if (!normalizedPublication) return null; let showComments = normalizedPublication.preferences?.showComments !== false; let showMentions = normalizedPublication.preferences?.showMentions !== false; + let showRecommends = + normalizedPublication.preferences?.showRecommends !== false; return (
+ {showRecommends && ( +
+ — +
+ )} {showComments && (
—
)} - {showComments && ( + {showMentions && (
—
)} + {showMentions !== false || + showComments !== false || + showRecommends === false ? ( + + ) : null} + + {!props.isSubpage && ( <> @@ -191,7 +206,7 @@ const CanvasMetadata = (props: { isSubpage: boolean | undefined }) => { className="flex flex-col gap-2 p-0! max-w-sm w-[1000px]" trigger={} > - + )} diff --git a/components/Icons/RecommendTiny.tsx b/components/Icons/RecommendTiny.tsx new file mode 100644 index 00000000..7ec07c14 --- /dev/null +++ b/components/Icons/RecommendTiny.tsx @@ -0,0 +1,37 @@ +import { Props } from "./Props"; + +export const RecommendTinyFilled = (props: Props) => { + return ( + + + + ); +}; + +export const RecommendTinyEmpty = (props: Props) => { + return ( + + + + ); +}; diff --git a/components/InteractionsPreview.tsx b/components/InteractionsPreview.tsx index 30ddefbb..9f393f86 100644 --- a/components/InteractionsPreview.tsx +++ b/components/InteractionsPreview.tsx @@ -7,35 +7,36 @@ import { Tag } from "./Tags"; import { Popover } from "./Popover"; import { TagTiny } from "./Icons/TagTiny"; import { SpeedyLink } from "./SpeedyLink"; +import { RecommendButton } from "./RecommendButton"; export const InteractionPreview = (props: { quotesCount: number; commentsCount: number; + recommendsCount: number; + documentUri: string; tags?: string[]; postUrl: string; showComments: boolean; showMentions: boolean; + showRecommends: boolean; share?: boolean; }) => { let smoker = useSmoker(); let interactionsAvailable = (props.quotesCount > 0 && props.showMentions) || - (props.showComments !== false && props.commentsCount > 0); + (props.showComments !== false && props.commentsCount > 0) || + (props.showRecommends !== false && props.recommendsCount > 0); const tagsCount = props.tags?.length || 0; return ( -
- {tagsCount === 0 ? null : ( - <> - - {interactionsAvailable || props.share ? ( - - ) : null} - +
+ {props.showRecommends === false ? null : ( + )} {!props.showMentions || props.quotesCount === 0 ? null : ( @@ -56,11 +57,16 @@ export const InteractionPreview = (props: { {props.commentsCount} )} - {interactionsAvailable && props.share ? ( - - ) : null} + {tagsCount === 0 ? null : ( + <> + {interactionsAvailable ? : null} + + + )} {props.share && ( <> + + + ); +} diff --git a/contexts/DocumentContext.tsx b/contexts/DocumentContext.tsx index 467d9ad9..d3157d69 100644 --- a/contexts/DocumentContext.tsx +++ b/contexts/DocumentContext.tsx @@ -21,6 +21,7 @@ export type DocumentContextValue = Pick< | "comments" | "mentions" | "leafletId" + | "recommendsCount" >; const DocumentContext = createContext(null); diff --git a/lexicons/api/types/pub/leaflet/publication.ts b/lexicons/api/types/pub/leaflet/publication.ts index 88314a28..51143ee0 100644 --- a/lexicons/api/types/pub/leaflet/publication.ts +++ b/lexicons/api/types/pub/leaflet/publication.ts @@ -1,89 +1,94 @@ /** * GENERATED CODE - DO NOT MODIFY */ -import { type ValidationResult, BlobRef } from '@atproto/lexicon' -import { CID } from 'multiformats/cid' -import { validate as _validate } from '../../../lexicons' -import { type $Typed, is$typed as _is$typed, type OmitKey } from '../../../util' -import type * as PubLeafletThemeColor from './theme/color' -import type * as PubLeafletThemeBackgroundImage from './theme/backgroundImage' +import { type ValidationResult, BlobRef } from "@atproto/lexicon"; +import { CID } from "multiformats/cid"; +import { validate as _validate } from "../../../lexicons"; +import { + type $Typed, + is$typed as _is$typed, + type OmitKey, +} from "../../../util"; +import type * as PubLeafletThemeColor from "./theme/color"; +import type * as PubLeafletThemeBackgroundImage from "./theme/backgroundImage"; const is$typed = _is$typed, - validate = _validate -const id = 'pub.leaflet.publication' + validate = _validate; +const id = "pub.leaflet.publication"; export interface Record { - $type: 'pub.leaflet.publication' - name: string - base_path?: string - description?: string - icon?: BlobRef - theme?: Theme - preferences?: Preferences - [k: string]: unknown + $type: "pub.leaflet.publication"; + name: string; + base_path?: string; + description?: string; + icon?: BlobRef; + theme?: Theme; + preferences?: Preferences; + [k: string]: unknown; } -const hashRecord = 'main' +const hashRecord = "main"; export function isRecord(v: V) { - return is$typed(v, id, hashRecord) + return is$typed(v, id, hashRecord); } export function validateRecord(v: V) { - return validate(v, id, hashRecord, true) + return validate(v, id, hashRecord, true); } export interface Preferences { - $type?: 'pub.leaflet.publication#preferences' - showInDiscover: boolean - showComments: boolean - showMentions: boolean - showPrevNext: boolean + $type?: "pub.leaflet.publication#preferences"; + showInDiscover: boolean; + showComments: boolean; + showMentions: boolean; + showPrevNext: boolean; + showRecommends: boolean; } -const hashPreferences = 'preferences' +const hashPreferences = "preferences"; export function isPreferences(v: V) { - return is$typed(v, id, hashPreferences) + return is$typed(v, id, hashPreferences); } export function validatePreferences(v: V) { - return validate(v, id, hashPreferences) + return validate(v, id, hashPreferences); } export interface Theme { - $type?: 'pub.leaflet.publication#theme' + $type?: "pub.leaflet.publication#theme"; backgroundColor?: | $Typed | $Typed - | { $type: string } - backgroundImage?: PubLeafletThemeBackgroundImage.Main - pageWidth?: number + | { $type: string }; + backgroundImage?: PubLeafletThemeBackgroundImage.Main; + pageWidth?: number; primary?: | $Typed | $Typed - | { $type: string } + | { $type: string }; pageBackground?: | $Typed | $Typed - | { $type: string } - showPageBackground: boolean + | { $type: string }; + showPageBackground: boolean; accentBackground?: | $Typed | $Typed - | { $type: string } + | { $type: string }; accentText?: | $Typed | $Typed - | { $type: string } + | { $type: string }; } -const hashTheme = 'theme' +const hashTheme = "theme"; export function isTheme(v: V) { - return is$typed(v, id, hashTheme) + return is$typed(v, id, hashTheme); } export function validateTheme(v: V) { - return validate(v, id, hashTheme) + return validate(v, id, hashTheme); } diff --git a/lexicons/api/types/site/standard/publication.ts b/lexicons/api/types/site/standard/publication.ts index 2daf12b6..08f80d9c 100644 --- a/lexicons/api/types/site/standard/publication.ts +++ b/lexicons/api/types/site/standard/publication.ts @@ -1,53 +1,58 @@ /** * GENERATED CODE - DO NOT MODIFY */ -import { type ValidationResult, BlobRef } from '@atproto/lexicon' -import { CID } from 'multiformats/cid' -import { validate as _validate } from '../../../lexicons' -import { type $Typed, is$typed as _is$typed, type OmitKey } from '../../../util' -import type * as SiteStandardThemeBasic from './theme/basic' -import type * as PubLeafletPublication from '../../pub/leaflet/publication' +import { type ValidationResult, BlobRef } from "@atproto/lexicon"; +import { CID } from "multiformats/cid"; +import { validate as _validate } from "../../../lexicons"; +import { + type $Typed, + is$typed as _is$typed, + type OmitKey, +} from "../../../util"; +import type * as SiteStandardThemeBasic from "./theme/basic"; +import type * as PubLeafletPublication from "../../pub/leaflet/publication"; const is$typed = _is$typed, - validate = _validate -const id = 'site.standard.publication' + validate = _validate; +const id = "site.standard.publication"; export interface Record { - $type: 'site.standard.publication' - basicTheme?: SiteStandardThemeBasic.Main - theme?: $Typed | { $type: string } - description?: string - icon?: BlobRef - name: string - preferences?: Preferences - url: string - [k: string]: unknown + $type: "site.standard.publication"; + basicTheme?: SiteStandardThemeBasic.Main; + theme?: $Typed | { $type: string }; + description?: string; + icon?: BlobRef; + name: string; + preferences?: Preferences; + url: string; + [k: string]: unknown; } -const hashRecord = 'main' +const hashRecord = "main"; export function isRecord(v: V) { - return is$typed(v, id, hashRecord) + return is$typed(v, id, hashRecord); } export function validateRecord(v: V) { - return validate(v, id, hashRecord, true) + return validate(v, id, hashRecord, true); } export interface Preferences { - $type?: 'site.standard.publication#preferences' - showInDiscover: boolean - showComments: boolean - showMentions: boolean - showPrevNext: boolean + $type?: "site.standard.publication#preferences"; + showInDiscover: boolean; + showComments: boolean; + showMentions: boolean; + showPrevNext: boolean; + showRecommends: boolean; } -const hashPreferences = 'preferences' +const hashPreferences = "preferences"; export function isPreferences(v: V) { - return is$typed(v, id, hashPreferences) + return is$typed(v, id, hashPreferences); } export function validatePreferences(v: V) { - return validate(v, id, hashPreferences) + return validate(v, id, hashPreferences); } diff --git a/lexicons/pub/leaflet/publication.json b/lexicons/pub/leaflet/publication.json index 0627addf..c98b1eb6 100644 --- a/lexicons/pub/leaflet/publication.json +++ b/lexicons/pub/leaflet/publication.json @@ -59,6 +59,10 @@ "showPrevNext": { "type": "boolean", "default": true + }, + "showRecommends": { + "type": "boolean", + "default": true } } }, diff --git a/lexicons/site/standard/publication.json b/lexicons/site/standard/publication.json index b1449052..7fdbcc22 100644 --- a/lexicons/site/standard/publication.json +++ b/lexicons/site/standard/publication.json @@ -58,6 +58,10 @@ "showPrevNext": { "default": false, "type": "boolean" + }, + "showRecommends": { + "default": true, + "type": "boolean" } }, "type": "object" diff --git a/lexicons/src/normalize.ts b/lexicons/src/normalize.ts index d4890e07..4f7294c7 100644 --- a/lexicons/src/normalize.ts +++ b/lexicons/src/normalize.ts @@ -50,7 +50,7 @@ export type NormalizedPublication = { * Checks if the record is a pub.leaflet.document */ export function isLeafletDocument( - record: unknown + record: unknown, ): record is PubLeafletDocument.Record { if (!record || typeof record !== "object") return false; const r = record as Record; @@ -65,7 +65,7 @@ export function isLeafletDocument( * Checks if the record is a site.standard.document */ export function isStandardDocument( - record: unknown + record: unknown, ): record is SiteStandardDocument.Record { if (!record || typeof record !== "object") return false; const r = record as Record; @@ -76,7 +76,7 @@ export function isStandardDocument( * Checks if the record is a pub.leaflet.publication */ export function isLeafletPublication( - record: unknown + record: unknown, ): record is PubLeafletPublication.Record { if (!record || typeof record !== "object") return false; const r = record as Record; @@ -91,7 +91,7 @@ export function isLeafletPublication( * Checks if the record is a site.standard.publication */ export function isStandardPublication( - record: unknown + record: unknown, ): record is SiteStandardPublication.Record { if (!record || typeof record !== "object") return false; const r = record as Record; @@ -106,7 +106,7 @@ function extractRgb( | $Typed | $Typed | { $type: string } - | undefined + | undefined, ): { r: number; g: number; b: number } | undefined { if (!color || typeof color !== "object") return undefined; const c = color as Record; @@ -124,12 +124,13 @@ function extractRgb( * Converts a pub.leaflet theme to a site.standard.theme.basic format */ export function leafletThemeToBasicTheme( - theme: PubLeafletPublication.Theme | undefined + theme: PubLeafletPublication.Theme | undefined, ): SiteStandardThemeBasic.Main | undefined { if (!theme) return undefined; const background = extractRgb(theme.backgroundColor); - const accent = extractRgb(theme.accentBackground) || extractRgb(theme.primary); + const accent = + extractRgb(theme.accentBackground) || extractRgb(theme.primary); const accentForeground = extractRgb(theme.accentText); // If we don't have the required colors, return undefined @@ -160,7 +161,10 @@ export function leafletThemeToBasicTheme( * @param uri - Optional document URI, used to extract the rkey for the path field when normalizing pub.leaflet records * @returns A normalized document in site.standard format, or null if invalid/unrecognized */ -export function normalizeDocument(record: unknown, uri?: string): NormalizedDocument | null { +export function normalizeDocument( + record: unknown, + uri?: string, +): NormalizedDocument | null { if (!record || typeof record !== "object") return null; // Pass through site.standard records directly (theme is already in correct format if present) @@ -219,7 +223,7 @@ export function normalizeDocument(record: unknown, uri?: string): NormalizedDocu * @returns A normalized publication in site.standard format, or null if invalid/unrecognized */ export function normalizePublication( - record: unknown + record: unknown, ): NormalizedPublication | null { if (!record || typeof record !== "object") return null; @@ -268,6 +272,7 @@ export function normalizePublication( showComments: record.preferences.showComments, showMentions: record.preferences.showMentions, showPrevNext: record.preferences.showPrevNext, + showRecommends: record.preferences.showRecommends, } : undefined; @@ -290,7 +295,7 @@ export function normalizePublication( * Type guard to check if a normalized document has leaflet content */ export function hasLeafletContent( - doc: NormalizedDocument + doc: NormalizedDocument, ): doc is NormalizedDocument & { content: $Typed; } { @@ -304,7 +309,7 @@ export function hasLeafletContent( * Gets the pages array from a normalized document, handling both formats */ export function getDocumentPages( - doc: NormalizedDocument + doc: NormalizedDocument, ): PubLeafletContent.Main["pages"] | undefined { if (!doc.content) return undefined; diff --git a/lexicons/src/publication.ts b/lexicons/src/publication.ts index 57bbbef9..cf5904d2 100644 --- a/lexicons/src/publication.ts +++ b/lexicons/src/publication.ts @@ -29,6 +29,7 @@ export const PubLeafletPublication: LexiconDoc = { showComments: { type: "boolean", default: true }, showMentions: { type: "boolean", default: true }, showPrevNext: { type: "boolean", default: true }, + showRecommends: { type: "boolean", default: true }, }, }, theme: { diff --git a/package-lock.json b/package-lock.json index 08cf16a1..bffdd153 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,6 +38,7 @@ "@vercel/analytics": "^1.5.0", "@vercel/functions": "^2.2.12", "@vercel/sdk": "^1.11.4", + "@yornaath/batshit": "^0.14.0", "babel-plugin-react-compiler": "^19.1.0-rc.1", "base64-js": "^1.5.1", "colorjs.io": "^0.5.2", @@ -8661,6 +8662,21 @@ } } }, + "node_modules/@yornaath/batshit": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@yornaath/batshit/-/batshit-0.14.0.tgz", + "integrity": "sha512-0I+xMi5JoRs3+qVXXhk2AmsEl43MwrG+L+VW+nqw/qQqMFtgRPszLaxhJCfsBKnjfJ0gJzTI1Q9Q9+y903HyHQ==", + "license": "MIT", + "dependencies": { + "@yornaath/batshit-devtools": "^1.7.1" + } + }, + "node_modules/@yornaath/batshit-devtools": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@yornaath/batshit-devtools/-/batshit-devtools-1.7.1.tgz", + "integrity": "sha512-AyttV1Njj5ug+XqEWY1smV45dTWMlWKtj1B8jcFYgBKUFyUlF/qEhD+iP1E5UaRYW6hQRYD9T2WNDwFTrOMWzQ==", + "license": "MIT" + }, "node_modules/abort-controller": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", diff --git a/package.json b/package.json index 8c589ee7..5e9e2ee4 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "@vercel/analytics": "^1.5.0", "@vercel/functions": "^2.2.12", "@vercel/sdk": "^1.11.4", + "@yornaath/batshit": "^0.14.0", "babel-plugin-react-compiler": "^19.1.0-rc.1", "base64-js": "^1.5.1", "colorjs.io": "^0.5.2", -- 2.51.2 From 8ae93e160e0a14b929337d19620e98f59992bde4 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Sun, 8 Feb 2026 05:09:42 +0400 Subject: [PATCH 09/51] add oauth scopes --- app/api/oauth/[route]/oauth-metadata.ts | 3 ++- app/api/oauth/[route]/route.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/api/oauth/[route]/oauth-metadata.ts b/app/api/oauth/[route]/oauth-metadata.ts index 8375f993..855fc0bc 100644 --- a/app/api/oauth/[route]/oauth-metadata.ts +++ b/app/api/oauth/[route]/oauth-metadata.ts @@ -7,7 +7,8 @@ const hostname = ? "http://localhost:3000" : "https://leaflet.pub"; -const scope = "atproto transition:generic transition:email"; +const scope = + "atproto transition:generic transition:email include:pub.leaflet.authFullPermissions include:site.standard.authFull include:app.bsky.authCreatePosts"; const localconfig: OAuthClientMetadataInput = { client_id: `http://localhost/?redirect_uri=${encodeURI(`http://127.0.0.1:3000/api/oauth/callback`)}&scope=${encodeURIComponent(scope)}`, client_name: `Leaflet`, diff --git a/app/api/oauth/[route]/route.ts b/app/api/oauth/[route]/route.ts index db2ab8f4..3d2c7641 100644 --- a/app/api/oauth/[route]/route.ts +++ b/app/api/oauth/[route]/route.ts @@ -42,7 +42,8 @@ export async function GET( const ac = new AbortController(); const url = await client.authorize(handle || "https://bsky.social", { - scope: "atproto transition:generic transition:email", + scope: + "atproto transition:email include:pub.leaflet.authFullPermissions include:site.standard.authFull include:app.bsky.authCreatePosts", signal: ac.signal, state: JSON.stringify(state), }); -- 2.51.2 From dbf7cf24e7b43fd2541d024c7c92848929e15f02 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Sun, 8 Feb 2026 10:07:24 +0400 Subject: [PATCH 10/51] add per post interactions --- actions/publishToPublication.ts | 21 + app/[leaflet_id]/Sidebar.tsx | 2 + app/[leaflet_id]/actions/PublishButton.tsx | 10 + app/[leaflet_id]/publish/PublishPost.tsx | 10 + app/api/rpc/[command]/pull.ts | 7 + .../[rkey]/DocumentPageRenderer.tsx | 3 +- .../[did]/[publication]/[rkey]/PostPages.tsx | 8 +- components/Canvas.tsx | 21 +- components/Pages/PublicationMetadata.tsx | 25 +- components/PostListing.tsx | 13 +- components/PostSettings.tsx | 96 +++++ lexicons/api/lexicons.ts | 17 + lexicons/api/types/pub/leaflet/document.ts | 1 + lexicons/api/types/pub/leaflet/publication.ts | 84 ++-- lexicons/api/types/site/standard/document.ts | 1 + .../api/types/site/standard/publication.ts | 62 ++- lexicons/pub/leaflet/document.json | 4 + lexicons/site/standard/document.json | 5 + lexicons/src/document.ts | 4 + lexicons/src/normalize.ts | 10 + package-lock.json | 397 +----------------- package.json | 1 - src/replicache/mutations.ts | 13 + src/utils/mergePreferences.ts | 24 ++ supabase/database.types.ts | 6 + ...260208000000_add_preferences_to_drafts.sql | 2 + 26 files changed, 356 insertions(+), 491 deletions(-) create mode 100644 components/PostSettings.tsx create mode 100644 src/utils/mergePreferences.ts create mode 100644 supabase/migrations/20260208000000_add_preferences_to_drafts.sql diff --git a/actions/publishToPublication.ts b/actions/publishToPublication.ts index db3c789e..a1a93592 100644 --- a/actions/publishToPublication.ts +++ b/actions/publishToPublication.ts @@ -78,6 +78,7 @@ export async function publishToPublication({ cover_image, entitiesToDelete, publishedAt, + postPreferences, }: { root_entity: string; publication_uri?: string; @@ -88,6 +89,11 @@ export async function publishToPublication({ cover_image?: string | null; entitiesToDelete?: string[]; publishedAt?: string; + postPreferences?: { + showComments?: boolean; + showMentions?: boolean; + showRecommends?: boolean; + } | null; }): Promise { let identity = await getIdentityData(); if (!identity || !identity.atp_did) { @@ -175,6 +181,9 @@ export async function publishToPublication({ }; } + // Resolve preferences: explicit param > draft DB value + const preferences = postPreferences ?? draft?.preferences; + // Extract theme for standalone documents (not for publications) let theme: PubLeafletPublication.Theme | undefined; if (!publication_uri) { @@ -245,6 +254,12 @@ export async function publishToPublication({ ...(coverImageBlob && { coverImage: coverImageBlob }), // Include theme for standalone documents (not for publication documents) ...(!publication_uri && theme && { theme }), + ...(preferences && { + preferences: { + $type: "pub.leaflet.publication#preferences" as const, + ...preferences, + }, + }), content: { $type: "pub.leaflet.content" as const, pages: pagesArray, @@ -257,6 +272,12 @@ export async function publishToPublication({ author: credentialSession.did!, ...(publication_uri && { publication: publication_uri }), ...(theme && { theme }), + ...(preferences && { + preferences: { + $type: "pub.leaflet.publication#preferences" as const, + ...preferences, + }, + }), title: title || "Untitled", description: description || "", ...(tags !== undefined && { tags }), diff --git a/app/[leaflet_id]/Sidebar.tsx b/app/[leaflet_id]/Sidebar.tsx index 568ed74d..14ac86cc 100644 --- a/app/[leaflet_id]/Sidebar.tsx +++ b/app/[leaflet_id]/Sidebar.tsx @@ -8,6 +8,7 @@ import { useLeafletPublicationData } from "components/PageSWRDataProvider"; import { ShareOptions } from "app/[leaflet_id]/actions/ShareOptions"; import { ThemePopover } from "components/ThemeManager/ThemeSetter"; import { PublishButton } from "./actions/PublishButton"; +import { PostSettings } from "components/PostSettings"; import { Watermark } from "components/Watermark"; import { BackToPubButton } from "./actions/BackToPubButton"; import { useIdentityData } from "components/IdentityProvider"; @@ -30,6 +31,7 @@ export function LeafletSidebar() { +
diff --git a/app/[leaflet_id]/actions/PublishButton.tsx b/app/[leaflet_id]/actions/PublishButton.tsx index 0e6c4bc2..7ba596ef 100644 --- a/app/[leaflet_id]/actions/PublishButton.tsx +++ b/app/[leaflet_id]/actions/PublishButton.tsx @@ -96,6 +96,15 @@ const UpdateButton = () => { tx.get("publication_cover_image"), ); + // Get post preferences from Replicache state + let postPreferences = useSubscribe(rep, (tx) => + tx.get<{ + showComments?: boolean; + showMentions?: boolean; + showRecommends?: boolean; + } | null>("post_preferences"), + ); + // Get local published at from Replicache (session-only state, not persisted to DB) let publishedAt = useLocalPublishedAt((s) => pub?.doc ? s[pub?.doc] : undefined, @@ -118,6 +127,7 @@ const UpdateButton = () => { tags: currentTags, cover_image: coverImage, publishedAt: publishedAt?.toISOString(), + postPreferences, }); setIsLoading(false); mutate(); diff --git a/app/[leaflet_id]/publish/PublishPost.tsx b/app/[leaflet_id]/publish/PublishPost.tsx index adf48c04..20c3e202 100644 --- a/app/[leaflet_id]/publish/PublishPost.tsx +++ b/app/[leaflet_id]/publish/PublishPost.tsx @@ -91,6 +91,15 @@ const PublishPostForm = ( tx.get("publication_cover_image"), ); + // Get post preferences from Replicache state + let postPreferences = useSubscribe(rep, (tx) => + tx.get<{ + showComments?: boolean; + showMentions?: boolean; + showRecommends?: boolean; + } | null>("post_preferences"), + ); + // Use Replicache tags only when we have a draft const currentTags = props.hasDraft ? Array.isArray(replicacheTags) @@ -124,6 +133,7 @@ const PublishPostForm = ( cover_image: replicacheCoverImage, entitiesToDelete: props.entitiesToDelete, publishedAt: localPublishedAt?.toISOString() || new Date().toISOString(), + postPreferences, }); if (!result.success) { diff --git a/app/api/rpc/[command]/pull.ts b/app/api/rpc/[command]/pull.ts index ea3b3845..31ae3ac6 100644 --- a/app/api/rpc/[command]/pull.ts +++ b/app/api/rpc/[command]/pull.ts @@ -9,6 +9,7 @@ import { FactWithIndexes } from "src/replicache/utils"; import type { Attribute } from "src/replicache/attributes"; import { makeRoute } from "../lib"; import type { Env } from "./route"; +import type { Json } from "supabase/database.types"; // First define the sub-types for V0 and V1 requests const pullRequestV0 = z.object({ @@ -75,6 +76,7 @@ export const pull = makeRoute({ title: string; tags: string[]; cover_image: string | null; + preferences: Json | null; }[]; let pub_patch = publication_data?.[0] ? [ @@ -98,6 +100,11 @@ export const pull = makeRoute({ key: "publication_cover_image", value: publication_data[0].cover_image || null, }, + { + op: "put", + key: "post_preferences", + value: publication_data[0].preferences || null, + }, ] : []; diff --git a/app/lish/[did]/[publication]/[rkey]/DocumentPageRenderer.tsx b/app/lish/[did]/[publication]/[rkey]/DocumentPageRenderer.tsx index bf57101e..7c9a5d55 100644 --- a/app/lish/[did]/[publication]/[rkey]/DocumentPageRenderer.tsx +++ b/app/lish/[did]/[publication]/[rkey]/DocumentPageRenderer.tsx @@ -21,6 +21,7 @@ import { } from "src/utils/normalizeRecords"; import { DocumentProvider } from "contexts/DocumentContext"; import { LeafletContentProvider } from "contexts/LeafletContentContext"; +import { mergePreferences } from "src/utils/mergePreferences"; export async function DocumentPageRenderer({ did, @@ -133,7 +134,7 @@ export async function DocumentPageRenderer({ { let { data: pub, normalizedPublication } = useLeafletPublicationData(); + let { rep } = useReplicache(); + let postPreferences = useSubscribe(rep, (tx) => + tx.get<{ + showComments?: boolean; + showMentions?: boolean; + showRecommends?: boolean; + } | null>("post_preferences"), + ); if (!pub || !pub.publications) return null; if (!normalizedPublication) return null; - let showComments = normalizedPublication.preferences?.showComments !== false; - let showMentions = normalizedPublication.preferences?.showMentions !== false; - let showRecommends = - normalizedPublication.preferences?.showRecommends !== false; + let merged = mergePreferences( + postPreferences || undefined, + normalizedPublication.preferences, + ); + let showComments = merged.showComments !== false; + let showMentions = merged.showMentions !== false; + let showRecommends = merged.showRecommends !== false; return (
diff --git a/components/Pages/PublicationMetadata.tsx b/components/Pages/PublicationMetadata.tsx index 2caa7d15..27803b9b 100644 --- a/components/Pages/PublicationMetadata.tsx +++ b/components/Pages/PublicationMetadata.tsx @@ -21,6 +21,7 @@ import { useIdentityData } from "components/IdentityProvider"; import { PostHeaderLayout } from "app/lish/[did]/[publication]/[rkey]/PostHeader/PostHeader"; import { Backdater } from "./Backdater"; import { RecommendTinyEmpty } from "components/Icons/RecommendTiny"; +import { mergePreferences } from "src/utils/mergePreferences"; export const PublicationMetadata = (props: { noInteractions?: boolean }) => { let { rep } = useReplicache(); @@ -34,6 +35,17 @@ export const PublicationMetadata = (props: { noInteractions?: boolean }) => { let description = useSubscribe(rep, (tx) => tx.get("publication_description"), ); + let postPreferences = useSubscribe(rep, (tx) => + tx.get<{ + showComments?: boolean; + showMentions?: boolean; + showRecommends?: boolean; + } | null>("post_preferences"), + ); + let merged = mergePreferences( + postPreferences || undefined, + normalizedPublication?.preferences, + ); let publishedAt = normalizedDocument?.publishedAt; if (!pub) return null; @@ -121,28 +133,27 @@ export const PublicationMetadata = (props: { noInteractions?: boolean }) => { )} {!props.noInteractions && (
- {normalizedPublication?.preferences?.showRecommends !== false && ( + {merged.showRecommends !== false && (
—
)} - {normalizedPublication?.preferences?.showMentions !== false && ( + {merged.showMentions !== false && (
—
)} - {normalizedPublication?.preferences?.showComments !== false && ( + {merged.showComments !== false && (
—
)} {tags && ( <> - {normalizedPublication?.preferences?.showRecommends !== - false || - normalizedPublication?.preferences?.showMentions !== false || - normalizedPublication?.preferences?.showComments !== false ? ( + {merged.showRecommends !== false || + merged.showMentions !== false || + merged.showComments !== false ? ( ) : null} diff --git a/components/PostListing.tsx b/components/PostListing.tsx index ef6105a4..32bd9bd4 100644 --- a/components/PostListing.tsx +++ b/components/PostListing.tsx @@ -17,6 +17,7 @@ import type { Post } from "app/(home-pages)/reader/getReaderFeed"; import Link from "next/link"; import { InteractionPreview } from "./InteractionsPreview"; import { useLocalizedDate } from "src/hooks/useLocalizedDate"; +import { mergePreferences } from "src/utils/mergePreferences"; export const PostListing = (props: Post) => { let pubRecord = props.publication?.pubRecord as @@ -48,9 +49,11 @@ export const PostListing = (props: Post) => { ? pubRecord?.theme?.showPageBackground : postRecord.theme?.showPageBackground ?? true; + let mergedPrefs = mergePreferences(postRecord?.preferences, pubRecord?.preferences); + let quotes = props.documents.document_mentions_in_bsky?.[0]?.count || 0; let comments = - pubRecord?.preferences?.showComments === false + mergedPrefs.showComments === false ? 0 : props.documents.comments_on_documents?.[0]?.count || 0; let recommends = props.documents.recommends_on_documents?.[0]?.count || 0; @@ -109,11 +112,9 @@ export const PostListing = (props: Post) => { recommendsCount={recommends} documentUri={props.documents.uri} tags={tags} - showComments={pubRecord?.preferences?.showComments !== false} - showMentions={pubRecord?.preferences?.showMentions !== false} - showRecommends={ - pubRecord?.preferences?.showRecommends !== false - } + showComments={mergedPrefs.showComments !== false} + showMentions={mergedPrefs.showMentions !== false} + showRecommends={mergedPrefs.showRecommends !== false} share />
diff --git a/components/PostSettings.tsx b/components/PostSettings.tsx new file mode 100644 index 00000000..d8a92a0b --- /dev/null +++ b/components/PostSettings.tsx @@ -0,0 +1,96 @@ +"use client"; + +import { ActionButton } from "components/ActionBar/ActionButton"; +import { SettingsSmall } from "components/Icons/SettingsSmall"; +import { Toggle } from "components/Toggle"; +import { Popover } from "components/Popover"; +import { useLeafletPublicationData } from "components/PageSWRDataProvider"; +import { useReplicache } from "src/replicache"; +import { useSubscribe } from "src/replicache/useSubscribe"; + +type PostPreferences = { + showComments?: boolean; + showMentions?: boolean; + showRecommends?: boolean; +}; + +export function PostSettings() { + let { data: pub, normalizedPublication } = useLeafletPublicationData(); + let { rep } = useReplicache(); + + let postPreferences = useSubscribe(rep, (tx) => + tx.get("post_preferences"), + ); + + if (!pub || !pub.publications) return null; + + let pubPrefs = normalizedPublication?.preferences; + + let showComments = + postPreferences?.showComments ?? pubPrefs?.showComments ?? true; + let showMentions = + postPreferences?.showMentions ?? pubPrefs?.showMentions ?? true; + let showRecommends = + postPreferences?.showRecommends ?? pubPrefs?.showRecommends ?? true; + + const updatePreference = ( + field: keyof PostPreferences, + value: boolean, + ) => { + let current: PostPreferences = postPreferences || {}; + rep?.mutate.updatePublicationDraft({ + preferences: { ...current, [field]: value }, + }); + }; + + return ( + } + label="Settings" + /> + } + > +
+
+ This Post Settings +
+
+ updatePreference("showComments", !showComments)} + > +
Show Comments
+
+ updatePreference("showMentions", !showMentions)} + > +
+
Show Mentions
+
+ Display a list Bluesky mentions about your post +
+
+
+ updatePreference("showRecommends", !showRecommends)} + > +
+
Show Recommends
+
+ Allow readers to recommend/like your post +
+
+
+
+
+
+ ); +} diff --git a/lexicons/api/lexicons.ts b/lexicons/api/lexicons.ts index a197ea95..a4b2a81f 100644 --- a/lexicons/api/lexicons.ts +++ b/lexicons/api/lexicons.ts @@ -1469,6 +1469,10 @@ export const schemaDict = { type: 'ref', ref: 'lex:pub.leaflet.publication#theme', }, + preferences: { + type: 'ref', + ref: 'lex:pub.leaflet.publication#preferences', + }, tags: { type: 'array', items: { @@ -1868,6 +1872,10 @@ export const schemaDict = { type: 'boolean', default: true, }, + showRecommends: { + type: 'boolean', + default: true, + }, }, }, theme: { @@ -2194,6 +2202,11 @@ export const schemaDict = { maxLength: 5000, type: 'string', }, + preferences: { + type: 'union', + refs: ['lex:pub.leaflet.publication#preferences'], + closed: false, + }, updatedAt: { format: 'datetime', type: 'string', @@ -2290,6 +2303,10 @@ export const schemaDict = { default: false, type: 'boolean', }, + showRecommends: { + default: true, + type: 'boolean', + }, }, type: 'object', }, diff --git a/lexicons/api/types/pub/leaflet/document.ts b/lexicons/api/types/pub/leaflet/document.ts index 6f6805da..ff587ec7 100644 --- a/lexicons/api/types/pub/leaflet/document.ts +++ b/lexicons/api/types/pub/leaflet/document.ts @@ -23,6 +23,7 @@ export interface Record { publication?: string author: string theme?: PubLeafletPublication.Theme + preferences?: PubLeafletPublication.Preferences tags?: string[] coverImage?: BlobRef pages: ( diff --git a/lexicons/api/types/pub/leaflet/publication.ts b/lexicons/api/types/pub/leaflet/publication.ts index 51143ee0..a2aaee34 100644 --- a/lexicons/api/types/pub/leaflet/publication.ts +++ b/lexicons/api/types/pub/leaflet/publication.ts @@ -1,94 +1,90 @@ /** * GENERATED CODE - DO NOT MODIFY */ -import { type ValidationResult, BlobRef } from "@atproto/lexicon"; -import { CID } from "multiformats/cid"; -import { validate as _validate } from "../../../lexicons"; -import { - type $Typed, - is$typed as _is$typed, - type OmitKey, -} from "../../../util"; -import type * as PubLeafletThemeColor from "./theme/color"; -import type * as PubLeafletThemeBackgroundImage from "./theme/backgroundImage"; +import { type ValidationResult, BlobRef } from '@atproto/lexicon' +import { CID } from 'multiformats/cid' +import { validate as _validate } from '../../../lexicons' +import { type $Typed, is$typed as _is$typed, type OmitKey } from '../../../util' +import type * as PubLeafletThemeColor from './theme/color' +import type * as PubLeafletThemeBackgroundImage from './theme/backgroundImage' const is$typed = _is$typed, - validate = _validate; -const id = "pub.leaflet.publication"; + validate = _validate +const id = 'pub.leaflet.publication' export interface Record { - $type: "pub.leaflet.publication"; - name: string; - base_path?: string; - description?: string; - icon?: BlobRef; - theme?: Theme; - preferences?: Preferences; - [k: string]: unknown; + $type: 'pub.leaflet.publication' + name: string + base_path?: string + description?: string + icon?: BlobRef + theme?: Theme + preferences?: Preferences + [k: string]: unknown } -const hashRecord = "main"; +const hashRecord = 'main' export function isRecord(v: V) { - return is$typed(v, id, hashRecord); + return is$typed(v, id, hashRecord) } export function validateRecord(v: V) { - return validate(v, id, hashRecord, true); + return validate(v, id, hashRecord, true) } export interface Preferences { - $type?: "pub.leaflet.publication#preferences"; - showInDiscover: boolean; - showComments: boolean; - showMentions: boolean; - showPrevNext: boolean; - showRecommends: boolean; + $type?: 'pub.leaflet.publication#preferences' + showInDiscover: boolean + showComments: boolean + showMentions: boolean + showPrevNext: boolean + showRecommends: boolean } -const hashPreferences = "preferences"; +const hashPreferences = 'preferences' export function isPreferences(v: V) { - return is$typed(v, id, hashPreferences); + return is$typed(v, id, hashPreferences) } export function validatePreferences(v: V) { - return validate(v, id, hashPreferences); + return validate(v, id, hashPreferences) } export interface Theme { - $type?: "pub.leaflet.publication#theme"; + $type?: 'pub.leaflet.publication#theme' backgroundColor?: | $Typed | $Typed - | { $type: string }; - backgroundImage?: PubLeafletThemeBackgroundImage.Main; - pageWidth?: number; + | { $type: string } + backgroundImage?: PubLeafletThemeBackgroundImage.Main + pageWidth?: number primary?: | $Typed | $Typed - | { $type: string }; + | { $type: string } pageBackground?: | $Typed | $Typed - | { $type: string }; - showPageBackground: boolean; + | { $type: string } + showPageBackground: boolean accentBackground?: | $Typed | $Typed - | { $type: string }; + | { $type: string } accentText?: | $Typed | $Typed - | { $type: string }; + | { $type: string } } -const hashTheme = "theme"; +const hashTheme = 'theme' export function isTheme(v: V) { - return is$typed(v, id, hashTheme); + return is$typed(v, id, hashTheme) } export function validateTheme(v: V) { - return validate(v, id, hashTheme); + return validate(v, id, hashTheme) } diff --git a/lexicons/api/types/site/standard/document.ts b/lexicons/api/types/site/standard/document.ts index dccb1dfe..b6ff4fc0 100644 --- a/lexicons/api/types/site/standard/document.ts +++ b/lexicons/api/types/site/standard/document.ts @@ -28,6 +28,7 @@ export interface Record { textContent?: string theme?: PubLeafletPublication.Theme title: string + preferences?: $Typed | { $type: string } updatedAt?: string [k: string]: unknown } diff --git a/lexicons/api/types/site/standard/publication.ts b/lexicons/api/types/site/standard/publication.ts index 08f80d9c..42211459 100644 --- a/lexicons/api/types/site/standard/publication.ts +++ b/lexicons/api/types/site/standard/publication.ts @@ -1,58 +1,54 @@ /** * GENERATED CODE - DO NOT MODIFY */ -import { type ValidationResult, BlobRef } from "@atproto/lexicon"; -import { CID } from "multiformats/cid"; -import { validate as _validate } from "../../../lexicons"; -import { - type $Typed, - is$typed as _is$typed, - type OmitKey, -} from "../../../util"; -import type * as SiteStandardThemeBasic from "./theme/basic"; -import type * as PubLeafletPublication from "../../pub/leaflet/publication"; +import { type ValidationResult, BlobRef } from '@atproto/lexicon' +import { CID } from 'multiformats/cid' +import { validate as _validate } from '../../../lexicons' +import { type $Typed, is$typed as _is$typed, type OmitKey } from '../../../util' +import type * as SiteStandardThemeBasic from './theme/basic' +import type * as PubLeafletPublication from '../../pub/leaflet/publication' const is$typed = _is$typed, - validate = _validate; -const id = "site.standard.publication"; + validate = _validate +const id = 'site.standard.publication' export interface Record { - $type: "site.standard.publication"; - basicTheme?: SiteStandardThemeBasic.Main; - theme?: $Typed | { $type: string }; - description?: string; - icon?: BlobRef; - name: string; - preferences?: Preferences; - url: string; - [k: string]: unknown; + $type: 'site.standard.publication' + basicTheme?: SiteStandardThemeBasic.Main + theme?: $Typed | { $type: string } + description?: string + icon?: BlobRef + name: string + preferences?: Preferences + url: string + [k: string]: unknown } -const hashRecord = "main"; +const hashRecord = 'main' export function isRecord(v: V) { - return is$typed(v, id, hashRecord); + return is$typed(v, id, hashRecord) } export function validateRecord(v: V) { - return validate(v, id, hashRecord, true); + return validate(v, id, hashRecord, true) } export interface Preferences { - $type?: "site.standard.publication#preferences"; - showInDiscover: boolean; - showComments: boolean; - showMentions: boolean; - showPrevNext: boolean; - showRecommends: boolean; + $type?: 'site.standard.publication#preferences' + showInDiscover: boolean + showComments: boolean + showMentions: boolean + showPrevNext: boolean + showRecommends: boolean } -const hashPreferences = "preferences"; +const hashPreferences = 'preferences' export function isPreferences(v: V) { - return is$typed(v, id, hashPreferences); + return is$typed(v, id, hashPreferences) } export function validatePreferences(v: V) { - return validate(v, id, hashPreferences); + return validate(v, id, hashPreferences) } diff --git a/lexicons/pub/leaflet/document.json b/lexicons/pub/leaflet/document.json index 2e714226..7ab00088 100644 --- a/lexicons/pub/leaflet/document.json +++ b/lexicons/pub/leaflet/document.json @@ -46,6 +46,10 @@ "type": "ref", "ref": "pub.leaflet.publication#theme" }, + "preferences": { + "type": "ref", + "ref": "pub.leaflet.publication#preferences" + }, "tags": { "type": "array", "items": { diff --git a/lexicons/site/standard/document.json b/lexicons/site/standard/document.json index 433f19dc..8305dd47 100644 --- a/lexicons/site/standard/document.json +++ b/lexicons/site/standard/document.json @@ -57,6 +57,11 @@ "maxLength": 5000, "type": "string" }, + "preferences": { + "type": "union", + "refs": ["pub.leaflet.publication#preferences"], + "closed": false + }, "updatedAt": { "format": "datetime", "type": "string" diff --git a/lexicons/src/document.ts b/lexicons/src/document.ts index f1beabff..15e3f00b 100644 --- a/lexicons/src/document.ts +++ b/lexicons/src/document.ts @@ -23,6 +23,10 @@ export const PubLeafletDocument: LexiconDoc = { publication: { type: "string", format: "at-uri" }, author: { type: "string", format: "at-identifier" }, theme: { type: "ref", ref: "pub.leaflet.publication#theme" }, + preferences: { + type: "ref", + ref: "pub.leaflet.publication#preferences", + }, tags: { type: "array", items: { type: "string", maxLength: 50 } }, coverImage: { type: "blob", diff --git a/lexicons/src/normalize.ts b/lexicons/src/normalize.ts index 4f7294c7..9e8755ec 100644 --- a/lexicons/src/normalize.ts +++ b/lexicons/src/normalize.ts @@ -28,6 +28,7 @@ import { AtUri } from "@atproto/syntax"; export type NormalizedDocument = SiteStandardDocument.Record & { // Keep the original theme for components that need leaflet-specific styling theme?: PubLeafletPublication.Theme; + preferences?: SiteStandardPublication.Preferences; }; // Normalized publication type - uses the generated site.standard.publication type @@ -169,9 +170,13 @@ export function normalizeDocument( // Pass through site.standard records directly (theme is already in correct format if present) if (isStandardDocument(record)) { + const preferences = record.preferences as + | SiteStandardPublication.Preferences + | undefined; return { ...record, theme: record.theme, + preferences, } as NormalizedDocument; } @@ -198,6 +203,10 @@ export function normalizeDocument( } : undefined; + // Extract preferences if present (available after lexicon rebuild) + const leafletPrefs = (record as Record) + .preferences as SiteStandardPublication.Preferences | undefined; + return { $type: "site.standard.document", title: record.title, @@ -210,6 +219,7 @@ export function normalizeDocument( bskyPostRef: record.postRef, content, theme: record.theme, + preferences: leafletPrefs, }; } diff --git a/package-lock.json b/package-lock.json index bffdd153..9fb9d0cb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,6 @@ "@atproto/oauth-client-node": "^0.3.8", "@atproto/sync": "^0.1.34", "@atproto/syntax": "^0.3.3", - "@atproto/tap": "^0.1.1", "@atproto/xrpc": "^0.7.5", "@atproto/xrpc-server": "^0.9.5", "@hono/node-server": "^1.14.3", @@ -396,92 +395,6 @@ "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==", "license": "(Apache-2.0 AND MIT)" }, - "node_modules/@atproto/lex": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/@atproto/lex/-/lex-0.0.9.tgz", - "integrity": "sha512-o6gauf1lz0iyzJR0rqSj4VHOrO+Nt8+/iPb0KPojw1ieXk13zOSTSxotAoDzO/dP6y8Ey5jxwuCQGuzab/4XnQ==", - "license": "MIT", - "dependencies": { - "@atproto/lex-builder": "0.0.9", - "@atproto/lex-client": "0.0.7", - "@atproto/lex-data": "0.0.6", - "@atproto/lex-installer": "0.0.9", - "@atproto/lex-json": "0.0.6", - "@atproto/lex-schema": "0.0.7", - "tslib": "^2.8.1", - "yargs": "^17.0.0" - }, - "bin": { - "lex": "bin/lex", - "ts-lex": "bin/lex" - } - }, - "node_modules/@atproto/lex-builder": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/@atproto/lex-builder/-/lex-builder-0.0.9.tgz", - "integrity": "sha512-buOFk1JpuW3twI7To7f/67zQQ1NulLHf/oasH/kTOPUAd0dNyeAa13t9eRSVGbwi0BcZYxRxBm0QzPmdLKyuyw==", - "license": "MIT", - "dependencies": { - "@atproto/lex-document": "0.0.8", - "@atproto/lex-schema": "0.0.7", - "prettier": "^3.2.5", - "ts-morph": "^27.0.0", - "tslib": "^2.8.1" - } - }, - "node_modules/@atproto/lex-builder/node_modules/@ts-morph/common": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.28.1.tgz", - "integrity": "sha512-W74iWf7ILp1ZKNYXY5qbddNaml7e9Sedv5lvU1V8lftlitkc9Pq1A+jlH23ltDgWYeZFFEqGCD1Ies9hqu3O+g==", - "license": "MIT", - "dependencies": { - "minimatch": "^10.0.1", - "path-browserify": "^1.0.1", - "tinyglobby": "^0.2.14" - } - }, - "node_modules/@atproto/lex-builder/node_modules/minimatch": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", - "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/brace-expansion": "^5.0.0" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@atproto/lex-builder/node_modules/ts-morph": { - "version": "27.0.2", - "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-27.0.2.tgz", - "integrity": "sha512-fhUhgeljcrdZ+9DZND1De1029PrE+cMkIP7ooqkLRTrRLTqcki2AstsyJm0vRNbTbVCNJ0idGlbBrfqc7/nA8w==", - "license": "MIT", - "dependencies": { - "@ts-morph/common": "~0.28.1", - "code-block-writer": "^13.0.3" - } - }, - "node_modules/@atproto/lex-cbor": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/@atproto/lex-cbor/-/lex-cbor-0.0.6.tgz", - "integrity": "sha512-lee2T00owDy3I1plRHuURT6f98NIpYZZr2wXa5pJZz5JzefZ+nv8gJ2V70C2f+jmSG+5S9NTIy4uJw94vaHf4A==", - "license": "MIT", - "dependencies": { - "@atproto/lex-data": "0.0.6", - "multiformats": "^9.9.0", - "tslib": "^2.8.1" - } - }, - "node_modules/@atproto/lex-cbor/node_modules/multiformats": { - "version": "9.9.0", - "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz", - "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==", - "license": "(Apache-2.0 AND MIT)" - }, "node_modules/@atproto/lex-cli": { "version": "0.9.5", "resolved": "https://registry.npmjs.org/@atproto/lex-cli/-/lex-cli-0.9.5.tgz", @@ -515,149 +428,6 @@ "tslib": "^2.8.1" } }, - "node_modules/@atproto/lex-client": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/@atproto/lex-client/-/lex-client-0.0.7.tgz", - "integrity": "sha512-ofUz3yXJ0nN/M9aqqF2ZUL/4D1wWT1P4popCfV3OEDsDrtWofMflYPFz1IWuyPa2e83paaEHRhaw3bZEhgXH1w==", - "license": "MIT", - "dependencies": { - "@atproto/lex-data": "0.0.6", - "@atproto/lex-json": "0.0.6", - "@atproto/lex-schema": "0.0.7", - "tslib": "^2.8.1" - } - }, - "node_modules/@atproto/lex-data": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/@atproto/lex-data/-/lex-data-0.0.6.tgz", - "integrity": "sha512-MBNB4ghRJQzuXK1zlUPljpPbQcF1LZ5dzxy274KqPt4p3uPuRw0mHjgcCoWzRUNBQC685WMQR4IN9DHtsnG57A==", - "license": "MIT", - "dependencies": { - "@atproto/syntax": "0.4.2", - "multiformats": "^9.9.0", - "tslib": "^2.8.1", - "uint8arrays": "3.0.0", - "unicode-segmenter": "^0.14.0" - } - }, - "node_modules/@atproto/lex-data/node_modules/@atproto/syntax": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.4.2.tgz", - "integrity": "sha512-X9XSRPinBy/0VQ677j8VXlBsYSsUXaiqxWVpGGxJYsAhugdQRb0jqaVKJFtm6RskeNkV6y9xclSUi9UYG/COrA==", - "license": "MIT" - }, - "node_modules/@atproto/lex-data/node_modules/multiformats": { - "version": "9.9.0", - "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz", - "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==", - "license": "(Apache-2.0 AND MIT)" - }, - "node_modules/@atproto/lex-document": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/@atproto/lex-document/-/lex-document-0.0.8.tgz", - "integrity": "sha512-p3l5h96Hx0vxUwbO/eas6x5h2vU0JVN1a/ktX4k3PlK9YLXfWMFsv+RdVwVZom8o0irHwlcyh1D/cY0PyUojDA==", - "license": "MIT", - "dependencies": { - "@atproto/lex-schema": "0.0.7", - "core-js": "^3", - "tslib": "^2.8.1" - } - }, - "node_modules/@atproto/lex-installer": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/@atproto/lex-installer/-/lex-installer-0.0.9.tgz", - "integrity": "sha512-zEeIeSaSCb3j+zNsqqMY7+X5FO6fxy/MafaCEj42KsXQHNcobuygZsnG/0fxMj/kMvhjrNUCp/w9PyOMwx4hQg==", - "license": "MIT", - "dependencies": { - "@atproto/lex-builder": "0.0.9", - "@atproto/lex-cbor": "0.0.6", - "@atproto/lex-data": "0.0.6", - "@atproto/lex-document": "0.0.8", - "@atproto/lex-resolver": "0.0.8", - "@atproto/lex-schema": "0.0.7", - "@atproto/syntax": "0.4.2", - "tslib": "^2.8.1" - } - }, - "node_modules/@atproto/lex-installer/node_modules/@atproto/syntax": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.4.2.tgz", - "integrity": "sha512-X9XSRPinBy/0VQ677j8VXlBsYSsUXaiqxWVpGGxJYsAhugdQRb0jqaVKJFtm6RskeNkV6y9xclSUi9UYG/COrA==", - "license": "MIT" - }, - "node_modules/@atproto/lex-json": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/@atproto/lex-json/-/lex-json-0.0.6.tgz", - "integrity": "sha512-EILnN5cditPvf+PCNjXt7reMuzjugxAL1fpSzmzJbEMGMUwxOf5pPWxRsaA/M3Boip4NQZ+6DVrPOGUMlnqceg==", - "license": "MIT", - "dependencies": { - "@atproto/lex-data": "0.0.6", - "tslib": "^2.8.1" - } - }, - "node_modules/@atproto/lex-resolver": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/@atproto/lex-resolver/-/lex-resolver-0.0.8.tgz", - "integrity": "sha512-4hXT560+k5BIttouuhXOr+UkhAuFvvkJaVdqYb8vx2Ez7eHPiZ+yWkUK6FKpyGsx2whHkJzgleEA6DNWtdDlWA==", - "license": "MIT", - "dependencies": { - "@atproto-labs/did-resolver": "0.2.5", - "@atproto/crypto": "0.4.5", - "@atproto/lex-client": "0.0.7", - "@atproto/lex-data": "0.0.6", - "@atproto/lex-document": "0.0.8", - "@atproto/lex-schema": "0.0.7", - "@atproto/repo": "0.8.12", - "@atproto/syntax": "0.4.2", - "tslib": "^2.8.1" - } - }, - "node_modules/@atproto/lex-resolver/node_modules/@atproto-labs/did-resolver": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/@atproto-labs/did-resolver/-/did-resolver-0.2.5.tgz", - "integrity": "sha512-he7EC6OMSifNs01a4RT9mta/yYitoKDzlK9ty2TFV5Uj/+HpB4vYMRdIDFrRW0Hcsehy90E2t/dw0t7361MEKQ==", - "license": "MIT", - "dependencies": { - "@atproto-labs/fetch": "0.2.3", - "@atproto-labs/pipe": "0.1.1", - "@atproto-labs/simple-store": "0.3.0", - "@atproto-labs/simple-store-memory": "0.1.4", - "@atproto/did": "0.2.4", - "zod": "^3.23.8" - } - }, - "node_modules/@atproto/lex-resolver/node_modules/@atproto/did": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/@atproto/did/-/did-0.2.4.tgz", - "integrity": "sha512-nxNiCgXeo7pfjojq9fpfZxCO0X0xUipNVKW+AHNZwQKiUDt6zYL0VXEfm8HBUwQOCmKvj2pRRSM1Cur+tUWk3g==", - "license": "MIT", - "dependencies": { - "zod": "^3.23.8" - } - }, - "node_modules/@atproto/lex-resolver/node_modules/@atproto/syntax": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.4.2.tgz", - "integrity": "sha512-X9XSRPinBy/0VQ677j8VXlBsYSsUXaiqxWVpGGxJYsAhugdQRb0jqaVKJFtm6RskeNkV6y9xclSUi9UYG/COrA==", - "license": "MIT" - }, - "node_modules/@atproto/lex-schema": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/@atproto/lex-schema/-/lex-schema-0.0.7.tgz", - "integrity": "sha512-/7HkTUsnP1rlzmVE6nnY0kl/hydL/W8V29V8BhFwdAvdDKpYcdRgzzsMe38LAt+ZOjHknRCZDIKGsbQMSbJErw==", - "license": "MIT", - "dependencies": { - "@atproto/lex-data": "0.0.6", - "@atproto/syntax": "0.4.2", - "tslib": "^2.8.1" - } - }, - "node_modules/@atproto/lex-schema/node_modules/@atproto/syntax": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.4.2.tgz", - "integrity": "sha512-X9XSRPinBy/0VQ677j8VXlBsYSsUXaiqxWVpGGxJYsAhugdQRb0jqaVKJFtm6RskeNkV6y9xclSUi9UYG/COrA==", - "license": "MIT" - }, "node_modules/@atproto/lexicon": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/@atproto/lexicon/-/lexicon-0.6.1.tgz", @@ -848,135 +618,6 @@ "integrity": "sha512-8CNmi5DipOLaVeSMPggMe7FCksVag0aO6XZy9WflbduTKM4dFZVCs4686UeMLfGRXX+X966XgwECHoLYrovMMg==", "license": "MIT" }, - "node_modules/@atproto/tap": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@atproto/tap/-/tap-0.1.1.tgz", - "integrity": "sha512-gW4NzLOxj74TzaDOVzzzt5kl2PdC0r75XkIpYpI5xobwCfsc/DmVtwpuSw1fW9gr4Vzk2Q90S9UE4ifAFl2gyA==", - "license": "MIT", - "dependencies": { - "@atproto/common": "^0.5.6", - "@atproto/lex": "^0.0.9", - "@atproto/syntax": "^0.4.2", - "@atproto/ws-client": "^0.0.4", - "ws": "^8.12.0", - "zod": "^3.23.8" - }, - "engines": { - "node": ">=18.7.0" - } - }, - "node_modules/@atproto/tap/node_modules/@atproto/common": { - "version": "0.5.10", - "resolved": "https://registry.npmjs.org/@atproto/common/-/common-0.5.10.tgz", - "integrity": "sha512-A1+4W3JmjZIgmtJFLJBAaoVruZhRL0ANtyjZ91aJR4rjHcZuaQ+v4IFR1UcE6yyTATacLdBk6ADy8OtxXzq14g==", - "license": "MIT", - "dependencies": { - "@atproto/common-web": "^0.4.15", - "@atproto/lex-cbor": "^0.0.10", - "@atproto/lex-data": "^0.0.10", - "iso-datestring-validator": "^2.2.2", - "multiformats": "^9.9.0", - "pino": "^8.21.0" - }, - "engines": { - "node": ">=18.7.0" - } - }, - "node_modules/@atproto/tap/node_modules/@atproto/lex-cbor": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/@atproto/lex-cbor/-/lex-cbor-0.0.10.tgz", - "integrity": "sha512-5RtV90iIhRNCXXvvETd3KlraV8XGAAAgOmiszUb+l8GySDU/sGk7AlVvArFfXnj/S/GXJq8DP6IaUxCw/sPASA==", - "license": "MIT", - "dependencies": { - "@atproto/lex-data": "^0.0.10", - "tslib": "^2.8.1" - } - }, - "node_modules/@atproto/tap/node_modules/@atproto/lex-data": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/@atproto/lex-data/-/lex-data-0.0.10.tgz", - "integrity": "sha512-FDbcy8VIUVzS9Mi1F8SMxbkL/jOUmRRpqbeM1xB4A0fMxeZJTxf6naAbFt4gYF3quu/+TPJGmio6/7cav05FqQ==", - "license": "MIT", - "dependencies": { - "multiformats": "^9.9.0", - "tslib": "^2.8.1", - "uint8arrays": "3.0.0", - "unicode-segmenter": "^0.14.0" - } - }, - "node_modules/@atproto/tap/node_modules/@atproto/syntax": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.4.3.tgz", - "integrity": "sha512-YoZUz40YAJr5nPwvCDWgodEOlt5IftZqPJvA0JDWjuZKD8yXddTwSzXSaKQAzGOpuM+/A3uXRtPzJJqlScc+iA==", - "license": "MIT", - "dependencies": { - "tslib": "^2.8.1" - } - }, - "node_modules/@atproto/tap/node_modules/multiformats": { - "version": "9.9.0", - "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz", - "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==", - "license": "(Apache-2.0 AND MIT)" - }, - "node_modules/@atproto/ws-client": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/@atproto/ws-client/-/ws-client-0.0.4.tgz", - "integrity": "sha512-dox1XIymuC7/ZRhUqKezIGgooZS45C6vHCfu0PnWjfvsLCK2kAlnvX4IBkA/WpcoijDhQ9ejChnFbo/sLmgvAg==", - "license": "MIT", - "dependencies": { - "@atproto/common": "^0.5.3", - "ws": "^8.12.0" - }, - "engines": { - "node": ">=18.7.0" - } - }, - "node_modules/@atproto/ws-client/node_modules/@atproto/common": { - "version": "0.5.10", - "resolved": "https://registry.npmjs.org/@atproto/common/-/common-0.5.10.tgz", - "integrity": "sha512-A1+4W3JmjZIgmtJFLJBAaoVruZhRL0ANtyjZ91aJR4rjHcZuaQ+v4IFR1UcE6yyTATacLdBk6ADy8OtxXzq14g==", - "license": "MIT", - "dependencies": { - "@atproto/common-web": "^0.4.15", - "@atproto/lex-cbor": "^0.0.10", - "@atproto/lex-data": "^0.0.10", - "iso-datestring-validator": "^2.2.2", - "multiformats": "^9.9.0", - "pino": "^8.21.0" - }, - "engines": { - "node": ">=18.7.0" - } - }, - "node_modules/@atproto/ws-client/node_modules/@atproto/lex-cbor": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/@atproto/lex-cbor/-/lex-cbor-0.0.10.tgz", - "integrity": "sha512-5RtV90iIhRNCXXvvETd3KlraV8XGAAAgOmiszUb+l8GySDU/sGk7AlVvArFfXnj/S/GXJq8DP6IaUxCw/sPASA==", - "license": "MIT", - "dependencies": { - "@atproto/lex-data": "^0.0.10", - "tslib": "^2.8.1" - } - }, - "node_modules/@atproto/ws-client/node_modules/@atproto/lex-data": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/@atproto/lex-data/-/lex-data-0.0.10.tgz", - "integrity": "sha512-FDbcy8VIUVzS9Mi1F8SMxbkL/jOUmRRpqbeM1xB4A0fMxeZJTxf6naAbFt4gYF3quu/+TPJGmio6/7cav05FqQ==", - "license": "MIT", - "dependencies": { - "multiformats": "^9.9.0", - "tslib": "^2.8.1", - "uint8arrays": "3.0.0", - "unicode-segmenter": "^0.14.0" - } - }, - "node_modules/@atproto/ws-client/node_modules/multiformats": { - "version": "9.9.0", - "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz", - "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==", - "license": "(Apache-2.0 AND MIT)" - }, "node_modules/@atproto/xrpc": { "version": "0.7.5", "resolved": "https://registry.npmjs.org/@atproto/xrpc/-/xrpc-0.7.5.tgz", @@ -3052,27 +2693,6 @@ "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==", "license": "(Apache-2.0 AND MIT)" }, - "node_modules/@isaacs/balanced-match": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", - "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", - "license": "MIT", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@isaacs/brace-expansion": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", - "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", - "license": "MIT", - "dependencies": { - "@isaacs/balanced-match": "^4.0.1" - }, - "engines": { - "node": "20 || >=22" - } - }, "node_modules/@isaacs/fs-minipass": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", @@ -9631,6 +9251,7 @@ "version": "13.0.3", "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.3.tgz", "integrity": "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==", + "dev": true, "license": "MIT" }, "node_modules/collapse-white-space": { @@ -9739,17 +9360,6 @@ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", "license": "MIT" }, - "node_modules/core-js": { - "version": "3.47.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.47.0.tgz", - "integrity": "sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg==", - "hasInstallScript": true, - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, "node_modules/crelt": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", @@ -16142,6 +15752,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, "license": "MIT" }, "node_modules/path-exists": { @@ -16424,6 +16035,7 @@ "version": "3.2.5", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", + "dev": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -18520,6 +18132,7 @@ "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", @@ -18536,6 +18149,7 @@ "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, "license": "MIT", "engines": { "node": ">=12.0.0" @@ -18553,6 +18167,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" diff --git a/package.json b/package.json index 5e9e2ee4..4f9c5397 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,6 @@ "@atproto/oauth-client-node": "^0.3.8", "@atproto/sync": "^0.1.34", "@atproto/syntax": "^0.3.3", - "@atproto/tap": "^0.1.1", "@atproto/xrpc": "^0.7.5", "@atproto/xrpc-server": "^0.9.5", "@hono/node-server": "^1.14.3", diff --git a/src/replicache/mutations.ts b/src/replicache/mutations.ts index d8bf93c6..40db9f73 100644 --- a/src/replicache/mutations.ts +++ b/src/replicache/mutations.ts @@ -659,6 +659,11 @@ const updatePublicationDraft: Mutation<{ tags?: string[]; cover_image?: string | null; localPublishedAt?: string | null; + preferences?: { + showComments?: boolean; + showMentions?: boolean; + showRecommends?: boolean; + } | null; }> = async (args, ctx) => { await ctx.runOnServer(async (serverCtx) => { console.log("updating"); @@ -667,11 +672,17 @@ const updatePublicationDraft: Mutation<{ title?: string; tags?: string[]; cover_image?: string | null; + preferences?: { + showComments?: boolean; + showMentions?: boolean; + showRecommends?: boolean; + } | null; } = {}; if (args.description !== undefined) updates.description = args.description; if (args.title !== undefined) updates.title = args.title; if (args.tags !== undefined) updates.tags = args.tags; if (args.cover_image !== undefined) updates.cover_image = args.cover_image; + if (args.preferences !== undefined) updates.preferences = args.preferences; if (Object.keys(updates).length > 0) { // First try to update leaflets_in_publications (for publications) @@ -700,6 +711,8 @@ const updatePublicationDraft: Mutation<{ await tx.set("publication_cover_image", args.cover_image); if (args.localPublishedAt !== undefined) await tx.set("publication_local_published_at", args.localPublishedAt); + if (args.preferences !== undefined) + await tx.set("post_preferences", args.preferences); }); }; diff --git a/src/utils/mergePreferences.ts b/src/utils/mergePreferences.ts new file mode 100644 index 00000000..c2e75a87 --- /dev/null +++ b/src/utils/mergePreferences.ts @@ -0,0 +1,24 @@ +type PreferencesInput = { + showComments?: boolean; + showMentions?: boolean; + showRecommends?: boolean; + showPrevNext?: boolean; +} | null; + +export function mergePreferences( + documentPrefs?: PreferencesInput, + publicationPrefs?: PreferencesInput, +): { + showComments?: boolean; + showMentions?: boolean; + showRecommends?: boolean; + showPrevNext?: boolean; +} { + return { + showComments: documentPrefs?.showComments ?? publicationPrefs?.showComments, + showMentions: documentPrefs?.showMentions ?? publicationPrefs?.showMentions, + showRecommends: + documentPrefs?.showRecommends ?? publicationPrefs?.showRecommends, + showPrevNext: publicationPrefs?.showPrevNext, + }; +} diff --git a/supabase/database.types.ts b/supabase/database.types.ts index edbfe86e..6153540b 100644 --- a/supabase/database.types.ts +++ b/supabase/database.types.ts @@ -589,6 +589,7 @@ export type Database = { description: string doc: string | null leaflet: string + preferences: Json | null publication: string tags: string[] | null title: string @@ -599,6 +600,7 @@ export type Database = { description?: string doc?: string | null leaflet: string + preferences?: Json | null publication: string tags?: string[] | null title?: string @@ -609,6 +611,7 @@ export type Database = { description?: string doc?: string | null leaflet?: string + preferences?: Json | null publication?: string tags?: string[] | null title?: string @@ -645,6 +648,7 @@ export type Database = { description: string document: string leaflet: string + preferences: Json | null tags: string[] | null title: string } @@ -655,6 +659,7 @@ export type Database = { description?: string document: string leaflet: string + preferences?: Json | null tags?: string[] | null title?: string } @@ -665,6 +670,7 @@ export type Database = { description?: string document?: string leaflet?: string + preferences?: Json | null tags?: string[] | null title?: string } diff --git a/supabase/migrations/20260208000000_add_preferences_to_drafts.sql b/supabase/migrations/20260208000000_add_preferences_to_drafts.sql new file mode 100644 index 00000000..e54b639a --- /dev/null +++ b/supabase/migrations/20260208000000_add_preferences_to_drafts.sql @@ -0,0 +1,2 @@ +ALTER TABLE leaflets_in_publications ADD COLUMN preferences jsonb; +ALTER TABLE leaflets_to_documents ADD COLUMN preferences jsonb; -- 2.51.2 From 04578602b038f2f1a9a5c885e1242056632676ea Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Sun, 8 Feb 2026 10:37:22 +0400 Subject: [PATCH 11/51] fix types --- lexicons/src/normalize.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lexicons/src/normalize.ts b/lexicons/src/normalize.ts index 9e8755ec..70ac45b9 100644 --- a/lexicons/src/normalize.ts +++ b/lexicons/src/normalize.ts @@ -219,7 +219,9 @@ export function normalizeDocument( bskyPostRef: record.postRef, content, theme: record.theme, - preferences: leafletPrefs, + preferences: leafletPrefs + ? { ...leafletPrefs, $type: "site.standard.publication#preferences" as const } + : undefined, }; } -- 2.51.2 From 71e47d3e8fada27ccc60eab1fefb9f904bd1a3ac Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Sun, 8 Feb 2026 11:21:25 +0400 Subject: [PATCH 12/51] request blob permissions --- app/api/oauth/[route]/oauth-metadata.ts | 2 +- app/api/oauth/[route]/route.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/api/oauth/[route]/oauth-metadata.ts b/app/api/oauth/[route]/oauth-metadata.ts index 855fc0bc..32ed3b35 100644 --- a/app/api/oauth/[route]/oauth-metadata.ts +++ b/app/api/oauth/[route]/oauth-metadata.ts @@ -8,7 +8,7 @@ const hostname = : "https://leaflet.pub"; const scope = - "atproto transition:generic transition:email include:pub.leaflet.authFullPermissions include:site.standard.authFull include:app.bsky.authCreatePosts"; + "atproto transition:generic transition:email include:pub.leaflet.authFullPermissions include:site.standard.authFull include:app.bsky.authCreatePosts blob:*/*"; const localconfig: OAuthClientMetadataInput = { client_id: `http://localhost/?redirect_uri=${encodeURI(`http://127.0.0.1:3000/api/oauth/callback`)}&scope=${encodeURIComponent(scope)}`, client_name: `Leaflet`, diff --git a/app/api/oauth/[route]/route.ts b/app/api/oauth/[route]/route.ts index 3d2c7641..108e032b 100644 --- a/app/api/oauth/[route]/route.ts +++ b/app/api/oauth/[route]/route.ts @@ -43,7 +43,7 @@ export async function GET( const url = await client.authorize(handle || "https://bsky.social", { scope: - "atproto transition:email include:pub.leaflet.authFullPermissions include:site.standard.authFull include:app.bsky.authCreatePosts", + "atproto transition:email include:pub.leaflet.authFullPermissions include:site.standard.authFull include:app.bsky.authCreatePosts blob:*/*", signal: ac.signal, state: JSON.stringify(state), }); -- 2.51.2 From 7451ae3aa9c984ed04883220af38376d7d071f08 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Sun, 8 Feb 2026 11:27:31 +0400 Subject: [PATCH 13/51] add post settings to footer --- app/[leaflet_id]/Footer.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/[leaflet_id]/Footer.tsx b/app/[leaflet_id]/Footer.tsx index d51cff0d..a202d2ee 100644 --- a/app/[leaflet_id]/Footer.tsx +++ b/app/[leaflet_id]/Footer.tsx @@ -14,6 +14,7 @@ import { useLeafletPublicationData } from "components/PageSWRDataProvider"; import { useIdentityData } from "components/IdentityProvider"; import { useEntity } from "src/replicache"; import { block } from "sharp"; +import { PostSettings } from "components/PostSettings"; export function hasBlockToolbar(blockType: string | null | undefined) { return ( @@ -64,6 +65,7 @@ export function LeafletFooter(props: { entityID: string }) { + ) : ( -- 2.51.2 From 42887e837060062581abca488709212b39c866ec Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Sun, 8 Feb 2026 02:45:45 -0500 Subject: [PATCH 14/51] Fix missing 'of' in Bluesky mentions copy (#265) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add the missing "of" between "list" and "Bluesky" in the "Show Mentions" toggle description across both PostSettings and PostOptions components. Changes: - components/PostSettings.tsx: "Display a list Bluesky mentions..." → "Display a list of Bluesky mentions..." - app/lish/[did]/[publication]/dashboard/settings/PostOptions.tsx: Same fix https://claude.ai/code/session_01UmMsAY9kbwJy4p1nD6sCh8 Co-authored-by: Claude --- app/lish/[did]/[publication]/dashboard/settings/PostOptions.tsx | 2 +- components/PostSettings.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/lish/[did]/[publication]/dashboard/settings/PostOptions.tsx b/app/lish/[did]/[publication]/dashboard/settings/PostOptions.tsx index 70ae3d75..07c02452 100644 --- a/app/lish/[did]/[publication]/dashboard/settings/PostOptions.tsx +++ b/app/lish/[did]/[publication]/dashboard/settings/PostOptions.tsx @@ -105,7 +105,7 @@ export const PostOptions = (props: {
Show Mentions
- Display a list Bluesky mentions about your post + Display a list of Bluesky mentions about your post
diff --git a/components/PostSettings.tsx b/components/PostSettings.tsx index d8a92a0b..a6ec7db8 100644 --- a/components/PostSettings.tsx +++ b/components/PostSettings.tsx @@ -74,7 +74,7 @@ export function PostSettings() {
Show Mentions
- Display a list Bluesky mentions about your post + Display a list of Bluesky mentions about your post
-- 2.51.2 From a4586e5fdac165f5e3953fbc79f2321d16d5cc79 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Mon, 9 Feb 2026 14:32:43 +0400 Subject: [PATCH 15/51] add inngest function to track bsky post likes --- app/api/inngest/client.ts | 6 +++ app/api/inngest/functions/sync_bsky_likes.ts | 41 +++++++++++++++++++ app/api/inngest/route.tsx | 2 + app/api/rpc/[command]/get_publication_data.ts | 1 + .../dashboard/PublishedPostsLists.tsx | 1 + appview/index.ts | 18 ++++++++ drizzle/schema.ts | 3 +- supabase/database.types.ts | 3 ++ .../20260209000000_add_bsky_like_count.sql | 1 + 9 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 app/api/inngest/functions/sync_bsky_likes.ts create mode 100644 supabase/migrations/20260209000000_add_bsky_like_count.sql diff --git a/app/api/inngest/client.ts b/app/api/inngest/client.ts index 4dceb80f..f648b39f 100644 --- a/app/api/inngest/client.ts +++ b/app/api/inngest/client.ts @@ -51,6 +51,12 @@ export type Events = { documentUris?: string[]; }; }; + "appview/sync-bsky-likes": { + data: { + document_uri: string; + bsky_post_uri: string; + }; + }; "user/write-records-to-pds": { data: { did: string; diff --git a/app/api/inngest/functions/sync_bsky_likes.ts b/app/api/inngest/functions/sync_bsky_likes.ts new file mode 100644 index 00000000..9b67aab8 --- /dev/null +++ b/app/api/inngest/functions/sync_bsky_likes.ts @@ -0,0 +1,41 @@ +import { inngest } from "../client"; +import { supabaseServerClient } from "supabase/serverClient"; +import { AtpAgent } from "@atproto/api"; + +const TOTAL_ITERATIONS = 144; // 36 hours at 15-minute intervals + +export const sync_bsky_likes = inngest.createFunction( + { + id: "sync_bsky_likes", + idempotency: "event.data.bsky_post_uri", + }, + { event: "appview/sync-bsky-likes" }, + async ({ event, step }) => { + const { document_uri, bsky_post_uri } = event.data; + + const agent = new AtpAgent({ service: "https://public.api.bsky.app" }); + + const fetchAndUpdate = async () => { + const res = await agent.app.bsky.feed.getPosts({ + uris: [bsky_post_uri], + }); + const post = res.data.posts[0]; + if (!post) return 0; + const likeCount = post.likeCount ?? 0; + await supabaseServerClient + .from("documents") + .update({ bsky_like_count: likeCount }) + .eq("uri", document_uri); + return likeCount; + }; + + let likeCount = await step.run("sync-0", fetchAndUpdate); + + for (let i = 1; i < TOTAL_ITERATIONS; i++) { + await step.sleep(`wait-${i}`, "15m"); + likeCount = await step.run(`sync-${i}`, fetchAndUpdate); + } + + return { likeCount }; + }, +); diff --git a/app/api/inngest/route.tsx b/app/api/inngest/route.tsx index ce86472d..ea0f2f9e 100644 --- a/app/api/inngest/route.tsx +++ b/app/api/inngest/route.tsx @@ -13,6 +13,7 @@ import { check_oauth_session, } from "./functions/cleanup_expired_oauth_sessions"; import { write_records_to_pds } from "./functions/write_records_to_pds"; +import { sync_bsky_likes } from "./functions/sync_bsky_likes"; export const { GET, POST, PUT } = serve({ client: inngest, @@ -28,5 +29,6 @@ export const { GET, POST, PUT } = serve({ cleanup_expired_oauth_sessions, check_oauth_session, write_records_to_pds, + sync_bsky_likes, ], }); diff --git a/app/api/rpc/[command]/get_publication_data.ts b/app/api/rpc/[command]/get_publication_data.ts index 09cc91f4..9fe23804 100644 --- a/app/api/rpc/[command]/get_publication_data.ts +++ b/app/api/rpc/[command]/get_publication_data.ts @@ -86,6 +86,7 @@ export const get_publication_data = makeRoute({ indexed_at: dip.documents.indexed_at, sort_date: dip.documents.sort_date, data: dip.documents.data, + bsky_like_count: dip.documents.bsky_like_count, commentsCount: dip.documents.comments_on_documents[0]?.count || 0, mentionsCount: dip.documents.document_mentions_in_bsky[0]?.count || 0, recommendsCount: diff --git a/app/lish/[did]/[publication]/dashboard/PublishedPostsLists.tsx b/app/lish/[did]/[publication]/dashboard/PublishedPostsLists.tsx index a7e4c630..29b4b8a8 100644 --- a/app/lish/[did]/[publication]/dashboard/PublishedPostsLists.tsx +++ b/app/lish/[did]/[publication]/dashboard/PublishedPostsLists.tsx @@ -112,6 +112,7 @@ function PublishedPostItem(props: { indexed_at: doc.indexed_at, sort_date: doc.sort_date, data: doc.data, + bsky_like_count: doc.bsky_like_count ?? 0, }, }, ], diff --git a/appview/index.ts b/appview/index.ts index 139ea9fd..7319f549 100644 --- a/appview/index.ts +++ b/appview/index.ts @@ -109,6 +109,15 @@ async function handleEvent(evt: Event) { data: record.value as Json, }); if (docResult.error) console.log(docResult.error); + if (record.value.postRef?.uri) { + await inngest.send({ + name: "appview/sync-bsky-likes", + data: { + document_uri: evt.uri.toString(), + bsky_post_uri: record.value.postRef.uri, + }, + }); + } if (record.value.publication) { let publicationURI = new AtUri(record.value.publication); @@ -269,6 +278,15 @@ async function handleEvent(evt: Event) { data: record.value as Json, }); if (docResult.error) console.log(docResult.error); + if (record.value.bskyPostRef?.uri) { + await inngest.send({ + name: "appview/sync-bsky-likes", + data: { + document_uri: evt.uri.toString(), + bsky_post_uri: record.value.bskyPostRef.uri, + }, + }); + } // site.standard.document uses "site" field to reference the publication // For documents in publications, site is an AT-URI (at://did:plc:xxx/site.standard.publication/rkey) diff --git a/drizzle/schema.ts b/drizzle/schema.ts index 65cd9add..05b5b5ad 100644 --- a/drizzle/schema.ts +++ b/drizzle/schema.ts @@ -1,4 +1,4 @@ -import { pgTable, pgEnum, text, jsonb, foreignKey, timestamp, boolean, uuid, index, bigint, unique, uniqueIndex, smallint, primaryKey } from "drizzle-orm/pg-core" +import { pgTable, pgEnum, text, jsonb, foreignKey, timestamp, boolean, uuid, index, bigint, unique, uniqueIndex, smallint, primaryKey, integer } from "drizzle-orm/pg-core" import { sql } from "drizzle-orm" export const aal_level = pgEnum("aal_level", ['aal1', 'aal2', 'aal3']) @@ -225,6 +225,7 @@ export const documents = pgTable("documents", { uri: text("uri").primaryKey().notNull(), data: jsonb("data").notNull(), indexed_at: timestamp("indexed_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), + bsky_like_count: integer("bsky_like_count").default(0).notNull(), }); export const atp_poll_votes = pgTable("atp_poll_votes", { diff --git a/supabase/database.types.ts b/supabase/database.types.ts index 6153540b..1eb727c2 100644 --- a/supabase/database.types.ts +++ b/supabase/database.types.ts @@ -335,17 +335,20 @@ export type Database = { } documents: { Row: { + bsky_like_count: number data: Json indexed_at: string sort_date: string uri: string } Insert: { + bsky_like_count?: number data: Json indexed_at?: string uri: string } Update: { + bsky_like_count?: number data?: Json indexed_at?: string uri?: string diff --git a/supabase/migrations/20260209000000_add_bsky_like_count.sql b/supabase/migrations/20260209000000_add_bsky_like_count.sql new file mode 100644 index 00000000..676476b0 --- /dev/null +++ b/supabase/migrations/20260209000000_add_bsky_like_count.sql @@ -0,0 +1 @@ +ALTER TABLE documents ADD COLUMN bsky_like_count integer NOT NULL DEFAULT 0; -- 2.51.2 From 89a5e06cfb4576b98ad25c74688e7e1d99cdd0fb Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Mon, 9 Feb 2026 15:35:45 +0400 Subject: [PATCH 16/51] don't sync bsky likes of brid.gy posts --- app/api/inngest/functions/sync_bsky_likes.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/app/api/inngest/functions/sync_bsky_likes.ts b/app/api/inngest/functions/sync_bsky_likes.ts index 9b67aab8..69dfe3c6 100644 --- a/app/api/inngest/functions/sync_bsky_likes.ts +++ b/app/api/inngest/functions/sync_bsky_likes.ts @@ -1,6 +1,7 @@ import { inngest } from "../client"; import { supabaseServerClient } from "supabase/serverClient"; -import { AtpAgent } from "@atproto/api"; +import { AtpAgent, AtUri } from "@atproto/api"; +import { idResolver } from "app/(home-pages)/reader/idResolver"; const TOTAL_ITERATIONS = 144; // 36 hours at 15-minute intervals @@ -13,6 +14,19 @@ export const sync_bsky_likes = inngest.createFunction( async ({ event, step }) => { const { document_uri, bsky_post_uri } = event.data; + const isBridgy = await step.run("check-bridgy", async () => { + const did = new AtUri(bsky_post_uri).host; + const doc = await idResolver.did.resolve(did); + const handle = doc?.alsoKnownAs + ?.find((a) => a.startsWith("at://")) + ?.replace("at://", ""); + return handle?.includes("brid.gy") ?? false; + }); + + if (isBridgy) { + return { skipped: true, reason: "brid.gy post" }; + } + const agent = new AtpAgent({ service: "https://public.api.bsky.app" }); const fetchAndUpdate = async () => { -- 2.51.2 From 764c811b667edc30b90e41f16a35ed4f7cc65a77 Mon Sep 17 00:00:00 2001 From: Henrique Dias Date: Mon, 9 Feb 2026 18:47:35 +0100 Subject: [PATCH 17/51] fix: copy post link button --- components/InteractionsPreview.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/InteractionsPreview.tsx b/components/InteractionsPreview.tsx index 9f393f86..930e2b36 100644 --- a/components/InteractionsPreview.tsx +++ b/components/InteractionsPreview.tsx @@ -77,7 +77,7 @@ export const InteractionPreview = (props: { let mouseY = e.clientY; if (!props.postUrl) return; - navigator.clipboard.writeText(`leaflet.pub${props.postUrl}`); + navigator.clipboard.writeText(props.postUrl); smoker({ text: Copied Link!, -- 2.51.2 From 1971c0e399e1e670b34b3c4781ca93756fefc80a Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Mon, 9 Feb 2026 22:34:43 +0400 Subject: [PATCH 18/51] add bsky auth view all permissions --- app/api/oauth/[route]/oauth-metadata.ts | 2 +- app/api/oauth/[route]/route.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/api/oauth/[route]/oauth-metadata.ts b/app/api/oauth/[route]/oauth-metadata.ts index 32ed3b35..e29dcb9b 100644 --- a/app/api/oauth/[route]/oauth-metadata.ts +++ b/app/api/oauth/[route]/oauth-metadata.ts @@ -8,7 +8,7 @@ const hostname = : "https://leaflet.pub"; const scope = - "atproto transition:generic transition:email include:pub.leaflet.authFullPermissions include:site.standard.authFull include:app.bsky.authCreatePosts blob:*/*"; + "atproto transition:generic transition:email include:pub.leaflet.authFullPermissions include:site.standard.authFull include:app.bsky.authCreatePosts include:app.bsky.authViewAll blob:*/*"; const localconfig: OAuthClientMetadataInput = { client_id: `http://localhost/?redirect_uri=${encodeURI(`http://127.0.0.1:3000/api/oauth/callback`)}&scope=${encodeURIComponent(scope)}`, client_name: `Leaflet`, diff --git a/app/api/oauth/[route]/route.ts b/app/api/oauth/[route]/route.ts index 108e032b..38f8ebee 100644 --- a/app/api/oauth/[route]/route.ts +++ b/app/api/oauth/[route]/route.ts @@ -43,7 +43,7 @@ export async function GET( const url = await client.authorize(handle || "https://bsky.social", { scope: - "atproto transition:email include:pub.leaflet.authFullPermissions include:site.standard.authFull include:app.bsky.authCreatePosts blob:*/*", + "atproto transition:email include:pub.leaflet.authFullPermissions include:site.standard.authFull include:app.bsky.authCreatePosts include:app.bsky.authViewAll blob:*/*", signal: ac.signal, state: JSON.stringify(state), }); -- 2.51.2 From 7bb38cc61bc41c0f391e4cd98909fcff608cb059 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Mon, 9 Feb 2026 23:28:35 +0400 Subject: [PATCH 19/51] add aud to bsky view permissions --- app/api/oauth/[route]/oauth-metadata.ts | 2 +- app/api/oauth/[route]/route.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/api/oauth/[route]/oauth-metadata.ts b/app/api/oauth/[route]/oauth-metadata.ts index e29dcb9b..6af54950 100644 --- a/app/api/oauth/[route]/oauth-metadata.ts +++ b/app/api/oauth/[route]/oauth-metadata.ts @@ -8,7 +8,7 @@ const hostname = : "https://leaflet.pub"; const scope = - "atproto transition:generic transition:email include:pub.leaflet.authFullPermissions include:site.standard.authFull include:app.bsky.authCreatePosts include:app.bsky.authViewAll blob:*/*"; + "atproto transition:generic transition:email include:pub.leaflet.authFullPermissions include:site.standard.authFull include:app.bsky.authCreatePosts include:app.bsky.authViewAll?aud=did:web:api.bsky.app%23bsky_appview blob:*/*"; const localconfig: OAuthClientMetadataInput = { client_id: `http://localhost/?redirect_uri=${encodeURI(`http://127.0.0.1:3000/api/oauth/callback`)}&scope=${encodeURIComponent(scope)}`, client_name: `Leaflet`, diff --git a/app/api/oauth/[route]/route.ts b/app/api/oauth/[route]/route.ts index 38f8ebee..9c7eb139 100644 --- a/app/api/oauth/[route]/route.ts +++ b/app/api/oauth/[route]/route.ts @@ -43,7 +43,7 @@ export async function GET( const url = await client.authorize(handle || "https://bsky.social", { scope: - "atproto transition:email include:pub.leaflet.authFullPermissions include:site.standard.authFull include:app.bsky.authCreatePosts include:app.bsky.authViewAll blob:*/*", + "atproto transition:email include:pub.leaflet.authFullPermissions include:site.standard.authFull include:app.bsky.authCreatePosts include:app.bsky.authViewAll?aud=did:web:api.bsky.app%23bsky_appview blob:*/*", signal: ac.signal, state: JSON.stringify(state), }); -- 2.51.2 From c81be9ad3fd9e3c74966ec1d88667b89b6c6e20f Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Tue, 10 Feb 2026 00:16:55 +0400 Subject: [PATCH 20/51] add recommend notification --- .../notifications/NotificationList.tsx | 4 ++ .../notifications/RecommendNotification.tsx | 48 ++++++++++++++ .../[rkey]/Interactions/recommendAction.ts | 21 +++++++ src/notifications.ts | 63 +++++++++++++++++-- 4 files changed, 132 insertions(+), 4 deletions(-) create mode 100644 app/(home-pages)/notifications/RecommendNotification.tsx diff --git a/app/(home-pages)/notifications/NotificationList.tsx b/app/(home-pages)/notifications/NotificationList.tsx index a0ec12ee..30852227 100644 --- a/app/(home-pages)/notifications/NotificationList.tsx +++ b/app/(home-pages)/notifications/NotificationList.tsx @@ -11,6 +11,7 @@ import { QuoteNotification } from "./QuoteNotification"; import { BskyPostEmbedNotification } from "./BskyPostEmbedNotification"; import { MentionNotification } from "./MentionNotification"; import { CommentMentionNotification } from "./CommentMentionNotification"; +import { RecommendNotification } from "./RecommendNotification"; export function NotificationList({ notifications, @@ -58,6 +59,9 @@ export function NotificationList({ if (n.type === "comment_mention") { return ; } + if (n.type === "recommend") { + return ; + } })}
diff --git a/app/(home-pages)/notifications/RecommendNotification.tsx b/app/(home-pages)/notifications/RecommendNotification.tsx new file mode 100644 index 00000000..3abb49d3 --- /dev/null +++ b/app/(home-pages)/notifications/RecommendNotification.tsx @@ -0,0 +1,48 @@ +import { ContentLayout, Notification } from "./Notification"; +import { HydratedRecommendNotification } from "src/notifications"; +import { blobRefToSrc } from "src/utils/blobRefToSrc"; +import { AppBskyActorProfile } from "lexicons/api"; +import { Avatar } from "components/Avatar"; +import { AtUri } from "@atproto/api"; +import { RecommendTinyFilled } from "components/Icons/RecommendTiny"; + +export const RecommendNotification = ( + props: HydratedRecommendNotification, +) => { + const profileRecord = props.recommendData?.identities?.bsky_profiles + ?.record as AppBskyActorProfile.Record; + const displayName = + profileRecord?.displayName || + props.recommendData?.identities?.bsky_profiles?.handle || + "Someone"; + const docRecord = props.normalizedDocument; + const pubRecord = props.normalizedPublication; + const avatarSrc = + profileRecord?.avatar?.ref && + blobRefToSrc( + profileRecord.avatar.ref, + props.recommendData?.recommender_did || "", + ); + + if (!docRecord) return null; + + const docUri = new AtUri(props.document.uri); + const rkey = docUri.rkey; + const did = docUri.host; + + const href = pubRecord ? `${pubRecord.url}/${rkey}` : `/p/${did}/${rkey}`; + + return ( + } + actionText={<>{displayName} recommended your post} + content={ + + {null} + + } + /> + ); +}; diff --git a/app/lish/[did]/[publication]/[rkey]/Interactions/recommendAction.ts b/app/lish/[did]/[publication]/[rkey]/Interactions/recommendAction.ts index d0295f26..c219720c 100644 --- a/app/lish/[did]/[publication]/[rkey]/Interactions/recommendAction.ts +++ b/app/lish/[did]/[publication]/[rkey]/Interactions/recommendAction.ts @@ -7,6 +7,11 @@ import { TID } from "@atproto/common"; import { AtUri, Un$Typed } from "@atproto/api"; import { supabaseServerClient } from "supabase/serverClient"; import { Json } from "supabase/database.types"; +import { v7 } from "uuid"; +import { + Notification, + pingIdentityToUpdateNotification, +} from "src/notifications"; type RecommendResult = | { success: true; uri: string } @@ -68,6 +73,22 @@ export async function recommendAction(args: { }); console.log(res); + // Notify the document owner + let documentOwner = new AtUri(args.document).host; + if (documentOwner !== credentialSession.did) { + let notification: Notification = { + id: v7(), + recipient: documentOwner, + data: { + type: "recommend", + document_uri: args.document, + recommend_uri: uri.toString(), + }, + }; + await supabaseServerClient.from("notifications").insert(notification); + await pingIdentityToUpdateNotification(documentOwner); + } + return { success: true, uri: uri.toString(), diff --git a/src/notifications.ts b/src/notifications.ts index f4353d2b..be5ae4bb 100644 --- a/src/notifications.ts +++ b/src/notifications.ts @@ -27,7 +27,8 @@ export type NotificationData = | { type: "mention"; document_uri: string; mention_type: "document"; mentioned_uri: string } | { type: "comment_mention"; comment_uri: string; mention_type: "did" } | { type: "comment_mention"; comment_uri: string; mention_type: "publication"; mentioned_uri: string } - | { type: "comment_mention"; comment_uri: string; mention_type: "document"; mentioned_uri: string }; + | { type: "comment_mention"; comment_uri: string; mention_type: "document"; mentioned_uri: string } + | { type: "recommend"; document_uri: string; recommend_uri: string }; export type HydratedNotification = | HydratedCommentNotification @@ -35,22 +36,24 @@ export type HydratedNotification = | HydratedQuoteNotification | HydratedBskyPostEmbedNotification | HydratedMentionNotification - | HydratedCommentMentionNotification; + | HydratedCommentMentionNotification + | HydratedRecommendNotification; export async function hydrateNotifications( notifications: NotificationRow[], ): Promise> { // Call all hydrators in parallel - const [commentNotifications, subscribeNotifications, quoteNotifications, bskyPostEmbedNotifications, mentionNotifications, commentMentionNotifications] = await Promise.all([ + const [commentNotifications, subscribeNotifications, quoteNotifications, bskyPostEmbedNotifications, mentionNotifications, commentMentionNotifications, recommendNotifications] = await Promise.all([ hydrateCommentNotifications(notifications), hydrateSubscribeNotifications(notifications), hydrateQuoteNotifications(notifications), hydrateBskyPostEmbedNotifications(notifications), hydrateMentionNotifications(notifications), hydrateCommentMentionNotifications(notifications), + hydrateRecommendNotifications(notifications), ]); // Combine all hydrated notifications - const allHydrated = [...commentNotifications, ...subscribeNotifications, ...quoteNotifications, ...bskyPostEmbedNotifications, ...mentionNotifications, ...commentMentionNotifications]; + const allHydrated = [...commentNotifications, ...subscribeNotifications, ...quoteNotifications, ...bskyPostEmbedNotifications, ...mentionNotifications, ...commentMentionNotifications, ...recommendNotifications]; // Sort by created_at to maintain order allHydrated.sort( @@ -519,6 +522,58 @@ async function hydrateCommentMentionNotifications(notifications: NotificationRow .filter((n) => n !== null); } +export type HydratedRecommendNotification = Awaited< + ReturnType +>[0]; + +async function hydrateRecommendNotifications(notifications: NotificationRow[]) { + const recommendNotifications = notifications.filter( + (n): n is NotificationRow & { data: ExtractNotificationType<"recommend"> } => + (n.data as NotificationData)?.type === "recommend", + ); + + if (recommendNotifications.length === 0) { + return []; + } + + // Fetch recommend data from the database + const recommendUris = recommendNotifications.map((n) => n.data.recommend_uri); + const documentUris = recommendNotifications.map((n) => n.data.document_uri); + + const [{ data: recommends }, { data: documents }] = await Promise.all([ + supabaseServerClient + .from("recommends_on_documents") + .select("*, identities(bsky_profiles(*))") + .in("uri", recommendUris), + supabaseServerClient + .from("documents") + .select("*, documents_in_publications(publications(*))") + .in("uri", documentUris), + ]); + + return recommendNotifications + .map((notification) => { + const recommendData = recommends?.find((r) => r.uri === notification.data.recommend_uri); + const document = documents?.find((d) => d.uri === notification.data.document_uri); + if (!recommendData || !document) return null; + return { + id: notification.id, + recipient: notification.recipient, + created_at: notification.created_at, + type: "recommend" as const, + recommend_uri: notification.data.recommend_uri, + document_uri: notification.data.document_uri, + recommendData, + document, + normalizedDocument: normalizeDocumentRecord(document.data, document.uri), + normalizedPublication: normalizePublicationRecord( + document.documents_in_publications[0]?.publications?.record, + ), + }; + }) + .filter((n) => n !== null); +} + export async function pingIdentityToUpdateNotification(did: string) { let channel = supabaseServerClient.channel(`identity.atp_did:${did}`); await channel.send({ -- 2.51.2 From 856e0c8fa0829bf8b382775d1483f96233cc758e Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Tue, 10 Feb 2026 13:01:14 +0400 Subject: [PATCH 21/51] fix post links to standard site blogs --- .../BskyPostEmbedNotification.tsx | 7 +-- .../CommentMentionNotification.tsx | 10 ++--- .../notifications/CommentNotication.tsx | 10 ++--- .../notifications/MentionNotification.tsx | 9 +--- .../notifications/QuoteNotification.tsx | 8 +--- .../notifications/RecommendNotification.tsx | 7 +-- .../notifications/ReplyNotification.tsx | 10 ++--- .../actions/ShareOptions/index.tsx | 16 ++++--- .../[command]/search_publication_documents.ts | 12 ++--- .../[publication]/[rkey]/getPostPageData.ts | 12 ++--- .../dashboard/PublishedPostsLists.tsx | 7 +-- app/lish/[did]/[publication]/generateFeed.ts | 6 ++- app/lish/[did]/[publication]/page.tsx | 7 +-- app/lish/createPub/getPublicationURL.ts | 45 +++++++++++++++++++ components/PageSWRDataProvider.tsx | 28 +++++++++--- components/PostListing.tsx | 5 +-- 16 files changed, 123 insertions(+), 76 deletions(-) diff --git a/app/(home-pages)/notifications/BskyPostEmbedNotification.tsx b/app/(home-pages)/notifications/BskyPostEmbedNotification.tsx index 2b433771..66cf0018 100644 --- a/app/(home-pages)/notifications/BskyPostEmbedNotification.tsx +++ b/app/(home-pages)/notifications/BskyPostEmbedNotification.tsx @@ -2,6 +2,7 @@ import { BlueskyTiny } from "components/Icons/BlueskyTiny"; import { ContentLayout, Notification } from "./Notification"; import { HydratedBskyPostEmbedNotification } from "src/notifications"; import { AtUri } from "@atproto/api"; +import { getDocumentURL } from "app/lish/createPub/getPublicationURL"; export const BskyPostEmbedNotification = ( props: HydratedBskyPostEmbedNotification, @@ -11,11 +12,7 @@ export const BskyPostEmbedNotification = ( if (!docRecord) return null; - const docUri = new AtUri(props.document.uri); - const rkey = docUri.rkey; - const did = docUri.host; - - const href = pubRecord ? `${pubRecord.url}/${rkey}` : `/p/${did}/${rkey}`; + const href = getDocumentURL(docRecord, props.document.uri, pubRecord); const embedder = props.documentCreatorHandle ? `@${props.documentCreatorHandle}` diff --git a/app/(home-pages)/notifications/CommentMentionNotification.tsx b/app/(home-pages)/notifications/CommentMentionNotification.tsx index 10917d61..3bf1e63d 100644 --- a/app/(home-pages)/notifications/CommentMentionNotification.tsx +++ b/app/(home-pages)/notifications/CommentMentionNotification.tsx @@ -8,6 +8,7 @@ import { Notification, } from "./Notification"; import { AtUri } from "@atproto/api"; +import { getDocumentURL } from "app/lish/createPub/getPublicationURL"; export const CommentMentionNotification = ( props: HydratedCommentMentionNotification, @@ -19,13 +20,10 @@ export const CommentMentionNotification = ( const profileRecord = props.commentData.bsky_profiles ?.record as AppBskyActorProfile.Record; const pubRecord = props.normalizedPublication; - const docUri = new AtUri(props.commentData.documents?.uri!); - const rkey = docUri.rkey; - const did = docUri.host; - const href = pubRecord - ? `${pubRecord.url}/${rkey}?interactionDrawer=comments` - : `/p/${did}/${rkey}?interactionDrawer=comments`; + const href = + getDocumentURL(docRecord, props.commentData.documents?.uri!, pubRecord) + + "?interactionDrawer=comments"; const commenter = props.commenterHandle ? `@${props.commenterHandle}` diff --git a/app/(home-pages)/notifications/CommentNotication.tsx b/app/(home-pages)/notifications/CommentNotication.tsx index 500d2783..fde0e160 100644 --- a/app/(home-pages)/notifications/CommentNotication.tsx +++ b/app/(home-pages)/notifications/CommentNotication.tsx @@ -10,6 +10,7 @@ import { Notification, } from "./Notification"; import { AtUri } from "@atproto/api"; +import { getDocumentURL } from "app/lish/createPub/getPublicationURL"; export const CommentNotification = (props: HydratedCommentNotification) => { const docRecord = props.normalizedDocument; @@ -24,13 +25,10 @@ export const CommentNotification = (props: HydratedCommentNotification) => { props.commentData.bsky_profiles?.handle || "Someone"; const pubRecord = props.normalizedPublication; - const docUri = new AtUri(props.commentData.documents?.uri!); - const rkey = docUri.rkey; - const did = docUri.host; - const href = pubRecord - ? `${pubRecord.url}/${rkey}?interactionDrawer=comments` - : `/p/${did}/${rkey}?interactionDrawer=comments`; + const href = + getDocumentURL(docRecord, props.commentData.documents?.uri!, pubRecord) + + "?interactionDrawer=comments"; return ( { const docRecord = props.normalizedDocument; @@ -9,13 +10,7 @@ export const MentionNotification = (props: HydratedMentionNotification) => { if (!docRecord) return null; - const docUri = new AtUri(props.document.uri); - const rkey = docUri.rkey; - const did = docUri.host; - - const href = pubRecord - ? `${pubRecord.url}/${rkey}` - : `/p/${did}/${rkey}`; + const href = getDocumentURL(docRecord, props.document.uri, pubRecord); let actionText: React.ReactNode; let mentionedItemName: string | undefined; diff --git a/app/(home-pages)/notifications/QuoteNotification.tsx b/app/(home-pages)/notifications/QuoteNotification.tsx index d6849205..6c7b3f16 100644 --- a/app/(home-pages)/notifications/QuoteNotification.tsx +++ b/app/(home-pages)/notifications/QuoteNotification.tsx @@ -3,6 +3,7 @@ import { ContentLayout, Notification } from "./Notification"; import { HydratedQuoteNotification } from "src/notifications"; import { AtUri } from "@atproto/api"; import { Avatar } from "components/Avatar"; +import { getDocumentURL } from "app/lish/createPub/getPublicationURL"; export const QuoteNotification = (props: HydratedQuoteNotification) => { const postView = props.bskyPost.post_view as any; @@ -13,14 +14,9 @@ export const QuoteNotification = (props: HydratedQuoteNotification) => { if (!docRecord) return null; - const docUri = new AtUri(props.document.uri); - const rkey = docUri.rkey; - const did = docUri.host; const postText = postView.record?.text || ""; - const href = pubRecord - ? `${pubRecord.url}/${rkey}` - : `/p/${did}/${rkey}`; + const href = getDocumentURL(docRecord, props.document.uri, pubRecord); return ( { const docRecord = props.normalizedDocument; @@ -32,14 +33,11 @@ export const ReplyNotification = (props: HydratedCommentNotification) => { props.parentData?.bsky_profiles?.handle || "Someone"; - const docUri = new AtUri(props.commentData.documents?.uri!); - const rkey = docUri.rkey; - const did = docUri.host; const pubRecord = props.normalizedPublication; - const href = pubRecord - ? `${pubRecord.url}/${rkey}?interactionDrawer=comments` - : `/p/${did}/${rkey}?interactionDrawer=comments`; + const href = + getDocumentURL(docRecord, props.commentData.documents?.uri!, pubRecord) + + "?interactionDrawer=comments"; return ( (null); useEffect(() => { diff --git a/app/api/rpc/[command]/search_publication_documents.ts b/app/api/rpc/[command]/search_publication_documents.ts index a58f6867..a52be5f6 100644 --- a/app/api/rpc/[command]/search_publication_documents.ts +++ b/app/api/rpc/[command]/search_publication_documents.ts @@ -2,7 +2,8 @@ import { AtUri } from "@atproto/api"; import { z } from "zod"; import { makeRoute } from "../lib"; import type { Env } from "./route"; -import { getPublicationURL } from "app/lish/createPub/getPublicationURL"; +import { getDocumentURL } from "app/lish/createPub/getPublicationURL"; +import { normalizeDocumentRecord } from "src/utils/normalizeRecords"; export type SearchPublicationDocumentsReturnType = Awaited< ReturnType<(typeof search_publication_documents)["handler"]> @@ -37,13 +38,14 @@ export const search_publication_documents = makeRoute({ } const result = documents.map((d) => { - const docUri = new AtUri(d.documents.uri); - const pubUrl = getPublicationURL(d.publications); + const normalizedDoc = normalizeDocumentRecord(d.documents.data, d.documents.uri); return { uri: d.documents.uri, - title: (d.documents.data as { title?: string })?.title || "Untitled", - url: `${pubUrl}/${docUri.rkey}`, + title: normalizedDoc?.title || (d.documents.data as { title?: string })?.title || "Untitled", + url: normalizedDoc + ? getDocumentURL(normalizedDoc, d.documents.uri, d.publications) + : `${d.documents.uri}`, }; }); diff --git a/app/lish/[did]/[publication]/[rkey]/getPostPageData.ts b/app/lish/[did]/[publication]/[rkey]/getPostPageData.ts index 7f16418a..49eb500b 100644 --- a/app/lish/[did]/[publication]/[rkey]/getPostPageData.ts +++ b/app/lish/[did]/[publication]/[rkey]/getPostPageData.ts @@ -8,6 +8,7 @@ import { } from "src/utils/normalizeRecords"; import { PubLeafletPublication, SiteStandardPublication } from "lexicons/api"; import { documentUriFilter } from "src/utils/uriHelpers"; +import { getDocumentURL } from "app/lish/createPub/getPublicationURL"; export async function getPostPageData(did: string, rkey: string) { let { data: documents } = await supabaseServerClient @@ -46,11 +47,12 @@ export async function getPostPageData(did: string, rkey: string) { ); // Fetch constellation backlinks for mentions - let aturi = new AtUri(document.uri); - const postUrl = normalizedPublication - ? `${normalizedPublication.url}/${aturi.rkey}` - : `https://leaflet.pub/p/${aturi.host}/${aturi.rkey}`; - const constellationBacklinks = await getConstellationBacklinks(postUrl); + const postUrl = getDocumentURL(normalizedDocument, document.uri, normalizedPublication); + // Constellation needs an absolute URL + const absolutePostUrl = postUrl.startsWith("/") + ? `https://leaflet.pub${postUrl}` + : postUrl; + const constellationBacklinks = await getConstellationBacklinks(absolutePostUrl); // Deduplicate constellation backlinks (same post could appear in both links and embeds) const uniqueBacklinks = Array.from( diff --git a/app/lish/[did]/[publication]/dashboard/PublishedPostsLists.tsx b/app/lish/[did]/[publication]/dashboard/PublishedPostsLists.tsx index 29b4b8a8..a3e25db9 100644 --- a/app/lish/[did]/[publication]/dashboard/PublishedPostsLists.tsx +++ b/app/lish/[did]/[publication]/dashboard/PublishedPostsLists.tsx @@ -9,7 +9,7 @@ import { } from "./PublicationSWRProvider"; import { Fragment } from "react"; import { useParams } from "next/navigation"; -import { getPublicationURL } from "app/lish/createPub/getPublicationURL"; +import { getPublicationURL, getDocumentURL } from "app/lish/createPub/getPublicationURL"; import { SpeedyLink } from "components/SpeedyLink"; import { InteractionPreview } from "components/InteractionsPreview"; import { useLocalizedDate } from "src/hooks/useLocalizedDate"; @@ -71,6 +71,7 @@ function PublishedPostItem(props: { const leaflet = publication.leaflets_in_publications.find( (l) => l.doc === doc.uri, ); + const docUrl = getDocumentURL(doc.record, doc.uri, publication); return ( @@ -87,7 +88,7 @@ function PublishedPostItem(props: {

{doc.record.title} @@ -144,7 +145,7 @@ function PublishedPostItem(props: { showComments={pubRecord?.preferences?.showComments !== false} showMentions={pubRecord?.preferences?.showMentions !== false} showRecommends={pubRecord?.preferences?.showRecommends !== false} - postUrl={`${getPublicationURL(publication)}/${uri.rkey}`} + postUrl={docUrl} />

diff --git a/app/lish/[did]/[publication]/generateFeed.ts b/app/lish/[did]/[publication]/generateFeed.ts index e12aa75f..383f9461 100644 --- a/app/lish/[did]/[publication]/generateFeed.ts +++ b/app/lish/[did]/[publication]/generateFeed.ts @@ -11,6 +11,7 @@ import { hasLeafletContent, } from "src/utils/normalizeRecords"; import { publicationNameOrUriFilter } from "src/utils/uriHelpers"; +import { getDocumentURL } from "app/lish/createPub/getPublicationURL"; export async function generateFeed( did: string, @@ -84,12 +85,13 @@ export async function generateFeed( } } + const docUrl = getDocumentURL(record, doc.documents.uri, pubRecord); feed.addItem({ title: record.title, description: record.description, date: record.publishedAt ? new Date(record.publishedAt) : new Date(), - id: `${pubRecord.url}/${rkey}`, - link: `${pubRecord.url}/${rkey}`, + id: docUrl, + link: docUrl, content: chunks.join(""), }); }), diff --git a/app/lish/[did]/[publication]/page.tsx b/app/lish/[did]/[publication]/page.tsx index 457f38b2..6d6e2e31 100644 --- a/app/lish/[did]/[publication]/page.tsx +++ b/app/lish/[did]/[publication]/page.tsx @@ -1,6 +1,6 @@ import { supabaseServerClient } from "supabase/serverClient"; import { AtUri } from "@atproto/syntax"; -import { getPublicationURL } from "app/lish/createPub/getPublicationURL"; +import { getPublicationURL, getDocumentURL } from "app/lish/createPub/getPublicationURL"; import { BskyAgent } from "@atproto/api"; import { publicationNameOrUriFilter } from "src/utils/uriHelpers"; import { SubscribeWithBluesky } from "app/lish/Subscribe"; @@ -135,11 +135,12 @@ export default async function Publication(props: { doc.documents.recommends_on_documents?.[0]?.count || 0; let tags = doc_record.tags || []; + const docUrl = getDocumentURL(doc_record, doc.documents.uri, publication); return (

{doc_record.title}

@@ -168,7 +169,7 @@ export default async function Publication(props: { recommendsCount={recommends} documentUri={doc.documents.uri} tags={tags} - postUrl={`${getPublicationURL(publication)}/${uri.rkey}`} + postUrl={docUrl} showComments={ record?.preferences?.showComments !== false } diff --git a/app/lish/createPub/getPublicationURL.ts b/app/lish/createPub/getPublicationURL.ts index 613bef1b..3f864599 100644 --- a/app/lish/createPub/getPublicationURL.ts +++ b/app/lish/createPub/getPublicationURL.ts @@ -5,6 +5,8 @@ import { Json } from "supabase/database.types"; import { normalizePublicationRecord, isLeafletPublication, + hasLeafletContent, + type NormalizedDocument, type NormalizedPublication, } from "src/utils/normalizeRecords"; @@ -44,3 +46,46 @@ export function getBasePublicationURL(pub: PublicationInput): string { const name = aturi.rkey || normalized?.name; return `/lish/${aturi.host}/${encodeURIComponent(name || "")}`; } + +/** + * Gets the full URL for a document. + * Always appends the document's path property. + * For non-leaflet documents (content.$type !== "pub.leaflet.content"), + * always uses the full publication site URL, not internal /lish/ URLs. + */ +export function getDocumentURL( + doc: NormalizedDocument, + docUri: string, + publication?: PublicationInput | NormalizedPublication | null, +): string { + const path = doc.path || "/" + new AtUri(docUri).rkey; + const aturi = new AtUri(docUri); + + const isNormalized = + !!publication && + (publication as NormalizedPublication).$type === "site.standard.publication"; + const normPub = isNormalized + ? (publication as NormalizedPublication) + : publication + ? normalizePublicationRecord((publication as PublicationInput).record) + : null; + const pubInput = isNormalized ? null : (publication as PublicationInput | null); + + // Non-leaflet documents always use the full publication site URL + if (doc.content && !hasLeafletContent(doc) && normPub?.url) { + return normPub.url + path; + } + + // For leaflet documents, use getPublicationURL (may return /lish/ internal paths) + if (pubInput) { + return getPublicationURL(pubInput) + path; + } + + // When we only have a normalized publication, use its URL directly + if (normPub?.url) { + return normPub.url + path; + } + + // Standalone document fallback + return `/p/${aturi.host}${path}`; +} diff --git a/components/PageSWRDataProvider.tsx b/components/PageSWRDataProvider.tsx index 227c17e9..189a4e43 100644 --- a/components/PageSWRDataProvider.tsx +++ b/components/PageSWRDataProvider.tsx @@ -8,7 +8,7 @@ import { getPollData } from "actions/pollActions"; import type { GetLeafletDataReturnType } from "app/api/rpc/[command]/get_leaflet_data"; import { createContext, useContext, useMemo } from "react"; import { getPublicationMetadataFromLeafletData } from "src/utils/getPublicationMetadataFromLeafletData"; -import { getPublicationURL } from "app/lish/createPub/getPublicationURL"; +import { getPublicationURL, getDocumentURL } from "app/lish/createPub/getPublicationURL"; import { AtUri } from "@atproto/syntax"; import { normalizeDocumentRecord, @@ -119,13 +119,27 @@ export function useLeafletPublicationStatus() { // Compute the full post URL for sharing let postShareLink: string | undefined; if (publishedInPublication?.publications && publishedInPublication.documents) { - // Published in a publication - use publication URL + document rkey - const docUri = new AtUri(publishedInPublication.documents.uri); - postShareLink = `${getPublicationURL(publishedInPublication.publications)}/${docUri.rkey}`; + const normalizedDoc = normalizeDocumentRecord( + publishedInPublication.documents.data, + publishedInPublication.documents.uri, + ); + if (normalizedDoc) { + postShareLink = getDocumentURL( + normalizedDoc, + publishedInPublication.documents.uri, + publishedInPublication.publications, + ); + } } else if (publishedStandalone?.document) { - // Standalone published post - use /p/{did}/{rkey} format - const docUri = new AtUri(publishedStandalone.document); - postShareLink = `/p/${docUri.host}/${docUri.rkey}`; + const normalizedDoc = publishedStandalone.documents + ? normalizeDocumentRecord(publishedStandalone.documents.data, publishedStandalone.document) + : null; + if (normalizedDoc) { + postShareLink = getDocumentURL(normalizedDoc, publishedStandalone.document); + } else { + const docUri = new AtUri(publishedStandalone.document); + postShareLink = `/p/${docUri.host}/${docUri.rkey}`; + } } return { diff --git a/components/PostListing.tsx b/components/PostListing.tsx index 32bd9bd4..cee5cd5a 100644 --- a/components/PostListing.tsx +++ b/components/PostListing.tsx @@ -18,6 +18,7 @@ import Link from "next/link"; import { InteractionPreview } from "./InteractionsPreview"; import { useLocalizedDate } from "src/hooks/useLocalizedDate"; import { mergePreferences } from "src/utils/mergePreferences"; +import { getDocumentURL } from "app/lish/createPub/getPublicationURL"; export const PostListing = (props: Post) => { let pubRecord = props.publication?.pubRecord as @@ -60,9 +61,7 @@ export const PostListing = (props: Post) => { let tags = (postRecord?.tags as string[] | undefined) || []; // For standalone posts, link directly to the document - let postHref = props.publication - ? `${props.publication.href}/${postUri.rkey}` - : `/p/${postUri.host}/${postUri.rkey}`; + let postHref = getDocumentURL(postRecord, props.documents.uri, pubRecord); return ( -- 2.51.2 From e023d527a637797a9daf04cc1e2df77e2f8a9bf1 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Tue, 10 Feb 2026 13:23:51 +0400 Subject: [PATCH 22/51] don't getpreferences on subscribe --- app/lish/subscribeToPublication.ts | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/app/lish/subscribeToPublication.ts b/app/lish/subscribeToPublication.ts index e321361d..ed712efe 100644 --- a/app/lish/subscribeToPublication.ts +++ b/app/lish/subscribeToPublication.ts @@ -3,10 +3,7 @@ import { AtpBaseClient } from "lexicons/api"; import { AppBskyActorDefs, Agent as BskyAgent } from "@atproto/api"; import { getIdentityData } from "actions/getIdentityData"; -import { - restoreOAuthSession, - OAuthSessionError, -} from "src/atproto-oauth"; +import { restoreOAuthSession, OAuthSessionError } from "src/atproto-oauth"; import { TID } from "@atproto/common"; import { supabaseServerClient } from "supabase/serverClient"; import { revalidatePath } from "next/cache"; @@ -79,8 +76,7 @@ export async function subscribeToPublication( } let bsky = new BskyAgent(credentialSession); - let [prefs, profile, resolveDid] = await Promise.all([ - bsky.app.bsky.actor.getPreferences(), + let [profile, resolveDid] = await Promise.all([ bsky.app.bsky.actor.profile .get({ repo: credentialSession.did!, @@ -96,13 +92,10 @@ export async function subscribeToPublication( handle: resolveDid?.alsoKnownAs?.[0]?.slice(5), }); } - let savedFeeds = prefs.data.preferences.find( - (pref) => pref.$type === "app.bsky.actor.defs#savedFeedsPrefV2", - ) as AppBskyActorDefs.SavedFeedsPrefV2; revalidatePath("/lish/[did]/[publication]", "layout"); return { success: true, - hasFeed: !!savedFeeds.items.find((feed) => feed.value === leafletFeedURI), + hasFeed: true, }; } @@ -111,7 +104,7 @@ type UnsubscribeResult = | { success: false; error: OAuthSessionError }; export async function unsubscribeToPublication( - publication: string + publication: string, ): Promise { let identity = await getIdentityData(); if (!identity || !identity.atp_did) { @@ -144,8 +137,12 @@ export async function unsubscribeToPublication( // Delete from both collections (old and new schema) - one or both may exist let rkey = new AtUri(existingSubscription.uri).rkey; await Promise.all([ - agent.pub.leaflet.graph.subscription.delete({ repo: credentialSession.did!, rkey }).catch(() => {}), - agent.site.standard.graph.subscription.delete({ repo: credentialSession.did!, rkey }).catch(() => {}), + agent.pub.leaflet.graph.subscription + .delete({ repo: credentialSession.did!, rkey }) + .catch(() => {}), + agent.site.standard.graph.subscription + .delete({ repo: credentialSession.did!, rkey }) + .catch(() => {}), ]); await supabaseServerClient -- 2.51.2 From 3ccb429f1b08799df585438be427195cf975325e Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Tue, 10 Feb 2026 17:04:12 +0400 Subject: [PATCH 23/51] add recommend column to documents --- .../20260210000000_add_recommend_count.sql | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 supabase/migrations/20260210000000_add_recommend_count.sql diff --git a/supabase/migrations/20260210000000_add_recommend_count.sql b/supabase/migrations/20260210000000_add_recommend_count.sql new file mode 100644 index 00000000..f0dd1160 --- /dev/null +++ b/supabase/migrations/20260210000000_add_recommend_count.sql @@ -0,0 +1,23 @@ +ALTER TABLE documents ADD COLUMN recommend_count integer NOT NULL DEFAULT 0; + +UPDATE documents d +SET recommend_count = ( + SELECT COUNT(*) FROM recommends_on_documents r WHERE r.document = d.uri +); + +CREATE OR REPLACE FUNCTION update_recommend_count() RETURNS trigger AS $$ +BEGIN + IF TG_OP = 'INSERT' THEN + UPDATE documents SET recommend_count = recommend_count + 1 + WHERE uri = NEW.document; + ELSIF TG_OP = 'DELETE' THEN + UPDATE documents SET recommend_count = recommend_count - 1 + WHERE uri = OLD.document; + END IF; + RETURN NULL; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER trg_recommend_count +AFTER INSERT OR DELETE ON recommends_on_documents +FOR EACH ROW EXECUTE FUNCTION update_recommend_count(); -- 2.51.2 From 1021011dc090d8a4b74cad59342326afa77445f1 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Tue, 10 Feb 2026 21:44:21 +0400 Subject: [PATCH 24/51] check permissions on retractions --- src/replicache/cachedServerMutationContext.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/replicache/cachedServerMutationContext.ts b/src/replicache/cachedServerMutationContext.ts index 322f773e..cad2eb64 100644 --- a/src/replicache/cachedServerMutationContext.ts +++ b/src/replicache/cachedServerMutationContext.ts @@ -155,6 +155,20 @@ export function cachedServerMutationContext( }); }, async retractFact(factID) { + let cachedFact = writeCache.find( + (f) => f.type === "put" && f.fact.id === factID, + ); + let entity: string | undefined; + if (cachedFact && cachedFact.type === "put") { + entity = cachedFact.fact.entity; + } else { + let [row] = await tx + .select({ entity: facts.entity }) + .from(facts) + .where(driz.eq(facts.id, factID)); + entity = row?.entity; + } + if (!entity || !(await this.checkPermission(entity))) return; writeCache = writeCache.filter((f) => f.fact.id !== factID); writeCache.push({ type: "del", fact: { id: factID } }); }, -- 2.51.2 From 27b896242aa84b1d3de24c74cafc1ca77691b6dc Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Tue, 10 Feb 2026 21:50:45 +0400 Subject: [PATCH 25/51] don't show block options if no write permissions --- components/Blocks/Block.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/components/Blocks/Block.tsx b/components/Blocks/Block.tsx index dbf1e61c..33ff933a 100644 --- a/components/Blocks/Block.tsx +++ b/components/Blocks/Block.tsx @@ -419,6 +419,7 @@ const NonTextBlockOptions = (props: { if (focusedEntity?.entityType === "page") return; if (isMultiselected) return; + if (!entity_set.permissions.write) return null; return (
Date: Tue, 10 Feb 2026 22:14:23 +0400 Subject: [PATCH 26/51] handle pasting bluesky posts --- components/Blocks/TextBlock/useHandlePaste.ts | 21 ++++++++++++++++++- src/utils/getBlocksAsHTML.tsx | 11 +++++++++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/components/Blocks/TextBlock/useHandlePaste.ts b/components/Blocks/TextBlock/useHandlePaste.ts index 1e50bc5d..3e165a67 100644 --- a/components/Blocks/TextBlock/useHandlePaste.ts +++ b/components/Blocks/TextBlock/useHandlePaste.ts @@ -396,6 +396,24 @@ const createBlockFromHTML = ( ]); } + if (child.tagName === "DIV" && child.getAttribute("data-bluesky-post")) { + let postData = child.getAttribute("data-bluesky-post"); + if (postData) { + rep.mutate.assertFact([ + { + entity: entityID, + attribute: "block/type", + data: { type: "block-type-union", value: "bluesky-post" }, + }, + { + entity: entityID, + attribute: "block/bluesky-post", + data: { type: "bluesky-post", value: JSON.parse(postData) }, + }, + ]); + } + } + if (child.tagName === "DIV" && child.getAttribute("data-entityid")) { let oldEntityID = child.getAttribute("data-entityid") as string; let factsData = child.getAttribute("data-facts"); @@ -593,7 +611,8 @@ function flattenHTMLToTextBlocks(element: HTMLElement): HTMLElement[] { "HR", ].includes(elementNode.tagName) || elementNode.getAttribute("data-entityid") || - elementNode.getAttribute("data-tex") + elementNode.getAttribute("data-tex") || + elementNode.getAttribute("data-bluesky-post") ) { htmlBlocks.push(elementNode); } else { diff --git a/src/utils/getBlocksAsHTML.tsx b/src/utils/getBlocksAsHTML.tsx index 98bf380a..d9ac5b73 100644 --- a/src/utils/getBlocksAsHTML.tsx +++ b/src/utils/getBlocksAsHTML.tsx @@ -79,7 +79,16 @@ const BlockTypeToHTML: { mailbox: async () => null, poll: async () => null, embed: async () => null, - "bluesky-post": async () => null, + "bluesky-post": async (b, tx) => { + let [post] = await scanIndex(tx).eav(b.value, "block/bluesky-post"); + if (!post) return null; + return ( +
+ ); + }, math: async (b, tx, a) => { let [math] = await scanIndex(tx).eav(b.value, "block/math"); const html = Katex.renderToString(math?.data.value || "", { -- 2.51.2 From f94d0da04728872b981230b270a447ba7d1b1fd3 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Wed, 11 Feb 2026 10:51:04 +0400 Subject: [PATCH 27/51] add indexed col to documents and update inngest --- app/api/inngest/client.ts | 4 +-- ...sky_likes.ts => sync_document_metadata.ts} | 30 +++++++++++------ app/api/inngest/route.tsx | 4 +-- appview/index.ts | 32 ++++++++----------- supabase/database.types.ts | 8 +++++ ...0_add_indexed_column_and_ranking_index.sql | 6 ++++ 6 files changed, 52 insertions(+), 32 deletions(-) rename app/api/inngest/functions/{sync_bsky_likes.ts => sync_document_metadata.ts} (62%) create mode 100644 supabase/migrations/20260210100000_add_indexed_column_and_ranking_index.sql diff --git a/app/api/inngest/client.ts b/app/api/inngest/client.ts index f648b39f..ffce505c 100644 --- a/app/api/inngest/client.ts +++ b/app/api/inngest/client.ts @@ -51,10 +51,10 @@ export type Events = { documentUris?: string[]; }; }; - "appview/sync-bsky-likes": { + "appview/sync-document-metadata": { data: { document_uri: string; - bsky_post_uri: string; + bsky_post_uri?: string; }; }; "user/write-records-to-pds": { diff --git a/app/api/inngest/functions/sync_bsky_likes.ts b/app/api/inngest/functions/sync_document_metadata.ts similarity index 62% rename from app/api/inngest/functions/sync_bsky_likes.ts rename to app/api/inngest/functions/sync_document_metadata.ts index 69dfe3c6..d2836a2a 100644 --- a/app/api/inngest/functions/sync_bsky_likes.ts +++ b/app/api/inngest/functions/sync_document_metadata.ts @@ -5,26 +5,36 @@ import { idResolver } from "app/(home-pages)/reader/idResolver"; const TOTAL_ITERATIONS = 144; // 36 hours at 15-minute intervals -export const sync_bsky_likes = inngest.createFunction( +export const sync_document_metadata = inngest.createFunction( { - id: "sync_bsky_likes", - idempotency: "event.data.bsky_post_uri", + id: "sync_document_metadata", + idempotency: "event.data.document_uri", }, - { event: "appview/sync-bsky-likes" }, + { event: "appview/sync-document-metadata" }, async ({ event, step }) => { const { document_uri, bsky_post_uri } = event.data; - const isBridgy = await step.run("check-bridgy", async () => { - const did = new AtUri(bsky_post_uri).host; + const did = new AtUri(document_uri).host; + + const handleResult = await step.run("resolve-handle", async () => { const doc = await idResolver.did.resolve(did); const handle = doc?.alsoKnownAs ?.find((a) => a.startsWith("at://")) ?.replace("at://", ""); - return handle?.includes("brid.gy") ?? false; + return { handle: handle ?? null, isBridgy: handle?.includes("brid.gy") ?? false }; }); - if (isBridgy) { - return { skipped: true, reason: "brid.gy post" }; + if (handleResult.isBridgy) { + await step.run("set-unindexed", async () => { + await supabaseServerClient + .from("documents") + .update({ indexed: false }) + .eq("uri", document_uri); + }); + } + + if (!bsky_post_uri || handleResult.isBridgy) { + return { handle: handleResult.handle }; } const agent = new AtpAgent({ service: "https://public.api.bsky.app" }); @@ -50,6 +60,6 @@ export const sync_bsky_likes = inngest.createFunction( likeCount = await step.run(`sync-${i}`, fetchAndUpdate); } - return { likeCount }; + return { likeCount, handle: handleResult.handle }; }, ); diff --git a/app/api/inngest/route.tsx b/app/api/inngest/route.tsx index ea0f2f9e..a74f1d98 100644 --- a/app/api/inngest/route.tsx +++ b/app/api/inngest/route.tsx @@ -13,7 +13,7 @@ import { check_oauth_session, } from "./functions/cleanup_expired_oauth_sessions"; import { write_records_to_pds } from "./functions/write_records_to_pds"; -import { sync_bsky_likes } from "./functions/sync_bsky_likes"; +import { sync_document_metadata } from "./functions/sync_document_metadata"; export const { GET, POST, PUT } = serve({ client: inngest, @@ -29,6 +29,6 @@ export const { GET, POST, PUT } = serve({ cleanup_expired_oauth_sessions, check_oauth_session, write_records_to_pds, - sync_bsky_likes, + sync_document_metadata, ], }); diff --git a/appview/index.ts b/appview/index.ts index 7319f549..31ead5fb 100644 --- a/appview/index.ts +++ b/appview/index.ts @@ -109,15 +109,13 @@ async function handleEvent(evt: Event) { data: record.value as Json, }); if (docResult.error) console.log(docResult.error); - if (record.value.postRef?.uri) { - await inngest.send({ - name: "appview/sync-bsky-likes", - data: { - document_uri: evt.uri.toString(), - bsky_post_uri: record.value.postRef.uri, - }, - }); - } + await inngest.send({ + name: "appview/sync-document-metadata", + data: { + document_uri: evt.uri.toString(), + bsky_post_uri: record.value.postRef?.uri, + }, + }); if (record.value.publication) { let publicationURI = new AtUri(record.value.publication); @@ -278,15 +276,13 @@ async function handleEvent(evt: Event) { data: record.value as Json, }); if (docResult.error) console.log(docResult.error); - if (record.value.bskyPostRef?.uri) { - await inngest.send({ - name: "appview/sync-bsky-likes", - data: { - document_uri: evt.uri.toString(), - bsky_post_uri: record.value.bskyPostRef.uri, - }, - }); - } + await inngest.send({ + name: "appview/sync-document-metadata", + data: { + document_uri: evt.uri.toString(), + bsky_post_uri: record.value.bskyPostRef?.uri, + }, + }); // site.standard.document uses "site" field to reference the publication // For documents in publications, site is an AT-URI (at://did:plc:xxx/site.standard.publication/rkey) diff --git a/supabase/database.types.ts b/supabase/database.types.ts index 1eb727c2..c4f53bde 100644 --- a/supabase/database.types.ts +++ b/supabase/database.types.ts @@ -337,20 +337,28 @@ export type Database = { Row: { bsky_like_count: number data: Json + indexed: boolean indexed_at: string + recommend_count: number sort_date: string uri: string } Insert: { bsky_like_count?: number data: Json + indexed?: boolean indexed_at?: string + recommend_count?: number + sort_date?: string uri: string } Update: { bsky_like_count?: number data?: Json + indexed?: boolean indexed_at?: string + recommend_count?: number + sort_date?: string uri?: string } Relationships: [] diff --git a/supabase/migrations/20260210100000_add_indexed_column_and_ranking_index.sql b/supabase/migrations/20260210100000_add_indexed_column_and_ranking_index.sql new file mode 100644 index 00000000..562f3b18 --- /dev/null +++ b/supabase/migrations/20260210100000_add_indexed_column_and_ranking_index.sql @@ -0,0 +1,6 @@ +ALTER TABLE documents ADD COLUMN indexed boolean NOT NULL DEFAULT true; + +CREATE INDEX idx_documents_ranking +ON documents (sort_date DESC) +INCLUDE (uri, bsky_like_count, recommend_count) +WHERE indexed = true; -- 2.51.2 From 1a7a36b5ac5ea04aca0c4a58f47a3fdbd7e23ff9 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Wed, 11 Feb 2026 11:06:46 +0400 Subject: [PATCH 28/51] fix type error --- app/lish/[did]/[publication]/dashboard/PublishedPostsLists.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/lish/[did]/[publication]/dashboard/PublishedPostsLists.tsx b/app/lish/[did]/[publication]/dashboard/PublishedPostsLists.tsx index a3e25db9..1de03e91 100644 --- a/app/lish/[did]/[publication]/dashboard/PublishedPostsLists.tsx +++ b/app/lish/[did]/[publication]/dashboard/PublishedPostsLists.tsx @@ -114,6 +114,8 @@ function PublishedPostItem(props: { sort_date: doc.sort_date, data: doc.data, bsky_like_count: doc.bsky_like_count ?? 0, + indexed: true, + recommend_count: doc.recommendsCount ?? 0, }, }, ], -- 2.51.2 From 1af1ce6f916bd0192b814f11864f1216ed0ac649 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Wed, 11 Feb 2026 13:21:18 +0400 Subject: [PATCH 29/51] filter out posts from feeds --- feeds/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/feeds/index.ts b/feeds/index.ts index 279f5d7c..a30e229f 100644 --- a/feeds/index.ts +++ b/feeds/index.ts @@ -115,6 +115,7 @@ app.get("/xrpc/app.bsky.feed.getFeedSkeleton", async (c) => { ); } query = query + .eq("indexed", true) .or("data->postRef.not.is.null,data->bskyPostRef.not.is.null") .order("sort_date", { ascending: false }) .order("uri", { ascending: false }) -- 2.51.2 From 44da33e46d65a5604a97cb35fc95c436fbd18cf4 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Wed, 11 Feb 2026 14:13:31 +0400 Subject: [PATCH 30/51] explicitly set if documents are indexed after checking pds host --- .../functions/sync_document_metadata.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/app/api/inngest/functions/sync_document_metadata.ts b/app/api/inngest/functions/sync_document_metadata.ts index d2836a2a..35acb38d 100644 --- a/app/api/inngest/functions/sync_document_metadata.ts +++ b/app/api/inngest/functions/sync_document_metadata.ts @@ -21,17 +21,18 @@ export const sync_document_metadata = inngest.createFunction( const handle = doc?.alsoKnownAs ?.find((a) => a.startsWith("at://")) ?.replace("at://", ""); - return { handle: handle ?? null, isBridgy: handle?.includes("brid.gy") ?? false }; + const isBridgy = !!doc?.service?.find( + (s) => s.serviceEndpoint === "https://atproto.brid.gy", + ); + return { handle: handle ?? null, isBridgy }; }); - if (handleResult.isBridgy) { - await step.run("set-unindexed", async () => { - await supabaseServerClient - .from("documents") - .update({ indexed: false }) - .eq("uri", document_uri); - }); - } + await step.run("set-indexed", async () => { + await supabaseServerClient + .from("documents") + .update({ indexed: !handleResult.isBridgy }) + .eq("uri", document_uri); + }); if (!bsky_post_uri || handleResult.isBridgy) { return { handle: handleResult.handle }; -- 2.51.2 From fc660a92f04333949277b5e541815c4c75b312cc Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Wed, 11 Feb 2026 14:23:42 +0400 Subject: [PATCH 31/51] indexed false by default --- appview/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/appview/index.ts b/appview/index.ts index 31ead5fb..6c4c1548 100644 --- a/appview/index.ts +++ b/appview/index.ts @@ -107,6 +107,7 @@ async function handleEvent(evt: Event) { let docResult = await supabase.from("documents").upsert({ uri: evt.uri.toString(), data: record.value as Json, + indexed: false, }); if (docResult.error) console.log(docResult.error); await inngest.send({ @@ -274,6 +275,7 @@ async function handleEvent(evt: Event) { let docResult = await supabase.from("documents").upsert({ uri: evt.uri.toString(), data: record.value as Json, + indexed: false, }); if (docResult.error) console.log(docResult.error); await inngest.send({ -- 2.51.2 From ffb44ee2c5658377096de934ac383cc0a5beb553 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Wed, 11 Feb 2026 16:17:12 +0400 Subject: [PATCH 32/51] return if no handle in sync document metadata --- app/api/inngest/functions/sync_document_metadata.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/api/inngest/functions/sync_document_metadata.ts b/app/api/inngest/functions/sync_document_metadata.ts index 35acb38d..cab26e98 100644 --- a/app/api/inngest/functions/sync_document_metadata.ts +++ b/app/api/inngest/functions/sync_document_metadata.ts @@ -21,11 +21,13 @@ export const sync_document_metadata = inngest.createFunction( const handle = doc?.alsoKnownAs ?.find((a) => a.startsWith("at://")) ?.replace("at://", ""); + if (!doc) return null; const isBridgy = !!doc?.service?.find( (s) => s.serviceEndpoint === "https://atproto.brid.gy", ); return { handle: handle ?? null, isBridgy }; }); + if (!handleResult) return { error: "No Handle" }; await step.run("set-indexed", async () => { await supabaseServerClient -- 2.51.2 From 9c5c5c6b9ff8bc85f808e031cdaf2ed437dde7b5 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Wed, 11 Feb 2026 16:17:42 +0400 Subject: [PATCH 33/51] add doc to inngest return --- app/api/inngest/functions/sync_document_metadata.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/api/inngest/functions/sync_document_metadata.ts b/app/api/inngest/functions/sync_document_metadata.ts index cab26e98..79b95de7 100644 --- a/app/api/inngest/functions/sync_document_metadata.ts +++ b/app/api/inngest/functions/sync_document_metadata.ts @@ -25,7 +25,7 @@ export const sync_document_metadata = inngest.createFunction( const isBridgy = !!doc?.service?.find( (s) => s.serviceEndpoint === "https://atproto.brid.gy", ); - return { handle: handle ?? null, isBridgy }; + return { handle: handle ?? null, isBridgy, doc }; }); if (!handleResult) return { error: "No Handle" }; -- 2.51.2 From 5ba035764042b0475f7785df35a3e040e18f7519 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Wed, 11 Feb 2026 16:23:40 +0400 Subject: [PATCH 34/51] handle trailing brid.gy / --- app/api/inngest/functions/sync_document_metadata.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/api/inngest/functions/sync_document_metadata.ts b/app/api/inngest/functions/sync_document_metadata.ts index 79b95de7..8dc0c775 100644 --- a/app/api/inngest/functions/sync_document_metadata.ts +++ b/app/api/inngest/functions/sync_document_metadata.ts @@ -23,7 +23,9 @@ export const sync_document_metadata = inngest.createFunction( ?.replace("at://", ""); if (!doc) return null; const isBridgy = !!doc?.service?.find( - (s) => s.serviceEndpoint === "https://atproto.brid.gy", + (s) => + typeof s.serviceEndpoint === "string" && + s.serviceEndpoint.includes("atproto.brid.gy"), ); return { handle: handle ?? null, isBridgy, doc }; }); -- 2.51.2 From b48c51c8497284b859304447d1fc761a9745685a Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Wed, 11 Feb 2026 21:28:08 +0400 Subject: [PATCH 35/51] sort rss feeds properly --- app/lish/[did]/[publication]/generateFeed.ts | 82 +++++++++++--------- 1 file changed, 46 insertions(+), 36 deletions(-) diff --git a/app/lish/[did]/[publication]/generateFeed.ts b/app/lish/[did]/[publication]/generateFeed.ts index 383f9461..e5b6de11 100644 --- a/app/lish/[did]/[publication]/generateFeed.ts +++ b/app/lish/[did]/[publication]/generateFeed.ts @@ -54,47 +54,57 @@ export async function generateFeed( }); await Promise.all( - publication.documents_in_publications.map(async (doc) => { - if (!doc.documents) return; - const record = normalizeDocumentRecord( - doc.documents?.data, - doc.documents?.uri, - ); - const uri = new AtUri(doc.documents?.uri); - const rkey = uri.rkey; - if (!record) return; + publication.documents_in_publications + .sort((a, b) => { + const dateA = a.documents?.sort_date + ? new Date(a.documents.sort_date).getTime() + : 0; + const dateB = b.documents?.sort_date + ? new Date(b.documents.sort_date).getTime() + : 0; + return dateB - dateA; // Sort in descending order (newest first) + }) + .map(async (doc) => { + if (!doc.documents) return; + const record = normalizeDocumentRecord( + doc.documents?.data, + doc.documents?.uri, + ); + const uri = new AtUri(doc.documents?.uri); + const rkey = uri.rkey; + if (!record) return; - let blocks: PubLeafletPagesLinearDocument.Block[] = []; - if (hasLeafletContent(record) && record.content.pages[0]) { - const firstPage = record.content.pages[0]; - if (PubLeafletPagesLinearDocument.isMain(firstPage)) { - blocks = firstPage.blocks || []; + let blocks: PubLeafletPagesLinearDocument.Block[] = []; + if (hasLeafletContent(record) && record.content.pages[0]) { + const firstPage = record.content.pages[0]; + if (PubLeafletPagesLinearDocument.isMain(firstPage)) { + blocks = firstPage.blocks || []; + } } - } - const stream = await renderToReadableStream( - createElement(StaticPostContent, { blocks, did: uri.host }), - ); - const reader = stream.getReader(); - const chunks = []; + const stream = await renderToReadableStream( + createElement(StaticPostContent, { blocks, did: uri.host }), + ); + const reader = stream.getReader(); + const chunks = []; - let done, value; - while (!done) { - ({ done, value } = await reader.read()); - if (value) { - chunks.push(new TextDecoder().decode(value)); + let done, value; + while (!done) { + ({ done, value } = await reader.read()); + if (value) { + chunks.push(new TextDecoder().decode(value)); + } } - } - const docUrl = getDocumentURL(record, doc.documents.uri, pubRecord); - feed.addItem({ - title: record.title, - description: record.description, - date: record.publishedAt ? new Date(record.publishedAt) : new Date(), - id: docUrl, - link: docUrl, - content: chunks.join(""), - }); - }), + const docUrl = getDocumentURL(record, doc.documents.uri, pubRecord); + feed.addItem({ + title: record.title, + description: record.description, + date: record.publishedAt ? new Date(record.publishedAt) : new Date(), + id: docUrl, + link: docUrl, + content: chunks.join(""), + }); + }), ); return feed; -- 2.51.2 From 5aaf7df819a607c99f64d18a973f0beaac855a52 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Wed, 11 Feb 2026 21:34:10 +0400 Subject: [PATCH 36/51] fix feed ordering --- app/lish/[did]/[publication]/generateFeed.ts | 97 ++++++++++---------- 1 file changed, 47 insertions(+), 50 deletions(-) diff --git a/app/lish/[did]/[publication]/generateFeed.ts b/app/lish/[did]/[publication]/generateFeed.ts index e5b6de11..94227ea0 100644 --- a/app/lish/[did]/[publication]/generateFeed.ts +++ b/app/lish/[did]/[publication]/generateFeed.ts @@ -53,59 +53,56 @@ export async function generateFeed( }, }); - await Promise.all( - publication.documents_in_publications - .sort((a, b) => { - const dateA = a.documents?.sort_date - ? new Date(a.documents.sort_date).getTime() - : 0; - const dateB = b.documents?.sort_date - ? new Date(b.documents.sort_date).getTime() - : 0; - return dateB - dateA; // Sort in descending order (newest first) - }) - .map(async (doc) => { - if (!doc.documents) return; - const record = normalizeDocumentRecord( - doc.documents?.data, - doc.documents?.uri, - ); - const uri = new AtUri(doc.documents?.uri); - const rkey = uri.rkey; - if (!record) return; + let docs = publication.documents_in_publications.sort((a, b) => { + const dateA = a.documents?.sort_date + ? new Date(a.documents.sort_date).getTime() + : 0; + const dateB = b.documents?.sort_date + ? new Date(b.documents.sort_date).getTime() + : 0; + return dateB - dateA; // Sort in descending order (newest first) + }); + for (const doc of docs) { + if (!doc.documents) continue; + const record = normalizeDocumentRecord( + doc.documents?.data, + doc.documents?.uri, + ); + const uri = new AtUri(doc.documents?.uri); + const rkey = uri.rkey; + if (!record) continue; - let blocks: PubLeafletPagesLinearDocument.Block[] = []; - if (hasLeafletContent(record) && record.content.pages[0]) { - const firstPage = record.content.pages[0]; - if (PubLeafletPagesLinearDocument.isMain(firstPage)) { - blocks = firstPage.blocks || []; - } - } - const stream = await renderToReadableStream( - createElement(StaticPostContent, { blocks, did: uri.host }), - ); - const reader = stream.getReader(); - const chunks = []; + let blocks: PubLeafletPagesLinearDocument.Block[] = []; + if (hasLeafletContent(record) && record.content.pages[0]) { + const firstPage = record.content.pages[0]; + if (PubLeafletPagesLinearDocument.isMain(firstPage)) { + blocks = firstPage.blocks || []; + } + } + const stream = await renderToReadableStream( + createElement(StaticPostContent, { blocks, did: uri.host }), + ); + const reader = stream.getReader(); + const chunks = []; - let done, value; - while (!done) { - ({ done, value } = await reader.read()); - if (value) { - chunks.push(new TextDecoder().decode(value)); - } - } + let done, value; + while (!done) { + ({ done, value } = await reader.read()); + if (value) { + chunks.push(new TextDecoder().decode(value)); + } + } - const docUrl = getDocumentURL(record, doc.documents.uri, pubRecord); - feed.addItem({ - title: record.title, - description: record.description, - date: record.publishedAt ? new Date(record.publishedAt) : new Date(), - id: docUrl, - link: docUrl, - content: chunks.join(""), - }); - }), - ); + const docUrl = getDocumentURL(record, doc.documents.uri, pubRecord); + feed.addItem({ + title: record.title, + description: record.description, + date: record.publishedAt ? new Date(record.publishedAt) : new Date(), + id: docUrl, + link: docUrl, + content: chunks.join(""), + }); + } return feed; } -- 2.51.2 From 5a7a4d3269a06b0b9b8607d161f3c7b0c1d091c7 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Wed, 11 Feb 2026 21:46:37 +0400 Subject: [PATCH 37/51] check if doc path has leading / and add if needed --- app/lish/createPub/getPublicationURL.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/app/lish/createPub/getPublicationURL.ts b/app/lish/createPub/getPublicationURL.ts index 3f864599..cfb39d3f 100644 --- a/app/lish/createPub/getPublicationURL.ts +++ b/app/lish/createPub/getPublicationURL.ts @@ -58,18 +58,22 @@ export function getDocumentURL( docUri: string, publication?: PublicationInput | NormalizedPublication | null, ): string { - const path = doc.path || "/" + new AtUri(docUri).rkey; + let path = doc.path || "/" + new AtUri(docUri).rkey; + if (path[0] !== "/") path = "/" + path; const aturi = new AtUri(docUri); const isNormalized = !!publication && - (publication as NormalizedPublication).$type === "site.standard.publication"; + (publication as NormalizedPublication).$type === + "site.standard.publication"; const normPub = isNormalized ? (publication as NormalizedPublication) : publication ? normalizePublicationRecord((publication as PublicationInput).record) : null; - const pubInput = isNormalized ? null : (publication as PublicationInput | null); + const pubInput = isNormalized + ? null + : (publication as PublicationInput | null); // Non-leaflet documents always use the full publication site URL if (doc.content && !hasLeafletContent(doc) && normPub?.url) { -- 2.51.2 From c460c609d3e2c17146302eb3c21ecae0fe29d9ae Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Wed, 11 Feb 2026 21:56:40 +0400 Subject: [PATCH 38/51] log when setting indexed --- app/api/inngest/functions/sync_document_metadata.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/api/inngest/functions/sync_document_metadata.ts b/app/api/inngest/functions/sync_document_metadata.ts index 8dc0c775..1c091192 100644 --- a/app/api/inngest/functions/sync_document_metadata.ts +++ b/app/api/inngest/functions/sync_document_metadata.ts @@ -32,7 +32,7 @@ export const sync_document_metadata = inngest.createFunction( if (!handleResult) return { error: "No Handle" }; await step.run("set-indexed", async () => { - await supabaseServerClient + return await supabaseServerClient .from("documents") .update({ indexed: !handleResult.isBridgy }) .eq("uri", document_uri); -- 2.51.2 From 67ea66e7a3ba57fef2364710a5ae0042ad071ebe Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Wed, 11 Feb 2026 22:02:54 +0400 Subject: [PATCH 39/51] log data from update indexed step --- app/api/inngest/functions/sync_document_metadata.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/api/inngest/functions/sync_document_metadata.ts b/app/api/inngest/functions/sync_document_metadata.ts index 1c091192..ee4e6b86 100644 --- a/app/api/inngest/functions/sync_document_metadata.ts +++ b/app/api/inngest/functions/sync_document_metadata.ts @@ -35,7 +35,8 @@ export const sync_document_metadata = inngest.createFunction( return await supabaseServerClient .from("documents") .update({ indexed: !handleResult.isBridgy }) - .eq("uri", document_uri); + .eq("uri", document_uri) + .select(); }); if (!bsky_post_uri || handleResult.isBridgy) { -- 2.51.2 From 471ed160881f9cf827b374fb511f6fd3c71c21e2 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Wed, 11 Feb 2026 22:48:22 +0400 Subject: [PATCH 40/51] default to indexed false and manually set on leaflet publish --- actions/publishToPublication.ts | 1 + appview/index.ts | 2 -- supabase/migrations/20260211000000_indexed_default_false.sql | 1 + 3 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 supabase/migrations/20260211000000_indexed_default_false.sql diff --git a/actions/publishToPublication.ts b/actions/publishToPublication.ts index a1a93592..70aa3d9d 100644 --- a/actions/publishToPublication.ts +++ b/actions/publishToPublication.ts @@ -300,6 +300,7 @@ export async function publishToPublication({ await supabaseServerClient.from("documents").upsert({ uri: result.uri, data: record as unknown as Json, + indexed: true, }); if (publication_uri) { diff --git a/appview/index.ts b/appview/index.ts index 6c4c1548..31ead5fb 100644 --- a/appview/index.ts +++ b/appview/index.ts @@ -107,7 +107,6 @@ async function handleEvent(evt: Event) { let docResult = await supabase.from("documents").upsert({ uri: evt.uri.toString(), data: record.value as Json, - indexed: false, }); if (docResult.error) console.log(docResult.error); await inngest.send({ @@ -275,7 +274,6 @@ async function handleEvent(evt: Event) { let docResult = await supabase.from("documents").upsert({ uri: evt.uri.toString(), data: record.value as Json, - indexed: false, }); if (docResult.error) console.log(docResult.error); await inngest.send({ diff --git a/supabase/migrations/20260211000000_indexed_default_false.sql b/supabase/migrations/20260211000000_indexed_default_false.sql new file mode 100644 index 00000000..09031c3a --- /dev/null +++ b/supabase/migrations/20260211000000_indexed_default_false.sql @@ -0,0 +1 @@ +ALTER TABLE documents ALTER COLUMN indexed SET DEFAULT false; -- 2.51.2 From 5c1da06c43b099a4836bef01e460585552edc851 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Fri, 13 Feb 2026 09:59:56 +0400 Subject: [PATCH 41/51] remove commit claude local settings --- .claude/settings.local.json | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 .claude/settings.local.json diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index 9371392c..00000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "permissions": { - "allow": [ - "mcp__acp__Edit", - "mcp__acp__Write", - "mcp__acp__Bash", - "mcp__primitive__say_hello", - "mcp__primitive__pending_delegations", - "mcp__primitive__claim_delegation", - "mcp__primitive__tasks_update", - "mcp__primitive__contexts_update", - "mcp__primitive__contexts_list" - ] - } -} -- 2.51.2 From 19c7b0af3767686a8d25be6956d1037dfec698d2 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Fri, 13 Feb 2026 10:09:37 +0400 Subject: [PATCH 42/51] prevent description from being overridden when blank --- app/lish/createPub/updatePublication.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/lish/createPub/updatePublication.ts b/app/lish/createPub/updatePublication.ts index 350b3f88..77e1a191 100644 --- a/app/lish/createPub/updatePublication.ts +++ b/app/lish/createPub/updatePublication.ts @@ -275,7 +275,7 @@ export async function updatePublication({ return buildRecord(normalizedPub, existingBasePath, publicationType, { name, - description, + ...(description !== undefined && { description }), icon: iconBlob, preferences, }); -- 2.51.2 From 10133c257cfbf8ad47aa3baf99b7c40f04f231aa Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Fri, 13 Feb 2026 15:47:22 -0500 Subject: [PATCH 43/51] Add proper reader experience and rework nav (#269) * moved around some stuff for our navigation * little tweak * messing around with navigation on mobile * Added a page title to the home layout * added pagetitle to looseleaf and pub, added shortcut if you only have one pub or looseleaf * some icons * quick little adjustments * moved action buttons on mobile to page title area, publist commented out for now * added the publicaions nav to the mobile footer * add states for logged out * stray console log eradication * some shifting around of things * update the homeempty layout * move publicationbuttons into separate compoennt, added state to handle jsut one pub or looseleaf * rename small prop in PubIcon to tiny * adding tabs to Reader * oops forgot the new files * updated postListing design * add a drawer for comments and mentions in reader * new files * remove features not in scope * adjust width of ActionButon in desktop * added indication that post is on non-leaflet page is applicable * repostion interaction drawer close button * small fixes per feedback * add inngest function to track bsky post likes * don't sync bsky likes of brid.gy posts * add bsky auth view all permissions * add aud to bsky view permissions * add recommend notification * fix post links to standard site blogs * don't getpreferences on subscribe * add recommend column to documents * check permissions on retractions * don't show block options if no write permissions * handle pasting bluesky posts * fix: copy post link button * add indexed col to documents and update inngest * fix type error * add hot feed * adding profile button to nav, removing things where necessary * Remove accidentally committed files from bbc6c135 Remove scratch/, scripts/, database.sql, appview/test.ts, CoordDebugger.tsx, search_loose_leafs.ts, supabase migration, and tmp_standard-site-migration.md that were unintentionally included in the previous commit. Co-Authored-By: Claude Opus 4.6 * added profile settings to nav * WIP suspense reader pages * add loading.tsx for route level suspense boundary * render what's hot for logged out users and don't render subs * highlight what's hot tab if default * persist nav state on reader/home * remove .claude/settings.local.json * wire up interactions * remove publications and fix external url logic * don't keep previous data in interactions sidebar in feed * fix empty subs state * fix share links in postlisting * remove help text * fix cover image positioning --------- Co-authored-by: celine Co-authored-by: Henrique Dias Co-authored-by: Claude Opus 4.6 --- app/(home-pages)/discover/SortButtons.tsx | 97 ------ .../discover/SortedPublicationList.tsx | 195 ------------ app/(home-pages)/discover/getPublications.ts | 133 -------- app/(home-pages)/discover/page.tsx | 53 ---- .../home/Actions/AccountSettings.tsx | 122 +------- app/(home-pages)/home/Actions/Actions.tsx | 8 +- .../home/Actions/CreateNewButton.tsx | 3 +- app/(home-pages)/home/HomeEmpty/HomeEmpty.tsx | 134 ++++---- .../home/HomeEmpty/WelcomeToLeafletIllo.tsx | 4 +- app/(home-pages)/home/HomeLayout.tsx | 13 +- app/(home-pages)/layout.tsx | 3 + .../looseleafs/LooseleafsLayout.tsx | 1 + .../p/[didOrHandle]/PostsContent.tsx | 2 +- .../p/[didOrHandle]/ProfileHeader.tsx | 6 +- .../p/[didOrHandle]/ProfileLayout.tsx | 4 +- .../p/[didOrHandle]/ProfileTabs.tsx | 2 +- .../[didOrHandle]}/PubListing.tsx | 30 +- .../comments/CommentsContent.tsx | 2 +- app/(home-pages)/p/[didOrHandle]/layout.tsx | 5 +- .../subscriptions/SubscriptionsContent.tsx | 4 +- app/(home-pages)/page.tsx | 31 ++ app/(home-pages)/reader/FeedSkeleton.tsx | 13 + app/(home-pages)/reader/GlobalContent.tsx | 49 +++ .../{ReaderContent.tsx => InboxContent.tsx} | 61 ++-- .../reader/InteractionDrawers.tsx | 108 +++++++ app/(home-pages)/reader/NewContent.tsx | 93 ++++++ app/(home-pages)/reader/PreviewDrawer.tsx | 0 .../reader/ReaderMentionsContent.tsx | 72 +++++ .../reader/SubscriptionsContent.tsx | 105 ------- app/(home-pages)/reader/enrichPost.ts | 88 ++++++ app/(home-pages)/reader/getHotFeed.ts | 100 ++++++ app/(home-pages)/reader/getNewFeed.ts | 54 ++++ app/(home-pages)/reader/getReaderFeed.ts | 51 +-- app/(home-pages)/reader/getSubscriptions.ts | 10 +- app/(home-pages)/reader/hot/page.tsx | 7 + app/(home-pages)/reader/layout.tsx | 79 +++++ app/(home-pages)/reader/loading.tsx | 4 + app/(home-pages)/reader/new/page.tsx | 7 + app/(home-pages)/reader/page.tsx | 45 +-- app/[leaflet_id]/Footer.tsx | 22 +- .../publish/BskyPostEditorProsemirror.tsx | 2 - .../[command]/get_document_interactions.ts | 90 ++++++ app/api/rpc/[command]/get_hot_feed.ts | 17 + app/api/rpc/[command]/route.ts | 4 + app/api/update-nav-state/route.ts | 16 + app/lish/Subscribe.tsx | 56 +++- .../[rkey]/Interactions/Comments/index.tsx | 32 +- .../[rkey]/Interactions/InteractionDrawer.tsx | 52 +++- .../[rkey]/Interactions/Quotes.tsx | 10 +- .../[did]/[publication]/[rkey]/ThreadPage.tsx | 1 - .../[publication]/[rkey]/getPostPageData.ts | 4 - .../[did]/[publication]/dashboard/Actions.tsx | 1 + .../[publication]/dashboard/DraftList.tsx | 10 +- .../dashboard/NewDraftButton.tsx | 26 +- .../dashboard/PublicationDashboard.tsx | 1 + .../dashboard/PublicationSWRProvider.tsx | 6 +- .../settings/PublicationSettings.tsx | 1 + app/route.ts | 11 - components/ActionBar/ActionButton.tsx | 20 +- components/ActionBar/DesktopNavigation.tsx | 66 ++++ components/ActionBar/Footer.tsx | 2 +- components/ActionBar/MobileNavigation.tsx | 63 ++++ components/ActionBar/Navigation.tsx | 179 ----------- components/ActionBar/NavigationButtons.tsx | 101 ++++++ components/ActionBar/ProfileButton.tsx | 61 ++++ .../ActionBar/PublicationNavigation.tsx | 148 +++++++++ components/ActionBar/Publications.tsx | 29 +- components/Blocks/BaseTextareaBlock.tsx | 1 - components/Icons/HomeTiny.tsx | 21 ++ components/Icons/LooseleafTiny.tsx | 19 ++ components/Icons/MoreOptionsVerticalTiny.tsx | 1 + components/Icons/NotificationSmall.tsx | 37 +-- components/Icons/RSSTiny.tsx | 18 ++ components/Icons/ReaderSmall.tsx | 7 +- components/Icons/ShareTiny.tsx | 19 ++ components/Icons/TagSmall.tsx | 19 ++ components/Icons/WriterSmall.tsx | 19 ++ components/InteractionsPreview.tsx | 2 +- components/LoginButton.tsx | 6 +- components/NavStateTracker.tsx | 27 ++ components/PageHeader.tsx | 2 +- components/PageLayouts/DashboardLayout.tsx | 49 ++- components/Pages/useHasBackgroundImage.ts | 5 + components/Popover/index.tsx | 2 +- components/PostListing.tsx | 294 +++++++++++++----- components/Tab.tsx | 4 + components/Tags.tsx | 1 - components/ThemeManager/ThemeProvider.tsx | 41 ++- src/replicache/mutations.ts | 1 - src/useSelectedPostState.ts | 16 + src/utils/useRecordFromDid.ts | 12 + 91 files changed, 2070 insertions(+), 1382 deletions(-) delete mode 100644 app/(home-pages)/discover/SortButtons.tsx delete mode 100644 app/(home-pages)/discover/SortedPublicationList.tsx delete mode 100644 app/(home-pages)/discover/getPublications.ts delete mode 100644 app/(home-pages)/discover/page.tsx rename app/(home-pages)/{discover => p/[didOrHandle]}/PubListing.tsx (71%) create mode 100644 app/(home-pages)/page.tsx create mode 100644 app/(home-pages)/reader/FeedSkeleton.tsx create mode 100644 app/(home-pages)/reader/GlobalContent.tsx rename app/(home-pages)/reader/{ReaderContent.tsx => InboxContent.tsx} (61%) create mode 100644 app/(home-pages)/reader/InteractionDrawers.tsx create mode 100644 app/(home-pages)/reader/NewContent.tsx create mode 100644 app/(home-pages)/reader/PreviewDrawer.tsx create mode 100644 app/(home-pages)/reader/ReaderMentionsContent.tsx delete mode 100644 app/(home-pages)/reader/SubscriptionsContent.tsx create mode 100644 app/(home-pages)/reader/enrichPost.ts create mode 100644 app/(home-pages)/reader/getHotFeed.ts create mode 100644 app/(home-pages)/reader/getNewFeed.ts create mode 100644 app/(home-pages)/reader/hot/page.tsx create mode 100644 app/(home-pages)/reader/layout.tsx create mode 100644 app/(home-pages)/reader/loading.tsx create mode 100644 app/(home-pages)/reader/new/page.tsx create mode 100644 app/api/rpc/[command]/get_document_interactions.ts create mode 100644 app/api/rpc/[command]/get_hot_feed.ts create mode 100644 app/api/update-nav-state/route.ts delete mode 100644 app/route.ts create mode 100644 components/ActionBar/DesktopNavigation.tsx create mode 100644 components/ActionBar/MobileNavigation.tsx delete mode 100644 components/ActionBar/Navigation.tsx create mode 100644 components/ActionBar/NavigationButtons.tsx create mode 100644 components/ActionBar/ProfileButton.tsx create mode 100644 components/ActionBar/PublicationNavigation.tsx create mode 100644 components/Icons/HomeTiny.tsx create mode 100644 components/Icons/LooseleafTiny.tsx create mode 100644 components/Icons/RSSTiny.tsx create mode 100644 components/Icons/ShareTiny.tsx create mode 100644 components/Icons/TagSmall.tsx create mode 100644 components/Icons/WriterSmall.tsx create mode 100644 components/NavStateTracker.tsx create mode 100644 components/Pages/useHasBackgroundImage.ts create mode 100644 src/useSelectedPostState.ts create mode 100644 src/utils/useRecordFromDid.ts diff --git a/app/(home-pages)/discover/SortButtons.tsx b/app/(home-pages)/discover/SortButtons.tsx deleted file mode 100644 index 3b90228d..00000000 --- a/app/(home-pages)/discover/SortButtons.tsx +++ /dev/null @@ -1,97 +0,0 @@ -"use client"; -import Link from "next/link"; -import { useState } from "react"; -import { theme } from "tailwind.config"; - -export default function SortButtons(props: { order: string }) { - const [selected, setSelected] = useState<"recentlyUpdated" | "popular">( - "recentlyUpdated", - ); - - return ( -
- - - Recently Updated - - - - - Popular - -
- ); -} - -const SortButton = (props: { - children: React.ReactNode; - selected: boolean; -}) => { - return ( -
- - {props.selected && ( - <> -
- -
-
- -
-
- -
- - )} -
- ); -}; - -const GlitterBig = () => { - return ( - - - - ); -}; - -const GlitterSmall = () => { - return ( - - - - ); -}; diff --git a/app/(home-pages)/discover/SortedPublicationList.tsx b/app/(home-pages)/discover/SortedPublicationList.tsx deleted file mode 100644 index d72a3e14..00000000 --- a/app/(home-pages)/discover/SortedPublicationList.tsx +++ /dev/null @@ -1,195 +0,0 @@ -"use client"; -import Link from "next/link"; -import { useState, useEffect, useRef } from "react"; -import { theme } from "tailwind.config"; -import { PubListing } from "./PubListing"; -import useSWRInfinite from "swr/infinite"; -import { getPublications, type Cursor, type Publication } from "./getPublications"; - -export function SortedPublicationList(props: { - publications: Publication[]; - order: string; - nextCursor: Cursor | null; -}) { - let [order, setOrder] = useState(props.order); - - const getKey = ( - pageIndex: number, - previousPageData: { publications: Publication[]; nextCursor: Cursor | null } | null, - ) => { - // Reached the end - if (previousPageData && !previousPageData.nextCursor) return null; - - // First page, we don't have previousPageData - if (pageIndex === 0) return ["discover-publications", order, null] as const; - - // Add the cursor to the key - return ["discover-publications", order, previousPageData?.nextCursor] as const; - }; - - const { data, error, size, setSize, isValidating } = useSWRInfinite( - getKey, - ([_, orderValue, cursor]) => { - const orderParam = orderValue === "popular" ? "popular" : "recentlyUpdated"; - return getPublications(orderParam, cursor); - }, - { - fallbackData: order === props.order - ? [{ publications: props.publications, nextCursor: props.nextCursor }] - : undefined, - revalidateFirstPage: false, - }, - ); - - const loadMoreRef = useRef(null); - - // Set up intersection observer to load more when trigger element is visible - useEffect(() => { - const observer = new IntersectionObserver( - (entries) => { - if (entries[0].isIntersecting && !isValidating) { - const hasMore = data && data[data.length - 1]?.nextCursor; - if (hasMore) { - setSize(size + 1); - } - } - }, - { threshold: 0.1 }, - ); - - if (loadMoreRef.current) { - observer.observe(loadMoreRef.current); - } - - return () => observer.disconnect(); - }, [data, size, setSize, isValidating]); - - const allPublications = data ? data.flatMap((page) => page.publications) : []; - - return ( -
- { - const url = new URL(window.location.href); - url.searchParams.set("order", o); - window.history.pushState({}, "", url); - setOrder(o); - }} - /> -
- {allPublications.map((pub) => ( - - ))} - {/* Trigger element for loading more publications */} - -
- ); -} - -export default function SortButtons(props: { - order: string; - setOrder: (order: string) => void; -}) { - const [selected, setSelected] = useState<"recentlyUpdated" | "popular">( - "recentlyUpdated", - ); - - return ( -
- props.setOrder("recentlyUpdated")} - > - Recently Updated - - - props.setOrder("popular")} - > - Popular - -
- ); -} - -const SortButton = (props: { - children: React.ReactNode; - onClick: () => void; - selected: boolean; -}) => { - return ( -
- - {props.selected && ( - <> -
- -
-
- -
-
- -
- - )} -
- ); -}; - -const GlitterBig = () => { - return ( - - - - ); -}; - -const GlitterSmall = () => { - return ( - - - - ); -}; diff --git a/app/(home-pages)/discover/getPublications.ts b/app/(home-pages)/discover/getPublications.ts deleted file mode 100644 index aa633c1a..00000000 --- a/app/(home-pages)/discover/getPublications.ts +++ /dev/null @@ -1,133 +0,0 @@ -"use server"; - -import { supabaseServerClient } from "supabase/serverClient"; -import { - normalizePublicationRow, - hasValidPublication, -} from "src/utils/normalizeRecords"; -import { deduplicateByUri } from "src/utils/deduplicateRecords"; - -export type Cursor = { - sort_date?: string; - count?: number; - uri: string; -}; - -export type Publication = Awaited< - ReturnType ->["publications"][number]; - -export async function getPublications( - order: "recentlyUpdated" | "popular" = "recentlyUpdated", - cursor?: Cursor | null, -): Promise<{ publications: any[]; nextCursor: Cursor | null }> { - const limit = 25; - - // Fetch all publications with their most recent document - let { data: publications, error } = await supabaseServerClient - .from("publications") - .select( - "*, documents_in_publications(*, documents(*)), publication_subscriptions(count)", - ) - .or( - "record->preferences->showInDiscover.is.null,record->preferences->>showInDiscover.eq.true", - ) - .order("documents(sort_date)", { - referencedTable: "documents_in_publications", - ascending: false, - }) - .limit(1, { referencedTable: "documents_in_publications" }); - - if (error) { - console.error("Error fetching publications:", error); - return { publications: [], nextCursor: null }; - } - - // Deduplicate records that may exist under both pub.leaflet and site.standard namespaces - const dedupedPublications = deduplicateByUri(publications || []); - - // Filter out publications without documents - const allPubs = dedupedPublications.filter( - (pub) => pub.documents_in_publications.length > 0, - ); - - // Sort on the server - allPubs.sort((a, b) => { - if (order === "popular") { - const aCount = a.publication_subscriptions[0]?.count || 0; - const bCount = b.publication_subscriptions[0]?.count || 0; - if (bCount !== aCount) { - return bCount - aCount; - } - // Secondary sort by uri for stability - return b.uri.localeCompare(a.uri); - } else { - // recentlyUpdated - const aDate = new Date( - a.documents_in_publications[0]?.documents?.sort_date || 0, - ).getTime(); - const bDate = new Date( - b.documents_in_publications[0]?.documents?.sort_date || 0, - ).getTime(); - if (bDate !== aDate) { - return bDate - aDate; - } - // Secondary sort by uri for stability - return b.uri.localeCompare(a.uri); - } - }); - - // Find cursor position and slice - let startIndex = 0; - if (cursor) { - startIndex = allPubs.findIndex((pub) => { - if (order === "popular") { - const pubCount = pub.publication_subscriptions[0]?.count || 0; - // Find first pub after cursor - return ( - pubCount < (cursor.count || 0) || - (pubCount === cursor.count && pub.uri < cursor.uri) - ); - } else { - const pubDate = pub.documents_in_publications[0]?.documents?.sort_date || ""; - // Find first pub after cursor - return ( - pubDate < (cursor.sort_date || "") || - (pubDate === cursor.sort_date && pub.uri < cursor.uri) - ); - } - }); - // If not found, we're at the end - if (startIndex === -1) { - return { publications: [], nextCursor: null }; - } - } - - // Get the page - const page = allPubs.slice(startIndex, startIndex + limit); - - // Normalize publication records - const normalizedPage = page - .map(normalizePublicationRow) - .filter(hasValidPublication); - - // Create next cursor based on last item in normalizedPage - const lastItem = normalizedPage[normalizedPage.length - 1]; - const nextCursor = - normalizedPage.length > 0 && startIndex + limit < allPubs.length - ? order === "recentlyUpdated" - ? { - sort_date: lastItem.documents_in_publications[0]?.documents?.sort_date, - uri: lastItem.uri, - } - : { - count: lastItem.publication_subscriptions[0]?.count || 0, - uri: lastItem.uri, - } - : null; - - return { - publications: normalizedPage, - nextCursor, - }; -} diff --git a/app/(home-pages)/discover/page.tsx b/app/(home-pages)/discover/page.tsx deleted file mode 100644 index facf7c12..00000000 --- a/app/(home-pages)/discover/page.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import Link from "next/link"; -import { SortedPublicationList } from "./SortedPublicationList"; -import { Metadata } from "next"; -import { DashboardLayout } from "components/PageLayouts/DashboardLayout"; -import { getPublications } from "./getPublications"; - -export const metadata: Metadata = { - title: "Leaflet Discover", - description: "Explore publications on Leaflet ✨ Or make your own!", -}; - -export default async function Discover(props: { - searchParams: Promise<{ [key: string]: string | string[] | undefined }>; -}) { - let order = ((await props.searchParams).order as string) || "recentlyUpdated"; - - return ( - , - }, - }} - /> - ); -} - -const DiscoverContent = async (props: { order: string }) => { - const orderValue = props.order === "popular" ? "popular" : "recentlyUpdated"; - let { publications, nextCursor } = await getPublications(orderValue); - - return ( -
-
-

Discover

-

- Explore publications on Leaflet ✨ Or{" "} - make your own! -

-
- -
- ); -}; diff --git a/app/(home-pages)/home/Actions/AccountSettings.tsx b/app/(home-pages)/home/Actions/AccountSettings.tsx index 67df4f37..c3cb0043 100644 --- a/app/(home-pages)/home/Actions/AccountSettings.tsx +++ b/app/(home-pages)/home/Actions/AccountSettings.tsx @@ -1,134 +1,24 @@ "use client"; import { ActionButton } from "components/ActionBar/ActionButton"; -import { mutate } from "swr"; -import { AccountSmall } from "components/Icons/AccountSmall"; -import { LogoutSmall } from "components/Icons/LogoutSmall"; import { Popover } from "components/Popover"; -import { ArrowRightTiny } from "components/Icons/ArrowRightTiny"; -import { SpeedyLink } from "components/SpeedyLink"; -import { GoBackSmall } from "components/Icons/GoBackSmall"; -import { useState } from "react"; import { ThemeSetterContent } from "components/ThemeManager/ThemeSetter"; import { useIsMobile } from "src/hooks/isMobile"; +import { PaintSmall } from "components/Icons/PaintSmall"; -export const AccountSettings = (props: { entityID: string }) => { - let [state, setState] = useState<"menu" | "general" | "theme">("menu"); +export const AccountTheme = (props: { entityID: string }) => { let isMobile = useIsMobile(); return ( setState("menu")} side={isMobile ? "top" : "right"} align={isMobile ? "center" : "start"} - className={`max-w-xs w-[1000px] ${state === "theme" && "bg-white!"}`} - trigger={ label="Settings" />} + className={`w-xs bg-white!`} + arrowFill="bg-white" + trigger={ label="Theme" />} > - {state === "general" ? ( - setState("menu")} /> - ) : state === "theme" ? ( - setState("menu")} - /> - ) : ( - - )} - - ); -}; - -const SettingsMenu = (props: { - state: "menu" | "general" | "theme"; - setState: (s: typeof props.state) => void; -}) => { - let menuItemClassName = - "menuItem -mx-[8px] text-left flex items-center justify-between hover:no-underline!"; - - return ( -
- - - -
- ); -}; - -const GeneralSettings = (props: { backToMenu: () => void }) => { - return ( -
- props.backToMenu()} - /> - - -
- ); -}; -const AccountThemeSettings = (props: { - entityID: string; - backToMenu: () => void; -}) => { - return ( -
- props.backToMenu()} - /> -
- ); -}; -export const AccountSettingsHeader = (props: { - state: "menu" | "general" | "theme"; - backToMenuAction?: () => void; -}) => { - return ( -
- {props.state === "menu" - ? "Settings" - : props.state === "general" - ? "General" - : props.state === "theme" - ? "Account Theme" - : ""} - {props.backToMenuAction && ( - - )} -
+ ); }; diff --git a/app/(home-pages)/home/Actions/Actions.tsx b/app/(home-pages)/home/Actions/Actions.tsx index 20852f4d..050f7f16 100644 --- a/app/(home-pages)/home/Actions/Actions.tsx +++ b/app/(home-pages)/home/Actions/Actions.tsx @@ -2,7 +2,7 @@ import { ThemePopover } from "components/ThemeManager/ThemeSetter"; import { CreateNewLeafletButton } from "./CreateNewButton"; import { HelpButton } from "app/[leaflet_id]/actions/HelpButton"; -import { AccountSettings } from "./AccountSettings"; +import { AccountTheme } from "./AccountSettings"; import { useIdentityData } from "components/IdentityProvider"; import { useReplicache } from "src/replicache"; import { LoginActionButton } from "components/LoginButton"; @@ -13,11 +13,7 @@ export const Actions = () => { return ( <> - {identity ? ( - - ) : ( - - )} + {identity && } ); }; diff --git a/app/(home-pages)/home/Actions/CreateNewButton.tsx b/app/(home-pages)/home/Actions/CreateNewButton.tsx index 417c5dcd..454ae7ca 100644 --- a/app/(home-pages)/home/Actions/CreateNewButton.tsx +++ b/app/(home-pages)/home/Actions/CreateNewButton.tsx @@ -26,8 +26,9 @@ export const CreateNewLeafletButton = (props: {}) => { + icon= label="New" + smallOnMobile /> } > diff --git a/app/(home-pages)/home/HomeEmpty/HomeEmpty.tsx b/app/(home-pages)/home/HomeEmpty/HomeEmpty.tsx index ecabbabc..517e2580 100644 --- a/app/(home-pages)/home/HomeEmpty/HomeEmpty.tsx +++ b/app/(home-pages)/home/HomeEmpty/HomeEmpty.tsx @@ -1,107 +1,83 @@ "use client"; - import { PubListEmptyIllo } from "components/ActionBar/Publications"; -import { ButtonPrimary } from "components/Buttons"; -import { AddSmall } from "components/Icons/AddSmall"; -import { Link } from "react-aria-components"; -import { DiscoverIllo } from "./DiscoverIllo"; +import { ButtonPrimary, ButtonSecondary } from "components/Buttons"; import { WelcomeToLeafletIllo } from "./WelcomeToLeafletIllo"; -import { DiscoverSmall } from "components/Icons/DiscoverSmall"; -import { PublishSmall } from "components/Icons/PublishSmall"; import { createNewLeaflet } from "actions/createNewLeaflet"; import { useIsMobile } from "src/hooks/isMobile"; +import { SpeedyLink } from "components/SpeedyLink"; export function HomeEmptyState() { - let isMobile = useIsMobile(); return ( -
-
-
- -
-
-

Leaflet

- {/*

A platform for social publishing.

*/} -
- Write and share delightful documents! -
- { - let openNewLeaflet = (id: string) => { - if (isMobile) { - window.location.href = `/${id}?focusFirstBlock`; - } else { - window.open(`/${id}?focusFirstBlock`, "_blank"); - } - }; - - let id = await createNewLeaflet({ - pageType: "doc", - redirectUser: false, - }); - openNewLeaflet(id); - }} - > - Write a Doc! - -
-
-
-
-
or
-
-
- +
- -
- Right now docs and publications are separate. Soon you'll be able to add - docs to pubs! +
+
+
or
+
+
); } -export const PublicationBanner = (props: { small?: boolean }) => { +let bannerStyles = + "flex flex-row sm:flex-col py-4 px-4 sm:items-center items-start gap-4"; + +const PublicationBanner = () => { return ( -
- {props.small ? ( - - ) : ( +
+
- )} -
- - Start a Publication - {" "} - and blog in the Atmosphere +
+

Create a Publication!

+
+ You can decide to share or publish it later. +
+ + Create a Publication + +
); }; -export const DiscoverBanner = (props: { small?: boolean }) => { +const DocBanner = () => { + let isMobile = useIsMobile(); + return ( -
- {props.small ? ( - - ) : ( -
- +
+
+ +
+ +
+

Just write something

+
+ You can decide to share or publish it later.
- )} -
- - Explore Publications - {" "} - on art, tech, games, music & more! + { + let openNewLeaflet = (id: string) => { + if (isMobile) { + window.location.href = `/${id}?focusFirstBlock`; + } else { + window.open(`/${id}?focusFirstBlock`, "_blank"); + } + }; + + let id = await createNewLeaflet({ + pageType: "doc", + redirectUser: false, + }); + openNewLeaflet(id); + }} + > + New Doc! +
); diff --git a/app/(home-pages)/home/HomeEmpty/WelcomeToLeafletIllo.tsx b/app/(home-pages)/home/HomeEmpty/WelcomeToLeafletIllo.tsx index 289d8355..d88c55a5 100644 --- a/app/(home-pages)/home/HomeEmpty/WelcomeToLeafletIllo.tsx +++ b/app/(home-pages)/home/HomeEmpty/WelcomeToLeafletIllo.tsx @@ -3,8 +3,8 @@ import { theme } from "tailwind.config"; export const WelcomeToLeafletIllo = () => { return ( ); }; @@ -170,10 +167,6 @@ export function HomeLeafletList(props: { showPreview />
- - {leaflets.filter((l) => !!l.token.leaflets_in_publications).length === - 0 && } - ); } @@ -204,7 +197,7 @@ export function LeafletList(props: { className={` leafletList w-full - ${display === "grid" ? "grid auto-rows-max md:grid-cols-4 sm:grid-cols-3 grid-cols-2 gap-y-4 gap-x-4 sm:gap-x-6 sm:gap-y-5 grow" : "flex flex-col gap-2 pt-2"} `} + ${display === "grid" ? "grid auto-rows-max md:grid-cols-4 sm:grid-cols-3 grid-cols-2 gap-y-4 gap-x-4 sm:gap-x-6 sm:gap-y-5 grow" : "flex flex-col gap-2"} `} > {props.leaflets.map(({ token: leaflet, added_at, archived }, index) => ( + {props.children} ); @@ -34,6 +36,7 @@ export default async function HomePagesLayout(props: { > + {props.children} diff --git a/app/(home-pages)/looseleafs/LooseleafsLayout.tsx b/app/(home-pages)/looseleafs/LooseleafsLayout.tsx index 5f0c12b3..c389d6e4 100644 --- a/app/(home-pages)/looseleafs/LooseleafsLayout.tsx +++ b/app/(home-pages)/looseleafs/LooseleafsLayout.tsx @@ -47,6 +47,7 @@ export const LooseleafsLayout = (props: { ), }, }} + pageTitle="Looseleafs" /> ); }; diff --git a/app/(home-pages)/p/[didOrHandle]/PostsContent.tsx b/app/(home-pages)/p/[didOrHandle]/PostsContent.tsx index eb841838..7bd67c3c 100644 --- a/app/(home-pages)/p/[didOrHandle]/PostsContent.tsx +++ b/app/(home-pages)/p/[didOrHandle]/PostsContent.tsx @@ -68,7 +68,7 @@ export const ProfilePostsContent = (props: { } return ( -
+
{allPosts.map((post) => ( ))} diff --git a/app/(home-pages)/p/[didOrHandle]/ProfileHeader.tsx b/app/(home-pages)/p/[didOrHandle]/ProfileHeader.tsx index 3fb2b0fd..f99934c7 100644 --- a/app/(home-pages)/p/[didOrHandle]/ProfileHeader.tsx +++ b/app/(home-pages)/p/[didOrHandle]/ProfileHeader.tsx @@ -42,8 +42,6 @@ export const ProfileHeader = (props: { @{props.profile.handle}
); - console.log(props.profile); - return (
-
+
{props.publications.map((p) => ( diff --git a/app/(home-pages)/p/[didOrHandle]/ProfileLayout.tsx b/app/(home-pages)/p/[didOrHandle]/ProfileLayout.tsx index a298dec3..15e598d0 100644 --- a/app/(home-pages)/p/[didOrHandle]/ProfileLayout.tsx +++ b/app/(home-pages)/p/[didOrHandle]/ProfileLayout.tsx @@ -11,10 +11,10 @@ export function ProfileLayout(props: { children: React.ReactNode }) { ${ cardBorderHidden ? "" - : "overflow-y-scroll h-full border border-border-light rounded-lg bg-bg-page" + : "overflow-y-scroll h-full border border-border-light rounded-lg bg-bg-page px-3 sm:px-4" } max-w-prose mx-auto w-full - flex flex-col + flex flex-col pb-3 text-center `} > diff --git a/app/(home-pages)/p/[didOrHandle]/ProfileTabs.tsx b/app/(home-pages)/p/[didOrHandle]/ProfileTabs.tsx index c5b9e3a2..bb058749 100644 --- a/app/(home-pages)/p/[didOrHandle]/ProfileTabs.tsx +++ b/app/(home-pages)/p/[didOrHandle]/ProfileTabs.tsx @@ -41,7 +41,7 @@ export const ProfileTabs = (props: { didOrHandle: string }) => { const bgColor = cardBorderHidden ? "var(--bg-leaflet)" : "var(--bg-page)"; return ( -
+
{ +export const PubListing = (props: PublicationSubscription) => { let record = props.record; let theme = usePubTheme(record?.theme); let backgroundImage = record?.theme?.backgroundImage?.image?.ref @@ -28,8 +25,7 @@ export const PubListing = ( if (!record) return null; return ( - +
); }; diff --git a/app/(home-pages)/p/[didOrHandle]/comments/CommentsContent.tsx b/app/(home-pages)/p/[didOrHandle]/comments/CommentsContent.tsx index b7a9a350..a2a3f474 100644 --- a/app/(home-pages)/p/[didOrHandle]/comments/CommentsContent.tsx +++ b/app/(home-pages)/p/[didOrHandle]/comments/CommentsContent.tsx @@ -85,7 +85,7 @@ export const ProfileCommentsContent = (props: { } return ( -
+
{allComments.map((comment) => ( ))} diff --git a/app/(home-pages)/p/[didOrHandle]/layout.tsx b/app/(home-pages)/p/[didOrHandle]/layout.tsx index 4108fe28..eb9f3531 100644 --- a/app/(home-pages)/p/[didOrHandle]/layout.tsx +++ b/app/(home-pages)/p/[didOrHandle]/layout.tsx @@ -82,6 +82,7 @@ export default async function ProfilePageLayout(props: { id="profile" defaultTab="default" currentPage="profile" + profileDid={did} actions={null} tabs={{ default: { @@ -93,9 +94,7 @@ export default async function ProfilePageLayout(props: { publications={publications || []} /> -
- {props.children} -
+ <>{props.children} ), }, diff --git a/app/(home-pages)/p/[didOrHandle]/subscriptions/SubscriptionsContent.tsx b/app/(home-pages)/p/[didOrHandle]/subscriptions/SubscriptionsContent.tsx index 9e5e4ca8..49818ecd 100644 --- a/app/(home-pages)/p/[didOrHandle]/subscriptions/SubscriptionsContent.tsx +++ b/app/(home-pages)/p/[didOrHandle]/subscriptions/SubscriptionsContent.tsx @@ -2,7 +2,7 @@ import { useEffect, useRef } from "react"; import useSWRInfinite from "swr/infinite"; -import { PubListing } from "app/(home-pages)/discover/PubListing"; +import { PubListing } from "app/(home-pages)/p/[didOrHandle]/PubListing"; import { getSubscriptions, type PublicationSubscription, @@ -82,7 +82,7 @@ export const ProfileSubscriptionsContent = (props: { return (
-
+
{allSubscriptions.map((sub) => ( ))} diff --git a/app/(home-pages)/page.tsx b/app/(home-pages)/page.tsx new file mode 100644 index 00000000..036e9562 --- /dev/null +++ b/app/(home-pages)/page.tsx @@ -0,0 +1,31 @@ +import { cookies } from "next/headers"; +import ReaderLayout from "./reader/layout"; +import ReaderPage from "./reader/page"; +import HomePage from "./home/page"; + +export default async function RootPage() { + const cookieStore = await cookies(); + const hasAuth = + cookieStore.has("auth_token") || + cookieStore.has("external_auth_token"); + + if (!hasAuth) { + return ( + + + + ); + } + + const navState = cookieStore.get("nav-state")?.value; + + if (navState === "reader") { + return ( + + + + ); + } + + return ; +} diff --git a/app/(home-pages)/reader/FeedSkeleton.tsx b/app/(home-pages)/reader/FeedSkeleton.tsx new file mode 100644 index 00000000..2541b869 --- /dev/null +++ b/app/(home-pages)/reader/FeedSkeleton.tsx @@ -0,0 +1,13 @@ +export function FeedSkeleton() { + return ( +
+ {[...Array(3)].map((_, i) => ( +
+
+
+
+
+ ))} +
+ ); +} diff --git a/app/(home-pages)/reader/GlobalContent.tsx b/app/(home-pages)/reader/GlobalContent.tsx new file mode 100644 index 00000000..53cf2462 --- /dev/null +++ b/app/(home-pages)/reader/GlobalContent.tsx @@ -0,0 +1,49 @@ +"use client"; +import { use } from "react"; +import useSWR from "swr"; +import { callRPC } from "app/api/rpc/client"; +import { PostListing } from "components/PostListing"; +import type { Post } from "./getReaderFeed"; +import { + DesktopInteractionPreviewDrawer, + MobileInteractionPreviewDrawer, +} from "./InteractionDrawers"; + +export const GlobalContent = (props: { + promise: Promise<{ posts: Post[] }>; +}) => { + const initialData = use(props.promise); + + const { data } = useSWR( + "hot_feed", + async () => { + const res = await callRPC("get_hot_feed", {}); + return res as unknown as { posts: Post[] }; + }, + { + fallbackData: { posts: initialData.posts }, + }, + ); + + const posts = data?.posts ?? []; + + if (posts.length === 0) { + return ( +
+ Nothing trending right now. Check back soon! +
+ ); + } + + return ( +
+
+ {posts.map((p) => ( + + ))} +
+ + +
+ ); +}; diff --git a/app/(home-pages)/reader/ReaderContent.tsx b/app/(home-pages)/reader/InboxContent.tsx similarity index 61% rename from app/(home-pages)/reader/ReaderContent.tsx rename to app/(home-pages)/reader/InboxContent.tsx index e73b3df9..791d4827 100644 --- a/app/(home-pages)/reader/ReaderContent.tsx +++ b/app/(home-pages)/reader/InboxContent.tsx @@ -1,4 +1,5 @@ "use client"; +import { use } from "react"; import { ButtonPrimary } from "components/Buttons"; import { DiscoverSmall } from "components/Icons/DiscoverSmall"; import type { Cursor, Post } from "./getReaderFeed"; @@ -7,11 +8,17 @@ import { getReaderFeed } from "./getReaderFeed"; import { useEffect, useRef } from "react"; import Link from "next/link"; import { PostListing } from "components/PostListing"; +import { useHasBackgroundImage } from "components/Pages/useHasBackgroundImage"; +import { + DesktopInteractionPreviewDrawer, + MobileInteractionPreviewDrawer, +} from "./InteractionDrawers"; -export const ReaderContent = (props: { - posts: Post[]; - nextCursor: Cursor | null; +export const InboxContent = (props: { + promise: Promise<{ posts: Post[]; nextCursor: Cursor | null }>; }) => { + const { posts, nextCursor } = use(props.promise); + const getKey = ( pageIndex: number, previousPageData: { @@ -33,7 +40,7 @@ export const ReaderContent = (props: { getKey, ([_, cursor]) => getReaderFeed(cursor), { - fallbackData: [{ posts: props.posts, nextCursor: props.nextCursor }], + fallbackData: [{ posts, nextCursor }], revalidateFirstPage: false, }, ); @@ -63,24 +70,36 @@ export const ReaderContent = (props: { const allPosts = data ? data.flatMap((page) => page.posts) : []; + const sortedPosts = allPosts.sort( + (a, b) => + new Date(b.documents.data?.publishedAt || 0).getTime() - + new Date(a.documents.data?.publishedAt || 0).getTime(), + ); + if (allPosts.length === 0 && !isValidating) return ; + let hasBackgroundImage = useHasBackgroundImage(); + return ( -
- {allPosts.map((p) => ( - - ))} - {/* Trigger element for loading more posts */} -