diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index dd84ea78..2190c39e 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -9,6 +9,7 @@ assignees: '' **Describe the bug** A clear and concise description of what the bug is. +- Which site: [e.g. dev.phanpy.social OR phanpy.social] **To Reproduce** Steps to reproduce the behavior: diff --git a/.prettierrc b/.prettierrc index 07e43051..4e525c56 100644 --- a/.prettierrc +++ b/.prettierrc @@ -4,6 +4,7 @@ "singleQuote": true, "trailingComma": "all", "importOrder": [ + "^[^.].*.css$", "index.css$", ".css$", "", diff --git a/package-lock.json b/package-lock.json index 36ae48bd..3ca48a21 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,20 +9,20 @@ "version": "0.1.0", "dependencies": { "@github/text-expander-element": "~2.3.0", - "@iconify-icons/mingcute": "~1.2.3", - "@szhsin/react-menu": "~3.4.0", + "@iconify-icons/mingcute": "~1.2.4", + "@szhsin/react-menu": "~3.4.1", "dayjs": "~1.11.7", "dayjs-twitter": "~0.5.0", "fast-blurhash": "~1.1.2", "fast-deep-equal": "~3.1.3", "idb-keyval": "~6.2.0", "just-debounce-it": "~3.2.0", - "masto": "~5.7.0", + "masto": "~5.10.0", "mem": "~9.0.2", "p-retry": "~5.1.2", - "preact": "~10.11.3", - "react-hotkeys-hook": "~4.3.3", - "react-intersection-observer": "~9.4.1", + "preact": "~10.12.1", + "react-hotkeys-hook": "~4.3.5", + "react-intersection-observer": "~9.4.2", "react-router-dom": "6.6.2", "string-length": "~5.0.1", "swiped-events": "~1.1.7", @@ -30,7 +30,7 @@ "uid": "~2.0.1", "use-debounce": "~9.0.3", "use-resize-observer": "~9.1.0", - "valtio": "~1.9.0" + "valtio": "1.9.0" }, "devDependencies": { "@preact/preset-vite": "~2.5.0", @@ -39,11 +39,11 @@ "postcss-dark-theme-class": "~0.7.3", "postcss-preset-env": "~8.0.1", "twitter-text": "~3.1.0", - "vite": "~4.0.4", + "vite": "~4.1.2", "vite-plugin-html-config": "~1.0.11", "vite-plugin-html-env": "~1.2.7", - "vite-plugin-pwa": "~0.14.1", - "vite-plugin-remove-console": "~1.3.0", + "vite-plugin-pwa": "~0.14.4", + "vite-plugin-remove-console": "~2.0.0", "workbox-cacheable-response": "~6.5.4", "workbox-expiration": "~6.5.4", "workbox-routing": "~6.5.4", @@ -2155,9 +2155,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.16.7.tgz", - "integrity": "sha512-yhzDbiVcmq6T1/XEvdcJIVcXHdLjDJ5cQ0Dp9R9p9ERMBTeO1dR5tc8YYv8zwDeBw1xZm+Eo3MRo8cwclhBS0g==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.16.17.tgz", + "integrity": "sha512-N9x1CMXVhtWEAMS7pNNONyA14f71VPQN9Cnavj1XQh6T7bskqiLLrSca4O0Vr8Wdcga943eThxnVp3JLnBMYtw==", "cpu": [ "arm" ], @@ -2171,9 +2171,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.16.7.tgz", - "integrity": "sha512-tYFw0lBJSEvLoGzzYh1kXuzoX1iPkbOk3O29VqzQb0HbOy7t/yw1hGkvwoJhXHwzQUPsShyYcTgRf6bDBcfnTw==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.16.17.tgz", + "integrity": "sha512-MIGl6p5sc3RDTLLkYL1MyL8BMRN4tLMRCn+yRJJmEDvYZ2M7tmAf80hx1kbNEUX2KJ50RRtxZ4JHLvCfuB6kBg==", "cpu": [ "arm64" ], @@ -2187,9 +2187,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.16.7.tgz", - "integrity": "sha512-3P2OuTxwAtM3k/yEWTNUJRjMPG1ce8rXs51GTtvEC5z1j8fC1plHeVVczdeHECU7aM2/Buc0MwZ6ciM/zysnWg==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.16.17.tgz", + "integrity": "sha512-a3kTv3m0Ghh4z1DaFEuEDfz3OLONKuFvI4Xqczqx4BqLyuFaFkuaG4j2MtA6fuWEFeC5x9IvqnX7drmRq/fyAQ==", "cpu": [ "x64" ], @@ -2203,9 +2203,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.16.7.tgz", - "integrity": "sha512-VUb9GK23z8jkosHU9yJNUgQpsfJn+7ZyBm6adi2Ec5/U241eR1tAn82QicnUzaFDaffeixiHwikjmnec/YXEZg==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.16.17.tgz", + "integrity": "sha512-/2agbUEfmxWHi9ARTX6OQ/KgXnOWfsNlTeLcoV7HSuSTv63E4DqtAc+2XqGw1KHxKMHGZgbVCZge7HXWX9Vn+w==", "cpu": [ "arm64" ], @@ -2219,9 +2219,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.16.7.tgz", - "integrity": "sha512-duterlv3tit3HI9vhzMWnSVaB1B6YsXpFq1Ntd6Fou82BB1l4tucYy3FI9dHv3tvtDuS0NiGf/k6XsdBqPZ01w==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.16.17.tgz", + "integrity": "sha512-2By45OBHulkd9Svy5IOCZt376Aa2oOkiE9QWUK9fe6Tb+WDr8hXL3dpqi+DeLiMed8tVXspzsTAvd0jUl96wmg==", "cpu": [ "x64" ], @@ -2235,9 +2235,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.16.7.tgz", - "integrity": "sha512-9kkycpBFes/vhi7B7o0cf+q2WdJi+EpVzpVTqtWFNiutARWDFFLcB93J8PR1cG228sucsl3B+7Ts27izE6qiaQ==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.16.17.tgz", + "integrity": "sha512-mt+cxZe1tVx489VTb4mBAOo2aKSnJ33L9fr25JXpqQqzbUIw/yzIzi+NHwAXK2qYV1lEFp4OoVeThGjUbmWmdw==", "cpu": [ "arm64" ], @@ -2251,9 +2251,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.16.7.tgz", - "integrity": "sha512-5Ahf6jzWXJ4J2uh9dpy5DKOO+PeRUE/9DMys6VuYfwgQzd6n5+pVFm58L2Z2gRe611RX6SdydnNaiIKM3svY7g==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.16.17.tgz", + "integrity": "sha512-8ScTdNJl5idAKjH8zGAsN7RuWcyHG3BAvMNpKOBaqqR7EbUhhVHOqXRdL7oZvz8WNHL2pr5+eIT5c65kA6NHug==", "cpu": [ "x64" ], @@ -2267,9 +2267,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.16.7.tgz", - "integrity": "sha512-QqJnyCfu5OF78Olt7JJSZ7OSv/B4Hf+ZJWp4kkq9xwMsgu7yWq3crIic8gGOpDYTqVKKMDAVDgRXy5Wd/nWZyQ==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.16.17.tgz", + "integrity": "sha512-iihzrWbD4gIT7j3caMzKb/RsFFHCwqqbrbH9SqUSRrdXkXaygSZCZg1FybsZz57Ju7N/SHEgPyaR0LZ8Zbe9gQ==", "cpu": [ "arm" ], @@ -2283,9 +2283,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.16.7.tgz", - "integrity": "sha512-2wv0xYDskk2+MzIm/AEprDip39a23Chptc4mL7hsHg26P0gD8RUhzmDu0KCH2vMThUI1sChXXoK9uH0KYQKaDg==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.16.17.tgz", + "integrity": "sha512-7S8gJnSlqKGVJunnMCrXHU9Q8Q/tQIxk/xL8BqAP64wchPCTzuM6W3Ra8cIa1HIflAvDnNOt2jaL17vaW+1V0g==", "cpu": [ "arm64" ], @@ -2299,9 +2299,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.16.7.tgz", - "integrity": "sha512-APVYbEilKbD5ptmKdnIcXej2/+GdV65TfTjxR2Uk8t1EsOk49t6HapZW6DS/Bwlvh5hDwtLapdSumIVNGxgqLg==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.16.17.tgz", + "integrity": "sha512-kiX69+wcPAdgl3Lonh1VI7MBr16nktEvOfViszBSxygRQqSpzv7BffMKRPMFwzeJGPxcio0pdD3kYQGpqQ2SSg==", "cpu": [ "ia32" ], @@ -2315,9 +2315,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.16.7.tgz", - "integrity": "sha512-5wPUAGclplQrAW7EFr3F84Y/d++7G0KykohaF4p54+iNWhUnMVU8Bh2sxiEOXUy4zKIdpHByMgJ5/Ko6QhtTUw==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.16.17.tgz", + "integrity": "sha512-dTzNnQwembNDhd654cA4QhbS9uDdXC3TKqMJjgOWsC0yNCbpzfWoXdZvp0mY7HU6nzk5E0zpRGGx3qoQg8T2DQ==", "cpu": [ "loong64" ], @@ -2331,9 +2331,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.16.7.tgz", - "integrity": "sha512-hxzlXtWF6yWfkE/SMTscNiVqLOAn7fOuIF3q/kiZaXxftz1DhZW/HpnTmTTWrzrS7zJWQxHHT4QSxyAj33COmA==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.16.17.tgz", + "integrity": "sha512-ezbDkp2nDl0PfIUn0CsQ30kxfcLTlcx4Foz2kYv8qdC6ia2oX5Q3E/8m6lq84Dj/6b0FrkgD582fJMIfHhJfSw==", "cpu": [ "mips64el" ], @@ -2347,9 +2347,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.16.7.tgz", - "integrity": "sha512-WM83Dac0LdXty5xPhlOuCD5Egfk1xLND/oRLYeB7Jb/tY4kzFSDgLlq91wYbHua/s03tQGA9iXvyjgymMw62Vw==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.16.17.tgz", + "integrity": "sha512-dzS678gYD1lJsW73zrFhDApLVdM3cUF2MvAa1D8K8KtcSKdLBPP4zZSLy6LFZ0jYqQdQ29bjAHJDgz0rVbLB3g==", "cpu": [ "ppc64" ], @@ -2363,9 +2363,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.16.7.tgz", - "integrity": "sha512-3nkNnNg4Ax6MS/l8O8Ynq2lGEVJYyJ2EoY3PHjNJ4PuZ80EYLMrFTFZ4L/Hc16AxgtXKwmNP9TM0YKNiBzBiJQ==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.16.17.tgz", + "integrity": "sha512-ylNlVsxuFjZK8DQtNUwiMskh6nT0vI7kYl/4fZgV1llP5d6+HIeL/vmmm3jpuoo8+NuXjQVZxmKuhDApK0/cKw==", "cpu": [ "riscv64" ], @@ -2379,9 +2379,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.16.7.tgz", - "integrity": "sha512-3SA/2VJuv0o1uD7zuqxEP+RrAyRxnkGddq0bwHQ98v1KNlzXD/JvxwTO3T6GM5RH6JUd29RTVQTOJfyzMkkppA==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.16.17.tgz", + "integrity": "sha512-gzy7nUTO4UA4oZ2wAMXPNBGTzZFP7mss3aKR2hH+/4UUkCOyqmjXiKpzGrY2TlEUhbbejzXVKKGazYcQTZWA/w==", "cpu": [ "s390x" ], @@ -2395,9 +2395,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.16.7.tgz", - "integrity": "sha512-xi/tbqCqvPIzU+zJVyrpz12xqciTAPMi2fXEWGnapZymoGhuL2GIWIRXg4O2v5BXaYA5TSaiKYE14L0QhUTuQg==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.16.17.tgz", + "integrity": "sha512-mdPjPxfnmoqhgpiEArqi4egmBAMYvaObgn4poorpUaqmvzzbvqbowRllQ+ZgzGVMGKaPkqUmPDOOFQRUFDmeUw==", "cpu": [ "x64" ], @@ -2411,9 +2411,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.16.7.tgz", - "integrity": "sha512-NUsYbq3B+JdNKn8SXkItFvdes9qTwEoS3aLALtiWciW/ystiCKM20Fgv9XQBOXfhUHyh5CLEeZDXzLOrwBXuCQ==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.16.17.tgz", + "integrity": "sha512-/PzmzD/zyAeTUsduZa32bn0ORug+Jd1EGGAUJvqfeixoEISYpGnAezN6lnJoskauoai0Jrs+XSyvDhppCPoKOA==", "cpu": [ "x64" ], @@ -2427,9 +2427,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.16.7.tgz", - "integrity": "sha512-qjwzsgeve9I8Tbsko2FEkdSk2iiezuNGFgipQxY/736NePXDaDZRodIejYGWOlbYXugdxb0nif5yvypH6lKBmA==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.16.17.tgz", + "integrity": "sha512-2yaWJhvxGEz2RiftSk0UObqJa/b+rIAjnODJgv2GbGGpRwAfpgzyrg1WLK8rqA24mfZa9GvpjLcBBg8JHkoodg==", "cpu": [ "x64" ], @@ -2443,9 +2443,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.16.7.tgz", - "integrity": "sha512-mFWDz4RoBTzPphTCkM7Kc7Qpa0o/Z01acajR+Ai7LdfKgcP/C6jYOaKwv7nKzD0+MjOT20j7You9g4ozYy1dKQ==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.16.17.tgz", + "integrity": "sha512-xtVUiev38tN0R3g8VhRfN7Zl42YCJvyBhRKw1RJjwE1d2emWTVToPLNEQj/5Qxc6lVFATDiy6LjVHYhIPrLxzw==", "cpu": [ "x64" ], @@ -2459,9 +2459,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.16.7.tgz", - "integrity": "sha512-m39UmX19RvEIuC8sYZ0M+eQtdXw4IePDSZ78ZQmYyFaXY9krq4YzQCK2XWIJomNLtg4q+W5aXr8bW3AbqWNoVg==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.16.17.tgz", + "integrity": "sha512-ga8+JqBDHY4b6fQAmOgtJJue36scANy4l/rL97W+0wYmijhxKetzZdKOJI7olaBaMhWt8Pac2McJdZLxXWUEQw==", "cpu": [ "arm64" ], @@ -2475,9 +2475,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.16.7.tgz", - "integrity": "sha512-1cbzSEZA1fANwmT6rjJ4G1qQXHxCxGIcNYFYR9ctI82/prT38lnwSRZ0i5p/MVXksw9eMlHlet6pGu2/qkXFCg==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.16.17.tgz", + "integrity": "sha512-WnsKaf46uSSF/sZhwnqE4L/F89AYNMiD4YtEcYekBt9Q7nj0DiId2XH2Ng2PHM54qi5oPrQ8luuzGszqi/veig==", "cpu": [ "ia32" ], @@ -2491,9 +2491,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.16.7.tgz", - "integrity": "sha512-QaQ8IH0JLacfGf5cf0HCCPnQuCTd/dAI257vXBgb/cccKGbH/6pVtI1gwhdAQ0Y48QSpTIFrh9etVyNdZY+zzw==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.16.17.tgz", + "integrity": "sha512-y+EHuSchhL7FjHgvQL/0fnnFmO4T1bhvWANX6gcnqTjtnKWbTvUMCpGnv2+t+31d7RzyEAYAd4u2fnIhHL6N/Q==", "cpu": [ "x64" ], @@ -2520,9 +2520,9 @@ } }, "node_modules/@iconify-icons/mingcute": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@iconify-icons/mingcute/-/mingcute-1.2.3.tgz", - "integrity": "sha512-yZyioZhNy61SkLxQoyHThsfuyaOej9n84PUS+K69qaS1Dyj7/wHwYhWXseFCnzyzicaEHkCpt6H/hYV8fwmMLg==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@iconify-icons/mingcute/-/mingcute-1.2.4.tgz", + "integrity": "sha512-4aaWYa6GxSdYmJg8iBVx6VDuKUcTDEbio929+GrswoxfyTsPUkOOgw2wffUDHjE3JDUAnrWj9teQTnBkFm7Gyg==", "dependencies": { "@iconify/types": "*" } @@ -2810,9 +2810,9 @@ } }, "node_modules/@szhsin/react-menu": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@szhsin/react-menu/-/react-menu-3.4.0.tgz", - "integrity": "sha512-BRxUF3BNmaQzL1z8UkS/eNFj+5ZPjBOpsYWP/ZtrzHNkkP6hUc+tYerBV4dwFiGYmWzkxyOP44ISn+EujwPpUw==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@szhsin/react-menu/-/react-menu-3.4.1.tgz", + "integrity": "sha512-Pxt7Kyp3yuX7zkT5tjdLRJGNFMa5Tx4BP+01gJ/dnMmHQpI1H2or9gEC0X+t3cLldO3LGmm4ViGypNCmQLv/4A==", "dependencies": { "prop-types": "^15.7.2", "react-transition-state": "^1.1.5" @@ -3719,9 +3719,9 @@ } }, "node_modules/esbuild": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.16.7.tgz", - "integrity": "sha512-P6OBFYFSQOGzfApqCeYKqfKRRbCIRsdppTXFo4aAvtiW3o8TTyiIplBvHJI171saPAiy3WlawJHCveJVIOIx1A==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.16.17.tgz", + "integrity": "sha512-G8LEkV0XzDMNwXKgM0Jwu3nY3lSTwSGY6XbxM9cr9+s0T/qSV1q1JVPBGzm3dcjhCic9+emZDmMffkwgPeOeLg==", "dev": true, "hasInstallScript": true, "bin": { @@ -3731,28 +3731,28 @@ "node": ">=12" }, "optionalDependencies": { - "@esbuild/android-arm": "0.16.7", - "@esbuild/android-arm64": "0.16.7", - "@esbuild/android-x64": "0.16.7", - "@esbuild/darwin-arm64": "0.16.7", - "@esbuild/darwin-x64": "0.16.7", - "@esbuild/freebsd-arm64": "0.16.7", - "@esbuild/freebsd-x64": "0.16.7", - "@esbuild/linux-arm": "0.16.7", - "@esbuild/linux-arm64": "0.16.7", - "@esbuild/linux-ia32": "0.16.7", - "@esbuild/linux-loong64": "0.16.7", - "@esbuild/linux-mips64el": "0.16.7", - "@esbuild/linux-ppc64": "0.16.7", - "@esbuild/linux-riscv64": "0.16.7", - "@esbuild/linux-s390x": "0.16.7", - "@esbuild/linux-x64": "0.16.7", - "@esbuild/netbsd-x64": "0.16.7", - "@esbuild/openbsd-x64": "0.16.7", - "@esbuild/sunos-x64": "0.16.7", - "@esbuild/win32-arm64": "0.16.7", - "@esbuild/win32-ia32": "0.16.7", - "@esbuild/win32-x64": "0.16.7" + "@esbuild/android-arm": "0.16.17", + "@esbuild/android-arm64": "0.16.17", + "@esbuild/android-x64": "0.16.17", + "@esbuild/darwin-arm64": "0.16.17", + "@esbuild/darwin-x64": "0.16.17", + "@esbuild/freebsd-arm64": "0.16.17", + "@esbuild/freebsd-x64": "0.16.17", + "@esbuild/linux-arm": "0.16.17", + "@esbuild/linux-arm64": "0.16.17", + "@esbuild/linux-ia32": "0.16.17", + "@esbuild/linux-loong64": "0.16.17", + "@esbuild/linux-mips64el": "0.16.17", + "@esbuild/linux-ppc64": "0.16.17", + "@esbuild/linux-riscv64": "0.16.17", + "@esbuild/linux-s390x": "0.16.17", + "@esbuild/linux-x64": "0.16.17", + "@esbuild/netbsd-x64": "0.16.17", + "@esbuild/openbsd-x64": "0.16.17", + "@esbuild/sunos-x64": "0.16.17", + "@esbuild/win32-arm64": "0.16.17", + "@esbuild/win32-ia32": "0.16.17", + "@esbuild/win32-x64": "0.16.17" } }, "node_modules/escalade": { @@ -4718,9 +4718,9 @@ } }, "node_modules/masto": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/masto/-/masto-5.7.0.tgz", - "integrity": "sha512-oCVReGLR9AoBSkShVgWrPb69xzfCpogiPH9wVT9AnPbM+CvjIqJuSaD1xgaEUi6jo66XGbhTC6UerSCVqq9emg==", + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/masto/-/masto-5.10.0.tgz", + "integrity": "sha512-RlTw3X2b2ipkcgsgoKEWKKFNYkpAlUtJhNOFKwBKWEBv+we/ZupQbnerGOJssB5rs7ig4HWWsZZHLtNeFdYQTQ==", "dependencies": { "@mastojs/ponyfills": "^1.0.4", "change-case": "^4.1.2", @@ -5650,9 +5650,9 @@ "dev": true }, "node_modules/preact": { - "version": "10.11.3", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.11.3.tgz", - "integrity": "sha512-eY93IVpod/zG3uMF22Unl8h9KkrcKIRs2EGar8hwLZZDU1lkjph303V9HZBwufh2s736U6VXuhD109LYqPoffg==", + "version": "10.12.1", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.12.1.tgz", + "integrity": "sha512-l8386ixSsBdbreOAkqtrwqHwdvR35ID8c3rKPa8lCWuO86dBi32QWHV4vfsZK1utLLFMvw+Z5Ad4XLkZzchscg==", "funding": { "type": "opencollective", "url": "https://opencollective.com/preact" @@ -5779,18 +5779,18 @@ } }, "node_modules/react-hotkeys-hook": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/react-hotkeys-hook/-/react-hotkeys-hook-4.3.3.tgz", - "integrity": "sha512-OYZCG2G+xLeiH0TkrW+v6eKFPvYq9iCA5sh9pwvnbGQaK86Lw/kWJDjjzSksFJoCk4K78OLe3MR1afNZH6+cLg==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/react-hotkeys-hook/-/react-hotkeys-hook-4.3.5.tgz", + "integrity": "sha512-tfwTwKP3ga7n4naNS/JOByaEwEkTCoXYCepDuhXpj8mBx+sFszV5JecRWM2dv+PbOowmmBpHAFtTXTnG/p8UkQ==", "peerDependencies": { "react": ">=16.8.1", "react-dom": ">=16.8.1" } }, "node_modules/react-intersection-observer": { - "version": "9.4.1", - "resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-9.4.1.tgz", - "integrity": "sha512-IXpIsPe6BleFOEHKzKh5UjwRUaz/JYS0lT/HPsupWEQou2hDqjhLMStc5zyE3eQVT4Fk3FufM8Fw33qW1uyeiw==", + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-9.4.2.tgz", + "integrity": "sha512-AdK+ryzZ7U9ZJYttDUZ8q2Am3nqE0exg5Ryl5Y124KeVsix/1hGZPbdu58EqA98TwnzwDNWHxg/kwNawmIiUig==", "peerDependencies": { "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" } @@ -5978,9 +5978,9 @@ } }, "node_modules/rollup": { - "version": "3.7.4", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.7.4.tgz", - "integrity": "sha512-jN9rx3k5pfg9H9al0r0y1EYKSeiRANZRYX32SuNXAnKzh6cVyf4LZVto1KAuDnbHT03E1CpsgqDKaqQ8FZtgxw==", + "version": "3.12.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.12.1.tgz", + "integrity": "sha512-t9elERrz2i4UU9z7AwISj3CQcXP39cWxgRWLdf4Tm6aKm1eYrqHIgjzXBgb67GNY1sZckTFFi0oMozh3/S++Ig==", "dev": true, "bin": { "rollup": "dist/bin/rollup" @@ -6603,15 +6603,15 @@ } }, "node_modules/vite": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.0.4.tgz", - "integrity": "sha512-xevPU7M8FU0i/80DMR+YhgrzR5KS2ORy1B4xcX/cXLsvnUWvfHuqMmVU6N0YiJ4JWGRJJsLCgjEzKjG9/GKoSw==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.1.2.tgz", + "integrity": "sha512-MWDb9Rfy3DI8omDQySbMK93nQqStwbsQWejXRY2EBzEWKmLAXWb1mkI9Yw2IJrc+oCvPCI1Os5xSSIBYY6DEAw==", "dev": true, "dependencies": { - "esbuild": "^0.16.3", - "postcss": "^8.4.20", + "esbuild": "^0.16.14", + "postcss": "^8.4.21", "resolve": "^1.22.1", - "rollup": "^3.7.0" + "rollup": "^3.10.0" }, "bin": { "vite": "bin/vite.js" @@ -6676,9 +6676,9 @@ } }, "node_modules/vite-plugin-pwa": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.14.1.tgz", - "integrity": "sha512-5zx7yhQ8RTLwV71+GA9YsQQ63ALKG8XXIMqRJDdZkR8ZYftFcRgnzM7wOWmQZ/DATspyhPih5wCdcZnAIsM+mA==", + "version": "0.14.4", + "resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.14.4.tgz", + "integrity": "sha512-M7Ct0so8OlouMkTWgXnl8W1xU95glITSKIe7qswZf1tniAstO2idElGCnsrTJ5NPNSx1XqfTCOUj8j94S6FD7Q==", "dev": true, "dependencies": { "@rollup/plugin-replace": "^5.0.1", @@ -6699,9 +6699,9 @@ } }, "node_modules/vite-plugin-remove-console": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/vite-plugin-remove-console/-/vite-plugin-remove-console-1.3.0.tgz", - "integrity": "sha512-5a/OLYB6yNRHMuHj9rBQRYMQ1NBKffxA8BaD77urUBLcGOWMHFHALjh6C26wZfZd41KytSwLp6DhvNKU78mNJg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/vite-plugin-remove-console/-/vite-plugin-remove-console-2.0.0.tgz", + "integrity": "sha512-bEsyShSacsunbm0X1zaVliwgmWlsaBPLk7FN4wr2xQMs8zSZPSwpRNTT5UZiF0+cfMEkN4VVnofITawmT3pjgQ==", "dev": true }, "node_modules/webidl-conversions": { @@ -8460,156 +8460,156 @@ "requires": {} }, "@esbuild/android-arm": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.16.7.tgz", - "integrity": "sha512-yhzDbiVcmq6T1/XEvdcJIVcXHdLjDJ5cQ0Dp9R9p9ERMBTeO1dR5tc8YYv8zwDeBw1xZm+Eo3MRo8cwclhBS0g==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.16.17.tgz", + "integrity": "sha512-N9x1CMXVhtWEAMS7pNNONyA14f71VPQN9Cnavj1XQh6T7bskqiLLrSca4O0Vr8Wdcga943eThxnVp3JLnBMYtw==", "dev": true, "optional": true }, "@esbuild/android-arm64": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.16.7.tgz", - "integrity": "sha512-tYFw0lBJSEvLoGzzYh1kXuzoX1iPkbOk3O29VqzQb0HbOy7t/yw1hGkvwoJhXHwzQUPsShyYcTgRf6bDBcfnTw==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.16.17.tgz", + "integrity": "sha512-MIGl6p5sc3RDTLLkYL1MyL8BMRN4tLMRCn+yRJJmEDvYZ2M7tmAf80hx1kbNEUX2KJ50RRtxZ4JHLvCfuB6kBg==", "dev": true, "optional": true }, "@esbuild/android-x64": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.16.7.tgz", - "integrity": "sha512-3P2OuTxwAtM3k/yEWTNUJRjMPG1ce8rXs51GTtvEC5z1j8fC1plHeVVczdeHECU7aM2/Buc0MwZ6ciM/zysnWg==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.16.17.tgz", + "integrity": "sha512-a3kTv3m0Ghh4z1DaFEuEDfz3OLONKuFvI4Xqczqx4BqLyuFaFkuaG4j2MtA6fuWEFeC5x9IvqnX7drmRq/fyAQ==", "dev": true, "optional": true }, "@esbuild/darwin-arm64": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.16.7.tgz", - "integrity": "sha512-VUb9GK23z8jkosHU9yJNUgQpsfJn+7ZyBm6adi2Ec5/U241eR1tAn82QicnUzaFDaffeixiHwikjmnec/YXEZg==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.16.17.tgz", + "integrity": "sha512-/2agbUEfmxWHi9ARTX6OQ/KgXnOWfsNlTeLcoV7HSuSTv63E4DqtAc+2XqGw1KHxKMHGZgbVCZge7HXWX9Vn+w==", "dev": true, "optional": true }, "@esbuild/darwin-x64": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.16.7.tgz", - "integrity": "sha512-duterlv3tit3HI9vhzMWnSVaB1B6YsXpFq1Ntd6Fou82BB1l4tucYy3FI9dHv3tvtDuS0NiGf/k6XsdBqPZ01w==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.16.17.tgz", + "integrity": "sha512-2By45OBHulkd9Svy5IOCZt376Aa2oOkiE9QWUK9fe6Tb+WDr8hXL3dpqi+DeLiMed8tVXspzsTAvd0jUl96wmg==", "dev": true, "optional": true }, "@esbuild/freebsd-arm64": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.16.7.tgz", - "integrity": "sha512-9kkycpBFes/vhi7B7o0cf+q2WdJi+EpVzpVTqtWFNiutARWDFFLcB93J8PR1cG228sucsl3B+7Ts27izE6qiaQ==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.16.17.tgz", + "integrity": "sha512-mt+cxZe1tVx489VTb4mBAOo2aKSnJ33L9fr25JXpqQqzbUIw/yzIzi+NHwAXK2qYV1lEFp4OoVeThGjUbmWmdw==", "dev": true, "optional": true }, "@esbuild/freebsd-x64": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.16.7.tgz", - "integrity": "sha512-5Ahf6jzWXJ4J2uh9dpy5DKOO+PeRUE/9DMys6VuYfwgQzd6n5+pVFm58L2Z2gRe611RX6SdydnNaiIKM3svY7g==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.16.17.tgz", + "integrity": "sha512-8ScTdNJl5idAKjH8zGAsN7RuWcyHG3BAvMNpKOBaqqR7EbUhhVHOqXRdL7oZvz8WNHL2pr5+eIT5c65kA6NHug==", "dev": true, "optional": true }, "@esbuild/linux-arm": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.16.7.tgz", - "integrity": "sha512-QqJnyCfu5OF78Olt7JJSZ7OSv/B4Hf+ZJWp4kkq9xwMsgu7yWq3crIic8gGOpDYTqVKKMDAVDgRXy5Wd/nWZyQ==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.16.17.tgz", + "integrity": "sha512-iihzrWbD4gIT7j3caMzKb/RsFFHCwqqbrbH9SqUSRrdXkXaygSZCZg1FybsZz57Ju7N/SHEgPyaR0LZ8Zbe9gQ==", "dev": true, "optional": true }, "@esbuild/linux-arm64": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.16.7.tgz", - "integrity": "sha512-2wv0xYDskk2+MzIm/AEprDip39a23Chptc4mL7hsHg26P0gD8RUhzmDu0KCH2vMThUI1sChXXoK9uH0KYQKaDg==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.16.17.tgz", + "integrity": "sha512-7S8gJnSlqKGVJunnMCrXHU9Q8Q/tQIxk/xL8BqAP64wchPCTzuM6W3Ra8cIa1HIflAvDnNOt2jaL17vaW+1V0g==", "dev": true, "optional": true }, "@esbuild/linux-ia32": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.16.7.tgz", - "integrity": "sha512-APVYbEilKbD5ptmKdnIcXej2/+GdV65TfTjxR2Uk8t1EsOk49t6HapZW6DS/Bwlvh5hDwtLapdSumIVNGxgqLg==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.16.17.tgz", + "integrity": "sha512-kiX69+wcPAdgl3Lonh1VI7MBr16nktEvOfViszBSxygRQqSpzv7BffMKRPMFwzeJGPxcio0pdD3kYQGpqQ2SSg==", "dev": true, "optional": true }, "@esbuild/linux-loong64": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.16.7.tgz", - "integrity": "sha512-5wPUAGclplQrAW7EFr3F84Y/d++7G0KykohaF4p54+iNWhUnMVU8Bh2sxiEOXUy4zKIdpHByMgJ5/Ko6QhtTUw==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.16.17.tgz", + "integrity": "sha512-dTzNnQwembNDhd654cA4QhbS9uDdXC3TKqMJjgOWsC0yNCbpzfWoXdZvp0mY7HU6nzk5E0zpRGGx3qoQg8T2DQ==", "dev": true, "optional": true }, "@esbuild/linux-mips64el": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.16.7.tgz", - "integrity": "sha512-hxzlXtWF6yWfkE/SMTscNiVqLOAn7fOuIF3q/kiZaXxftz1DhZW/HpnTmTTWrzrS7zJWQxHHT4QSxyAj33COmA==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.16.17.tgz", + "integrity": "sha512-ezbDkp2nDl0PfIUn0CsQ30kxfcLTlcx4Foz2kYv8qdC6ia2oX5Q3E/8m6lq84Dj/6b0FrkgD582fJMIfHhJfSw==", "dev": true, "optional": true }, "@esbuild/linux-ppc64": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.16.7.tgz", - "integrity": "sha512-WM83Dac0LdXty5xPhlOuCD5Egfk1xLND/oRLYeB7Jb/tY4kzFSDgLlq91wYbHua/s03tQGA9iXvyjgymMw62Vw==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.16.17.tgz", + "integrity": "sha512-dzS678gYD1lJsW73zrFhDApLVdM3cUF2MvAa1D8K8KtcSKdLBPP4zZSLy6LFZ0jYqQdQ29bjAHJDgz0rVbLB3g==", "dev": true, "optional": true }, "@esbuild/linux-riscv64": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.16.7.tgz", - "integrity": "sha512-3nkNnNg4Ax6MS/l8O8Ynq2lGEVJYyJ2EoY3PHjNJ4PuZ80EYLMrFTFZ4L/Hc16AxgtXKwmNP9TM0YKNiBzBiJQ==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.16.17.tgz", + "integrity": "sha512-ylNlVsxuFjZK8DQtNUwiMskh6nT0vI7kYl/4fZgV1llP5d6+HIeL/vmmm3jpuoo8+NuXjQVZxmKuhDApK0/cKw==", "dev": true, "optional": true }, "@esbuild/linux-s390x": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.16.7.tgz", - "integrity": "sha512-3SA/2VJuv0o1uD7zuqxEP+RrAyRxnkGddq0bwHQ98v1KNlzXD/JvxwTO3T6GM5RH6JUd29RTVQTOJfyzMkkppA==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.16.17.tgz", + "integrity": "sha512-gzy7nUTO4UA4oZ2wAMXPNBGTzZFP7mss3aKR2hH+/4UUkCOyqmjXiKpzGrY2TlEUhbbejzXVKKGazYcQTZWA/w==", "dev": true, "optional": true }, "@esbuild/linux-x64": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.16.7.tgz", - "integrity": "sha512-xi/tbqCqvPIzU+zJVyrpz12xqciTAPMi2fXEWGnapZymoGhuL2GIWIRXg4O2v5BXaYA5TSaiKYE14L0QhUTuQg==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.16.17.tgz", + "integrity": "sha512-mdPjPxfnmoqhgpiEArqi4egmBAMYvaObgn4poorpUaqmvzzbvqbowRllQ+ZgzGVMGKaPkqUmPDOOFQRUFDmeUw==", "dev": true, "optional": true }, "@esbuild/netbsd-x64": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.16.7.tgz", - "integrity": "sha512-NUsYbq3B+JdNKn8SXkItFvdes9qTwEoS3aLALtiWciW/ystiCKM20Fgv9XQBOXfhUHyh5CLEeZDXzLOrwBXuCQ==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.16.17.tgz", + "integrity": "sha512-/PzmzD/zyAeTUsduZa32bn0ORug+Jd1EGGAUJvqfeixoEISYpGnAezN6lnJoskauoai0Jrs+XSyvDhppCPoKOA==", "dev": true, "optional": true }, "@esbuild/openbsd-x64": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.16.7.tgz", - "integrity": "sha512-qjwzsgeve9I8Tbsko2FEkdSk2iiezuNGFgipQxY/736NePXDaDZRodIejYGWOlbYXugdxb0nif5yvypH6lKBmA==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.16.17.tgz", + "integrity": "sha512-2yaWJhvxGEz2RiftSk0UObqJa/b+rIAjnODJgv2GbGGpRwAfpgzyrg1WLK8rqA24mfZa9GvpjLcBBg8JHkoodg==", "dev": true, "optional": true }, "@esbuild/sunos-x64": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.16.7.tgz", - "integrity": "sha512-mFWDz4RoBTzPphTCkM7Kc7Qpa0o/Z01acajR+Ai7LdfKgcP/C6jYOaKwv7nKzD0+MjOT20j7You9g4ozYy1dKQ==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.16.17.tgz", + "integrity": "sha512-xtVUiev38tN0R3g8VhRfN7Zl42YCJvyBhRKw1RJjwE1d2emWTVToPLNEQj/5Qxc6lVFATDiy6LjVHYhIPrLxzw==", "dev": true, "optional": true }, "@esbuild/win32-arm64": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.16.7.tgz", - "integrity": "sha512-m39UmX19RvEIuC8sYZ0M+eQtdXw4IePDSZ78ZQmYyFaXY9krq4YzQCK2XWIJomNLtg4q+W5aXr8bW3AbqWNoVg==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.16.17.tgz", + "integrity": "sha512-ga8+JqBDHY4b6fQAmOgtJJue36scANy4l/rL97W+0wYmijhxKetzZdKOJI7olaBaMhWt8Pac2McJdZLxXWUEQw==", "dev": true, "optional": true }, "@esbuild/win32-ia32": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.16.7.tgz", - "integrity": "sha512-1cbzSEZA1fANwmT6rjJ4G1qQXHxCxGIcNYFYR9ctI82/prT38lnwSRZ0i5p/MVXksw9eMlHlet6pGu2/qkXFCg==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.16.17.tgz", + "integrity": "sha512-WnsKaf46uSSF/sZhwnqE4L/F89AYNMiD4YtEcYekBt9Q7nj0DiId2XH2Ng2PHM54qi5oPrQ8luuzGszqi/veig==", "dev": true, "optional": true }, "@esbuild/win32-x64": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.16.7.tgz", - "integrity": "sha512-QaQ8IH0JLacfGf5cf0HCCPnQuCTd/dAI257vXBgb/cccKGbH/6pVtI1gwhdAQ0Y48QSpTIFrh9etVyNdZY+zzw==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.16.17.tgz", + "integrity": "sha512-y+EHuSchhL7FjHgvQL/0fnnFmO4T1bhvWANX6gcnqTjtnKWbTvUMCpGnv2+t+31d7RzyEAYAd4u2fnIhHL6N/Q==", "dev": true, "optional": true }, @@ -8627,9 +8627,9 @@ } }, "@iconify-icons/mingcute": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@iconify-icons/mingcute/-/mingcute-1.2.3.tgz", - "integrity": "sha512-yZyioZhNy61SkLxQoyHThsfuyaOej9n84PUS+K69qaS1Dyj7/wHwYhWXseFCnzyzicaEHkCpt6H/hYV8fwmMLg==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@iconify-icons/mingcute/-/mingcute-1.2.4.tgz", + "integrity": "sha512-4aaWYa6GxSdYmJg8iBVx6VDuKUcTDEbio929+GrswoxfyTsPUkOOgw2wffUDHjE3JDUAnrWj9teQTnBkFm7Gyg==", "requires": { "@iconify/types": "*" } @@ -8856,9 +8856,9 @@ } }, "@szhsin/react-menu": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@szhsin/react-menu/-/react-menu-3.4.0.tgz", - "integrity": "sha512-BRxUF3BNmaQzL1z8UkS/eNFj+5ZPjBOpsYWP/ZtrzHNkkP6hUc+tYerBV4dwFiGYmWzkxyOP44ISn+EujwPpUw==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@szhsin/react-menu/-/react-menu-3.4.1.tgz", + "integrity": "sha512-Pxt7Kyp3yuX7zkT5tjdLRJGNFMa5Tx4BP+01gJ/dnMmHQpI1H2or9gEC0X+t3cLldO3LGmm4ViGypNCmQLv/4A==", "requires": { "prop-types": "^15.7.2", "react-transition-state": "^1.1.5" @@ -9542,33 +9542,33 @@ } }, "esbuild": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.16.7.tgz", - "integrity": "sha512-P6OBFYFSQOGzfApqCeYKqfKRRbCIRsdppTXFo4aAvtiW3o8TTyiIplBvHJI171saPAiy3WlawJHCveJVIOIx1A==", + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.16.17.tgz", + "integrity": "sha512-G8LEkV0XzDMNwXKgM0Jwu3nY3lSTwSGY6XbxM9cr9+s0T/qSV1q1JVPBGzm3dcjhCic9+emZDmMffkwgPeOeLg==", "dev": true, "requires": { - "@esbuild/android-arm": "0.16.7", - "@esbuild/android-arm64": "0.16.7", - "@esbuild/android-x64": "0.16.7", - "@esbuild/darwin-arm64": "0.16.7", - "@esbuild/darwin-x64": "0.16.7", - "@esbuild/freebsd-arm64": "0.16.7", - "@esbuild/freebsd-x64": "0.16.7", - "@esbuild/linux-arm": "0.16.7", - "@esbuild/linux-arm64": "0.16.7", - "@esbuild/linux-ia32": "0.16.7", - "@esbuild/linux-loong64": "0.16.7", - "@esbuild/linux-mips64el": "0.16.7", - "@esbuild/linux-ppc64": "0.16.7", - "@esbuild/linux-riscv64": "0.16.7", - "@esbuild/linux-s390x": "0.16.7", - "@esbuild/linux-x64": "0.16.7", - "@esbuild/netbsd-x64": "0.16.7", - "@esbuild/openbsd-x64": "0.16.7", - "@esbuild/sunos-x64": "0.16.7", - "@esbuild/win32-arm64": "0.16.7", - "@esbuild/win32-ia32": "0.16.7", - "@esbuild/win32-x64": "0.16.7" + "@esbuild/android-arm": "0.16.17", + "@esbuild/android-arm64": "0.16.17", + "@esbuild/android-x64": "0.16.17", + "@esbuild/darwin-arm64": "0.16.17", + "@esbuild/darwin-x64": "0.16.17", + "@esbuild/freebsd-arm64": "0.16.17", + "@esbuild/freebsd-x64": "0.16.17", + "@esbuild/linux-arm": "0.16.17", + "@esbuild/linux-arm64": "0.16.17", + "@esbuild/linux-ia32": "0.16.17", + "@esbuild/linux-loong64": "0.16.17", + "@esbuild/linux-mips64el": "0.16.17", + "@esbuild/linux-ppc64": "0.16.17", + "@esbuild/linux-riscv64": "0.16.17", + "@esbuild/linux-s390x": "0.16.17", + "@esbuild/linux-x64": "0.16.17", + "@esbuild/netbsd-x64": "0.16.17", + "@esbuild/openbsd-x64": "0.16.17", + "@esbuild/sunos-x64": "0.16.17", + "@esbuild/win32-arm64": "0.16.17", + "@esbuild/win32-ia32": "0.16.17", + "@esbuild/win32-x64": "0.16.17" } }, "escalade": { @@ -10288,9 +10288,9 @@ } }, "masto": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/masto/-/masto-5.7.0.tgz", - "integrity": "sha512-oCVReGLR9AoBSkShVgWrPb69xzfCpogiPH9wVT9AnPbM+CvjIqJuSaD1xgaEUi6jo66XGbhTC6UerSCVqq9emg==", + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/masto/-/masto-5.10.0.tgz", + "integrity": "sha512-RlTw3X2b2ipkcgsgoKEWKKFNYkpAlUtJhNOFKwBKWEBv+we/ZupQbnerGOJssB5rs7ig4HWWsZZHLtNeFdYQTQ==", "requires": { "@mastojs/ponyfills": "^1.0.4", "change-case": "^4.1.2", @@ -10870,9 +10870,9 @@ "dev": true }, "preact": { - "version": "10.11.3", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.11.3.tgz", - "integrity": "sha512-eY93IVpod/zG3uMF22Unl8h9KkrcKIRs2EGar8hwLZZDU1lkjph303V9HZBwufh2s736U6VXuhD109LYqPoffg==" + "version": "10.12.1", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.12.1.tgz", + "integrity": "sha512-l8386ixSsBdbreOAkqtrwqHwdvR35ID8c3rKPa8lCWuO86dBi32QWHV4vfsZK1utLLFMvw+Z5Ad4XLkZzchscg==" }, "prettier": { "version": "2.8.0", @@ -10951,15 +10951,15 @@ } }, "react-hotkeys-hook": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/react-hotkeys-hook/-/react-hotkeys-hook-4.3.3.tgz", - "integrity": "sha512-OYZCG2G+xLeiH0TkrW+v6eKFPvYq9iCA5sh9pwvnbGQaK86Lw/kWJDjjzSksFJoCk4K78OLe3MR1afNZH6+cLg==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/react-hotkeys-hook/-/react-hotkeys-hook-4.3.5.tgz", + "integrity": "sha512-tfwTwKP3ga7n4naNS/JOByaEwEkTCoXYCepDuhXpj8mBx+sFszV5JecRWM2dv+PbOowmmBpHAFtTXTnG/p8UkQ==", "requires": {} }, "react-intersection-observer": { - "version": "9.4.1", - "resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-9.4.1.tgz", - "integrity": "sha512-IXpIsPe6BleFOEHKzKh5UjwRUaz/JYS0lT/HPsupWEQou2hDqjhLMStc5zyE3eQVT4Fk3FufM8Fw33qW1uyeiw==", + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-9.4.2.tgz", + "integrity": "sha512-AdK+ryzZ7U9ZJYttDUZ8q2Am3nqE0exg5Ryl5Y124KeVsix/1hGZPbdu58EqA98TwnzwDNWHxg/kwNawmIiUig==", "requires": {} }, "react-is": { @@ -11097,9 +11097,9 @@ "dev": true }, "rollup": { - "version": "3.7.4", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.7.4.tgz", - "integrity": "sha512-jN9rx3k5pfg9H9al0r0y1EYKSeiRANZRYX32SuNXAnKzh6cVyf4LZVto1KAuDnbHT03E1CpsgqDKaqQ8FZtgxw==", + "version": "3.12.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.12.1.tgz", + "integrity": "sha512-t9elERrz2i4UU9z7AwISj3CQcXP39cWxgRWLdf4Tm6aKm1eYrqHIgjzXBgb67GNY1sZckTFFi0oMozh3/S++Ig==", "dev": true, "requires": { "fsevents": "~2.3.2" @@ -11545,16 +11545,16 @@ } }, "vite": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.0.4.tgz", - "integrity": "sha512-xevPU7M8FU0i/80DMR+YhgrzR5KS2ORy1B4xcX/cXLsvnUWvfHuqMmVU6N0YiJ4JWGRJJsLCgjEzKjG9/GKoSw==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.1.2.tgz", + "integrity": "sha512-MWDb9Rfy3DI8omDQySbMK93nQqStwbsQWejXRY2EBzEWKmLAXWb1mkI9Yw2IJrc+oCvPCI1Os5xSSIBYY6DEAw==", "dev": true, "requires": { - "esbuild": "^0.16.3", + "esbuild": "^0.16.14", "fsevents": "~2.3.2", - "postcss": "^8.4.20", + "postcss": "^8.4.21", "resolve": "^1.22.1", - "rollup": "^3.7.0" + "rollup": "^3.10.0" } }, "vite-plugin-html-config": { @@ -11572,9 +11572,9 @@ "requires": {} }, "vite-plugin-pwa": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.14.1.tgz", - "integrity": "sha512-5zx7yhQ8RTLwV71+GA9YsQQ63ALKG8XXIMqRJDdZkR8ZYftFcRgnzM7wOWmQZ/DATspyhPih5wCdcZnAIsM+mA==", + "version": "0.14.4", + "resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.14.4.tgz", + "integrity": "sha512-M7Ct0so8OlouMkTWgXnl8W1xU95glITSKIe7qswZf1tniAstO2idElGCnsrTJ5NPNSx1XqfTCOUj8j94S6FD7Q==", "dev": true, "requires": { "@rollup/plugin-replace": "^5.0.1", @@ -11587,9 +11587,9 @@ } }, "vite-plugin-remove-console": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/vite-plugin-remove-console/-/vite-plugin-remove-console-1.3.0.tgz", - "integrity": "sha512-5a/OLYB6yNRHMuHj9rBQRYMQ1NBKffxA8BaD77urUBLcGOWMHFHALjh6C26wZfZd41KytSwLp6DhvNKU78mNJg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/vite-plugin-remove-console/-/vite-plugin-remove-console-2.0.0.tgz", + "integrity": "sha512-bEsyShSacsunbm0X1zaVliwgmWlsaBPLk7FN4wr2xQMs8zSZPSwpRNTT5UZiF0+cfMEkN4VVnofITawmT3pjgQ==", "dev": true }, "webidl-conversions": { diff --git a/package.json b/package.json index 54a1a8ee..1c7e14e6 100644 --- a/package.json +++ b/package.json @@ -11,20 +11,20 @@ }, "dependencies": { "@github/text-expander-element": "~2.3.0", - "@iconify-icons/mingcute": "~1.2.3", - "@szhsin/react-menu": "~3.4.0", + "@iconify-icons/mingcute": "~1.2.4", + "@szhsin/react-menu": "~3.4.1", "dayjs": "~1.11.7", "dayjs-twitter": "~0.5.0", "fast-blurhash": "~1.1.2", "fast-deep-equal": "~3.1.3", "idb-keyval": "~6.2.0", "just-debounce-it": "~3.2.0", - "masto": "~5.7.0", + "masto": "~5.10.0", "mem": "~9.0.2", "p-retry": "~5.1.2", - "preact": "~10.11.3", - "react-hotkeys-hook": "~4.3.3", - "react-intersection-observer": "~9.4.1", + "preact": "~10.12.1", + "react-hotkeys-hook": "~4.3.5", + "react-intersection-observer": "~9.4.2", "react-router-dom": "6.6.2", "string-length": "~5.0.1", "swiped-events": "~1.1.7", @@ -32,7 +32,7 @@ "uid": "~2.0.1", "use-debounce": "~9.0.3", "use-resize-observer": "~9.1.0", - "valtio": "~1.9.0" + "valtio": "1.9.0" }, "devDependencies": { "@preact/preset-vite": "~2.5.0", @@ -41,11 +41,11 @@ "postcss-dark-theme-class": "~0.7.3", "postcss-preset-env": "~8.0.1", "twitter-text": "~3.1.0", - "vite": "~4.0.4", + "vite": "~4.1.2", "vite-plugin-html-config": "~1.0.11", "vite-plugin-html-env": "~1.2.7", - "vite-plugin-pwa": "~0.14.1", - "vite-plugin-remove-console": "~1.3.0", + "vite-plugin-pwa": "~0.14.4", + "vite-plugin-remove-console": "~2.0.0", "workbox-cacheable-response": "~6.5.4", "workbox-expiration": "~6.5.4", "workbox-routing": "~6.5.4", diff --git a/public/sw.js b/public/sw.js index 17ded602..46fa48e8 100644 --- a/public/sw.js +++ b/public/sw.js @@ -33,7 +33,7 @@ const imageRoute = new Route( ); registerRoute(imageRoute); -// Cache /instance because masto.js has to keep calling it while initializing +// 1-day cache for /api/v1/instance and /api/v1/custom_emojis const apiExtendedRoute = new RegExpRoute( /^https?:\/\/[^\/]+\/api\/v\d+\/(instance|custom_emojis)/, new StaleWhileRevalidate({ diff --git a/src/app.css b/src/app.css index d92bd4b9..f1f125a2 100644 --- a/src/app.css +++ b/src/app.css @@ -1,3 +1,6 @@ +@import url('@szhsin/react-menu/dist/core.css'); +@import url('toastify-js/src/toastify.css'); + html, body { margin: 0; @@ -69,7 +72,7 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) { min-height: 100vh; min-height: 100dvh; margin: auto; - width: 40em; + width: var(--main-width); max-width: 100%; border-left: 1px solid rgba(0, 0, 0, 0.1); border-right: 1px solid rgba(0, 0, 0, 0.1); @@ -84,46 +87,65 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) { } .deck > header { - min-height: 3em; position: sticky; top: 0; - background-color: var(--bg-blur-color); - background-image: linear-gradient(to bottom, var(--bg-color), transparent); - backdrop-filter: saturate(180%) blur(20px); - border-bottom: var(--hairline-width) solid var(--divider-color); z-index: 1; cursor: default; z-index: 10; - display: grid; - grid-template-columns: 1fr 1fr 1fr; - align-items: center; user-select: none; transition: transform 0.5s ease-in-out; user-select: none; } .deck > header[hidden] { + display: block; transform: translateY(-100%); pointer-events: none; user-select: none; } -.deck > header > .header-side:last-of-type { +.deck > header .header-grid { + background-color: var(--bg-blur-color); + background-image: linear-gradient(to bottom, var(--bg-color), transparent); + backdrop-filter: saturate(180%) blur(20px); + border-bottom: var(--hairline-width) solid var(--divider-color); + min-height: 3em; + display: grid; + grid-template-columns: 1fr max-content 1fr; + align-items: center; + text-overflow: ellipsis; + white-space: nowrap; +} +.deck > header .header-grid > .header-side:last-of-type { text-align: right; grid-column: 3; } -.deck > header :is(button, .button).plain { +.deck > header .header-grid :is(button, .button).plain { backdrop-filter: none; } -.deck > header h1 { +.deck > header .header-grid h1 { margin: 0 8px; padding: 0; font-size: 1.2em; text-align: center; - white-space: nowrap; } -.deck > header h1:first-child { +.deck > header .header-grid.header-grid-2 { + grid-template-columns: 1fr max-content; +} +.deck > header .header-grid-2 h1 { text-align: left; padding-left: 8px; } +.deck > header .header-grid h1:has(.ancestors-indicator) { + display: flex; + gap: 8px; + align-items: center; + max-width: fit-content; +} +.deck > header .header-grid h1:has(.ancestors-indicator) .hero-heading { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} .deck h2 { font-size: 1.45em; } @@ -528,46 +550,44 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) { filter: brightness(0.95); } -.boost-carousel { +.status-carousel { + --carousel-faded-color: var(--bg-faded-color); background: linear-gradient( to bottom right, - var(--reblog-faded-color), + var(--carousel-faded-color), transparent 150% ); position: relative; } -.boost-carousel:after { +.status-carousel:after { content: ''; position: absolute; inset: 0; pointer-events: none; background-image: radial-gradient( ellipse 50% 32px at bottom center, - var(--reblog-faded-color), + var(--carousel-faded-color), transparent ), linear-gradient(to top, var(--bg-color), transparent 64px); background-repeat: no-repeat; background-position: bottom center; } -.boost-carousel .status-reblog { - background-image: none; -} -.boost-carousel header { +.status-carousel header { padding: 8px 16px 0; display: flex; justify-content: space-between; align-items: center; } -.boost-carousel h3 { +.status-carousel h3 { margin: 0; padding: 0; font-size: 14px; text-transform: uppercase; - color: var(--reblog-color); + color: var(--carousel-color); text-shadow: 0 1px var(--bg-color); } -.boost-carousel ul { +.status-carousel ul { display: flex; overflow-x: auto; overflow-y: hidden; @@ -579,7 +599,7 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) { align-items: flex-start; counter-reset: index; } -.boost-carousel ul > li { +.status-carousel ul > li { scroll-snap-align: center; scroll-snap-stop: always; flex-shrink: 0; @@ -594,12 +614,33 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) { counter-increment: index; position: relative; } -.boost-carousel ul > li:before { +.status-carousel .content-container .content:only-child { + font-size: calc(100% + 25% * max(2 - var(--content-text-weight), 0)); +} +.status-carousel + .content-container:is( + [style*='content-text-weight:1'], + [style*='content-text-weight: 1'] + ) + .media-container.media-eq1 { + /* LOL, this is madness, reading a value from the style attribute */ + height: auto; + min-height: 160px; + max-height: max(160px, 50vh); +} +.status-carousel.boosts-carousel { + --carousel-color: var(--reblog-color); + --carousel-faded-color: var(--reblog-faded-color); +} +.status-carousel.boosts-carousel .status-reblog { + background-image: none; +} +.status-carousel.boosts-carousel ul > li:before { content: counter(index); position: absolute; left: 0; font-size: 10px; - color: var(--reblog-color); + color: var(--carousel-color); padding: 8px; } @@ -608,7 +649,7 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) { text-align: center; } -.status-boost-link { +.status-carousel-link { display: block; width: 100%; text-decoration-line: none; @@ -623,15 +664,15 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) { overflow: hidden; box-shadow: 0 1px var(--bg-color); } -.status-boost-link::focus { +.status-carousel-link::focus { background-color: var(--link-bg-hover-color); } @media (hover: hover) { - .status-boost-link:hover { + .status-carousel-link:hover { background-color: var(--link-bg-hover-color); } } -.status-boost-link:active:not(:has(:is(.media, button):active)) { +.status-carousel-link:active:not(:has(:is(.media, button):active)) { filter: brightness(0.95); } @@ -659,14 +700,14 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) { } } .deck-backdrop .deck { - width: 40em; + width: var(--main-width); max-width: 100vw; background-color: var(--bg-color); animation: slide-in 0.5s var(--timing-function); box-shadow: -1px 0 var(--bg-color); } .deck-backdrop .deck .status { - max-width: 40em; + max-width: var(--main-width); } .deck-close { @@ -701,18 +742,19 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) { .updates-button { position: absolute; z-index: 2; - animation: fade-from-top 0.3s ease-out; + top: 3em; + animation: fade-from-top 0.3s var(--timing-function); left: 50%; - margin-top: 8px; + margin-top: 16px; transform: translate(-50%, 0); font-size: 90%; - background: linear-gradient( - to bottom, - var(--button-bg-blur-color), - var(--button-bg-color) + background-color: var(--button-bg-color); + background-image: linear-gradient( + 160deg, + rgba(255, 255, 255, 0.5), + rgba(255, 255, 255, 0) 50% ); - backdrop-filter: blur(16px); - box-shadow: 0 3px 8px -1px var(--bg-faded-blur-color), + box-shadow: 0 3px 8px -1px var(--drop-shadow-color), 0 10px 36px -4px var(--button-bg-blur-color); } .updates-button .icon { @@ -722,7 +764,7 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) { /* BOX */ .box { - width: 40em; + width: var(--main-width); max-width: 100vw; padding: 16px; } @@ -853,6 +895,9 @@ button.carousel-dot:is(.active, [disabled].active) { .media-post-link .button-label { display: none; } +body:has(.status-deck) .media-post-link { + display: none; +} @media (min-width: calc(40em + 350px)) { .media-post-link .button-label { @@ -889,10 +934,11 @@ button.carousel-dot:is(.active, [disabled].active) { background-color: var(--button-bg-blur-color); backdrop-filter: blur(16px); z-index: 1; - box-shadow: 0 3px 8px -1px var(--bg-faded-blur-color), + box-shadow: 0 3px 8px -1px var(--drop-shadow-color), 0 10px 36px -4px var(--button-bg-blur-color); transition: all 0.3s ease-in-out; } +#home-page:has(header[hidden]) ~ #compose-button, #compose-button[hidden] { transform: translateY(200%); pointer-events: none; @@ -902,14 +948,15 @@ button.carousel-dot:is(.active, [disabled].active) { transition: transform 0.3s ease-in-out; } #compose-button[hidden] .icon { - transform: rotate3d(0, 1, 0, 180deg); + transform: rotate3d(0, 0, 1, -25deg); } #compose-button:is(:hover, :focus) { background-color: var(--button-bg-color); filter: none; } #compose-button:active { - filter: brightness(0.75); + transform: scale(0.95); + transition: none; } #compose-button .icon { filter: drop-shadow(0 1px 2px var(--button-bg-color)); @@ -926,7 +973,7 @@ button.carousel-dot:is(.active, [disabled].active) { overflow: hidden; background-color: var(--bg-color); width: 100%; - max-width: calc(40em - 50px - 16px); + max-width: calc(var(--main-width) - 50px - 16px); border-radius: 16px 16px 0 0; box-shadow: 0 -1px 32px var(--divider-color); animation: slide-up 0.3s var(--timing-function); @@ -962,6 +1009,12 @@ button.carousel-dot:is(.active, [disabled].active) { mask-image: linear-gradient(to bottom, transparent 0%, black 10px); } +/* ICON */ + +.icon { + flex-shrink: 0; +} + /* TAG */ .tag { @@ -981,22 +1034,48 @@ button.carousel-dot:is(.active, [disabled].active) { /* MENU POPUP */ .szh-menu { - padding: 8px 0 !important; + padding: 8px 0; margin: 0; font-size: 16px; - background-color: var(--bg-color) !important; - border: 1px solid var(--outline-color) !important; + background-color: var(--bg-color); + border: 1px solid var(--outline-color); border-radius: 8px; box-shadow: 0 3px 6px var(--drop-shadow-color); text-align: left; - animation: appear 0.15s ease-in-out; + animation: appear-smooth 0.15s ease-in-out; + width: 16em; + max-width: 90vw; + overflow: hidden; +} +.szh-menu__item--focusable { + background-color: transparent; } .szh-menu .szh-menu__item { padding: 8px 16px !important; + transition: all 0.1s ease-in-out; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } .szh-menu .szh-menu__item * { vertical-align: middle; } +.szh-menu .szh-menu__item a { + overflow: hidden; + text-overflow: ellipsis; + display: flex; + color: inherit; + text-decoration: none; + padding: 8px 16px !important; + margin: -8px -16px !important; + gap: 8px; +} +.szh-menu .szh-menu__item a.is-active { + font-weight: bold; +} +.szh-menu .szh-menu__item .icon { + opacity: 0.5; +} .szh-menu .szh-menu__item:not(.szh-menu__item--disabled, .szh-menu__item--hover) { color: var(--text-color); @@ -1005,6 +1084,28 @@ button.carousel-dot:is(.active, [disabled].active) { color: var(--button-text-color); background-color: var(--button-bg-color); } +.szh-menu__divider { + background-color: var(--divider-color); +} +.szh-menu .szh-menu__item .menu-grow { + flex-grow: 1; +} +.szh-menu .szh-menu__item .menu-shortcut { + opacity: 0.5; + font-weight: normal; +} + +/* GLASS MENU */ + +.glass-menu { + background-color: var(--bg-blur-color); + backdrop-filter: blur(8px) saturate(3); + border: var(--hairline-width) solid var(--bg-color); + box-shadow: 0 3px 8px -1px var(--drop-shadow-color); +} +.glass-menu .szh-menu__item--hover { + background-color: var(--button-bg-blur-color); +} /* DONUT METER */ @@ -1064,16 +1165,16 @@ meter.donut:is(.danger, .explode):after { /* TOAST */ :root .toastify { + background-color: var(--button-bg-color); background-image: linear-gradient( - to bottom, - var(--button-bg-blur-color), - var(--button-bg-color) + 160deg, + rgba(255, 255, 255, 0.5), + rgba(255, 255, 255, 0) 50% ); - backdrop-filter: blur(16px); color: var(--button-text-color); border-radius: 10em; padding: 8px 16px; - box-shadow: 0 3px 8px -1px var(--bg-faded-blur-color), + box-shadow: 0 3px 8px -1px var(--drop-shadow-color), 0 10px 36px -4px var(--button-bg-blur-color); } .toastify-bottom { @@ -1110,18 +1211,19 @@ meter.donut:is(.danger, .explode):after { width: 100%; flex-grow: 1; } -#home-page ~ .deck-container { +:is(#home-page, #welcome, #columns) ~ .deck-container { z-index: 10; position: fixed; inset: 0; } -#home-page:has(~ .deck-container) { +:is(#home-page, #welcome, #columns):has(~ .deck-container) { display: block; position: absolute; user-select: none; pointer-events: none; opacity: 0; - content-visibility: hidden; + /* This causes scrollTop to be reset to 0 when the page is hidden */ + /* content-visibility: hidden; */ } /* TAB BAR */ @@ -1131,7 +1233,7 @@ meter.donut:is(.danger, .explode):after { bottom: 16px; bottom: max(16px, env(safe-area-inset-bottom)); width: calc(100% - 32px); - max-width: calc(40em - 32px); + max-width: calc(var(--main-width) - 32px); z-index: 100; display: flex; background-color: var(--bg-blur-color); @@ -1150,6 +1252,15 @@ meter.donut:is(.danger, .explode):after { text-align: center; padding: 16px 0; display: block; + color: var(--text-insignificant-color); +} +#tab-bar li a.is-active { + color: var(--link-color); + background-image: radial-gradient( + closest-side at 50% 50%, + var(--bg-blur-color), + transparent 75% + ); } /* 404 */ @@ -1181,6 +1292,145 @@ meter.donut:is(.danger, .explode):after { color: var(--text-insignificant-color); } +/* LINK LISTS? */ + +ul.link-list { + list-style: none; + padding: 16px; + margin: 0; + display: flex; + flex-direction: column; + gap: 1px; +} +ul.link-list li { + padding: 0; + margin: 0; +} +ul.link-list li a { + --radius: 8px; + display: block; + background-color: var(--bg-faded-color); + line-height: 1.25; + padding: 12px; + text-decoration: none; + line-height: 1.4; + font-weight: 500; + display: flex; + align-items: center; + gap: 8px; +} +ul.link-list li:first-child a { + border-top-left-radius: var(--radius); + border-top-right-radius: var(--radius); +} +ul.link-list li:last-child a { + border-bottom-left-radius: var(--radius); + border-bottom-right-radius: var(--radius); +} +ul.link-list li a:is(:hover, :focus) { + color: var(--text-color); +} +ul.link-list li a:active { + filter: brightness(0.9); +} +ul.link-list li a * { + vertical-align: middle; +} +ul.link-list li a .icon { + flex-shrink: 0; +} + +@media (min-width: 40em) { + ul.link-list li a { + background-color: var(--bg-color); + } +} + +/* COLUMNS */ + +#columns { + display: flex; + width: 100vw; + overflow-y: hidden; + overflow-x: scroll; + scroll-snap-type: x mandatory; + scroll-behavior: smooth; + /* scrollbar-width: none; */ + overscroll-behavior: contain; + overscroll-behavior-x: contain; +} +/* #columns::-webkit-scrollbar { + display: none; +} */ +#columns > * { + overscroll-behavior: auto; + scroll-snap-align: left; + scroll-snap-stop: always; + overscroll-behavior: auto; + flex-basis: min(100vw, 360px); + flex-shrink: 0; +} +#columns .header-grid input { + pointer-events: none; +} +#columns + .header-grid + .header-side:first-of-type + :is(button, .button) + ~ :is(button, .button), +#columns .deck-container:not(:first-of-type) .header-grid .header-side > * { + display: none; +} +@media (min-width: 40em) { + #columns { + gap: 16px; + padding: 16px; + background-color: var(--bg-blur-color); + height: 100vh; + height: 100dvh; + justify-content: stretch; + align-items: stretch; + } + #columns > * { + padding: 0 16px; + border: var(--hairline-width) solid var(--outline-color); + border-radius: 16px; + box-shadow: 0 4px 16px var(--drop-shadow-color); + height: unset; + background-image: linear-gradient( + 160deg, + transparent 20%, + var(--bg-color), + transparent 75% + ); + } + #columns > *:focus-visible, + #columns > *:has(:focus-visible) { + box-shadow: 0 4px 16px var(--drop-shadow-color), + 0 4px 16px var(--drop-shadow-color); + border-color: var(--outline-hover-color); + } + #columns .timeline:not(.flat) > li:has(.status-link.is-active), + #columns + .timeline:not(.flat) + > li:not(:has(.status-carousel)):has(+ li .status-link.is-active), + #columns + .timeline:not(.flat) + > li:not(:has(.status-carousel)):has(.status-link.is-active) + + li { + transform: none; + } + #columns .timeline-deck > header { + margin: 0; + } + #columns li:has(.status-carousel) { + width: auto; + transform: none; + } +} + +/* OTHERS */ + @media (min-width: 40em) { html, body { @@ -1201,7 +1451,7 @@ meter.donut:is(.danger, .explode):after { } .deck-backdrop .deck { width: 50%; - min-width: 40em; + min-width: var(--main-width); border-left: 1px solid var(--divider-color); } .timeline-deck { @@ -1210,14 +1460,16 @@ meter.donut:is(.danger, .explode):after { } .timeline-deck > header { --margin-top: 8px; - min-height: 4em; top: var(--margin-top); - border-bottom: 0; - background-color: var(--bg-faded-blur-color); - background-image: none; + margin-inline: 8px; + } + .timeline-deck > header .header-grid { border-bottom: 0; border-radius: 16px; - margin-inline: 8px; + background-color: var(--bg-faded-blur-color); + background-image: none; + border-radius: 16px; + min-height: 4em; } .timeline-deck > header[hidden] { transform: translate3d(0, calc((100% + var(--margin-top)) * -1), 0); @@ -1228,7 +1480,7 @@ meter.donut:is(.danger, .explode):after { .updates-button { margin-top: 24px; } - .timeline-deck .timeline:not(.flat) > li { + .timeline:not(.flat) > li { border: 1px solid var(--divider-color); margin: 16px 0; background-color: var(--bg-color); @@ -1238,16 +1490,14 @@ meter.donut:is(.danger, .explode):after { transition: transform 0.4s var(--timing-function); --back-transition: transform 0.4s ease-out; } - .timeline-deck .timeline:not(.flat) > li:has(.status-link.is-active) { + .timeline:not(.flat) > li:has(.status-link.is-active) { transition: var(--back-transition); transform: translate3d(-2.5vw, 0, 0); } - .timeline-deck - .timeline:not(.flat) - > li:not(:has(.boost-carousel)):has(+ li .status-link.is-active), - .timeline-deck - .timeline:not(.flat) - > li:not(:has(.boost-carousel)):has(.status-link.is-active) + .timeline:not(.flat) + > li:not(:has(.status-carousel)):has(+ li .status-link.is-active), + .timeline:not(.flat) + > li:not(:has(.status-carousel)):has(.status-link.is-active) + li { transition: var(--back-transition); transform: translate3d(-1.25vw, 0, 0); @@ -1258,7 +1508,7 @@ meter.donut:is(.danger, .explode):after { /* :is(.carousel-top-controls, .carousel-controls) { padding: 32px; } */ - li:has(.boost-carousel) { + li:has(.status-carousel) { width: 95vw; max-width: calc(320px * 3.3); transform: translateX(calc(-50% + 20em)); diff --git a/src/app.jsx b/src/app.jsx index f69c1964..1a3b6606 100644 --- a/src/app.jsx +++ b/src/app.jsx @@ -1,8 +1,5 @@ import './app.css'; -import 'toastify-js/src/toastify.css'; -import debounce from 'just-debounce-it'; -import { createClient } from 'masto'; import { useEffect, useLayoutEffect, @@ -10,7 +7,13 @@ import { useRef, useState, } from 'preact/hooks'; -import { Route, Routes, useLocation, useNavigate } from 'react-router-dom'; +import { + matchPath, + Route, + Routes, + useLocation, + useNavigate, +} from 'react-router-dom'; import Toastify from 'toastify-js'; import { useSnapshot } from 'valtio'; @@ -22,22 +25,38 @@ import Link from './components/link'; import Loader from './components/loader'; import MediaModal from './components/media-modal'; import Modal from './components/modal'; +import Shortcuts from './components/shortcuts'; +import ShortcutsSettings from './components/shortcuts-settings'; import NotFound from './pages/404'; import AccountStatuses from './pages/account-statuses'; import Bookmarks from './pages/bookmarks'; import Favourites from './pages/favourites'; -import Hashtags from './pages/hashtags'; +import FollowedHashtags from './pages/followed-hashtags'; +import Following from './pages/following'; +import Hashtag from './pages/hashtag'; import Home from './pages/home'; +import HomeV1 from './pages/home-v1'; +import List from './pages/list'; import Lists from './pages/lists'; import Login from './pages/login'; import Notifications from './pages/notifications'; import Public from './pages/public'; +import Search from './pages/search'; import Settings from './pages/settings'; import Status from './pages/status'; import Welcome from './pages/welcome'; +import { + api, + initAccount, + initClient, + initInstance, + initPreferences, +} from './utils/api'; import { getAccessToken } from './utils/auth'; -import states, { saveStatus } from './utils/states'; +import states, { getStatus, saveStatus } from './utils/states'; import store from './utils/store'; +import { getCurrentAccount } from './utils/store-utils'; +import usePageVisibility from './utils/usePageVisibility'; window.__STATES__ = states; @@ -53,13 +72,12 @@ function App() { document.documentElement.classList.add(`is-${theme}`); document .querySelector('meta[name="color-scheme"]') - .setAttribute('content', theme); + .setAttribute('content', theme === 'auto' ? 'dark light' : theme); } }, []); useEffect(() => { const instanceURL = store.local.get('instanceURL'); - const accounts = store.local.getJSON('accounts') || []; const code = (window.location.search.match(/code=([^&]+)/) || [])[1]; if (code) { @@ -72,59 +90,43 @@ function App() { (async () => { setUIState('loading'); - const tokenJSON = await getAccessToken({ + const { access_token: accessToken } = await getAccessToken({ instanceURL, client_id: clientID, client_secret: clientSecret, code, }); - const { access_token: accessToken } = tokenJSON; - store.session.set('accessToken', accessToken); - initMasto({ - url: `https://${instanceURL}`, - accessToken, - }); - - const mastoAccount = await masto.v1.accounts.verifyCredentials(); - - // console.log({ tokenJSON, mastoAccount }); - - let account = accounts.find((a) => a.info.id === mastoAccount.id); - if (account) { - account.info = mastoAccount; - account.instanceURL = instanceURL.toLowerCase(); - account.accessToken = accessToken; - } else { - account = { - info: mastoAccount, - instanceURL, - accessToken, - }; - accounts.push(account); - } - - store.local.setJSON('accounts', accounts); - store.session.set('currentAccount', account.info.id); + const masto = initClient({ instance: instanceURL, accessToken }); + await Promise.allSettled([ + initInstance(masto), + initAccount(masto, instanceURL, accessToken), + ]); + initPreferences(masto); setIsLoggedIn(true); setUIState('default'); })(); - } else if (accounts.length) { - const currentAccount = store.session.get('currentAccount'); - const account = - accounts.find((a) => a.info.id === currentAccount) || accounts[0]; - const instanceURL = account.instanceURL; - const accessToken = account.accessToken; - store.session.set('currentAccount', account.info.id); - if (accessToken) setIsLoggedIn(true); - - initMasto({ - url: `https://${instanceURL}`, - accessToken, - }); } else { - setUIState('default'); + const account = getCurrentAccount(); + if (account) { + store.session.set('currentAccount', account.info.id); + const { masto } = api({ account }); + console.log('masto', masto); + initPreferences(masto); + setUIState('loading'); + (async () => { + try { + await initInstance(masto); + } catch (e) { + } finally { + setIsLoggedIn(true); + setUIState('default'); + } + })(); + } else { + setUIState('default'); + } } }, []); @@ -146,28 +148,79 @@ function App() { return () => clearTimeout(timer); }; useEffect(focusDeck, [location]); + const showModal = + snapStates.showCompose || + snapStates.showSettings || + snapStates.showAccount || + snapStates.showDrafts || + snapStates.showMediaModal || + snapStates.showShortcutsSettings; useEffect(() => { - if ( - !snapStates.showCompose && - !snapStates.showSettings && - !snapStates.showAccount - ) { - focusDeck(); - } - }, [snapStates.showCompose, snapStates.showSettings, snapStates.showAccount]); + if (!showModal) focusDeck(); + }, [showModal]); + // useEffect(() => { + // // HACK: prevent this from running again due to HMR + // if (states.init) return; + // if (isLoggedIn) { + // requestAnimationFrame(startVisibility); + // states.init = true; + // } + // }, [isLoggedIn]); + + // Notifications service + // - WebSocket to receive notifications when page is visible + const [visible, setVisible] = useState(true); + usePageVisibility(setVisible); + const notificationStream = useRef(); useEffect(() => { - // HACK: prevent this from running again due to HMR - if (states.init) return; - if (isLoggedIn) { - requestAnimationFrame(startVisibility); - states.init = true; + if (isLoggedIn && visible) { + const { masto } = api(); + (async () => { + // 1. Get the latest notification + if (states.notificationsLast) { + const notificationsIterator = masto.v1.notifications.list({ + limit: 1, + since_id: states.notificationsLast.id, + }); + const { value: notifications } = await notificationsIterator.next(); + if (notifications?.length) { + states.notificationsShowNew = true; + } + } + + // 2. Start streaming + notificationStream.current = await masto.ws.stream( + '/api/v1/streaming', + { + stream: 'user:notification', + }, + ); + console.log('🎏 Streaming notification', notificationStream.current); + + notificationStream.current.on('notification', (notification) => { + console.log('🔔🔔 Notification', notification); + states.notificationsShowNew = true; + }); + + notificationStream.current.ws.onclose = () => { + console.log('🔔🔔 Notification stream closed'); + }; + })(); } - }, [isLoggedIn]); + return () => { + if (notificationStream.current) { + notificationStream.current.ws.close(); + notificationStream.current = null; + } + }; + }, [visible, isLoggedIn]); const { prevLocation } = snapStates; const backgroundLocation = useRef(prevLocation || null); - const isModalPage = /^\/s\//i.test(location.pathname); + const isModalPage = + matchPath('/:instance/s/:id', location.pathname) || + matchPath('/s/:id', location.pathname); if (isModalPage) { if (!backgroundLocation.current) backgroundLocation.current = prevLocation; } else { @@ -180,7 +233,7 @@ function App() { const nonRootLocation = useMemo(() => { const { pathname } = location; - return !/^\/(login|welcome|p)/.test(pathname); + return !/^\/(login|welcome)/.test(pathname); }, [location]); return ( @@ -205,16 +258,28 @@ function App() { {isLoggedIn && ( } /> )} + {isLoggedIn && } />} + {isLoggedIn && } />} {isLoggedIn && } />} {isLoggedIn && } />} - {isLoggedIn && } />} - {isLoggedIn && } />} - {isLoggedIn && } />} - } /> + {isLoggedIn && ( + + } /> + } /> + + )} + {isLoggedIn && } />} + } /> + } /> + + } /> + } /> + + } /> {/* } /> */} - {isLoggedIn && } />} + } /> + {!snapStates.settings.shortcutsColumnsMode && } {!!snapStates.showCompose && ( { states.showAccount = false; }} @@ -333,6 +400,7 @@ function App() { > { @@ -341,213 +409,179 @@ function App() { /> )} + {!!snapStates.showShortcutsSettings && ( + { + if (e.target === e.currentTarget) { + states.showShortcutsSettings = false; + } + }} + > + + + )} ); } -function initMasto(params) { - const clientParams = { - url: params.url || 'https://mastodon.social', - accessToken: params.accessToken || null, - disableVersionCheck: true, - timeout: 30_000, - }; - window.masto = createClient(clientParams); +// let ws; +// async function startStream() { +// const { masto, instance } = api(); +// if ( +// ws && +// (ws.readyState === WebSocket.CONNECTING || ws.readyState === WebSocket.OPEN) +// ) { +// return; +// } - (async () => { - // Request v2, fallback to v1 if fail - let info; - try { - info = await masto.v2.instance.fetch(); - } catch (e) {} - if (!info) { - try { - info = await masto.v1.instances.fetch(); - } catch (e) {} - } - if (!info) return; - console.log(info); - const { - // v1 - uri, - urls: { streamingApi } = {}, - // v2 - domain, - configuration: { urls: { streaming } = {} } = {}, - } = info; - if (uri || domain) { - const instances = store.local.getJSON('instances') || {}; - instances[ - (domain || uri) - .replace(/^https?:\/\//, '') - .replace(/\/+$/, '') - .toLowerCase() - ] = info; - store.local.setJSON('instances', instances); - } - if (streamingApi || streaming) { - window.masto = createClient({ - ...clientParams, - streamingApiUrl: streaming || streamingApi, - }); - } - })(); -} +// const stream = await masto.v1.stream.streamUser(); +// console.log('STREAM START', { stream }); +// ws = stream.ws; -let ws; -async function startStream() { - if ( - ws && - (ws.readyState === WebSocket.CONNECTING || ws.readyState === WebSocket.OPEN) - ) { - return; - } +// const handleNewStatus = debounce((status) => { +// console.log('UPDATE', status); +// if (document.visibilityState === 'hidden') return; - const stream = await masto.v1.stream.streamUser(); - console.log('STREAM START', { stream }); - ws = stream.ws; +// const inHomeNew = states.homeNew.find((s) => s.id === status.id); +// const inHome = status.id === states.homeLast?.id; +// if (!inHomeNew && !inHome) { +// if (states.settings.boostsCarousel && status.reblog) { +// // do nothing +// } else { +// states.homeNew.unshift({ +// id: status.id, +// reblog: status.reblog?.id, +// reply: !!status.inReplyToAccountId, +// }); +// console.log('homeNew 1', [...states.homeNew]); +// } +// } - const handleNewStatus = debounce((status) => { - console.log('UPDATE', status); +// saveStatus(status, instance); +// }, 5000); +// stream.on('update', handleNewStatus); +// stream.on('status.update', (status) => { +// console.log('STATUS.UPDATE', status); +// saveStatus(status, instance); +// }); +// stream.on('delete', (statusID) => { +// console.log('DELETE', statusID); +// // delete states.statuses[statusID]; +// const s = getStatus(statusID); +// if (s) s._deleted = true; +// }); +// stream.on('notification', (notification) => { +// console.log('NOTIFICATION', notification); - const inHomeNew = states.homeNew.find((s) => s.id === status.id); - const inHome = status.id === states.homeLast?.id; - if (!inHomeNew && !inHome) { - if (states.settings.boostsCarousel && status.reblog) { - // do nothing - } else { - states.homeNew.unshift({ - id: status.id, - reblog: status.reblog?.id, - reply: !!status.inReplyToAccountId, - }); - console.log('homeNew 1', [...states.homeNew]); - } - } +// const inNotificationsNew = states.notificationsNew.find( +// (n) => n.id === notification.id, +// ); +// const inNotifications = notification.id === states.notificationsLast?.id; +// if (!inNotificationsNew && !inNotifications) { +// states.notificationsNew.unshift(notification); +// } - saveStatus(status); - }, 5000); - stream.on('update', handleNewStatus); - stream.on('status.update', (status) => { - console.log('STATUS.UPDATE', status); - saveStatus(status); - }); - stream.on('delete', (statusID) => { - console.log('DELETE', statusID); - // delete states.statuses[statusID]; - const s = states.statuses[statusID]; - if (s) s._deleted = true; - }); - stream.on('notification', (notification) => { - console.log('NOTIFICATION', notification); +// saveStatus(notification.status, instance, { override: false }); +// }); - const inNotificationsNew = states.notificationsNew.find( - (n) => n.id === notification.id, - ); - const inNotifications = notification.id === states.notificationLast?.id; - if (!inNotificationsNew && !inNotifications) { - states.notificationsNew.unshift(notification); - } +// stream.ws.onclose = () => { +// console.log('STREAM CLOSED!'); +// if (document.visibilityState !== 'hidden') { +// startStream(); +// } +// }; - saveStatus(notification.status, { override: false }); - }); +// return { +// stream, +// stopStream: () => { +// stream.ws.close(); +// }, +// }; +// } - stream.ws.onclose = () => { - console.log('STREAM CLOSED!'); - if (document.visibilityState !== 'hidden') { - startStream(); - } - }; +// let lastHidden; +// function startVisibility() { +// const { masto, instance } = api(); +// const handleVisible = (visible) => { +// if (!visible) { +// const timestamp = Date.now(); +// lastHidden = timestamp; +// } else { +// const timestamp = Date.now(); +// const diff = timestamp - lastHidden; +// const diffMins = Math.round(diff / 1000 / 60); +// console.log(`visible: ${visible}`, { lastHidden, diffMins }); +// if (!lastHidden || diffMins > 1) { +// (async () => { +// try { +// const firstStatusID = states.homeLast?.id; +// const firstNotificationID = states.notificationsLast?.id; +// console.log({ states, firstNotificationID, firstStatusID }); +// const fetchHome = masto.v1.timelines.listHome({ +// limit: 5, +// ...(firstStatusID && { sinceId: firstStatusID }), +// }); +// const fetchNotifications = masto.v1.notifications.list({ +// limit: 1, +// ...(firstNotificationID && { sinceId: firstNotificationID }), +// }); - return { - stream, - stopStream: () => { - stream.ws.close(); - }, - }; -} +// const newStatuses = await fetchHome; +// const hasOneAndReblog = +// newStatuses.length === 1 && newStatuses?.[0]?.reblog; +// if (newStatuses.length) { +// if (states.settings.boostsCarousel && hasOneAndReblog) { +// // do nothing +// } else { +// states.homeNew = newStatuses.map((status) => { +// saveStatus(status, instance); +// return { +// id: status.id, +// reblog: status.reblog?.id, +// reply: !!status.inReplyToAccountId, +// }; +// }); +// console.log('homeNew 2', [...states.homeNew]); +// } +// } -let lastHidden; -function startVisibility() { - const handleVisible = (visible) => { - if (!visible) { - const timestamp = Date.now(); - lastHidden = timestamp; - } else { - const timestamp = Date.now(); - const diff = timestamp - lastHidden; - const diffMins = Math.round(diff / 1000 / 60); - console.log(`visible: ${visible}`, { lastHidden, diffMins }); - if (!lastHidden || diffMins > 1) { - (async () => { - try { - const firstStatusID = states.homeLast?.id; - const firstNotificationID = states.notificationsLast?.id; - const fetchHome = masto.v1.timelines.listHome({ - limit: 5, - ...(firstStatusID && { sinceId: firstStatusID }), - }); - const fetchNotifications = masto.v1.notifications.list({ - limit: 1, - ...(firstNotificationID && { sinceId: firstNotificationID }), - }); +// const newNotifications = await fetchNotifications; +// if (newNotifications.length) { +// const notification = newNotifications[0]; +// const inNotificationsNew = states.notificationsNew.find( +// (n) => n.id === notification.id, +// ); +// const inNotifications = +// notification.id === states.notificationsLast?.id; +// if (!inNotificationsNew && !inNotifications) { +// states.notificationsNew.unshift(notification); +// } - const newStatuses = await fetchHome; - const hasOneAndReblog = - newStatuses.length === 1 && newStatuses?.[0]?.reblog; - if (newStatuses.length) { - if (states.settings.boostsCarousel && hasOneAndReblog) { - // do nothing - } else { - states.homeNew = newStatuses.map((status) => { - saveStatus(status); - return { - id: status.id, - reblog: status.reblog?.id, - reply: !!status.inReplyToAccountId, - }; - }); - console.log('homeNew 2', [...states.homeNew]); - } - } +// saveStatus(notification.status, instance, { override: false }); +// } +// } catch (e) { +// // Silently fail +// console.error(e); +// } finally { +// startStream(); +// } +// })(); +// } +// } +// }; - const newNotifications = await fetchNotifications; - if (newNotifications.length) { - const notification = newNotifications[0]; - const inNotificationsNew = states.notificationsNew.find( - (n) => n.id === notification.id, - ); - const inNotifications = - notification.id === states.notificationLast?.id; - if (!inNotificationsNew && !inNotifications) { - states.notificationsNew.unshift(notification); - } - - saveStatus(notification.status, { override: false }); - } - } catch (e) { - // Silently fail - console.error(e); - } finally { - startStream(); - } - })(); - } - } - }; - - const handleVisibilityChange = () => { - const hidden = document.visibilityState === 'hidden'; - handleVisible(!hidden); - console.log('VISIBILITY: ' + (hidden ? 'hidden' : 'visible')); - }; - document.addEventListener('visibilitychange', handleVisibilityChange); - requestAnimationFrame(handleVisibilityChange); - return { - stop: () => { - document.removeEventListener('visibilitychange', handleVisibilityChange); - }, - }; -} +// const handleVisibilityChange = () => { +// const hidden = document.visibilityState === 'hidden'; +// handleVisible(!hidden); +// console.log('VISIBILITY: ' + (hidden ? 'hidden' : 'visible')); +// }; +// document.addEventListener('visibilitychange', handleVisibilityChange); +// requestAnimationFrame(handleVisibilityChange); +// return { +// stop: () => { +// document.removeEventListener('visibilitychange', handleVisibilityChange); +// }, +// }; +// } export { App }; diff --git a/src/components/AsyncText.jsx b/src/components/AsyncText.jsx new file mode 100644 index 00000000..7d10346a --- /dev/null +++ b/src/components/AsyncText.jsx @@ -0,0 +1,12 @@ +import { useEffect, useState } from 'preact/hooks'; + +function AsyncText({ children }) { + if (typeof children === 'string') return children; + const [text, setText] = useState(''); + useEffect(() => { + Promise.resolve(children).then(setText); + }, [children]); + return text; +} + +export default AsyncText; diff --git a/src/components/MenuLink.jsx b/src/components/MenuLink.jsx new file mode 100644 index 00000000..ed799844 --- /dev/null +++ b/src/components/MenuLink.jsx @@ -0,0 +1,21 @@ +import { FocusableItem } from '@szhsin/react-menu'; + +import Link from './link'; + +function MenuLink(props) { + return ( + + {({ ref, closeMenu }) => ( + + closeMenu(detail === 0 ? 'Enter' : undefined) + } + /> + )} + + ); +} + +export default MenuLink; diff --git a/src/components/account-block.css b/src/components/account-block.css new file mode 100644 index 00000000..2b760d80 --- /dev/null +++ b/src/components/account-block.css @@ -0,0 +1,14 @@ +.account-block { + display: flex; + align-items: center; + gap: 8px; + color: var(--text-color); + text-decoration: none; +} +.account-block:hover b { + text-decoration: underline; +} + +.account-block.skeleton { + color: var(--bg-faded-color); +} diff --git a/src/components/account-block.jsx b/src/components/account-block.jsx new file mode 100644 index 00000000..c8c2e54b --- /dev/null +++ b/src/components/account-block.jsx @@ -0,0 +1,66 @@ +import './account-block.css'; + +import emojifyText from '../utils/emojify-text'; +import states from '../utils/states'; + +import Avatar from './avatar'; + +function AccountBlock({ + skeleton, + account, + avatarSize = 'xl', + instance, + external, + onClick, +}) { + if (skeleton) { + return ( + + ); + } + + const { acct, avatar, avatarStatic, displayName, username, emojis, url } = + account; + const displayNameWithEmoji = emojifyText(displayName, emojis); + + return ( + + ); +} + +export default AccountBlock; diff --git a/src/components/account.jsx b/src/components/account.jsx index 87feb00f..e24d0958 100644 --- a/src/components/account.jsx +++ b/src/components/account.jsx @@ -2,19 +2,27 @@ import './account.css'; import { useEffect, useState } from 'preact/hooks'; +import { api } from '../utils/api'; import emojifyText from '../utils/emojify-text'; import enhanceContent from '../utils/enhance-content'; import handleContentLinks from '../utils/handle-content-links'; import shortenNumber from '../utils/shorten-number'; -import states from '../utils/states'; +import states, { hideAllModals } from '../utils/states'; import store from '../utils/store'; +import AccountBlock from './account-block'; import Avatar from './avatar'; import Icon from './icon'; import Link from './link'; -import NameText from './name-text'; -function Account({ account, onClose }) { +function Account({ account, instance: propInstance, onClose }) { + const { masto, instance, authenticated } = api({ instance: propInstance }); + const { + masto: currentMasto, + instance: currentInstance, + authenticated: currentAuthenticated, + } = api(); + const sameInstance = instance === currentInstance; const [uiState, setUIState] = useState('default'); const isString = typeof account === 'string'; const [info, setInfo] = useState(isString ? null : account); @@ -36,16 +44,18 @@ function Account({ account, onClose }) { q: account, type: 'accounts', limit: 1, - resolve: true, + resolve: authenticated, }); if (result.accounts.length) { setInfo(result.accounts[0]); setUIState('default'); return; } + setInfo(null); setUIState('error'); } catch (err) { console.error(err); + setInfo(null); setUIState('error'); } } @@ -84,17 +94,42 @@ function Account({ account, onClose }) { useEffect(() => { if (info) { const currentAccount = store.session.get('currentAccount'); - if (currentAccount === id) { - // It's myself! - return; - } - setRelationshipUIState('loading'); - setFamiliarFollowers([]); - + let accountID; (async () => { - const fetchRelationships = masto.v1.accounts.fetchRelationships([id]); + if (sameInstance && authenticated) { + accountID = id; + } else if (!sameInstance && currentAuthenticated) { + // Grab this account from my logged-in instance + const acctHasInstance = info.acct.includes('@'); + try { + const results = await currentMasto.v2.search({ + q: acctHasInstance ? info.acct : `${info.username}@${instance}`, + type: 'accounts', + limit: 1, + resolve: true, + }); + console.log('🥏 Fetched account from logged-in instance', results); + accountID = results.accounts[0].id; + } catch (e) { + console.error(e); + } + } + + if (!accountID) return; + + if (currentAccount === accountID) { + // It's myself! + return; + } + + setRelationshipUIState('loading'); + setFamiliarFollowers([]); + + const fetchRelationships = currentMasto.v1.accounts.fetchRelationships([ + accountID, + ]); const fetchFamiliarFollowers = - masto.v1.accounts.fetchFamiliarFollowers(id); + currentMasto.v1.accounts.fetchFamiliarFollowers(accountID); try { const relationships = await fetchRelationships; @@ -120,7 +155,7 @@ function Account({ account, onClose }) { } })(); } - }, [info]); + }, [info, authenticated]); const { following, @@ -154,8 +189,7 @@ function Account({ account, onClose }) { {uiState === 'loading' ? ( <>
- - ███ ████████████ +
@@ -173,8 +207,12 @@ function Account({ account, onClose }) { info && ( <>
- - +
{bot && ( @@ -186,7 +224,9 @@ function Account({ account, onClose }) { )}
)}

- + { + hideAllModals(); + }} + > Posts
@@ -265,7 +310,10 @@ function Account({ account, onClose }) { rel="noopener noreferrer" onClick={(e) => { e.preventDefault(); - states.showAccount = follower; + states.showAccount = { + account: follower, + instance, + }; }} > { + const { type, ...params } = shortcut; + const Component = { + following: Following, + notifications: Notifications, + list: List, + public: Public, + bookmarks: Bookmarks, + favourites: Favourites, + hashtag: Hashtag, + }[type]; + if (!Component) return null; + return ; + }); + + useHotkeys(['1', '2', '3', '4', '5', '6', '7', '8', '9'], (e, handler) => { + try { + const index = parseInt(handler.keys[0], 10) - 1; + document.querySelectorAll('#columns > *')[index].focus(); + } catch (e) { + console.error(e); + } + }); + + return

