commit
29896dfe0e
1
.github/ISSUE_TEMPLATE/bug_report.md
vendored
1
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
@ -9,6 +9,7 @@ assignees: ''
|
||||||
|
|
||||||
**Describe the bug**
|
**Describe the bug**
|
||||||
A clear and concise description of what the bug is.
|
A clear and concise description of what the bug is.
|
||||||
|
- Which site: [e.g. dev.phanpy.social OR phanpy.social]
|
||||||
|
|
||||||
**To Reproduce**
|
**To Reproduce**
|
||||||
Steps to reproduce the behavior:
|
Steps to reproduce the behavior:
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
"trailingComma": "all",
|
"trailingComma": "all",
|
||||||
"importOrder": [
|
"importOrder": [
|
||||||
|
"^[^.].*.css$",
|
||||||
"index.css$",
|
"index.css$",
|
||||||
".css$",
|
".css$",
|
||||||
"<THIRD_PARTY_MODULES>",
|
"<THIRD_PARTY_MODULES>",
|
||||||
|
|
516
package-lock.json
generated
516
package-lock.json
generated
|
@ -9,20 +9,20 @@
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@github/text-expander-element": "~2.3.0",
|
"@github/text-expander-element": "~2.3.0",
|
||||||
"@iconify-icons/mingcute": "~1.2.3",
|
"@iconify-icons/mingcute": "~1.2.4",
|
||||||
"@szhsin/react-menu": "~3.4.0",
|
"@szhsin/react-menu": "~3.4.1",
|
||||||
"dayjs": "~1.11.7",
|
"dayjs": "~1.11.7",
|
||||||
"dayjs-twitter": "~0.5.0",
|
"dayjs-twitter": "~0.5.0",
|
||||||
"fast-blurhash": "~1.1.2",
|
"fast-blurhash": "~1.1.2",
|
||||||
"fast-deep-equal": "~3.1.3",
|
"fast-deep-equal": "~3.1.3",
|
||||||
"idb-keyval": "~6.2.0",
|
"idb-keyval": "~6.2.0",
|
||||||
"just-debounce-it": "~3.2.0",
|
"just-debounce-it": "~3.2.0",
|
||||||
"masto": "~5.7.0",
|
"masto": "~5.10.0",
|
||||||
"mem": "~9.0.2",
|
"mem": "~9.0.2",
|
||||||
"p-retry": "~5.1.2",
|
"p-retry": "~5.1.2",
|
||||||
"preact": "~10.11.3",
|
"preact": "~10.12.1",
|
||||||
"react-hotkeys-hook": "~4.3.3",
|
"react-hotkeys-hook": "~4.3.5",
|
||||||
"react-intersection-observer": "~9.4.1",
|
"react-intersection-observer": "~9.4.2",
|
||||||
"react-router-dom": "6.6.2",
|
"react-router-dom": "6.6.2",
|
||||||
"string-length": "~5.0.1",
|
"string-length": "~5.0.1",
|
||||||
"swiped-events": "~1.1.7",
|
"swiped-events": "~1.1.7",
|
||||||
|
@ -30,7 +30,7 @@
|
||||||
"uid": "~2.0.1",
|
"uid": "~2.0.1",
|
||||||
"use-debounce": "~9.0.3",
|
"use-debounce": "~9.0.3",
|
||||||
"use-resize-observer": "~9.1.0",
|
"use-resize-observer": "~9.1.0",
|
||||||
"valtio": "~1.9.0"
|
"valtio": "1.9.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@preact/preset-vite": "~2.5.0",
|
"@preact/preset-vite": "~2.5.0",
|
||||||
|
@ -39,11 +39,11 @@
|
||||||
"postcss-dark-theme-class": "~0.7.3",
|
"postcss-dark-theme-class": "~0.7.3",
|
||||||
"postcss-preset-env": "~8.0.1",
|
"postcss-preset-env": "~8.0.1",
|
||||||
"twitter-text": "~3.1.0",
|
"twitter-text": "~3.1.0",
|
||||||
"vite": "~4.0.4",
|
"vite": "~4.1.2",
|
||||||
"vite-plugin-html-config": "~1.0.11",
|
"vite-plugin-html-config": "~1.0.11",
|
||||||
"vite-plugin-html-env": "~1.2.7",
|
"vite-plugin-html-env": "~1.2.7",
|
||||||
"vite-plugin-pwa": "~0.14.1",
|
"vite-plugin-pwa": "~0.14.4",
|
||||||
"vite-plugin-remove-console": "~1.3.0",
|
"vite-plugin-remove-console": "~2.0.0",
|
||||||
"workbox-cacheable-response": "~6.5.4",
|
"workbox-cacheable-response": "~6.5.4",
|
||||||
"workbox-expiration": "~6.5.4",
|
"workbox-expiration": "~6.5.4",
|
||||||
"workbox-routing": "~6.5.4",
|
"workbox-routing": "~6.5.4",
|
||||||
|
@ -2155,9 +2155,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/android-arm": {
|
"node_modules/@esbuild/android-arm": {
|
||||||
"version": "0.16.7",
|
"version": "0.16.17",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.16.7.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.16.17.tgz",
|
||||||
"integrity": "sha512-yhzDbiVcmq6T1/XEvdcJIVcXHdLjDJ5cQ0Dp9R9p9ERMBTeO1dR5tc8YYv8zwDeBw1xZm+Eo3MRo8cwclhBS0g==",
|
"integrity": "sha512-N9x1CMXVhtWEAMS7pNNONyA14f71VPQN9Cnavj1XQh6T7bskqiLLrSca4O0Vr8Wdcga943eThxnVp3JLnBMYtw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
|
@ -2171,9 +2171,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/android-arm64": {
|
"node_modules/@esbuild/android-arm64": {
|
||||||
"version": "0.16.7",
|
"version": "0.16.17",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.16.7.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.16.17.tgz",
|
||||||
"integrity": "sha512-tYFw0lBJSEvLoGzzYh1kXuzoX1iPkbOk3O29VqzQb0HbOy7t/yw1hGkvwoJhXHwzQUPsShyYcTgRf6bDBcfnTw==",
|
"integrity": "sha512-MIGl6p5sc3RDTLLkYL1MyL8BMRN4tLMRCn+yRJJmEDvYZ2M7tmAf80hx1kbNEUX2KJ50RRtxZ4JHLvCfuB6kBg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
@ -2187,9 +2187,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/android-x64": {
|
"node_modules/@esbuild/android-x64": {
|
||||||
"version": "0.16.7",
|
"version": "0.16.17",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.16.7.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.16.17.tgz",
|
||||||
"integrity": "sha512-3P2OuTxwAtM3k/yEWTNUJRjMPG1ce8rXs51GTtvEC5z1j8fC1plHeVVczdeHECU7aM2/Buc0MwZ6ciM/zysnWg==",
|
"integrity": "sha512-a3kTv3m0Ghh4z1DaFEuEDfz3OLONKuFvI4Xqczqx4BqLyuFaFkuaG4j2MtA6fuWEFeC5x9IvqnX7drmRq/fyAQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
@ -2203,9 +2203,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/darwin-arm64": {
|
"node_modules/@esbuild/darwin-arm64": {
|
||||||
"version": "0.16.7",
|
"version": "0.16.17",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.16.7.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.16.17.tgz",
|
||||||
"integrity": "sha512-VUb9GK23z8jkosHU9yJNUgQpsfJn+7ZyBm6adi2Ec5/U241eR1tAn82QicnUzaFDaffeixiHwikjmnec/YXEZg==",
|
"integrity": "sha512-/2agbUEfmxWHi9ARTX6OQ/KgXnOWfsNlTeLcoV7HSuSTv63E4DqtAc+2XqGw1KHxKMHGZgbVCZge7HXWX9Vn+w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
@ -2219,9 +2219,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/darwin-x64": {
|
"node_modules/@esbuild/darwin-x64": {
|
||||||
"version": "0.16.7",
|
"version": "0.16.17",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.16.7.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.16.17.tgz",
|
||||||
"integrity": "sha512-duterlv3tit3HI9vhzMWnSVaB1B6YsXpFq1Ntd6Fou82BB1l4tucYy3FI9dHv3tvtDuS0NiGf/k6XsdBqPZ01w==",
|
"integrity": "sha512-2By45OBHulkd9Svy5IOCZt376Aa2oOkiE9QWUK9fe6Tb+WDr8hXL3dpqi+DeLiMed8tVXspzsTAvd0jUl96wmg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
@ -2235,9 +2235,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/freebsd-arm64": {
|
"node_modules/@esbuild/freebsd-arm64": {
|
||||||
"version": "0.16.7",
|
"version": "0.16.17",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.16.7.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.16.17.tgz",
|
||||||
"integrity": "sha512-9kkycpBFes/vhi7B7o0cf+q2WdJi+EpVzpVTqtWFNiutARWDFFLcB93J8PR1cG228sucsl3B+7Ts27izE6qiaQ==",
|
"integrity": "sha512-mt+cxZe1tVx489VTb4mBAOo2aKSnJ33L9fr25JXpqQqzbUIw/yzIzi+NHwAXK2qYV1lEFp4OoVeThGjUbmWmdw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
@ -2251,9 +2251,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/freebsd-x64": {
|
"node_modules/@esbuild/freebsd-x64": {
|
||||||
"version": "0.16.7",
|
"version": "0.16.17",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.16.7.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.16.17.tgz",
|
||||||
"integrity": "sha512-5Ahf6jzWXJ4J2uh9dpy5DKOO+PeRUE/9DMys6VuYfwgQzd6n5+pVFm58L2Z2gRe611RX6SdydnNaiIKM3svY7g==",
|
"integrity": "sha512-8ScTdNJl5idAKjH8zGAsN7RuWcyHG3BAvMNpKOBaqqR7EbUhhVHOqXRdL7oZvz8WNHL2pr5+eIT5c65kA6NHug==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
@ -2267,9 +2267,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-arm": {
|
"node_modules/@esbuild/linux-arm": {
|
||||||
"version": "0.16.7",
|
"version": "0.16.17",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.16.7.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.16.17.tgz",
|
||||||
"integrity": "sha512-QqJnyCfu5OF78Olt7JJSZ7OSv/B4Hf+ZJWp4kkq9xwMsgu7yWq3crIic8gGOpDYTqVKKMDAVDgRXy5Wd/nWZyQ==",
|
"integrity": "sha512-iihzrWbD4gIT7j3caMzKb/RsFFHCwqqbrbH9SqUSRrdXkXaygSZCZg1FybsZz57Ju7N/SHEgPyaR0LZ8Zbe9gQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
|
@ -2283,9 +2283,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-arm64": {
|
"node_modules/@esbuild/linux-arm64": {
|
||||||
"version": "0.16.7",
|
"version": "0.16.17",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.16.7.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.16.17.tgz",
|
||||||
"integrity": "sha512-2wv0xYDskk2+MzIm/AEprDip39a23Chptc4mL7hsHg26P0gD8RUhzmDu0KCH2vMThUI1sChXXoK9uH0KYQKaDg==",
|
"integrity": "sha512-7S8gJnSlqKGVJunnMCrXHU9Q8Q/tQIxk/xL8BqAP64wchPCTzuM6W3Ra8cIa1HIflAvDnNOt2jaL17vaW+1V0g==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
@ -2299,9 +2299,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-ia32": {
|
"node_modules/@esbuild/linux-ia32": {
|
||||||
"version": "0.16.7",
|
"version": "0.16.17",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.16.7.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.16.17.tgz",
|
||||||
"integrity": "sha512-APVYbEilKbD5ptmKdnIcXej2/+GdV65TfTjxR2Uk8t1EsOk49t6HapZW6DS/Bwlvh5hDwtLapdSumIVNGxgqLg==",
|
"integrity": "sha512-kiX69+wcPAdgl3Lonh1VI7MBr16nktEvOfViszBSxygRQqSpzv7BffMKRPMFwzeJGPxcio0pdD3kYQGpqQ2SSg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
|
@ -2315,9 +2315,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-loong64": {
|
"node_modules/@esbuild/linux-loong64": {
|
||||||
"version": "0.16.7",
|
"version": "0.16.17",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.16.7.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.16.17.tgz",
|
||||||
"integrity": "sha512-5wPUAGclplQrAW7EFr3F84Y/d++7G0KykohaF4p54+iNWhUnMVU8Bh2sxiEOXUy4zKIdpHByMgJ5/Ko6QhtTUw==",
|
"integrity": "sha512-dTzNnQwembNDhd654cA4QhbS9uDdXC3TKqMJjgOWsC0yNCbpzfWoXdZvp0mY7HU6nzk5E0zpRGGx3qoQg8T2DQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"loong64"
|
"loong64"
|
||||||
],
|
],
|
||||||
|
@ -2331,9 +2331,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-mips64el": {
|
"node_modules/@esbuild/linux-mips64el": {
|
||||||
"version": "0.16.7",
|
"version": "0.16.17",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.16.7.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.16.17.tgz",
|
||||||
"integrity": "sha512-hxzlXtWF6yWfkE/SMTscNiVqLOAn7fOuIF3q/kiZaXxftz1DhZW/HpnTmTTWrzrS7zJWQxHHT4QSxyAj33COmA==",
|
"integrity": "sha512-ezbDkp2nDl0PfIUn0CsQ30kxfcLTlcx4Foz2kYv8qdC6ia2oX5Q3E/8m6lq84Dj/6b0FrkgD582fJMIfHhJfSw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"mips64el"
|
"mips64el"
|
||||||
],
|
],
|
||||||
|
@ -2347,9 +2347,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-ppc64": {
|
"node_modules/@esbuild/linux-ppc64": {
|
||||||
"version": "0.16.7",
|
"version": "0.16.17",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.16.7.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.16.17.tgz",
|
||||||
"integrity": "sha512-WM83Dac0LdXty5xPhlOuCD5Egfk1xLND/oRLYeB7Jb/tY4kzFSDgLlq91wYbHua/s03tQGA9iXvyjgymMw62Vw==",
|
"integrity": "sha512-dzS678gYD1lJsW73zrFhDApLVdM3cUF2MvAa1D8K8KtcSKdLBPP4zZSLy6LFZ0jYqQdQ29bjAHJDgz0rVbLB3g==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
|
@ -2363,9 +2363,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-riscv64": {
|
"node_modules/@esbuild/linux-riscv64": {
|
||||||
"version": "0.16.7",
|
"version": "0.16.17",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.16.7.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.16.17.tgz",
|
||||||
"integrity": "sha512-3nkNnNg4Ax6MS/l8O8Ynq2lGEVJYyJ2EoY3PHjNJ4PuZ80EYLMrFTFZ4L/Hc16AxgtXKwmNP9TM0YKNiBzBiJQ==",
|
"integrity": "sha512-ylNlVsxuFjZK8DQtNUwiMskh6nT0vI7kYl/4fZgV1llP5d6+HIeL/vmmm3jpuoo8+NuXjQVZxmKuhDApK0/cKw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
|
@ -2379,9 +2379,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-s390x": {
|
"node_modules/@esbuild/linux-s390x": {
|
||||||
"version": "0.16.7",
|
"version": "0.16.17",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.16.7.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.16.17.tgz",
|
||||||
"integrity": "sha512-3SA/2VJuv0o1uD7zuqxEP+RrAyRxnkGddq0bwHQ98v1KNlzXD/JvxwTO3T6GM5RH6JUd29RTVQTOJfyzMkkppA==",
|
"integrity": "sha512-gzy7nUTO4UA4oZ2wAMXPNBGTzZFP7mss3aKR2hH+/4UUkCOyqmjXiKpzGrY2TlEUhbbejzXVKKGazYcQTZWA/w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
|
@ -2395,9 +2395,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-x64": {
|
"node_modules/@esbuild/linux-x64": {
|
||||||
"version": "0.16.7",
|
"version": "0.16.17",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.16.7.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.16.17.tgz",
|
||||||
"integrity": "sha512-xi/tbqCqvPIzU+zJVyrpz12xqciTAPMi2fXEWGnapZymoGhuL2GIWIRXg4O2v5BXaYA5TSaiKYE14L0QhUTuQg==",
|
"integrity": "sha512-mdPjPxfnmoqhgpiEArqi4egmBAMYvaObgn4poorpUaqmvzzbvqbowRllQ+ZgzGVMGKaPkqUmPDOOFQRUFDmeUw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
@ -2411,9 +2411,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/netbsd-x64": {
|
"node_modules/@esbuild/netbsd-x64": {
|
||||||
"version": "0.16.7",
|
"version": "0.16.17",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.16.7.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.16.17.tgz",
|
||||||
"integrity": "sha512-NUsYbq3B+JdNKn8SXkItFvdes9qTwEoS3aLALtiWciW/ystiCKM20Fgv9XQBOXfhUHyh5CLEeZDXzLOrwBXuCQ==",
|
"integrity": "sha512-/PzmzD/zyAeTUsduZa32bn0ORug+Jd1EGGAUJvqfeixoEISYpGnAezN6lnJoskauoai0Jrs+XSyvDhppCPoKOA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
@ -2427,9 +2427,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/openbsd-x64": {
|
"node_modules/@esbuild/openbsd-x64": {
|
||||||
"version": "0.16.7",
|
"version": "0.16.17",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.16.7.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.16.17.tgz",
|
||||||
"integrity": "sha512-qjwzsgeve9I8Tbsko2FEkdSk2iiezuNGFgipQxY/736NePXDaDZRodIejYGWOlbYXugdxb0nif5yvypH6lKBmA==",
|
"integrity": "sha512-2yaWJhvxGEz2RiftSk0UObqJa/b+rIAjnODJgv2GbGGpRwAfpgzyrg1WLK8rqA24mfZa9GvpjLcBBg8JHkoodg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
@ -2443,9 +2443,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/sunos-x64": {
|
"node_modules/@esbuild/sunos-x64": {
|
||||||
"version": "0.16.7",
|
"version": "0.16.17",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.16.7.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.16.17.tgz",
|
||||||
"integrity": "sha512-mFWDz4RoBTzPphTCkM7Kc7Qpa0o/Z01acajR+Ai7LdfKgcP/C6jYOaKwv7nKzD0+MjOT20j7You9g4ozYy1dKQ==",
|
"integrity": "sha512-xtVUiev38tN0R3g8VhRfN7Zl42YCJvyBhRKw1RJjwE1d2emWTVToPLNEQj/5Qxc6lVFATDiy6LjVHYhIPrLxzw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
@ -2459,9 +2459,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/win32-arm64": {
|
"node_modules/@esbuild/win32-arm64": {
|
||||||
"version": "0.16.7",
|
"version": "0.16.17",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.16.7.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.16.17.tgz",
|
||||||
"integrity": "sha512-m39UmX19RvEIuC8sYZ0M+eQtdXw4IePDSZ78ZQmYyFaXY9krq4YzQCK2XWIJomNLtg4q+W5aXr8bW3AbqWNoVg==",
|
"integrity": "sha512-ga8+JqBDHY4b6fQAmOgtJJue36scANy4l/rL97W+0wYmijhxKetzZdKOJI7olaBaMhWt8Pac2McJdZLxXWUEQw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
@ -2475,9 +2475,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/win32-ia32": {
|
"node_modules/@esbuild/win32-ia32": {
|
||||||
"version": "0.16.7",
|
"version": "0.16.17",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.16.7.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.16.17.tgz",
|
||||||
"integrity": "sha512-1cbzSEZA1fANwmT6rjJ4G1qQXHxCxGIcNYFYR9ctI82/prT38lnwSRZ0i5p/MVXksw9eMlHlet6pGu2/qkXFCg==",
|
"integrity": "sha512-WnsKaf46uSSF/sZhwnqE4L/F89AYNMiD4YtEcYekBt9Q7nj0DiId2XH2Ng2PHM54qi5oPrQ8luuzGszqi/veig==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
|
@ -2491,9 +2491,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/win32-x64": {
|
"node_modules/@esbuild/win32-x64": {
|
||||||
"version": "0.16.7",
|
"version": "0.16.17",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.16.7.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.16.17.tgz",
|
||||||
"integrity": "sha512-QaQ8IH0JLacfGf5cf0HCCPnQuCTd/dAI257vXBgb/cccKGbH/6pVtI1gwhdAQ0Y48QSpTIFrh9etVyNdZY+zzw==",
|
"integrity": "sha512-y+EHuSchhL7FjHgvQL/0fnnFmO4T1bhvWANX6gcnqTjtnKWbTvUMCpGnv2+t+31d7RzyEAYAd4u2fnIhHL6N/Q==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
@ -2520,9 +2520,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@iconify-icons/mingcute": {
|
"node_modules/@iconify-icons/mingcute": {
|
||||||
"version": "1.2.3",
|
"version": "1.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@iconify-icons/mingcute/-/mingcute-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/@iconify-icons/mingcute/-/mingcute-1.2.4.tgz",
|
||||||
"integrity": "sha512-yZyioZhNy61SkLxQoyHThsfuyaOej9n84PUS+K69qaS1Dyj7/wHwYhWXseFCnzyzicaEHkCpt6H/hYV8fwmMLg==",
|
"integrity": "sha512-4aaWYa6GxSdYmJg8iBVx6VDuKUcTDEbio929+GrswoxfyTsPUkOOgw2wffUDHjE3JDUAnrWj9teQTnBkFm7Gyg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@iconify/types": "*"
|
"@iconify/types": "*"
|
||||||
}
|
}
|
||||||
|
@ -2810,9 +2810,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@szhsin/react-menu": {
|
"node_modules/@szhsin/react-menu": {
|
||||||
"version": "3.4.0",
|
"version": "3.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/@szhsin/react-menu/-/react-menu-3.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/@szhsin/react-menu/-/react-menu-3.4.1.tgz",
|
||||||
"integrity": "sha512-BRxUF3BNmaQzL1z8UkS/eNFj+5ZPjBOpsYWP/ZtrzHNkkP6hUc+tYerBV4dwFiGYmWzkxyOP44ISn+EujwPpUw==",
|
"integrity": "sha512-Pxt7Kyp3yuX7zkT5tjdLRJGNFMa5Tx4BP+01gJ/dnMmHQpI1H2or9gEC0X+t3cLldO3LGmm4ViGypNCmQLv/4A==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"prop-types": "^15.7.2",
|
"prop-types": "^15.7.2",
|
||||||
"react-transition-state": "^1.1.5"
|
"react-transition-state": "^1.1.5"
|
||||||
|
@ -3719,9 +3719,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/esbuild": {
|
"node_modules/esbuild": {
|
||||||
"version": "0.16.7",
|
"version": "0.16.17",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.16.7.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.16.17.tgz",
|
||||||
"integrity": "sha512-P6OBFYFSQOGzfApqCeYKqfKRRbCIRsdppTXFo4aAvtiW3o8TTyiIplBvHJI171saPAiy3WlawJHCveJVIOIx1A==",
|
"integrity": "sha512-G8LEkV0XzDMNwXKgM0Jwu3nY3lSTwSGY6XbxM9cr9+s0T/qSV1q1JVPBGzm3dcjhCic9+emZDmMffkwgPeOeLg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
|
@ -3731,28 +3731,28 @@
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@esbuild/android-arm": "0.16.7",
|
"@esbuild/android-arm": "0.16.17",
|
||||||
"@esbuild/android-arm64": "0.16.7",
|
"@esbuild/android-arm64": "0.16.17",
|
||||||
"@esbuild/android-x64": "0.16.7",
|
"@esbuild/android-x64": "0.16.17",
|
||||||
"@esbuild/darwin-arm64": "0.16.7",
|
"@esbuild/darwin-arm64": "0.16.17",
|
||||||
"@esbuild/darwin-x64": "0.16.7",
|
"@esbuild/darwin-x64": "0.16.17",
|
||||||
"@esbuild/freebsd-arm64": "0.16.7",
|
"@esbuild/freebsd-arm64": "0.16.17",
|
||||||
"@esbuild/freebsd-x64": "0.16.7",
|
"@esbuild/freebsd-x64": "0.16.17",
|
||||||
"@esbuild/linux-arm": "0.16.7",
|
"@esbuild/linux-arm": "0.16.17",
|
||||||
"@esbuild/linux-arm64": "0.16.7",
|
"@esbuild/linux-arm64": "0.16.17",
|
||||||
"@esbuild/linux-ia32": "0.16.7",
|
"@esbuild/linux-ia32": "0.16.17",
|
||||||
"@esbuild/linux-loong64": "0.16.7",
|
"@esbuild/linux-loong64": "0.16.17",
|
||||||
"@esbuild/linux-mips64el": "0.16.7",
|
"@esbuild/linux-mips64el": "0.16.17",
|
||||||
"@esbuild/linux-ppc64": "0.16.7",
|
"@esbuild/linux-ppc64": "0.16.17",
|
||||||
"@esbuild/linux-riscv64": "0.16.7",
|
"@esbuild/linux-riscv64": "0.16.17",
|
||||||
"@esbuild/linux-s390x": "0.16.7",
|
"@esbuild/linux-s390x": "0.16.17",
|
||||||
"@esbuild/linux-x64": "0.16.7",
|
"@esbuild/linux-x64": "0.16.17",
|
||||||
"@esbuild/netbsd-x64": "0.16.7",
|
"@esbuild/netbsd-x64": "0.16.17",
|
||||||
"@esbuild/openbsd-x64": "0.16.7",
|
"@esbuild/openbsd-x64": "0.16.17",
|
||||||
"@esbuild/sunos-x64": "0.16.7",
|
"@esbuild/sunos-x64": "0.16.17",
|
||||||
"@esbuild/win32-arm64": "0.16.7",
|
"@esbuild/win32-arm64": "0.16.17",
|
||||||
"@esbuild/win32-ia32": "0.16.7",
|
"@esbuild/win32-ia32": "0.16.17",
|
||||||
"@esbuild/win32-x64": "0.16.7"
|
"@esbuild/win32-x64": "0.16.17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/escalade": {
|
"node_modules/escalade": {
|
||||||
|
@ -4718,9 +4718,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/masto": {
|
"node_modules/masto": {
|
||||||
"version": "5.7.0",
|
"version": "5.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/masto/-/masto-5.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/masto/-/masto-5.10.0.tgz",
|
||||||
"integrity": "sha512-oCVReGLR9AoBSkShVgWrPb69xzfCpogiPH9wVT9AnPbM+CvjIqJuSaD1xgaEUi6jo66XGbhTC6UerSCVqq9emg==",
|
"integrity": "sha512-RlTw3X2b2ipkcgsgoKEWKKFNYkpAlUtJhNOFKwBKWEBv+we/ZupQbnerGOJssB5rs7ig4HWWsZZHLtNeFdYQTQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mastojs/ponyfills": "^1.0.4",
|
"@mastojs/ponyfills": "^1.0.4",
|
||||||
"change-case": "^4.1.2",
|
"change-case": "^4.1.2",
|
||||||
|
@ -5650,9 +5650,9 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/preact": {
|
"node_modules/preact": {
|
||||||
"version": "10.11.3",
|
"version": "10.12.1",
|
||||||
"resolved": "https://registry.npmjs.org/preact/-/preact-10.11.3.tgz",
|
"resolved": "https://registry.npmjs.org/preact/-/preact-10.12.1.tgz",
|
||||||
"integrity": "sha512-eY93IVpod/zG3uMF22Unl8h9KkrcKIRs2EGar8hwLZZDU1lkjph303V9HZBwufh2s736U6VXuhD109LYqPoffg==",
|
"integrity": "sha512-l8386ixSsBdbreOAkqtrwqHwdvR35ID8c3rKPa8lCWuO86dBi32QWHV4vfsZK1utLLFMvw+Z5Ad4XLkZzchscg==",
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
"url": "https://opencollective.com/preact"
|
"url": "https://opencollective.com/preact"
|
||||||
|
@ -5779,18 +5779,18 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-hotkeys-hook": {
|
"node_modules/react-hotkeys-hook": {
|
||||||
"version": "4.3.3",
|
"version": "4.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/react-hotkeys-hook/-/react-hotkeys-hook-4.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/react-hotkeys-hook/-/react-hotkeys-hook-4.3.5.tgz",
|
||||||
"integrity": "sha512-OYZCG2G+xLeiH0TkrW+v6eKFPvYq9iCA5sh9pwvnbGQaK86Lw/kWJDjjzSksFJoCk4K78OLe3MR1afNZH6+cLg==",
|
"integrity": "sha512-tfwTwKP3ga7n4naNS/JOByaEwEkTCoXYCepDuhXpj8mBx+sFszV5JecRWM2dv+PbOowmmBpHAFtTXTnG/p8UkQ==",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": ">=16.8.1",
|
"react": ">=16.8.1",
|
||||||
"react-dom": ">=16.8.1"
|
"react-dom": ">=16.8.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-intersection-observer": {
|
"node_modules/react-intersection-observer": {
|
||||||
"version": "9.4.1",
|
"version": "9.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-9.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-9.4.2.tgz",
|
||||||
"integrity": "sha512-IXpIsPe6BleFOEHKzKh5UjwRUaz/JYS0lT/HPsupWEQou2hDqjhLMStc5zyE3eQVT4Fk3FufM8Fw33qW1uyeiw==",
|
"integrity": "sha512-AdK+ryzZ7U9ZJYttDUZ8q2Am3nqE0exg5Ryl5Y124KeVsix/1hGZPbdu58EqA98TwnzwDNWHxg/kwNawmIiUig==",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0"
|
"react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0"
|
||||||
}
|
}
|
||||||
|
@ -5978,9 +5978,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/rollup": {
|
"node_modules/rollup": {
|
||||||
"version": "3.7.4",
|
"version": "3.12.1",
|
||||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.7.4.tgz",
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.12.1.tgz",
|
||||||
"integrity": "sha512-jN9rx3k5pfg9H9al0r0y1EYKSeiRANZRYX32SuNXAnKzh6cVyf4LZVto1KAuDnbHT03E1CpsgqDKaqQ8FZtgxw==",
|
"integrity": "sha512-t9elERrz2i4UU9z7AwISj3CQcXP39cWxgRWLdf4Tm6aKm1eYrqHIgjzXBgb67GNY1sZckTFFi0oMozh3/S++Ig==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"rollup": "dist/bin/rollup"
|
"rollup": "dist/bin/rollup"
|
||||||
|
@ -6603,15 +6603,15 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "4.0.4",
|
"version": "4.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-4.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-4.1.2.tgz",
|
||||||
"integrity": "sha512-xevPU7M8FU0i/80DMR+YhgrzR5KS2ORy1B4xcX/cXLsvnUWvfHuqMmVU6N0YiJ4JWGRJJsLCgjEzKjG9/GKoSw==",
|
"integrity": "sha512-MWDb9Rfy3DI8omDQySbMK93nQqStwbsQWejXRY2EBzEWKmLAXWb1mkI9Yw2IJrc+oCvPCI1Os5xSSIBYY6DEAw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.16.3",
|
"esbuild": "^0.16.14",
|
||||||
"postcss": "^8.4.20",
|
"postcss": "^8.4.21",
|
||||||
"resolve": "^1.22.1",
|
"resolve": "^1.22.1",
|
||||||
"rollup": "^3.7.0"
|
"rollup": "^3.10.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"vite": "bin/vite.js"
|
"vite": "bin/vite.js"
|
||||||
|
@ -6676,9 +6676,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite-plugin-pwa": {
|
"node_modules/vite-plugin-pwa": {
|
||||||
"version": "0.14.1",
|
"version": "0.14.4",
|
||||||
"resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.14.1.tgz",
|
"resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.14.4.tgz",
|
||||||
"integrity": "sha512-5zx7yhQ8RTLwV71+GA9YsQQ63ALKG8XXIMqRJDdZkR8ZYftFcRgnzM7wOWmQZ/DATspyhPih5wCdcZnAIsM+mA==",
|
"integrity": "sha512-M7Ct0so8OlouMkTWgXnl8W1xU95glITSKIe7qswZf1tniAstO2idElGCnsrTJ5NPNSx1XqfTCOUj8j94S6FD7Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@rollup/plugin-replace": "^5.0.1",
|
"@rollup/plugin-replace": "^5.0.1",
|
||||||
|
@ -6699,9 +6699,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite-plugin-remove-console": {
|
"node_modules/vite-plugin-remove-console": {
|
||||||
"version": "1.3.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/vite-plugin-remove-console/-/vite-plugin-remove-console-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/vite-plugin-remove-console/-/vite-plugin-remove-console-2.0.0.tgz",
|
||||||
"integrity": "sha512-5a/OLYB6yNRHMuHj9rBQRYMQ1NBKffxA8BaD77urUBLcGOWMHFHALjh6C26wZfZd41KytSwLp6DhvNKU78mNJg==",
|
"integrity": "sha512-bEsyShSacsunbm0X1zaVliwgmWlsaBPLk7FN4wr2xQMs8zSZPSwpRNTT5UZiF0+cfMEkN4VVnofITawmT3pjgQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/webidl-conversions": {
|
"node_modules/webidl-conversions": {
|
||||||
|
@ -8460,156 +8460,156 @@
|
||||||
"requires": {}
|
"requires": {}
|
||||||
},
|
},
|
||||||
"@esbuild/android-arm": {
|
"@esbuild/android-arm": {
|
||||||
"version": "0.16.7",
|
"version": "0.16.17",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.16.7.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.16.17.tgz",
|
||||||
"integrity": "sha512-yhzDbiVcmq6T1/XEvdcJIVcXHdLjDJ5cQ0Dp9R9p9ERMBTeO1dR5tc8YYv8zwDeBw1xZm+Eo3MRo8cwclhBS0g==",
|
"integrity": "sha512-N9x1CMXVhtWEAMS7pNNONyA14f71VPQN9Cnavj1XQh6T7bskqiLLrSca4O0Vr8Wdcga943eThxnVp3JLnBMYtw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/android-arm64": {
|
"@esbuild/android-arm64": {
|
||||||
"version": "0.16.7",
|
"version": "0.16.17",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.16.7.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.16.17.tgz",
|
||||||
"integrity": "sha512-tYFw0lBJSEvLoGzzYh1kXuzoX1iPkbOk3O29VqzQb0HbOy7t/yw1hGkvwoJhXHwzQUPsShyYcTgRf6bDBcfnTw==",
|
"integrity": "sha512-MIGl6p5sc3RDTLLkYL1MyL8BMRN4tLMRCn+yRJJmEDvYZ2M7tmAf80hx1kbNEUX2KJ50RRtxZ4JHLvCfuB6kBg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/android-x64": {
|
"@esbuild/android-x64": {
|
||||||
"version": "0.16.7",
|
"version": "0.16.17",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.16.7.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.16.17.tgz",
|
||||||
"integrity": "sha512-3P2OuTxwAtM3k/yEWTNUJRjMPG1ce8rXs51GTtvEC5z1j8fC1plHeVVczdeHECU7aM2/Buc0MwZ6ciM/zysnWg==",
|
"integrity": "sha512-a3kTv3m0Ghh4z1DaFEuEDfz3OLONKuFvI4Xqczqx4BqLyuFaFkuaG4j2MtA6fuWEFeC5x9IvqnX7drmRq/fyAQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/darwin-arm64": {
|
"@esbuild/darwin-arm64": {
|
||||||
"version": "0.16.7",
|
"version": "0.16.17",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.16.7.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.16.17.tgz",
|
||||||
"integrity": "sha512-VUb9GK23z8jkosHU9yJNUgQpsfJn+7ZyBm6adi2Ec5/U241eR1tAn82QicnUzaFDaffeixiHwikjmnec/YXEZg==",
|
"integrity": "sha512-/2agbUEfmxWHi9ARTX6OQ/KgXnOWfsNlTeLcoV7HSuSTv63E4DqtAc+2XqGw1KHxKMHGZgbVCZge7HXWX9Vn+w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/darwin-x64": {
|
"@esbuild/darwin-x64": {
|
||||||
"version": "0.16.7",
|
"version": "0.16.17",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.16.7.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.16.17.tgz",
|
||||||
"integrity": "sha512-duterlv3tit3HI9vhzMWnSVaB1B6YsXpFq1Ntd6Fou82BB1l4tucYy3FI9dHv3tvtDuS0NiGf/k6XsdBqPZ01w==",
|
"integrity": "sha512-2By45OBHulkd9Svy5IOCZt376Aa2oOkiE9QWUK9fe6Tb+WDr8hXL3dpqi+DeLiMed8tVXspzsTAvd0jUl96wmg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/freebsd-arm64": {
|
"@esbuild/freebsd-arm64": {
|
||||||
"version": "0.16.7",
|
"version": "0.16.17",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.16.7.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.16.17.tgz",
|
||||||
"integrity": "sha512-9kkycpBFes/vhi7B7o0cf+q2WdJi+EpVzpVTqtWFNiutARWDFFLcB93J8PR1cG228sucsl3B+7Ts27izE6qiaQ==",
|
"integrity": "sha512-mt+cxZe1tVx489VTb4mBAOo2aKSnJ33L9fr25JXpqQqzbUIw/yzIzi+NHwAXK2qYV1lEFp4OoVeThGjUbmWmdw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/freebsd-x64": {
|
"@esbuild/freebsd-x64": {
|
||||||
"version": "0.16.7",
|
"version": "0.16.17",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.16.7.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.16.17.tgz",
|
||||||
"integrity": "sha512-5Ahf6jzWXJ4J2uh9dpy5DKOO+PeRUE/9DMys6VuYfwgQzd6n5+pVFm58L2Z2gRe611RX6SdydnNaiIKM3svY7g==",
|
"integrity": "sha512-8ScTdNJl5idAKjH8zGAsN7RuWcyHG3BAvMNpKOBaqqR7EbUhhVHOqXRdL7oZvz8WNHL2pr5+eIT5c65kA6NHug==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/linux-arm": {
|
"@esbuild/linux-arm": {
|
||||||
"version": "0.16.7",
|
"version": "0.16.17",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.16.7.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.16.17.tgz",
|
||||||
"integrity": "sha512-QqJnyCfu5OF78Olt7JJSZ7OSv/B4Hf+ZJWp4kkq9xwMsgu7yWq3crIic8gGOpDYTqVKKMDAVDgRXy5Wd/nWZyQ==",
|
"integrity": "sha512-iihzrWbD4gIT7j3caMzKb/RsFFHCwqqbrbH9SqUSRrdXkXaygSZCZg1FybsZz57Ju7N/SHEgPyaR0LZ8Zbe9gQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/linux-arm64": {
|
"@esbuild/linux-arm64": {
|
||||||
"version": "0.16.7",
|
"version": "0.16.17",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.16.7.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.16.17.tgz",
|
||||||
"integrity": "sha512-2wv0xYDskk2+MzIm/AEprDip39a23Chptc4mL7hsHg26P0gD8RUhzmDu0KCH2vMThUI1sChXXoK9uH0KYQKaDg==",
|
"integrity": "sha512-7S8gJnSlqKGVJunnMCrXHU9Q8Q/tQIxk/xL8BqAP64wchPCTzuM6W3Ra8cIa1HIflAvDnNOt2jaL17vaW+1V0g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/linux-ia32": {
|
"@esbuild/linux-ia32": {
|
||||||
"version": "0.16.7",
|
"version": "0.16.17",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.16.7.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.16.17.tgz",
|
||||||
"integrity": "sha512-APVYbEilKbD5ptmKdnIcXej2/+GdV65TfTjxR2Uk8t1EsOk49t6HapZW6DS/Bwlvh5hDwtLapdSumIVNGxgqLg==",
|
"integrity": "sha512-kiX69+wcPAdgl3Lonh1VI7MBr16nktEvOfViszBSxygRQqSpzv7BffMKRPMFwzeJGPxcio0pdD3kYQGpqQ2SSg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/linux-loong64": {
|
"@esbuild/linux-loong64": {
|
||||||
"version": "0.16.7",
|
"version": "0.16.17",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.16.7.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.16.17.tgz",
|
||||||
"integrity": "sha512-5wPUAGclplQrAW7EFr3F84Y/d++7G0KykohaF4p54+iNWhUnMVU8Bh2sxiEOXUy4zKIdpHByMgJ5/Ko6QhtTUw==",
|
"integrity": "sha512-dTzNnQwembNDhd654cA4QhbS9uDdXC3TKqMJjgOWsC0yNCbpzfWoXdZvp0mY7HU6nzk5E0zpRGGx3qoQg8T2DQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/linux-mips64el": {
|
"@esbuild/linux-mips64el": {
|
||||||
"version": "0.16.7",
|
"version": "0.16.17",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.16.7.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.16.17.tgz",
|
||||||
"integrity": "sha512-hxzlXtWF6yWfkE/SMTscNiVqLOAn7fOuIF3q/kiZaXxftz1DhZW/HpnTmTTWrzrS7zJWQxHHT4QSxyAj33COmA==",
|
"integrity": "sha512-ezbDkp2nDl0PfIUn0CsQ30kxfcLTlcx4Foz2kYv8qdC6ia2oX5Q3E/8m6lq84Dj/6b0FrkgD582fJMIfHhJfSw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/linux-ppc64": {
|
"@esbuild/linux-ppc64": {
|
||||||
"version": "0.16.7",
|
"version": "0.16.17",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.16.7.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.16.17.tgz",
|
||||||
"integrity": "sha512-WM83Dac0LdXty5xPhlOuCD5Egfk1xLND/oRLYeB7Jb/tY4kzFSDgLlq91wYbHua/s03tQGA9iXvyjgymMw62Vw==",
|
"integrity": "sha512-dzS678gYD1lJsW73zrFhDApLVdM3cUF2MvAa1D8K8KtcSKdLBPP4zZSLy6LFZ0jYqQdQ29bjAHJDgz0rVbLB3g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/linux-riscv64": {
|
"@esbuild/linux-riscv64": {
|
||||||
"version": "0.16.7",
|
"version": "0.16.17",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.16.7.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.16.17.tgz",
|
||||||
"integrity": "sha512-3nkNnNg4Ax6MS/l8O8Ynq2lGEVJYyJ2EoY3PHjNJ4PuZ80EYLMrFTFZ4L/Hc16AxgtXKwmNP9TM0YKNiBzBiJQ==",
|
"integrity": "sha512-ylNlVsxuFjZK8DQtNUwiMskh6nT0vI7kYl/4fZgV1llP5d6+HIeL/vmmm3jpuoo8+NuXjQVZxmKuhDApK0/cKw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/linux-s390x": {
|
"@esbuild/linux-s390x": {
|
||||||
"version": "0.16.7",
|
"version": "0.16.17",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.16.7.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.16.17.tgz",
|
||||||
"integrity": "sha512-3SA/2VJuv0o1uD7zuqxEP+RrAyRxnkGddq0bwHQ98v1KNlzXD/JvxwTO3T6GM5RH6JUd29RTVQTOJfyzMkkppA==",
|
"integrity": "sha512-gzy7nUTO4UA4oZ2wAMXPNBGTzZFP7mss3aKR2hH+/4UUkCOyqmjXiKpzGrY2TlEUhbbejzXVKKGazYcQTZWA/w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/linux-x64": {
|
"@esbuild/linux-x64": {
|
||||||
"version": "0.16.7",
|
"version": "0.16.17",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.16.7.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.16.17.tgz",
|
||||||
"integrity": "sha512-xi/tbqCqvPIzU+zJVyrpz12xqciTAPMi2fXEWGnapZymoGhuL2GIWIRXg4O2v5BXaYA5TSaiKYE14L0QhUTuQg==",
|
"integrity": "sha512-mdPjPxfnmoqhgpiEArqi4egmBAMYvaObgn4poorpUaqmvzzbvqbowRllQ+ZgzGVMGKaPkqUmPDOOFQRUFDmeUw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/netbsd-x64": {
|
"@esbuild/netbsd-x64": {
|
||||||
"version": "0.16.7",
|
"version": "0.16.17",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.16.7.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.16.17.tgz",
|
||||||
"integrity": "sha512-NUsYbq3B+JdNKn8SXkItFvdes9qTwEoS3aLALtiWciW/ystiCKM20Fgv9XQBOXfhUHyh5CLEeZDXzLOrwBXuCQ==",
|
"integrity": "sha512-/PzmzD/zyAeTUsduZa32bn0ORug+Jd1EGGAUJvqfeixoEISYpGnAezN6lnJoskauoai0Jrs+XSyvDhppCPoKOA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/openbsd-x64": {
|
"@esbuild/openbsd-x64": {
|
||||||
"version": "0.16.7",
|
"version": "0.16.17",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.16.7.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.16.17.tgz",
|
||||||
"integrity": "sha512-qjwzsgeve9I8Tbsko2FEkdSk2iiezuNGFgipQxY/736NePXDaDZRodIejYGWOlbYXugdxb0nif5yvypH6lKBmA==",
|
"integrity": "sha512-2yaWJhvxGEz2RiftSk0UObqJa/b+rIAjnODJgv2GbGGpRwAfpgzyrg1WLK8rqA24mfZa9GvpjLcBBg8JHkoodg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/sunos-x64": {
|
"@esbuild/sunos-x64": {
|
||||||
"version": "0.16.7",
|
"version": "0.16.17",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.16.7.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.16.17.tgz",
|
||||||
"integrity": "sha512-mFWDz4RoBTzPphTCkM7Kc7Qpa0o/Z01acajR+Ai7LdfKgcP/C6jYOaKwv7nKzD0+MjOT20j7You9g4ozYy1dKQ==",
|
"integrity": "sha512-xtVUiev38tN0R3g8VhRfN7Zl42YCJvyBhRKw1RJjwE1d2emWTVToPLNEQj/5Qxc6lVFATDiy6LjVHYhIPrLxzw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/win32-arm64": {
|
"@esbuild/win32-arm64": {
|
||||||
"version": "0.16.7",
|
"version": "0.16.17",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.16.7.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.16.17.tgz",
|
||||||
"integrity": "sha512-m39UmX19RvEIuC8sYZ0M+eQtdXw4IePDSZ78ZQmYyFaXY9krq4YzQCK2XWIJomNLtg4q+W5aXr8bW3AbqWNoVg==",
|
"integrity": "sha512-ga8+JqBDHY4b6fQAmOgtJJue36scANy4l/rL97W+0wYmijhxKetzZdKOJI7olaBaMhWt8Pac2McJdZLxXWUEQw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/win32-ia32": {
|
"@esbuild/win32-ia32": {
|
||||||
"version": "0.16.7",
|
"version": "0.16.17",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.16.7.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.16.17.tgz",
|
||||||
"integrity": "sha512-1cbzSEZA1fANwmT6rjJ4G1qQXHxCxGIcNYFYR9ctI82/prT38lnwSRZ0i5p/MVXksw9eMlHlet6pGu2/qkXFCg==",
|
"integrity": "sha512-WnsKaf46uSSF/sZhwnqE4L/F89AYNMiD4YtEcYekBt9Q7nj0DiId2XH2Ng2PHM54qi5oPrQ8luuzGszqi/veig==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/win32-x64": {
|
"@esbuild/win32-x64": {
|
||||||
"version": "0.16.7",
|
"version": "0.16.17",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.16.7.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.16.17.tgz",
|
||||||
"integrity": "sha512-QaQ8IH0JLacfGf5cf0HCCPnQuCTd/dAI257vXBgb/cccKGbH/6pVtI1gwhdAQ0Y48QSpTIFrh9etVyNdZY+zzw==",
|
"integrity": "sha512-y+EHuSchhL7FjHgvQL/0fnnFmO4T1bhvWANX6gcnqTjtnKWbTvUMCpGnv2+t+31d7RzyEAYAd4u2fnIhHL6N/Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
@ -8627,9 +8627,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@iconify-icons/mingcute": {
|
"@iconify-icons/mingcute": {
|
||||||
"version": "1.2.3",
|
"version": "1.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@iconify-icons/mingcute/-/mingcute-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/@iconify-icons/mingcute/-/mingcute-1.2.4.tgz",
|
||||||
"integrity": "sha512-yZyioZhNy61SkLxQoyHThsfuyaOej9n84PUS+K69qaS1Dyj7/wHwYhWXseFCnzyzicaEHkCpt6H/hYV8fwmMLg==",
|
"integrity": "sha512-4aaWYa6GxSdYmJg8iBVx6VDuKUcTDEbio929+GrswoxfyTsPUkOOgw2wffUDHjE3JDUAnrWj9teQTnBkFm7Gyg==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@iconify/types": "*"
|
"@iconify/types": "*"
|
||||||
}
|
}
|
||||||
|
@ -8856,9 +8856,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@szhsin/react-menu": {
|
"@szhsin/react-menu": {
|
||||||
"version": "3.4.0",
|
"version": "3.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/@szhsin/react-menu/-/react-menu-3.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/@szhsin/react-menu/-/react-menu-3.4.1.tgz",
|
||||||
"integrity": "sha512-BRxUF3BNmaQzL1z8UkS/eNFj+5ZPjBOpsYWP/ZtrzHNkkP6hUc+tYerBV4dwFiGYmWzkxyOP44ISn+EujwPpUw==",
|
"integrity": "sha512-Pxt7Kyp3yuX7zkT5tjdLRJGNFMa5Tx4BP+01gJ/dnMmHQpI1H2or9gEC0X+t3cLldO3LGmm4ViGypNCmQLv/4A==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"prop-types": "^15.7.2",
|
"prop-types": "^15.7.2",
|
||||||
"react-transition-state": "^1.1.5"
|
"react-transition-state": "^1.1.5"
|
||||||
|
@ -9542,33 +9542,33 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"esbuild": {
|
"esbuild": {
|
||||||
"version": "0.16.7",
|
"version": "0.16.17",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.16.7.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.16.17.tgz",
|
||||||
"integrity": "sha512-P6OBFYFSQOGzfApqCeYKqfKRRbCIRsdppTXFo4aAvtiW3o8TTyiIplBvHJI171saPAiy3WlawJHCveJVIOIx1A==",
|
"integrity": "sha512-G8LEkV0XzDMNwXKgM0Jwu3nY3lSTwSGY6XbxM9cr9+s0T/qSV1q1JVPBGzm3dcjhCic9+emZDmMffkwgPeOeLg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@esbuild/android-arm": "0.16.7",
|
"@esbuild/android-arm": "0.16.17",
|
||||||
"@esbuild/android-arm64": "0.16.7",
|
"@esbuild/android-arm64": "0.16.17",
|
||||||
"@esbuild/android-x64": "0.16.7",
|
"@esbuild/android-x64": "0.16.17",
|
||||||
"@esbuild/darwin-arm64": "0.16.7",
|
"@esbuild/darwin-arm64": "0.16.17",
|
||||||
"@esbuild/darwin-x64": "0.16.7",
|
"@esbuild/darwin-x64": "0.16.17",
|
||||||
"@esbuild/freebsd-arm64": "0.16.7",
|
"@esbuild/freebsd-arm64": "0.16.17",
|
||||||
"@esbuild/freebsd-x64": "0.16.7",
|
"@esbuild/freebsd-x64": "0.16.17",
|
||||||
"@esbuild/linux-arm": "0.16.7",
|
"@esbuild/linux-arm": "0.16.17",
|
||||||
"@esbuild/linux-arm64": "0.16.7",
|
"@esbuild/linux-arm64": "0.16.17",
|
||||||
"@esbuild/linux-ia32": "0.16.7",
|
"@esbuild/linux-ia32": "0.16.17",
|
||||||
"@esbuild/linux-loong64": "0.16.7",
|
"@esbuild/linux-loong64": "0.16.17",
|
||||||
"@esbuild/linux-mips64el": "0.16.7",
|
"@esbuild/linux-mips64el": "0.16.17",
|
||||||
"@esbuild/linux-ppc64": "0.16.7",
|
"@esbuild/linux-ppc64": "0.16.17",
|
||||||
"@esbuild/linux-riscv64": "0.16.7",
|
"@esbuild/linux-riscv64": "0.16.17",
|
||||||
"@esbuild/linux-s390x": "0.16.7",
|
"@esbuild/linux-s390x": "0.16.17",
|
||||||
"@esbuild/linux-x64": "0.16.7",
|
"@esbuild/linux-x64": "0.16.17",
|
||||||
"@esbuild/netbsd-x64": "0.16.7",
|
"@esbuild/netbsd-x64": "0.16.17",
|
||||||
"@esbuild/openbsd-x64": "0.16.7",
|
"@esbuild/openbsd-x64": "0.16.17",
|
||||||
"@esbuild/sunos-x64": "0.16.7",
|
"@esbuild/sunos-x64": "0.16.17",
|
||||||
"@esbuild/win32-arm64": "0.16.7",
|
"@esbuild/win32-arm64": "0.16.17",
|
||||||
"@esbuild/win32-ia32": "0.16.7",
|
"@esbuild/win32-ia32": "0.16.17",
|
||||||
"@esbuild/win32-x64": "0.16.7"
|
"@esbuild/win32-x64": "0.16.17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"escalade": {
|
"escalade": {
|
||||||
|
@ -10288,9 +10288,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"masto": {
|
"masto": {
|
||||||
"version": "5.7.0",
|
"version": "5.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/masto/-/masto-5.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/masto/-/masto-5.10.0.tgz",
|
||||||
"integrity": "sha512-oCVReGLR9AoBSkShVgWrPb69xzfCpogiPH9wVT9AnPbM+CvjIqJuSaD1xgaEUi6jo66XGbhTC6UerSCVqq9emg==",
|
"integrity": "sha512-RlTw3X2b2ipkcgsgoKEWKKFNYkpAlUtJhNOFKwBKWEBv+we/ZupQbnerGOJssB5rs7ig4HWWsZZHLtNeFdYQTQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@mastojs/ponyfills": "^1.0.4",
|
"@mastojs/ponyfills": "^1.0.4",
|
||||||
"change-case": "^4.1.2",
|
"change-case": "^4.1.2",
|
||||||
|
@ -10870,9 +10870,9 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"preact": {
|
"preact": {
|
||||||
"version": "10.11.3",
|
"version": "10.12.1",
|
||||||
"resolved": "https://registry.npmjs.org/preact/-/preact-10.11.3.tgz",
|
"resolved": "https://registry.npmjs.org/preact/-/preact-10.12.1.tgz",
|
||||||
"integrity": "sha512-eY93IVpod/zG3uMF22Unl8h9KkrcKIRs2EGar8hwLZZDU1lkjph303V9HZBwufh2s736U6VXuhD109LYqPoffg=="
|
"integrity": "sha512-l8386ixSsBdbreOAkqtrwqHwdvR35ID8c3rKPa8lCWuO86dBi32QWHV4vfsZK1utLLFMvw+Z5Ad4XLkZzchscg=="
|
||||||
},
|
},
|
||||||
"prettier": {
|
"prettier": {
|
||||||
"version": "2.8.0",
|
"version": "2.8.0",
|
||||||
|
@ -10951,15 +10951,15 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"react-hotkeys-hook": {
|
"react-hotkeys-hook": {
|
||||||
"version": "4.3.3",
|
"version": "4.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/react-hotkeys-hook/-/react-hotkeys-hook-4.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/react-hotkeys-hook/-/react-hotkeys-hook-4.3.5.tgz",
|
||||||
"integrity": "sha512-OYZCG2G+xLeiH0TkrW+v6eKFPvYq9iCA5sh9pwvnbGQaK86Lw/kWJDjjzSksFJoCk4K78OLe3MR1afNZH6+cLg==",
|
"integrity": "sha512-tfwTwKP3ga7n4naNS/JOByaEwEkTCoXYCepDuhXpj8mBx+sFszV5JecRWM2dv+PbOowmmBpHAFtTXTnG/p8UkQ==",
|
||||||
"requires": {}
|
"requires": {}
|
||||||
},
|
},
|
||||||
"react-intersection-observer": {
|
"react-intersection-observer": {
|
||||||
"version": "9.4.1",
|
"version": "9.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-9.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-9.4.2.tgz",
|
||||||
"integrity": "sha512-IXpIsPe6BleFOEHKzKh5UjwRUaz/JYS0lT/HPsupWEQou2hDqjhLMStc5zyE3eQVT4Fk3FufM8Fw33qW1uyeiw==",
|
"integrity": "sha512-AdK+ryzZ7U9ZJYttDUZ8q2Am3nqE0exg5Ryl5Y124KeVsix/1hGZPbdu58EqA98TwnzwDNWHxg/kwNawmIiUig==",
|
||||||
"requires": {}
|
"requires": {}
|
||||||
},
|
},
|
||||||
"react-is": {
|
"react-is": {
|
||||||
|
@ -11097,9 +11097,9 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"rollup": {
|
"rollup": {
|
||||||
"version": "3.7.4",
|
"version": "3.12.1",
|
||||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.7.4.tgz",
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.12.1.tgz",
|
||||||
"integrity": "sha512-jN9rx3k5pfg9H9al0r0y1EYKSeiRANZRYX32SuNXAnKzh6cVyf4LZVto1KAuDnbHT03E1CpsgqDKaqQ8FZtgxw==",
|
"integrity": "sha512-t9elERrz2i4UU9z7AwISj3CQcXP39cWxgRWLdf4Tm6aKm1eYrqHIgjzXBgb67GNY1sZckTFFi0oMozh3/S++Ig==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"fsevents": "~2.3.2"
|
"fsevents": "~2.3.2"
|
||||||
|
@ -11545,16 +11545,16 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"vite": {
|
"vite": {
|
||||||
"version": "4.0.4",
|
"version": "4.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-4.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-4.1.2.tgz",
|
||||||
"integrity": "sha512-xevPU7M8FU0i/80DMR+YhgrzR5KS2ORy1B4xcX/cXLsvnUWvfHuqMmVU6N0YiJ4JWGRJJsLCgjEzKjG9/GKoSw==",
|
"integrity": "sha512-MWDb9Rfy3DI8omDQySbMK93nQqStwbsQWejXRY2EBzEWKmLAXWb1mkI9Yw2IJrc+oCvPCI1Os5xSSIBYY6DEAw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"esbuild": "^0.16.3",
|
"esbuild": "^0.16.14",
|
||||||
"fsevents": "~2.3.2",
|
"fsevents": "~2.3.2",
|
||||||
"postcss": "^8.4.20",
|
"postcss": "^8.4.21",
|
||||||
"resolve": "^1.22.1",
|
"resolve": "^1.22.1",
|
||||||
"rollup": "^3.7.0"
|
"rollup": "^3.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"vite-plugin-html-config": {
|
"vite-plugin-html-config": {
|
||||||
|
@ -11572,9 +11572,9 @@
|
||||||
"requires": {}
|
"requires": {}
|
||||||
},
|
},
|
||||||
"vite-plugin-pwa": {
|
"vite-plugin-pwa": {
|
||||||
"version": "0.14.1",
|
"version": "0.14.4",
|
||||||
"resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.14.1.tgz",
|
"resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.14.4.tgz",
|
||||||
"integrity": "sha512-5zx7yhQ8RTLwV71+GA9YsQQ63ALKG8XXIMqRJDdZkR8ZYftFcRgnzM7wOWmQZ/DATspyhPih5wCdcZnAIsM+mA==",
|
"integrity": "sha512-M7Ct0so8OlouMkTWgXnl8W1xU95glITSKIe7qswZf1tniAstO2idElGCnsrTJ5NPNSx1XqfTCOUj8j94S6FD7Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@rollup/plugin-replace": "^5.0.1",
|
"@rollup/plugin-replace": "^5.0.1",
|
||||||
|
@ -11587,9 +11587,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"vite-plugin-remove-console": {
|
"vite-plugin-remove-console": {
|
||||||
"version": "1.3.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/vite-plugin-remove-console/-/vite-plugin-remove-console-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/vite-plugin-remove-console/-/vite-plugin-remove-console-2.0.0.tgz",
|
||||||
"integrity": "sha512-5a/OLYB6yNRHMuHj9rBQRYMQ1NBKffxA8BaD77urUBLcGOWMHFHALjh6C26wZfZd41KytSwLp6DhvNKU78mNJg==",
|
"integrity": "sha512-bEsyShSacsunbm0X1zaVliwgmWlsaBPLk7FN4wr2xQMs8zSZPSwpRNTT5UZiF0+cfMEkN4VVnofITawmT3pjgQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"webidl-conversions": {
|
"webidl-conversions": {
|
||||||
|
|
20
package.json
20
package.json
|
@ -11,20 +11,20 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@github/text-expander-element": "~2.3.0",
|
"@github/text-expander-element": "~2.3.0",
|
||||||
"@iconify-icons/mingcute": "~1.2.3",
|
"@iconify-icons/mingcute": "~1.2.4",
|
||||||
"@szhsin/react-menu": "~3.4.0",
|
"@szhsin/react-menu": "~3.4.1",
|
||||||
"dayjs": "~1.11.7",
|
"dayjs": "~1.11.7",
|
||||||
"dayjs-twitter": "~0.5.0",
|
"dayjs-twitter": "~0.5.0",
|
||||||
"fast-blurhash": "~1.1.2",
|
"fast-blurhash": "~1.1.2",
|
||||||
"fast-deep-equal": "~3.1.3",
|
"fast-deep-equal": "~3.1.3",
|
||||||
"idb-keyval": "~6.2.0",
|
"idb-keyval": "~6.2.0",
|
||||||
"just-debounce-it": "~3.2.0",
|
"just-debounce-it": "~3.2.0",
|
||||||
"masto": "~5.7.0",
|
"masto": "~5.10.0",
|
||||||
"mem": "~9.0.2",
|
"mem": "~9.0.2",
|
||||||
"p-retry": "~5.1.2",
|
"p-retry": "~5.1.2",
|
||||||
"preact": "~10.11.3",
|
"preact": "~10.12.1",
|
||||||
"react-hotkeys-hook": "~4.3.3",
|
"react-hotkeys-hook": "~4.3.5",
|
||||||
"react-intersection-observer": "~9.4.1",
|
"react-intersection-observer": "~9.4.2",
|
||||||
"react-router-dom": "6.6.2",
|
"react-router-dom": "6.6.2",
|
||||||
"string-length": "~5.0.1",
|
"string-length": "~5.0.1",
|
||||||
"swiped-events": "~1.1.7",
|
"swiped-events": "~1.1.7",
|
||||||
|
@ -32,7 +32,7 @@
|
||||||
"uid": "~2.0.1",
|
"uid": "~2.0.1",
|
||||||
"use-debounce": "~9.0.3",
|
"use-debounce": "~9.0.3",
|
||||||
"use-resize-observer": "~9.1.0",
|
"use-resize-observer": "~9.1.0",
|
||||||
"valtio": "~1.9.0"
|
"valtio": "1.9.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@preact/preset-vite": "~2.5.0",
|
"@preact/preset-vite": "~2.5.0",
|
||||||
|
@ -41,11 +41,11 @@
|
||||||
"postcss-dark-theme-class": "~0.7.3",
|
"postcss-dark-theme-class": "~0.7.3",
|
||||||
"postcss-preset-env": "~8.0.1",
|
"postcss-preset-env": "~8.0.1",
|
||||||
"twitter-text": "~3.1.0",
|
"twitter-text": "~3.1.0",
|
||||||
"vite": "~4.0.4",
|
"vite": "~4.1.2",
|
||||||
"vite-plugin-html-config": "~1.0.11",
|
"vite-plugin-html-config": "~1.0.11",
|
||||||
"vite-plugin-html-env": "~1.2.7",
|
"vite-plugin-html-env": "~1.2.7",
|
||||||
"vite-plugin-pwa": "~0.14.1",
|
"vite-plugin-pwa": "~0.14.4",
|
||||||
"vite-plugin-remove-console": "~1.3.0",
|
"vite-plugin-remove-console": "~2.0.0",
|
||||||
"workbox-cacheable-response": "~6.5.4",
|
"workbox-cacheable-response": "~6.5.4",
|
||||||
"workbox-expiration": "~6.5.4",
|
"workbox-expiration": "~6.5.4",
|
||||||
"workbox-routing": "~6.5.4",
|
"workbox-routing": "~6.5.4",
|
||||||
|
|
|
@ -33,7 +33,7 @@ const imageRoute = new Route(
|
||||||
);
|
);
|
||||||
registerRoute(imageRoute);
|
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(
|
const apiExtendedRoute = new RegExpRoute(
|
||||||
/^https?:\/\/[^\/]+\/api\/v\d+\/(instance|custom_emojis)/,
|
/^https?:\/\/[^\/]+\/api\/v\d+\/(instance|custom_emojis)/,
|
||||||
new StaleWhileRevalidate({
|
new StaleWhileRevalidate({
|
||||||
|
|
396
src/app.css
396
src/app.css
|
@ -1,3 +1,6 @@
|
||||||
|
@import url('@szhsin/react-menu/dist/core.css');
|
||||||
|
@import url('toastify-js/src/toastify.css');
|
||||||
|
|
||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
@ -69,7 +72,7 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
min-height: 100dvh;
|
min-height: 100dvh;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
width: 40em;
|
width: var(--main-width);
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
border-left: 1px solid rgba(0, 0, 0, 0.1);
|
border-left: 1px solid rgba(0, 0, 0, 0.1);
|
||||||
border-right: 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 {
|
.deck > header {
|
||||||
min-height: 3em;
|
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
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;
|
z-index: 1;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr 1fr;
|
|
||||||
align-items: center;
|
|
||||||
user-select: none;
|
user-select: none;
|
||||||
transition: transform 0.5s ease-in-out;
|
transition: transform 0.5s ease-in-out;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
.deck > header[hidden] {
|
.deck > header[hidden] {
|
||||||
|
display: block;
|
||||||
transform: translateY(-100%);
|
transform: translateY(-100%);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
user-select: 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;
|
text-align: right;
|
||||||
grid-column: 3;
|
grid-column: 3;
|
||||||
}
|
}
|
||||||
.deck > header :is(button, .button).plain {
|
.deck > header .header-grid :is(button, .button).plain {
|
||||||
backdrop-filter: none;
|
backdrop-filter: none;
|
||||||
}
|
}
|
||||||
.deck > header h1 {
|
.deck > header .header-grid h1 {
|
||||||
margin: 0 8px;
|
margin: 0 8px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
font-size: 1.2em;
|
font-size: 1.2em;
|
||||||
text-align: center;
|
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;
|
text-align: left;
|
||||||
padding-left: 8px;
|
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 {
|
.deck h2 {
|
||||||
font-size: 1.45em;
|
font-size: 1.45em;
|
||||||
}
|
}
|
||||||
|
@ -528,46 +550,44 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
|
||||||
filter: brightness(0.95);
|
filter: brightness(0.95);
|
||||||
}
|
}
|
||||||
|
|
||||||
.boost-carousel {
|
.status-carousel {
|
||||||
|
--carousel-faded-color: var(--bg-faded-color);
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
to bottom right,
|
to bottom right,
|
||||||
var(--reblog-faded-color),
|
var(--carousel-faded-color),
|
||||||
transparent 150%
|
transparent 150%
|
||||||
);
|
);
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
.boost-carousel:after {
|
.status-carousel:after {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
background-image: radial-gradient(
|
background-image: radial-gradient(
|
||||||
ellipse 50% 32px at bottom center,
|
ellipse 50% 32px at bottom center,
|
||||||
var(--reblog-faded-color),
|
var(--carousel-faded-color),
|
||||||
transparent
|
transparent
|
||||||
),
|
),
|
||||||
linear-gradient(to top, var(--bg-color), transparent 64px);
|
linear-gradient(to top, var(--bg-color), transparent 64px);
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-position: bottom center;
|
background-position: bottom center;
|
||||||
}
|
}
|
||||||
.boost-carousel .status-reblog {
|
.status-carousel header {
|
||||||
background-image: none;
|
|
||||||
}
|
|
||||||
.boost-carousel header {
|
|
||||||
padding: 8px 16px 0;
|
padding: 8px 16px 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
.boost-carousel h3 {
|
.status-carousel h3 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
color: var(--reblog-color);
|
color: var(--carousel-color);
|
||||||
text-shadow: 0 1px var(--bg-color);
|
text-shadow: 0 1px var(--bg-color);
|
||||||
}
|
}
|
||||||
.boost-carousel ul {
|
.status-carousel ul {
|
||||||
display: flex;
|
display: flex;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
|
@ -579,7 +599,7 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
counter-reset: index;
|
counter-reset: index;
|
||||||
}
|
}
|
||||||
.boost-carousel ul > li {
|
.status-carousel ul > li {
|
||||||
scroll-snap-align: center;
|
scroll-snap-align: center;
|
||||||
scroll-snap-stop: always;
|
scroll-snap-stop: always;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
@ -594,12 +614,33 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
|
||||||
counter-increment: index;
|
counter-increment: index;
|
||||||
position: relative;
|
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);
|
content: counter(index);
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
color: var(--reblog-color);
|
color: var(--carousel-color);
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -608,7 +649,7 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-boost-link {
|
.status-carousel-link {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
text-decoration-line: none;
|
text-decoration-line: none;
|
||||||
|
@ -623,15 +664,15 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-shadow: 0 1px var(--bg-color);
|
box-shadow: 0 1px var(--bg-color);
|
||||||
}
|
}
|
||||||
.status-boost-link::focus {
|
.status-carousel-link::focus {
|
||||||
background-color: var(--link-bg-hover-color);
|
background-color: var(--link-bg-hover-color);
|
||||||
}
|
}
|
||||||
@media (hover: hover) {
|
@media (hover: hover) {
|
||||||
.status-boost-link:hover {
|
.status-carousel-link:hover {
|
||||||
background-color: var(--link-bg-hover-color);
|
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);
|
filter: brightness(0.95);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -659,14 +700,14 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.deck-backdrop .deck {
|
.deck-backdrop .deck {
|
||||||
width: 40em;
|
width: var(--main-width);
|
||||||
max-width: 100vw;
|
max-width: 100vw;
|
||||||
background-color: var(--bg-color);
|
background-color: var(--bg-color);
|
||||||
animation: slide-in 0.5s var(--timing-function);
|
animation: slide-in 0.5s var(--timing-function);
|
||||||
box-shadow: -1px 0 var(--bg-color);
|
box-shadow: -1px 0 var(--bg-color);
|
||||||
}
|
}
|
||||||
.deck-backdrop .deck .status {
|
.deck-backdrop .deck .status {
|
||||||
max-width: 40em;
|
max-width: var(--main-width);
|
||||||
}
|
}
|
||||||
|
|
||||||
.deck-close {
|
.deck-close {
|
||||||
|
@ -701,18 +742,19 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
|
||||||
.updates-button {
|
.updates-button {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
animation: fade-from-top 0.3s ease-out;
|
top: 3em;
|
||||||
|
animation: fade-from-top 0.3s var(--timing-function);
|
||||||
left: 50%;
|
left: 50%;
|
||||||
margin-top: 8px;
|
margin-top: 16px;
|
||||||
transform: translate(-50%, 0);
|
transform: translate(-50%, 0);
|
||||||
font-size: 90%;
|
font-size: 90%;
|
||||||
background: linear-gradient(
|
background-color: var(--button-bg-color);
|
||||||
to bottom,
|
background-image: linear-gradient(
|
||||||
var(--button-bg-blur-color),
|
160deg,
|
||||||
var(--button-bg-color)
|
rgba(255, 255, 255, 0.5),
|
||||||
|
rgba(255, 255, 255, 0) 50%
|
||||||
);
|
);
|
||||||
backdrop-filter: blur(16px);
|
box-shadow: 0 3px 8px -1px var(--drop-shadow-color),
|
||||||
box-shadow: 0 3px 8px -1px var(--bg-faded-blur-color),
|
|
||||||
0 10px 36px -4px var(--button-bg-blur-color);
|
0 10px 36px -4px var(--button-bg-blur-color);
|
||||||
}
|
}
|
||||||
.updates-button .icon {
|
.updates-button .icon {
|
||||||
|
@ -722,7 +764,7 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
|
||||||
/* BOX */
|
/* BOX */
|
||||||
|
|
||||||
.box {
|
.box {
|
||||||
width: 40em;
|
width: var(--main-width);
|
||||||
max-width: 100vw;
|
max-width: 100vw;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
|
@ -853,6 +895,9 @@ button.carousel-dot:is(.active, [disabled].active) {
|
||||||
.media-post-link .button-label {
|
.media-post-link .button-label {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
body:has(.status-deck) .media-post-link {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
@media (min-width: calc(40em + 350px)) {
|
@media (min-width: calc(40em + 350px)) {
|
||||||
.media-post-link .button-label {
|
.media-post-link .button-label {
|
||||||
|
@ -889,10 +934,11 @@ button.carousel-dot:is(.active, [disabled].active) {
|
||||||
background-color: var(--button-bg-blur-color);
|
background-color: var(--button-bg-blur-color);
|
||||||
backdrop-filter: blur(16px);
|
backdrop-filter: blur(16px);
|
||||||
z-index: 1;
|
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);
|
0 10px 36px -4px var(--button-bg-blur-color);
|
||||||
transition: all 0.3s ease-in-out;
|
transition: all 0.3s ease-in-out;
|
||||||
}
|
}
|
||||||
|
#home-page:has(header[hidden]) ~ #compose-button,
|
||||||
#compose-button[hidden] {
|
#compose-button[hidden] {
|
||||||
transform: translateY(200%);
|
transform: translateY(200%);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
@ -902,14 +948,15 @@ button.carousel-dot:is(.active, [disabled].active) {
|
||||||
transition: transform 0.3s ease-in-out;
|
transition: transform 0.3s ease-in-out;
|
||||||
}
|
}
|
||||||
#compose-button[hidden] .icon {
|
#compose-button[hidden] .icon {
|
||||||
transform: rotate3d(0, 1, 0, 180deg);
|
transform: rotate3d(0, 0, 1, -25deg);
|
||||||
}
|
}
|
||||||
#compose-button:is(:hover, :focus) {
|
#compose-button:is(:hover, :focus) {
|
||||||
background-color: var(--button-bg-color);
|
background-color: var(--button-bg-color);
|
||||||
filter: none;
|
filter: none;
|
||||||
}
|
}
|
||||||
#compose-button:active {
|
#compose-button:active {
|
||||||
filter: brightness(0.75);
|
transform: scale(0.95);
|
||||||
|
transition: none;
|
||||||
}
|
}
|
||||||
#compose-button .icon {
|
#compose-button .icon {
|
||||||
filter: drop-shadow(0 1px 2px var(--button-bg-color));
|
filter: drop-shadow(0 1px 2px var(--button-bg-color));
|
||||||
|
@ -926,7 +973,7 @@ button.carousel-dot:is(.active, [disabled].active) {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background-color: var(--bg-color);
|
background-color: var(--bg-color);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: calc(40em - 50px - 16px);
|
max-width: calc(var(--main-width) - 50px - 16px);
|
||||||
border-radius: 16px 16px 0 0;
|
border-radius: 16px 16px 0 0;
|
||||||
box-shadow: 0 -1px 32px var(--divider-color);
|
box-shadow: 0 -1px 32px var(--divider-color);
|
||||||
animation: slide-up 0.3s var(--timing-function);
|
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);
|
mask-image: linear-gradient(to bottom, transparent 0%, black 10px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ICON */
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* TAG */
|
/* TAG */
|
||||||
|
|
||||||
.tag {
|
.tag {
|
||||||
|
@ -981,22 +1034,48 @@ button.carousel-dot:is(.active, [disabled].active) {
|
||||||
/* MENU POPUP */
|
/* MENU POPUP */
|
||||||
|
|
||||||
.szh-menu {
|
.szh-menu {
|
||||||
padding: 8px 0 !important;
|
padding: 8px 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
background-color: var(--bg-color) !important;
|
background-color: var(--bg-color);
|
||||||
border: 1px solid var(--outline-color) !important;
|
border: 1px solid var(--outline-color);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 3px 6px var(--drop-shadow-color);
|
box-shadow: 0 3px 6px var(--drop-shadow-color);
|
||||||
text-align: left;
|
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 {
|
.szh-menu .szh-menu__item {
|
||||||
padding: 8px 16px !important;
|
padding: 8px 16px !important;
|
||||||
|
transition: all 0.1s ease-in-out;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
.szh-menu .szh-menu__item * {
|
.szh-menu .szh-menu__item * {
|
||||||
vertical-align: middle;
|
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
|
||||||
.szh-menu__item:not(.szh-menu__item--disabled, .szh-menu__item--hover) {
|
.szh-menu__item:not(.szh-menu__item--disabled, .szh-menu__item--hover) {
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
|
@ -1005,6 +1084,28 @@ button.carousel-dot:is(.active, [disabled].active) {
|
||||||
color: var(--button-text-color);
|
color: var(--button-text-color);
|
||||||
background-color: var(--button-bg-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 */
|
/* DONUT METER */
|
||||||
|
|
||||||
|
@ -1064,16 +1165,16 @@ meter.donut:is(.danger, .explode):after {
|
||||||
/* TOAST */
|
/* TOAST */
|
||||||
|
|
||||||
:root .toastify {
|
:root .toastify {
|
||||||
|
background-color: var(--button-bg-color);
|
||||||
background-image: linear-gradient(
|
background-image: linear-gradient(
|
||||||
to bottom,
|
160deg,
|
||||||
var(--button-bg-blur-color),
|
rgba(255, 255, 255, 0.5),
|
||||||
var(--button-bg-color)
|
rgba(255, 255, 255, 0) 50%
|
||||||
);
|
);
|
||||||
backdrop-filter: blur(16px);
|
|
||||||
color: var(--button-text-color);
|
color: var(--button-text-color);
|
||||||
border-radius: 10em;
|
border-radius: 10em;
|
||||||
padding: 8px 16px;
|
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);
|
0 10px 36px -4px var(--button-bg-blur-color);
|
||||||
}
|
}
|
||||||
.toastify-bottom {
|
.toastify-bottom {
|
||||||
|
@ -1110,18 +1211,19 @@ meter.donut:is(.danger, .explode):after {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
#home-page ~ .deck-container {
|
:is(#home-page, #welcome, #columns) ~ .deck-container {
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
}
|
}
|
||||||
#home-page:has(~ .deck-container) {
|
:is(#home-page, #welcome, #columns):has(~ .deck-container) {
|
||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
content-visibility: hidden;
|
/* This causes scrollTop to be reset to 0 when the page is hidden */
|
||||||
|
/* content-visibility: hidden; */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* TAB BAR */
|
/* TAB BAR */
|
||||||
|
@ -1131,7 +1233,7 @@ meter.donut:is(.danger, .explode):after {
|
||||||
bottom: 16px;
|
bottom: 16px;
|
||||||
bottom: max(16px, env(safe-area-inset-bottom));
|
bottom: max(16px, env(safe-area-inset-bottom));
|
||||||
width: calc(100% - 32px);
|
width: calc(100% - 32px);
|
||||||
max-width: calc(40em - 32px);
|
max-width: calc(var(--main-width) - 32px);
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
display: flex;
|
display: flex;
|
||||||
background-color: var(--bg-blur-color);
|
background-color: var(--bg-blur-color);
|
||||||
|
@ -1150,6 +1252,15 @@ meter.donut:is(.danger, .explode):after {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 16px 0;
|
padding: 16px 0;
|
||||||
display: block;
|
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 */
|
/* 404 */
|
||||||
|
@ -1181,6 +1292,145 @@ meter.donut:is(.danger, .explode):after {
|
||||||
color: var(--text-insignificant-color);
|
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) {
|
@media (min-width: 40em) {
|
||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
|
@ -1201,7 +1451,7 @@ meter.donut:is(.danger, .explode):after {
|
||||||
}
|
}
|
||||||
.deck-backdrop .deck {
|
.deck-backdrop .deck {
|
||||||
width: 50%;
|
width: 50%;
|
||||||
min-width: 40em;
|
min-width: var(--main-width);
|
||||||
border-left: 1px solid var(--divider-color);
|
border-left: 1px solid var(--divider-color);
|
||||||
}
|
}
|
||||||
.timeline-deck {
|
.timeline-deck {
|
||||||
|
@ -1210,14 +1460,16 @@ meter.donut:is(.danger, .explode):after {
|
||||||
}
|
}
|
||||||
.timeline-deck > header {
|
.timeline-deck > header {
|
||||||
--margin-top: 8px;
|
--margin-top: 8px;
|
||||||
min-height: 4em;
|
|
||||||
top: var(--margin-top);
|
top: var(--margin-top);
|
||||||
border-bottom: 0;
|
margin-inline: 8px;
|
||||||
background-color: var(--bg-faded-blur-color);
|
}
|
||||||
background-image: none;
|
.timeline-deck > header .header-grid {
|
||||||
border-bottom: 0;
|
border-bottom: 0;
|
||||||
border-radius: 16px;
|
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] {
|
.timeline-deck > header[hidden] {
|
||||||
transform: translate3d(0, calc((100% + var(--margin-top)) * -1), 0);
|
transform: translate3d(0, calc((100% + var(--margin-top)) * -1), 0);
|
||||||
|
@ -1228,7 +1480,7 @@ meter.donut:is(.danger, .explode):after {
|
||||||
.updates-button {
|
.updates-button {
|
||||||
margin-top: 24px;
|
margin-top: 24px;
|
||||||
}
|
}
|
||||||
.timeline-deck .timeline:not(.flat) > li {
|
.timeline:not(.flat) > li {
|
||||||
border: 1px solid var(--divider-color);
|
border: 1px solid var(--divider-color);
|
||||||
margin: 16px 0;
|
margin: 16px 0;
|
||||||
background-color: var(--bg-color);
|
background-color: var(--bg-color);
|
||||||
|
@ -1238,16 +1490,14 @@ meter.donut:is(.danger, .explode):after {
|
||||||
transition: transform 0.4s var(--timing-function);
|
transition: transform 0.4s var(--timing-function);
|
||||||
--back-transition: transform 0.4s ease-out;
|
--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);
|
transition: var(--back-transition);
|
||||||
transform: translate3d(-2.5vw, 0, 0);
|
transform: translate3d(-2.5vw, 0, 0);
|
||||||
}
|
}
|
||||||
.timeline-deck
|
|
||||||
.timeline:not(.flat)
|
.timeline:not(.flat)
|
||||||
> li:not(:has(.boost-carousel)):has(+ li .status-link.is-active),
|
> li:not(:has(.status-carousel)):has(+ li .status-link.is-active),
|
||||||
.timeline-deck
|
|
||||||
.timeline:not(.flat)
|
.timeline:not(.flat)
|
||||||
> li:not(:has(.boost-carousel)):has(.status-link.is-active)
|
> li:not(:has(.status-carousel)):has(.status-link.is-active)
|
||||||
+ li {
|
+ li {
|
||||||
transition: var(--back-transition);
|
transition: var(--back-transition);
|
||||||
transform: translate3d(-1.25vw, 0, 0);
|
transform: translate3d(-1.25vw, 0, 0);
|
||||||
|
@ -1258,7 +1508,7 @@ meter.donut:is(.danger, .explode):after {
|
||||||
/* :is(.carousel-top-controls, .carousel-controls) {
|
/* :is(.carousel-top-controls, .carousel-controls) {
|
||||||
padding: 32px;
|
padding: 32px;
|
||||||
} */
|
} */
|
||||||
li:has(.boost-carousel) {
|
li:has(.status-carousel) {
|
||||||
width: 95vw;
|
width: 95vw;
|
||||||
max-width: calc(320px * 3.3);
|
max-width: calc(320px * 3.3);
|
||||||
transform: translateX(calc(-50% + 20em));
|
transform: translateX(calc(-50% + 20em));
|
||||||
|
|
556
src/app.jsx
556
src/app.jsx
|
@ -1,8 +1,5 @@
|
||||||
import './app.css';
|
import './app.css';
|
||||||
import 'toastify-js/src/toastify.css';
|
|
||||||
|
|
||||||
import debounce from 'just-debounce-it';
|
|
||||||
import { createClient } from 'masto';
|
|
||||||
import {
|
import {
|
||||||
useEffect,
|
useEffect,
|
||||||
useLayoutEffect,
|
useLayoutEffect,
|
||||||
|
@ -10,7 +7,13 @@ import {
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from 'preact/hooks';
|
} 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 Toastify from 'toastify-js';
|
||||||
import { useSnapshot } from 'valtio';
|
import { useSnapshot } from 'valtio';
|
||||||
|
|
||||||
|
@ -22,22 +25,38 @@ import Link from './components/link';
|
||||||
import Loader from './components/loader';
|
import Loader from './components/loader';
|
||||||
import MediaModal from './components/media-modal';
|
import MediaModal from './components/media-modal';
|
||||||
import Modal from './components/modal';
|
import Modal from './components/modal';
|
||||||
|
import Shortcuts from './components/shortcuts';
|
||||||
|
import ShortcutsSettings from './components/shortcuts-settings';
|
||||||
import NotFound from './pages/404';
|
import NotFound from './pages/404';
|
||||||
import AccountStatuses from './pages/account-statuses';
|
import AccountStatuses from './pages/account-statuses';
|
||||||
import Bookmarks from './pages/bookmarks';
|
import Bookmarks from './pages/bookmarks';
|
||||||
import Favourites from './pages/favourites';
|
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 Home from './pages/home';
|
||||||
|
import HomeV1 from './pages/home-v1';
|
||||||
|
import List from './pages/list';
|
||||||
import Lists from './pages/lists';
|
import Lists from './pages/lists';
|
||||||
import Login from './pages/login';
|
import Login from './pages/login';
|
||||||
import Notifications from './pages/notifications';
|
import Notifications from './pages/notifications';
|
||||||
import Public from './pages/public';
|
import Public from './pages/public';
|
||||||
|
import Search from './pages/search';
|
||||||
import Settings from './pages/settings';
|
import Settings from './pages/settings';
|
||||||
import Status from './pages/status';
|
import Status from './pages/status';
|
||||||
import Welcome from './pages/welcome';
|
import Welcome from './pages/welcome';
|
||||||
|
import {
|
||||||
|
api,
|
||||||
|
initAccount,
|
||||||
|
initClient,
|
||||||
|
initInstance,
|
||||||
|
initPreferences,
|
||||||
|
} from './utils/api';
|
||||||
import { getAccessToken } from './utils/auth';
|
import { getAccessToken } from './utils/auth';
|
||||||
import states, { saveStatus } from './utils/states';
|
import states, { getStatus, saveStatus } from './utils/states';
|
||||||
import store from './utils/store';
|
import store from './utils/store';
|
||||||
|
import { getCurrentAccount } from './utils/store-utils';
|
||||||
|
import usePageVisibility from './utils/usePageVisibility';
|
||||||
|
|
||||||
window.__STATES__ = states;
|
window.__STATES__ = states;
|
||||||
|
|
||||||
|
@ -53,13 +72,12 @@ function App() {
|
||||||
document.documentElement.classList.add(`is-${theme}`);
|
document.documentElement.classList.add(`is-${theme}`);
|
||||||
document
|
document
|
||||||
.querySelector('meta[name="color-scheme"]')
|
.querySelector('meta[name="color-scheme"]')
|
||||||
.setAttribute('content', theme);
|
.setAttribute('content', theme === 'auto' ? 'dark light' : theme);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const instanceURL = store.local.get('instanceURL');
|
const instanceURL = store.local.get('instanceURL');
|
||||||
const accounts = store.local.getJSON('accounts') || [];
|
|
||||||
const code = (window.location.search.match(/code=([^&]+)/) || [])[1];
|
const code = (window.location.search.match(/code=([^&]+)/) || [])[1];
|
||||||
|
|
||||||
if (code) {
|
if (code) {
|
||||||
|
@ -72,60 +90,44 @@ function App() {
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
setUIState('loading');
|
setUIState('loading');
|
||||||
const tokenJSON = await getAccessToken({
|
const { access_token: accessToken } = await getAccessToken({
|
||||||
instanceURL,
|
instanceURL,
|
||||||
client_id: clientID,
|
client_id: clientID,
|
||||||
client_secret: clientSecret,
|
client_secret: clientSecret,
|
||||||
code,
|
code,
|
||||||
});
|
});
|
||||||
const { access_token: accessToken } = tokenJSON;
|
|
||||||
store.session.set('accessToken', accessToken);
|
|
||||||
|
|
||||||
initMasto({
|
const masto = initClient({ instance: instanceURL, accessToken });
|
||||||
url: `https://${instanceURL}`,
|
await Promise.allSettled([
|
||||||
accessToken,
|
initInstance(masto),
|
||||||
});
|
initAccount(masto, instanceURL, accessToken),
|
||||||
|
]);
|
||||||
const mastoAccount = await masto.v1.accounts.verifyCredentials();
|
initPreferences(masto);
|
||||||
|
|
||||||
// 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);
|
|
||||||
|
|
||||||
setIsLoggedIn(true);
|
setIsLoggedIn(true);
|
||||||
setUIState('default');
|
setUIState('default');
|
||||||
})();
|
})();
|
||||||
} else if (accounts.length) {
|
} else {
|
||||||
const currentAccount = store.session.get('currentAccount');
|
const account = getCurrentAccount();
|
||||||
const account =
|
if (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);
|
store.session.set('currentAccount', account.info.id);
|
||||||
if (accessToken) setIsLoggedIn(true);
|
const { masto } = api({ account });
|
||||||
|
console.log('masto', masto);
|
||||||
initMasto({
|
initPreferences(masto);
|
||||||
url: `https://${instanceURL}`,
|
setUIState('loading');
|
||||||
accessToken,
|
(async () => {
|
||||||
});
|
try {
|
||||||
|
await initInstance(masto);
|
||||||
|
} catch (e) {
|
||||||
|
} finally {
|
||||||
|
setIsLoggedIn(true);
|
||||||
|
setUIState('default');
|
||||||
|
}
|
||||||
|
})();
|
||||||
} else {
|
} else {
|
||||||
setUIState('default');
|
setUIState('default');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
let location = useLocation();
|
let location = useLocation();
|
||||||
|
@ -146,28 +148,79 @@ function App() {
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
};
|
};
|
||||||
useEffect(focusDeck, [location]);
|
useEffect(focusDeck, [location]);
|
||||||
|
const showModal =
|
||||||
|
snapStates.showCompose ||
|
||||||
|
snapStates.showSettings ||
|
||||||
|
snapStates.showAccount ||
|
||||||
|
snapStates.showDrafts ||
|
||||||
|
snapStates.showMediaModal ||
|
||||||
|
snapStates.showShortcutsSettings;
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (!showModal) focusDeck();
|
||||||
!snapStates.showCompose &&
|
}, [showModal]);
|
||||||
!snapStates.showSettings &&
|
|
||||||
!snapStates.showAccount
|
|
||||||
) {
|
|
||||||
focusDeck();
|
|
||||||
}
|
|
||||||
}, [snapStates.showCompose, snapStates.showSettings, snapStates.showAccount]);
|
|
||||||
|
|
||||||
|
// 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(() => {
|
useEffect(() => {
|
||||||
// HACK: prevent this from running again due to HMR
|
if (isLoggedIn && visible) {
|
||||||
if (states.init) return;
|
const { masto } = api();
|
||||||
if (isLoggedIn) {
|
(async () => {
|
||||||
requestAnimationFrame(startVisibility);
|
// 1. Get the latest notification
|
||||||
states.init = true;
|
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;
|
||||||
}
|
}
|
||||||
}, [isLoggedIn]);
|
}
|
||||||
|
|
||||||
|
// 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');
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
if (notificationStream.current) {
|
||||||
|
notificationStream.current.ws.close();
|
||||||
|
notificationStream.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [visible, isLoggedIn]);
|
||||||
|
|
||||||
const { prevLocation } = snapStates;
|
const { prevLocation } = snapStates;
|
||||||
const backgroundLocation = useRef(prevLocation || null);
|
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 (isModalPage) {
|
||||||
if (!backgroundLocation.current) backgroundLocation.current = prevLocation;
|
if (!backgroundLocation.current) backgroundLocation.current = prevLocation;
|
||||||
} else {
|
} else {
|
||||||
|
@ -180,7 +233,7 @@ function App() {
|
||||||
|
|
||||||
const nonRootLocation = useMemo(() => {
|
const nonRootLocation = useMemo(() => {
|
||||||
const { pathname } = location;
|
const { pathname } = location;
|
||||||
return !/^\/(login|welcome|p)/.test(pathname);
|
return !/^\/(login|welcome)/.test(pathname);
|
||||||
}, [location]);
|
}, [location]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -205,16 +258,28 @@ function App() {
|
||||||
{isLoggedIn && (
|
{isLoggedIn && (
|
||||||
<Route path="/notifications" element={<Notifications />} />
|
<Route path="/notifications" element={<Notifications />} />
|
||||||
)}
|
)}
|
||||||
|
{isLoggedIn && <Route path="/following" element={<Following />} />}
|
||||||
|
{isLoggedIn && <Route path="/homev1" element={<HomeV1 />} />}
|
||||||
{isLoggedIn && <Route path="/b" element={<Bookmarks />} />}
|
{isLoggedIn && <Route path="/b" element={<Bookmarks />} />}
|
||||||
{isLoggedIn && <Route path="/f" element={<Favourites />} />}
|
{isLoggedIn && <Route path="/f" element={<Favourites />} />}
|
||||||
{isLoggedIn && <Route path="/l/:id" element={<Lists />} />}
|
{isLoggedIn && (
|
||||||
{isLoggedIn && <Route path="/t/:hashtag" element={<Hashtags />} />}
|
<Route path="/l">
|
||||||
{isLoggedIn && <Route path="/a/:id" element={<AccountStatuses />} />}
|
<Route index element={<Lists />} />
|
||||||
<Route path="/p/l?/:instance" element={<Public />} />
|
<Route path=":id" element={<List />} />
|
||||||
|
</Route>
|
||||||
|
)}
|
||||||
|
{isLoggedIn && <Route path="/ft" element={<FollowedHashtags />} />}
|
||||||
|
<Route path="/:instance?/t/:hashtag" element={<Hashtag />} />
|
||||||
|
<Route path="/:instance?/a/:id" element={<AccountStatuses />} />
|
||||||
|
<Route path="/:instance?/p">
|
||||||
|
<Route index element={<Public />} />
|
||||||
|
<Route path="l" element={<Public local />} />
|
||||||
|
</Route>
|
||||||
|
<Route path="/:instance?/search" element={<Search />} />
|
||||||
{/* <Route path="/:anything" element={<NotFound />} /> */}
|
{/* <Route path="/:anything" element={<NotFound />} /> */}
|
||||||
</Routes>
|
</Routes>
|
||||||
<Routes>
|
<Routes>
|
||||||
{isLoggedIn && <Route path="/s/:id" element={<Status />} />}
|
<Route path="/:instance?/s/:id" element={<Status />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
<nav id="tab-bar" hidden>
|
<nav id="tab-bar" hidden>
|
||||||
<li>
|
<li>
|
||||||
|
@ -233,6 +298,7 @@ function App() {
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
</nav>
|
</nav>
|
||||||
|
{!snapStates.settings.shortcutsColumnsMode && <Shortcuts />}
|
||||||
{!!snapStates.showCompose && (
|
{!!snapStates.showCompose && (
|
||||||
<Modal>
|
<Modal>
|
||||||
<Compose
|
<Compose
|
||||||
|
@ -302,7 +368,8 @@ function App() {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Account
|
<Account
|
||||||
account={snapStates.showAccount}
|
account={snapStates.showAccount?.account || snapStates.showAccount}
|
||||||
|
instance={snapStates.showAccount?.instance}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
states.showAccount = false;
|
states.showAccount = false;
|
||||||
}}
|
}}
|
||||||
|
@ -333,6 +400,7 @@ function App() {
|
||||||
>
|
>
|
||||||
<MediaModal
|
<MediaModal
|
||||||
mediaAttachments={snapStates.showMediaModal.mediaAttachments}
|
mediaAttachments={snapStates.showMediaModal.mediaAttachments}
|
||||||
|
instance={snapStates.showMediaModal.instance}
|
||||||
index={snapStates.showMediaModal.index}
|
index={snapStates.showMediaModal.index}
|
||||||
statusID={snapStates.showMediaModal.statusID}
|
statusID={snapStates.showMediaModal.statusID}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
|
@ -341,213 +409,179 @@ function App() {
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
)}
|
)}
|
||||||
|
{!!snapStates.showShortcutsSettings && (
|
||||||
|
<Modal
|
||||||
|
onClick={(e) => {
|
||||||
|
if (e.target === e.currentTarget) {
|
||||||
|
states.showShortcutsSettings = false;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ShortcutsSettings />
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function initMasto(params) {
|
// let ws;
|
||||||
const clientParams = {
|
// async function startStream() {
|
||||||
url: params.url || 'https://mastodon.social',
|
// const { masto, instance } = api();
|
||||||
accessToken: params.accessToken || null,
|
// if (
|
||||||
disableVersionCheck: true,
|
// ws &&
|
||||||
timeout: 30_000,
|
// (ws.readyState === WebSocket.CONNECTING || ws.readyState === WebSocket.OPEN)
|
||||||
};
|
// ) {
|
||||||
window.masto = createClient(clientParams);
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
(async () => {
|
// const stream = await masto.v1.stream.streamUser();
|
||||||
// Request v2, fallback to v1 if fail
|
// console.log('STREAM START', { stream });
|
||||||
let info;
|
// ws = stream.ws;
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
|
|
||||||
let ws;
|
// const handleNewStatus = debounce((status) => {
|
||||||
async function startStream() {
|
// console.log('UPDATE', status);
|
||||||
if (
|
// if (document.visibilityState === 'hidden') return;
|
||||||
ws &&
|
|
||||||
(ws.readyState === WebSocket.CONNECTING || ws.readyState === WebSocket.OPEN)
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const stream = await masto.v1.stream.streamUser();
|
// const inHomeNew = states.homeNew.find((s) => s.id === status.id);
|
||||||
console.log('STREAM START', { stream });
|
// const inHome = status.id === states.homeLast?.id;
|
||||||
ws = stream.ws;
|
// 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) => {
|
// saveStatus(status, instance);
|
||||||
console.log('UPDATE', status);
|
// }, 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 inNotificationsNew = states.notificationsNew.find(
|
||||||
const inHome = status.id === states.homeLast?.id;
|
// (n) => n.id === notification.id,
|
||||||
if (!inHomeNew && !inHome) {
|
// );
|
||||||
if (states.settings.boostsCarousel && status.reblog) {
|
// const inNotifications = notification.id === states.notificationsLast?.id;
|
||||||
// do nothing
|
// if (!inNotificationsNew && !inNotifications) {
|
||||||
} else {
|
// states.notificationsNew.unshift(notification);
|
||||||
states.homeNew.unshift({
|
// }
|
||||||
id: status.id,
|
|
||||||
reblog: status.reblog?.id,
|
|
||||||
reply: !!status.inReplyToAccountId,
|
|
||||||
});
|
|
||||||
console.log('homeNew 1', [...states.homeNew]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
saveStatus(status);
|
// saveStatus(notification.status, instance, { override: false });
|
||||||
}, 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);
|
|
||||||
|
|
||||||
const inNotificationsNew = states.notificationsNew.find(
|
// stream.ws.onclose = () => {
|
||||||
(n) => n.id === notification.id,
|
// console.log('STREAM CLOSED!');
|
||||||
);
|
// if (document.visibilityState !== 'hidden') {
|
||||||
const inNotifications = notification.id === states.notificationLast?.id;
|
// startStream();
|
||||||
if (!inNotificationsNew && !inNotifications) {
|
// }
|
||||||
states.notificationsNew.unshift(notification);
|
// };
|
||||||
}
|
|
||||||
|
|
||||||
saveStatus(notification.status, { override: false });
|
// return {
|
||||||
});
|
// stream,
|
||||||
|
// stopStream: () => {
|
||||||
|
// stream.ws.close();
|
||||||
|
// },
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
|
||||||
stream.ws.onclose = () => {
|
// let lastHidden;
|
||||||
console.log('STREAM CLOSED!');
|
// function startVisibility() {
|
||||||
if (document.visibilityState !== 'hidden') {
|
// const { masto, instance } = api();
|
||||||
startStream();
|
// 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 {
|
// const newStatuses = await fetchHome;
|
||||||
stream,
|
// const hasOneAndReblog =
|
||||||
stopStream: () => {
|
// newStatuses.length === 1 && newStatuses?.[0]?.reblog;
|
||||||
stream.ws.close();
|
// 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;
|
// const newNotifications = await fetchNotifications;
|
||||||
function startVisibility() {
|
// if (newNotifications.length) {
|
||||||
const handleVisible = (visible) => {
|
// const notification = newNotifications[0];
|
||||||
if (!visible) {
|
// const inNotificationsNew = states.notificationsNew.find(
|
||||||
const timestamp = Date.now();
|
// (n) => n.id === notification.id,
|
||||||
lastHidden = timestamp;
|
// );
|
||||||
} else {
|
// const inNotifications =
|
||||||
const timestamp = Date.now();
|
// notification.id === states.notificationsLast?.id;
|
||||||
const diff = timestamp - lastHidden;
|
// if (!inNotificationsNew && !inNotifications) {
|
||||||
const diffMins = Math.round(diff / 1000 / 60);
|
// states.notificationsNew.unshift(notification);
|
||||||
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 newStatuses = await fetchHome;
|
// saveStatus(notification.status, instance, { override: false });
|
||||||
const hasOneAndReblog =
|
// }
|
||||||
newStatuses.length === 1 && newStatuses?.[0]?.reblog;
|
// } catch (e) {
|
||||||
if (newStatuses.length) {
|
// // Silently fail
|
||||||
if (states.settings.boostsCarousel && hasOneAndReblog) {
|
// console.error(e);
|
||||||
// do nothing
|
// } finally {
|
||||||
} else {
|
// startStream();
|
||||||
states.homeNew = newStatuses.map((status) => {
|
// }
|
||||||
saveStatus(status);
|
// })();
|
||||||
return {
|
// }
|
||||||
id: status.id,
|
// }
|
||||||
reblog: status.reblog?.id,
|
// };
|
||||||
reply: !!status.inReplyToAccountId,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
console.log('homeNew 2', [...states.homeNew]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const newNotifications = await fetchNotifications;
|
// const handleVisibilityChange = () => {
|
||||||
if (newNotifications.length) {
|
// const hidden = document.visibilityState === 'hidden';
|
||||||
const notification = newNotifications[0];
|
// handleVisible(!hidden);
|
||||||
const inNotificationsNew = states.notificationsNew.find(
|
// console.log('VISIBILITY: ' + (hidden ? 'hidden' : 'visible'));
|
||||||
(n) => n.id === notification.id,
|
// };
|
||||||
);
|
// document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||||
const inNotifications =
|
// requestAnimationFrame(handleVisibilityChange);
|
||||||
notification.id === states.notificationLast?.id;
|
// return {
|
||||||
if (!inNotificationsNew && !inNotifications) {
|
// stop: () => {
|
||||||
states.notificationsNew.unshift(notification);
|
// document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||||
}
|
// },
|
||||||
|
// };
|
||||||
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);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export { App };
|
export { App };
|
||||||
|
|
12
src/components/AsyncText.jsx
Normal file
12
src/components/AsyncText.jsx
Normal file
|
@ -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;
|
21
src/components/MenuLink.jsx
Normal file
21
src/components/MenuLink.jsx
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import { FocusableItem } from '@szhsin/react-menu';
|
||||||
|
|
||||||
|
import Link from './link';
|
||||||
|
|
||||||
|
function MenuLink(props) {
|
||||||
|
return (
|
||||||
|
<FocusableItem>
|
||||||
|
{({ ref, closeMenu }) => (
|
||||||
|
<Link
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
onClick={({ detail }) =>
|
||||||
|
closeMenu(detail === 0 ? 'Enter' : undefined)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</FocusableItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MenuLink;
|
14
src/components/account-block.css
Normal file
14
src/components/account-block.css
Normal file
|
@ -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);
|
||||||
|
}
|
66
src/components/account-block.jsx
Normal file
66
src/components/account-block.jsx
Normal file
|
@ -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 (
|
||||||
|
<div class="account-block skeleton">
|
||||||
|
<Avatar size={avatarSize} />
|
||||||
|
<span>
|
||||||
|
<b>████████</b>
|
||||||
|
<br />
|
||||||
|
@██████
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { acct, avatar, avatarStatic, displayName, username, emojis, url } =
|
||||||
|
account;
|
||||||
|
const displayNameWithEmoji = emojifyText(displayName, emojis);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
class="account-block"
|
||||||
|
href={url}
|
||||||
|
target={external ? '_blank' : null}
|
||||||
|
title={`@${acct}`}
|
||||||
|
onClick={(e) => {
|
||||||
|
if (external) return;
|
||||||
|
e.preventDefault();
|
||||||
|
if (onClick) return onClick(e);
|
||||||
|
states.showAccount = {
|
||||||
|
account,
|
||||||
|
instance,
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Avatar url={avatar} size={avatarSize} />
|
||||||
|
<span>
|
||||||
|
{displayName ? (
|
||||||
|
<b
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: displayNameWithEmoji,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<b>{username}</b>
|
||||||
|
)}
|
||||||
|
<br />@{acct}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AccountBlock;
|
|
@ -2,19 +2,27 @@ import './account.css';
|
||||||
|
|
||||||
import { useEffect, useState } from 'preact/hooks';
|
import { useEffect, useState } from 'preact/hooks';
|
||||||
|
|
||||||
|
import { api } from '../utils/api';
|
||||||
import emojifyText from '../utils/emojify-text';
|
import emojifyText from '../utils/emojify-text';
|
||||||
import enhanceContent from '../utils/enhance-content';
|
import enhanceContent from '../utils/enhance-content';
|
||||||
import handleContentLinks from '../utils/handle-content-links';
|
import handleContentLinks from '../utils/handle-content-links';
|
||||||
import shortenNumber from '../utils/shorten-number';
|
import shortenNumber from '../utils/shorten-number';
|
||||||
import states from '../utils/states';
|
import states, { hideAllModals } from '../utils/states';
|
||||||
import store from '../utils/store';
|
import store from '../utils/store';
|
||||||
|
|
||||||
|
import AccountBlock from './account-block';
|
||||||
import Avatar from './avatar';
|
import Avatar from './avatar';
|
||||||
import Icon from './icon';
|
import Icon from './icon';
|
||||||
import Link from './link';
|
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 [uiState, setUIState] = useState('default');
|
||||||
const isString = typeof account === 'string';
|
const isString = typeof account === 'string';
|
||||||
const [info, setInfo] = useState(isString ? null : account);
|
const [info, setInfo] = useState(isString ? null : account);
|
||||||
|
@ -36,16 +44,18 @@ function Account({ account, onClose }) {
|
||||||
q: account,
|
q: account,
|
||||||
type: 'accounts',
|
type: 'accounts',
|
||||||
limit: 1,
|
limit: 1,
|
||||||
resolve: true,
|
resolve: authenticated,
|
||||||
});
|
});
|
||||||
if (result.accounts.length) {
|
if (result.accounts.length) {
|
||||||
setInfo(result.accounts[0]);
|
setInfo(result.accounts[0]);
|
||||||
setUIState('default');
|
setUIState('default');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
setInfo(null);
|
||||||
setUIState('error');
|
setUIState('error');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
setInfo(null);
|
||||||
setUIState('error');
|
setUIState('error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -84,17 +94,42 @@ function Account({ account, onClose }) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (info) {
|
if (info) {
|
||||||
const currentAccount = store.session.get('currentAccount');
|
const currentAccount = store.session.get('currentAccount');
|
||||||
if (currentAccount === id) {
|
let accountID;
|
||||||
|
(async () => {
|
||||||
|
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!
|
// It's myself!
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setRelationshipUIState('loading');
|
setRelationshipUIState('loading');
|
||||||
setFamiliarFollowers([]);
|
setFamiliarFollowers([]);
|
||||||
|
|
||||||
(async () => {
|
const fetchRelationships = currentMasto.v1.accounts.fetchRelationships([
|
||||||
const fetchRelationships = masto.v1.accounts.fetchRelationships([id]);
|
accountID,
|
||||||
|
]);
|
||||||
const fetchFamiliarFollowers =
|
const fetchFamiliarFollowers =
|
||||||
masto.v1.accounts.fetchFamiliarFollowers(id);
|
currentMasto.v1.accounts.fetchFamiliarFollowers(accountID);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const relationships = await fetchRelationships;
|
const relationships = await fetchRelationships;
|
||||||
|
@ -120,7 +155,7 @@ function Account({ account, onClose }) {
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
}, [info]);
|
}, [info, authenticated]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
following,
|
following,
|
||||||
|
@ -154,8 +189,7 @@ function Account({ account, onClose }) {
|
||||||
{uiState === 'loading' ? (
|
{uiState === 'loading' ? (
|
||||||
<>
|
<>
|
||||||
<header>
|
<header>
|
||||||
<Avatar size="xxxl" />
|
<AccountBlock avatarSize="xxxl" skeleton />
|
||||||
███ ████████████
|
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
<div class="note">
|
<div class="note">
|
||||||
|
@ -173,8 +207,12 @@ function Account({ account, onClose }) {
|
||||||
info && (
|
info && (
|
||||||
<>
|
<>
|
||||||
<header>
|
<header>
|
||||||
<Avatar url={avatar} size="xxxl" />
|
<AccountBlock
|
||||||
<NameText account={info} showAcct external />
|
account={info}
|
||||||
|
instance={instance}
|
||||||
|
avatarSize="xxxl"
|
||||||
|
external
|
||||||
|
/>
|
||||||
</header>
|
</header>
|
||||||
<main tabIndex="-1">
|
<main tabIndex="-1">
|
||||||
{bot && (
|
{bot && (
|
||||||
|
@ -186,7 +224,9 @@ function Account({ account, onClose }) {
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
class="note"
|
class="note"
|
||||||
onClick={handleContentLinks()}
|
onClick={handleContentLinks({
|
||||||
|
instance,
|
||||||
|
})}
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: enhanceContent(note, { emojis }),
|
__html: enhanceContent(note, { emojis }),
|
||||||
}}
|
}}
|
||||||
|
@ -218,7 +258,12 @@ function Account({ account, onClose }) {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<p class="stats">
|
<p class="stats">
|
||||||
<Link to={`/a/${id}`} onClick={onClose}>
|
<Link
|
||||||
|
to={instance ? `/${instance}/a/${id}` : `/a/${id}`}
|
||||||
|
onClick={() => {
|
||||||
|
hideAllModals();
|
||||||
|
}}
|
||||||
|
>
|
||||||
Posts
|
Posts
|
||||||
<br />
|
<br />
|
||||||
<b title={statusesCount}>
|
<b title={statusesCount}>
|
||||||
|
@ -265,7 +310,10 @@ function Account({ account, onClose }) {
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
states.showAccount = follower;
|
states.showAccount = {
|
||||||
|
account: follower,
|
||||||
|
instance,
|
||||||
|
};
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
|
@ -294,17 +342,16 @@ function Account({ account, onClose }) {
|
||||||
if (following || requested) {
|
if (following || requested) {
|
||||||
const yes = confirm(
|
const yes = confirm(
|
||||||
requested
|
requested
|
||||||
? 'Are you sure that you want to withdraw follow request?'
|
? 'Withdraw follow request?'
|
||||||
: 'Are you sure that you want to unfollow this account?',
|
: `Unfollow @${info.acct || info.username}?`,
|
||||||
);
|
);
|
||||||
if (yes) {
|
if (yes) {
|
||||||
newRelationship =
|
newRelationship =
|
||||||
await masto.v1.accounts.unfollow(id);
|
await currentMasto.v1.accounts.unfollow(id);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
newRelationship = await masto.v1.accounts.follow(
|
newRelationship =
|
||||||
id,
|
await currentMasto.v1.accounts.follow(id);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (newRelationship) setRelationship(newRelationship);
|
if (newRelationship) setRelationship(newRelationship);
|
||||||
setRelationshipUIState('default');
|
setRelationshipUIState('default');
|
||||||
|
|
46
src/components/columns.jsx
Normal file
46
src/components/columns.jsx
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
import { useSnapshot } from 'valtio';
|
||||||
|
|
||||||
|
import Bookmarks from '../pages/bookmarks';
|
||||||
|
import Favourites from '../pages/favourites';
|
||||||
|
import Following from '../pages/following';
|
||||||
|
import Hashtag from '../pages/hashtag';
|
||||||
|
import List from '../pages/list';
|
||||||
|
import Notifications from '../pages/notifications';
|
||||||
|
import Public from '../pages/public';
|
||||||
|
import states from '../utils/states';
|
||||||
|
import useTitle from '../utils/useTitle';
|
||||||
|
|
||||||
|
function Columns() {
|
||||||
|
useTitle('Home', '/');
|
||||||
|
const snapStates = useSnapshot(states);
|
||||||
|
const { shortcuts } = snapStates;
|
||||||
|
|
||||||
|
const components = shortcuts.map((shortcut) => {
|
||||||
|
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 <Component {...params} />;
|
||||||
|
});
|
||||||
|
|
||||||
|
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 <div id="columns">{components}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Columns;
|
|
@ -1,5 +1,5 @@
|
||||||
#compose-container {
|
#compose-container {
|
||||||
width: 40em;
|
width: var(--main-width);
|
||||||
max-width: 100vw;
|
max-width: 100vw;
|
||||||
align-self: stretch;
|
align-self: stretch;
|
||||||
animation: fade-in 0.2s ease-out;
|
animation: fade-in 0.2s ease-out;
|
||||||
|
@ -60,6 +60,11 @@
|
||||||
animation: appear-up 1s ease-in-out;
|
animation: appear-up 1s ease-in-out;
|
||||||
overflow: auto;
|
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 * {
|
#compose-container.standalone .status-preview * {
|
||||||
/*
|
/*
|
||||||
For standalone mode (new window), prevent interacting with the status preview for now
|
For standalone mode (new window), prevent interacting with the status preview for now
|
||||||
|
@ -164,7 +169,7 @@
|
||||||
left: -100vw !important;
|
left: -100vw !important;
|
||||||
}
|
}
|
||||||
#compose-container .toolbar-button select {
|
#compose-container .toolbar-button select {
|
||||||
background-color: transparent;
|
background-color: inherit;
|
||||||
border: 0;
|
border: 0;
|
||||||
padding: 0 0 0 8px;
|
padding: 0 0 0 8px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ import './compose.css';
|
||||||
import '@github/text-expander-element';
|
import '@github/text-expander-element';
|
||||||
import equal from 'fast-deep-equal';
|
import equal from 'fast-deep-equal';
|
||||||
import { forwardRef } from 'preact/compat';
|
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 { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import stringLength from 'string-length';
|
import stringLength from 'string-length';
|
||||||
import { uid } from 'uid/single';
|
import { uid } from 'uid/single';
|
||||||
|
@ -12,12 +12,18 @@ import { useSnapshot } from 'valtio';
|
||||||
|
|
||||||
import supportedLanguages from '../data/status-supported-languages';
|
import supportedLanguages from '../data/status-supported-languages';
|
||||||
import urlRegex from '../data/url-regex';
|
import urlRegex from '../data/url-regex';
|
||||||
|
import { api } from '../utils/api';
|
||||||
import db from '../utils/db';
|
import db from '../utils/db';
|
||||||
import emojifyText from '../utils/emojify-text';
|
import emojifyText from '../utils/emojify-text';
|
||||||
import openCompose from '../utils/open-compose';
|
import openCompose from '../utils/open-compose';
|
||||||
import states from '../utils/states';
|
import states, { saveStatus } from '../utils/states';
|
||||||
import store from '../utils/store';
|
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 useInterval from '../utils/useInterval';
|
||||||
import visibilityIconsMap from '../utils/visibility-icons-map';
|
import visibilityIconsMap from '../utils/visibility-icons-map';
|
||||||
|
|
||||||
|
@ -99,6 +105,7 @@ function Compose({
|
||||||
hasOpener,
|
hasOpener,
|
||||||
}) {
|
}) {
|
||||||
console.warn('RENDER COMPOSER');
|
console.warn('RENDER COMPOSER');
|
||||||
|
const { masto } = api();
|
||||||
const [uiState, setUIState] = useState('default');
|
const [uiState, setUIState] = useState('default');
|
||||||
const UID = useRef(draftStatus?.uid || uid());
|
const UID = useRef(draftStatus?.uid || uid());
|
||||||
console.log('Compose UID', UID.current);
|
console.log('Compose UID', UID.current);
|
||||||
|
@ -106,22 +113,8 @@ function Compose({
|
||||||
const currentAccount = getCurrentAccount();
|
const currentAccount = getCurrentAccount();
|
||||||
const currentAccountInfo = currentAccount.info;
|
const currentAccountInfo = currentAccount.info;
|
||||||
|
|
||||||
const configuration = useMemo(() => {
|
const { configuration } = getCurrentInstance();
|
||||||
try {
|
console.log('⚙️ Configuration', configuration);
|
||||||
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 {
|
const {
|
||||||
statuses: { maxCharacters, maxMediaAttachments, charactersReservedPerUrl },
|
statuses: { maxCharacters, maxMediaAttachments, charactersReservedPerUrl },
|
||||||
|
@ -146,6 +139,8 @@ function Compose({
|
||||||
const [mediaAttachments, setMediaAttachments] = useState([]);
|
const [mediaAttachments, setMediaAttachments] = useState([]);
|
||||||
const [poll, setPoll] = useState(null);
|
const [poll, setPoll] = useState(null);
|
||||||
|
|
||||||
|
const prefs = store.account.get('preferences') || {};
|
||||||
|
|
||||||
const customEmojis = useRef();
|
const customEmojis = useRef();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
|
@ -192,7 +187,7 @@ function Compose({
|
||||||
}
|
}
|
||||||
focusTextarea();
|
focusTextarea();
|
||||||
setVisibility(visibility);
|
setVisibility(visibility);
|
||||||
setLanguage(language || DEFAULT_LANG);
|
setLanguage(language || prefs.postingDefaultLanguage || DEFAULT_LANG);
|
||||||
setSensitive(sensitive);
|
setSensitive(sensitive);
|
||||||
}
|
}
|
||||||
if (draftStatus) {
|
if (draftStatus) {
|
||||||
|
@ -215,7 +210,7 @@ function Compose({
|
||||||
focusTextarea();
|
focusTextarea();
|
||||||
spoilerTextRef.current.value = spoilerText;
|
spoilerTextRef.current.value = spoilerText;
|
||||||
setVisibility(visibility);
|
setVisibility(visibility);
|
||||||
setLanguage(language || DEFAULT_LANG);
|
setLanguage(language || prefs.postingDefaultLanguage || DEFAULT_LANG);
|
||||||
setSensitive(sensitive);
|
setSensitive(sensitive);
|
||||||
setPoll(composablePoll);
|
setPoll(composablePoll);
|
||||||
setMediaAttachments(mediaAttachments);
|
setMediaAttachments(mediaAttachments);
|
||||||
|
@ -241,7 +236,7 @@ function Compose({
|
||||||
focusTextarea();
|
focusTextarea();
|
||||||
spoilerTextRef.current.value = spoilerText;
|
spoilerTextRef.current.value = spoilerText;
|
||||||
setVisibility(visibility);
|
setVisibility(visibility);
|
||||||
setLanguage(language || DEFAULT_LANG);
|
setLanguage(language || presf.postingDefaultLanguage || DEFAULT_LANG);
|
||||||
setSensitive(sensitive);
|
setSensitive(sensitive);
|
||||||
setPoll(composablePoll);
|
setPoll(composablePoll);
|
||||||
setMediaAttachments(mediaAttachments);
|
setMediaAttachments(mediaAttachments);
|
||||||
|
@ -254,13 +249,22 @@ function Compose({
|
||||||
})();
|
})();
|
||||||
} else {
|
} else {
|
||||||
focusTextarea();
|
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]);
|
}, [draftStatus, editStatus, replyToStatus]);
|
||||||
|
|
||||||
const formRef = useRef();
|
const formRef = useRef();
|
||||||
|
|
||||||
const beforeUnloadCopy =
|
const beforeUnloadCopy = 'You have unsaved changes. Discard this post?';
|
||||||
'You have unsaved changes. Are you sure you want to discard this post?';
|
|
||||||
const canClose = () => {
|
const canClose = () => {
|
||||||
const { value, dataset } = textareaRef.current;
|
const { value, dataset } = textareaRef.current;
|
||||||
|
|
||||||
|
@ -362,6 +366,8 @@ function Compose({
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enableOnFormTags: true,
|
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),
|
// mediaIds: mediaAttachments.map((attachment) => attachment.id),
|
||||||
media_ids: 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.visibility = visibility;
|
||||||
// params.inReplyToId = replyToStatus?.id || undefined;
|
// params.inReplyToId = replyToStatus?.id || undefined;
|
||||||
params.in_reply_to_id = replyToStatus?.id || undefined;
|
params.in_reply_to_id = replyToStatus?.id || undefined;
|
||||||
|
@ -771,6 +786,9 @@ function Compose({
|
||||||
editStatus.id,
|
editStatus.id,
|
||||||
params,
|
params,
|
||||||
);
|
);
|
||||||
|
saveStatus(newStatus, {
|
||||||
|
skipThreading: true,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
newStatus = await masto.v1.statuses.create(params, {
|
newStatus = await masto.v1.statuses.create(params, {
|
||||||
idempotencyKey: UID.current,
|
idempotencyKey: UID.current,
|
||||||
|
@ -799,6 +817,7 @@ function Compose({
|
||||||
disabled={uiState === 'loading'}
|
disabled={uiState === 'loading'}
|
||||||
class="spoiler-text-field"
|
class="spoiler-text-field"
|
||||||
lang={language}
|
lang={language}
|
||||||
|
spellCheck="true"
|
||||||
style={{
|
style={{
|
||||||
opacity: sensitive ? 1 : 0,
|
opacity: sensitive ? 1 : 0,
|
||||||
pointerEvents: sensitive ? 'auto' : 'none',
|
pointerEvents: sensitive ? 'auto' : 'none',
|
||||||
|
@ -868,6 +887,9 @@ function Compose({
|
||||||
updateCharCount();
|
updateCharCount();
|
||||||
}}
|
}}
|
||||||
maxCharacters={maxCharacters}
|
maxCharacters={maxCharacters}
|
||||||
|
performSearch={(params) => {
|
||||||
|
return masto.v2.search(params);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
{mediaAttachments.length > 0 && (
|
{mediaAttachments.length > 0 && (
|
||||||
<div class="media-attachments">
|
<div class="media-attachments">
|
||||||
|
@ -1031,7 +1053,7 @@ function Compose({
|
||||||
|
|
||||||
const Textarea = forwardRef((props, ref) => {
|
const Textarea = forwardRef((props, ref) => {
|
||||||
const [text, setText] = useState(ref.current?.value || '');
|
const [text, setText] = useState(ref.current?.value || '');
|
||||||
const { maxCharacters, ...textareaProps } = props;
|
const { maxCharacters, performSearch = () => {}, ...textareaProps } = props;
|
||||||
const snapStates = useSnapshot(states);
|
const snapStates = useSnapshot(states);
|
||||||
const charCount = snapStates.composerCharacterCount;
|
const charCount = snapStates.composerCharacterCount;
|
||||||
|
|
||||||
|
@ -1087,7 +1109,7 @@ const Textarea = forwardRef((props, ref) => {
|
||||||
}[key];
|
}[key];
|
||||||
provide(
|
provide(
|
||||||
new Promise((resolve) => {
|
new Promise((resolve) => {
|
||||||
const searchResults = masto.v2.search({
|
const searchResults = performSearch({
|
||||||
type,
|
type,
|
||||||
q: text,
|
q: text,
|
||||||
limit: 5,
|
limit: 5,
|
||||||
|
@ -1258,6 +1280,7 @@ function MediaAttachment({
|
||||||
onDescriptionChange = () => {},
|
onDescriptionChange = () => {},
|
||||||
onRemove = () => {},
|
onRemove = () => {},
|
||||||
}) {
|
}) {
|
||||||
|
const supportsEdit = supports('@mastodon/edit-media-attributes');
|
||||||
const { url, type, id } = attachment;
|
const { url, type, id } = attachment;
|
||||||
console.log({ attachment });
|
console.log({ attachment });
|
||||||
const [description, setDescription] = useState(attachment.description);
|
const [description, setDescription] = useState(attachment.description);
|
||||||
|
@ -1283,7 +1306,7 @@ function MediaAttachment({
|
||||||
|
|
||||||
const descTextarea = (
|
const descTextarea = (
|
||||||
<>
|
<>
|
||||||
{!!id ? (
|
{!!id && !supportsEdit ? (
|
||||||
<div class="media-desc">
|
<div class="media-desc">
|
||||||
<span class="tag">Uploaded</span>
|
<span class="tag">Uploaded</span>
|
||||||
<p title={description}>
|
<p title={description}>
|
||||||
|
@ -1413,6 +1436,7 @@ function Poll({
|
||||||
maxlength={maxCharactersPerOption}
|
maxlength={maxCharactersPerOption}
|
||||||
placeholder={`Choice ${i + 1}`}
|
placeholder={`Choice ${i + 1}`}
|
||||||
lang={lang}
|
lang={lang}
|
||||||
|
spellCheck="true"
|
||||||
onInput={(e) => {
|
onInput={(e) => {
|
||||||
const { value } = e.target;
|
const { value } = e.target;
|
||||||
options[i] = value;
|
options[i] = value;
|
||||||
|
|
|
@ -2,6 +2,7 @@ import './drafts.css';
|
||||||
|
|
||||||
import { useEffect, useMemo, useReducer, useState } from 'react';
|
import { useEffect, useMemo, useReducer, useState } from 'react';
|
||||||
|
|
||||||
|
import { api } from '../utils/api';
|
||||||
import db from '../utils/db';
|
import db from '../utils/db';
|
||||||
import states from '../utils/states';
|
import states from '../utils/states';
|
||||||
import { getCurrentAccountNS } from '../utils/store-utils';
|
import { getCurrentAccountNS } from '../utils/store-utils';
|
||||||
|
@ -10,6 +11,7 @@ import Icon from './icon';
|
||||||
import Loader from './loader';
|
import Loader from './loader';
|
||||||
|
|
||||||
function Drafts() {
|
function Drafts() {
|
||||||
|
const { masto } = api();
|
||||||
const [uiState, setUIState] = useState('default');
|
const [uiState, setUIState] = useState('default');
|
||||||
const [drafts, setDrafts] = useState([]);
|
const [drafts, setDrafts] = useState([]);
|
||||||
const [reloadCount, reload] = useReducer((c) => c + 1, 0);
|
const [reloadCount, reload] = useReducer((c) => c + 1, 0);
|
||||||
|
@ -101,9 +103,7 @@ function Drafts() {
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const yes = confirm(
|
const yes = confirm('Delete this draft?');
|
||||||
'Are you sure you want to delete this draft?',
|
|
||||||
);
|
|
||||||
if (yes) {
|
if (yes) {
|
||||||
await db.drafts.del(key);
|
await db.drafts.del(key);
|
||||||
reload();
|
reload();
|
||||||
|
@ -159,9 +159,7 @@ function Drafts() {
|
||||||
disabled={uiState === 'loading'}
|
disabled={uiState === 'loading'}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
const yes = confirm(
|
const yes = confirm('Delete all drafts?');
|
||||||
'Are you sure you want to delete all drafts?',
|
|
||||||
);
|
|
||||||
if (yes) {
|
if (yes) {
|
||||||
setUIState('loading');
|
setUIState('loading');
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -26,7 +26,7 @@ const ICONS = {
|
||||||
'eye-open': 'mingcute:eye-2-line',
|
'eye-open': 'mingcute:eye-2-line',
|
||||||
message: 'mingcute:mail-line',
|
message: 'mingcute:mail-line',
|
||||||
comment: 'mingcute:chat-3-line',
|
comment: 'mingcute:chat-3-line',
|
||||||
home: 'mingcute:home-5-line',
|
home: 'mingcute:home-3-line',
|
||||||
notification: 'mingcute:notification-line',
|
notification: 'mingcute:notification-line',
|
||||||
follow: 'mingcute:user-follow-line',
|
follow: 'mingcute:user-follow-line',
|
||||||
'follow-add': 'mingcute:user-add-line',
|
'follow-add': 'mingcute:user-add-line',
|
||||||
|
@ -48,6 +48,15 @@ const ICONS = {
|
||||||
thread: 'mingcute:route-line',
|
thread: 'mingcute:route-line',
|
||||||
group: 'mingcute:group-line',
|
group: 'mingcute:group-line',
|
||||||
bot: 'mingcute:android-2-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');
|
const modules = import.meta.glob('/node_modules/@iconify-icons/mingcute/*.js');
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { forwardRef } from 'preact/compat';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
import states from '../utils/states';
|
import states from '../utils/states';
|
||||||
|
@ -10,7 +11,7 @@ import states from '../utils/states';
|
||||||
3. Not using <Link state/> because it modifies history.state that *persists* across page reloads. I don't need that, so using valtio's states instead.
|
3. Not using <Link state/> 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;
|
let routerLocation;
|
||||||
try {
|
try {
|
||||||
routerLocation = useLocation();
|
routerLocation = useLocation();
|
||||||
|
@ -21,6 +22,7 @@ const Link = (props) => {
|
||||||
const isActive = hash === to;
|
const isActive = hash === to;
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
|
ref={ref}
|
||||||
href={`#${to}`}
|
href={`#${to}`}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
class={`${props.class || ''} ${isActive ? 'is-active' : ''}`}
|
class={`${props.class || ''} ${isActive ? 'is-active' : ''}`}
|
||||||
|
@ -30,6 +32,6 @@ const Link = (props) => {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
export default Link;
|
export default Link;
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { getBlurHashAverageColor } from 'fast-blurhash';
|
import { getBlurHashAverageColor } from 'fast-blurhash';
|
||||||
import { useEffect, useLayoutEffect, useRef, useState } from 'preact/hooks';
|
import { useEffect, useLayoutEffect, useRef, useState } from 'preact/hooks';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { useMatch } from 'react-router-dom';
|
|
||||||
|
|
||||||
import Icon from './icon';
|
import Icon from './icon';
|
||||||
import Link from './link';
|
import Link from './link';
|
||||||
|
@ -11,11 +10,11 @@ import Modal from './modal';
|
||||||
function MediaModal({
|
function MediaModal({
|
||||||
mediaAttachments,
|
mediaAttachments,
|
||||||
statusID,
|
statusID,
|
||||||
|
instance,
|
||||||
index = 0,
|
index = 0,
|
||||||
onClose = () => {},
|
onClose = () => {},
|
||||||
}) {
|
}) {
|
||||||
const carouselRef = useRef(null);
|
const carouselRef = useRef(null);
|
||||||
const isStatusLocation = useMatch('/s/:id');
|
|
||||||
|
|
||||||
const [currentIndex, setCurrentIndex] = useState(index);
|
const [currentIndex, setCurrentIndex] = useState(index);
|
||||||
const carouselFocusItem = useRef(null);
|
const carouselFocusItem = useRef(null);
|
||||||
|
@ -119,7 +118,7 @@ function MediaModal({
|
||||||
setShowMediaAlt(media.description);
|
setShowMediaAlt(media.description);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span class="tag">ALT</span>{' '}
|
<Icon icon="info" />
|
||||||
<span class="media-alt-desc">{media.description}</span>
|
<span class="media-alt-desc">{media.description}</span>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
@ -165,9 +164,8 @@ function MediaModal({
|
||||||
<span />
|
<span />
|
||||||
)}
|
)}
|
||||||
<span>
|
<span>
|
||||||
{!isStatusLocation && (
|
|
||||||
<Link
|
<Link
|
||||||
to={`/s/${statusID}`}
|
to={instance ? `/${instance}/s/${statusID}` : `/s/${statusID}`}
|
||||||
class="button carousel-button media-post-link plain3"
|
class="button carousel-button media-post-link plain3"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
// if small screen (not media query min-width 40em + 350px), run onClose
|
// if small screen (not media query min-width 40em + 350px), run onClose
|
||||||
|
@ -179,8 +177,7 @@ function MediaModal({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span class="button-label">See post </span>»
|
<span class="button-label">See post </span>»
|
||||||
</Link>
|
</Link>{' '}
|
||||||
)}{' '}
|
|
||||||
<a
|
<a
|
||||||
href={
|
href={
|
||||||
mediaAttachments[currentIndex]?.remoteUrl ||
|
mediaAttachments[currentIndex]?.remoteUrl ||
|
||||||
|
|
105
src/components/menu.jsx
Normal file
105
src/components/menu.jsx
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
import { Menu, MenuDivider, MenuItem } from '@szhsin/react-menu';
|
||||||
|
import { useSnapshot } from 'valtio';
|
||||||
|
|
||||||
|
import { api } from '../utils/api';
|
||||||
|
import states from '../utils/states';
|
||||||
|
|
||||||
|
import Icon from './icon';
|
||||||
|
import MenuLink from './MenuLink';
|
||||||
|
|
||||||
|
function NavMenu(props) {
|
||||||
|
const snapStates = useSnapshot(states);
|
||||||
|
const { instance, authenticated } = api();
|
||||||
|
|
||||||
|
// Home = Following
|
||||||
|
// But when in multi-column mode, Home becomes columns of anything
|
||||||
|
// User may choose pin or not to pin Following
|
||||||
|
// If user doesn't pin Following, we show it in the menu
|
||||||
|
const showFollowing =
|
||||||
|
snapStates.settings.shortcutsColumnsMode &&
|
||||||
|
!snapStates.shortcuts.find((pin) => pin.type === 'following');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu
|
||||||
|
portal={{
|
||||||
|
target: document.body,
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
overflow="auto"
|
||||||
|
viewScroll="close"
|
||||||
|
boundingBoxPadding="8 8 8 8"
|
||||||
|
menuButton={
|
||||||
|
<button type="button" class="button plain">
|
||||||
|
<Icon icon="menu" size="l" />
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<MenuLink to="/">
|
||||||
|
<Icon icon="home" size="l" /> <span>Home</span>
|
||||||
|
</MenuLink>
|
||||||
|
{authenticated && (
|
||||||
|
<>
|
||||||
|
{showFollowing && (
|
||||||
|
<MenuLink to="/following">
|
||||||
|
<Icon icon="following" size="l" /> <span>Following</span>
|
||||||
|
</MenuLink>
|
||||||
|
)}
|
||||||
|
<MenuLink to="/notifications">
|
||||||
|
<Icon icon="notification" size="l" /> <span>Notifications</span>
|
||||||
|
{snapStates.notificationsShowNew && (
|
||||||
|
<sup title="New" style={{ opacity: 0.5 }}>
|
||||||
|
{' '}
|
||||||
|
•
|
||||||
|
</sup>
|
||||||
|
)}
|
||||||
|
</MenuLink>
|
||||||
|
<MenuDivider />
|
||||||
|
<MenuLink to="/l">
|
||||||
|
<Icon icon="list" size="l" /> <span>Lists</span>
|
||||||
|
</MenuLink>
|
||||||
|
<MenuLink to="/ft">
|
||||||
|
<Icon icon="hashtag" size="l" /> <span>Followed Hashtags</span>
|
||||||
|
</MenuLink>
|
||||||
|
<MenuLink to="/b">
|
||||||
|
<Icon icon="bookmark" size="l" /> <span>Bookmarks</span>
|
||||||
|
</MenuLink>
|
||||||
|
<MenuLink to="/f">
|
||||||
|
<Icon icon="heart" size="l" /> <span>Favourites</span>
|
||||||
|
</MenuLink>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<MenuDivider />
|
||||||
|
<MenuLink to={`/search`}>
|
||||||
|
<Icon icon="search" size="l" /> <span>Search</span>
|
||||||
|
</MenuLink>
|
||||||
|
<MenuLink to={`/${instance}/p/l`}>
|
||||||
|
<Icon icon="group" size="l" /> <span>Local</span>
|
||||||
|
</MenuLink>
|
||||||
|
<MenuLink to={`/${instance}/p`}>
|
||||||
|
<Icon icon="earth" size="l" /> <span>Federated</span>
|
||||||
|
</MenuLink>
|
||||||
|
{authenticated && (
|
||||||
|
<>
|
||||||
|
<MenuDivider />
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
states.showShortcutsSettings = true;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon="shortcut" size="l" />{' '}
|
||||||
|
<span>Shortcuts Settings…</span>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
states.showSettings = true;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon="gear" size="l" /> <span>Settings…</span>
|
||||||
|
</MenuItem>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NavMenu;
|
|
@ -5,21 +5,31 @@ import states from '../utils/states';
|
||||||
|
|
||||||
import Avatar from './avatar';
|
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;
|
const { acct, avatar, avatarStatic, id, url, displayName, emojis } = account;
|
||||||
let { username } = account;
|
let { username } = account;
|
||||||
|
|
||||||
const displayNameWithEmoji = emojifyText(displayName, emojis);
|
const displayNameWithEmoji = emojifyText(displayName, emojis);
|
||||||
|
|
||||||
if (
|
const trimmedUsername = username.toLowerCase().trim();
|
||||||
!short &&
|
const trimmedDisplayName = (displayName || '').toLowerCase().trim();
|
||||||
username.toLowerCase().trim() ===
|
const shortenedDisplayName = trimmedDisplayName
|
||||||
(displayName || '')
|
|
||||||
.replace(/(\:(\w|\+|\-)+\:)(?=|[\!\.\?]|$)/g, '') // Remove shortcodes, regex from https://regex101.com/r/iE9uV0/1
|
.replace(/(\:(\w|\+|\-)+\:)(?=|[\!\.\?]|$)/g, '') // Remove shortcodes, regex from https://regex101.com/r/iE9uV0/1
|
||||||
.replace(/\s+/g, '') // E.g. "My name" === "myname"
|
.replace(/\s+/g, '') // E.g. "My name" === "myname"
|
||||||
.replace(/[^a-z0-9]/gi, '') // Remove non-alphanumeric characters
|
.replace(/[^a-z0-9]/gi, ''); // Remove non-alphanumeric characters
|
||||||
.toLowerCase()
|
|
||||||
.trim()
|
if (
|
||||||
|
!short &&
|
||||||
|
(trimmedUsername === trimmedDisplayName ||
|
||||||
|
trimmedUsername === shortenedDisplayName)
|
||||||
) {
|
) {
|
||||||
username = null;
|
username = null;
|
||||||
}
|
}
|
||||||
|
@ -34,7 +44,10 @@ function NameText({ account, showAvatar, showAcct, short, external, onClick }) {
|
||||||
if (external) return;
|
if (external) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (onClick) return onClick(e);
|
if (onClick) return onClick(e);
|
||||||
states.showAccount = account;
|
states.showAccount = {
|
||||||
|
account,
|
||||||
|
instance,
|
||||||
|
};
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{showAvatar && (
|
{showAvatar && (
|
||||||
|
|
73
src/components/shortcuts-settings.css
Normal file
73
src/components/shortcuts-settings.css
Normal file
|
@ -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;
|
||||||
|
}
|
387
src/components/shortcuts-settings.jsx
Normal file
387
src/components/shortcuts-settings.jsx
Normal file
|
@ -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 (
|
||||||
|
<div id="shortcuts-settings-container" class="sheet" tabindex="-1">
|
||||||
|
<header>
|
||||||
|
<h2>
|
||||||
|
<Icon icon="shortcut" /> Shortcuts{' '}
|
||||||
|
<sup
|
||||||
|
style={{
|
||||||
|
fontSize: 12,
|
||||||
|
opacity: 0.5,
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
beta
|
||||||
|
</sup>
|
||||||
|
</h2>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<p>
|
||||||
|
Specify a list of shortcuts that'll appear in the floating Shortcuts
|
||||||
|
button.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<details>
|
||||||
|
<summary class="insignificant">
|
||||||
|
Experimental Multi-column mode
|
||||||
|
</summary>
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={snapStates.settings.shortcutsColumnsMode}
|
||||||
|
onChange={(e) => {
|
||||||
|
states.settings.shortcutsColumnsMode = e.target.checked;
|
||||||
|
}}
|
||||||
|
/>{' '}
|
||||||
|
Show shortcuts in multiple columns instead of the floating button.
|
||||||
|
</label>
|
||||||
|
</details>
|
||||||
|
</p>
|
||||||
|
{shortcuts.length > 0 ? (
|
||||||
|
<ol class="shortcuts-list">
|
||||||
|
{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 (
|
||||||
|
<li key={key}>
|
||||||
|
<Icon icon={icon} />
|
||||||
|
<span class="shortcut-text">
|
||||||
|
<AsyncText>{title}</AsyncText>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="plain small"
|
||||||
|
disabled={i === 0}
|
||||||
|
onClick={() => {
|
||||||
|
const shortcutsArr = Array.from(states.shortcuts);
|
||||||
|
if (i > 0) {
|
||||||
|
const temp = states.shortcuts[i - 1];
|
||||||
|
shortcutsArr[i - 1] = shortcut;
|
||||||
|
shortcutsArr[i] = temp;
|
||||||
|
states.shortcuts = shortcutsArr;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon="arrow-up" alt="Move up" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="plain small"
|
||||||
|
disabled={i === shortcuts.length - 1}
|
||||||
|
onClick={() => {
|
||||||
|
const shortcutsArr = Array.from(states.shortcuts);
|
||||||
|
if (i < states.shortcuts.length - 1) {
|
||||||
|
const temp = states.shortcuts[i + 1];
|
||||||
|
shortcutsArr[i + 1] = shortcut;
|
||||||
|
shortcutsArr[i] = temp;
|
||||||
|
states.shortcuts = shortcutsArr;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon="arrow-down" alt="Move down" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="plain small"
|
||||||
|
onClick={() => {
|
||||||
|
states.shortcuts.splice(i, 1);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon="x" alt="Remove" />
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ol>
|
||||||
|
) : (
|
||||||
|
<p class="ui-state insignificant">
|
||||||
|
No shortcuts yet. Add one from the form below.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<hr />
|
||||||
|
<ShortcutForm
|
||||||
|
disabled={shortcuts.length >= SHORTCUTS_LIMIT}
|
||||||
|
lists={lists}
|
||||||
|
followedHashtags={followedHashtags}
|
||||||
|
onSubmit={(data) => {
|
||||||
|
console.log('onSubmit', data);
|
||||||
|
states.shortcuts.push(data);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ShortcutsSettings;
|
||||||
|
function ShortcutForm({ type, lists, followedHashtags, onSubmit, disabled }) {
|
||||||
|
const [currentType, setCurrentType] = useState(type);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => {
|
||||||
|
// 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);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<header>
|
||||||
|
<h3>Add a shortcut</h3>
|
||||||
|
<button type="submit" disabled={disabled}>
|
||||||
|
Add
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
<p>
|
||||||
|
<label>
|
||||||
|
<span>Timeline</span>
|
||||||
|
<select
|
||||||
|
required
|
||||||
|
disabled={disabled}
|
||||||
|
onChange={(e) => {
|
||||||
|
setCurrentType(e.target.value);
|
||||||
|
}}
|
||||||
|
name="type"
|
||||||
|
>
|
||||||
|
<option></option>
|
||||||
|
{TYPES.map((type) => (
|
||||||
|
<option value={type}>{TYPE_TEXT[type]}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</p>
|
||||||
|
{TYPE_PARAMS[currentType]?.map?.(
|
||||||
|
({ text, name, type, placeholder }) => {
|
||||||
|
if (currentType === 'list') {
|
||||||
|
return (
|
||||||
|
<p>
|
||||||
|
<label>
|
||||||
|
<span>List</span>
|
||||||
|
<select name="id" required disabled={disabled}>
|
||||||
|
{lists.map((list) => (
|
||||||
|
<option value={list.id}>{list.title}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<p>
|
||||||
|
<label>
|
||||||
|
<span>{text}</span>{' '}
|
||||||
|
<input
|
||||||
|
type={type}
|
||||||
|
name={name}
|
||||||
|
placeholder={placeholder}
|
||||||
|
required={type === 'text'}
|
||||||
|
disabled={disabled}
|
||||||
|
list={
|
||||||
|
currentType === 'hashtag'
|
||||||
|
? 'followed-hashtags-datalist'
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{currentType === 'hashtag' && followedHashtags.length > 0 && (
|
||||||
|
<datalist id="followed-hashtags-datalist">
|
||||||
|
{followedHashtags.map((tag) => (
|
||||||
|
<option value={tag.name} />
|
||||||
|
))}
|
||||||
|
</datalist>
|
||||||
|
)}
|
||||||
|
</label>
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
46
src/components/shortcuts.css
Normal file
46
src/components/shortcuts.css
Normal file
|
@ -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%);
|
||||||
|
}
|
||||||
|
}
|
109
src/components/shortcuts.jsx
Normal file
109
src/components/shortcuts.jsx
Normal file
|
@ -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 (
|
||||||
|
<div id="shortcuts">
|
||||||
|
<Menu
|
||||||
|
instanceRef={menuRef}
|
||||||
|
overflow="auto"
|
||||||
|
viewScroll="close"
|
||||||
|
boundingBoxPadding="8 8 8 8"
|
||||||
|
menuClassName="glass-menu shortcuts-menu"
|
||||||
|
offsetY={8}
|
||||||
|
position="anchor"
|
||||||
|
menuButton={
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
id="shortcuts-button"
|
||||||
|
class="plain"
|
||||||
|
onTransitionStart={(e) => {
|
||||||
|
// Close menu if the button disappears
|
||||||
|
try {
|
||||||
|
const { target } = e;
|
||||||
|
if (getComputedStyle(target).pointerEvents === 'none') {
|
||||||
|
menuRef.current?.closeMenu?.();
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon="shortcut" size="xl" alt="Shortcuts" />
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{formattedShortcuts.map(({ path, title, icon }, i) => {
|
||||||
|
return (
|
||||||
|
<MenuLink to={path} key={i + title} class="glass-menu-item">
|
||||||
|
<Icon icon={icon} size="l" />{' '}
|
||||||
|
<span class="menu-grow">
|
||||||
|
<AsyncText>{title}</AsyncText>
|
||||||
|
</span>
|
||||||
|
<span class="menu-shortcut hide-until-focus-visible">
|
||||||
|
{i + 1}
|
||||||
|
</span>
|
||||||
|
</MenuLink>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Shortcuts;
|
|
@ -286,6 +286,8 @@
|
||||||
.status .content p {
|
.status .content p {
|
||||||
/* 12px = 75% of 16px */
|
/* 12px = 75% of 16px */
|
||||||
margin-block: min(0.75em, 12px);
|
margin-block: min(0.75em, 12px);
|
||||||
|
white-space: pre-wrap;
|
||||||
|
tab-size: 2;
|
||||||
}
|
}
|
||||||
.status .content p:first-child {
|
.status .content p:first-child {
|
||||||
margin-block-start: 0;
|
margin-block-start: 0;
|
||||||
|
@ -422,6 +424,9 @@
|
||||||
.status .media img:hover {
|
.status .media img:hover {
|
||||||
animation: position-object 5s ease-in-out 1s 5;
|
animation: position-object 5s ease-in-out 1s 5;
|
||||||
}
|
}
|
||||||
|
body:has(#modal-container .carousel) .status .media img:hover {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
.status .media video {
|
.status .media video {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
@ -443,7 +448,7 @@
|
||||||
top: 50%;
|
top: 50%;
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
color: var(--text-insignificant-color);
|
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);
|
backdrop-filter: blur(6px) saturate(3) invert(0.2);
|
||||||
display: flex;
|
display: flex;
|
||||||
place-content: center;
|
place-content: center;
|
||||||
|
@ -532,23 +537,17 @@
|
||||||
text-align: left;
|
text-align: left;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
padding: 4px 8px 4px 4px;
|
padding: 4px 8px;
|
||||||
border: 1px solid var(--outline-color);
|
border: 1px solid var(--outline-color);
|
||||||
box-shadow: 0 4px 16px 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;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 4px;
|
gap: 8px;
|
||||||
font-size: 90%;
|
font-size: 90%;
|
||||||
}
|
}
|
||||||
.carousel-item button.media-alt .media-alt-desc {
|
.carousel-item button.media-alt .media-alt-desc {
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
@media (min-width: 40em) {
|
|
||||||
.carousel-item button.media-alt .media-alt-desc {
|
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
display: box;
|
display: box;
|
||||||
|
@ -556,7 +555,7 @@
|
||||||
box-orient: vertical;
|
box-orient: vertical;
|
||||||
-webkit-line-clamp: 2;
|
-webkit-line-clamp: 2;
|
||||||
line-clamp: 2;
|
line-clamp: 2;
|
||||||
}
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
.carousel-item button.media-alt[hidden] {
|
.carousel-item button.media-alt[hidden] {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
@ -897,6 +896,7 @@ a.card:is(:hover, :focus) {
|
||||||
transparent 160px
|
transparent 160px
|
||||||
);
|
);
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status .content p code {
|
.status .content p code {
|
||||||
|
@ -922,6 +922,9 @@ a.card:is(:hover, :focus) {
|
||||||
.status-badge .bookmark {
|
.status-badge .bookmark {
|
||||||
color: var(--link-color);
|
color: var(--link-color);
|
||||||
}
|
}
|
||||||
|
.status-badge .pin {
|
||||||
|
color: var(--red-color);
|
||||||
|
}
|
||||||
|
|
||||||
/* MISC */
|
/* MISC */
|
||||||
|
|
||||||
|
@ -949,7 +952,8 @@ a.card:is(:hover, :focus) {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#edit-history :is(ol, ol li) {
|
#edit-history ol,
|
||||||
|
#edit-history ol li {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
|
@ -1,17 +1,9 @@
|
||||||
import './status.css';
|
import './status.css';
|
||||||
|
|
||||||
import { Menu, MenuItem } from '@szhsin/react-menu';
|
import { Menu, MenuItem } from '@szhsin/react-menu';
|
||||||
import { getBlurHashAverageColor } from 'fast-blurhash';
|
|
||||||
import mem from 'mem';
|
import mem from 'mem';
|
||||||
import { memo } from 'preact/compat';
|
import { memo } from 'preact/compat';
|
||||||
import {
|
import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
|
||||||
useEffect,
|
|
||||||
useLayoutEffect,
|
|
||||||
useMemo,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from 'preact/hooks';
|
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
|
||||||
import 'swiped-events';
|
import 'swiped-events';
|
||||||
import useResizeObserver from 'use-resize-observer';
|
import useResizeObserver from 'use-resize-observer';
|
||||||
import { useSnapshot } from 'valtio';
|
import { useSnapshot } from 'valtio';
|
||||||
|
@ -19,11 +11,12 @@ import { useSnapshot } from 'valtio';
|
||||||
import Loader from '../components/loader';
|
import Loader from '../components/loader';
|
||||||
import Modal from '../components/modal';
|
import Modal from '../components/modal';
|
||||||
import NameText from '../components/name-text';
|
import NameText from '../components/name-text';
|
||||||
|
import { api } from '../utils/api';
|
||||||
import enhanceContent from '../utils/enhance-content';
|
import enhanceContent from '../utils/enhance-content';
|
||||||
import handleContentLinks from '../utils/handle-content-links';
|
import handleContentLinks from '../utils/handle-content-links';
|
||||||
import htmlContentLength from '../utils/html-content-length';
|
import htmlContentLength from '../utils/html-content-length';
|
||||||
import shortenNumber from '../utils/shorten-number';
|
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 store from '../utils/store';
|
||||||
import visibilityIconsMap from '../utils/visibility-icons-map';
|
import visibilityIconsMap from '../utils/visibility-icons-map';
|
||||||
|
|
||||||
|
@ -33,7 +26,7 @@ import Link from './link';
|
||||||
import Media from './media';
|
import Media from './media';
|
||||||
import RelativeTime from './relative-time';
|
import RelativeTime from './relative-time';
|
||||||
|
|
||||||
function fetchAccount(id) {
|
function fetchAccount(id, masto) {
|
||||||
try {
|
try {
|
||||||
return masto.v1.accounts.fetch(id);
|
return masto.v1.accounts.fetch(id);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -45,30 +38,36 @@ const memFetchAccount = mem(fetchAccount);
|
||||||
function Status({
|
function Status({
|
||||||
statusID,
|
statusID,
|
||||||
status,
|
status,
|
||||||
|
instance: propInstance,
|
||||||
withinContext,
|
withinContext,
|
||||||
size = 'm',
|
size = 'm',
|
||||||
skeleton,
|
skeleton,
|
||||||
readOnly,
|
readOnly,
|
||||||
|
contentTextWeight,
|
||||||
}) {
|
}) {
|
||||||
if (skeleton) {
|
if (skeleton) {
|
||||||
return (
|
return (
|
||||||
<div class="status skeleton">
|
<div class="status skeleton">
|
||||||
<Avatar size="xxl" />
|
<Avatar size="xxl" />
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="meta">███ ████████████</div>
|
<div class="meta">███ ████████</div>
|
||||||
<div class="content-container">
|
<div class="content-container">
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<p>████ ████████████</p>
|
<p>████ ████████</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
const { masto, instance, authenticated } = api({ instance: propInstance });
|
||||||
|
const { instance: currentInstance } = api();
|
||||||
|
const sameInstance = instance === currentInstance;
|
||||||
|
|
||||||
|
const sKey = statusKey(statusID, instance);
|
||||||
const snapStates = useSnapshot(states);
|
const snapStates = useSnapshot(states);
|
||||||
if (!status) {
|
if (!status) {
|
||||||
status = snapStates.statuses[statusID];
|
status = snapStates.statuses[sKey] || snapStates.statuses[statusID];
|
||||||
}
|
}
|
||||||
if (!status) {
|
if (!status) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -110,7 +109,9 @@ function Status({
|
||||||
reblog,
|
reblog,
|
||||||
uri,
|
uri,
|
||||||
emojis,
|
emojis,
|
||||||
|
// Non-API props
|
||||||
_deleted,
|
_deleted,
|
||||||
|
_pinned,
|
||||||
} = status;
|
} = status;
|
||||||
|
|
||||||
console.debug('RENDER Status', id, status?.account.displayName);
|
console.debug('RENDER Status', id, status?.account.displayName);
|
||||||
|
@ -135,7 +136,7 @@ function Status({
|
||||||
if (account) {
|
if (account) {
|
||||||
setInReplyToAccount(account);
|
setInReplyToAccount(account);
|
||||||
} else {
|
} else {
|
||||||
memFetchAccount(inReplyToAccountId)
|
memFetchAccount(inReplyToAccountId, masto)
|
||||||
.then((account) => {
|
.then((account) => {
|
||||||
setInReplyToAccount(account);
|
setInReplyToAccount(account);
|
||||||
states.accounts[account.id] = account;
|
states.accounts[account.id] = account;
|
||||||
|
@ -157,9 +158,15 @@ function Status({
|
||||||
<div class="status-reblog" onMouseEnter={debugHover}>
|
<div class="status-reblog" onMouseEnter={debugHover}>
|
||||||
<div class="status-pre-meta">
|
<div class="status-pre-meta">
|
||||||
<Icon icon="rocket" size="l" />{' '}
|
<Icon icon="rocket" size="l" />{' '}
|
||||||
<NameText account={status.account} showAvatar /> boosted
|
<NameText account={status.account} instance={instance} showAvatar />{' '}
|
||||||
|
boosted
|
||||||
</div>
|
</div>
|
||||||
<Status status={reblog} size={size} />
|
<Status
|
||||||
|
status={reblog}
|
||||||
|
instance={instance}
|
||||||
|
size={size}
|
||||||
|
contentTextWeight={contentTextWeight}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -198,13 +205,15 @@ function Status({
|
||||||
|
|
||||||
const statusRef = useRef(null);
|
const statusRef = useRef(null);
|
||||||
|
|
||||||
|
const unauthInteractionErrorMessage = `Sorry, your current logged-in instance can't interact with this status from another instance.`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<article
|
<article
|
||||||
ref={statusRef}
|
ref={statusRef}
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
class={`status ${
|
class={`status ${
|
||||||
!withinContext && inReplyToAccount ? 'status-reply-to' : ''
|
!withinContext && inReplyToAccount ? 'status-reply-to' : ''
|
||||||
} visibility-${visibility} ${
|
} visibility-${visibility} ${_pinned ? 'status-pinned' : ''} ${
|
||||||
{
|
{
|
||||||
s: 'small',
|
s: 'small',
|
||||||
m: 'medium',
|
m: 'medium',
|
||||||
|
@ -218,6 +227,7 @@ function Status({
|
||||||
{reblogged && <Icon class="reblog" icon="rocket" size="s" />}
|
{reblogged && <Icon class="reblog" icon="rocket" size="s" />}
|
||||||
{favourited && <Icon class="favourite" icon="heart" size="s" />}
|
{favourited && <Icon class="favourite" icon="heart" size="s" />}
|
||||||
{bookmarked && <Icon class="bookmark" icon="bookmark" size="s" />}
|
{bookmarked && <Icon class="bookmark" icon="bookmark" size="s" />}
|
||||||
|
{_pinned && <Icon class="pin" icon="pin" size="s" />}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{size !== 's' && (
|
{size !== 's' && (
|
||||||
|
@ -229,7 +239,10 @@ function Status({
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
states.showAccount = status.account;
|
states.showAccount = {
|
||||||
|
account: status.account,
|
||||||
|
instance,
|
||||||
|
};
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Avatar url={avatarStatic} size="xxl" />
|
<Avatar url={avatarStatic} size="xxl" />
|
||||||
|
@ -240,6 +253,7 @@ function Status({
|
||||||
{/* <span> */}
|
{/* <span> */}
|
||||||
<NameText
|
<NameText
|
||||||
account={status.account}
|
account={status.account}
|
||||||
|
instance={instance}
|
||||||
showAvatar={size === 's'}
|
showAvatar={size === 's'}
|
||||||
showAcct={size === 'l'}
|
showAcct={size === 'l'}
|
||||||
/>
|
/>
|
||||||
|
@ -248,14 +262,17 @@ function Status({
|
||||||
{' '}
|
{' '}
|
||||||
<span class="ib">
|
<span class="ib">
|
||||||
<Icon icon="arrow-right" class="arrow" />{' '}
|
<Icon icon="arrow-right" class="arrow" />{' '}
|
||||||
<NameText account={inReplyToAccount} short />
|
<NameText account={inReplyToAccount} instance={instance} short />
|
||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
)} */}
|
)} */}
|
||||||
{/* </span> */}{' '}
|
{/* </span> */}{' '}
|
||||||
{size !== 'l' &&
|
{size !== 'l' &&
|
||||||
(uri ? (
|
(uri ? (
|
||||||
<Link to={`/s/${id}`} class="time">
|
<Link
|
||||||
|
to={instance ? `/${instance}/s/${id}` : `/s/${id}`}
|
||||||
|
class="time"
|
||||||
|
>
|
||||||
<Icon
|
<Icon
|
||||||
icon={visibilityIconsMap[visibility]}
|
icon={visibilityIconsMap[visibility]}
|
||||||
alt={visibility}
|
alt={visibility}
|
||||||
|
@ -277,12 +294,12 @@ function Status({
|
||||||
{!withinContext && (
|
{!withinContext && (
|
||||||
<>
|
<>
|
||||||
{inReplyToAccountId === status.account?.id ||
|
{inReplyToAccountId === status.account?.id ||
|
||||||
!!snapStates.statusThreadNumber[id] ? (
|
!!snapStates.statusThreadNumber[sKey] ? (
|
||||||
<div class="status-thread-badge">
|
<div class="status-thread-badge">
|
||||||
<Icon icon="thread" size="s" />
|
<Icon icon="thread" size="s" />
|
||||||
Thread
|
Thread
|
||||||
{snapStates.statusThreadNumber[id]
|
{snapStates.statusThreadNumber[sKey]
|
||||||
? ` ${snapStates.statusThreadNumber[id]}/X`
|
? ` ${snapStates.statusThreadNumber[sKey]}/X`
|
||||||
: ''}
|
: ''}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
@ -294,7 +311,11 @@ function Status({
|
||||||
})) && (
|
})) && (
|
||||||
<div class="status-reply-badge">
|
<div class="status-reply-badge">
|
||||||
<Icon icon="reply" />{' '}
|
<Icon icon="reply" />{' '}
|
||||||
<NameText account={inReplyToAccount} short />
|
<NameText
|
||||||
|
account={inReplyToAccount}
|
||||||
|
instance={instance}
|
||||||
|
short
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
@ -302,10 +323,10 @@ function Status({
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
class={`content-container ${
|
class={`content-container ${
|
||||||
sensitive || spoilerText ? 'has-spoiler' : ''
|
spoilerText || sensitive ? 'has-spoiler' : ''
|
||||||
} ${showSpoiler ? 'show-spoiler' : ''}`}
|
} ${showSpoiler ? 'show-spoiler' : ''}`}
|
||||||
style={
|
style={
|
||||||
size === 'l' && {
|
(size === 'l' || contentTextWeight) && {
|
||||||
'--content-text-weight':
|
'--content-text-weight':
|
||||||
Math.round(
|
Math.round(
|
||||||
(spoilerText.length + htmlContentLength(content)) / 140,
|
(spoilerText.length + htmlContentLength(content)) / 140,
|
||||||
|
@ -313,7 +334,7 @@ function Status({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{!!spoilerText && sensitive && (
|
{!!spoilerText && (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
class="content"
|
class="content"
|
||||||
|
@ -346,7 +367,7 @@ function Status({
|
||||||
lang={language}
|
lang={language}
|
||||||
ref={contentRef}
|
ref={contentRef}
|
||||||
data-read-more={readMoreText}
|
data-read-more={readMoreText}
|
||||||
onClick={handleContentLinks({ mentions })}
|
onClick={handleContentLinks({ mentions, instance })}
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: enhanceContent(content, {
|
__html: enhanceContent(content, {
|
||||||
emojis,
|
emojis,
|
||||||
|
@ -367,9 +388,27 @@ function Status({
|
||||||
<Poll
|
<Poll
|
||||||
lang={language}
|
lang={language}
|
||||||
poll={poll}
|
poll={poll}
|
||||||
readOnly={readOnly}
|
readOnly={readOnly || !sameInstance || !authenticated}
|
||||||
onUpdate={(newPoll) => {
|
onUpdate={(newPoll) => {
|
||||||
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 = {
|
states.showMediaModal = {
|
||||||
mediaAttachments,
|
mediaAttachments,
|
||||||
index: i,
|
index: i,
|
||||||
|
instance,
|
||||||
statusID: readOnly ? null : id,
|
statusID: readOnly ? null : id,
|
||||||
};
|
};
|
||||||
}}
|
}}
|
||||||
|
@ -477,6 +517,9 @@ function Status({
|
||||||
icon="comment"
|
icon="comment"
|
||||||
count={repliesCount}
|
count={repliesCount}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
if (!sameInstance || !authenticated) {
|
||||||
|
return alert(unauthInteractionErrorMessage);
|
||||||
|
}
|
||||||
states.showCompose = {
|
states.showCompose = {
|
||||||
replyToStatus: status,
|
replyToStatus: status,
|
||||||
};
|
};
|
||||||
|
@ -494,17 +537,18 @@ function Status({
|
||||||
icon="rocket"
|
icon="rocket"
|
||||||
count={reblogsCount}
|
count={reblogsCount}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
|
if (!sameInstance || !authenticated) {
|
||||||
|
return alert(unauthInteractionErrorMessage);
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
if (!reblogged) {
|
if (!reblogged) {
|
||||||
const yes = confirm(
|
const yes = confirm('Boost this post?');
|
||||||
'Are you sure that you want to boost this post?',
|
|
||||||
);
|
|
||||||
if (!yes) {
|
if (!yes) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Optimistic
|
// Optimistic
|
||||||
states.statuses[id] = {
|
states.statuses[sKey] = {
|
||||||
...status,
|
...status,
|
||||||
reblogged: !reblogged,
|
reblogged: !reblogged,
|
||||||
reblogsCount: reblogsCount + (reblogged ? -1 : 1),
|
reblogsCount: reblogsCount + (reblogged ? -1 : 1),
|
||||||
|
@ -513,15 +557,15 @@ function Status({
|
||||||
const newStatus = await masto.v1.statuses.unreblog(
|
const newStatus = await masto.v1.statuses.unreblog(
|
||||||
id,
|
id,
|
||||||
);
|
);
|
||||||
saveStatus(newStatus);
|
saveStatus(newStatus, instance);
|
||||||
} else {
|
} else {
|
||||||
const newStatus = await masto.v1.statuses.reblog(id);
|
const newStatus = await masto.v1.statuses.reblog(id);
|
||||||
saveStatus(newStatus);
|
saveStatus(newStatus, instance);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
// Revert optimistism
|
// Revert optimistism
|
||||||
states.statuses[id] = status;
|
states.statuses[sKey] = status;
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -536,9 +580,12 @@ function Status({
|
||||||
icon="heart"
|
icon="heart"
|
||||||
count={favouritesCount}
|
count={favouritesCount}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
|
if (!sameInstance || !authenticated) {
|
||||||
|
return alert(unauthInteractionErrorMessage);
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
// Optimistic
|
// Optimistic
|
||||||
states.statuses[statusID] = {
|
states.statuses[sKey] = {
|
||||||
...status,
|
...status,
|
||||||
favourited: !favourited,
|
favourited: !favourited,
|
||||||
favouritesCount:
|
favouritesCount:
|
||||||
|
@ -548,15 +595,15 @@ function Status({
|
||||||
const newStatus = await masto.v1.statuses.unfavourite(
|
const newStatus = await masto.v1.statuses.unfavourite(
|
||||||
id,
|
id,
|
||||||
);
|
);
|
||||||
saveStatus(newStatus);
|
saveStatus(newStatus, instance);
|
||||||
} else {
|
} else {
|
||||||
const newStatus = await masto.v1.statuses.favourite(id);
|
const newStatus = await masto.v1.statuses.favourite(id);
|
||||||
saveStatus(newStatus);
|
saveStatus(newStatus, instance);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
// Revert optimistism
|
// Revert optimistism
|
||||||
states.statuses[statusID] = status;
|
states.statuses[sKey] = status;
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -569,9 +616,12 @@ function Status({
|
||||||
class="bookmark-button"
|
class="bookmark-button"
|
||||||
icon="bookmark"
|
icon="bookmark"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
|
if (!sameInstance || !authenticated) {
|
||||||
|
return alert(unauthInteractionErrorMessage);
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
// Optimistic
|
// Optimistic
|
||||||
states.statuses[statusID] = {
|
states.statuses[sKey] = {
|
||||||
...status,
|
...status,
|
||||||
bookmarked: !bookmarked,
|
bookmarked: !bookmarked,
|
||||||
};
|
};
|
||||||
|
@ -579,15 +629,15 @@ function Status({
|
||||||
const newStatus = await masto.v1.statuses.unbookmark(
|
const newStatus = await masto.v1.statuses.unbookmark(
|
||||||
id,
|
id,
|
||||||
);
|
);
|
||||||
saveStatus(newStatus);
|
saveStatus(newStatus, instance);
|
||||||
} else {
|
} else {
|
||||||
const newStatus = await masto.v1.statuses.bookmark(id);
|
const newStatus = await masto.v1.statuses.bookmark(id);
|
||||||
saveStatus(newStatus);
|
saveStatus(newStatus, instance);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
// Revert optimistism
|
// Revert optimistism
|
||||||
states.statuses[statusID] = status;
|
states.statuses[sKey] = status;
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -615,7 +665,7 @@ function Status({
|
||||||
};
|
};
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Edit…
|
<Icon icon="pencil" /> <span>Edit…</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
</Menu>
|
</Menu>
|
||||||
|
@ -635,6 +685,10 @@ function Status({
|
||||||
>
|
>
|
||||||
<EditedAtModal
|
<EditedAtModal
|
||||||
statusID={showEdited}
|
statusID={showEdited}
|
||||||
|
instance={instance}
|
||||||
|
fetchStatusHistory={() => {
|
||||||
|
return masto.v1.statuses.listHistory(showEdited);
|
||||||
|
}}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setShowEdited(false);
|
setShowEdited(false);
|
||||||
statusRef.current?.focus();
|
statusRef.current?.focus();
|
||||||
|
@ -698,12 +752,7 @@ function Card({ card }) {
|
||||||
</div>
|
</div>
|
||||||
<div class="meta-container">
|
<div class="meta-container">
|
||||||
<p class="meta domain">{domain}</p>
|
<p class="meta domain">{domain}</p>
|
||||||
<p
|
<p class="title">{title}</p>
|
||||||
class="title"
|
|
||||||
dangerouslySetInnerHTML={{
|
|
||||||
__html: title,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<p class="meta">{description || providerName || authorName}</p>
|
<p class="meta">{description || providerName || authorName}</p>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
@ -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 [uiState, setUIState] = useState('default');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
@ -768,12 +823,7 @@ function Poll({ poll, lang, readOnly, onUpdate = () => {} }) {
|
||||||
timeout = setTimeout(() => {
|
timeout = setTimeout(() => {
|
||||||
setUIState('loading');
|
setUIState('loading');
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
await refresh();
|
||||||
const pollResponse = await masto.v1.polls.fetch(id);
|
|
||||||
onUpdate(pollResponse);
|
|
||||||
} catch (e) {
|
|
||||||
// Silent fail
|
|
||||||
}
|
|
||||||
setUIState('default');
|
setUIState('default');
|
||||||
})();
|
})();
|
||||||
}, ms);
|
}, ms);
|
||||||
|
@ -847,19 +897,14 @@ function Poll({ poll, lang, readOnly, onUpdate = () => {} }) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const form = e.target;
|
const form = e.target;
|
||||||
const formData = new FormData(form);
|
const formData = new FormData(form);
|
||||||
const votes = [];
|
const choices = [];
|
||||||
formData.forEach((value, key) => {
|
formData.forEach((value, key) => {
|
||||||
if (key === 'poll') {
|
if (key === 'poll') {
|
||||||
votes.push(value);
|
choices.push(value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
console.log(votes);
|
|
||||||
setUIState('loading');
|
setUIState('loading');
|
||||||
const pollResponse = await masto.v1.polls.vote(id, {
|
await votePoll(choices);
|
||||||
choices: votes,
|
|
||||||
});
|
|
||||||
console.log(pollResponse);
|
|
||||||
onUpdate(pollResponse);
|
|
||||||
setUIState('default');
|
setUIState('default');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -903,12 +948,7 @@ function Poll({ poll, lang, readOnly, onUpdate = () => {} }) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setUIState('loading');
|
setUIState('loading');
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
await refresh();
|
||||||
const pollResponse = await masto.v1.polls.fetch(id);
|
|
||||||
onUpdate(pollResponse);
|
|
||||||
} catch (e) {
|
|
||||||
// Silent fail
|
|
||||||
}
|
|
||||||
setUIState('default');
|
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 [uiState, setUIState] = useState('default');
|
||||||
const [editHistory, setEditHistory] = useState([]);
|
const [editHistory, setEditHistory] = useState([]);
|
||||||
|
|
||||||
|
@ -945,7 +990,7 @@ function EditedAtModal({ statusID, onClose = () => {} }) {
|
||||||
setUIState('loading');
|
setUIState('loading');
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const editHistory = await masto.v1.statuses.listHistory(statusID);
|
const editHistory = await fetchStatusHistory();
|
||||||
console.log(editHistory);
|
console.log(editHistory);
|
||||||
setEditHistory(editHistory);
|
setEditHistory(editHistory);
|
||||||
setUIState('default');
|
setUIState('default');
|
||||||
|
@ -997,7 +1042,13 @@ function EditedAtModal({ statusID, onClose = () => {} }) {
|
||||||
}).format(createdAtDate)}
|
}).format(createdAtDate)}
|
||||||
</time>
|
</time>
|
||||||
</h3>
|
</h3>
|
||||||
<Status status={status} size="s" withinContext readOnly />
|
<Status
|
||||||
|
status={status}
|
||||||
|
instance={instance}
|
||||||
|
size="s"
|
||||||
|
withinContext
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
|
@ -1,39 +1,52 @@
|
||||||
import { useEffect, useRef, useState } from 'preact/hooks';
|
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 useScroll from '../utils/useScroll';
|
||||||
import useTitle from '../utils/useTitle';
|
|
||||||
|
|
||||||
import Icon from './icon';
|
import Icon from './icon';
|
||||||
import Link from './link';
|
import Link from './link';
|
||||||
import Loader from './loader';
|
import Loader from './loader';
|
||||||
|
import Menu from './menu';
|
||||||
import Status from './status';
|
import Status from './status';
|
||||||
|
|
||||||
function Timeline({
|
function Timeline({
|
||||||
title,
|
title,
|
||||||
titleComponent,
|
titleComponent,
|
||||||
path,
|
|
||||||
id,
|
id,
|
||||||
|
instance,
|
||||||
emptyText,
|
emptyText,
|
||||||
errorText,
|
errorText,
|
||||||
|
useItemID, // use statusID instead of status object, assuming it's already in states
|
||||||
|
boostsCarousel,
|
||||||
fetchItems = () => {},
|
fetchItems = () => {},
|
||||||
|
checkForUpdates = () => {},
|
||||||
|
checkForUpdatesInterval = 60_000, // 1 minute
|
||||||
|
headerStart,
|
||||||
|
headerEnd,
|
||||||
}) {
|
}) {
|
||||||
if (title) {
|
|
||||||
useTitle(title, path);
|
|
||||||
}
|
|
||||||
const [items, setItems] = useState([]);
|
const [items, setItems] = useState([]);
|
||||||
const [uiState, setUIState] = useState('default');
|
const [uiState, setUIState] = useState('default');
|
||||||
const [showMore, setShowMore] = useState(false);
|
const [showMore, setShowMore] = useState(false);
|
||||||
const scrollableRef = useRef(null);
|
const [showNew, setShowNew] = useState(false);
|
||||||
const { nearReachEnd, reachStart } = useScroll({
|
const [visible, setVisible] = useState(true);
|
||||||
scrollableElement: scrollableRef.current,
|
const scrollableRef = useRef();
|
||||||
});
|
|
||||||
|
|
||||||
const loadItems = (firstLoad) => {
|
const loadItems = useDebouncedCallback(
|
||||||
|
(firstLoad) => {
|
||||||
|
setShowNew(false);
|
||||||
|
if (uiState === 'loading') return;
|
||||||
setUIState('loading');
|
setUIState('loading');
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const { done, value } = await fetchItems(firstLoad);
|
let { done, value } = await fetchItems(firstLoad);
|
||||||
if (value?.length) {
|
if (value?.length) {
|
||||||
|
if (boostsCarousel) {
|
||||||
|
value = groupBoosts(value);
|
||||||
|
}
|
||||||
|
console.log(value);
|
||||||
if (firstLoad) {
|
if (firstLoad) {
|
||||||
setItems(value);
|
setItems(value);
|
||||||
} else {
|
} else {
|
||||||
|
@ -49,7 +62,113 @@ function Timeline({
|
||||||
setUIState('error');
|
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 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(() => {
|
useEffect(() => {
|
||||||
scrollableRef.current?.scrollTo({ top: 0 });
|
scrollableRef.current?.scrollTo({ top: 0 });
|
||||||
|
@ -63,69 +182,211 @@ function Timeline({
|
||||||
}, [reachStart]);
|
}, [reachStart]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (nearReachEnd && showMore) {
|
if (nearReachEnd || (reachEnd && showMore)) {
|
||||||
loadItems();
|
loadItems();
|
||||||
}
|
}
|
||||||
}, [nearReachEnd, showMore]);
|
}, [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 (
|
return (
|
||||||
<div
|
<div
|
||||||
id={`${id}-page`}
|
id={`${id}-page`}
|
||||||
class="deck-container"
|
class="deck-container"
|
||||||
ref={scrollableRef}
|
ref={(node) => {
|
||||||
|
scrollableRef.current = node;
|
||||||
|
jRef.current = node;
|
||||||
|
kRef.current = node;
|
||||||
|
oRef.current = node;
|
||||||
|
}}
|
||||||
tabIndex="-1"
|
tabIndex="-1"
|
||||||
>
|
>
|
||||||
<div class="timeline-deck deck">
|
<div class="timeline-deck deck">
|
||||||
<header
|
<header
|
||||||
|
hidden={hiddenUI}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
if (e.target === e.currentTarget) {
|
if (!e.target.closest('a, button')) {
|
||||||
scrollableRef.current?.scrollTo({
|
scrollableRef.current?.scrollTo({
|
||||||
top: 0,
|
top: 0,
|
||||||
behavior: 'smooth',
|
behavior: 'smooth',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
onDblClick={(e) => {
|
||||||
|
if (!e.target.closest('a, button')) {
|
||||||
|
loadItems(true);
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
|
<div class="header-grid">
|
||||||
<div class="header-side">
|
<div class="header-side">
|
||||||
|
<Menu />
|
||||||
|
{headerStart !== null && headerStart !== undefined ? (
|
||||||
|
headerStart
|
||||||
|
) : (
|
||||||
<Link to="/" class="button plain">
|
<Link to="/" class="button plain">
|
||||||
<Icon icon="home" size="l" />
|
<Icon icon="home" size="l" />
|
||||||
</Link>
|
</Link>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{title && (titleComponent ? titleComponent : <h1>{title}</h1>)}
|
{title && (titleComponent ? titleComponent : <h1>{title}</h1>)}
|
||||||
<div class="header-side">
|
<div class="header-side">
|
||||||
<Loader hidden={uiState !== 'loading'} />
|
<Loader hidden={uiState !== 'loading'} />
|
||||||
|
{!!headerEnd && headerEnd}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
{items.length > 0 &&
|
||||||
|
uiState !== 'loading' &&
|
||||||
|
!hiddenUI &&
|
||||||
|
showNew && (
|
||||||
|
<button
|
||||||
|
class="updates-button"
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
loadItems(true);
|
||||||
|
scrollableRef.current?.scrollTo({
|
||||||
|
top: 0,
|
||||||
|
behavior: 'smooth',
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon="arrow-up" /> New posts
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</header>
|
</header>
|
||||||
{!!items.length ? (
|
{!!items.length ? (
|
||||||
<>
|
<>
|
||||||
<ul class="timeline">
|
<ul class="timeline">
|
||||||
{items.map((status) => {
|
{items.map((status) => {
|
||||||
const { id: statusID, reblog } = status;
|
const { id: statusID, reblog, items, type } = status;
|
||||||
const actualStatusID = reblog?.id || statusID;
|
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 (
|
return (
|
||||||
<li key={`timeline-${statusID}`}>
|
<li key={`timeline-${statusID}`}>
|
||||||
<Link class="status-link" to={`/s/${actualStatusID}`}>
|
<StatusCarousel title={title} class={`${type}-carousel`}>
|
||||||
<Status status={status} />
|
{items.map((item) => {
|
||||||
|
const { id: statusID, reblog } = item;
|
||||||
|
const actualStatusID = reblog?.id || statusID;
|
||||||
|
const url = instance
|
||||||
|
? `/${instance}/s/${actualStatusID}`
|
||||||
|
: `/s/${actualStatusID}`;
|
||||||
|
return (
|
||||||
|
<li key={statusID}>
|
||||||
|
<Link
|
||||||
|
class="status-carousel-link timeline-item-alt"
|
||||||
|
to={url}
|
||||||
|
>
|
||||||
|
{useItemID ? (
|
||||||
|
<Status
|
||||||
|
statusID={statusID}
|
||||||
|
instance={instance}
|
||||||
|
size="s"
|
||||||
|
contentTextWeight
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Status
|
||||||
|
status={item}
|
||||||
|
instance={instance}
|
||||||
|
size="s"
|
||||||
|
contentTextWeight
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
</StatusCarousel>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<li key={`timeline-${statusID}`}>
|
||||||
|
<Link class="status-link timeline-item" to={url}>
|
||||||
|
{useItemID ? (
|
||||||
|
<Status statusID={statusID} instance={instance} />
|
||||||
|
) : (
|
||||||
|
<Status status={status} instance={instance} />
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{showMore && uiState === 'loading' && (
|
||||||
|
<>
|
||||||
|
<li
|
||||||
|
style={{
|
||||||
|
height: '20vh',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Status skeleton />
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
style={{
|
||||||
|
height: '25vh',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Status skeleton />
|
||||||
|
</li>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
{showMore && (
|
{uiState === 'default' &&
|
||||||
|
(showMore ? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="plain block"
|
class="plain block"
|
||||||
disabled={uiState === 'loading'}
|
|
||||||
onClick={() => loadItems()}
|
onClick={() => loadItems()}
|
||||||
style={{ marginBlockEnd: '6em' }}
|
style={{ marginBlockEnd: '6em' }}
|
||||||
>
|
>
|
||||||
{uiState === 'loading' ? (
|
Show more…
|
||||||
<Loader abrupt />
|
|
||||||
) : (
|
|
||||||
<>Show more…</>
|
|
||||||
)}
|
|
||||||
</button>
|
</button>
|
||||||
)}
|
) : (
|
||||||
|
<p class="ui-state insignificant">The end.</p>
|
||||||
|
))}
|
||||||
</>
|
</>
|
||||||
) : uiState === 'loading' ? (
|
) : uiState === 'loading' ? (
|
||||||
<ul class="timeline">
|
<ul class="timeline">
|
||||||
|
@ -136,9 +397,9 @@ function Timeline({
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
) : (
|
) : (
|
||||||
uiState !== 'loading' && <p class="ui-state">{emptyText}</p>
|
uiState !== 'error' && <p class="ui-state">{emptyText}</p>
|
||||||
)}
|
)}
|
||||||
{uiState === 'error' ? (
|
{uiState === 'error' && (
|
||||||
<p class="ui-state">
|
<p class="ui-state">
|
||||||
{errorText}
|
{errorText}
|
||||||
<br />
|
<br />
|
||||||
|
@ -150,14 +411,104 @@ function Timeline({
|
||||||
Try again
|
Try again
|
||||||
</button>
|
</button>
|
||||||
</p>
|
</p>
|
||||||
) : (
|
|
||||||
uiState !== 'loading' &&
|
|
||||||
!!items.length &&
|
|
||||||
!showMore && <p class="ui-state insignificant">The end.</p>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div class={`status-carousel ${className}`}>
|
||||||
|
<header>
|
||||||
|
<h3>{title}</h3>
|
||||||
|
<span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="small plain2"
|
||||||
|
disabled={reachStart}
|
||||||
|
onClick={() => {
|
||||||
|
carouselRef.current?.scrollBy({
|
||||||
|
left: -Math.min(320, carouselRef.current?.offsetWidth),
|
||||||
|
behavior: 'smooth',
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon="chevron-left" />
|
||||||
|
</button>{' '}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="small plain2"
|
||||||
|
disabled={reachEnd}
|
||||||
|
onClick={() => {
|
||||||
|
carouselRef.current?.scrollBy({
|
||||||
|
left: Math.min(320, carouselRef.current?.offsetWidth),
|
||||||
|
behavior: 'smooth',
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon="chevron-right" />
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</header>
|
||||||
|
<ul ref={carouselRef}>{children}</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default Timeline;
|
export default Timeline;
|
||||||
|
|
|
@ -2,36 +2,16 @@ import './index.css';
|
||||||
|
|
||||||
import './app.css';
|
import './app.css';
|
||||||
|
|
||||||
import { createClient } from 'masto';
|
|
||||||
import { render } from 'preact';
|
import { render } from 'preact';
|
||||||
import { useEffect, useState } from 'preact/hooks';
|
import { useEffect, useState } from 'preact/hooks';
|
||||||
|
|
||||||
import Compose from './components/compose';
|
import Compose from './components/compose';
|
||||||
import { getCurrentAccount } from './utils/store-utils';
|
|
||||||
import useTitle from './utils/useTitle';
|
import useTitle from './utils/useTitle';
|
||||||
|
|
||||||
if (window.opener) {
|
if (window.opener) {
|
||||||
console = window.opener.console;
|
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() {
|
function App() {
|
||||||
const [uiState, setUIState] = useState('default');
|
const [uiState, setUIState] = useState('default');
|
||||||
|
|
||||||
|
|
3
src/data/features.json
Normal file
3
src/data/features.json
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"@mastodon/edit-media-attributes": ">=4.1"
|
||||||
|
}
|
|
@ -1,386 +1,415 @@
|
||||||
[
|
[
|
||||||
"mastodon.social",
|
"mastodon.social",
|
||||||
|
"mstdn.jp",
|
||||||
"mstdn.social",
|
"mstdn.social",
|
||||||
"mastodon.world",
|
"mastodon.world",
|
||||||
"mas.to",
|
"mas.to",
|
||||||
"pawoo.net",
|
|
||||||
"mastodon.online",
|
"mastodon.online",
|
||||||
"infosec.exchange",
|
"infosec.exchange",
|
||||||
"mstdn.jp",
|
|
||||||
"mastodonapp.uk",
|
|
||||||
"hachyderm.io",
|
"hachyderm.io",
|
||||||
"techhub.social",
|
|
||||||
"fosstodon.org",
|
"fosstodon.org",
|
||||||
"universeodon.com",
|
|
||||||
"mastodon.lol",
|
"mastodon.lol",
|
||||||
"mastodon.sdf.org",
|
"techhub.social",
|
||||||
|
"mastodonapp.uk",
|
||||||
"troet.cafe",
|
"troet.cafe",
|
||||||
"mastodon.uno",
|
"pawoo.net",
|
||||||
"mastodon.nl",
|
|
||||||
"mstdn.party",
|
|
||||||
"masto.ai",
|
|
||||||
"mstdn.ca",
|
|
||||||
"home.social",
|
|
||||||
"c.im",
|
|
||||||
"kolektiva.social",
|
|
||||||
"m.cmx.im",
|
|
||||||
"sfba.social",
|
|
||||||
"fedibird.com",
|
"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",
|
"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",
|
"mastodon.scot",
|
||||||
|
"social.vivaldi.net",
|
||||||
|
"aus.social",
|
||||||
|
"det.social",
|
||||||
|
"norden.social",
|
||||||
|
"nrw.social",
|
||||||
|
"toot.community",
|
||||||
"mindly.social",
|
"mindly.social",
|
||||||
"ohai.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",
|
"alive.bar",
|
||||||
|
"mastodon.ie",
|
||||||
"social.tchncs.de",
|
"social.tchncs.de",
|
||||||
"mastodon.nu",
|
|
||||||
"social.cologne",
|
|
||||||
"mastouille.fr",
|
|
||||||
"o3o.ca",
|
|
||||||
"mathstodon.xyz",
|
|
||||||
"noagendasocial.com",
|
"noagendasocial.com",
|
||||||
"newsie.social",
|
"o3o.ca",
|
||||||
"sigmoid.social",
|
"mastodon.top",
|
||||||
"mastodon.com.tr",
|
"sueden.social",
|
||||||
"hessen.social",
|
"mastodon.au",
|
||||||
"muenchen.social",
|
|
||||||
"meow.social",
|
|
||||||
"masto.es",
|
|
||||||
"masto.nu",
|
|
||||||
"tech.lgbt",
|
|
||||||
"ruhr.social",
|
|
||||||
"mastodon.green",
|
|
||||||
"mstdn.plus",
|
|
||||||
"wxw.moe",
|
"wxw.moe",
|
||||||
"qoto.org",
|
"newsie.social",
|
||||||
"mamot.fr",
|
"mastodontech.de",
|
||||||
"tkz.one",
|
"mathstodon.xyz",
|
||||||
"dice.camp",
|
"loforo.com",
|
||||||
"social.anoxinon.de",
|
"ioc.exchange",
|
||||||
"mastodon.nz",
|
|
||||||
"twit.social",
|
"twit.social",
|
||||||
"ravenation.club",
|
"mamot.fr",
|
||||||
"planet.moe",
|
"meow.social",
|
||||||
"mstdn.science",
|
"dice.camp",
|
||||||
"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",
|
|
||||||
"nerdculture.de",
|
"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",
|
"octodon.social",
|
||||||
"union.place",
|
|
||||||
"mastodon-belgium.be",
|
|
||||||
"mastodon.radio",
|
|
||||||
"pol.social",
|
|
||||||
"rheinneckar.social",
|
|
||||||
"hometech.social",
|
|
||||||
"androiddev.social",
|
|
||||||
"social.librem.one",
|
"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",
|
"phpc.social",
|
||||||
"mast.lat",
|
"kinky.business",
|
||||||
"muenster.im",
|
"genomic.social",
|
||||||
"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",
|
|
||||||
"vocalodon.net",
|
"vocalodon.net",
|
||||||
"vis.social",
|
"qdon.space",
|
||||||
"yiff.life",
|
"androiddev.social",
|
||||||
"fur.lgbt",
|
"masto.pt",
|
||||||
"peoplemaking.games",
|
"digitalcourage.social",
|
||||||
"hcommons.social",
|
"theblower.au",
|
||||||
"mstdn.io",
|
"graphics.social",
|
||||||
"libretooth.gr",
|
"rollenspiel.social",
|
||||||
"m.sclo.nl",
|
"mastodonners.nl",
|
||||||
"pettingzoo.co",
|
"awscommunity.social",
|
||||||
"mastodon.zaclys.com",
|
"witches.live",
|
||||||
"equestria.social",
|
"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",
|
"best-friends.chat",
|
||||||
"ursal.zone",
|
"scholar.social",
|
||||||
"bitcoinhackers.org",
|
"social.lol",
|
||||||
"uiuxdev.social",
|
"cr8r.gg",
|
||||||
"queer.party",
|
"hci.social",
|
||||||
|
"kemonodon.club",
|
||||||
|
"toad.social",
|
||||||
|
"rubber.social",
|
||||||
|
"mastodonbooks.net",
|
||||||
|
"snabelen.no",
|
||||||
|
"astrodon.social",
|
||||||
|
"muenster.im",
|
||||||
|
"paquita.masto.host",
|
||||||
"mastodon.ml",
|
"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",
|
"aethy.com",
|
||||||
"abdl.link",
|
"mstdn.io",
|
||||||
"mastodon.com.py",
|
"ursal.zone",
|
||||||
"mapstodon.space",
|
"tooting.ch",
|
||||||
|
"queer.party",
|
||||||
|
"pewtix.com",
|
||||||
|
"mastodon.zaclys.com",
|
||||||
|
"historians.social",
|
||||||
"typo.social",
|
"typo.social",
|
||||||
"cryptodon.lol",
|
"pettingzoo.co",
|
||||||
|
"peoplemaking.games",
|
||||||
|
"abdl.link",
|
||||||
|
"climatejustice.rocks",
|
||||||
"tilde.zone",
|
"tilde.zone",
|
||||||
"computerfairi.es",
|
"wien.rocks",
|
||||||
"social.coop",
|
|
||||||
"mast.dragon-fly.club",
|
|
||||||
"dragon-fly.club",
|
|
||||||
"floss.social",
|
"floss.social",
|
||||||
"photog.social",
|
|
||||||
"bonn.social",
|
|
||||||
"sciencemastodon.com",
|
|
||||||
"mastodon.coffee",
|
|
||||||
"mastorol.es",
|
|
||||||
"federated.press",
|
|
||||||
"toot.funami.tech",
|
"toot.funami.tech",
|
||||||
"mastodon.gal",
|
"social.coop",
|
||||||
|
"lile.cl",
|
||||||
|
"openbiblio.social",
|
||||||
|
"twiukraine.com",
|
||||||
"tabletop.social",
|
"tabletop.social",
|
||||||
"shakedown.social",
|
"imastodon.net",
|
||||||
"dizl.de",
|
"bitcoinhackers.org",
|
||||||
"romancelandia.club",
|
"medibubble.org",
|
||||||
"oslo.town",
|
"disabled.social",
|
||||||
"graz.social",
|
"photog.social",
|
||||||
"sociale.network",
|
"macaw.social",
|
||||||
"todon.nl",
|
"mustard.blog",
|
||||||
|
"mstdn.maud.io",
|
||||||
"nofan.xyz",
|
"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",
|
"scicomm.xyz",
|
||||||
"layer8.space",
|
"layer8.space",
|
||||||
"artisan.chat",
|
"uiuxdev.social",
|
||||||
"freeradical.zone",
|
"veganism.social",
|
||||||
"toot.cat",
|
"oslo.town",
|
||||||
"fandom.ink",
|
"artsio.com",
|
||||||
"twiukraine.com",
|
|
||||||
"eupolicy.social",
|
|
||||||
"xarxa.cloud",
|
|
||||||
"bsd.network",
|
|
||||||
"weirder.earth",
|
|
||||||
"linuxrocks.online",
|
|
||||||
"mastodon.cat",
|
|
||||||
"girlcock.club",
|
|
||||||
"bolha.us",
|
|
||||||
"zeroes.ca",
|
|
||||||
"douchi.space",
|
|
||||||
"cybre.space",
|
"cybre.space",
|
||||||
"mastodon.la",
|
"freeradical.zone",
|
||||||
"sunny.garden",
|
"social.veraciousnetwork.com",
|
||||||
"bbq.snoot.com",
|
"douchi.space",
|
||||||
|
"mstdn.dk",
|
||||||
|
"federated.press",
|
||||||
|
"jorts.horse",
|
||||||
|
"girlcock.club",
|
||||||
|
"artisan.chat",
|
||||||
|
"bolha.us",
|
||||||
"liker.social",
|
"liker.social",
|
||||||
"vulpine.club",
|
"vulpine.club",
|
||||||
"imastodon.net",
|
"linuxrocks.online",
|
||||||
"mstdn.maud.io",
|
"eupolicy.social",
|
||||||
"freeatlantis.com",
|
"equestria.social",
|
||||||
"is.nota.live",
|
"graz.social",
|
||||||
"mastodon.org.uk",
|
"mastodo.fi",
|
||||||
"mastodon.arch-linux.cz",
|
"pnw.zone",
|
||||||
|
"dizl.de",
|
||||||
"mona.do",
|
"mona.do",
|
||||||
"tyrol.social",
|
"guitar.rodeo",
|
||||||
"mstdn.id",
|
"sociale.network",
|
||||||
"mastodon.uy",
|
"opalstack.social",
|
||||||
"mastodon.in.th",
|
"mas.town",
|
||||||
"kurry.social",
|
"mastodon.la",
|
||||||
"toot.cafe",
|
"arvr.social",
|
||||||
"shelter.moe",
|
"zeroes.ca",
|
||||||
"social.politicaconciencia.org",
|
"mastorol.es",
|
||||||
"h-net.social",
|
"ffxiv-mastodon.com",
|
||||||
"mstdn.mx",
|
"data-folks.masto.host",
|
||||||
"kopiti.am",
|
"witter.cz",
|
||||||
"mastodon.vlaanderen",
|
"romancelandia.club",
|
||||||
"mao.mastodonhub.com",
|
"freeatlantis.com",
|
||||||
"cloud-native.social",
|
"darmstadt.social",
|
||||||
"mograph.social",
|
"mastodon.cat",
|
||||||
"oc.todon.fr",
|
"mastodon.energy",
|
||||||
"ura-mstdn.com",
|
"computerfairi.es",
|
||||||
"uri.life",
|
"mastodon.org.uk",
|
||||||
"liberdon.com",
|
"xarxa.cloud",
|
||||||
"kinkyelephant.com",
|
"masto.nyc",
|
||||||
"nojack.easydns.ca",
|
"cryptodon.lol",
|
||||||
"mastodon.be",
|
"gametoots.de",
|
||||||
"podcastindex.social",
|
"sunny.garden",
|
||||||
"blacktwitter.io",
|
|
||||||
"awoo.space",
|
|
||||||
"woof.group",
|
|
||||||
"ani.work",
|
|
||||||
"colorid.es",
|
|
||||||
"seo.chat",
|
|
||||||
"mental.social",
|
|
||||||
"plural.cafe",
|
|
||||||
"ika.queloud.net",
|
"ika.queloud.net",
|
||||||
"mastodon.com.br",
|
"nederland.online",
|
||||||
"mstdn.tokyocameraclub.com",
|
"hometech.social",
|
||||||
"donphan.social",
|
"ura-mstdn.com",
|
||||||
"gensokyo.town",
|
"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",
|
"ichiji.social",
|
||||||
"sunbeam.city",
|
"mental.social",
|
||||||
"mstdn.kemono-friends.info",
|
"nnia.space",
|
||||||
"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",
|
|
||||||
"iztasocial.site",
|
"iztasocial.site",
|
||||||
"mastodon.pirateparty.be",
|
"nojack.easydns.ca",
|
||||||
"dingdash.com",
|
"arsenalfc.social",
|
||||||
"mastodon.partipirate.org",
|
"tyrol.social",
|
||||||
"oulipo.social",
|
"est.social",
|
||||||
"anticapitalist.party",
|
"kinkyelephant.com",
|
||||||
"kemonodon.club",
|
"mograph.social",
|
||||||
"toot.turbo.chat",
|
"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",
|
"photodn.net",
|
||||||
"otogamer.me",
|
"otogamer.me",
|
||||||
"bear.community",
|
"hello.2heng.xin",
|
||||||
|
"blabber.lu-rp.net",
|
||||||
"tablegame.mstdn.cloud",
|
"tablegame.mstdn.cloud",
|
||||||
"anarchism.space",
|
"elekk.xyz",
|
||||||
"ffxiv-mastodon.com",
|
"uwu.social",
|
||||||
"lgbt.io",
|
"hispagatos.space",
|
||||||
|
"mstdn.fr",
|
||||||
|
"dobbs.town",
|
||||||
|
"mastodon.gougere.fr",
|
||||||
|
"gameliberty.club",
|
||||||
|
"poweredbygay.social",
|
||||||
|
"dingdash.com",
|
||||||
|
"seo.chat",
|
||||||
|
"mastodon.cc",
|
||||||
|
"oulipo.social",
|
||||||
"lou.lt",
|
"lou.lt",
|
||||||
"social.chinwag.org",
|
"mastodon.partipirate.org",
|
||||||
"chinwag.org",
|
"gensokyo.social",
|
||||||
"aleph.land",
|
"anticapitalist.party",
|
||||||
"social.slat.org",
|
|
||||||
"mastodon.juggler.jp",
|
|
||||||
"eigadon.net",
|
|
||||||
"vocalounge.cafe",
|
|
||||||
"acg.mn",
|
|
||||||
"acg.debula.ml",
|
|
||||||
"eletusk.club",
|
"eletusk.club",
|
||||||
|
"mastodon.juggler.jp",
|
||||||
|
"social.slat.org",
|
||||||
|
"bear.community",
|
||||||
|
"mathtod.online",
|
||||||
|
"mastodon.pirateparty.be",
|
||||||
|
"toot.turbo.chat",
|
||||||
|
"lgbt.io",
|
||||||
|
"anarchism.space",
|
||||||
"otoya.space",
|
"otoya.space",
|
||||||
"social.coletivos.org",
|
"acg.mn",
|
||||||
"mastodon.cipherbliss.com",
|
"social.chinwag.org",
|
||||||
|
"eigadon.net",
|
||||||
|
"aleph.land",
|
||||||
|
"piano.masto.host",
|
||||||
|
"baraag.net",
|
||||||
|
"m.rthome.me",
|
||||||
"truthsocial.co.in",
|
"truthsocial.co.in",
|
||||||
"mstdn.osaka",
|
"mstdn.osaka",
|
||||||
|
"mastodol.jp",
|
||||||
|
"mastodon.cipherbliss.com",
|
||||||
"social.targaryen.house",
|
"social.targaryen.house",
|
||||||
|
"vocalounge.cafe",
|
||||||
"catdon.life",
|
"catdon.life",
|
||||||
|
"social.coletivos.org",
|
||||||
|
"mastoturk.org",
|
||||||
|
"mastodon.librelabucm.org",
|
||||||
"stereodon.social",
|
"stereodon.social",
|
||||||
"social.opendesktop.org",
|
"social.opendesktop.org",
|
||||||
"nasface.cz",
|
|
||||||
"toot.site",
|
|
||||||
"fetswing.org",
|
|
||||||
"vipgirlfriend.xxx",
|
|
||||||
"mastodon.elte.hu",
|
"mastodon.elte.hu",
|
||||||
|
"toot.site",
|
||||||
|
"vipgirlfriend.xxx",
|
||||||
|
"nasface.cz",
|
||||||
"bgme.me",
|
"bgme.me",
|
||||||
|
"fetswing.org",
|
||||||
"kinbaku.club",
|
"kinbaku.club",
|
||||||
"m.rthome.me",
|
|
||||||
"animalliberation.social",
|
"animalliberation.social",
|
||||||
"mastodon.librelabucm.org",
|
|
||||||
"mastodon.gza.jp",
|
|
||||||
"med-mammoth.com",
|
"med-mammoth.com",
|
||||||
|
"mastodon.gza.jp",
|
||||||
"hearthtodon.com",
|
"hearthtodon.com",
|
||||||
"counter.social",
|
"counter.social",
|
||||||
"kfem.cat",
|
|
||||||
"pet123.club",
|
"pet123.club",
|
||||||
"beta.woof.group",
|
|
||||||
"explosion.party",
|
|
||||||
"id.cc",
|
"id.cc",
|
||||||
"freespeechextremist.com",
|
"freespeechextremist.com",
|
||||||
"cawfee.club",
|
"cawfee.club",
|
||||||
|
@ -394,6 +423,7 @@
|
||||||
"libranet.de",
|
"libranet.de",
|
||||||
"tea.codes",
|
"tea.codes",
|
||||||
"pixelfed.social",
|
"pixelfed.social",
|
||||||
|
"stop.voring.me",
|
||||||
"shitposter.club",
|
"shitposter.club",
|
||||||
"squeet.me",
|
"squeet.me",
|
||||||
"shared.graphics",
|
"shared.graphics",
|
||||||
|
@ -401,13 +431,16 @@
|
||||||
"pxlmo.com",
|
"pxlmo.com",
|
||||||
"pixel.tchncs.de",
|
"pixel.tchncs.de",
|
||||||
"love.alicecomplex.com",
|
"love.alicecomplex.com",
|
||||||
|
"mastodon.london",
|
||||||
"friendica.eskimo.com",
|
"friendica.eskimo.com",
|
||||||
"meatbag.app",
|
"meatbag.app",
|
||||||
"fediverse.bbad.com",
|
"fediverse.bbad.com",
|
||||||
"pix.toot.wales",
|
"pix.toot.wales",
|
||||||
"fgc.network",
|
"fgc.network",
|
||||||
"bookrastinating.com",
|
"bookrastinating.com",
|
||||||
|
"electricrequiem.com",
|
||||||
"pixey.org",
|
"pixey.org",
|
||||||
|
"mk.pupbrained.xyz",
|
||||||
"pixelfed.tokyo",
|
"pixelfed.tokyo",
|
||||||
"chudbuds.lol",
|
"chudbuds.lol",
|
||||||
"freeframe.masto.host",
|
"freeframe.masto.host",
|
||||||
|
@ -423,13 +456,16 @@
|
||||||
"pixelfed.de",
|
"pixelfed.de",
|
||||||
"metapixl.com",
|
"metapixl.com",
|
||||||
"venera.social",
|
"venera.social",
|
||||||
|
"pixelfed.fi",
|
||||||
"blob.cat",
|
"blob.cat",
|
||||||
"onevery.ignorelist.com",
|
"onevery.ignorelist.com",
|
||||||
"cliq.buzz",
|
"cliq.buzz",
|
||||||
"pxl.roflcopter.fr",
|
"pxl.roflcopter.fr",
|
||||||
"p.1069-3.com",
|
"p.1069-3.com",
|
||||||
"www2.patriot.online",
|
|
||||||
"gc2.jp",
|
"gc2.jp",
|
||||||
"soap.shitposter.club",
|
"an.eldritch.gift",
|
||||||
"www.mastodon.scot"
|
"bz.pawdev.me",
|
||||||
|
"ck.borgar.space",
|
||||||
|
"fedi.s1i.dev",
|
||||||
|
"kids.0px.io"
|
||||||
]
|
]
|
|
@ -919,6 +919,11 @@
|
||||||
"Sorani (Kurdish)",
|
"Sorani (Kurdish)",
|
||||||
"سۆرانی"
|
"سۆرانی"
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
"cnr",
|
||||||
|
"Montenegrin",
|
||||||
|
"crnogorski"
|
||||||
|
],
|
||||||
[
|
[
|
||||||
"jbo",
|
"jbo",
|
||||||
"Lojban",
|
"Lojban",
|
||||||
|
@ -949,6 +954,26 @@
|
||||||
"Scots",
|
"Scots",
|
||||||
"Scots"
|
"Scots"
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
"sma",
|
||||||
|
"Southern Sami",
|
||||||
|
"Åarjelsaemien Gïele"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"smj",
|
||||||
|
"Lule Sami",
|
||||||
|
"Julevsámegiella"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"szl",
|
||||||
|
"Silesian",
|
||||||
|
"ślůnsko godka"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"tai",
|
||||||
|
"Tai",
|
||||||
|
"ภาษาไท or ภาษาไต"
|
||||||
|
],
|
||||||
[
|
[
|
||||||
"tok",
|
"tok",
|
||||||
"Toki Pona",
|
"Toki Pona",
|
||||||
|
|
|
@ -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 {
|
:root {
|
||||||
|
--main-width: 40em;
|
||||||
text-size-adjust: none;
|
text-size-adjust: none;
|
||||||
--hairline-width: 1px;
|
--hairline-width: 1px;
|
||||||
|
|
||||||
|
@ -295,6 +299,13 @@ code {
|
||||||
color: var(--text-insignificant-color);
|
color: var(--text-insignificant-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hide-until-focus-visible {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
:has(:focus-visible) .hide-until-focus-visible {
|
||||||
|
display: initial;
|
||||||
|
}
|
||||||
|
|
||||||
/* KEYFRAMES */
|
/* KEYFRAMES */
|
||||||
|
|
||||||
@keyframes appear {
|
@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 {
|
@keyframes fade-in {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
|
||||||
import '@szhsin/react-menu/dist/core.css';
|
|
||||||
|
|
||||||
import { render } from 'preact';
|
import { render } from 'preact';
|
||||||
import { HashRouter } from 'react-router-dom';
|
import { HashRouter } from 'react-router-dom';
|
||||||
|
|
||||||
|
@ -18,10 +16,10 @@ render(
|
||||||
document.getElementById('app'),
|
document.getElementById('app'),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Clean up iconify localStorage
|
// Storage cleanup
|
||||||
// TODO: Remove this after few weeks?
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
try {
|
try {
|
||||||
|
// Clean up iconify localStorage
|
||||||
Object.keys(localStorage).forEach((key) => {
|
Object.keys(localStorage).forEach((key) => {
|
||||||
if (key.startsWith('iconify')) {
|
if (key.startsWith('iconify')) {
|
||||||
localStorage.removeItem(key);
|
localStorage.removeItem(key);
|
||||||
|
@ -32,5 +30,8 @@ setTimeout(() => {
|
||||||
sessionStorage.removeItem(key);
|
sessionStorage.removeItem(key);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Clean up old settings key
|
||||||
|
localStorage.removeItem('settings:boostsCarousel');
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}, 5000);
|
}, 5000);
|
||||||
|
|
|
@ -1,24 +1,64 @@
|
||||||
import { useEffect, useRef, useState } from 'preact/hooks';
|
import { useEffect, useRef, useState } from 'preact/hooks';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
|
import { useSnapshot } from 'valtio';
|
||||||
|
|
||||||
import Timeline from '../components/timeline';
|
import Timeline from '../components/timeline';
|
||||||
|
import { api } from '../utils/api';
|
||||||
|
import emojifyText from '../utils/emojify-text';
|
||||||
import states from '../utils/states';
|
import states from '../utils/states';
|
||||||
|
import useTitle from '../utils/useTitle';
|
||||||
|
|
||||||
const LIMIT = 20;
|
const LIMIT = 20;
|
||||||
|
|
||||||
function AccountStatuses() {
|
function AccountStatuses() {
|
||||||
const { id } = useParams();
|
const snapStates = useSnapshot(states);
|
||||||
|
const { id, ...params } = useParams();
|
||||||
|
const { masto, instance } = api({ instance: params.instance });
|
||||||
const accountStatusesIterator = useRef();
|
const accountStatusesIterator = useRef();
|
||||||
async function fetchAccountStatuses(firstLoad) {
|
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) {
|
if (firstLoad || !accountStatusesIterator.current) {
|
||||||
accountStatusesIterator.current = masto.v1.accounts.listStatuses(id, {
|
accountStatusesIterator.current = masto.v1.accounts.listStatuses(id, {
|
||||||
limit: LIMIT,
|
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({});
|
const [account, setAccount] = useState({});
|
||||||
|
useTitle(
|
||||||
|
`${account?.acct ? '@' + account.acct : 'Posts'}`,
|
||||||
|
'/:instance?/a/:id',
|
||||||
|
);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
|
@ -31,6 +71,8 @@ function AccountStatuses() {
|
||||||
})();
|
})();
|
||||||
}, [id]);
|
}, [id]);
|
||||||
|
|
||||||
|
const { displayName, acct, emojis } = account;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Timeline
|
<Timeline
|
||||||
key={id}
|
key={id}
|
||||||
|
@ -38,21 +80,29 @@ function AccountStatuses() {
|
||||||
titleComponent={
|
titleComponent={
|
||||||
<h1
|
<h1
|
||||||
class="header-account"
|
class="header-account"
|
||||||
onClick={() => {
|
// onClick={() => {
|
||||||
states.showAccount = account;
|
// states.showAccount = {
|
||||||
}}
|
// account,
|
||||||
|
// instance,
|
||||||
|
// };
|
||||||
|
// }}
|
||||||
>
|
>
|
||||||
{account?.displayName}
|
<b
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: emojifyText(displayName, emojis),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<div>
|
<div>
|
||||||
<span>@{account?.acct}</span>
|
<span>@{acct}</span>
|
||||||
</div>
|
</div>
|
||||||
</h1>
|
</h1>
|
||||||
}
|
}
|
||||||
path="/a/:id"
|
|
||||||
id="account_statuses"
|
id="account_statuses"
|
||||||
|
instance={instance}
|
||||||
emptyText="Nothing to see here yet."
|
emptyText="Nothing to see here yet."
|
||||||
errorText="Unable to load statuses"
|
errorText="Unable to load statuses"
|
||||||
fetchItems={fetchAccountStatuses}
|
fetchItems={fetchAccountStatuses}
|
||||||
|
boostsCarousel={snapStates.settings.boostsCarousel}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,14 @@
|
||||||
import { useRef } from 'preact/hooks';
|
import { useRef } from 'preact/hooks';
|
||||||
|
|
||||||
import Timeline from '../components/timeline';
|
import Timeline from '../components/timeline';
|
||||||
|
import { api } from '../utils/api';
|
||||||
|
import useTitle from '../utils/useTitle';
|
||||||
|
|
||||||
const LIMIT = 20;
|
const LIMIT = 20;
|
||||||
|
|
||||||
function Bookmarks() {
|
function Bookmarks() {
|
||||||
|
useTitle('Bookmarks', '/b');
|
||||||
|
const { masto, instance } = api();
|
||||||
const bookmarksIterator = useRef();
|
const bookmarksIterator = useRef();
|
||||||
async function fetchBookmarks(firstLoad) {
|
async function fetchBookmarks(firstLoad) {
|
||||||
if (firstLoad || !bookmarksIterator.current) {
|
if (firstLoad || !bookmarksIterator.current) {
|
||||||
|
@ -19,6 +23,7 @@ function Bookmarks() {
|
||||||
id="bookmarks"
|
id="bookmarks"
|
||||||
emptyText="No bookmarks yet. Go bookmark something!"
|
emptyText="No bookmarks yet. Go bookmark something!"
|
||||||
errorText="Unable to load bookmarks"
|
errorText="Unable to load bookmarks"
|
||||||
|
instance={instance}
|
||||||
fetchItems={fetchBookmarks}
|
fetchItems={fetchBookmarks}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,10 +1,14 @@
|
||||||
import { useRef } from 'preact/hooks';
|
import { useRef } from 'preact/hooks';
|
||||||
|
|
||||||
import Timeline from '../components/timeline';
|
import Timeline from '../components/timeline';
|
||||||
|
import { api } from '../utils/api';
|
||||||
|
import useTitle from '../utils/useTitle';
|
||||||
|
|
||||||
const LIMIT = 20;
|
const LIMIT = 20;
|
||||||
|
|
||||||
function Favourites() {
|
function Favourites() {
|
||||||
|
useTitle('Favourites', '/f');
|
||||||
|
const { masto, instance } = api();
|
||||||
const favouritesIterator = useRef();
|
const favouritesIterator = useRef();
|
||||||
async function fetchFavourites(firstLoad) {
|
async function fetchFavourites(firstLoad) {
|
||||||
if (firstLoad || !favouritesIterator.current) {
|
if (firstLoad || !favouritesIterator.current) {
|
||||||
|
@ -19,6 +23,7 @@ function Favourites() {
|
||||||
id="favourites"
|
id="favourites"
|
||||||
emptyText="No favourites yet. Go favourite something!"
|
emptyText="No favourites yet. Go favourite something!"
|
||||||
errorText="Unable to load favourites"
|
errorText="Unable to load favourites"
|
||||||
|
instance={instance}
|
||||||
fetchItems={fetchFavourites}
|
fetchItems={fetchFavourites}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
86
src/pages/followed-hashtags.jsx
Normal file
86
src/pages/followed-hashtags.jsx
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
import { useEffect, useState } from 'preact/hooks';
|
||||||
|
|
||||||
|
import Icon from '../components/icon';
|
||||||
|
import Link from '../components/link';
|
||||||
|
import Loader from '../components/loader';
|
||||||
|
import Menu from '../components/menu';
|
||||||
|
import { api } from '../utils/api';
|
||||||
|
import useTitle from '../utils/useTitle';
|
||||||
|
|
||||||
|
const LIMIT = 200;
|
||||||
|
|
||||||
|
function FollowedHashtags() {
|
||||||
|
const { masto, instance } = api();
|
||||||
|
useTitle(`Followed Hashtags`, `/ft`);
|
||||||
|
const [uiState, setUiState] = useState('default');
|
||||||
|
|
||||||
|
const [followedHashtags, setFollowedHashtags] = useState([]);
|
||||||
|
useEffect(() => {
|
||||||
|
setUiState('loading');
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const iterator = masto.v1.followedTags.list({
|
||||||
|
limit: LIMIT,
|
||||||
|
});
|
||||||
|
const tags = [];
|
||||||
|
do {
|
||||||
|
const { value, done } = await iterator.next();
|
||||||
|
if (done || value?.length === 0) break;
|
||||||
|
tags.push(...value);
|
||||||
|
} while (true);
|
||||||
|
console.log(tags);
|
||||||
|
setFollowedHashtags(tags);
|
||||||
|
setUiState('default');
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
setUiState('error');
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div id="followed-hashtags-page" class="deck-container">
|
||||||
|
<div class="timeline-deck deck">
|
||||||
|
<header>
|
||||||
|
<div class="header-grid">
|
||||||
|
<div class="header-side">
|
||||||
|
<Menu />
|
||||||
|
<Link to="/" class="button plain">
|
||||||
|
<Icon icon="home" size="l" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<h1>Followed Hashtags</h1>
|
||||||
|
<div class="header-side" />
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
{followedHashtags.length > 0 ? (
|
||||||
|
<ul class="link-list">
|
||||||
|
{followedHashtags.map((tag) => (
|
||||||
|
<li>
|
||||||
|
<Link
|
||||||
|
to={
|
||||||
|
instance ? `/${instance}/t/${tag.name}` : `/t/${tag.name}`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Icon icon="hashtag" /> <span>{tag.name}</span>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
) : uiState === 'loading' ? (
|
||||||
|
<p class="ui-state">
|
||||||
|
<Loader abrupt />
|
||||||
|
</p>
|
||||||
|
) : uiState === 'error' ? (
|
||||||
|
<p class="ui-state">Unable to load followed hashtags.</p>
|
||||||
|
) : (
|
||||||
|
<p class="ui-state">No hashtags followed yet.</p>
|
||||||
|
)}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FollowedHashtags;
|
126
src/pages/following.jsx
Normal file
126
src/pages/following.jsx
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
import { useEffect, useRef } from 'preact/hooks';
|
||||||
|
import { useSnapshot } from 'valtio';
|
||||||
|
|
||||||
|
import Timeline from '../components/timeline';
|
||||||
|
import { api } from '../utils/api';
|
||||||
|
import states from '../utils/states';
|
||||||
|
import { getStatus, saveStatus } from '../utils/states';
|
||||||
|
import useTitle from '../utils/useTitle';
|
||||||
|
|
||||||
|
const LIMIT = 20;
|
||||||
|
|
||||||
|
function Following({ title, path, id, ...props }) {
|
||||||
|
useTitle(title || 'Following', path || '/following');
|
||||||
|
const { masto, instance } = api();
|
||||||
|
const snapStates = useSnapshot(states);
|
||||||
|
const homeIterator = useRef();
|
||||||
|
const latestItem = useRef();
|
||||||
|
|
||||||
|
async function fetchHome(firstLoad) {
|
||||||
|
if (firstLoad || !homeIterator.current) {
|
||||||
|
homeIterator.current = masto.v1.timelines.listHome({ limit: LIMIT });
|
||||||
|
}
|
||||||
|
const results = await homeIterator.current.next();
|
||||||
|
const { value } = results;
|
||||||
|
if (value?.length) {
|
||||||
|
if (firstLoad) {
|
||||||
|
latestItem.current = value[0].id;
|
||||||
|
}
|
||||||
|
|
||||||
|
value.forEach((item) => {
|
||||||
|
saveStatus(item, instance);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ENFORCE sort by datetime (Latest first)
|
||||||
|
value.sort((a, b) => {
|
||||||
|
const aDate = new Date(a.createdAt);
|
||||||
|
const bDate = new Date(b.createdAt);
|
||||||
|
return bDate - aDate;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkForUpdates() {
|
||||||
|
try {
|
||||||
|
const results = await masto.v1.timelines
|
||||||
|
.listHome({
|
||||||
|
limit: 5,
|
||||||
|
since_id: latestItem.current,
|
||||||
|
})
|
||||||
|
.next();
|
||||||
|
const { value } = results;
|
||||||
|
console.log('checkForUpdates', latestItem.current, value);
|
||||||
|
if (value?.length && value.some((item) => !item.reblog)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ws = useRef();
|
||||||
|
const streamUser = async () => {
|
||||||
|
console.log('🎏 Start streaming user', ws.current);
|
||||||
|
if (
|
||||||
|
ws.current &&
|
||||||
|
(ws.current.readyState === WebSocket.CONNECTING ||
|
||||||
|
ws.current.readyState === WebSocket.OPEN)
|
||||||
|
) {
|
||||||
|
console.log('🎏 Streaming user already open');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const stream = await masto.v1.stream.streamUser();
|
||||||
|
ws.current = stream.ws;
|
||||||
|
ws.current.__id = Math.random();
|
||||||
|
console.log('🎏 Streaming user', ws.current);
|
||||||
|
|
||||||
|
stream.on('status.update', (status) => {
|
||||||
|
console.log(`🔄 Status ${status.id} updated`);
|
||||||
|
saveStatus(status, instance);
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.on('delete', (statusID) => {
|
||||||
|
console.log(`❌ Status ${statusID} deleted`);
|
||||||
|
// delete states.statuses[statusID];
|
||||||
|
const s = getStatus(statusID, instance);
|
||||||
|
if (s) s._deleted = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.ws.onclose = () => {
|
||||||
|
console.log('🎏 Streaming user closed');
|
||||||
|
};
|
||||||
|
|
||||||
|
return stream;
|
||||||
|
};
|
||||||
|
useEffect(() => {
|
||||||
|
let stream;
|
||||||
|
(async () => {
|
||||||
|
stream = await streamUser();
|
||||||
|
})();
|
||||||
|
return () => {
|
||||||
|
if (stream) {
|
||||||
|
stream.ws.close();
|
||||||
|
ws.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Timeline
|
||||||
|
title={title || 'Following'}
|
||||||
|
id={id || 'following'}
|
||||||
|
emptyText="Nothing to see here."
|
||||||
|
errorText="Unable to load posts."
|
||||||
|
instance={instance}
|
||||||
|
fetchItems={fetchHome}
|
||||||
|
checkForUpdates={checkForUpdates}
|
||||||
|
useItemID
|
||||||
|
boostsCarousel={snapStates.settings.boostsCarousel}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Following;
|
75
src/pages/hashtag.jsx
Normal file
75
src/pages/hashtag.jsx
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
import { useRef } from 'preact/hooks';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
import Timeline from '../components/timeline';
|
||||||
|
import { api } from '../utils/api';
|
||||||
|
import useTitle from '../utils/useTitle';
|
||||||
|
|
||||||
|
const LIMIT = 20;
|
||||||
|
|
||||||
|
function Hashtags(props) {
|
||||||
|
let { hashtag, ...params } = useParams();
|
||||||
|
if (props.hashtag) hashtag = props.hashtag;
|
||||||
|
const { masto, instance } = api({ instance: params.instance });
|
||||||
|
const title = instance ? `#${hashtag} on ${instance}` : `#${hashtag}`;
|
||||||
|
useTitle(title, `/:instance?/t/:hashtag`);
|
||||||
|
const latestItem = useRef();
|
||||||
|
|
||||||
|
const hashtagsIterator = useRef();
|
||||||
|
async function fetchHashtags(firstLoad) {
|
||||||
|
if (firstLoad || !hashtagsIterator.current) {
|
||||||
|
hashtagsIterator.current = masto.v1.timelines.listHashtag(hashtag, {
|
||||||
|
limit: LIMIT,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const results = await hashtagsIterator.current.next();
|
||||||
|
const { value } = results;
|
||||||
|
if (value?.length) {
|
||||||
|
if (firstLoad) {
|
||||||
|
latestItem.current = value[0].id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkForUpdates() {
|
||||||
|
try {
|
||||||
|
const results = await masto.v1.timelines
|
||||||
|
.listHashtag(hashtag, {
|
||||||
|
limit: 1,
|
||||||
|
since_id: latestItem.current,
|
||||||
|
})
|
||||||
|
.next();
|
||||||
|
const { value } = results;
|
||||||
|
if (value?.length) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Timeline
|
||||||
|
key={hashtag}
|
||||||
|
title={title}
|
||||||
|
titleComponent={
|
||||||
|
!!instance && (
|
||||||
|
<h1 class="header-account">
|
||||||
|
<b>#{hashtag}</b>
|
||||||
|
<div>{instance}</div>
|
||||||
|
</h1>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
id="hashtags"
|
||||||
|
instance={instance}
|
||||||
|
emptyText="No one has posted anything with this tag yet."
|
||||||
|
errorText="Unable to load posts with this tag"
|
||||||
|
fetchItems={fetchHashtags}
|
||||||
|
checkForUpdates={checkForUpdates}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Hashtags;
|
|
@ -1,32 +0,0 @@
|
||||||
import { useRef } from 'preact/hooks';
|
|
||||||
import { useParams } from 'react-router-dom';
|
|
||||||
|
|
||||||
import Timeline from '../components/timeline';
|
|
||||||
|
|
||||||
const LIMIT = 20;
|
|
||||||
|
|
||||||
function Hashtags() {
|
|
||||||
const { hashtag } = useParams();
|
|
||||||
const hashtagsIterator = useRef();
|
|
||||||
async function fetchHashtags(firstLoad) {
|
|
||||||
if (firstLoad || !hashtagsIterator.current) {
|
|
||||||
hashtagsIterator.current = masto.v1.timelines.listHashtag(hashtag, {
|
|
||||||
limit: LIMIT,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return await hashtagsIterator.current.next();
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Timeline
|
|
||||||
key={hashtag}
|
|
||||||
title={`#${hashtag}`}
|
|
||||||
id="hashtags"
|
|
||||||
emptyText="No one has posted anything with this tag yet."
|
|
||||||
errorText="Unable to load posts with this tag"
|
|
||||||
fetchItems={fetchHashtags}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Hashtags;
|
|
546
src/pages/home-v1.jsx
Normal file
546
src/pages/home-v1.jsx
Normal file
|
@ -0,0 +1,546 @@
|
||||||
|
import { memo } from 'preact/compat';
|
||||||
|
import { useEffect, useRef, useState } from 'preact/hooks';
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
import { useDebouncedCallback } from 'use-debounce';
|
||||||
|
import { useSnapshot } from 'valtio';
|
||||||
|
|
||||||
|
import Icon from '../components/icon';
|
||||||
|
import Link from '../components/link';
|
||||||
|
import Loader from '../components/loader';
|
||||||
|
import Status from '../components/status';
|
||||||
|
import { api } from '../utils/api';
|
||||||
|
import db from '../utils/db';
|
||||||
|
import states, { saveStatus } from '../utils/states';
|
||||||
|
import { getCurrentAccountNS } from '../utils/store-utils';
|
||||||
|
import useScroll from '../utils/useScroll';
|
||||||
|
import useTitle from '../utils/useTitle';
|
||||||
|
|
||||||
|
const LIMIT = 20;
|
||||||
|
|
||||||
|
function Home({ hidden }) {
|
||||||
|
useTitle('Home', '/');
|
||||||
|
const { masto, instance } = api();
|
||||||
|
const snapStates = useSnapshot(states);
|
||||||
|
const [uiState, setUIState] = useState('default');
|
||||||
|
const [showMore, setShowMore] = useState(false);
|
||||||
|
|
||||||
|
console.debug('RENDER Home');
|
||||||
|
|
||||||
|
const homeIterator = useRef();
|
||||||
|
async function fetchStatuses(firstLoad) {
|
||||||
|
if (firstLoad) {
|
||||||
|
// Reset iterator
|
||||||
|
homeIterator.current = masto.v1.timelines.listHome({
|
||||||
|
limit: LIMIT,
|
||||||
|
});
|
||||||
|
states.homeNew = [];
|
||||||
|
}
|
||||||
|
const allStatuses = await homeIterator.current.next();
|
||||||
|
if (allStatuses.value?.length) {
|
||||||
|
// ENFORCE sort by datetime (Latest first)
|
||||||
|
allStatuses.value.sort((a, b) => {
|
||||||
|
const aDate = new Date(a.createdAt);
|
||||||
|
const bDate = new Date(b.createdAt);
|
||||||
|
return bDate - aDate;
|
||||||
|
});
|
||||||
|
const homeValues = allStatuses.value.map((status) => {
|
||||||
|
saveStatus(status, instance);
|
||||||
|
return {
|
||||||
|
id: status.id,
|
||||||
|
reblog: status.reblog?.id,
|
||||||
|
reply: !!status.inReplyToAccountId,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// BOOSTS CAROUSEL
|
||||||
|
if (snapStates.settings.boostsCarousel) {
|
||||||
|
let specialHome = [];
|
||||||
|
let boostStash = [];
|
||||||
|
let serialBoosts = 0;
|
||||||
|
for (let i = 0; i < homeValues.length; i++) {
|
||||||
|
const status = homeValues[i];
|
||||||
|
if (status.reblog) {
|
||||||
|
boostStash.push(status);
|
||||||
|
serialBoosts++;
|
||||||
|
} else {
|
||||||
|
specialHome.push(status);
|
||||||
|
if (serialBoosts < 3) {
|
||||||
|
serialBoosts = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// if boostStash is more than quarter of homeValues
|
||||||
|
// or if there are 3 or more boosts in a row
|
||||||
|
if (boostStash.length > homeValues.length / 4 || serialBoosts >= 3) {
|
||||||
|
// if boostStash is more than 3 quarter of homeValues
|
||||||
|
const boostStashID = boostStash.map((status) => status.id);
|
||||||
|
if (boostStash.length > (homeValues.length * 3) / 4) {
|
||||||
|
// insert boost array at the end of specialHome list
|
||||||
|
specialHome = [
|
||||||
|
...specialHome,
|
||||||
|
{ id: boostStashID, boosts: boostStash },
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
// insert boosts array in the middle of specialHome list
|
||||||
|
const half = Math.floor(specialHome.length / 2);
|
||||||
|
specialHome = [
|
||||||
|
...specialHome.slice(0, half),
|
||||||
|
{
|
||||||
|
id: boostStashID,
|
||||||
|
boosts: boostStash,
|
||||||
|
},
|
||||||
|
...specialHome.slice(half),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Untouched, this is fine
|
||||||
|
specialHome = homeValues;
|
||||||
|
}
|
||||||
|
console.log({
|
||||||
|
specialHome,
|
||||||
|
});
|
||||||
|
if (firstLoad) {
|
||||||
|
states.homeLast = specialHome[0];
|
||||||
|
states.home = specialHome;
|
||||||
|
} else {
|
||||||
|
states.home.push(...specialHome);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (firstLoad) {
|
||||||
|
states.homeLast = homeValues[0];
|
||||||
|
states.home = homeValues;
|
||||||
|
} else {
|
||||||
|
states.home.push(...homeValues);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
states.homeLastFetchTime = Date.now();
|
||||||
|
return allStatuses;
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadStatuses = useDebouncedCallback(
|
||||||
|
(firstLoad) => {
|
||||||
|
if (uiState === 'loading') return;
|
||||||
|
setUIState('loading');
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const { done } = await fetchStatuses(firstLoad);
|
||||||
|
setShowMore(!done);
|
||||||
|
setUIState('default');
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(e);
|
||||||
|
setUIState('error');
|
||||||
|
} finally {
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
},
|
||||||
|
1500,
|
||||||
|
{
|
||||||
|
leading: true,
|
||||||
|
trailing: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadStatuses(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const scrollableRef = useRef();
|
||||||
|
|
||||||
|
const jRef = useHotkeys('j, shift+j', (_, handler) => {
|
||||||
|
// focus on next status after active status
|
||||||
|
// Traverses .timeline li .status-link, focus on .status-link
|
||||||
|
const activeStatus = document.activeElement.closest(
|
||||||
|
'.status-link, .status-boost-link',
|
||||||
|
);
|
||||||
|
const activeStatusRect = activeStatus?.getBoundingClientRect();
|
||||||
|
const allStatusLinks = Array.from(
|
||||||
|
scrollableRef.current.querySelectorAll(
|
||||||
|
'.status-link, .status-boost-link',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
activeStatus &&
|
||||||
|
activeStatusRect.top < scrollableRef.current.clientHeight &&
|
||||||
|
activeStatusRect.bottom > 0
|
||||||
|
) {
|
||||||
|
const activeStatusIndex = allStatusLinks.indexOf(activeStatus);
|
||||||
|
let nextStatus = allStatusLinks[activeStatusIndex + 1];
|
||||||
|
if (handler.shift) {
|
||||||
|
// get next status that's not .status-boost-link
|
||||||
|
nextStatus = allStatusLinks.find(
|
||||||
|
(statusLink, index) =>
|
||||||
|
index > activeStatusIndex &&
|
||||||
|
!statusLink.classList.contains('status-boost-link'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (nextStatus) {
|
||||||
|
nextStatus.focus();
|
||||||
|
nextStatus.scrollIntoViewIfNeeded?.();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If active status is not in viewport, get the topmost status-link in viewport
|
||||||
|
const topmostStatusLink = allStatusLinks.find((statusLink) => {
|
||||||
|
const statusLinkRect = statusLink.getBoundingClientRect();
|
||||||
|
return statusLinkRect.top >= 44 && statusLinkRect.left >= 0; // 44 is the magic number for header height, not real
|
||||||
|
});
|
||||||
|
if (topmostStatusLink) {
|
||||||
|
topmostStatusLink.focus();
|
||||||
|
topmostStatusLink.scrollIntoViewIfNeeded?.();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const kRef = useHotkeys('k, shift+k', (_, handler) => {
|
||||||
|
// focus on previous status after active status
|
||||||
|
// Traverses .timeline li .status-link, focus on .status-link
|
||||||
|
const activeStatus = document.activeElement.closest(
|
||||||
|
'.status-link, .status-boost-link',
|
||||||
|
);
|
||||||
|
const activeStatusRect = activeStatus?.getBoundingClientRect();
|
||||||
|
const allStatusLinks = Array.from(
|
||||||
|
scrollableRef.current.querySelectorAll(
|
||||||
|
'.status-link, .status-boost-link',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
activeStatus &&
|
||||||
|
activeStatusRect.top < scrollableRef.current.clientHeight &&
|
||||||
|
activeStatusRect.bottom > 0
|
||||||
|
) {
|
||||||
|
const activeStatusIndex = allStatusLinks.indexOf(activeStatus);
|
||||||
|
let prevStatus = allStatusLinks[activeStatusIndex - 1];
|
||||||
|
if (handler.shift) {
|
||||||
|
// get prev status that's not .status-boost-link
|
||||||
|
prevStatus = allStatusLinks.findLast(
|
||||||
|
(statusLink, index) =>
|
||||||
|
index < activeStatusIndex &&
|
||||||
|
!statusLink.classList.contains('status-boost-link'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (prevStatus) {
|
||||||
|
prevStatus.focus();
|
||||||
|
prevStatus.scrollIntoViewIfNeeded?.();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If active status is not in viewport, get the topmost status-link in viewport
|
||||||
|
const topmostStatusLink = allStatusLinks.find((statusLink) => {
|
||||||
|
const statusLinkRect = statusLink.getBoundingClientRect();
|
||||||
|
return statusLinkRect.top >= 44 && statusLinkRect.left >= 0; // 44 is the magic number for header height, not real
|
||||||
|
});
|
||||||
|
if (topmostStatusLink) {
|
||||||
|
topmostStatusLink.focus();
|
||||||
|
topmostStatusLink.scrollIntoViewIfNeeded?.();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const oRef = useHotkeys(['enter', 'o'], () => {
|
||||||
|
// open active status
|
||||||
|
const activeStatus = document.activeElement.closest(
|
||||||
|
'.status-link, .status-boost-link',
|
||||||
|
);
|
||||||
|
if (activeStatus) {
|
||||||
|
activeStatus.click();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
scrollDirection,
|
||||||
|
reachStart,
|
||||||
|
nearReachStart,
|
||||||
|
nearReachEnd,
|
||||||
|
reachEnd,
|
||||||
|
} = useScroll({
|
||||||
|
scrollableElement: scrollableRef.current,
|
||||||
|
distanceFromEnd: 3,
|
||||||
|
scrollThresholdStart: 44,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (nearReachEnd || (reachEnd && showMore)) {
|
||||||
|
loadStatuses();
|
||||||
|
}
|
||||||
|
}, [nearReachEnd, reachEnd]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (reachStart) {
|
||||||
|
loadStatuses(true);
|
||||||
|
}
|
||||||
|
}, [reachStart]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
const keys = await db.drafts.keys();
|
||||||
|
if (keys.length) {
|
||||||
|
const ns = getCurrentAccountNS();
|
||||||
|
const ownKeys = keys.filter((key) => key.startsWith(ns));
|
||||||
|
if (ownKeys.length) {
|
||||||
|
states.showDrafts = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// const showUpdatesButton = snapStates.homeNew.length > 0 && reachStart;
|
||||||
|
const [showUpdatesButton, setShowUpdatesButton] = useState(false);
|
||||||
|
useEffect(() => {
|
||||||
|
const isNewAndTop = snapStates.homeNew.length > 0 && reachStart;
|
||||||
|
console.log(
|
||||||
|
'isNewAndTop',
|
||||||
|
isNewAndTop,
|
||||||
|
snapStates.homeNew.length,
|
||||||
|
reachStart,
|
||||||
|
);
|
||||||
|
setShowUpdatesButton(isNewAndTop);
|
||||||
|
}, [snapStates.homeNew.length, reachStart]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
id="home-page"
|
||||||
|
class="deck-container"
|
||||||
|
hidden={hidden}
|
||||||
|
ref={(node) => {
|
||||||
|
scrollableRef.current = node;
|
||||||
|
jRef.current = node;
|
||||||
|
kRef.current = node;
|
||||||
|
oRef.current = node;
|
||||||
|
}}
|
||||||
|
tabIndex="-1"
|
||||||
|
>
|
||||||
|
<div class="timeline-deck deck">
|
||||||
|
<header
|
||||||
|
hidden={scrollDirection === 'end' && !nearReachStart}
|
||||||
|
onClick={() => {
|
||||||
|
scrollableRef.current?.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
|
}}
|
||||||
|
onDblClick={() => {
|
||||||
|
loadStatuses(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="header-grid">
|
||||||
|
<div class="header-side">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="plain"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
states.showSettings = true;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon="gear" size="l" alt="Settings" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<h1>Home</h1>
|
||||||
|
<div class="header-side">
|
||||||
|
<Loader hidden={uiState !== 'loading'} />{' '}
|
||||||
|
<Link
|
||||||
|
to="/notifications"
|
||||||
|
class={`button plain ${
|
||||||
|
snapStates.notificationsNew.length > 0 ? 'has-badge' : ''
|
||||||
|
}`}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon="notification" size="l" alt="Notifications" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{snapStates.homeNew.length > 0 &&
|
||||||
|
uiState !== 'loading' &&
|
||||||
|
((scrollDirection === 'start' &&
|
||||||
|
!nearReachStart &&
|
||||||
|
!nearReachEnd) ||
|
||||||
|
showUpdatesButton) && (
|
||||||
|
<button
|
||||||
|
class="updates-button"
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
if (!snapStates.settings.boostsCarousel) {
|
||||||
|
const uniqueHomeNew = snapStates.homeNew.filter(
|
||||||
|
(status) =>
|
||||||
|
!states.home.some((s) => s.id === status.id),
|
||||||
|
);
|
||||||
|
states.home.unshift(...uniqueHomeNew);
|
||||||
|
}
|
||||||
|
loadStatuses(true);
|
||||||
|
states.homeNew = [];
|
||||||
|
|
||||||
|
scrollableRef.current?.scrollTo({
|
||||||
|
top: 0,
|
||||||
|
behavior: 'smooth',
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon="arrow-up" /> New posts
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</header>
|
||||||
|
{snapStates.home.length ? (
|
||||||
|
<>
|
||||||
|
<ul class="timeline">
|
||||||
|
{snapStates.home.map(({ id: statusID, reblog, boosts }) => {
|
||||||
|
const actualStatusID = reblog || statusID;
|
||||||
|
if (boosts) {
|
||||||
|
return (
|
||||||
|
<li key={statusID}>
|
||||||
|
<BoostsCarousel boosts={boosts} />
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<li key={statusID}>
|
||||||
|
<Link class="status-link" to={`/s/${actualStatusID}`}>
|
||||||
|
<Status statusID={statusID} />
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{showMore && uiState === 'loading' && (
|
||||||
|
<>
|
||||||
|
<li
|
||||||
|
style={{
|
||||||
|
height: '20vh',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Status skeleton />
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
style={{
|
||||||
|
height: '25vh',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Status skeleton />
|
||||||
|
</li>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
{uiState === 'default' &&
|
||||||
|
(showMore ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="plain block"
|
||||||
|
onClick={() => loadStatuses()}
|
||||||
|
style={{ marginBlockEnd: '6em' }}
|
||||||
|
>
|
||||||
|
Show more…
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<p class="ui-state insignificant">The end.</p>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
) : uiState === 'loading' ? (
|
||||||
|
<ul class="timeline">
|
||||||
|
{Array.from({ length: 5 }).map((_, i) => (
|
||||||
|
<li key={i}>
|
||||||
|
<Status skeleton />
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
) : (
|
||||||
|
uiState !== 'error' && <p class="ui-state">Nothing to see here.</p>
|
||||||
|
)}
|
||||||
|
{uiState === 'error' && (
|
||||||
|
<p class="ui-state">
|
||||||
|
Unable to load statuses
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
loadStatuses(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Try again
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
hidden={scrollDirection === 'end' && !nearReachStart}
|
||||||
|
type="button"
|
||||||
|
id="compose-button"
|
||||||
|
onClick={(e) => {
|
||||||
|
if (e.shiftKey) {
|
||||||
|
const newWin = openCompose();
|
||||||
|
if (!newWin) {
|
||||||
|
alert('Looks like your browser is blocking popups.');
|
||||||
|
states.showCompose = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
states.showCompose = true;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon="quill" size="xxl" alt="Compose" />
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function BoostsCarousel({ boosts }) {
|
||||||
|
const carouselRef = useRef();
|
||||||
|
const { reachStart, reachEnd, init } = useScroll({
|
||||||
|
scrollableElement: carouselRef.current,
|
||||||
|
direction: 'horizontal',
|
||||||
|
});
|
||||||
|
useEffect(() => {
|
||||||
|
init?.();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="boost-carousel">
|
||||||
|
<header>
|
||||||
|
<h3>{boosts.length} Boosts</h3>
|
||||||
|
<span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="small plain2"
|
||||||
|
disabled={reachStart}
|
||||||
|
onClick={() => {
|
||||||
|
carouselRef.current?.scrollBy({
|
||||||
|
left: -Math.min(320, carouselRef.current?.offsetWidth),
|
||||||
|
behavior: 'smooth',
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon="chevron-left" />
|
||||||
|
</button>{' '}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="small plain2"
|
||||||
|
disabled={reachEnd}
|
||||||
|
onClick={() => {
|
||||||
|
carouselRef.current?.scrollBy({
|
||||||
|
left: Math.min(320, carouselRef.current?.offsetWidth),
|
||||||
|
behavior: 'smooth',
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon="chevron-right" />
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</header>
|
||||||
|
<ul ref={carouselRef}>
|
||||||
|
{boosts.map((boost) => {
|
||||||
|
const { id: statusID, reblog } = boost;
|
||||||
|
const actualStatusID = reblog || statusID;
|
||||||
|
return (
|
||||||
|
<li key={statusID}>
|
||||||
|
<Link class="status-boost-link" to={`/s/${actualStatusID}`}>
|
||||||
|
<Status statusID={statusID} size="s" />
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(Home);
|
|
@ -1,293 +1,18 @@
|
||||||
import { memo } from 'preact/compat';
|
import { useEffect } from 'preact/hooks';
|
||||||
import { useEffect, useRef, useState } from 'preact/hooks';
|
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
|
||||||
import { useDebouncedCallback } from 'use-debounce';
|
|
||||||
import { useSnapshot } from 'valtio';
|
import { useSnapshot } from 'valtio';
|
||||||
|
|
||||||
|
import Columns from '../components/columns';
|
||||||
import Icon from '../components/icon';
|
import Icon from '../components/icon';
|
||||||
import Link from '../components/link';
|
import Link from '../components/link';
|
||||||
import Loader from '../components/loader';
|
|
||||||
import Status from '../components/status';
|
|
||||||
import db from '../utils/db';
|
import db from '../utils/db';
|
||||||
import states, { saveStatus } from '../utils/states';
|
import openCompose from '../utils/open-compose';
|
||||||
|
import states from '../utils/states';
|
||||||
import { getCurrentAccountNS } from '../utils/store-utils';
|
import { getCurrentAccountNS } from '../utils/store-utils';
|
||||||
import useScroll from '../utils/useScroll';
|
|
||||||
import useTitle from '../utils/useTitle';
|
|
||||||
|
|
||||||
const LIMIT = 20;
|
import Following from './following';
|
||||||
|
|
||||||
function Home({ hidden }) {
|
function Home() {
|
||||||
useTitle('Home', '/');
|
|
||||||
const snapStates = useSnapshot(states);
|
const snapStates = useSnapshot(states);
|
||||||
const isHomeLocation = snapStates.currentLocation === '/';
|
|
||||||
const [uiState, setUIState] = useState('default');
|
|
||||||
const [showMore, setShowMore] = useState(false);
|
|
||||||
|
|
||||||
console.debug('RENDER Home');
|
|
||||||
|
|
||||||
const homeIterator = useRef();
|
|
||||||
async function fetchStatuses(firstLoad) {
|
|
||||||
if (firstLoad) {
|
|
||||||
// Reset iterator
|
|
||||||
homeIterator.current = masto.v1.timelines.listHome({
|
|
||||||
limit: LIMIT,
|
|
||||||
});
|
|
||||||
states.homeNew = [];
|
|
||||||
}
|
|
||||||
const allStatuses = await homeIterator.current.next();
|
|
||||||
if (allStatuses.value?.length) {
|
|
||||||
// ENFORCE sort by datetime (Latest first)
|
|
||||||
allStatuses.value.sort((a, b) => {
|
|
||||||
const aDate = new Date(a.createdAt);
|
|
||||||
const bDate = new Date(b.createdAt);
|
|
||||||
return bDate - aDate;
|
|
||||||
});
|
|
||||||
const homeValues = allStatuses.value.map((status) => {
|
|
||||||
saveStatus(status);
|
|
||||||
return {
|
|
||||||
id: status.id,
|
|
||||||
reblog: status.reblog?.id,
|
|
||||||
reply: !!status.inReplyToAccountId,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// BOOSTS CAROUSEL
|
|
||||||
if (snapStates.settings.boostsCarousel) {
|
|
||||||
let specialHome = [];
|
|
||||||
let boostStash = [];
|
|
||||||
let serialBoosts = 0;
|
|
||||||
for (let i = 0; i < homeValues.length; i++) {
|
|
||||||
const status = homeValues[i];
|
|
||||||
if (status.reblog) {
|
|
||||||
boostStash.push(status);
|
|
||||||
serialBoosts++;
|
|
||||||
} else {
|
|
||||||
specialHome.push(status);
|
|
||||||
if (serialBoosts < 3) {
|
|
||||||
serialBoosts = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// if boostStash is more than quarter of homeValues
|
|
||||||
// or if there are 3 or more boosts in a row
|
|
||||||
if (boostStash.length > homeValues.length / 4 || serialBoosts >= 3) {
|
|
||||||
// if boostStash is more than 3 quarter of homeValues
|
|
||||||
const boostStashID = boostStash.map((status) => status.id);
|
|
||||||
if (boostStash.length > (homeValues.length * 3) / 4) {
|
|
||||||
// insert boost array at the end of specialHome list
|
|
||||||
specialHome = [
|
|
||||||
...specialHome,
|
|
||||||
{ id: boostStashID, boosts: boostStash },
|
|
||||||
];
|
|
||||||
} else {
|
|
||||||
// insert boosts array in the middle of specialHome list
|
|
||||||
const half = Math.floor(specialHome.length / 2);
|
|
||||||
specialHome = [
|
|
||||||
...specialHome.slice(0, half),
|
|
||||||
{
|
|
||||||
id: boostStashID,
|
|
||||||
boosts: boostStash,
|
|
||||||
},
|
|
||||||
...specialHome.slice(half),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Untouched, this is fine
|
|
||||||
specialHome = homeValues;
|
|
||||||
}
|
|
||||||
console.log({
|
|
||||||
specialHome,
|
|
||||||
});
|
|
||||||
if (firstLoad) {
|
|
||||||
states.homeLast = specialHome[0];
|
|
||||||
states.home = specialHome;
|
|
||||||
} else {
|
|
||||||
states.home.push(...specialHome);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (firstLoad) {
|
|
||||||
states.homeLast = homeValues[0];
|
|
||||||
states.home = homeValues;
|
|
||||||
} else {
|
|
||||||
states.home.push(...homeValues);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
states.homeLastFetchTime = Date.now();
|
|
||||||
return allStatuses;
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadingStatuses = useRef(false);
|
|
||||||
const loadStatuses = useDebouncedCallback(
|
|
||||||
(firstLoad) => {
|
|
||||||
if (loadingStatuses.current) return;
|
|
||||||
loadingStatuses.current = true;
|
|
||||||
setUIState('loading');
|
|
||||||
(async () => {
|
|
||||||
try {
|
|
||||||
const { done } = await fetchStatuses(firstLoad);
|
|
||||||
setShowMore(!done);
|
|
||||||
setUIState('default');
|
|
||||||
} catch (e) {
|
|
||||||
console.warn(e);
|
|
||||||
setUIState('error');
|
|
||||||
} finally {
|
|
||||||
loadingStatuses.current = false;
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
},
|
|
||||||
3000,
|
|
||||||
{ leading: true, trailing: false },
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
loadStatuses(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const scrollableRef = useRef();
|
|
||||||
|
|
||||||
useHotkeys(
|
|
||||||
'j, shift+j',
|
|
||||||
(_, handler) => {
|
|
||||||
// focus on next status after active status
|
|
||||||
// Traverses .timeline li .status-link, focus on .status-link
|
|
||||||
const activeStatus = document.activeElement.closest(
|
|
||||||
'.status-link, .status-boost-link',
|
|
||||||
);
|
|
||||||
const activeStatusRect = activeStatus?.getBoundingClientRect();
|
|
||||||
const allStatusLinks = Array.from(
|
|
||||||
scrollableRef.current.querySelectorAll(
|
|
||||||
'.status-link, .status-boost-link',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
if (
|
|
||||||
activeStatus &&
|
|
||||||
activeStatusRect.top < scrollableRef.current.clientHeight &&
|
|
||||||
activeStatusRect.bottom > 0
|
|
||||||
) {
|
|
||||||
const activeStatusIndex = allStatusLinks.indexOf(activeStatus);
|
|
||||||
let nextStatus = allStatusLinks[activeStatusIndex + 1];
|
|
||||||
if (handler.shift) {
|
|
||||||
// get next status that's not .status-boost-link
|
|
||||||
nextStatus = allStatusLinks.find(
|
|
||||||
(statusLink, index) =>
|
|
||||||
index > activeStatusIndex &&
|
|
||||||
!statusLink.classList.contains('status-boost-link'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (nextStatus) {
|
|
||||||
nextStatus.focus();
|
|
||||||
nextStatus.scrollIntoViewIfNeeded?.();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// If active status is not in viewport, get the topmost status-link in viewport
|
|
||||||
const topmostStatusLink = allStatusLinks.find((statusLink) => {
|
|
||||||
const statusLinkRect = statusLink.getBoundingClientRect();
|
|
||||||
return statusLinkRect.top >= 44 && statusLinkRect.left >= 0; // 44 is the magic number for header height, not real
|
|
||||||
});
|
|
||||||
if (topmostStatusLink) {
|
|
||||||
topmostStatusLink.focus();
|
|
||||||
topmostStatusLink.scrollIntoViewIfNeeded?.();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: isHomeLocation,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
useHotkeys(
|
|
||||||
'k, shift+k',
|
|
||||||
(_, handler) => {
|
|
||||||
// focus on previous status after active status
|
|
||||||
// Traverses .timeline li .status-link, focus on .status-link
|
|
||||||
const activeStatus = document.activeElement.closest(
|
|
||||||
'.status-link, .status-boost-link',
|
|
||||||
);
|
|
||||||
const activeStatusRect = activeStatus?.getBoundingClientRect();
|
|
||||||
const allStatusLinks = Array.from(
|
|
||||||
scrollableRef.current.querySelectorAll(
|
|
||||||
'.status-link, .status-boost-link',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
if (
|
|
||||||
activeStatus &&
|
|
||||||
activeStatusRect.top < scrollableRef.current.clientHeight &&
|
|
||||||
activeStatusRect.bottom > 0
|
|
||||||
) {
|
|
||||||
const activeStatusIndex = allStatusLinks.indexOf(activeStatus);
|
|
||||||
let prevStatus = allStatusLinks[activeStatusIndex - 1];
|
|
||||||
if (handler.shift) {
|
|
||||||
// get prev status that's not .status-boost-link
|
|
||||||
prevStatus = allStatusLinks.findLast(
|
|
||||||
(statusLink, index) =>
|
|
||||||
index < activeStatusIndex &&
|
|
||||||
!statusLink.classList.contains('status-boost-link'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (prevStatus) {
|
|
||||||
prevStatus.focus();
|
|
||||||
prevStatus.scrollIntoViewIfNeeded?.();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// If active status is not in viewport, get the topmost status-link in viewport
|
|
||||||
const topmostStatusLink = allStatusLinks.find((statusLink) => {
|
|
||||||
const statusLinkRect = statusLink.getBoundingClientRect();
|
|
||||||
return statusLinkRect.top >= 44 && statusLinkRect.left >= 0; // 44 is the magic number for header height, not real
|
|
||||||
});
|
|
||||||
if (topmostStatusLink) {
|
|
||||||
topmostStatusLink.focus();
|
|
||||||
topmostStatusLink.scrollIntoViewIfNeeded?.();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: isHomeLocation,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
useHotkeys(
|
|
||||||
['enter', 'o'],
|
|
||||||
() => {
|
|
||||||
// open active status
|
|
||||||
const activeStatus = document.activeElement.closest(
|
|
||||||
'.status-link, .status-boost-link',
|
|
||||||
);
|
|
||||||
if (activeStatus) {
|
|
||||||
activeStatus.click();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: isHomeLocation,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const {
|
|
||||||
scrollDirection,
|
|
||||||
reachStart,
|
|
||||||
nearReachStart,
|
|
||||||
nearReachEnd,
|
|
||||||
reachEnd,
|
|
||||||
} = useScroll({
|
|
||||||
scrollableElement: scrollableRef.current,
|
|
||||||
distanceFromStart: 1,
|
|
||||||
distanceFromEnd: 3,
|
|
||||||
scrollThresholdStart: 44,
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (nearReachEnd || (reachEnd && showMore)) {
|
|
||||||
loadStatuses();
|
|
||||||
}
|
|
||||||
}, [nearReachEnd, reachEnd]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (reachStart) {
|
|
||||||
loadStatuses(true);
|
|
||||||
}
|
|
||||||
}, [reachStart]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
const keys = await db.drafts.keys();
|
const keys = await db.drafts.keys();
|
||||||
|
@ -301,52 +26,21 @@ function Home({ hidden }) {
|
||||||
})();
|
})();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// const showUpdatesButton = snapStates.homeNew.length > 0 && reachStart;
|
|
||||||
const [showUpdatesButton, setShowUpdatesButton] = useState(false);
|
|
||||||
useEffect(() => {
|
|
||||||
const isNewAndTop = snapStates.homeNew.length > 0 && reachStart;
|
|
||||||
setShowUpdatesButton(isNewAndTop);
|
|
||||||
}, [snapStates.homeNew.length]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
{snapStates.settings.shortcutsColumnsMode ? (
|
||||||
id="home-page"
|
<Columns />
|
||||||
class="deck-container"
|
) : (
|
||||||
hidden={hidden}
|
<Following
|
||||||
ref={scrollableRef}
|
title="Home"
|
||||||
tabIndex="-1"
|
path="/"
|
||||||
>
|
id="home"
|
||||||
<div class="timeline-deck deck">
|
headerStart={false}
|
||||||
<header
|
headerEnd={
|
||||||
hidden={scrollDirection === 'end' && !nearReachStart}
|
|
||||||
onClick={() => {
|
|
||||||
scrollableRef.current?.scrollTo({ top: 0, behavior: 'smooth' });
|
|
||||||
}}
|
|
||||||
onDblClick={() => {
|
|
||||||
loadStatuses(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div class="header-side">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="plain"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
states.showSettings = true;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon icon="gear" size="l" alt="Settings" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<h1>Home</h1>
|
|
||||||
<div class="header-side">
|
|
||||||
<Loader hidden={uiState !== 'loading'} />{' '}
|
|
||||||
<Link
|
<Link
|
||||||
to="/notifications"
|
to="/notifications"
|
||||||
class={`button plain ${
|
class={`button plain ${
|
||||||
snapStates.notificationsNew.length > 0 ? 'has-badge' : ''
|
snapStates.notificationsShowNew ? 'has-badge' : ''
|
||||||
}`}
|
}`}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
@ -354,108 +48,11 @@ function Home({ hidden }) {
|
||||||
>
|
>
|
||||||
<Icon icon="notification" size="l" alt="Notifications" />
|
<Icon icon="notification" size="l" alt="Notifications" />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
{snapStates.homeNew.length > 0 &&
|
|
||||||
uiState !== 'loading' &&
|
|
||||||
((scrollDirection === 'start' &&
|
|
||||||
!nearReachStart &&
|
|
||||||
!nearReachEnd) ||
|
|
||||||
showUpdatesButton) && (
|
|
||||||
<button
|
|
||||||
class="updates-button"
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
if (!snapStates.settings.boostsCarousel) {
|
|
||||||
const uniqueHomeNew = snapStates.homeNew.filter(
|
|
||||||
(status) => !states.home.some((s) => s.id === status.id),
|
|
||||||
);
|
|
||||||
states.home.unshift(...uniqueHomeNew);
|
|
||||||
}
|
}
|
||||||
loadStatuses(true);
|
/>
|
||||||
states.homeNew = [];
|
|
||||||
|
|
||||||
scrollableRef.current?.scrollTo({
|
|
||||||
top: 0,
|
|
||||||
behavior: 'smooth',
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon icon="arrow-up" /> New posts
|
|
||||||
</button>
|
|
||||||
)}
|
)}
|
||||||
{snapStates.home.length ? (
|
|
||||||
<>
|
|
||||||
<ul class="timeline">
|
|
||||||
{snapStates.home.map(({ id: statusID, reblog, boosts }) => {
|
|
||||||
const actualStatusID = reblog || statusID;
|
|
||||||
if (boosts) {
|
|
||||||
return (
|
|
||||||
<li key={statusID}>
|
|
||||||
<BoostsCarousel boosts={boosts} />
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<li key={statusID}>
|
|
||||||
<Link class="status-link" to={`/s/${actualStatusID}`}>
|
|
||||||
<Status statusID={statusID} />
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
{showMore && (
|
|
||||||
<>
|
|
||||||
<li
|
|
||||||
style={{
|
|
||||||
height: '20vh',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Status skeleton />
|
|
||||||
</li>
|
|
||||||
<li
|
|
||||||
style={{
|
|
||||||
height: '25vh',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Status skeleton />
|
|
||||||
</li>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</ul>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{uiState === 'loading' && (
|
|
||||||
<ul class="timeline">
|
|
||||||
{Array.from({ length: 5 }).map((_, i) => (
|
|
||||||
<li key={i}>
|
|
||||||
<Status skeleton />
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
{uiState === 'error' && (
|
|
||||||
<p class="ui-state">
|
|
||||||
Unable to load statuses
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
// hidden={scrollDirection === 'end' && !nearReachStart}
|
||||||
onClick={() => {
|
|
||||||
loadStatuses(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Try again
|
|
||||||
</button>
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
hidden={scrollDirection === 'end' && !nearReachStart}
|
|
||||||
type="button"
|
type="button"
|
||||||
id="compose-button"
|
id="compose-button"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
|
@ -470,70 +67,10 @@ function Home({ hidden }) {
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="quill" size="xxl" alt="Compose" />
|
<Icon icon="quill" size="xl" alt="Compose" />
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function BoostsCarousel({ boosts }) {
|
export default Home;
|
||||||
const carouselRef = useRef();
|
|
||||||
const { reachStart, reachEnd, init } = useScroll({
|
|
||||||
scrollableElement: carouselRef.current,
|
|
||||||
direction: 'horizontal',
|
|
||||||
});
|
|
||||||
useEffect(() => {
|
|
||||||
init?.();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div class="boost-carousel">
|
|
||||||
<header>
|
|
||||||
<h3>{boosts.length} Boosts</h3>
|
|
||||||
<span>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="small plain2"
|
|
||||||
disabled={reachStart}
|
|
||||||
onClick={() => {
|
|
||||||
carouselRef.current?.scrollBy({
|
|
||||||
left: -Math.min(320, carouselRef.current?.offsetWidth),
|
|
||||||
behavior: 'smooth',
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon icon="chevron-left" />
|
|
||||||
</button>{' '}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="small plain2"
|
|
||||||
disabled={reachEnd}
|
|
||||||
onClick={() => {
|
|
||||||
carouselRef.current?.scrollBy({
|
|
||||||
left: Math.min(320, carouselRef.current?.offsetWidth),
|
|
||||||
behavior: 'smooth',
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon icon="chevron-right" />
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
</header>
|
|
||||||
<ul ref={carouselRef}>
|
|
||||||
{boosts.map((boost) => {
|
|
||||||
const { id: statusID, reblog } = boost;
|
|
||||||
const actualStatusID = reblog || statusID;
|
|
||||||
return (
|
|
||||||
<li key={statusID}>
|
|
||||||
<Link class="status-boost-link" to={`/s/${actualStatusID}`}>
|
|
||||||
<Status statusID={statusID} size="s" />
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default memo(Home);
|
|
||||||
|
|
82
src/pages/list.jsx
Normal file
82
src/pages/list.jsx
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
import { useEffect, useRef, useState } from 'preact/hooks';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
import Icon from '../components/icon';
|
||||||
|
import Link from '../components/link';
|
||||||
|
import Timeline from '../components/timeline';
|
||||||
|
import { api } from '../utils/api';
|
||||||
|
import useTitle from '../utils/useTitle';
|
||||||
|
|
||||||
|
const LIMIT = 20;
|
||||||
|
|
||||||
|
function List(props) {
|
||||||
|
const { masto, instance } = api();
|
||||||
|
const id = props?.id || useParams()?.id;
|
||||||
|
const latestItem = useRef();
|
||||||
|
|
||||||
|
const listIterator = useRef();
|
||||||
|
async function fetchList(firstLoad) {
|
||||||
|
if (firstLoad || !listIterator.current) {
|
||||||
|
listIterator.current = masto.v1.timelines.listList(id, {
|
||||||
|
limit: LIMIT,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const results = await listIterator.current.next();
|
||||||
|
const { value } = results;
|
||||||
|
if (value?.length) {
|
||||||
|
if (firstLoad) {
|
||||||
|
latestItem.current = value[0].id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkForUpdates() {
|
||||||
|
try {
|
||||||
|
const results = await masto.v1.timelines.listList(id, {
|
||||||
|
limit: 1,
|
||||||
|
since_id: latestItem.current,
|
||||||
|
});
|
||||||
|
const { value } = results;
|
||||||
|
if (value?.length) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const [title, setTitle] = useState(`List`);
|
||||||
|
useTitle(title, `/l/:id`);
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const list = await masto.v1.lists.fetch(id);
|
||||||
|
setTitle(list.title);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Timeline
|
||||||
|
title={title}
|
||||||
|
id="list"
|
||||||
|
emptyText="Nothing yet."
|
||||||
|
errorText="Unable to load posts."
|
||||||
|
instance={instance}
|
||||||
|
fetchItems={fetchList}
|
||||||
|
checkForUpdates={checkForUpdates}
|
||||||
|
boostsCarousel
|
||||||
|
headerStart={
|
||||||
|
<Link to="/l" class="button plain">
|
||||||
|
<Icon icon="list" size="l" />
|
||||||
|
</Link>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default List;
|
|
@ -1,42 +1,71 @@
|
||||||
import { useEffect, useRef, useState } from 'preact/hooks';
|
import { useEffect, useState } from 'preact/hooks';
|
||||||
import { useParams } from 'react-router-dom';
|
|
||||||
|
|
||||||
import Timeline from '../components/timeline';
|
import Icon from '../components/icon';
|
||||||
|
import Link from '../components/link';
|
||||||
const LIMIT = 20;
|
import Loader from '../components/loader';
|
||||||
|
import Menu from '../components/menu';
|
||||||
|
import { api } from '../utils/api';
|
||||||
|
import useTitle from '../utils/useTitle';
|
||||||
|
|
||||||
function Lists() {
|
function Lists() {
|
||||||
const { id } = useParams();
|
const { masto } = api();
|
||||||
const listsIterator = useRef();
|
useTitle(`Lists`, `/l`);
|
||||||
async function fetchLists(firstLoad) {
|
const [uiState, setUiState] = useState('default');
|
||||||
if (firstLoad || !listsIterator.current) {
|
|
||||||
listsIterator.current = masto.v1.timelines.listList(id, {
|
|
||||||
limit: LIMIT,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return await listsIterator.current.next();
|
|
||||||
}
|
|
||||||
|
|
||||||
const [title, setTitle] = useState(`List ${id}`);
|
const [lists, setLists] = useState([]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
setUiState('loading');
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const list = await masto.v1.lists.fetch(id);
|
const lists = await masto.v1.lists.list();
|
||||||
setTitle(list.title);
|
console.log(lists);
|
||||||
|
setLists(lists);
|
||||||
|
setUiState('default');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
setUiState('error');
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}, [id]);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Timeline
|
<div id="lists-page" class="deck-container">
|
||||||
title={title}
|
<div class="timeline-deck deck">
|
||||||
id="lists"
|
<header>
|
||||||
emptyText="Nothing yet."
|
<div class="header-grid">
|
||||||
errorText="Unable to load posts."
|
<div class="header-side">
|
||||||
fetchItems={fetchLists}
|
<Menu />
|
||||||
/>
|
<Link to="/" class="button plain">
|
||||||
|
<Icon icon="home" size="l" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<h1>Lists</h1>
|
||||||
|
<div class="header-side" />
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
{lists.length > 0 ? (
|
||||||
|
<ul class="link-list">
|
||||||
|
{lists.map((list) => (
|
||||||
|
<li>
|
||||||
|
<Link to={`/l/${list.id}`}>
|
||||||
|
<Icon icon="list" /> <span>{list.title}</span>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
) : uiState === 'loading' ? (
|
||||||
|
<p class="ui-state">
|
||||||
|
<Loader />
|
||||||
|
</p>
|
||||||
|
) : uiState === 'error' ? (
|
||||||
|
<p class="ui-state">Unable to load lists.</p>
|
||||||
|
) : (
|
||||||
|
<p class="ui-state">No lists yet.</p>
|
||||||
|
)}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -40,7 +40,9 @@ function Login() {
|
||||||
const { elements } = e.target;
|
const { elements } = e.target;
|
||||||
let instanceURL = elements.instanceURL.value.toLowerCase();
|
let instanceURL = elements.instanceURL.value.toLowerCase();
|
||||||
// Remove protocol from instance URL
|
// Remove protocol from instance URL
|
||||||
instanceURL = instanceURL.replace(/(^\w+:|^)\/\//, '');
|
instanceURL = instanceURL.replace(/^https?:\/\//, '').replace(/\/+$/, '');
|
||||||
|
// Remove @acct@ or acct@ from instance URL
|
||||||
|
instanceURL = instanceURL.replace(/^@?[^@]+@/, '');
|
||||||
store.local.set('instanceURL', instanceURL);
|
store.local.set('instanceURL', instanceURL);
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
|
|
|
@ -8,9 +8,11 @@ import Avatar from '../components/avatar';
|
||||||
import Icon from '../components/icon';
|
import Icon from '../components/icon';
|
||||||
import Link from '../components/link';
|
import Link from '../components/link';
|
||||||
import Loader from '../components/loader';
|
import Loader from '../components/loader';
|
||||||
|
import Menu from '../components/menu';
|
||||||
import NameText from '../components/name-text';
|
import NameText from '../components/name-text';
|
||||||
import RelativeTime from '../components/relative-time';
|
import RelativeTime from '../components/relative-time';
|
||||||
import Status from '../components/status';
|
import Status from '../components/status';
|
||||||
|
import { api } from '../utils/api';
|
||||||
import states, { saveStatus } from '../utils/states';
|
import states, { saveStatus } from '../utils/states';
|
||||||
import store from '../utils/store';
|
import store from '../utils/store';
|
||||||
import useScroll from '../utils/useScroll';
|
import useScroll from '../utils/useScroll';
|
||||||
|
@ -48,45 +50,50 @@ const LIMIT = 30; // 30 is the maximum limit :(
|
||||||
|
|
||||||
function Notifications() {
|
function Notifications() {
|
||||||
useTitle('Notifications', '/notifications');
|
useTitle('Notifications', '/notifications');
|
||||||
|
const { masto, instance } = api();
|
||||||
const snapStates = useSnapshot(states);
|
const snapStates = useSnapshot(states);
|
||||||
const [uiState, setUIState] = useState('default');
|
const [uiState, setUIState] = useState('default');
|
||||||
const [showMore, setShowMore] = useState(false);
|
const [showMore, setShowMore] = useState(false);
|
||||||
const [onlyMentions, setOnlyMentions] = useState(false);
|
const [onlyMentions, setOnlyMentions] = useState(false);
|
||||||
const scrollableRef = useRef();
|
const scrollableRef = useRef();
|
||||||
const { nearReachEnd, reachStart } = useScroll({
|
const { nearReachEnd, scrollDirection, reachStart, nearReachStart } =
|
||||||
|
useScroll({
|
||||||
scrollableElement: scrollableRef.current,
|
scrollableElement: scrollableRef.current,
|
||||||
});
|
});
|
||||||
|
const hiddenUI = scrollDirection === 'end' && !nearReachStart;
|
||||||
|
|
||||||
console.debug('RENDER Notifications');
|
console.debug('RENDER Notifications');
|
||||||
|
|
||||||
const notificationsIterator = useRef();
|
const notificationsIterator = useRef();
|
||||||
async function fetchNotifications(firstLoad) {
|
async function fetchNotifications(firstLoad) {
|
||||||
if (firstLoad) {
|
if (firstLoad || !notificationsIterator.current) {
|
||||||
// Reset iterator
|
// Reset iterator
|
||||||
notificationsIterator.current = masto.v1.notifications.list({
|
notificationsIterator.current = masto.v1.notifications.list({
|
||||||
limit: LIMIT,
|
limit: LIMIT,
|
||||||
});
|
});
|
||||||
states.notificationsNew = [];
|
|
||||||
}
|
}
|
||||||
const allNotifications = await notificationsIterator.current.next();
|
const allNotifications = await notificationsIterator.current.next();
|
||||||
if (allNotifications.value?.length) {
|
const notifications = allNotifications.value;
|
||||||
const notificationsValues = allNotifications.value.map((notification) => {
|
|
||||||
|
if (notifications?.length) {
|
||||||
|
notifications.forEach((notification) => {
|
||||||
saveStatus(notification.status, {
|
saveStatus(notification.status, {
|
||||||
skipThreading: true,
|
skipThreading: true,
|
||||||
override: false,
|
override: false,
|
||||||
});
|
});
|
||||||
return notification;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const groupedNotifications = groupNotifications(notificationsValues);
|
const groupedNotifications = groupNotifications(notifications);
|
||||||
|
|
||||||
if (firstLoad) {
|
if (firstLoad) {
|
||||||
states.notificationLast = notificationsValues[0];
|
states.notificationsLast = notifications[0];
|
||||||
states.notifications = groupedNotifications;
|
states.notifications = groupedNotifications;
|
||||||
} else {
|
} else {
|
||||||
states.notifications.push(...groupedNotifications);
|
states.notifications.push(...groupedNotifications);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
states.notificationsShowNew = false;
|
||||||
states.notificationsLastFetchTime = Date.now();
|
states.notificationsLastFetchTime = Date.now();
|
||||||
return allNotifications;
|
return allNotifications;
|
||||||
}
|
}
|
||||||
|
@ -137,11 +144,16 @@ function Notifications() {
|
||||||
>
|
>
|
||||||
<div class={`timeline-deck deck ${onlyMentions ? 'only-mentions' : ''}`}>
|
<div class={`timeline-deck deck ${onlyMentions ? 'only-mentions' : ''}`}>
|
||||||
<header
|
<header
|
||||||
onClick={() => {
|
hidden={hiddenUI}
|
||||||
|
onClick={(e) => {
|
||||||
|
if (!e.target.closest('a, button')) {
|
||||||
scrollableRef.current?.scrollTo({ top: 0, behavior: 'smooth' });
|
scrollableRef.current?.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<div class="header-grid">
|
||||||
<div class="header-side">
|
<div class="header-side">
|
||||||
|
<Menu />
|
||||||
<Link to="/" class="button plain">
|
<Link to="/" class="button plain">
|
||||||
<Icon icon="home" size="l" />
|
<Icon icon="home" size="l" />
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -150,15 +162,13 @@ function Notifications() {
|
||||||
<div class="header-side">
|
<div class="header-side">
|
||||||
<Loader hidden={uiState !== 'loading'} />
|
<Loader hidden={uiState !== 'loading'} />
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</div>
|
||||||
{snapStates.notificationsNew.length > 0 && uiState !== 'loading' && (
|
{snapStates.notificationsShowNew && uiState !== 'loading' && (
|
||||||
<button
|
<button
|
||||||
class="updates-button"
|
class="updates-button"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
loadNotifications(true);
|
loadNotifications(true);
|
||||||
states.notificationsNew = [];
|
|
||||||
|
|
||||||
scrollableRef.current?.scrollTo({
|
scrollableRef.current?.scrollTo({
|
||||||
top: 0,
|
top: 0,
|
||||||
behavior: 'smooth',
|
behavior: 'smooth',
|
||||||
|
@ -168,6 +178,7 @@ function Notifications() {
|
||||||
<Icon icon="arrow-up" /> New notifications
|
<Icon icon="arrow-up" /> New notifications
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
</header>
|
||||||
<div id="mentions-option">
|
<div id="mentions-option">
|
||||||
<label>
|
<label>
|
||||||
<input
|
<input
|
||||||
|
@ -216,6 +227,7 @@ function Notifications() {
|
||||||
<>
|
<>
|
||||||
{differentDay && <h2 class="timeline-header">{heading}</h2>}
|
{differentDay && <h2 class="timeline-header">{heading}</h2>}
|
||||||
<Notification
|
<Notification
|
||||||
|
instance={instance}
|
||||||
notification={notification}
|
notification={notification}
|
||||||
key={notification.id}
|
key={notification.id}
|
||||||
/>
|
/>
|
||||||
|
@ -268,7 +280,7 @@ function Notifications() {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
function Notification({ notification }) {
|
function Notification({ notification, instance }) {
|
||||||
const { id, type, status, account, _accounts } = notification;
|
const { id, type, status, account, _accounts } = notification;
|
||||||
|
|
||||||
// status = Attached when type of the notification is favourite, reblog, status, mention, poll, or update
|
// status = Attached when type of the notification is favourite, reblog, status, mention, poll, or update
|
||||||
|
@ -381,7 +393,11 @@ function Notification({ notification }) {
|
||||||
{status && (
|
{status && (
|
||||||
<Link
|
<Link
|
||||||
class={`status-link status-type-${type}`}
|
class={`status-link status-type-${type}`}
|
||||||
to={`/s/${actualStatusID}`}
|
to={
|
||||||
|
instance
|
||||||
|
? `/${instance}/s/${actualStatusID}`
|
||||||
|
: `/s/${actualStatusID}`
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Status status={status} size="s" />
|
<Status status={status} size="s" />
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -392,6 +408,7 @@ function Notification({ notification }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function FollowRequestButtons({ accountID, onChange }) {
|
function FollowRequestButtons({ accountID, onChange }) {
|
||||||
|
const { masto } = api();
|
||||||
const [uiState, setUIState] = useState('default');
|
const [uiState, setUIState] = useState('default');
|
||||||
return (
|
return (
|
||||||
<p>
|
<p>
|
||||||
|
@ -449,7 +466,9 @@ function groupNotifications(notifications) {
|
||||||
const date = new Date(createdAt).toLocaleDateString();
|
const date = new Date(createdAt).toLocaleDateString();
|
||||||
const key = `${status?.id}-${type}-${date}`;
|
const key = `${status?.id}-${type}-${date}`;
|
||||||
const mappedNotification = notificationsMap[key];
|
const mappedNotification = notificationsMap[key];
|
||||||
if (mappedNotification?.account) {
|
if (type === 'follow_request') {
|
||||||
|
cleanNotifications[j++] = notification;
|
||||||
|
} else if (mappedNotification?.account) {
|
||||||
mappedNotification._accounts.push(account);
|
mappedNotification._accounts.push(account);
|
||||||
} else {
|
} else {
|
||||||
let n = (notificationsMap[key] = {
|
let n = (notificationsMap[key] = {
|
||||||
|
|
|
@ -1,76 +1,101 @@
|
||||||
// EXPERIMENTAL: This is a work in progress and may not work as expected.
|
// EXPERIMENTAL: This is a work in progress and may not work as expected.
|
||||||
import { useMatch, useParams } from 'react-router-dom';
|
import { useRef } from 'preact/hooks';
|
||||||
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
import Icon from '../components/icon';
|
||||||
import Timeline from '../components/timeline';
|
import Timeline from '../components/timeline';
|
||||||
|
import { api } from '../utils/api';
|
||||||
|
import useTitle from '../utils/useTitle';
|
||||||
|
|
||||||
const LIMIT = 20;
|
const LIMIT = 20;
|
||||||
|
|
||||||
let nextUrl = null;
|
function Public({ local, ...props }) {
|
||||||
|
const isLocal = !!local;
|
||||||
function Public() {
|
|
||||||
const isLocal = !!useMatch('/p/l/:instance');
|
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const { instance = '' } = params;
|
const { masto, instance } = api({
|
||||||
async function fetchPublic(firstLoad) {
|
instance: props?.instance || params.instance,
|
||||||
const url = firstLoad
|
|
||||||
? `https://${instance}/api/v1/timelines/public?limit=${LIMIT}&local=${isLocal}`
|
|
||||||
: nextUrl;
|
|
||||||
if (!url) return { values: [], done: true };
|
|
||||||
const response = await fetch(url);
|
|
||||||
let value = await response.json();
|
|
||||||
if (value) {
|
|
||||||
value = camelCaseKeys(value);
|
|
||||||
}
|
|
||||||
const done = !response.headers.has('link');
|
|
||||||
nextUrl = done
|
|
||||||
? null
|
|
||||||
: response.headers.get('link').match(/<(.+?)>; rel="next"/)?.[1];
|
|
||||||
console.debug({
|
|
||||||
url,
|
|
||||||
value,
|
|
||||||
done,
|
|
||||||
nextUrl,
|
|
||||||
});
|
});
|
||||||
return { value, done };
|
const title = `${isLocal ? 'Local' : 'Federated'} timeline (${instance})`;
|
||||||
|
useTitle(title, isLocal ? `/:instance?/p/l` : `/:instance?/p`);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const latestItem = useRef();
|
||||||
|
|
||||||
|
const publicIterator = useRef();
|
||||||
|
async function fetchPublic(firstLoad) {
|
||||||
|
if (firstLoad || !publicIterator.current) {
|
||||||
|
publicIterator.current = masto.v1.timelines.listPublic({
|
||||||
|
limit: LIMIT,
|
||||||
|
local: isLocal,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const results = await publicIterator.current.next();
|
||||||
|
const { value } = results;
|
||||||
|
if (value?.length) {
|
||||||
|
if (firstLoad) {
|
||||||
|
latestItem.current = value[0].id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkForUpdates() {
|
||||||
|
try {
|
||||||
|
const results = await masto.v1.timelines
|
||||||
|
.listPublic({
|
||||||
|
limit: 1,
|
||||||
|
local: isLocal,
|
||||||
|
since_id: latestItem.current,
|
||||||
|
})
|
||||||
|
.next();
|
||||||
|
const { value } = results;
|
||||||
|
if (value?.length) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Timeline
|
<Timeline
|
||||||
key={instance + isLocal}
|
key={instance + isLocal}
|
||||||
title={`${instance} (${isLocal ? 'local' : 'federated'})`}
|
title={title}
|
||||||
|
titleComponent={
|
||||||
|
<h1 class="header-account">
|
||||||
|
<b>{isLocal ? 'Local timeline' : 'Federated timeline'}</b>
|
||||||
|
<div>{instance}</div>
|
||||||
|
</h1>
|
||||||
|
}
|
||||||
id="public"
|
id="public"
|
||||||
|
instance={instance}
|
||||||
emptyText="No one has posted anything yet."
|
emptyText="No one has posted anything yet."
|
||||||
errorText="Unable to load posts"
|
errorText="Unable to load posts"
|
||||||
fetchItems={fetchPublic}
|
fetchItems={fetchPublic}
|
||||||
|
checkForUpdates={checkForUpdates}
|
||||||
|
headerStart={<></>}
|
||||||
|
headerEnd={
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="plain"
|
||||||
|
onClick={() => {
|
||||||
|
const newInstance = prompt(
|
||||||
|
'Enter a new instance e.g. "mastodon.social"',
|
||||||
|
);
|
||||||
|
if (!/\./.test(newInstance)) {
|
||||||
|
if (newInstance) alert('Invalid instance');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (newInstance) {
|
||||||
|
navigate(isLocal ? `/${newInstance}/p/l` : `/${newInstance}/p`);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon="transfer" alt="Switch instance" />
|
||||||
|
</button>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function camelCaseKeys(obj) {
|
|
||||||
if (Array.isArray(obj)) {
|
|
||||||
return obj.map((item) => camelCaseKeys(item));
|
|
||||||
}
|
|
||||||
return new Proxy(obj, {
|
|
||||||
get(target, prop) {
|
|
||||||
let value = undefined;
|
|
||||||
if (prop in target) {
|
|
||||||
value = target[prop];
|
|
||||||
}
|
|
||||||
if (!value) {
|
|
||||||
const snakeCaseProp = prop.replace(
|
|
||||||
/([A-Z])/g,
|
|
||||||
(g) => `_${g.toLowerCase()}`,
|
|
||||||
);
|
|
||||||
if (snakeCaseProp in target) {
|
|
||||||
value = target[snakeCaseProp];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (value && typeof value === 'object') {
|
|
||||||
return camelCaseKeys(value);
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Public;
|
export default Public;
|
||||||
|
|
45
src/pages/search.css
Normal file
45
src/pages/search.css
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
#search-page .deck > header .header-grid {
|
||||||
|
grid-template-columns: auto 1fr auto;
|
||||||
|
}
|
||||||
|
#search-page header input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 999px;
|
||||||
|
background-color: var(--bg-faded-color);
|
||||||
|
border: 2px solid transparent;
|
||||||
|
}
|
||||||
|
#search-page header input:focus {
|
||||||
|
outline: 0;
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
border-color: var(--link-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
#search-page ul.accounts-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
#search-page ul.accounts-list li {
|
||||||
|
flex-basis: 320px;
|
||||||
|
display: flex;
|
||||||
|
padding: 8px 16px;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.link-list.hashtag-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.link-list.hashtag-list li a {
|
||||||
|
border-radius: var(--radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 40em) {
|
||||||
|
#search-page header input {
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
}
|
||||||
|
}
|
158
src/pages/search.jsx
Normal file
158
src/pages/search.jsx
Normal file
|
@ -0,0 +1,158 @@
|
||||||
|
import './search.css';
|
||||||
|
|
||||||
|
import { useEffect, useRef, useState } from 'preact/hooks';
|
||||||
|
import { useParams, useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
import AccountBlock from '../components/account-block';
|
||||||
|
import Icon from '../components/icon';
|
||||||
|
import Link from '../components/link';
|
||||||
|
import Loader from '../components/loader';
|
||||||
|
import Menu from '../components/menu';
|
||||||
|
import Status from '../components/status';
|
||||||
|
import { api } from '../utils/api';
|
||||||
|
import useTitle from '../utils/useTitle';
|
||||||
|
|
||||||
|
function Search(props) {
|
||||||
|
const params = useParams();
|
||||||
|
const { masto, instance, authenticated } = api({
|
||||||
|
instance: params.instance,
|
||||||
|
});
|
||||||
|
const [uiState, setUiState] = useState('default');
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
const searchFieldRef = useRef();
|
||||||
|
const q = props?.query || searchParams.get('q');
|
||||||
|
useTitle(q ? `Search: ${q}` : 'Search', `/search`);
|
||||||
|
|
||||||
|
const [statusResults, setStatusResults] = useState([]);
|
||||||
|
const [accountResults, setAccountResults] = useState([]);
|
||||||
|
const [hashtagResults, setHashtagResults] = useState([]);
|
||||||
|
useEffect(() => {
|
||||||
|
searchFieldRef.current?.focus?.();
|
||||||
|
if (q) {
|
||||||
|
searchFieldRef.current.value = q;
|
||||||
|
|
||||||
|
setUiState('loading');
|
||||||
|
(async () => {
|
||||||
|
const results = await masto.v2.search({
|
||||||
|
q,
|
||||||
|
limit: 20,
|
||||||
|
resolve: authenticated,
|
||||||
|
});
|
||||||
|
console.log(results);
|
||||||
|
setStatusResults(results.statuses);
|
||||||
|
setAccountResults(results.accounts);
|
||||||
|
setHashtagResults(results.hashtags);
|
||||||
|
setUiState('default');
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
}, [q, instance]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div id="search-page" class="deck-container">
|
||||||
|
<div class="timeline-deck deck">
|
||||||
|
<header>
|
||||||
|
<div class="header-grid">
|
||||||
|
<div class="header-side">
|
||||||
|
<Menu />
|
||||||
|
</div>
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const { q } = e.target;
|
||||||
|
if (q.value) {
|
||||||
|
setSearchParams({ q: q.value });
|
||||||
|
} else {
|
||||||
|
setSearchParams({});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
ref={searchFieldRef}
|
||||||
|
name="q"
|
||||||
|
type="search"
|
||||||
|
autofocus
|
||||||
|
placeholder="Search"
|
||||||
|
onSearch={(e) => {
|
||||||
|
if (!e.target.value) {
|
||||||
|
setSearchParams({});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
<div class="header-side" />
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
{!!q && uiState !== 'loading' ? (
|
||||||
|
<>
|
||||||
|
<h2 class="timeline-header">Accounts</h2>
|
||||||
|
{accountResults.length > 0 ? (
|
||||||
|
<ul class="timeline flat accounts-list">
|
||||||
|
{accountResults.map((account) => (
|
||||||
|
<li>
|
||||||
|
<AccountBlock account={account} instance={instance} />
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
) : (
|
||||||
|
<p class="ui-state">No accounts found.</p>
|
||||||
|
)}
|
||||||
|
<h2 class="timeline-header">Hashtags</h2>
|
||||||
|
{hashtagResults.length > 0 ? (
|
||||||
|
<ul class="link-list hashtag-list">
|
||||||
|
{hashtagResults.map((hashtag) => (
|
||||||
|
<li>
|
||||||
|
<Link
|
||||||
|
to={
|
||||||
|
instance
|
||||||
|
? `/${instance}/t/${hashtag.name}`
|
||||||
|
: `/t/${hashtag.name}`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Icon icon="hashtag" />
|
||||||
|
<span>{hashtag.name}</span>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
) : (
|
||||||
|
<p class="ui-state">No hashtags found.</p>
|
||||||
|
)}
|
||||||
|
<h2 class="timeline-header">Posts</h2>
|
||||||
|
{statusResults.length > 0 ? (
|
||||||
|
<ul class="timeline">
|
||||||
|
{statusResults.map((status) => (
|
||||||
|
<li>
|
||||||
|
<Link
|
||||||
|
class="status-link"
|
||||||
|
to={
|
||||||
|
instance
|
||||||
|
? `/${instance}/s/${status.id}`
|
||||||
|
: `/s/${status.id}`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Status status={status} />
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
) : (
|
||||||
|
<p class="ui-state">No posts found.</p>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : uiState === 'loading' ? (
|
||||||
|
<p class="ui-state">
|
||||||
|
<Loader abrupt />
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<p class="ui-state">
|
||||||
|
Enter your search term or paste a URL above to get started.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Search;
|
|
@ -10,6 +10,7 @@ import Icon from '../components/icon';
|
||||||
import Link from '../components/link';
|
import Link from '../components/link';
|
||||||
import NameText from '../components/name-text';
|
import NameText from '../components/name-text';
|
||||||
import RelativeTime from '../components/relative-time';
|
import RelativeTime from '../components/relative-time';
|
||||||
|
import { api } from '../utils/api';
|
||||||
import states from '../utils/states';
|
import states from '../utils/states';
|
||||||
import store from '../utils/store';
|
import store from '../utils/store';
|
||||||
|
|
||||||
|
@ -20,6 +21,7 @@ import store from '../utils/store';
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function Settings({ onClose }) {
|
function Settings({ onClose }) {
|
||||||
|
const { masto } = api();
|
||||||
const snapStates = useSnapshot(states);
|
const snapStates = useSnapshot(states);
|
||||||
// Accounts
|
// Accounts
|
||||||
const accounts = store.local.getJSON('accounts');
|
const accounts = store.local.getJSON('accounts');
|
||||||
|
@ -123,13 +125,12 @@ function Settings({ onClose }) {
|
||||||
<MenuItem
|
<MenuItem
|
||||||
disabled={!isCurrent}
|
disabled={!isCurrent}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const yes = confirm(
|
const yes = confirm('Log out?');
|
||||||
'Are you sure you want to log out?',
|
|
||||||
);
|
|
||||||
if (!yes) return;
|
if (!yes) return;
|
||||||
accounts.splice(i, 1);
|
accounts.splice(i, 1);
|
||||||
store.local.setJSON('accounts', accounts);
|
store.local.setJSON('accounts', accounts);
|
||||||
location.reload();
|
// location.reload();
|
||||||
|
location.href = '/';
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Log out
|
Log out
|
||||||
|
@ -178,7 +179,10 @@ function Settings({ onClose }) {
|
||||||
}
|
}
|
||||||
document
|
document
|
||||||
.querySelector('meta[name="color-scheme"]')
|
.querySelector('meta[name="color-scheme"]')
|
||||||
.setAttribute('content', theme);
|
.setAttribute(
|
||||||
|
'content',
|
||||||
|
theme === 'auto' ? 'dark light' : theme,
|
||||||
|
);
|
||||||
|
|
||||||
if (theme === 'auto') {
|
if (theme === 'auto') {
|
||||||
store.local.del('theme');
|
store.local.del('theme');
|
||||||
|
|
|
@ -2,8 +2,6 @@
|
||||||
grid-column: 1 / 3;
|
grid-column: 1 / 3;
|
||||||
}
|
}
|
||||||
.status-deck header {
|
.status-deck header {
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.status-deck header h1 {
|
.status-deck header h1 {
|
||||||
|
@ -21,7 +19,6 @@
|
||||||
.hero-heading {
|
.hero-heading {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-bottom: 0.25em;
|
|
||||||
}
|
}
|
||||||
.hero-heading .icon {
|
.hero-heading .icon {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
@ -38,3 +35,21 @@
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.post-status-banner {
|
||||||
|
position: sticky;
|
||||||
|
bottom: 16px;
|
||||||
|
bottom: max(16px, env(safe-area-inset-bottom));
|
||||||
|
font-size: 90%;
|
||||||
|
background-color: var(--bg-faded-color);
|
||||||
|
padding: 16px;
|
||||||
|
margin: 0 16px;
|
||||||
|
border-radius: 16px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
line-height: 1.2;
|
||||||
|
max-width: var(--main-width);
|
||||||
|
}
|
||||||
|
.post-status-banner > p:first-of-type {
|
||||||
|
margin-top: 0;
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ import pRetry from 'p-retry';
|
||||||
import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
|
import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { InView } from 'react-intersection-observer';
|
import { InView } from 'react-intersection-observer';
|
||||||
import { useLocation, useNavigate, useParams } from 'react-router-dom';
|
import { matchPath, useNavigate, useParams } from 'react-router-dom';
|
||||||
import { useDebouncedCallback } from 'use-debounce';
|
import { useDebouncedCallback } from 'use-debounce';
|
||||||
import { useSnapshot } from 'valtio';
|
import { useSnapshot } from 'valtio';
|
||||||
|
|
||||||
|
@ -17,9 +17,14 @@ import Loader from '../components/loader';
|
||||||
import NameText from '../components/name-text';
|
import NameText from '../components/name-text';
|
||||||
import RelativeTime from '../components/relative-time';
|
import RelativeTime from '../components/relative-time';
|
||||||
import Status from '../components/status';
|
import Status from '../components/status';
|
||||||
|
import { api } from '../utils/api';
|
||||||
import htmlContentLength from '../utils/html-content-length';
|
import htmlContentLength from '../utils/html-content-length';
|
||||||
import shortenNumber from '../utils/shorten-number';
|
import shortenNumber from '../utils/shorten-number';
|
||||||
import states, { saveStatus, threadifyStatus } from '../utils/states';
|
import states, {
|
||||||
|
saveStatus,
|
||||||
|
statusKey,
|
||||||
|
threadifyStatus,
|
||||||
|
} from '../utils/states';
|
||||||
import { getCurrentAccount } from '../utils/store-utils';
|
import { getCurrentAccount } from '../utils/store-utils';
|
||||||
import useScroll from '../utils/useScroll';
|
import useScroll from '../utils/useScroll';
|
||||||
import useTitle from '../utils/useTitle';
|
import useTitle from '../utils/useTitle';
|
||||||
|
@ -34,13 +39,16 @@ function resetScrollPosition(id) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function StatusPage() {
|
function StatusPage() {
|
||||||
const { id } = useParams();
|
const { id, ...params } = useParams();
|
||||||
const location = useLocation();
|
const { masto, instance, authenticated } = api({ instance: params.instance });
|
||||||
|
const { masto: currentMasto, instance: currentInstance } = api();
|
||||||
|
const sameInstance = instance === currentInstance;
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const snapStates = useSnapshot(states);
|
const snapStates = useSnapshot(states);
|
||||||
const [statuses, setStatuses] = useState([]);
|
const [statuses, setStatuses] = useState([]);
|
||||||
const [uiState, setUIState] = useState('default');
|
const [uiState, setUIState] = useState('default');
|
||||||
const heroStatusRef = useRef();
|
const heroStatusRef = useRef();
|
||||||
|
const sKey = statusKey(id, instance);
|
||||||
|
|
||||||
const scrollableRef = useRef();
|
const scrollableRef = useRef();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -75,7 +83,7 @@ function StatusPage() {
|
||||||
if (cachedStatuses) {
|
if (cachedStatuses) {
|
||||||
// Case 1: It's cached, let's restore them to make it snappy
|
// Case 1: It's cached, let's restore them to make it snappy
|
||||||
const reallyCachedStatuses = cachedStatuses.filter(
|
const reallyCachedStatuses = cachedStatuses.filter(
|
||||||
(s) => states.statuses[s.id],
|
(s) => states.statuses[sKey],
|
||||||
// Some are not cached in the global state, so we need to filter them out
|
// Some are not cached in the global state, so we need to filter them out
|
||||||
);
|
);
|
||||||
setStatuses(reallyCachedStatuses);
|
setStatuses(reallyCachedStatuses);
|
||||||
|
@ -100,14 +108,14 @@ function StatusPage() {
|
||||||
retries: 8,
|
retries: 8,
|
||||||
});
|
});
|
||||||
|
|
||||||
const hasStatus = !!snapStates.statuses[id];
|
const hasStatus = !!snapStates.statuses[sKey];
|
||||||
let heroStatus = snapStates.statuses[id];
|
let heroStatus = snapStates.statuses[sKey];
|
||||||
if (hasStatus) {
|
if (hasStatus) {
|
||||||
console.debug('Hero status is cached');
|
console.debug('Hero status is cached');
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
heroStatus = await heroFetch();
|
heroStatus = await heroFetch();
|
||||||
saveStatus(heroStatus);
|
saveStatus(heroStatus, instance);
|
||||||
// Give time for context to appear
|
// Give time for context to appear
|
||||||
await new Promise((resolve) => {
|
await new Promise((resolve) => {
|
||||||
setTimeout(resolve, 100);
|
setTimeout(resolve, 100);
|
||||||
|
@ -124,11 +132,15 @@ function StatusPage() {
|
||||||
const { ancestors, descendants } = context;
|
const { ancestors, descendants } = context;
|
||||||
|
|
||||||
ancestors.forEach((status) => {
|
ancestors.forEach((status) => {
|
||||||
states.statuses[status.id] = status;
|
saveStatus(status, instance, {
|
||||||
|
skipThreading: true,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
const nestedDescendants = [];
|
const nestedDescendants = [];
|
||||||
descendants.forEach((status) => {
|
descendants.forEach((status) => {
|
||||||
states.statuses[status.id] = status;
|
saveStatus(status, instance, {
|
||||||
|
skipThreading: true,
|
||||||
|
});
|
||||||
if (status.inReplyToAccountId === status.account.id) {
|
if (status.inReplyToAccountId === status.account.id) {
|
||||||
// If replying to self, it's part of the thread, level 1
|
// If replying to self, it's part of the thread, level 1
|
||||||
nestedDescendants.push(status);
|
nestedDescendants.push(status);
|
||||||
|
@ -199,7 +211,7 @@ function StatusPage() {
|
||||||
// Let's threadify this one
|
// Let's threadify this one
|
||||||
// Note that all non-hero statuses will trigger saveStatus which will threadify them too
|
// Note that all non-hero statuses will trigger saveStatus which will threadify them too
|
||||||
// By right, at this point, all descendant statuses should be cached
|
// By right, at this point, all descendant statuses should be cached
|
||||||
threadifyStatus(heroStatus);
|
threadifyStatus(heroStatus, instance);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
setUIState('error');
|
setUIState('error');
|
||||||
|
@ -211,7 +223,7 @@ function StatusPage() {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(initContext, [id]);
|
useEffect(initContext, [id, masto]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!statuses.length) return;
|
if (!statuses.length) return;
|
||||||
console.debug('STATUSES', statuses);
|
console.debug('STATUSES', statuses);
|
||||||
|
@ -277,7 +289,7 @@ function StatusPage() {
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const heroStatus = snapStates.statuses[id];
|
const heroStatus = snapStates.statuses[sKey] || snapStates.statuses[id];
|
||||||
const heroDisplayName = useMemo(() => {
|
const heroDisplayName = useMemo(() => {
|
||||||
// Remove shortcodes from display name
|
// Remove shortcodes from display name
|
||||||
if (!heroStatus) return '';
|
if (!heroStatus) return '';
|
||||||
|
@ -308,12 +320,20 @@ function StatusPage() {
|
||||||
heroDisplayName && heroContentText
|
heroDisplayName && heroContentText
|
||||||
? `${heroDisplayName}: "${heroContentText}"`
|
? `${heroDisplayName}: "${heroContentText}"`
|
||||||
: 'Status',
|
: 'Status',
|
||||||
'/s/:id',
|
'/:instance?/s/:id',
|
||||||
);
|
);
|
||||||
|
|
||||||
const closeLink = useMemo(() => {
|
const closeLink = useMemo(() => {
|
||||||
const pathname = snapStates.prevLocation?.pathname;
|
const { prevLocation } = snapStates;
|
||||||
if (!pathname || pathname.startsWith('/s/')) return '/';
|
const pathname =
|
||||||
|
(prevLocation?.pathname || '') + (prevLocation?.search || '');
|
||||||
|
if (
|
||||||
|
!pathname ||
|
||||||
|
matchPath('/:instance/s/:id', pathname) ||
|
||||||
|
matchPath('/s/:id', pathname)
|
||||||
|
) {
|
||||||
|
return '/';
|
||||||
|
}
|
||||||
return pathname;
|
return pathname;
|
||||||
}, []);
|
}, []);
|
||||||
const onClose = () => {
|
const onClose = () => {
|
||||||
|
@ -458,11 +478,17 @@ function StatusPage() {
|
||||||
<Icon icon="chevron-left" size="xl" />
|
<Icon icon="chevron-left" size="xl" />
|
||||||
</Link>
|
</Link>
|
||||||
</div> */}
|
</div> */}
|
||||||
|
<div class="header-grid header-grid-2">
|
||||||
<h1>
|
<h1>
|
||||||
{!heroInView && heroStatus && uiState !== 'loading' ? (
|
{!heroInView && heroStatus && uiState !== 'loading' ? (
|
||||||
<>
|
<>
|
||||||
<span class="hero-heading">
|
<span class="hero-heading">
|
||||||
<NameText showAvatar account={heroStatus.account} short />{' '}
|
<NameText
|
||||||
|
account={heroStatus.account}
|
||||||
|
instance={instance}
|
||||||
|
showAvatar
|
||||||
|
short
|
||||||
|
/>{' '}
|
||||||
<span class="insignificant">
|
<span class="insignificant">
|
||||||
•{' '}
|
•{' '}
|
||||||
<RelativeTime
|
<RelativeTime
|
||||||
|
@ -515,7 +541,9 @@ function StatusPage() {
|
||||||
)}
|
)}
|
||||||
</h1>
|
</h1>
|
||||||
<div class="header-side">
|
<div class="header-side">
|
||||||
<Loader hidden={uiState !== 'loading'} />
|
{uiState === 'loading' ? (
|
||||||
|
<Loader abrupt />
|
||||||
|
) : (
|
||||||
<Menu
|
<Menu
|
||||||
align="end"
|
align="end"
|
||||||
portal={{
|
portal={{
|
||||||
|
@ -541,9 +569,11 @@ function StatusPage() {
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="eye-open" /> <span>Show all sensitive content</span>
|
<Icon icon="eye-open" />{' '}
|
||||||
|
<span>Show all sensitive content</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</Menu>
|
</Menu>
|
||||||
|
)}
|
||||||
<Link
|
<Link
|
||||||
class="button plain deck-close"
|
class="button plain deck-close"
|
||||||
to={closeLink}
|
to={closeLink}
|
||||||
|
@ -552,6 +582,7 @@ function StatusPage() {
|
||||||
<Icon icon="x" size="xl" />
|
<Icon icon="x" size="xl" />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
{!!statuses.length && heroStatus ? (
|
{!!statuses.length && heroStatus ? (
|
||||||
<ul
|
<ul
|
||||||
|
@ -577,24 +608,85 @@ function StatusPage() {
|
||||||
} ${thread ? 'thread' : ''} ${isHero ? 'hero' : ''}`}
|
} ${thread ? 'thread' : ''} ${isHero ? 'hero' : ''}`}
|
||||||
>
|
>
|
||||||
{isHero ? (
|
{isHero ? (
|
||||||
|
<>
|
||||||
<InView
|
<InView
|
||||||
threshold={0.1}
|
threshold={0.1}
|
||||||
onChange={onView}
|
onChange={onView}
|
||||||
class="status-focus"
|
class="status-focus"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
<Status statusID={statusID} withinContext size="l" />
|
<Status
|
||||||
|
statusID={statusID}
|
||||||
|
instance={instance}
|
||||||
|
withinContext
|
||||||
|
size="l"
|
||||||
|
/>
|
||||||
</InView>
|
</InView>
|
||||||
|
{!sameInstance && uiState !== 'loading' && (
|
||||||
|
<div class="post-status-banner">
|
||||||
|
<p>
|
||||||
|
This post is from another instance (
|
||||||
|
<b>{instance}</b>). Interactions (reply, boost, etc)
|
||||||
|
are not possible.
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const results = await currentMasto.v2.search({
|
||||||
|
q: heroStatus.url,
|
||||||
|
type: 'statuses',
|
||||||
|
resolve: true,
|
||||||
|
limit: 1,
|
||||||
|
});
|
||||||
|
if (results.statuses.length) {
|
||||||
|
const status = results.statuses[0];
|
||||||
|
navigate(`/s/${status.id}`);
|
||||||
|
} else {
|
||||||
|
throw new Error('No results');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
alert('Error: ' + e);
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon="transfer" /> Switch to my instance to
|
||||||
|
enable interactions
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{sameInstance &&
|
||||||
|
!authenticated &&
|
||||||
|
uiState !== 'loading' && (
|
||||||
|
<div class="post-status-banner">
|
||||||
|
<p>
|
||||||
|
You're not logged in. Interactions (reply, boost,
|
||||||
|
etc) are not possible.
|
||||||
|
</p>
|
||||||
|
<Link to="/login" class="button">
|
||||||
|
Log in
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Link
|
<Link
|
||||||
class="status-link"
|
class="status-link"
|
||||||
to={`/s/${statusID}`}
|
to={
|
||||||
|
instance
|
||||||
|
? `/${instance}/s/${statusID}`
|
||||||
|
: `/s/${statusID}`
|
||||||
|
}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
resetScrollPosition(statusID);
|
resetScrollPosition(statusID);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Status
|
<Status
|
||||||
statusID={statusID}
|
statusID={statusID}
|
||||||
|
instance={instance}
|
||||||
withinContext
|
withinContext
|
||||||
size={thread || ancestor ? 'm' : 's'}
|
size={thread || ancestor ? 'm' : 's'}
|
||||||
/>
|
/>
|
||||||
|
@ -610,8 +702,10 @@ function StatusPage() {
|
||||||
)}
|
)}
|
||||||
{descendant && replies?.length > 0 && (
|
{descendant && replies?.length > 0 && (
|
||||||
<SubComments
|
<SubComments
|
||||||
|
instance={instance}
|
||||||
hasManyStatuses={hasManyStatuses}
|
hasManyStatuses={hasManyStatuses}
|
||||||
replies={replies}
|
replies={replies}
|
||||||
|
hasParentThread={thread}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{uiState === 'loading' &&
|
{uiState === 'loading' &&
|
||||||
|
@ -691,7 +785,7 @@ function StatusPage() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SubComments({ hasManyStatuses, replies }) {
|
function SubComments({ hasManyStatuses, replies, instance, hasParentThread }) {
|
||||||
// Set isBrief = true:
|
// Set isBrief = true:
|
||||||
// - if less than or 2 replies
|
// - if less than or 2 replies
|
||||||
// - if replies have no sub-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)
|
.filter((a, i, arr) => arr.findIndex((b) => b.id === a.id) === i)
|
||||||
.slice(0, 3);
|
.slice(0, 3);
|
||||||
|
|
||||||
const open = isBrief || !hasManyStatuses;
|
const open =
|
||||||
|
(!hasParentThread || replies.length === 1) && (isBrief || !hasManyStatuses);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<details class="replies" open={open}>
|
<details class="replies" open={open}>
|
||||||
|
@ -764,12 +859,17 @@ function SubComments({ hasManyStatuses, replies }) {
|
||||||
<li key={r.id}>
|
<li key={r.id}>
|
||||||
<Link
|
<Link
|
||||||
class="status-link"
|
class="status-link"
|
||||||
to={`/s/${r.id}`}
|
to={instance ? `/${instance}/s/${r.id}` : `/s/${r.id}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
resetScrollPosition(r.id);
|
resetScrollPosition(r.id);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Status statusID={r.id} withinContext size="s" />
|
<Status
|
||||||
|
statusID={r.id}
|
||||||
|
instance={instance}
|
||||||
|
withinContext
|
||||||
|
size="s"
|
||||||
|
/>
|
||||||
{!r.replies?.length && r.repliesCount > 0 && (
|
{!r.replies?.length && r.repliesCount > 0 && (
|
||||||
<div class="replies-link">
|
<div class="replies-link">
|
||||||
<Icon icon="comment" />{' '}
|
<Icon icon="comment" />{' '}
|
||||||
|
@ -781,6 +881,7 @@ function SubComments({ hasManyStatuses, replies }) {
|
||||||
</Link>
|
</Link>
|
||||||
{r.replies?.length && (
|
{r.replies?.length && (
|
||||||
<SubComments
|
<SubComments
|
||||||
|
instance={instance}
|
||||||
hasManyStatuses={hasManyStatuses}
|
hasManyStatuses={hasManyStatuses}
|
||||||
replies={r.replies}
|
replies={r.replies}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -2,6 +2,7 @@ import './welcome.css';
|
||||||
|
|
||||||
import logo from '../assets/logo.svg';
|
import logo from '../assets/logo.svg';
|
||||||
import Link from '../components/link';
|
import Link from '../components/link';
|
||||||
|
import states from '../utils/states';
|
||||||
import useTitle from '../utils/useTitle';
|
import useTitle from '../utils/useTitle';
|
||||||
|
|
||||||
function Welcome() {
|
function Welcome() {
|
||||||
|
@ -41,7 +42,14 @@ function Welcome() {
|
||||||
Built
|
Built
|
||||||
</a>{' '}
|
</a>{' '}
|
||||||
by{' '}
|
by{' '}
|
||||||
<a href="https://mastodon.social/@cheeaun" target="_blank">
|
<a
|
||||||
|
href="https://mastodon.social/@cheeaun"
|
||||||
|
target="_blank"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
states.showAccount = 'cheeaun@mastodon.social';
|
||||||
|
}}
|
||||||
|
>
|
||||||
@cheeaun
|
@cheeaun
|
||||||
</a>
|
</a>
|
||||||
.{' '}
|
.{' '}
|
||||||
|
|
189
src/utils/api.js
Normal file
189
src/utils/api.js
Normal file
|
@ -0,0 +1,189 @@
|
||||||
|
import { createClient } from 'masto';
|
||||||
|
|
||||||
|
import store from './store';
|
||||||
|
import { getAccount, getCurrentAccount, saveAccount } from './store-utils';
|
||||||
|
|
||||||
|
// Default *fallback* instance
|
||||||
|
const DEFAULT_INSTANCE = 'mastodon.social';
|
||||||
|
|
||||||
|
// Per-instance masto instance
|
||||||
|
// Useful when only one account is logged in
|
||||||
|
// I'm not sure if I'll ever allow multiple logged-in accounts but oh well...
|
||||||
|
// E.g. apis['mastodon.social']
|
||||||
|
const apis = {};
|
||||||
|
|
||||||
|
// Per-account masto instance
|
||||||
|
// Note: There can be many accounts per instance
|
||||||
|
// Useful when multiple accounts are logged in or when certain actions require a specific account
|
||||||
|
// Just in case if I need this one day.
|
||||||
|
// E.g. accountApis['mastodon.social']['ACCESS_TOKEN']
|
||||||
|
const accountApis = {};
|
||||||
|
|
||||||
|
// Current account masto instance
|
||||||
|
let currentAccountApi;
|
||||||
|
|
||||||
|
export function initClient({ instance, accessToken }) {
|
||||||
|
if (/^https?:\/\//.test(instance)) {
|
||||||
|
instance = instance
|
||||||
|
.replace(/^https?:\/\//, '')
|
||||||
|
.replace(/\/+$/, '')
|
||||||
|
.toLowerCase();
|
||||||
|
}
|
||||||
|
const url = instance ? `https://${instance}` : `https://${DEFAULT_INSTANCE}`;
|
||||||
|
|
||||||
|
const client = createClient({
|
||||||
|
url,
|
||||||
|
accessToken, // Can be null
|
||||||
|
disableVersionCheck: true, // Allow non-Mastodon instances
|
||||||
|
timeout: 30_000, // Unfortunatly this is global instead of per-request
|
||||||
|
});
|
||||||
|
client.__instance__ = instance;
|
||||||
|
|
||||||
|
apis[instance] = client;
|
||||||
|
if (!accountApis[instance]) accountApis[instance] = {};
|
||||||
|
if (accessToken) accountApis[instance][accessToken] = client;
|
||||||
|
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the instance information
|
||||||
|
// The config is needed for composing
|
||||||
|
export async function initInstance(client) {
|
||||||
|
const masto = client;
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
// This is a weird place to put this but here's updating the masto instance with the streaming API URL set in the configuration
|
||||||
|
// Reason: Streaming WebSocket URL may change, unlike the standard API REST URLs
|
||||||
|
if (streamingApi || streaming) {
|
||||||
|
console.log('🎏 Streaming API URL:', streaming || streamingApi);
|
||||||
|
masto.config.props.streamingApiUrl = streaming || streamingApi;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the account information and store it
|
||||||
|
export async function initAccount(client, instance, accessToken) {
|
||||||
|
const masto = client;
|
||||||
|
const mastoAccount = await masto.v1.accounts.verifyCredentials();
|
||||||
|
|
||||||
|
saveAccount({
|
||||||
|
info: mastoAccount,
|
||||||
|
instanceURL: instance.toLowerCase(),
|
||||||
|
accessToken,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get preferences
|
||||||
|
export async function initPreferences(client) {
|
||||||
|
try {
|
||||||
|
const masto = client;
|
||||||
|
const preferences = await masto.v1.preferences.fetch();
|
||||||
|
store.account.set('preferences', preferences);
|
||||||
|
} catch (e) {
|
||||||
|
// silently fail
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the masto instance
|
||||||
|
// If accountID is provided, get the masto instance for that account
|
||||||
|
export function api({ instance, accessToken, accountID, account } = {}) {
|
||||||
|
// If instance and accessToken are provided, get the masto instance for that account
|
||||||
|
if (instance && accessToken) {
|
||||||
|
return {
|
||||||
|
masto:
|
||||||
|
accountApis[instance]?.[accessToken] ||
|
||||||
|
initClient({ instance, accessToken }),
|
||||||
|
authenticated: true,
|
||||||
|
instance,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// If account is provided, get the masto instance for that account
|
||||||
|
if (account || accountID) {
|
||||||
|
account = account || getAccount(accountID);
|
||||||
|
if (account) {
|
||||||
|
const accessToken = account.accessToken;
|
||||||
|
const instance = account.instanceURL;
|
||||||
|
return {
|
||||||
|
masto:
|
||||||
|
accountApis[instance]?.[accessToken] ||
|
||||||
|
initClient({ instance, accessToken }),
|
||||||
|
authenticated: true,
|
||||||
|
instance,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
throw new Error(`Account ${accountID} not found`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If only instance is provided, get the masto instance for that instance
|
||||||
|
if (instance) {
|
||||||
|
const masto = apis[instance] || initClient({ instance });
|
||||||
|
return {
|
||||||
|
masto,
|
||||||
|
authenticated: !!masto.config.props.accessToken,
|
||||||
|
instance,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no instance is provided, get the masto instance for the current account
|
||||||
|
if (currentAccountApi)
|
||||||
|
return {
|
||||||
|
masto: currentAccountApi,
|
||||||
|
authenticated: true,
|
||||||
|
instance: currentAccountApi.__instance__,
|
||||||
|
};
|
||||||
|
const currentAccount = getCurrentAccount();
|
||||||
|
if (currentAccount) {
|
||||||
|
const { accessToken, instanceURL: instance } = currentAccount;
|
||||||
|
currentAccountApi =
|
||||||
|
accountApis[instance]?.[accessToken] ||
|
||||||
|
initClient({ instance, accessToken });
|
||||||
|
return {
|
||||||
|
masto: currentAccountApi,
|
||||||
|
authenticated: true,
|
||||||
|
instance,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no instance is provided and no account is logged in, get the masto instance for DEFAULT_INSTANCE
|
||||||
|
return {
|
||||||
|
masto: apis[DEFAULT_INSTANCE] || initClient({ instance: DEFAULT_INSTANCE }),
|
||||||
|
authenticated: false,
|
||||||
|
instance: DEFAULT_INSTANCE,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
window.__API__ = {
|
||||||
|
currentAccountApi,
|
||||||
|
apis,
|
||||||
|
accountApis,
|
||||||
|
};
|
|
@ -43,6 +43,43 @@ function enhanceContent(content, opts = {}) {
|
||||||
block.replaceWith(pre);
|
block.replaceWith(pre);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Convert multi-paragraph code blocks to <pre><code>code</code></pre>
|
||||||
|
const paragraphs = Array.from(dom.querySelectorAll('p'));
|
||||||
|
// Filter out paragraphs with ``` in beginning only
|
||||||
|
const codeBlocks = paragraphs.filter((p) => /^```/g.test(p.innerText));
|
||||||
|
// For each codeBlocks, get all paragraphs until the last paragraph with ``` at the end only
|
||||||
|
codeBlocks.forEach((block) => {
|
||||||
|
const nextParagraphs = [block];
|
||||||
|
let hasCodeBlock = false;
|
||||||
|
let currentBlock = block;
|
||||||
|
while (currentBlock.nextElementSibling) {
|
||||||
|
const next = currentBlock.nextElementSibling;
|
||||||
|
if (next && next.tagName === 'P') {
|
||||||
|
if (/```$/g.test(next.innerText)) {
|
||||||
|
nextParagraphs.push(next);
|
||||||
|
hasCodeBlock = true;
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
nextParagraphs.push(next);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
currentBlock = next;
|
||||||
|
}
|
||||||
|
if (hasCodeBlock) {
|
||||||
|
const pre = document.createElement('pre');
|
||||||
|
nextParagraphs.forEach((p) => {
|
||||||
|
// Replace <br /> with newlines
|
||||||
|
p.querySelectorAll('br').forEach((br) => br.replaceWith('\n'));
|
||||||
|
});
|
||||||
|
const codeText = nextParagraphs.map((p) => p.innerHTML).join('\n\n');
|
||||||
|
pre.innerHTML = `<code>${codeText}</code>`;
|
||||||
|
block.replaceWith(pre);
|
||||||
|
nextParagraphs.forEach((p) => p.remove());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// INLINE CODE
|
// INLINE CODE
|
||||||
// ===========
|
// ===========
|
||||||
// Convert `code` to <code>code</code>
|
// Convert `code` to <code>code</code>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import states from './states';
|
import states from './states';
|
||||||
|
|
||||||
function handleContentLinks(opts) {
|
function handleContentLinks(opts) {
|
||||||
const { mentions = [] } = opts || {};
|
const { mentions = [], instance } = opts || {};
|
||||||
return (e) => {
|
return (e) => {
|
||||||
let { target } = e;
|
let { target } = e;
|
||||||
if (target.parentNode.tagName.toLowerCase() === 'a') {
|
if (target.parentNode.tagName.toLowerCase() === 'a') {
|
||||||
|
@ -25,13 +25,19 @@ function handleContentLinks(opts) {
|
||||||
if (mention) {
|
if (mention) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
states.showAccount = mention.acct;
|
states.showAccount = {
|
||||||
|
account: mention.acct,
|
||||||
|
instance,
|
||||||
|
};
|
||||||
} else if (!/^http/i.test(targetText)) {
|
} else if (!/^http/i.test(targetText)) {
|
||||||
console.log('mention not found', targetText);
|
console.log('mention not found', targetText);
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const href = target.getAttribute('href');
|
const href = target.getAttribute('href');
|
||||||
states.showAccount = href;
|
states.showAccount = {
|
||||||
|
account: href,
|
||||||
|
instance,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
} else if (
|
} else if (
|
||||||
target.tagName.toLowerCase() === 'a' &&
|
target.tagName.toLowerCase() === 'a' &&
|
||||||
|
@ -40,7 +46,9 @@ function handleContentLinks(opts) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const tag = target.innerText.replace(/^#/, '').trim();
|
const tag = target.innerText.replace(/^#/, '').trim();
|
||||||
location.hash = `#/t/${tag}`;
|
const hashURL = instance ? `#/${instance}/t/${tag}` : `#/t/${tag}`;
|
||||||
|
console.log({ hashURL });
|
||||||
|
location.hash = hashURL;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,9 +13,9 @@ export default function openCompose(opts) {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (newWin) {
|
if (newWin) {
|
||||||
if (masto) {
|
// if (masto) {
|
||||||
newWin.masto = masto;
|
// newWin.masto = masto;
|
||||||
}
|
// }
|
||||||
|
|
||||||
newWin.__COMPOSE__ = opts;
|
newWin.__COMPOSE__ = opts;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import { proxy, subscribe } from 'valtio';
|
import { proxy, subscribe } from 'valtio';
|
||||||
|
import { subscribeKey } from 'valtio/utils';
|
||||||
|
|
||||||
|
import { api } from './api';
|
||||||
import store from './store';
|
import store from './store';
|
||||||
|
|
||||||
const states = proxy({
|
const states = proxy({
|
||||||
|
@ -14,8 +16,9 @@ const states = proxy({
|
||||||
homeLast: null, // Last item in 'home' list
|
homeLast: null, // Last item in 'home' list
|
||||||
homeLastFetchTime: null,
|
homeLastFetchTime: null,
|
||||||
notifications: [],
|
notifications: [],
|
||||||
notificationLast: null, // Last item in 'notifications' list
|
notificationsLast: store.account.get('notificationsLast') || null, // Last read notification
|
||||||
notificationsNew: [],
|
notificationsNew: [],
|
||||||
|
notificationsShowNew: false,
|
||||||
notificationsLastFetchTime: null,
|
notificationsLastFetchTime: null,
|
||||||
accounts: {},
|
accounts: {},
|
||||||
reloadStatusPage: 0,
|
reloadStatusPage: 0,
|
||||||
|
@ -27,38 +30,80 @@ const states = proxy({
|
||||||
showAccount: false,
|
showAccount: false,
|
||||||
showDrafts: false,
|
showDrafts: false,
|
||||||
showMediaModal: false,
|
showMediaModal: false,
|
||||||
composeCharacterCount: 0,
|
showShortcutsSettings: false,
|
||||||
|
// Shortcuts
|
||||||
|
shortcuts: store.account.get('shortcuts') ?? [],
|
||||||
|
// Settings
|
||||||
settings: {
|
settings: {
|
||||||
boostsCarousel: store.local.get('settings:boostsCarousel')
|
shortcutsColumnsMode:
|
||||||
? store.local.get('settings:boostsCarousel') === '1'
|
store.account.get('settings-shortcutsColumnsMode') ?? false,
|
||||||
: true,
|
boostsCarousel: store.account.get('settings-boostsCarousel') ?? true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default states;
|
export default states;
|
||||||
|
|
||||||
subscribe(states.settings, () => {
|
subscribeKey(states, 'notificationsLast', (v) => {
|
||||||
store.local.set(
|
console.log('CHANGE', v);
|
||||||
'settings:boostsCarousel',
|
store.account.set('notificationsLast', states.notificationsLast);
|
||||||
states.settings.boostsCarousel ? '1' : '0',
|
});
|
||||||
);
|
subscribe(states, (v) => {
|
||||||
|
console.debug('STATES change', v);
|
||||||
|
const [action, path, value, prevValue] = v[0];
|
||||||
|
if (path.join('.') === 'settings.boostsCarousel') {
|
||||||
|
store.account.set('settings-boostsCarousel', !!value);
|
||||||
|
}
|
||||||
|
if (path.join('.') === 'settings.shortcutsColumnsMode') {
|
||||||
|
store.account.set('settings-shortcutsColumnsMode', !!value);
|
||||||
|
}
|
||||||
|
if (path?.[0] === 'shortcuts') {
|
||||||
|
store.account.set('shortcuts', states.shortcuts);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export function saveStatus(status, opts) {
|
export function hideAllModals() {
|
||||||
|
states.showCompose = false;
|
||||||
|
states.showSettings = false;
|
||||||
|
states.showAccount = false;
|
||||||
|
states.showDrafts = false;
|
||||||
|
states.showMediaModal = false;
|
||||||
|
states.showShortcutsSettings = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function statusKey(id, instance) {
|
||||||
|
return instance ? `${instance}/${id}` : id;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getStatus(statusID, instance) {
|
||||||
|
if (instance) {
|
||||||
|
const key = statusKey(statusID, instance);
|
||||||
|
return states.statuses[key];
|
||||||
|
}
|
||||||
|
return states.statuses[statusID];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveStatus(status, instance, opts) {
|
||||||
|
if (typeof instance === 'object') {
|
||||||
|
opts = instance;
|
||||||
|
instance = null;
|
||||||
|
}
|
||||||
const { override, skipThreading } = Object.assign(
|
const { override, skipThreading } = Object.assign(
|
||||||
{ override: true, skipThreading: false },
|
{ override: true, skipThreading: false },
|
||||||
opts,
|
opts,
|
||||||
);
|
);
|
||||||
if (!status) return;
|
if (!status) return;
|
||||||
if (!override && states.statuses[status.id]) return;
|
if (!override && getStatus(status.id)) return;
|
||||||
states.statuses[status.id] = status;
|
const key = statusKey(status.id, instance);
|
||||||
|
states.statuses[key] = status;
|
||||||
if (status.reblog) {
|
if (status.reblog) {
|
||||||
states.statuses[status.reblog.id] = status.reblog;
|
const key = statusKey(status.reblog.id, instance);
|
||||||
|
states.statuses[key] = status.reblog;
|
||||||
}
|
}
|
||||||
|
|
||||||
// THREAD TRAVERSER
|
// THREAD TRAVERSER
|
||||||
if (!skipThreading) {
|
if (!skipThreading) {
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
threadifyStatus(status);
|
threadifyStatus(status, instance);
|
||||||
if (status.reblog) {
|
if (status.reblog) {
|
||||||
threadifyStatus(status.reblog);
|
threadifyStatus(status.reblog);
|
||||||
}
|
}
|
||||||
|
@ -66,7 +111,8 @@ export function saveStatus(status, opts) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function threadifyStatus(status) {
|
export function threadifyStatus(status, propInstance) {
|
||||||
|
const { masto, instance } = api({ instance: propInstance });
|
||||||
// Return all statuses in the thread, via inReplyToId, if inReplyToAccountId === account.id
|
// Return all statuses in the thread, via inReplyToId, if inReplyToAccountId === account.id
|
||||||
let fetchIndex = 0;
|
let fetchIndex = 0;
|
||||||
async function traverse(status, index = 0) {
|
async function traverse(status, index = 0) {
|
||||||
|
@ -78,12 +124,13 @@ export function threadifyStatus(status) {
|
||||||
throw 'Not a thread';
|
throw 'Not a thread';
|
||||||
// Possibly thread of replies by multiple people?
|
// Possibly thread of replies by multiple people?
|
||||||
}
|
}
|
||||||
let prevStatus = states.statuses[inReplyToId];
|
const key = statusKey(inReplyToId, instance);
|
||||||
|
let prevStatus = states.statuses[key];
|
||||||
if (!prevStatus) {
|
if (!prevStatus) {
|
||||||
if (fetchIndex++ > 3) throw 'Too many fetches for thread'; // Some people revive old threads
|
if (fetchIndex++ > 3) throw 'Too many fetches for thread'; // Some people revive old threads
|
||||||
await new Promise((r) => setTimeout(r, 500 * fetchIndex)); // Be nice to rate limits
|
await new Promise((r) => setTimeout(r, 500 * fetchIndex)); // Be nice to rate limits
|
||||||
prevStatus = await masto.v1.statuses.fetch(inReplyToId);
|
prevStatus = await masto.v1.statuses.fetch(inReplyToId);
|
||||||
saveStatus(prevStatus, { skipThreading: true });
|
saveStatus(prevStatus, instance, { skipThreading: true });
|
||||||
}
|
}
|
||||||
// Prepend so that first status in thread will be index 0
|
// Prepend so that first status in thread will be index 0
|
||||||
return [...(await traverse(prevStatus, ++index)), status];
|
return [...(await traverse(prevStatus, ++index)), status];
|
||||||
|
@ -93,7 +140,8 @@ export function threadifyStatus(status) {
|
||||||
if (statuses.length > 1) {
|
if (statuses.length > 1) {
|
||||||
console.debug('THREAD', statuses);
|
console.debug('THREAD', statuses);
|
||||||
statuses.forEach((status, index) => {
|
statuses.forEach((status, index) => {
|
||||||
states.statusThreadNumber[status.id] = index + 1;
|
const key = statusKey(status.id, instance);
|
||||||
|
states.statusThreadNumber[key] = index + 1;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
import store from './store';
|
import store from './store';
|
||||||
|
|
||||||
export function getCurrentAccount() {
|
export function getAccount(id) {
|
||||||
const accounts = store.local.getJSON('accounts') || [];
|
const accounts = store.local.getJSON('accounts') || [];
|
||||||
|
return accounts.find((a) => a.info.id === id) || accounts[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCurrentAccount() {
|
||||||
const currentAccount = store.session.get('currentAccount');
|
const currentAccount = store.session.get('currentAccount');
|
||||||
const account =
|
const account = getAccount(currentAccount);
|
||||||
accounts.find((a) => a.info.id === currentAccount) || accounts[0];
|
|
||||||
return account;
|
return account;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,3 +19,35 @@ export function getCurrentAccountNS() {
|
||||||
} = account;
|
} = account;
|
||||||
return `${id}@${instanceURL}`;
|
return `${id}@${instanceURL}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function saveAccount(account) {
|
||||||
|
const accounts = store.local.getJSON('accounts') || [];
|
||||||
|
const acc = accounts.find((a) => a.info.id === account.info.id);
|
||||||
|
if (acc) {
|
||||||
|
acc.info = account.info;
|
||||||
|
acc.instanceURL = account.instanceURL;
|
||||||
|
acc.accessToken = account.accessToken;
|
||||||
|
} else {
|
||||||
|
accounts.push(account);
|
||||||
|
}
|
||||||
|
store.local.setJSON('accounts', accounts);
|
||||||
|
store.session.set('currentAccount', account.info.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentInstance = null;
|
||||||
|
export function getCurrentInstance() {
|
||||||
|
if (currentInstance) return currentInstance;
|
||||||
|
try {
|
||||||
|
const account = getCurrentAccount();
|
||||||
|
const instances = store.local.getJSON('instances');
|
||||||
|
const instance = account.instanceURL.toLowerCase();
|
||||||
|
return (currentInstance = instances[instance]);
|
||||||
|
} 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 {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { getCurrentAccountNS } from './store-utils';
|
||||||
|
|
||||||
const local = {
|
const local = {
|
||||||
get: (key) => {
|
get: (key) => {
|
||||||
try {
|
try {
|
||||||
|
@ -84,4 +86,36 @@ const session = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default { local, session };
|
// Store with account namespace (id@domain.tld) <- uses id, not username
|
||||||
|
const account = {
|
||||||
|
get: (key) => {
|
||||||
|
try {
|
||||||
|
return local.getJSON(key)[getCurrentAccountNS()];
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
set: (key, value) => {
|
||||||
|
try {
|
||||||
|
const data = local.getJSON(key) || {};
|
||||||
|
data[getCurrentAccountNS()] = value;
|
||||||
|
return local.setJSON(key, data);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
del: (key) => {
|
||||||
|
try {
|
||||||
|
const data = local.getJSON(key) || {};
|
||||||
|
delete data[getCurrentAccountNS()];
|
||||||
|
return local.setJSON(key, data);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default { local, session, account };
|
||||||
|
|
25
src/utils/supports.js
Normal file
25
src/utils/supports.js
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import { satisfies } from 'semver';
|
||||||
|
|
||||||
|
import features from '../data/features.json';
|
||||||
|
|
||||||
|
import { getCurrentInstance } from './store-utils';
|
||||||
|
|
||||||
|
const supportsCache = {};
|
||||||
|
|
||||||
|
function supports(feature) {
|
||||||
|
try {
|
||||||
|
const { version, domain } = getCurrentInstance();
|
||||||
|
const key = `${domain}-${feature}`;
|
||||||
|
if (supportsCache[key]) return supportsCache[key];
|
||||||
|
const range = features[feature];
|
||||||
|
if (!range) return false;
|
||||||
|
return (supportsCache[key] = satisfies(version, range, {
|
||||||
|
includePrerelease: true,
|
||||||
|
loose: true,
|
||||||
|
}));
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default supports;
|
|
@ -1,22 +1,25 @@
|
||||||
// useInterval with Preact
|
|
||||||
import { useEffect, useRef } from 'preact/hooks';
|
import { useEffect, useRef } from 'preact/hooks';
|
||||||
|
|
||||||
export default function useInterval(callback, delay) {
|
const noop = () => {};
|
||||||
const savedCallback = useRef();
|
|
||||||
|
function useInterval(callback, delay, immediate) {
|
||||||
|
const savedCallback = useRef(noop);
|
||||||
|
|
||||||
// Remember the latest callback.
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
savedCallback.current = callback;
|
savedCallback.current = callback;
|
||||||
}, [callback]);
|
}, []);
|
||||||
|
|
||||||
// Set up the interval.
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function tick() {
|
if (!immediate || delay === null || delay === false) return;
|
||||||
savedCallback.current();
|
savedCallback.current();
|
||||||
}
|
}, [immediate]);
|
||||||
if (delay !== null) {
|
|
||||||
let id = setInterval(tick, delay);
|
useEffect(() => {
|
||||||
|
if (delay === null || delay === false) return;
|
||||||
|
const tick = () => savedCallback.current();
|
||||||
|
const id = setInterval(tick, delay);
|
||||||
return () => clearInterval(id);
|
return () => clearInterval(id);
|
||||||
}
|
|
||||||
}, [delay]);
|
}, [delay]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default useInterval;
|
||||||
|
|
17
src/utils/usePageVisibility.js
Normal file
17
src/utils/usePageVisibility.js
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import { useEffect, useRef } from 'preact/hooks';
|
||||||
|
|
||||||
|
export default function usePageVisibility(fn = () => {}, deps = []) {
|
||||||
|
const savedCallback = useRef(fn, deps);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleVisibilityChange = () => {
|
||||||
|
const hidden = document.hidden || document.visibilityState === 'hidden';
|
||||||
|
console.log('👀 Page visibility changed', hidden ? 'hidden' : 'visible');
|
||||||
|
savedCallback.current(!hidden);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||||
|
return () =>
|
||||||
|
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||||
|
}, []);
|
||||||
|
}
|
|
@ -38,8 +38,16 @@ export default function useScroll({
|
||||||
const scrollDimension = isVertical ? scrollHeight : scrollWidth;
|
const scrollDimension = isVertical ? scrollHeight : scrollWidth;
|
||||||
const clientDimension = isVertical ? clientHeight : clientWidth;
|
const clientDimension = isVertical ? clientHeight : clientWidth;
|
||||||
const scrollDistance = Math.abs(scrollStart - previousScrollStart);
|
const scrollDistance = Math.abs(scrollStart - previousScrollStart);
|
||||||
const distanceFromStartPx = clientDimension * distanceFromStart;
|
const distanceFromStartPx = Math.min(
|
||||||
const distanceFromEndPx = clientDimension * distanceFromEnd;
|
clientDimension * distanceFromStart,
|
||||||
|
scrollDimension,
|
||||||
|
scrollStart,
|
||||||
|
);
|
||||||
|
const distanceFromEndPx = Math.min(
|
||||||
|
clientDimension * distanceFromEnd,
|
||||||
|
scrollDimension,
|
||||||
|
scrollDimension - scrollStart - clientDimension,
|
||||||
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
scrollDistance >=
|
scrollDistance >=
|
||||||
|
|
|
@ -8,8 +8,23 @@ const { VITE_CLIENT_NAME: CLIENT_NAME } = import.meta.env;
|
||||||
|
|
||||||
export default function useTitle(title, path) {
|
export default function useTitle(title, path) {
|
||||||
const snapStates = useSnapshot(states);
|
const snapStates = useSnapshot(states);
|
||||||
|
const { currentLocation } = snapStates;
|
||||||
|
let paths = [];
|
||||||
|
// Workaround for matchPath not working for optional path segments
|
||||||
|
// https://github.com/remix-run/react-router/discussions/9862
|
||||||
|
if (/:?\w+\?/.test(path)) {
|
||||||
|
paths.push(path.replace(/(:\w+)\?/g, '$1'));
|
||||||
|
paths.push(path.replace(/\/?:\w+\?/g, ''));
|
||||||
|
}
|
||||||
|
let matched = false;
|
||||||
|
if (paths.length) {
|
||||||
|
matched = paths.some((p) => matchPath(p, currentLocation));
|
||||||
|
} else if (path) {
|
||||||
|
matched = matchPath(path, currentLocation);
|
||||||
|
}
|
||||||
|
console.debug({ paths, matched, currentLocation });
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (path && !matchPath(path, snapStates.currentLocation)) return;
|
if (path && !matched) return;
|
||||||
document.title = title ? `${title} / ${CLIENT_NAME}` : CLIENT_NAME;
|
document.title = title ? `${title} / ${CLIENT_NAME}` : CLIENT_NAME;
|
||||||
}, [title, snapStates.currentLocation]);
|
}, [title, snapStates.currentLocation]);
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,9 @@ export default defineConfig({
|
||||||
preact(),
|
preact(),
|
||||||
splitVendorChunkPlugin(),
|
splitVendorChunkPlugin(),
|
||||||
VitePluginHtmlEnv(),
|
VitePluginHtmlEnv(),
|
||||||
removeConsole(),
|
removeConsole({
|
||||||
|
includes: ['log', 'debug', 'info', 'warn', 'error'],
|
||||||
|
}),
|
||||||
htmlPlugin({
|
htmlPlugin({
|
||||||
headScripts: ERROR_LOGGING ? [rollbarCode] : [],
|
headScripts: ERROR_LOGGING ? [rollbarCode] : [],
|
||||||
}),
|
}),
|
||||||
|
|
Loading…
Reference in a new issue