{components}
; +} + +export default Columns; diff --git a/src/components/compose.css b/src/components/compose.css index 5bdb8cd3..8c0d0956 100644 --- a/src/components/compose.css +++ b/src/components/compose.css @@ -1,5 +1,5 @@ #compose-container { - width: 40em; + width: var(--main-width); max-width: 100vw; align-self: stretch; animation: fade-in 0.2s ease-out; @@ -60,6 +60,11 @@ animation: appear-up 1s ease-in-out; overflow: auto; } +#compose-container .status-preview .hashtag { + /* Prevent hashtags from being clickable */ + /* TODO: maybe use a different solution? */ + pointer-events: none; +} #compose-container.standalone .status-preview * { /* For standalone mode (new window), prevent interacting with the status preview for now @@ -164,7 +169,7 @@ left: -100vw !important; } #compose-container .toolbar-button select { - background-color: transparent; + background-color: inherit; border: 0; padding: 0 0 0 8px; } diff --git a/src/components/compose.jsx b/src/components/compose.jsx index fd170e93..c0a2bb2f 100644 --- a/src/components/compose.jsx +++ b/src/components/compose.jsx @@ -3,7 +3,7 @@ import './compose.css'; import '@github/text-expander-element'; import equal from 'fast-deep-equal'; import { forwardRef } from 'preact/compat'; -import { useEffect, useMemo, useRef, useState } from 'preact/hooks'; +import { useEffect, useRef, useState } from 'preact/hooks'; import { useHotkeys } from 'react-hotkeys-hook'; import stringLength from 'string-length'; import { uid } from 'uid/single'; @@ -12,12 +12,18 @@ import { useSnapshot } from 'valtio'; import supportedLanguages from '../data/status-supported-languages'; import urlRegex from '../data/url-regex'; +import { api } from '../utils/api'; import db from '../utils/db'; import emojifyText from '../utils/emojify-text'; import openCompose from '../utils/open-compose'; -import states from '../utils/states'; +import states, { saveStatus } from '../utils/states'; import store from '../utils/store'; -import { getCurrentAccount, getCurrentAccountNS } from '../utils/store-utils'; +import { + getCurrentAccount, + getCurrentAccountNS, + getCurrentInstance, +} from '../utils/store-utils'; +import supports from '../utils/supports'; import useInterval from '../utils/useInterval'; import visibilityIconsMap from '../utils/visibility-icons-map'; @@ -99,6 +105,7 @@ function Compose({ hasOpener, }) { console.warn('RENDER COMPOSER'); + const { masto } = api(); const [uiState, setUIState] = useState('default'); const UID = useRef(draftStatus?.uid || uid()); console.log('Compose UID', UID.current); @@ -106,22 +113,8 @@ function Compose({ const currentAccount = getCurrentAccount(); const currentAccountInfo = currentAccount.info; - const configuration = useMemo(() => { - try { - const instances = store.local.getJSON('instances'); - const currentInstance = currentAccount.instanceURL.toLowerCase(); - const config = instances[currentInstance].configuration; - console.log(config); - return config; - } catch (e) { - console.error(e); - alert('Failed to load instance configuration. Please try again.'); - // Temporary fix for corrupted data - store.local.del('instances'); - location.reload(); - return {}; - } - }, []); + const { configuration } = getCurrentInstance(); + console.log('⚙️ Configuration', configuration); const { statuses: { maxCharacters, maxMediaAttachments, charactersReservedPerUrl }, @@ -146,6 +139,8 @@ function Compose({ const [mediaAttachments, setMediaAttachments] = useState([]); const [poll, setPoll] = useState(null); + const prefs = store.account.get('preferences') || {}; + const customEmojis = useRef(); useEffect(() => { (async () => { @@ -192,7 +187,7 @@ function Compose({ } focusTextarea(); setVisibility(visibility); - setLanguage(language || DEFAULT_LANG); + setLanguage(language || prefs.postingDefaultLanguage || DEFAULT_LANG); setSensitive(sensitive); } if (draftStatus) { @@ -215,7 +210,7 @@ function Compose({ focusTextarea(); spoilerTextRef.current.value = spoilerText; setVisibility(visibility); - setLanguage(language || DEFAULT_LANG); + setLanguage(language || prefs.postingDefaultLanguage || DEFAULT_LANG); setSensitive(sensitive); setPoll(composablePoll); setMediaAttachments(mediaAttachments); @@ -241,7 +236,7 @@ function Compose({ focusTextarea(); spoilerTextRef.current.value = spoilerText; setVisibility(visibility); - setLanguage(language || DEFAULT_LANG); + setLanguage(language || presf.postingDefaultLanguage || DEFAULT_LANG); setSensitive(sensitive); setPoll(composablePoll); setMediaAttachments(mediaAttachments); @@ -254,13 +249,22 @@ function Compose({ })(); } else { focusTextarea(); + console.log('Apply prefs', prefs); + if (prefs.postingDefaultVisibility) { + setVisibility(prefs.postingDefaultVisibility); + } + if (prefs.postingDefaultLanguage) { + setLanguage(prefs.postingDefaultLanguage); + } + if (prefs.postingDefaultSensitive) { + setSensitive(prefs.postingDefaultSensitive); + } } }, [draftStatus, editStatus, replyToStatus]); const formRef = useRef(); - const beforeUnloadCopy = - 'You have unsaved changes. Are you sure you want to discard this post?'; + const beforeUnloadCopy = 'You have unsaved changes. Discard this post?'; const canClose = () => { const { value, dataset } = textareaRef.current; @@ -362,6 +366,8 @@ function Compose({ }, { enableOnFormTags: true, + // Use keyup because Esc keydown will close the confirm dialog on Safari + keyup: true, }, ); @@ -757,7 +763,16 @@ function Compose({ // mediaIds: mediaAttachments.map((attachment) => attachment.id), media_ids: mediaAttachments.map((attachment) => attachment.id), }; - if (!editStatus) { + if (editStatus && supports('@mastodon/edit-media-attributes')) { + params.media_attributes = mediaAttachments.map((attachment) => { + return { + id: attachment.id, + description: attachment.description, + // focus + // thumbnail + }; + }); + } else if (!editStatus) { params.visibility = visibility; // params.inReplyToId = replyToStatus?.id || undefined; params.in_reply_to_id = replyToStatus?.id || undefined; @@ -771,6 +786,9 @@ function Compose({ editStatus.id, params, ); + saveStatus(newStatus, { + skipThreading: true, + }); } else { newStatus = await masto.v1.statuses.create(params, { idempotencyKey: UID.current, @@ -799,6 +817,7 @@ function Compose({ disabled={uiState === 'loading'} class="spoiler-text-field" lang={language} + spellCheck="true" style={{ opacity: sensitive ? 1 : 0, pointerEvents: sensitive ? 'auto' : 'none', @@ -868,6 +887,9 @@ function Compose({ updateCharCount(); }} maxCharacters={maxCharacters} + performSearch={(params) => { + return masto.v2.search(params); + }} /> {mediaAttachments.length > 0 && (
@@ -1031,7 +1053,7 @@ function Compose({ const Textarea = forwardRef((props, ref) => { const [text, setText] = useState(ref.current?.value || ''); - const { maxCharacters, ...textareaProps } = props; + const { maxCharacters, performSearch = () => {}, ...textareaProps } = props; const snapStates = useSnapshot(states); const charCount = snapStates.composerCharacterCount; @@ -1087,7 +1109,7 @@ const Textarea = forwardRef((props, ref) => { }[key]; provide( new Promise((resolve) => { - const searchResults = masto.v2.search({ + const searchResults = performSearch({ type, q: text, limit: 5, @@ -1258,6 +1280,7 @@ function MediaAttachment({ onDescriptionChange = () => {}, onRemove = () => {}, }) { + const supportsEdit = supports('@mastodon/edit-media-attributes'); const { url, type, id } = attachment; console.log({ attachment }); const [description, setDescription] = useState(attachment.description); @@ -1283,7 +1306,7 @@ function MediaAttachment({ const descTextarea = ( <> - {!!id ? ( + {!!id && !supportsEdit ? (
Uploaded

@@ -1413,6 +1436,7 @@ function Poll({ maxlength={maxCharactersPerOption} placeholder={`Choice ${i + 1}`} lang={lang} + spellCheck="true" onInput={(e) => { const { value } = e.target; options[i] = value; diff --git a/src/components/drafts.jsx b/src/components/drafts.jsx index 96aca1ed..28c7c35d 100644 --- a/src/components/drafts.jsx +++ b/src/components/drafts.jsx @@ -2,6 +2,7 @@ import './drafts.css'; import { useEffect, useMemo, useReducer, useState } from 'react'; +import { api } from '../utils/api'; import db from '../utils/db'; import states from '../utils/states'; import { getCurrentAccountNS } from '../utils/store-utils'; @@ -10,6 +11,7 @@ import Icon from './icon'; import Loader from './loader'; function Drafts() { + const { masto } = api(); const [uiState, setUIState] = useState('default'); const [drafts, setDrafts] = useState([]); const [reloadCount, reload] = useReducer((c) => c + 1, 0); @@ -101,9 +103,7 @@ function Drafts() { onClick={() => { (async () => { try { - const yes = confirm( - 'Are you sure you want to delete this draft?', - ); + const yes = confirm('Delete this draft?'); if (yes) { await db.drafts.del(key); reload(); @@ -159,9 +159,7 @@ function Drafts() { disabled={uiState === 'loading'} onClick={() => { (async () => { - const yes = confirm( - 'Are you sure you want to delete all drafts?', - ); + const yes = confirm('Delete all drafts?'); if (yes) { setUIState('loading'); try { diff --git a/src/components/icon.jsx b/src/components/icon.jsx index ffd39a69..82221ee1 100644 --- a/src/components/icon.jsx +++ b/src/components/icon.jsx @@ -26,7 +26,7 @@ const ICONS = { 'eye-open': 'mingcute:eye-2-line', message: 'mingcute:mail-line', comment: 'mingcute:chat-3-line', - home: 'mingcute:home-5-line', + home: 'mingcute:home-3-line', notification: 'mingcute:notification-line', follow: 'mingcute:user-follow-line', 'follow-add': 'mingcute:user-add-line', @@ -48,6 +48,15 @@ const ICONS = { thread: 'mingcute:route-line', group: 'mingcute:group-line', bot: 'mingcute:android-2-line', + menu: 'mingcute:rows-4-line', + list: 'mingcute:list-check-line', + search: 'mingcute:search-2-line', + hashtag: 'mingcute:hashtag-line', + info: 'mingcute:information-line', + shortcut: 'mingcute:lightning-line', + user: 'mingcute:user-4-line', + following: 'mingcute:walk-line', + pin: 'mingcute:pin-line', }; const modules = import.meta.glob('/node_modules/@iconify-icons/mingcute/*.js'); diff --git a/src/components/link.jsx b/src/components/link.jsx index c55084c6..ba4886d7 100644 --- a/src/components/link.jsx +++ b/src/components/link.jsx @@ -1,3 +1,4 @@ +import { forwardRef } from 'preact/compat'; import { useLocation } from 'react-router-dom'; import states from '../utils/states'; @@ -10,7 +11,7 @@ import states from '../utils/states'; 3. Not using because it modifies history.state that *persists* across page reloads. I don't need that, so using valtio's states instead. */ -const Link = (props) => { +const Link = forwardRef((props, ref) => { let routerLocation; try { routerLocation = useLocation(); @@ -21,6 +22,7 @@ const Link = (props) => { const isActive = hash === to; return ( { }} /> ); -}; +}); export default Link; diff --git a/src/components/media-modal.jsx b/src/components/media-modal.jsx index f9b5ef63..4adb542b 100644 --- a/src/components/media-modal.jsx +++ b/src/components/media-modal.jsx @@ -1,7 +1,6 @@ import { getBlurHashAverageColor } from 'fast-blurhash'; import { useEffect, useLayoutEffect, useRef, useState } from 'preact/hooks'; import { useHotkeys } from 'react-hotkeys-hook'; -import { useMatch } from 'react-router-dom'; import Icon from './icon'; import Link from './link'; @@ -11,11 +10,11 @@ import Modal from './modal'; function MediaModal({ mediaAttachments, statusID, + instance, index = 0, onClose = () => {}, }) { const carouselRef = useRef(null); - const isStatusLocation = useMatch('/s/:id'); const [currentIndex, setCurrentIndex] = useState(index); const carouselFocusItem = useRef(null); @@ -119,7 +118,7 @@ function MediaModal({ setShowMediaAlt(media.description); }} > - ALT{' '} + {media.description} )} @@ -165,22 +164,20 @@ function MediaModal({ )} - {!isStatusLocation && ( - { - // if small screen (not media query min-width 40em + 350px), run onClose - if ( - !window.matchMedia('(min-width: calc(40em + 350px))').matches - ) { - onClose(); - } - }} - > - See post » - - )}{' '} + { + // if small screen (not media query min-width 40em + 350px), run onClose + if ( + !window.matchMedia('(min-width: calc(40em + 350px))').matches + ) { + onClose(); + } + }} + > + See post » + {' '} pin.type === 'following'); + + return ( +

+ + + } + > + + Home + + {authenticated && ( + <> + {showFollowing && ( + + Following + + )} + + Notifications + {snapStates.notificationsShowNew && ( + + {' '} + • + + )} + + + + Lists + + + Followed Hashtags + + + Bookmarks + + + Favourites + + + )} + + + Search + + + Local + + + Federated + + {authenticated && ( + <> + + { + states.showShortcutsSettings = true; + }} + > + {' '} + Shortcuts Settings… + + { + states.showSettings = true; + }} + > + Settings… + + + )} + + ); +} + +export default NavMenu; diff --git a/src/components/name-text.jsx b/src/components/name-text.jsx index 3a9b9aea..0137f500 100644 --- a/src/components/name-text.jsx +++ b/src/components/name-text.jsx @@ -5,21 +5,31 @@ import states from '../utils/states'; import Avatar from './avatar'; -function NameText({ account, showAvatar, showAcct, short, external, onClick }) { +function NameText({ + account, + instance, + showAvatar, + showAcct, + short, + external, + onClick, +}) { const { acct, avatar, avatarStatic, id, url, displayName, emojis } = account; let { username } = account; const displayNameWithEmoji = emojifyText(displayName, emojis); + const trimmedUsername = username.toLowerCase().trim(); + const trimmedDisplayName = (displayName || '').toLowerCase().trim(); + const shortenedDisplayName = trimmedDisplayName + .replace(/(\:(\w|\+|\-)+\:)(?=|[\!\.\?]|$)/g, '') // Remove shortcodes, regex from https://regex101.com/r/iE9uV0/1 + .replace(/\s+/g, '') // E.g. "My name" === "myname" + .replace(/[^a-z0-9]/gi, ''); // Remove non-alphanumeric characters + if ( !short && - username.toLowerCase().trim() === - (displayName || '') - .replace(/(\:(\w|\+|\-)+\:)(?=|[\!\.\?]|$)/g, '') // Remove shortcodes, regex from https://regex101.com/r/iE9uV0/1 - .replace(/\s+/g, '') // E.g. "My name" === "myname" - .replace(/[^a-z0-9]/gi, '') // Remove non-alphanumeric characters - .toLowerCase() - .trim() + (trimmedUsername === trimmedDisplayName || + trimmedUsername === shortenedDisplayName) ) { username = null; } @@ -34,7 +44,10 @@ function NameText({ account, showAvatar, showAcct, short, external, onClick }) { if (external) return; e.preventDefault(); if (onClick) return onClick(e); - states.showAccount = account; + states.showAccount = { + account, + instance, + }; }} > {showAvatar && ( diff --git a/src/components/shortcuts-settings.css b/src/components/shortcuts-settings.css new file mode 100644 index 00000000..bec8246a --- /dev/null +++ b/src/components/shortcuts-settings.css @@ -0,0 +1,73 @@ +#shortcuts-settings-container .shortcuts-list { + line-height: 1.5; + padding: 0; + margin: 8px 0 0; + counter-reset: index; + border-radius: 8px; + overflow: hidden; +} +#shortcuts-settings-container .shortcuts-list li { + display: flex; + align-items: center; + padding: 8px; + gap: 4px; + background-color: var(--bg-faded-color); +} +#shortcuts-settings-container .shortcuts-list li::before { + content: counter(index); + counter-increment: index; + display: inline-block; + width: 1.2em; + text-align: right; + margin-right: 8px; + color: var(--text-insignificant-color); + font-size: 90%; +} +#shortcuts-settings-container .shortcuts-list li .shortcut-text { + flex-grow: 1; +} + +#shortcuts-settings-container summary { + cursor: pointer; +} + +#shortcuts-settings-container form { + display: flex; + gap: 8px; + flex-wrap: wrap; + align-items: center; + padding: 16px; + background-color: var(--bg-faded-color); + border-radius: 16px; +} + +#shortcuts-settings-container form header { + display: flex; + align-items: center; + justify-content: space-between; +} + +#shortcuts-settings-container form > * { + flex-basis: max(320px, 100%); + margin: 0; + padding: 0; +} + +#shortcuts-settings-container form label { + display: flex; + flex-direction: row; + gap: 8px; + align-items: center; +} +#shortcuts-settings-container form label > span:first-child { + flex-basis: 5em; + text-align: right; +} +#shortcuts-settings-container form :is(input[type='text'], select) { + flex-grow: 1; + flex-basis: 70%; + flex-shrink: 1; + /* width: calc(100% - 32px); */ + min-width: 0; + max-width: 320px; +} diff --git a/src/components/shortcuts-settings.jsx b/src/components/shortcuts-settings.jsx new file mode 100644 index 00000000..84cb01dc --- /dev/null +++ b/src/components/shortcuts-settings.jsx @@ -0,0 +1,387 @@ +import './shortcuts-settings.css'; + +import { useEffect, useState } from 'preact/hooks'; +import { useSnapshot } from 'valtio'; + +import { api } from '../utils/api'; +import states from '../utils/states'; + +import AsyncText from './AsyncText'; +import Icon from './icon'; + +const SHORTCUTS_LIMIT = 9; + +const TYPES = [ + 'following', + 'notifications', + 'list', + 'public', + // NOTE: Hide for now + // 'search', // Search on Mastodon ain't great + // 'account-statuses', // Need @acct search first + 'bookmarks', + 'favourites', + 'hashtag', +]; +const TYPE_TEXT = { + following: 'Home / Following', + notifications: 'Notifications', + list: 'List', + public: 'Public', + search: 'Search', + 'account-statuses': 'Account', + bookmarks: 'Bookmarks', + favourites: 'Favourites', + hashtag: 'Hashtag', +}; +const TYPE_PARAMS = { + list: [ + { + text: 'List ID', + name: 'id', + }, + ], + public: [ + { + text: 'Local only', + name: 'local', + type: 'checkbox', + }, + { + text: 'Instance', + name: 'instance', + type: 'text', + placeholder: 'e.g. mastodon.social', + }, + ], + search: [ + { + text: 'Search term', + name: 'query', + type: 'text', + }, + ], + 'account-statuses': [ + { + text: '@', + name: 'id', + type: 'text', + placeholder: 'cheeaun@mastodon.social', + }, + ], + hashtag: [ + { + text: '#', + name: 'hashtag', + type: 'text', + placeholder: 'e.g PixelArt', + }, + ], +}; +export const SHORTCUTS_META = { + following: { + title: 'Home / Following', + path: (_, index) => (index === 0 ? '/' : '/following'), + icon: 'home', + }, + notifications: { + title: 'Notifications', + path: '/notifications', + icon: 'notification', + }, + list: { + title: async ({ id }) => { + const list = await api().masto.v1.lists.fetch(id); + return list.title; + }, + path: ({ id }) => `/l/${id}`, + icon: 'list', + }, + public: { + title: ({ local, instance }) => + `${local ? 'Local' : 'Federated'} (${instance})`, + path: ({ local, instance }) => `/${instance}/p${local ? '/l' : ''}`, + icon: ({ local }) => (local ? 'group' : 'earth'), + }, + search: { + title: ({ query }) => query, + path: ({ query }) => `/search?q=${query}`, + icon: 'search', + }, + 'account-statuses': { + title: async ({ id }) => { + const account = await api().masto.v1.accounts.fetch(id); + return account.username || account.acct || account.displayName; + }, + path: ({ id }) => `/a/${id}`, + icon: 'user', + }, + bookmarks: { + title: 'Bookmarks', + path: '/b', + icon: 'bookmark', + }, + favourites: { + title: 'Favourites', + path: '/f', + icon: 'heart', + }, + hashtag: { + title: ({ hashtag }) => hashtag, + path: ({ hashtag }) => `/t/${hashtag}`, + icon: 'hashtag', + }, +}; + +function ShortcutsSettings() { + const snapStates = useSnapshot(states); + const { masto } = api(); + const { shortcuts } = snapStates; + + const [lists, setLists] = useState([]); + const [followedHashtags, setFollowedHashtags] = useState([]); + + useEffect(() => { + (async () => { + try { + const lists = await masto.v1.lists.list(); + setLists(lists); + } catch (e) { + console.error(e); + } + })(); + + (async () => { + try { + const iterator = masto.v1.followedTags.list(); + const tags = []; + do { + const { value, done } = await iterator.next(); + if (done || value?.length === 0) break; + tags.push(...value); + } while (true); + setFollowedHashtags(tags); + } catch (e) { + console.error(e); + } + })(); + }, []); + + return ( +
+
+

+ Shortcuts{' '} + + beta + +

+
+
+

+ Specify a list of shortcuts that'll appear in the floating Shortcuts + button. +

+

+

+ + Experimental Multi-column mode + + +
+

+ {shortcuts.length > 0 ? ( +
    + {shortcuts.map((shortcut, i) => { + const key = i + Object.values(shortcut); + const { type } = shortcut; + if (!SHORTCUTS_META[type]) return null; + let { icon, title } = SHORTCUTS_META[type]; + if (typeof title === 'function') { + title = title(shortcut); + } + if (typeof icon === 'function') { + icon = icon(shortcut); + } + return ( +
  1. + + + {title} + + + + + + +
  2. + ); + })} +
+ ) : ( +

+ No shortcuts yet. Add one from the form below. +

+ )} +
+ = SHORTCUTS_LIMIT} + lists={lists} + followedHashtags={followedHashtags} + onSubmit={(data) => { + console.log('onSubmit', data); + states.shortcuts.push(data); + }} + /> +
+
+ ); +} + +export default ShortcutsSettings; +function ShortcutForm({ type, lists, followedHashtags, onSubmit, disabled }) { + const [currentType, setCurrentType] = useState(type); + return ( + <> +
{ + // Construct a nice object from form + e.preventDefault(); + const data = new FormData(e.target); + const result = {}; + data.forEach((value, key) => { + result[key] = value; + }); + if (!result.type) return; + onSubmit(result); + // Reset + e.target.reset(); + setCurrentType(null); + }} + > +
+

Add a shortcut

+ +
+

+ +

+ {TYPE_PARAMS[currentType]?.map?.( + ({ text, name, type, placeholder }) => { + if (currentType === 'list') { + return ( +

+ +

+ ); + } + + return ( +

+ +

+ ); + }, + )} +
+ + ); +} diff --git a/src/components/shortcuts.css b/src/components/shortcuts.css new file mode 100644 index 00000000..851a9d05 --- /dev/null +++ b/src/components/shortcuts.css @@ -0,0 +1,46 @@ +#shortcuts-button { + position: fixed; + bottom: 16px; + bottom: max(16px, env(safe-area-inset-bottom)); + left: 16px; + left: max(16px, env(safe-area-inset-left)); + padding: 16px; + background-color: var(--bg-faded-blur-color); + z-index: 101; + transition: all 0.3s ease-in-out; + border: var(--hairline-width) solid var(--bg-color); + box-shadow: inset 0 -4px 16px -8px var(--button-bg-blur-color), + 0 3px 8px -1px var(--drop-shadow-color); +} +#shortcuts-button .icon { + transform: translateY(2px); /* Balance the icon's vertical alignment */ +} +#app:has(header[hidden]) #shortcuts-button, +#shortcuts-button[hidden] { + transform: translateY(200%); + pointer-events: none; + user-select: none; +} +#shortcuts-button:is(:hover, :focus) { + background-color: var(--bg-color); + filter: none; +} +#shortcuts-button:active { + transform: scale(0.95); + transition: none; +} + +@media (min-width: calc(40em + 56px + 8px)) { + #shortcuts-button { + right: 16px; + right: max(16px, env(safe-area-inset-right)); + left: auto; + top: 16px; + top: max(16px, env(safe-area-inset-top)); + bottom: auto; + } + #app:has(header[hidden]) #shortcuts-button, + #shortcuts-button[hidden] { + transform: translateY(-200%); + } +} diff --git a/src/components/shortcuts.jsx b/src/components/shortcuts.jsx new file mode 100644 index 00000000..280f75cc --- /dev/null +++ b/src/components/shortcuts.jsx @@ -0,0 +1,109 @@ +import './shortcuts.css'; + +import { Menu, MenuItem } from '@szhsin/react-menu'; +import { useRef } from 'preact/hooks'; +import { useHotkeys } from 'react-hotkeys-hook'; +import { useNavigate } from 'react-router-dom'; +import { useSnapshot } from 'valtio'; + +import { SHORTCUTS_META } from '../components/shortcuts-settings'; +import states from '../utils/states'; + +import AsyncText from './AsyncText'; +import Icon from './icon'; +import MenuLink from './MenuLink'; + +function Shortcuts() { + const snapStates = useSnapshot(states); + const { shortcuts } = snapStates; + + if (!shortcuts.length) { + return null; + } + + const menuRef = useRef(); + + const formattedShortcuts = shortcuts + .map((pin, i) => { + const { type, ...data } = pin; + if (!SHORTCUTS_META[type]) return null; + let { path, title, icon } = SHORTCUTS_META[type]; + + if (typeof path === 'function') { + path = path(data, i); + } + if (typeof title === 'function') { + title = title(data); + } + if (typeof icon === 'function') { + icon = icon(data); + } + + return { + path, + title, + icon, + }; + }) + .filter(Boolean); + + const navigate = useNavigate(); + useHotkeys(['1', '2', '3', '4', '5', '6', '7', '8', '9'], (e, handler) => { + const index = parseInt(handler.keys[0], 10) - 1; + if (index < formattedShortcuts.length) { + const { path } = formattedShortcuts[index]; + if (path) { + navigate(path); + menuRef.current?.closeMenu?.(); + } + } + }); + + return ( +
+ { + // Close menu if the button disappears + try { + const { target } = e; + if (getComputedStyle(target).pointerEvents === 'none') { + menuRef.current?.closeMenu?.(); + } + } catch (e) {} + }} + > + + + } + > + {formattedShortcuts.map(({ path, title, icon }, i) => { + return ( + + {' '} + + {title} + + + {i + 1} + + + ); + })} + +
+ ); +} + +export default Shortcuts; diff --git a/src/components/status.css b/src/components/status.css index 58e332aa..5a349c6b 100644 --- a/src/components/status.css +++ b/src/components/status.css @@ -286,6 +286,8 @@ .status .content p { /* 12px = 75% of 16px */ margin-block: min(0.75em, 12px); + white-space: pre-wrap; + tab-size: 2; } .status .content p:first-child { margin-block-start: 0; @@ -422,6 +424,9 @@ .status .media img:hover { animation: position-object 5s ease-in-out 1s 5; } +body:has(#modal-container .carousel) .status .media img:hover { + animation: none; +} .status .media video { width: 100%; height: 100%; @@ -443,7 +448,7 @@ top: 50%; transform: translate(-50%, -50%); color: var(--text-insignificant-color); - background-color: var(--backdrop-color); + background-color: var(--bg-faded-blur-color); backdrop-filter: blur(6px) saturate(3) invert(0.2); display: flex; place-content: center; @@ -532,32 +537,26 @@ text-align: left; border-radius: 8px; color: var(--text-color); - padding: 4px 8px 4px 4px; + padding: 4px 8px; border: 1px solid var(--outline-color); box-shadow: 0 4px 16px var(--outline-color); - max-width: min(40em, calc(100% - 32px)); + max-width: min(var(--main-width), calc(100% - 32px)); display: flex; align-items: center; - gap: 4px; + gap: 8px; font-size: 90%; } .carousel-item button.media-alt .media-alt-desc { - white-space: nowrap; overflow: hidden; - text-overflow: ellipsis; + white-space: normal; + display: -webkit-box; + display: box; + -webkit-box-orient: vertical; + box-orient: vertical; + -webkit-line-clamp: 2; + line-clamp: 2; line-height: 1.4; } -@media (min-width: 40em) { - .carousel-item button.media-alt .media-alt-desc { - white-space: normal; - display: -webkit-box; - display: box; - -webkit-box-orient: vertical; - box-orient: vertical; - -webkit-line-clamp: 2; - line-clamp: 2; - } -} .carousel-item button.media-alt[hidden] { opacity: 0; } @@ -897,6 +896,7 @@ a.card:is(:hover, :focus) { transparent 160px ); white-space: pre-wrap; + line-height: 1.2; } .status .content p code { @@ -922,6 +922,9 @@ a.card:is(:hover, :focus) { .status-badge .bookmark { color: var(--link-color); } +.status-badge .pin { + color: var(--red-color); +} /* MISC */ @@ -949,7 +952,8 @@ a.card:is(:hover, :focus) { padding: 0; } -#edit-history :is(ol, ol li) { +#edit-history ol, +#edit-history ol li { list-style: none; margin: 0; padding: 0; diff --git a/src/components/status.jsx b/src/components/status.jsx index 08cf2b1a..d32fd7df 100644 --- a/src/components/status.jsx +++ b/src/components/status.jsx @@ -1,17 +1,9 @@ import './status.css'; import { Menu, MenuItem } from '@szhsin/react-menu'; -import { getBlurHashAverageColor } from 'fast-blurhash'; import mem from 'mem'; import { memo } from 'preact/compat'; -import { - useEffect, - useLayoutEffect, - useMemo, - useRef, - useState, -} from 'preact/hooks'; -import { useHotkeys } from 'react-hotkeys-hook'; +import { useEffect, useMemo, useRef, useState } from 'preact/hooks'; import 'swiped-events'; import useResizeObserver from 'use-resize-observer'; import { useSnapshot } from 'valtio'; @@ -19,11 +11,12 @@ import { useSnapshot } from 'valtio'; import Loader from '../components/loader'; import Modal from '../components/modal'; import NameText from '../components/name-text'; +import { api } from '../utils/api'; import enhanceContent from '../utils/enhance-content'; import handleContentLinks from '../utils/handle-content-links'; import htmlContentLength from '../utils/html-content-length'; import shortenNumber from '../utils/shorten-number'; -import states, { saveStatus } from '../utils/states'; +import states, { saveStatus, statusKey } from '../utils/states'; import store from '../utils/store'; import visibilityIconsMap from '../utils/visibility-icons-map'; @@ -33,7 +26,7 @@ import Link from './link'; import Media from './media'; import RelativeTime from './relative-time'; -function fetchAccount(id) { +function fetchAccount(id, masto) { try { return masto.v1.accounts.fetch(id); } catch (e) { @@ -45,30 +38,36 @@ const memFetchAccount = mem(fetchAccount); function Status({ statusID, status, + instance: propInstance, withinContext, size = 'm', skeleton, readOnly, + contentTextWeight, }) { if (skeleton) { return (
-
███ ████████████
+
███ ████████
-

████ ████████████

+

████ ████████

); } + const { masto, instance, authenticated } = api({ instance: propInstance }); + const { instance: currentInstance } = api(); + const sameInstance = instance === currentInstance; + const sKey = statusKey(statusID, instance); const snapStates = useSnapshot(states); if (!status) { - status = snapStates.statuses[statusID]; + status = snapStates.statuses[sKey] || snapStates.statuses[statusID]; } if (!status) { return null; @@ -110,7 +109,9 @@ function Status({ reblog, uri, emojis, + // Non-API props _deleted, + _pinned, } = status; console.debug('RENDER Status', id, status?.account.displayName); @@ -135,7 +136,7 @@ function Status({ if (account) { setInReplyToAccount(account); } else { - memFetchAccount(inReplyToAccountId) + memFetchAccount(inReplyToAccountId, masto) .then((account) => { setInReplyToAccount(account); states.accounts[account.id] = account; @@ -157,9 +158,15 @@ function Status({
{' '} - boosted + {' '} + boosted
- +
); } @@ -198,13 +205,15 @@ function Status({ const statusRef = useRef(null); + const unauthInteractionErrorMessage = `Sorry, your current logged-in instance can't interact with this status from another instance.`; + return (
} {favourited && } {bookmarked && } + {_pinned && }
)} {size !== 's' && ( @@ -229,7 +239,10 @@ function Status({ onClick={(e) => { e.preventDefault(); e.stopPropagation(); - states.showAccount = status.account; + states.showAccount = { + account: status.account, + instance, + }; }} > @@ -240,6 +253,7 @@ function Status({ {/* */} @@ -248,14 +262,17 @@ function Status({ {' '} {' '} - + )} */} {/* */}{' '} {size !== 'l' && (uri ? ( - + {inReplyToAccountId === status.account?.id || - !!snapStates.statusThreadNumber[id] ? ( + !!snapStates.statusThreadNumber[sKey] ? (
Thread - {snapStates.statusThreadNumber[id] - ? ` ${snapStates.statusThreadNumber[id]}/X` + {snapStates.statusThreadNumber[sKey] + ? ` ${snapStates.statusThreadNumber[sKey]}/X` : ''}
) : ( @@ -294,7 +311,11 @@ function Status({ })) && (
{' '} - +
) )} @@ -302,10 +323,10 @@ function Status({ )}
- {!!spoilerText && sensitive && ( + {!!spoilerText && ( <>
{ - states.statuses[id].poll = newPoll; + states.statuses[sKey].poll = newPoll; + }} + refresh={() => { + return masto.v1.polls + .fetch(poll.id) + .then((pollResponse) => { + states.statuses[sKey].poll = pollResponse; + }) + .catch((e) => {}); // Silently fail + }} + votePoll={(choices) => { + return masto.v1.polls + .vote(poll.id, { + choices, + }) + .then((pollResponse) => { + states.statuses[sKey].poll = pollResponse; + }) + .catch((e) => {}); // Silently fail }} /> )} @@ -410,6 +449,7 @@ function Status({ states.showMediaModal = { mediaAttachments, index: i, + instance, statusID: readOnly ? null : id, }; }} @@ -477,6 +517,9 @@ function Status({ icon="comment" count={repliesCount} onClick={() => { + if (!sameInstance || !authenticated) { + return alert(unauthInteractionErrorMessage); + } states.showCompose = { replyToStatus: status, }; @@ -494,17 +537,18 @@ function Status({ icon="rocket" count={reblogsCount} onClick={async () => { + if (!sameInstance || !authenticated) { + return alert(unauthInteractionErrorMessage); + } try { if (!reblogged) { - const yes = confirm( - 'Are you sure that you want to boost this post?', - ); + const yes = confirm('Boost this post?'); if (!yes) { return; } } // Optimistic - states.statuses[id] = { + states.statuses[sKey] = { ...status, reblogged: !reblogged, reblogsCount: reblogsCount + (reblogged ? -1 : 1), @@ -513,15 +557,15 @@ function Status({ const newStatus = await masto.v1.statuses.unreblog( id, ); - saveStatus(newStatus); + saveStatus(newStatus, instance); } else { const newStatus = await masto.v1.statuses.reblog(id); - saveStatus(newStatus); + saveStatus(newStatus, instance); } } catch (e) { console.error(e); // Revert optimistism - states.statuses[id] = status; + states.statuses[sKey] = status; } }} /> @@ -536,9 +580,12 @@ function Status({ icon="heart" count={favouritesCount} onClick={async () => { + if (!sameInstance || !authenticated) { + return alert(unauthInteractionErrorMessage); + } try { // Optimistic - states.statuses[statusID] = { + states.statuses[sKey] = { ...status, favourited: !favourited, favouritesCount: @@ -548,15 +595,15 @@ function Status({ const newStatus = await masto.v1.statuses.unfavourite( id, ); - saveStatus(newStatus); + saveStatus(newStatus, instance); } else { const newStatus = await masto.v1.statuses.favourite(id); - saveStatus(newStatus); + saveStatus(newStatus, instance); } } catch (e) { console.error(e); // Revert optimistism - states.statuses[statusID] = status; + states.statuses[sKey] = status; } }} /> @@ -569,9 +616,12 @@ function Status({ class="bookmark-button" icon="bookmark" onClick={async () => { + if (!sameInstance || !authenticated) { + return alert(unauthInteractionErrorMessage); + } try { // Optimistic - states.statuses[statusID] = { + states.statuses[sKey] = { ...status, bookmarked: !bookmarked, }; @@ -579,15 +629,15 @@ function Status({ const newStatus = await masto.v1.statuses.unbookmark( id, ); - saveStatus(newStatus); + saveStatus(newStatus, instance); } else { const newStatus = await masto.v1.statuses.bookmark(id); - saveStatus(newStatus); + saveStatus(newStatus, instance); } } catch (e) { console.error(e); // Revert optimistism - states.statuses[statusID] = status; + states.statuses[sKey] = status; } }} /> @@ -615,7 +665,7 @@ function Status({ }; }} > - Edit… + Edit… )} @@ -635,6 +685,10 @@ function Status({ > { + return masto.v1.statuses.listHistory(showEdited); + }} onClose={() => { setShowEdited(false); statusRef.current?.focus(); @@ -698,12 +752,7 @@ function Card({ card }) {

{domain}

-

+

{title}

{description || providerName || authorName}

@@ -742,7 +791,13 @@ function Card({ card }) { } } -function Poll({ poll, lang, readOnly, onUpdate = () => {} }) { +function Poll({ + poll, + lang, + readOnly, + refresh = () => {}, + votePoll = () => {}, +}) { const [uiState, setUIState] = useState('default'); const { @@ -768,12 +823,7 @@ function Poll({ poll, lang, readOnly, onUpdate = () => {} }) { timeout = setTimeout(() => { setUIState('loading'); (async () => { - try { - const pollResponse = await masto.v1.polls.fetch(id); - onUpdate(pollResponse); - } catch (e) { - // Silent fail - } + await refresh(); setUIState('default'); })(); }, ms); @@ -847,19 +897,14 @@ function Poll({ poll, lang, readOnly, onUpdate = () => {} }) { e.preventDefault(); const form = e.target; const formData = new FormData(form); - const votes = []; + const choices = []; formData.forEach((value, key) => { if (key === 'poll') { - votes.push(value); + choices.push(value); } }); - console.log(votes); setUIState('loading'); - const pollResponse = await masto.v1.polls.vote(id, { - choices: votes, - }); - console.log(pollResponse); - onUpdate(pollResponse); + await votePoll(choices); setUIState('default'); }} > @@ -903,12 +948,7 @@ function Poll({ poll, lang, readOnly, onUpdate = () => {} }) { e.preventDefault(); setUIState('loading'); (async () => { - try { - const pollResponse = await masto.v1.polls.fetch(id); - onUpdate(pollResponse); - } catch (e) { - // Silent fail - } + await refresh(); setUIState('default'); })(); }} @@ -937,7 +977,12 @@ function Poll({ poll, lang, readOnly, onUpdate = () => {} }) { ); } -function EditedAtModal({ statusID, onClose = () => {} }) { +function EditedAtModal({ + statusID, + instance, + fetchStatusHistory = () => {}, + onClose = () => {}, +}) { const [uiState, setUIState] = useState('default'); const [editHistory, setEditHistory] = useState([]); @@ -945,7 +990,7 @@ function EditedAtModal({ statusID, onClose = () => {} }) { setUIState('loading'); (async () => { try { - const editHistory = await masto.v1.statuses.listHistory(statusID); + const editHistory = await fetchStatusHistory(); console.log(editHistory); setEditHistory(editHistory); setUIState('default'); @@ -997,7 +1042,13 @@ function EditedAtModal({ statusID, onClose = () => {} }) { }).format(createdAtDate)} - + ); })} diff --git a/src/components/timeline.jsx b/src/components/timeline.jsx index baaca095..6692368e 100644 --- a/src/components/timeline.jsx +++ b/src/components/timeline.jsx @@ -1,55 +1,174 @@ import { useEffect, useRef, useState } from 'preact/hooks'; +import { useHotkeys } from 'react-hotkeys-hook'; +import { useDebouncedCallback } from 'use-debounce'; +import useInterval from '../utils/useInterval'; +import usePageVisibility from '../utils/usePageVisibility'; import useScroll from '../utils/useScroll'; -import useTitle from '../utils/useTitle'; import Icon from './icon'; import Link from './link'; import Loader from './loader'; +import Menu from './menu'; import Status from './status'; function Timeline({ title, titleComponent, - path, id, + instance, emptyText, errorText, + useItemID, // use statusID instead of status object, assuming it's already in states + boostsCarousel, fetchItems = () => {}, + checkForUpdates = () => {}, + checkForUpdatesInterval = 60_000, // 1 minute + headerStart, + headerEnd, }) { - if (title) { - useTitle(title, path); - } const [items, setItems] = useState([]); const [uiState, setUIState] = useState('default'); const [showMore, setShowMore] = useState(false); - const scrollableRef = useRef(null); - const { nearReachEnd, reachStart } = useScroll({ - scrollableElement: scrollableRef.current, + const [showNew, setShowNew] = useState(false); + const [visible, setVisible] = useState(true); + const scrollableRef = useRef(); + + const loadItems = useDebouncedCallback( + (firstLoad) => { + setShowNew(false); + if (uiState === 'loading') return; + setUIState('loading'); + (async () => { + try { + let { done, value } = await fetchItems(firstLoad); + if (value?.length) { + if (boostsCarousel) { + value = groupBoosts(value); + } + console.log(value); + if (firstLoad) { + setItems(value); + } else { + setItems([...items, ...value]); + } + setShowMore(!done); + } else { + setShowMore(false); + } + setUIState('default'); + } catch (e) { + console.error(e); + setUIState('error'); + } + })(); + }, + 1500, + { + leading: true, + trailing: false, + }, + ); + + const itemsSelector = '.timeline-item, .timeline-item-alt'; + + const jRef = useHotkeys('j, shift+j', (_, handler) => { + // focus on next status after active item + const activeItem = document.activeElement.closest(itemsSelector); + const activeItemRect = activeItem?.getBoundingClientRect(); + const allItems = Array.from( + scrollableRef.current.querySelectorAll(itemsSelector), + ); + if ( + activeItem && + activeItemRect.top < scrollableRef.current.clientHeight && + activeItemRect.bottom > 0 + ) { + const activeItemIndex = allItems.indexOf(activeItem); + let nextItem = allItems[activeItemIndex + 1]; + if (handler.shift) { + // get next status that's not .timeline-item-alt + nextItem = allItems.find( + (item, index) => + index > activeItemIndex && + !item.classList.contains('timeline-item-alt'), + ); + } + if (nextItem) { + nextItem.focus(); + nextItem.scrollIntoViewIfNeeded?.(); + } + } else { + // If active status is not in viewport, get the topmost status-link in viewport + const topmostItem = allItems.find((item) => { + const itemRect = item.getBoundingClientRect(); + return itemRect.top >= 44 && itemRect.left >= 0; // 44 is the magic number for header height, not real + }); + if (topmostItem) { + topmostItem.focus(); + topmostItem.scrollIntoViewIfNeeded?.(); + } + } }); - const loadItems = (firstLoad) => { - setUIState('loading'); - (async () => { - try { - const { done, value } = await fetchItems(firstLoad); - if (value?.length) { - if (firstLoad) { - setItems(value); - } else { - setItems([...items, ...value]); - } - setShowMore(!done); - } else { - setShowMore(false); - } - setUIState('default'); - } catch (e) { - console.error(e); - setUIState('error'); + const kRef = useHotkeys('k, shift+k', (_, handler) => { + // focus on previous status after active item + const activeItem = document.activeElement.closest(itemsSelector); + const activeItemRect = activeItem?.getBoundingClientRect(); + const allItems = Array.from( + scrollableRef.current.querySelectorAll(itemsSelector), + ); + if ( + activeItem && + activeItemRect.top < scrollableRef.current.clientHeight && + activeItemRect.bottom > 0 + ) { + const activeItemIndex = allItems.indexOf(activeItem); + let prevItem = allItems[activeItemIndex - 1]; + if (handler.shift) { + // get prev status that's not .timeline-item-alt + prevItem = allItems.findLast( + (item, index) => + index < activeItemIndex && + !item.classList.contains('timeline-item-alt'), + ); } - })(); - }; + if (prevItem) { + prevItem.focus(); + prevItem.scrollIntoViewIfNeeded?.(); + } + } else { + // If active status is not in viewport, get the topmost status-link in viewport + const topmostItem = allItems.find((item) => { + const itemRect = item.getBoundingClientRect(); + return itemRect.top >= 44 && itemRect.left >= 0; // 44 is the magic number for header height, not real + }); + if (topmostItem) { + topmostItem.focus(); + topmostItem.scrollIntoViewIfNeeded?.(); + } + } + }); + + const oRef = useHotkeys(['enter', 'o'], () => { + // open active status + const activeItem = document.activeElement.closest(itemsSelector); + if (activeItem) { + activeItem.click(); + } + }); + + const { + scrollDirection, + nearReachStart, + nearReachEnd, + reachStart, + reachEnd, + } = useScroll({ + scrollableElement: scrollableRef.current, + distanceFromEnd: 2, + scrollThresholdStart: 44, + }); useEffect(() => { scrollableRef.current?.scrollTo({ top: 0 }); @@ -63,69 +182,211 @@ function Timeline({ }, [reachStart]); useEffect(() => { - if (nearReachEnd && showMore) { + if (nearReachEnd || (reachEnd && showMore)) { loadItems(); } }, [nearReachEnd, showMore]); + const lastHiddenTime = useRef(); + usePageVisibility((visible) => { + if (visible) { + const timeDiff = Date.now() - lastHiddenTime.current; + if (!lastHiddenTime.current || timeDiff > 1000 * 60) { + (async () => { + console.log('✨ Check updates'); + const hasUpdate = await checkForUpdates(); + if (hasUpdate) { + console.log('✨ Has new updates'); + setShowNew(true); + } + })(); + } + } else { + lastHiddenTime.current = Date.now(); + } + setVisible(visible); + }, []); + + // checkForUpdates interval + useInterval( + () => { + (async () => { + console.log('✨ Check updates'); + const hasUpdate = await checkForUpdates(); + if (hasUpdate) { + console.log('✨ Has new updates'); + setShowNew(true); + } + })(); + }, + visible && !showNew ? checkForUpdatesInterval : null, + ); + + const hiddenUI = scrollDirection === 'end' && !nearReachStart; + return (
{ + scrollableRef.current = node; + jRef.current = node; + kRef.current = node; + oRef.current = node; + }} tabIndex="-1" >
{!!items.length ? ( <>
    {items.map((status) => { - const { id: statusID, reblog } = status; + const { id: statusID, reblog, items, type } = status; const actualStatusID = reblog?.id || statusID; + const url = instance + ? `/${instance}/s/${actualStatusID}` + : `/s/${actualStatusID}`; + let title = ''; + if (type === 'boosts') { + title = `${items.length} Boosts`; + } else if (type === 'pinned') { + title = 'Pinned posts'; + } + if (items) { + return ( +
  • + + {items.map((item) => { + const { id: statusID, reblog } = item; + const actualStatusID = reblog?.id || statusID; + const url = instance + ? `/${instance}/s/${actualStatusID}` + : `/s/${actualStatusID}`; + return ( +
  • + + {useItemID ? ( + + ) : ( + + )} + +
  • + ); + })} + + + ); + } return (
  • - - + + {useItemID ? ( + + ) : ( + + )}
  • ); })} + {showMore && uiState === 'loading' && ( + <> +
  • + +
  • +
  • + +
  • + + )}
- {showMore && ( - - )} + {uiState === 'default' && + (showMore ? ( + + ) : ( +

The end.

+ ))} ) : uiState === 'loading' ? (
    @@ -136,9 +397,9 @@ function Timeline({ ))}
) : ( - uiState !== 'loading' &&

{emptyText}

+ uiState !== 'error' &&

{emptyText}

)} - {uiState === 'error' ? ( + {uiState === 'error' && (

{errorText}
@@ -150,14 +411,104 @@ function Timeline({ Try again

- ) : ( - uiState !== 'loading' && - !!items.length && - !showMore &&

The end.

)}
); } +function groupBoosts(values) { + let newValues = []; + let boostStash = []; + let serialBoosts = 0; + for (let i = 0; i < values.length; i++) { + const item = values[i]; + if (item.reblog) { + boostStash.push(item); + serialBoosts++; + } else { + newValues.push(item); + if (serialBoosts < 3) { + serialBoosts = 0; + } + } + } + // if boostStash is more than quarter of values + // or if there are 3 or more boosts in a row + if (boostStash.length > values.length / 4 || serialBoosts >= 3) { + // if boostStash is more than 3 quarter of values + const boostStashID = boostStash.map((status) => status.id); + if (boostStash.length > (values.length * 3) / 4) { + // insert boost array at the end of specialHome list + newValues = [ + ...newValues, + { id: boostStashID, items: boostStash, type: 'boosts' }, + ]; + } else { + // insert boosts array in the middle of specialHome list + const half = Math.floor(newValues.length / 2); + newValues = [ + ...newValues.slice(0, half), + { + id: boostStashID, + items: boostStash, + type: 'boosts', + }, + ...newValues.slice(half), + ]; + } + return newValues; + } else { + return values; + } +} + +function StatusCarousel({ title, class: className, children }) { + const carouselRef = useRef(); + const { reachStart, reachEnd, init } = useScroll({ + scrollableElement: carouselRef.current, + direction: 'horizontal', + }); + useEffect(() => { + init?.(); + }, []); + + return ( + + ); +} + export default Timeline; diff --git a/src/compose.jsx b/src/compose.jsx index 8a339378..30856359 100644 --- a/src/compose.jsx +++ b/src/compose.jsx @@ -2,36 +2,16 @@ import './index.css'; import './app.css'; -import { createClient } from 'masto'; import { render } from 'preact'; import { useEffect, useState } from 'preact/hooks'; import Compose from './components/compose'; -import { getCurrentAccount } from './utils/store-utils'; import useTitle from './utils/useTitle'; if (window.opener) { console = window.opener.console; } -(() => { - if (window.masto) return; - console.warn('window.masto not found. Trying to log in...'); - try { - const { instanceURL, accessToken } = getCurrentAccount(); - window.masto = createClient({ - url: `https://${instanceURL}`, - accessToken, - disableVersionCheck: true, - timeout: 30_000, - }); - console.info('Logged in successfully.'); - } catch (e) { - console.error(e); - alert('Failed to log in. Please try again.'); - } -})(); - function App() { const [uiState, setUIState] = useState('default'); diff --git a/src/data/features.json b/src/data/features.json new file mode 100644 index 00000000..16340ca3 --- /dev/null +++ b/src/data/features.json @@ -0,0 +1,3 @@ +{ + "@mastodon/edit-media-attributes": ">=4.1" +} diff --git a/src/data/instances.json b/src/data/instances.json index a936f9ad..7d0425dd 100644 --- a/src/data/instances.json +++ b/src/data/instances.json @@ -1,386 +1,415 @@ [ "mastodon.social", + "mstdn.jp", "mstdn.social", "mastodon.world", "mas.to", - "pawoo.net", "mastodon.online", "infosec.exchange", - "mstdn.jp", - "mastodonapp.uk", "hachyderm.io", - "techhub.social", "fosstodon.org", - "universeodon.com", "mastodon.lol", - "mastodon.sdf.org", + "techhub.social", + "mastodonapp.uk", "troet.cafe", - "mastodon.uno", - "mastodon.nl", - "mstdn.party", - "masto.ai", - "mstdn.ca", - "home.social", - "c.im", - "kolektiva.social", - "m.cmx.im", - "sfba.social", + "pawoo.net", "fedibird.com", - "piaille.fr", + "universeodon.com", + "m.cmx.im", + "mastodon.uno", + "mastodon.sdf.org", + "mastodon.nl", + "planet.moe", + "kolektiva.social", + "masto.ai", "mastodon.gamedev.place", + "piaille.fr", + "mstdn.ca", + "c.im", + "mstdn.party", + "sfba.social", + "mastodon.cloud", + "chaos.social", + "home.social", + "mastodon.art", + "twingyeo.kr", "mastodon.scot", + "social.vivaldi.net", + "aus.social", + "det.social", + "norden.social", + "nrw.social", + "toot.community", "mindly.social", "ohai.social", - "mastodon.cloud", - "toot.community", - "det.social", - "aus.social", - "nrw.social", - "mastodon.art", - "chaos.social", - "social.vivaldi.net", - "mastodon.ie", - "norden.social", - "sueden.social", - "mastodon.top", - "mastodon.au", - "mastodontech.de", - "mas.todon.de", - "ioc.exchange", "alive.bar", + "mastodon.ie", "social.tchncs.de", - "mastodon.nu", - "social.cologne", - "mastouille.fr", - "o3o.ca", - "mathstodon.xyz", "noagendasocial.com", - "newsie.social", - "sigmoid.social", - "mastodon.com.tr", - "hessen.social", - "muenchen.social", - "meow.social", - "masto.es", - "masto.nu", - "tech.lgbt", - "ruhr.social", - "mastodon.green", - "mstdn.plus", + "o3o.ca", + "mastodon.top", + "sueden.social", + "mastodon.au", "wxw.moe", - "qoto.org", - "mamot.fr", - "tkz.one", - "dice.camp", - "social.anoxinon.de", - "mastodon.nz", + "newsie.social", + "mastodontech.de", + "mathstodon.xyz", + "loforo.com", + "ioc.exchange", "twit.social", - "ravenation.club", - "planet.moe", - "mstdn.science", - "med-mastodon.com", - "econtwitter.net", - "fediscience.org", - "toot.io", - "masthead.social", - "social.dev-wiki.de", - "mastodont.cat", - "toot.wales", - "ieji.de", - "ecoevo.social", - "ro-mastodon.puyo.jp", - "zirk.us", - "noc.social", - "social.linux.pizza", - "cyberplace.social", - "indieweb.social", - "mastodonners.nl", - "convo.casa", - "twingyeo.kr", - "sself.co", - "urbanists.social", - "glasgow.social", - "botsin.space", - "eldritch.cafe", - "climatejustice.social", - "theblower.au", - "framapiaf.org", - "artsio.com", - "mastodon.iriseden.eu", - "socel.net", - "g0v.social", - "mastodonczech.cz", - "mastodontti.fi", - "wandering.shop", - "thu.closed.social", - "mastodon.bida.im", - "geekdom.social", - "stranger.social", - "cupoftea.social", - "bildung.social", - "awscommunity.social", - "mas.town", - "ruby.social", - "sciences.social", - "wien.rocks", - "respublicae.eu", - "metalhead.club", - "pouet.chapril.org", - "genomic.social", - "dju.social", - "101010.pl", - "graphics.social", - "defcon.social", - "mastodon.xyz", - "bark.lgbt", - "witches.live", - "climatejustice.rocks", - "rollenspiel.social", - "berlin.social", - "masto.pt", - "litmind.club", - "livellosegreto.it", - "mstdn.guru", + "mamot.fr", + "meow.social", + "dice.camp", "nerdculture.de", - "journa.host", + "mastodon.nu", + "masto.es", + "hessen.social", + "mastouille.fr", + "ruhr.social", + "social.cologne", + "mastodon.green", + "muenchen.social", + "mastodon.nz", + "mastodon.com.tr", + "qoto.org", + "tkz.one", + "botsin.space", + "sigmoid.social", + "social.anoxinon.de", + "ro-mastodon.puyo.jp", + "fediscience.org", + "mstdn.science", + "masthead.social", + "ravenation.club", + "defcon.social", + "indieweb.social", + "ecoevo.social", + "med-mastodon.com", + "zirk.us", + "econtwitter.net", + "mastodont.cat", + "social.linux.pizza", + "noc.social", + "toot.wales", + "wandering.shop", + "framapiaf.org", + "mastodon.xyz", + "eldritch.cafe", + "masto.nu", + "mastodon-japan.net", + "g0v.social", + "ieji.de", + "toot.io", + "social.dev-wiki.de", + "akamdon.com", + "ruby.social", + "respublicae.eu", + "mstdn.plus", + "bildung.social", + "urbanists.social", + "mstdn.guru", + "cyberplace.social", + "sciences.social", + "climatejustice.social", + "glasgow.social", + "livellosegreto.it", + "pouet.chapril.org", + "mastodontti.fi", + "101010.pl", + "mastodonczech.cz", "octodon.social", - "union.place", - "mastodon-belgium.be", - "mastodon.radio", - "pol.social", - "rheinneckar.social", - "hometech.social", - "androiddev.social", "social.librem.one", - "kinky.business", + "metalhead.club", + "socel.net", + "thu.closed.social", + "stranger.social", + "mastodon.iriseden.eu", + "mastodon.radio", + "berlin.social", + "mastodon.bida.im", "phpc.social", - "mast.lat", - "muenster.im", - "mastodon.chasem.dev", - "tooot.im", - "musician.social", - "dresden.network", - "swiss.social", - "h4.io", - "toot.aquilenet.fr", - "digitalcourage.social", - "toad.social", - "poweredbygay.social", - "hostux.social", - "mastodon.se", - "mastodon.me.uk", - "rubber.social", - "pewtix.com", - "mastodon.berlin", - "lor.sh", - "mastodon.fun", - "me.ns.ci", - "snabelen.no", - "freiburg.social", - "disabled.social", - "spore.social", - "qdon.space", - "beta.qdon.space", - "scholar.social", - "vmst.io", - "astrodon.social", - "masto.nobigtech.es", - "hci.social", - "mastodon.eus", - "todon.eu", - "discuss.systems", - "tooting.ch", - "paquita.masto.host", - "fulda.social", - "lile.cl", - "medibubble.org", - "writing.exchange", - "historians.social", + "kinky.business", + "genomic.social", "vocalodon.net", - "vis.social", - "yiff.life", - "fur.lgbt", - "peoplemaking.games", - "hcommons.social", - "mstdn.io", - "libretooth.gr", - "m.sclo.nl", - "pettingzoo.co", - "mastodon.zaclys.com", - "equestria.social", + "qdon.space", + "androiddev.social", + "masto.pt", + "digitalcourage.social", + "theblower.au", + "graphics.social", + "rollenspiel.social", + "mastodonners.nl", + "awscommunity.social", + "witches.live", + "pol.social", + "mstdn.id", + "swiss.social", + "geekdom.social", + "mast.lat", + "mastodon.me.uk", + "mastodon.fun", + "journa.host", + "mastodon-belgium.be", + "tooot.im", + "sself.co", + "dresden.network", + "expressional.social", + "woof.group", + "hostux.social", + "union.place", + "mastodon.eus", + "dju.social", "best-friends.chat", - "ursal.zone", - "bitcoinhackers.org", - "uiuxdev.social", - "queer.party", + "scholar.social", + "social.lol", + "cr8r.gg", + "hci.social", + "kemonodon.club", + "toad.social", + "rubber.social", + "mastodonbooks.net", + "snabelen.no", + "astrodon.social", + "muenster.im", + "paquita.masto.host", "mastodon.ml", + "todon.eu", + "freiburg.social", + "h4.io", + "cupoftea.social", + "toot.aquilenet.fr", + "lor.sh", + "bark.lgbt", + "convo.casa", + "rheinneckar.social", + "yiff.life", + "stoners.social", + "writing.exchange", + "mastodon.se", + "hcommons.social", + "mstdn.games", + "spore.social", + "mastodon.berlin", + "vmst.io", + "vis.social", + "discuss.systems", + "uri.life", "aethy.com", - "abdl.link", - "mastodon.com.py", - "mapstodon.space", + "mstdn.io", + "ursal.zone", + "tooting.ch", + "queer.party", + "pewtix.com", + "mastodon.zaclys.com", + "historians.social", "typo.social", - "cryptodon.lol", + "pettingzoo.co", + "peoplemaking.games", + "abdl.link", + "climatejustice.rocks", "tilde.zone", - "computerfairi.es", - "social.coop", - "mast.dragon-fly.club", - "dragon-fly.club", + "wien.rocks", "floss.social", - "photog.social", - "bonn.social", - "sciencemastodon.com", - "mastodon.coffee", - "mastorol.es", - "federated.press", "toot.funami.tech", - "mastodon.gal", + "social.coop", + "lile.cl", + "openbiblio.social", + "twiukraine.com", "tabletop.social", - "shakedown.social", - "dizl.de", - "romancelandia.club", - "oslo.town", - "graz.social", - "sociale.network", - "todon.nl", + "imastodon.net", + "bitcoinhackers.org", + "medibubble.org", + "disabled.social", + "photog.social", + "macaw.social", + "mustard.blog", + "mstdn.maud.io", "nofan.xyz", - "data-folks.masto.host", + "mapstodon.space", + "bonn.social", + "vkl.world", + "lounge.town", + "fulda.social", + "mast.dragon-fly.club", + "masto.nobigtech.es", + "sciencemastodon.com", + "weirder.earth", + "todon.nl", + "obo.sh", + "shakedown.social", + "musician.social", + "bsd.network", + "mastodon.gal", + "mastodon.coffee", + "toot.cat", + "libretooth.gr", "scicomm.xyz", "layer8.space", - "artisan.chat", - "freeradical.zone", - "toot.cat", - "fandom.ink", - "twiukraine.com", - "eupolicy.social", - "xarxa.cloud", - "bsd.network", - "weirder.earth", - "linuxrocks.online", - "mastodon.cat", - "girlcock.club", - "bolha.us", - "zeroes.ca", - "douchi.space", + "uiuxdev.social", + "veganism.social", + "oslo.town", + "artsio.com", "cybre.space", - "mastodon.la", - "sunny.garden", - "bbq.snoot.com", + "freeradical.zone", + "social.veraciousnetwork.com", + "douchi.space", + "mstdn.dk", + "federated.press", + "jorts.horse", + "girlcock.club", + "artisan.chat", + "bolha.us", "liker.social", "vulpine.club", - "imastodon.net", - "mstdn.maud.io", - "freeatlantis.com", - "is.nota.live", - "mastodon.org.uk", - "mastodon.arch-linux.cz", + "linuxrocks.online", + "eupolicy.social", + "equestria.social", + "graz.social", + "mastodo.fi", + "pnw.zone", + "dizl.de", "mona.do", - "tyrol.social", - "mstdn.id", - "mastodon.uy", - "mastodon.in.th", - "kurry.social", - "toot.cafe", - "shelter.moe", - "social.politicaconciencia.org", - "h-net.social", - "mstdn.mx", - "kopiti.am", - "mastodon.vlaanderen", - "mao.mastodonhub.com", - "cloud-native.social", - "mograph.social", - "oc.todon.fr", - "ura-mstdn.com", - "uri.life", - "liberdon.com", - "kinkyelephant.com", - "nojack.easydns.ca", - "mastodon.be", - "podcastindex.social", - "blacktwitter.io", - "awoo.space", - "woof.group", - "ani.work", - "colorid.es", - "seo.chat", - "mental.social", - "plural.cafe", + "guitar.rodeo", + "sociale.network", + "opalstack.social", + "mas.town", + "mastodon.la", + "arvr.social", + "zeroes.ca", + "mastorol.es", + "ffxiv-mastodon.com", + "data-folks.masto.host", + "witter.cz", + "romancelandia.club", + "freeatlantis.com", + "darmstadt.social", + "mastodon.cat", + "mastodon.energy", + "computerfairi.es", + "mastodon.org.uk", + "xarxa.cloud", + "masto.nyc", + "cryptodon.lol", + "gametoots.de", + "sunny.garden", "ika.queloud.net", - "mastodon.com.br", - "mstdn.tokyocameraclub.com", - "donphan.social", - "gensokyo.town", + "nederland.online", + "hometech.social", + "ura-mstdn.com", + "shelter.moe", + "kurry.social", + "halifaxsocial.ca", + "bbq.snoot.com", + "mastodon.arch-linux.cz", + "toot.cafe", + "mao.mastodonhub.com", + "ani.work", + "mastodon.uy", + "mastodon.com.py", + "mstdn.mx", + "mastodon.chasem.dev", + "toolboxtalk.tech", + "mastodon.in.th", "ichiji.social", - "sunbeam.city", - "mstdn.kemono-friends.info", - "littlefo.rest", - "kirakiratter.com", - "uwu.social", - "elekk.xyz", - "hispagatos.space", - "hello.2heng.xin", - "the.fores.top", - "mstdn.fr", - "mastodon.mnetwork.co.kr", - "mastodon.gougere.fr", - "dobbs.town", - "gameliberty.club", - "gensokyo.social", - "mathtod.online", - "mastodon.cc", + "mental.social", + "nnia.space", "iztasocial.site", - "mastodon.pirateparty.be", - "dingdash.com", - "mastodon.partipirate.org", - "oulipo.social", - "anticapitalist.party", - "kemonodon.club", - "toot.turbo.chat", + "nojack.easydns.ca", + "arsenalfc.social", + "tyrol.social", + "est.social", + "kinkyelephant.com", + "mograph.social", + "mastodon.mnetwork.co.kr", + "kirakiratter.com", + "h-net.social", + "podcastindex.social", + "esperanto.masto.host", + "awoo.space", + "kopiti.am", + "is.nota.live", + "social.politicaconciencia.org", + "nafo.uk", + "liberdon.com", + "oc.todon.fr", + "mstdn.kemono-friends.info", + "me.dm", + "plural.cafe", + "donphan.social", + "cloud-native.social", + "blacktwitter.io", + "mastodon.be", + "mastodon.vlaanderen", + "mastodon.com.br", + "gensokyo.town", + "mstdn.tokyocameraclub.com", + "kfem.cat", + "earthstream.social", + "sunbeam.city", + "colorid.es", "photodn.net", "otogamer.me", - "bear.community", + "hello.2heng.xin", + "blabber.lu-rp.net", "tablegame.mstdn.cloud", - "anarchism.space", - "ffxiv-mastodon.com", - "lgbt.io", + "elekk.xyz", + "uwu.social", + "hispagatos.space", + "mstdn.fr", + "dobbs.town", + "mastodon.gougere.fr", + "gameliberty.club", + "poweredbygay.social", + "dingdash.com", + "seo.chat", + "mastodon.cc", + "oulipo.social", "lou.lt", - "social.chinwag.org", - "chinwag.org", - "aleph.land", - "social.slat.org", - "mastodon.juggler.jp", - "eigadon.net", - "vocalounge.cafe", - "acg.mn", - "acg.debula.ml", + "mastodon.partipirate.org", + "gensokyo.social", + "anticapitalist.party", "eletusk.club", + "mastodon.juggler.jp", + "social.slat.org", + "bear.community", + "mathtod.online", + "mastodon.pirateparty.be", + "toot.turbo.chat", + "lgbt.io", + "anarchism.space", "otoya.space", - "social.coletivos.org", - "mastodon.cipherbliss.com", + "acg.mn", + "social.chinwag.org", + "eigadon.net", + "aleph.land", + "piano.masto.host", + "baraag.net", + "m.rthome.me", "truthsocial.co.in", "mstdn.osaka", + "mastodol.jp", + "mastodon.cipherbliss.com", "social.targaryen.house", + "vocalounge.cafe", "catdon.life", + "social.coletivos.org", + "mastoturk.org", + "mastodon.librelabucm.org", "stereodon.social", "social.opendesktop.org", - "nasface.cz", - "toot.site", - "fetswing.org", - "vipgirlfriend.xxx", "mastodon.elte.hu", + "toot.site", + "vipgirlfriend.xxx", + "nasface.cz", "bgme.me", + "fetswing.org", "kinbaku.club", - "m.rthome.me", "animalliberation.social", - "mastodon.librelabucm.org", - "mastodon.gza.jp", "med-mammoth.com", + "mastodon.gza.jp", "hearthtodon.com", "counter.social", - "kfem.cat", "pet123.club", - "beta.woof.group", - "explosion.party", "id.cc", "freespeechextremist.com", "cawfee.club", @@ -394,6 +423,7 @@ "libranet.de", "tea.codes", "pixelfed.social", + "stop.voring.me", "shitposter.club", "squeet.me", "shared.graphics", @@ -401,13 +431,16 @@ "pxlmo.com", "pixel.tchncs.de", "love.alicecomplex.com", + "mastodon.london", "friendica.eskimo.com", "meatbag.app", "fediverse.bbad.com", "pix.toot.wales", "fgc.network", "bookrastinating.com", + "electricrequiem.com", "pixey.org", + "mk.pupbrained.xyz", "pixelfed.tokyo", "chudbuds.lol", "freeframe.masto.host", @@ -423,13 +456,16 @@ "pixelfed.de", "metapixl.com", "venera.social", + "pixelfed.fi", "blob.cat", "onevery.ignorelist.com", "cliq.buzz", "pxl.roflcopter.fr", "p.1069-3.com", - "www2.patriot.online", "gc2.jp", - "soap.shitposter.club", - "www.mastodon.scot" + "an.eldritch.gift", + "bz.pawdev.me", + "ck.borgar.space", + "fedi.s1i.dev", + "kids.0px.io" ] \ No newline at end of file diff --git a/src/data/status-supported-languages.json b/src/data/status-supported-languages.json index 915bea4c..500629f4 100644 --- a/src/data/status-supported-languages.json +++ b/src/data/status-supported-languages.json @@ -919,6 +919,11 @@ "Sorani (Kurdish)", "سۆرانی" ], + [ + "cnr", + "Montenegrin", + "crnogorski" + ], [ "jbo", "Lojban", @@ -949,6 +954,26 @@ "Scots", "Scots" ], + [ + "sma", + "Southern Sami", + "Åarjelsaemien Gïele" + ], + [ + "smj", + "Lule Sami", + "Julevsámegiella" + ], + [ + "szl", + "Silesian", + "ślůnsko godka" + ], + [ + "tai", + "Tai", + "ภาษาไท or ภาษาไต" + ], [ "tok", "Toki Pona", diff --git a/src/index.css b/src/index.css index 70a017af..15b128ff 100644 --- a/src/index.css +++ b/src/index.css @@ -1,4 +1,8 @@ +/* Can't use this yet because custom-media doesn't propagate to other CSS files yet, in Vite */ +@custom-media --viewport-medium (min-width: 40em); + :root { + --main-width: 40em; text-size-adjust: none; --hairline-width: 1px; @@ -295,6 +299,13 @@ code { color: var(--text-insignificant-color); } +.hide-until-focus-visible { + display: none; +} +:has(:focus-visible) .hide-until-focus-visible { + display: initial; +} + /* KEYFRAMES */ @keyframes appear { @@ -306,6 +317,17 @@ code { } } +@keyframes appear-smooth { + from { + opacity: 0; + transform: scale(0.98); + } + to { + opacity: 1; + transform: scale(1); + } +} + @keyframes fade-in { from { opacity: 0; diff --git a/src/main.jsx b/src/main.jsx index bee7d956..e2006dbf 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -1,7 +1,5 @@ import './index.css'; -import '@szhsin/react-menu/dist/core.css'; - import { render } from 'preact'; import { HashRouter } from 'react-router-dom'; @@ -18,10 +16,10 @@ render( document.getElementById('app'), ); -// Clean up iconify localStorage -// TODO: Remove this after few weeks? +// Storage cleanup setTimeout(() => { try { + // Clean up iconify localStorage Object.keys(localStorage).forEach((key) => { if (key.startsWith('iconify')) { localStorage.removeItem(key); @@ -32,5 +30,8 @@ setTimeout(() => { sessionStorage.removeItem(key); } }); + + // Clean up old settings key + localStorage.removeItem('settings:boostsCarousel'); } catch (e) {} }, 5000); diff --git a/src/pages/account-statuses.jsx b/src/pages/account-statuses.jsx index 69a8aca6..346d4fe2 100644 --- a/src/pages/account-statuses.jsx +++ b/src/pages/account-statuses.jsx @@ -1,24 +1,64 @@ import { useEffect, useRef, useState } from 'preact/hooks'; import { useParams } from 'react-router-dom'; +import { useSnapshot } from 'valtio'; import Timeline from '../components/timeline'; +import { api } from '../utils/api'; +import emojifyText from '../utils/emojify-text'; import states from '../utils/states'; +import useTitle from '../utils/useTitle'; const LIMIT = 20; function AccountStatuses() { - const { id } = useParams(); + const snapStates = useSnapshot(states); + const { id, ...params } = useParams(); + const { masto, instance } = api({ instance: params.instance }); const accountStatusesIterator = useRef(); async function fetchAccountStatuses(firstLoad) { + const results = []; + if (firstLoad) { + const { value: pinnedStatuses } = await masto.v1.accounts + .listStatuses(id, { + pinned: true, + }) + .next(); + if (pinnedStatuses?.length) { + pinnedStatuses.forEach((status) => { + status._pinned = true; + }); + if (pinnedStatuses.length > 1) { + const pinnedStatusesIds = pinnedStatuses.map((status) => status.id); + results.push({ + id: pinnedStatusesIds, + items: pinnedStatuses, + type: 'pinned', + }); + } else { + results.push(...pinnedStatuses); + } + } + } if (firstLoad || !accountStatusesIterator.current) { accountStatusesIterator.current = masto.v1.accounts.listStatuses(id, { limit: LIMIT, }); } - return await accountStatusesIterator.current.next(); + const { value, done } = await accountStatusesIterator.current.next(); + if (value?.length) { + results.push(...value); + } + return { + value: results, + done, + }; } const [account, setAccount] = useState({}); + useTitle( + `${account?.acct ? '@' + account.acct : 'Posts'}`, + '/:instance?/a/:id', + ); useEffect(() => { (async () => { try { @@ -31,6 +71,8 @@ function AccountStatuses() { })(); }, [id]); + const { displayName, acct, emojis } = account; + return (
{!!statuses.length && heroStatus ? ( @@ -577,24 +608,85 @@ function StatusPage() { } ${thread ? 'thread' : ''} ${isHero ? 'hero' : ''}`} > {isHero ? ( - - - + <> + + + + {!sameInstance && uiState !== 'loading' && ( +
+

+ This post is from another instance ( + {instance}). Interactions (reply, boost, etc) + are not possible. +

+ +
+ )} + {sameInstance && + !authenticated && + uiState !== 'loading' && ( +
+

+ You're not logged in. Interactions (reply, boost, + etc) are not possible. +

+ + Log in + +
+ )} + ) : ( { resetScrollPosition(statusID); }} > @@ -610,8 +702,10 @@ function StatusPage() { )} {descendant && replies?.length > 0 && ( )} {uiState === 'loading' && @@ -691,7 +785,7 @@ function StatusPage() { ); } -function SubComments({ hasManyStatuses, replies }) { +function SubComments({ hasManyStatuses, replies, instance, hasParentThread }) { // Set isBrief = true: // - if less than or 2 replies // - if replies have no sub-replies @@ -728,7 +822,8 @@ function SubComments({ hasManyStatuses, replies }) { .filter((a, i, arr) => arr.findIndex((b) => b.id === a.id) === i) .slice(0, 3); - const open = isBrief || !hasManyStatuses; + const open = + (!hasParentThread || replies.length === 1) && (isBrief || !hasManyStatuses); return (
@@ -764,12 +859,17 @@ function SubComments({ hasManyStatuses, replies }) {
  • { resetScrollPosition(r.id); }} > - + {!r.replies?.length && r.repliesCount > 0 && (