commit
e29f14bbcf
|
@ -105,6 +105,8 @@ And here I am. Building a Mastodon web client.
|
||||||
- [Mastodeck](https://mastodeck.com/)
|
- [Mastodeck](https://mastodeck.com/)
|
||||||
- [Trunks (alpha)](https://alpha.trunks.social/)
|
- [Trunks (alpha)](https://alpha.trunks.social/)
|
||||||
- [Tooty](https://github.com/n1k0/tooty)
|
- [Tooty](https://github.com/n1k0/tooty)
|
||||||
|
- [Litterbox](https://litterbox.koyu.space/)
|
||||||
|
- [Statuzer](https://statuzer.com/)
|
||||||
- [More...](https://github.com/hueyy/awesome-mastodon/#clients)
|
- [More...](https://github.com/hueyy/awesome-mastodon/#clients)
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Compose / Phanpy</title>
|
<title>Compose / %VITE_CLIENT_NAME%</title>
|
||||||
<meta name="color-scheme" content="dark light" />
|
<meta name="color-scheme" content="dark light" />
|
||||||
<meta name="google" content="notranslate" />
|
<meta name="google" content="notranslate" />
|
||||||
</head>
|
</head>
|
||||||
|
|
12
index.html
12
index.html
|
@ -6,7 +6,7 @@
|
||||||
name="viewport"
|
name="viewport"
|
||||||
content="width=device-width, initial-scale=1, viewport-fit=cover"
|
content="width=device-width, initial-scale=1, viewport-fit=cover"
|
||||||
/>
|
/>
|
||||||
<title><{ VITE_CLIENT_NAME }></title>
|
<title>%VITE_CLIENT_NAME%</title>
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
name="description"
|
||||||
content="Minimalistic opinionated Mastodon web client"
|
content="Minimalistic opinionated Mastodon web client"
|
||||||
|
@ -14,10 +14,10 @@
|
||||||
<meta name="color-scheme" content="dark light" />
|
<meta name="color-scheme" content="dark light" />
|
||||||
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||||
<meta name="apple-mobile-web-app-title" content="<{ VITE_CLIENT_NAME }>" />
|
<meta name="apple-mobile-web-app-title" content="%VITE_CLIENT_NAME%" />
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
<meta name="mobile-web-app-capable" content="yes" />
|
<meta name="mobile-web-app-capable" content="yes" />
|
||||||
<link rel="canonical" href="<{ VITE_WEBSITE }>" />
|
<link rel="canonical" href="%VITE_WEBSITE%" />
|
||||||
<meta
|
<meta
|
||||||
name="theme-color"
|
name="theme-color"
|
||||||
content="#fff"
|
content="#fff"
|
||||||
|
@ -33,13 +33,13 @@
|
||||||
|
|
||||||
<!-- Metacrap https://broken-links.com/2015/12/01/little-less-metacrap/ -->
|
<!-- Metacrap https://broken-links.com/2015/12/01/little-less-metacrap/ -->
|
||||||
<meta property="twitter:card" content="summary_large_image" />
|
<meta property="twitter:card" content="summary_large_image" />
|
||||||
<meta property="og:url" content="<{ VITE_WEBSITE }>" />
|
<meta property="og:url" content="%VITE_WEBSITE%" />
|
||||||
<meta property="og:title" content="<{ VITE_CLIENT_NAME }>" />
|
<meta property="og:title" content="%VITE_CLIENT_NAME%" />
|
||||||
<meta
|
<meta
|
||||||
property="og:description"
|
property="og:description"
|
||||||
content="Minimalistic opinionated Mastodon web client"
|
content="Minimalistic opinionated Mastodon web client"
|
||||||
/>
|
/>
|
||||||
<meta property="og:image" content="<{ VITE_WEBSITE }>/og-image.png" />
|
<meta property="og:image" content="%VITE_WEBSITE%/og-image.png" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|
471
package-lock.json
generated
471
package-lock.json
generated
|
@ -11,6 +11,7 @@
|
||||||
"@formatjs/intl-localematcher": "~0.2.32",
|
"@formatjs/intl-localematcher": "~0.2.32",
|
||||||
"@github/text-expander-element": "~2.3.0",
|
"@github/text-expander-element": "~2.3.0",
|
||||||
"@iconify-icons/mingcute": "~1.2.4",
|
"@iconify-icons/mingcute": "~1.2.4",
|
||||||
|
"@justinribeiro/lite-youtube": "~1.5.0",
|
||||||
"@szhsin/react-menu": "~3.5.2",
|
"@szhsin/react-menu": "~3.5.2",
|
||||||
"dayjs": "~1.11.7",
|
"dayjs": "~1.11.7",
|
||||||
"dayjs-twitter": "~0.5.0",
|
"dayjs-twitter": "~0.5.0",
|
||||||
|
@ -23,8 +24,9 @@
|
||||||
"p-retry": "~5.1.2",
|
"p-retry": "~5.1.2",
|
||||||
"p-throttle": "~5.0.0",
|
"p-throttle": "~5.0.0",
|
||||||
"preact": "~10.13.1",
|
"preact": "~10.13.1",
|
||||||
"react-hotkeys-hook": "~4.3.7",
|
"react-hotkeys-hook": "~4.3.8",
|
||||||
"react-intersection-observer": "~9.4.3",
|
"react-intersection-observer": "~9.4.3",
|
||||||
|
"react-quick-pinch-zoom": "~4.6.0",
|
||||||
"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",
|
||||||
|
@ -42,10 +44,9 @@
|
||||||
"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.1.4",
|
"vite": "~4.2.1",
|
||||||
"vite-plugin-generate-file": "~0.0.4",
|
"vite-plugin-generate-file": "~0.0.4",
|
||||||
"vite-plugin-html-config": "~1.0.11",
|
"vite-plugin-html-config": "~1.0.11",
|
||||||
"vite-plugin-html-env": "~1.2.7",
|
|
||||||
"vite-plugin-pwa": "~0.14.4",
|
"vite-plugin-pwa": "~0.14.4",
|
||||||
"vite-plugin-remove-console": "~2.1.0",
|
"vite-plugin-remove-console": "~2.1.0",
|
||||||
"workbox-cacheable-response": "~6.5.4",
|
"workbox-cacheable-response": "~6.5.4",
|
||||||
|
@ -2159,9 +2160,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/android-arm": {
|
"node_modules/@esbuild/android-arm": {
|
||||||
"version": "0.16.17",
|
"version": "0.17.12",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.16.17.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.12.tgz",
|
||||||
"integrity": "sha512-N9x1CMXVhtWEAMS7pNNONyA14f71VPQN9Cnavj1XQh6T7bskqiLLrSca4O0Vr8Wdcga943eThxnVp3JLnBMYtw==",
|
"integrity": "sha512-E/sgkvwoIfj4aMAPL2e35VnUJspzVYl7+M1B2cqeubdBhADV4uPon0KCc8p2G+LqSJ6i8ocYPCqY3A4GGq0zkQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
|
@ -2175,9 +2176,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/android-arm64": {
|
"node_modules/@esbuild/android-arm64": {
|
||||||
"version": "0.16.17",
|
"version": "0.17.12",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.16.17.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.12.tgz",
|
||||||
"integrity": "sha512-MIGl6p5sc3RDTLLkYL1MyL8BMRN4tLMRCn+yRJJmEDvYZ2M7tmAf80hx1kbNEUX2KJ50RRtxZ4JHLvCfuB6kBg==",
|
"integrity": "sha512-WQ9p5oiXXYJ33F2EkE3r0FRDFVpEdcDiwNX3u7Xaibxfx6vQE0Sb8ytrfQsA5WO6kDn6mDfKLh6KrPBjvkk7xA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
@ -2191,9 +2192,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/android-x64": {
|
"node_modules/@esbuild/android-x64": {
|
||||||
"version": "0.16.17",
|
"version": "0.17.12",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.16.17.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.12.tgz",
|
||||||
"integrity": "sha512-a3kTv3m0Ghh4z1DaFEuEDfz3OLONKuFvI4Xqczqx4BqLyuFaFkuaG4j2MtA6fuWEFeC5x9IvqnX7drmRq/fyAQ==",
|
"integrity": "sha512-m4OsaCr5gT+se25rFPHKQXARMyAehHTQAz4XX1Vk3d27VtqiX0ALMBPoXZsGaB6JYryCLfgGwUslMqTfqeLU0w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
@ -2207,9 +2208,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/darwin-arm64": {
|
"node_modules/@esbuild/darwin-arm64": {
|
||||||
"version": "0.16.17",
|
"version": "0.17.12",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.16.17.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.12.tgz",
|
||||||
"integrity": "sha512-/2agbUEfmxWHi9ARTX6OQ/KgXnOWfsNlTeLcoV7HSuSTv63E4DqtAc+2XqGw1KHxKMHGZgbVCZge7HXWX9Vn+w==",
|
"integrity": "sha512-O3GCZghRIx+RAN0NDPhyyhRgwa19MoKlzGonIb5hgTj78krqp9XZbYCvFr9N1eUxg0ZQEpiiZ4QvsOQwBpP+lg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
@ -2223,9 +2224,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/darwin-x64": {
|
"node_modules/@esbuild/darwin-x64": {
|
||||||
"version": "0.16.17",
|
"version": "0.17.12",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.16.17.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.12.tgz",
|
||||||
"integrity": "sha512-2By45OBHulkd9Svy5IOCZt376Aa2oOkiE9QWUK9fe6Tb+WDr8hXL3dpqi+DeLiMed8tVXspzsTAvd0jUl96wmg==",
|
"integrity": "sha512-5D48jM3tW27h1qjaD9UNRuN+4v0zvksqZSPZqeSWggfMlsVdAhH3pwSfQIFJwcs9QJ9BRibPS4ViZgs3d2wsCA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
@ -2239,9 +2240,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/freebsd-arm64": {
|
"node_modules/@esbuild/freebsd-arm64": {
|
||||||
"version": "0.16.17",
|
"version": "0.17.12",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.16.17.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.12.tgz",
|
||||||
"integrity": "sha512-mt+cxZe1tVx489VTb4mBAOo2aKSnJ33L9fr25JXpqQqzbUIw/yzIzi+NHwAXK2qYV1lEFp4OoVeThGjUbmWmdw==",
|
"integrity": "sha512-OWvHzmLNTdF1erSvrfoEBGlN94IE6vCEaGEkEH29uo/VoONqPnoDFfShi41Ew+yKimx4vrmmAJEGNoyyP+OgOQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
@ -2255,9 +2256,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/freebsd-x64": {
|
"node_modules/@esbuild/freebsd-x64": {
|
||||||
"version": "0.16.17",
|
"version": "0.17.12",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.16.17.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.12.tgz",
|
||||||
"integrity": "sha512-8ScTdNJl5idAKjH8zGAsN7RuWcyHG3BAvMNpKOBaqqR7EbUhhVHOqXRdL7oZvz8WNHL2pr5+eIT5c65kA6NHug==",
|
"integrity": "sha512-A0Xg5CZv8MU9xh4a+7NUpi5VHBKh1RaGJKqjxe4KG87X+mTjDE6ZvlJqpWoeJxgfXHT7IMP9tDFu7IZ03OtJAw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
@ -2271,9 +2272,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-arm": {
|
"node_modules/@esbuild/linux-arm": {
|
||||||
"version": "0.16.17",
|
"version": "0.17.12",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.16.17.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.12.tgz",
|
||||||
"integrity": "sha512-iihzrWbD4gIT7j3caMzKb/RsFFHCwqqbrbH9SqUSRrdXkXaygSZCZg1FybsZz57Ju7N/SHEgPyaR0LZ8Zbe9gQ==",
|
"integrity": "sha512-WsHyJ7b7vzHdJ1fv67Yf++2dz3D726oO3QCu8iNYik4fb5YuuReOI9OtA+n7Mk0xyQivNTPbl181s+5oZ38gyA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
|
@ -2287,9 +2288,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-arm64": {
|
"node_modules/@esbuild/linux-arm64": {
|
||||||
"version": "0.16.17",
|
"version": "0.17.12",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.16.17.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.12.tgz",
|
||||||
"integrity": "sha512-7S8gJnSlqKGVJunnMCrXHU9Q8Q/tQIxk/xL8BqAP64wchPCTzuM6W3Ra8cIa1HIflAvDnNOt2jaL17vaW+1V0g==",
|
"integrity": "sha512-cK3AjkEc+8v8YG02hYLQIQlOznW+v9N+OI9BAFuyqkfQFR+DnDLhEM5N8QRxAUz99cJTo1rLNXqRrvY15gbQUg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
@ -2303,9 +2304,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-ia32": {
|
"node_modules/@esbuild/linux-ia32": {
|
||||||
"version": "0.16.17",
|
"version": "0.17.12",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.16.17.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.12.tgz",
|
||||||
"integrity": "sha512-kiX69+wcPAdgl3Lonh1VI7MBr16nktEvOfViszBSxygRQqSpzv7BffMKRPMFwzeJGPxcio0pdD3kYQGpqQ2SSg==",
|
"integrity": "sha512-jdOBXJqcgHlah/nYHnj3Hrnl9l63RjtQ4vn9+bohjQPI2QafASB5MtHAoEv0JQHVb/xYQTFOeuHnNYE1zF7tYw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
|
@ -2319,9 +2320,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-loong64": {
|
"node_modules/@esbuild/linux-loong64": {
|
||||||
"version": "0.16.17",
|
"version": "0.17.12",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.16.17.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.12.tgz",
|
||||||
"integrity": "sha512-dTzNnQwembNDhd654cA4QhbS9uDdXC3TKqMJjgOWsC0yNCbpzfWoXdZvp0mY7HU6nzk5E0zpRGGx3qoQg8T2DQ==",
|
"integrity": "sha512-GTOEtj8h9qPKXCyiBBnHconSCV9LwFyx/gv3Phw0pa25qPYjVuuGZ4Dk14bGCfGX3qKF0+ceeQvwmtI+aYBbVA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"loong64"
|
"loong64"
|
||||||
],
|
],
|
||||||
|
@ -2335,9 +2336,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-mips64el": {
|
"node_modules/@esbuild/linux-mips64el": {
|
||||||
"version": "0.16.17",
|
"version": "0.17.12",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.16.17.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.12.tgz",
|
||||||
"integrity": "sha512-ezbDkp2nDl0PfIUn0CsQ30kxfcLTlcx4Foz2kYv8qdC6ia2oX5Q3E/8m6lq84Dj/6b0FrkgD582fJMIfHhJfSw==",
|
"integrity": "sha512-o8CIhfBwKcxmEENOH9RwmUejs5jFiNoDw7YgS0EJTF6kgPgcqLFjgoc5kDey5cMHRVCIWc6kK2ShUePOcc7RbA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"mips64el"
|
"mips64el"
|
||||||
],
|
],
|
||||||
|
@ -2351,9 +2352,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-ppc64": {
|
"node_modules/@esbuild/linux-ppc64": {
|
||||||
"version": "0.16.17",
|
"version": "0.17.12",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.16.17.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.12.tgz",
|
||||||
"integrity": "sha512-dzS678gYD1lJsW73zrFhDApLVdM3cUF2MvAa1D8K8KtcSKdLBPP4zZSLy6LFZ0jYqQdQ29bjAHJDgz0rVbLB3g==",
|
"integrity": "sha512-biMLH6NR/GR4z+ap0oJYb877LdBpGac8KfZoEnDiBKd7MD/xt8eaw1SFfYRUeMVx519kVkAOL2GExdFmYnZx3A==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
|
@ -2367,9 +2368,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-riscv64": {
|
"node_modules/@esbuild/linux-riscv64": {
|
||||||
"version": "0.16.17",
|
"version": "0.17.12",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.16.17.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.12.tgz",
|
||||||
"integrity": "sha512-ylNlVsxuFjZK8DQtNUwiMskh6nT0vI7kYl/4fZgV1llP5d6+HIeL/vmmm3jpuoo8+NuXjQVZxmKuhDApK0/cKw==",
|
"integrity": "sha512-jkphYUiO38wZGeWlfIBMB72auOllNA2sLfiZPGDtOBb1ELN8lmqBrlMiucgL8awBw1zBXN69PmZM6g4yTX84TA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
|
@ -2383,9 +2384,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-s390x": {
|
"node_modules/@esbuild/linux-s390x": {
|
||||||
"version": "0.16.17",
|
"version": "0.17.12",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.16.17.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.12.tgz",
|
||||||
"integrity": "sha512-gzy7nUTO4UA4oZ2wAMXPNBGTzZFP7mss3aKR2hH+/4UUkCOyqmjXiKpzGrY2TlEUhbbejzXVKKGazYcQTZWA/w==",
|
"integrity": "sha512-j3ucLdeY9HBcvODhCY4b+Ds3hWGO8t+SAidtmWu/ukfLLG/oYDMaA+dnugTVAg5fnUOGNbIYL9TOjhWgQB8W5g==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
|
@ -2399,9 +2400,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-x64": {
|
"node_modules/@esbuild/linux-x64": {
|
||||||
"version": "0.16.17",
|
"version": "0.17.12",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.16.17.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.12.tgz",
|
||||||
"integrity": "sha512-mdPjPxfnmoqhgpiEArqi4egmBAMYvaObgn4poorpUaqmvzzbvqbowRllQ+ZgzGVMGKaPkqUmPDOOFQRUFDmeUw==",
|
"integrity": "sha512-uo5JL3cgaEGotaqSaJdRfFNSCUJOIliKLnDGWaVCgIKkHxwhYMm95pfMbWZ9l7GeW9kDg0tSxcy9NYdEtjwwmA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
@ -2415,9 +2416,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/netbsd-x64": {
|
"node_modules/@esbuild/netbsd-x64": {
|
||||||
"version": "0.16.17",
|
"version": "0.17.12",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.16.17.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.12.tgz",
|
||||||
"integrity": "sha512-/PzmzD/zyAeTUsduZa32bn0ORug+Jd1EGGAUJvqfeixoEISYpGnAezN6lnJoskauoai0Jrs+XSyvDhppCPoKOA==",
|
"integrity": "sha512-DNdoRg8JX+gGsbqt2gPgkgb00mqOgOO27KnrWZtdABl6yWTST30aibGJ6geBq3WM2TIeW6COs5AScnC7GwtGPg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
@ -2431,9 +2432,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/openbsd-x64": {
|
"node_modules/@esbuild/openbsd-x64": {
|
||||||
"version": "0.16.17",
|
"version": "0.17.12",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.16.17.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.12.tgz",
|
||||||
"integrity": "sha512-2yaWJhvxGEz2RiftSk0UObqJa/b+rIAjnODJgv2GbGGpRwAfpgzyrg1WLK8rqA24mfZa9GvpjLcBBg8JHkoodg==",
|
"integrity": "sha512-aVsENlr7B64w8I1lhHShND5o8cW6sB9n9MUtLumFlPhG3elhNWtE7M1TFpj3m7lT3sKQUMkGFjTQBrvDDO1YWA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
@ -2447,9 +2448,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/sunos-x64": {
|
"node_modules/@esbuild/sunos-x64": {
|
||||||
"version": "0.16.17",
|
"version": "0.17.12",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.16.17.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.12.tgz",
|
||||||
"integrity": "sha512-xtVUiev38tN0R3g8VhRfN7Zl42YCJvyBhRKw1RJjwE1d2emWTVToPLNEQj/5Qxc6lVFATDiy6LjVHYhIPrLxzw==",
|
"integrity": "sha512-qbHGVQdKSwi0JQJuZznS4SyY27tYXYF0mrgthbxXrZI3AHKuRvU+Eqbg/F0rmLDpW/jkIZBlCO1XfHUBMNJ1pg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
@ -2463,9 +2464,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/win32-arm64": {
|
"node_modules/@esbuild/win32-arm64": {
|
||||||
"version": "0.16.17",
|
"version": "0.17.12",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.16.17.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.12.tgz",
|
||||||
"integrity": "sha512-ga8+JqBDHY4b6fQAmOgtJJue36scANy4l/rL97W+0wYmijhxKetzZdKOJI7olaBaMhWt8Pac2McJdZLxXWUEQw==",
|
"integrity": "sha512-zsCp8Ql+96xXTVTmm6ffvoTSZSV2B/LzzkUXAY33F/76EajNw1m+jZ9zPfNJlJ3Rh4EzOszNDHsmG/fZOhtqDg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
@ -2479,9 +2480,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/win32-ia32": {
|
"node_modules/@esbuild/win32-ia32": {
|
||||||
"version": "0.16.17",
|
"version": "0.17.12",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.16.17.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.12.tgz",
|
||||||
"integrity": "sha512-WnsKaf46uSSF/sZhwnqE4L/F89AYNMiD4YtEcYekBt9Q7nj0DiId2XH2Ng2PHM54qi5oPrQ8luuzGszqi/veig==",
|
"integrity": "sha512-FfrFjR4id7wcFYOdqbDfDET3tjxCozUgbqdkOABsSFzoZGFC92UK7mg4JKRc/B3NNEf1s2WHxJ7VfTdVDPN3ng==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
|
@ -2495,9 +2496,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/win32-x64": {
|
"node_modules/@esbuild/win32-x64": {
|
||||||
"version": "0.16.17",
|
"version": "0.17.12",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.16.17.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.12.tgz",
|
||||||
"integrity": "sha512-y+EHuSchhL7FjHgvQL/0fnnFmO4T1bhvWANX6gcnqTjtnKWbTvUMCpGnv2+t+31d7RzyEAYAd4u2fnIhHL6N/Q==",
|
"integrity": "sha512-JOOxw49BVZx2/5tW3FqkdjSD/5gXYeVGPDcB0lvap0gLQshkh1Nyel1QazC+wNxus3xPlsYAgqU1BUmrmCvWtw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
@ -2620,6 +2621,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.4.0.tgz",
|
||||||
"integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA=="
|
"integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA=="
|
||||||
},
|
},
|
||||||
|
"node_modules/@justinribeiro/lite-youtube": {
|
||||||
|
"version": "1.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@justinribeiro/lite-youtube/-/lite-youtube-1.5.0.tgz",
|
||||||
|
"integrity": "sha512-TU92RKtz9BI9PRYrVwDIUsnFadLZtqRKWl1ZOdbxb7roJDb8Dd/xURllAsLEmCg6oJNyhXlVa5RsnUc0EKd8Cw=="
|
||||||
|
},
|
||||||
"node_modules/@lukeed/csprng": {
|
"node_modules/@lukeed/csprng": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.0.1.tgz",
|
||||||
|
@ -3705,9 +3711,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/esbuild": {
|
"node_modules/esbuild": {
|
||||||
"version": "0.16.17",
|
"version": "0.17.12",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.16.17.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.12.tgz",
|
||||||
"integrity": "sha512-G8LEkV0XzDMNwXKgM0Jwu3nY3lSTwSGY6XbxM9cr9+s0T/qSV1q1JVPBGzm3dcjhCic9+emZDmMffkwgPeOeLg==",
|
"integrity": "sha512-bX/zHl7Gn2CpQwcMtRogTTBf9l1nl+H6R8nUbjk+RuKqAE3+8FDulLA+pHvX7aA7Xe07Iwa+CWvy9I8Y2qqPKQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
|
@ -3717,28 +3723,28 @@
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@esbuild/android-arm": "0.16.17",
|
"@esbuild/android-arm": "0.17.12",
|
||||||
"@esbuild/android-arm64": "0.16.17",
|
"@esbuild/android-arm64": "0.17.12",
|
||||||
"@esbuild/android-x64": "0.16.17",
|
"@esbuild/android-x64": "0.17.12",
|
||||||
"@esbuild/darwin-arm64": "0.16.17",
|
"@esbuild/darwin-arm64": "0.17.12",
|
||||||
"@esbuild/darwin-x64": "0.16.17",
|
"@esbuild/darwin-x64": "0.17.12",
|
||||||
"@esbuild/freebsd-arm64": "0.16.17",
|
"@esbuild/freebsd-arm64": "0.17.12",
|
||||||
"@esbuild/freebsd-x64": "0.16.17",
|
"@esbuild/freebsd-x64": "0.17.12",
|
||||||
"@esbuild/linux-arm": "0.16.17",
|
"@esbuild/linux-arm": "0.17.12",
|
||||||
"@esbuild/linux-arm64": "0.16.17",
|
"@esbuild/linux-arm64": "0.17.12",
|
||||||
"@esbuild/linux-ia32": "0.16.17",
|
"@esbuild/linux-ia32": "0.17.12",
|
||||||
"@esbuild/linux-loong64": "0.16.17",
|
"@esbuild/linux-loong64": "0.17.12",
|
||||||
"@esbuild/linux-mips64el": "0.16.17",
|
"@esbuild/linux-mips64el": "0.17.12",
|
||||||
"@esbuild/linux-ppc64": "0.16.17",
|
"@esbuild/linux-ppc64": "0.17.12",
|
||||||
"@esbuild/linux-riscv64": "0.16.17",
|
"@esbuild/linux-riscv64": "0.17.12",
|
||||||
"@esbuild/linux-s390x": "0.16.17",
|
"@esbuild/linux-s390x": "0.17.12",
|
||||||
"@esbuild/linux-x64": "0.16.17",
|
"@esbuild/linux-x64": "0.17.12",
|
||||||
"@esbuild/netbsd-x64": "0.16.17",
|
"@esbuild/netbsd-x64": "0.17.12",
|
||||||
"@esbuild/openbsd-x64": "0.16.17",
|
"@esbuild/openbsd-x64": "0.17.12",
|
||||||
"@esbuild/sunos-x64": "0.16.17",
|
"@esbuild/sunos-x64": "0.17.12",
|
||||||
"@esbuild/win32-arm64": "0.16.17",
|
"@esbuild/win32-arm64": "0.17.12",
|
||||||
"@esbuild/win32-ia32": "0.16.17",
|
"@esbuild/win32-ia32": "0.17.12",
|
||||||
"@esbuild/win32-x64": "0.16.17"
|
"@esbuild/win32-x64": "0.17.12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/escalade": {
|
"node_modules/escalade": {
|
||||||
|
@ -5788,9 +5794,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-hotkeys-hook": {
|
"node_modules/react-hotkeys-hook": {
|
||||||
"version": "4.3.7",
|
"version": "4.3.8",
|
||||||
"resolved": "https://registry.npmjs.org/react-hotkeys-hook/-/react-hotkeys-hook-4.3.7.tgz",
|
"resolved": "https://registry.npmjs.org/react-hotkeys-hook/-/react-hotkeys-hook-4.3.8.tgz",
|
||||||
"integrity": "sha512-qUcA5vl/liGWr9wLYI5/8oppHLa6nExFqOAMC6CyZhpj7C56PIzYZ76xAtJ+5lgxObgl4A4pQz8upy+nq7orSQ==",
|
"integrity": "sha512-RmrIQ3M259c84MnYVEAQsmHkD6s7XUgLG0rW6S7qjt1Lh7q+SPIz5b6obVU8OJw1Utsj1mUCj6twtBPaK/ytww==",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": ">=16.8.1",
|
"react": ">=16.8.1",
|
||||||
"react-dom": ">=16.8.1"
|
"react-dom": ">=16.8.1"
|
||||||
|
@ -5809,6 +5815,27 @@
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/react-quick-pinch-zoom": {
|
||||||
|
"version": "4.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-quick-pinch-zoom/-/react-quick-pinch-zoom-4.6.0.tgz",
|
||||||
|
"integrity": "sha512-M3woYVzWt8Kh6FCAytBtJJ4KC/5noG98GpI8ZTxTIE2sjR1XRPjV0NpFRhFgxPQpDvD+lkMp63sxP130uhafaw==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.4.0",
|
||||||
|
"react-dom": ">=16.4.0",
|
||||||
|
"tslib": ">=2.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-dom": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"tslib": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-router": {
|
"node_modules/react-router": {
|
||||||
"version": "6.6.2",
|
"version": "6.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.6.2.tgz",
|
||||||
|
@ -5987,9 +6014,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/rollup": {
|
"node_modules/rollup": {
|
||||||
"version": "3.12.1",
|
"version": "3.19.1",
|
||||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.12.1.tgz",
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.19.1.tgz",
|
||||||
"integrity": "sha512-t9elERrz2i4UU9z7AwISj3CQcXP39cWxgRWLdf4Tm6aKm1eYrqHIgjzXBgb67GNY1sZckTFFi0oMozh3/S++Ig==",
|
"integrity": "sha512-lAbrdN7neYCg/8WaoWn/ckzCtz+jr70GFfYdlf50OF7387HTg+wiuiqJRFYawwSPpqfqDNYqK7smY/ks2iAudg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"rollup": "dist/bin/rollup"
|
"rollup": "dist/bin/rollup"
|
||||||
|
@ -6624,15 +6651,15 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "4.1.4",
|
"version": "4.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-4.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-4.2.1.tgz",
|
||||||
"integrity": "sha512-3knk/HsbSTKEin43zHu7jTwYWv81f8kgAL99G5NWBcA1LKvtvcVAC4JjBH1arBunO9kQka+1oGbrMKOjk4ZrBg==",
|
"integrity": "sha512-7MKhqdy0ISo4wnvwtqZkjke6XN4taqQ2TBaTccLIpOKv7Vp2h4Y+NpmWCnGDeSvvn45KxvWgGyb0MkHvY1vgbg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.16.14",
|
"esbuild": "^0.17.5",
|
||||||
"postcss": "^8.4.21",
|
"postcss": "^8.4.21",
|
||||||
"resolve": "^1.22.1",
|
"resolve": "^1.22.1",
|
||||||
"rollup": "^3.10.0"
|
"rollup": "^3.18.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"vite": "bin/vite.js"
|
"vite": "bin/vite.js"
|
||||||
|
@ -6766,18 +6793,6 @@
|
||||||
"vite": ">=2.0.0"
|
"vite": ">=2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite-plugin-html-env": {
|
|
||||||
"version": "1.2.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/vite-plugin-html-env/-/vite-plugin-html-env-1.2.7.tgz",
|
|
||||||
"integrity": "sha512-vdTnKtuBeB8Zp93DCbN0Qjf4odW2msVRq45r7lGKA6nwQGJFj6YemU54u3xPPkvDeZhG8DEEU64xbLwzVEBilQ==",
|
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12.0.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"vite": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/vite-plugin-pwa": {
|
"node_modules/vite-plugin-pwa": {
|
||||||
"version": "0.14.4",
|
"version": "0.14.4",
|
||||||
"resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.14.4.tgz",
|
"resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.14.4.tgz",
|
||||||
|
@ -8563,156 +8578,156 @@
|
||||||
"requires": {}
|
"requires": {}
|
||||||
},
|
},
|
||||||
"@esbuild/android-arm": {
|
"@esbuild/android-arm": {
|
||||||
"version": "0.16.17",
|
"version": "0.17.12",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.16.17.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.12.tgz",
|
||||||
"integrity": "sha512-N9x1CMXVhtWEAMS7pNNONyA14f71VPQN9Cnavj1XQh6T7bskqiLLrSca4O0Vr8Wdcga943eThxnVp3JLnBMYtw==",
|
"integrity": "sha512-E/sgkvwoIfj4aMAPL2e35VnUJspzVYl7+M1B2cqeubdBhADV4uPon0KCc8p2G+LqSJ6i8ocYPCqY3A4GGq0zkQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/android-arm64": {
|
"@esbuild/android-arm64": {
|
||||||
"version": "0.16.17",
|
"version": "0.17.12",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.16.17.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.12.tgz",
|
||||||
"integrity": "sha512-MIGl6p5sc3RDTLLkYL1MyL8BMRN4tLMRCn+yRJJmEDvYZ2M7tmAf80hx1kbNEUX2KJ50RRtxZ4JHLvCfuB6kBg==",
|
"integrity": "sha512-WQ9p5oiXXYJ33F2EkE3r0FRDFVpEdcDiwNX3u7Xaibxfx6vQE0Sb8ytrfQsA5WO6kDn6mDfKLh6KrPBjvkk7xA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/android-x64": {
|
"@esbuild/android-x64": {
|
||||||
"version": "0.16.17",
|
"version": "0.17.12",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.16.17.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.12.tgz",
|
||||||
"integrity": "sha512-a3kTv3m0Ghh4z1DaFEuEDfz3OLONKuFvI4Xqczqx4BqLyuFaFkuaG4j2MtA6fuWEFeC5x9IvqnX7drmRq/fyAQ==",
|
"integrity": "sha512-m4OsaCr5gT+se25rFPHKQXARMyAehHTQAz4XX1Vk3d27VtqiX0ALMBPoXZsGaB6JYryCLfgGwUslMqTfqeLU0w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/darwin-arm64": {
|
"@esbuild/darwin-arm64": {
|
||||||
"version": "0.16.17",
|
"version": "0.17.12",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.16.17.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.12.tgz",
|
||||||
"integrity": "sha512-/2agbUEfmxWHi9ARTX6OQ/KgXnOWfsNlTeLcoV7HSuSTv63E4DqtAc+2XqGw1KHxKMHGZgbVCZge7HXWX9Vn+w==",
|
"integrity": "sha512-O3GCZghRIx+RAN0NDPhyyhRgwa19MoKlzGonIb5hgTj78krqp9XZbYCvFr9N1eUxg0ZQEpiiZ4QvsOQwBpP+lg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/darwin-x64": {
|
"@esbuild/darwin-x64": {
|
||||||
"version": "0.16.17",
|
"version": "0.17.12",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.16.17.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.12.tgz",
|
||||||
"integrity": "sha512-2By45OBHulkd9Svy5IOCZt376Aa2oOkiE9QWUK9fe6Tb+WDr8hXL3dpqi+DeLiMed8tVXspzsTAvd0jUl96wmg==",
|
"integrity": "sha512-5D48jM3tW27h1qjaD9UNRuN+4v0zvksqZSPZqeSWggfMlsVdAhH3pwSfQIFJwcs9QJ9BRibPS4ViZgs3d2wsCA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/freebsd-arm64": {
|
"@esbuild/freebsd-arm64": {
|
||||||
"version": "0.16.17",
|
"version": "0.17.12",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.16.17.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.12.tgz",
|
||||||
"integrity": "sha512-mt+cxZe1tVx489VTb4mBAOo2aKSnJ33L9fr25JXpqQqzbUIw/yzIzi+NHwAXK2qYV1lEFp4OoVeThGjUbmWmdw==",
|
"integrity": "sha512-OWvHzmLNTdF1erSvrfoEBGlN94IE6vCEaGEkEH29uo/VoONqPnoDFfShi41Ew+yKimx4vrmmAJEGNoyyP+OgOQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/freebsd-x64": {
|
"@esbuild/freebsd-x64": {
|
||||||
"version": "0.16.17",
|
"version": "0.17.12",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.16.17.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.12.tgz",
|
||||||
"integrity": "sha512-8ScTdNJl5idAKjH8zGAsN7RuWcyHG3BAvMNpKOBaqqR7EbUhhVHOqXRdL7oZvz8WNHL2pr5+eIT5c65kA6NHug==",
|
"integrity": "sha512-A0Xg5CZv8MU9xh4a+7NUpi5VHBKh1RaGJKqjxe4KG87X+mTjDE6ZvlJqpWoeJxgfXHT7IMP9tDFu7IZ03OtJAw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/linux-arm": {
|
"@esbuild/linux-arm": {
|
||||||
"version": "0.16.17",
|
"version": "0.17.12",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.16.17.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.12.tgz",
|
||||||
"integrity": "sha512-iihzrWbD4gIT7j3caMzKb/RsFFHCwqqbrbH9SqUSRrdXkXaygSZCZg1FybsZz57Ju7N/SHEgPyaR0LZ8Zbe9gQ==",
|
"integrity": "sha512-WsHyJ7b7vzHdJ1fv67Yf++2dz3D726oO3QCu8iNYik4fb5YuuReOI9OtA+n7Mk0xyQivNTPbl181s+5oZ38gyA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/linux-arm64": {
|
"@esbuild/linux-arm64": {
|
||||||
"version": "0.16.17",
|
"version": "0.17.12",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.16.17.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.12.tgz",
|
||||||
"integrity": "sha512-7S8gJnSlqKGVJunnMCrXHU9Q8Q/tQIxk/xL8BqAP64wchPCTzuM6W3Ra8cIa1HIflAvDnNOt2jaL17vaW+1V0g==",
|
"integrity": "sha512-cK3AjkEc+8v8YG02hYLQIQlOznW+v9N+OI9BAFuyqkfQFR+DnDLhEM5N8QRxAUz99cJTo1rLNXqRrvY15gbQUg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/linux-ia32": {
|
"@esbuild/linux-ia32": {
|
||||||
"version": "0.16.17",
|
"version": "0.17.12",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.16.17.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.12.tgz",
|
||||||
"integrity": "sha512-kiX69+wcPAdgl3Lonh1VI7MBr16nktEvOfViszBSxygRQqSpzv7BffMKRPMFwzeJGPxcio0pdD3kYQGpqQ2SSg==",
|
"integrity": "sha512-jdOBXJqcgHlah/nYHnj3Hrnl9l63RjtQ4vn9+bohjQPI2QafASB5MtHAoEv0JQHVb/xYQTFOeuHnNYE1zF7tYw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/linux-loong64": {
|
"@esbuild/linux-loong64": {
|
||||||
"version": "0.16.17",
|
"version": "0.17.12",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.16.17.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.12.tgz",
|
||||||
"integrity": "sha512-dTzNnQwembNDhd654cA4QhbS9uDdXC3TKqMJjgOWsC0yNCbpzfWoXdZvp0mY7HU6nzk5E0zpRGGx3qoQg8T2DQ==",
|
"integrity": "sha512-GTOEtj8h9qPKXCyiBBnHconSCV9LwFyx/gv3Phw0pa25qPYjVuuGZ4Dk14bGCfGX3qKF0+ceeQvwmtI+aYBbVA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/linux-mips64el": {
|
"@esbuild/linux-mips64el": {
|
||||||
"version": "0.16.17",
|
"version": "0.17.12",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.16.17.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.12.tgz",
|
||||||
"integrity": "sha512-ezbDkp2nDl0PfIUn0CsQ30kxfcLTlcx4Foz2kYv8qdC6ia2oX5Q3E/8m6lq84Dj/6b0FrkgD582fJMIfHhJfSw==",
|
"integrity": "sha512-o8CIhfBwKcxmEENOH9RwmUejs5jFiNoDw7YgS0EJTF6kgPgcqLFjgoc5kDey5cMHRVCIWc6kK2ShUePOcc7RbA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/linux-ppc64": {
|
"@esbuild/linux-ppc64": {
|
||||||
"version": "0.16.17",
|
"version": "0.17.12",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.16.17.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.12.tgz",
|
||||||
"integrity": "sha512-dzS678gYD1lJsW73zrFhDApLVdM3cUF2MvAa1D8K8KtcSKdLBPP4zZSLy6LFZ0jYqQdQ29bjAHJDgz0rVbLB3g==",
|
"integrity": "sha512-biMLH6NR/GR4z+ap0oJYb877LdBpGac8KfZoEnDiBKd7MD/xt8eaw1SFfYRUeMVx519kVkAOL2GExdFmYnZx3A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/linux-riscv64": {
|
"@esbuild/linux-riscv64": {
|
||||||
"version": "0.16.17",
|
"version": "0.17.12",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.16.17.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.12.tgz",
|
||||||
"integrity": "sha512-ylNlVsxuFjZK8DQtNUwiMskh6nT0vI7kYl/4fZgV1llP5d6+HIeL/vmmm3jpuoo8+NuXjQVZxmKuhDApK0/cKw==",
|
"integrity": "sha512-jkphYUiO38wZGeWlfIBMB72auOllNA2sLfiZPGDtOBb1ELN8lmqBrlMiucgL8awBw1zBXN69PmZM6g4yTX84TA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/linux-s390x": {
|
"@esbuild/linux-s390x": {
|
||||||
"version": "0.16.17",
|
"version": "0.17.12",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.16.17.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.12.tgz",
|
||||||
"integrity": "sha512-gzy7nUTO4UA4oZ2wAMXPNBGTzZFP7mss3aKR2hH+/4UUkCOyqmjXiKpzGrY2TlEUhbbejzXVKKGazYcQTZWA/w==",
|
"integrity": "sha512-j3ucLdeY9HBcvODhCY4b+Ds3hWGO8t+SAidtmWu/ukfLLG/oYDMaA+dnugTVAg5fnUOGNbIYL9TOjhWgQB8W5g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/linux-x64": {
|
"@esbuild/linux-x64": {
|
||||||
"version": "0.16.17",
|
"version": "0.17.12",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.16.17.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.12.tgz",
|
||||||
"integrity": "sha512-mdPjPxfnmoqhgpiEArqi4egmBAMYvaObgn4poorpUaqmvzzbvqbowRllQ+ZgzGVMGKaPkqUmPDOOFQRUFDmeUw==",
|
"integrity": "sha512-uo5JL3cgaEGotaqSaJdRfFNSCUJOIliKLnDGWaVCgIKkHxwhYMm95pfMbWZ9l7GeW9kDg0tSxcy9NYdEtjwwmA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/netbsd-x64": {
|
"@esbuild/netbsd-x64": {
|
||||||
"version": "0.16.17",
|
"version": "0.17.12",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.16.17.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.12.tgz",
|
||||||
"integrity": "sha512-/PzmzD/zyAeTUsduZa32bn0ORug+Jd1EGGAUJvqfeixoEISYpGnAezN6lnJoskauoai0Jrs+XSyvDhppCPoKOA==",
|
"integrity": "sha512-DNdoRg8JX+gGsbqt2gPgkgb00mqOgOO27KnrWZtdABl6yWTST30aibGJ6geBq3WM2TIeW6COs5AScnC7GwtGPg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/openbsd-x64": {
|
"@esbuild/openbsd-x64": {
|
||||||
"version": "0.16.17",
|
"version": "0.17.12",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.16.17.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.12.tgz",
|
||||||
"integrity": "sha512-2yaWJhvxGEz2RiftSk0UObqJa/b+rIAjnODJgv2GbGGpRwAfpgzyrg1WLK8rqA24mfZa9GvpjLcBBg8JHkoodg==",
|
"integrity": "sha512-aVsENlr7B64w8I1lhHShND5o8cW6sB9n9MUtLumFlPhG3elhNWtE7M1TFpj3m7lT3sKQUMkGFjTQBrvDDO1YWA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/sunos-x64": {
|
"@esbuild/sunos-x64": {
|
||||||
"version": "0.16.17",
|
"version": "0.17.12",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.16.17.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.12.tgz",
|
||||||
"integrity": "sha512-xtVUiev38tN0R3g8VhRfN7Zl42YCJvyBhRKw1RJjwE1d2emWTVToPLNEQj/5Qxc6lVFATDiy6LjVHYhIPrLxzw==",
|
"integrity": "sha512-qbHGVQdKSwi0JQJuZznS4SyY27tYXYF0mrgthbxXrZI3AHKuRvU+Eqbg/F0rmLDpW/jkIZBlCO1XfHUBMNJ1pg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/win32-arm64": {
|
"@esbuild/win32-arm64": {
|
||||||
"version": "0.16.17",
|
"version": "0.17.12",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.16.17.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.12.tgz",
|
||||||
"integrity": "sha512-ga8+JqBDHY4b6fQAmOgtJJue36scANy4l/rL97W+0wYmijhxKetzZdKOJI7olaBaMhWt8Pac2McJdZLxXWUEQw==",
|
"integrity": "sha512-zsCp8Ql+96xXTVTmm6ffvoTSZSV2B/LzzkUXAY33F/76EajNw1m+jZ9zPfNJlJ3Rh4EzOszNDHsmG/fZOhtqDg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/win32-ia32": {
|
"@esbuild/win32-ia32": {
|
||||||
"version": "0.16.17",
|
"version": "0.17.12",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.16.17.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.12.tgz",
|
||||||
"integrity": "sha512-WnsKaf46uSSF/sZhwnqE4L/F89AYNMiD4YtEcYekBt9Q7nj0DiId2XH2Ng2PHM54qi5oPrQ8luuzGszqi/veig==",
|
"integrity": "sha512-FfrFjR4id7wcFYOdqbDfDET3tjxCozUgbqdkOABsSFzoZGFC92UK7mg4JKRc/B3NNEf1s2WHxJ7VfTdVDPN3ng==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/win32-x64": {
|
"@esbuild/win32-x64": {
|
||||||
"version": "0.16.17",
|
"version": "0.17.12",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.16.17.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.12.tgz",
|
||||||
"integrity": "sha512-y+EHuSchhL7FjHgvQL/0fnnFmO4T1bhvWANX6gcnqTjtnKWbTvUMCpGnv2+t+31d7RzyEAYAd4u2fnIhHL6N/Q==",
|
"integrity": "sha512-JOOxw49BVZx2/5tW3FqkdjSD/5gXYeVGPDcB0lvap0gLQshkh1Nyel1QazC+wNxus3xPlsYAgqU1BUmrmCvWtw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
@ -8816,6 +8831,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.4.0.tgz",
|
||||||
"integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA=="
|
"integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA=="
|
||||||
},
|
},
|
||||||
|
"@justinribeiro/lite-youtube": {
|
||||||
|
"version": "1.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@justinribeiro/lite-youtube/-/lite-youtube-1.5.0.tgz",
|
||||||
|
"integrity": "sha512-TU92RKtz9BI9PRYrVwDIUsnFadLZtqRKWl1ZOdbxb7roJDb8Dd/xURllAsLEmCg6oJNyhXlVa5RsnUc0EKd8Cw=="
|
||||||
|
},
|
||||||
"@lukeed/csprng": {
|
"@lukeed/csprng": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.0.1.tgz",
|
||||||
|
@ -9635,33 +9655,33 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"esbuild": {
|
"esbuild": {
|
||||||
"version": "0.16.17",
|
"version": "0.17.12",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.16.17.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.12.tgz",
|
||||||
"integrity": "sha512-G8LEkV0XzDMNwXKgM0Jwu3nY3lSTwSGY6XbxM9cr9+s0T/qSV1q1JVPBGzm3dcjhCic9+emZDmMffkwgPeOeLg==",
|
"integrity": "sha512-bX/zHl7Gn2CpQwcMtRogTTBf9l1nl+H6R8nUbjk+RuKqAE3+8FDulLA+pHvX7aA7Xe07Iwa+CWvy9I8Y2qqPKQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@esbuild/android-arm": "0.16.17",
|
"@esbuild/android-arm": "0.17.12",
|
||||||
"@esbuild/android-arm64": "0.16.17",
|
"@esbuild/android-arm64": "0.17.12",
|
||||||
"@esbuild/android-x64": "0.16.17",
|
"@esbuild/android-x64": "0.17.12",
|
||||||
"@esbuild/darwin-arm64": "0.16.17",
|
"@esbuild/darwin-arm64": "0.17.12",
|
||||||
"@esbuild/darwin-x64": "0.16.17",
|
"@esbuild/darwin-x64": "0.17.12",
|
||||||
"@esbuild/freebsd-arm64": "0.16.17",
|
"@esbuild/freebsd-arm64": "0.17.12",
|
||||||
"@esbuild/freebsd-x64": "0.16.17",
|
"@esbuild/freebsd-x64": "0.17.12",
|
||||||
"@esbuild/linux-arm": "0.16.17",
|
"@esbuild/linux-arm": "0.17.12",
|
||||||
"@esbuild/linux-arm64": "0.16.17",
|
"@esbuild/linux-arm64": "0.17.12",
|
||||||
"@esbuild/linux-ia32": "0.16.17",
|
"@esbuild/linux-ia32": "0.17.12",
|
||||||
"@esbuild/linux-loong64": "0.16.17",
|
"@esbuild/linux-loong64": "0.17.12",
|
||||||
"@esbuild/linux-mips64el": "0.16.17",
|
"@esbuild/linux-mips64el": "0.17.12",
|
||||||
"@esbuild/linux-ppc64": "0.16.17",
|
"@esbuild/linux-ppc64": "0.17.12",
|
||||||
"@esbuild/linux-riscv64": "0.16.17",
|
"@esbuild/linux-riscv64": "0.17.12",
|
||||||
"@esbuild/linux-s390x": "0.16.17",
|
"@esbuild/linux-s390x": "0.17.12",
|
||||||
"@esbuild/linux-x64": "0.16.17",
|
"@esbuild/linux-x64": "0.17.12",
|
||||||
"@esbuild/netbsd-x64": "0.16.17",
|
"@esbuild/netbsd-x64": "0.17.12",
|
||||||
"@esbuild/openbsd-x64": "0.16.17",
|
"@esbuild/openbsd-x64": "0.17.12",
|
||||||
"@esbuild/sunos-x64": "0.16.17",
|
"@esbuild/sunos-x64": "0.17.12",
|
||||||
"@esbuild/win32-arm64": "0.16.17",
|
"@esbuild/win32-arm64": "0.17.12",
|
||||||
"@esbuild/win32-ia32": "0.16.17",
|
"@esbuild/win32-ia32": "0.17.12",
|
||||||
"@esbuild/win32-x64": "0.16.17"
|
"@esbuild/win32-x64": "0.17.12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"escalade": {
|
"escalade": {
|
||||||
|
@ -11058,9 +11078,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"react-hotkeys-hook": {
|
"react-hotkeys-hook": {
|
||||||
"version": "4.3.7",
|
"version": "4.3.8",
|
||||||
"resolved": "https://registry.npmjs.org/react-hotkeys-hook/-/react-hotkeys-hook-4.3.7.tgz",
|
"resolved": "https://registry.npmjs.org/react-hotkeys-hook/-/react-hotkeys-hook-4.3.8.tgz",
|
||||||
"integrity": "sha512-qUcA5vl/liGWr9wLYI5/8oppHLa6nExFqOAMC6CyZhpj7C56PIzYZ76xAtJ+5lgxObgl4A4pQz8upy+nq7orSQ==",
|
"integrity": "sha512-RmrIQ3M259c84MnYVEAQsmHkD6s7XUgLG0rW6S7qjt1Lh7q+SPIz5b6obVU8OJw1Utsj1mUCj6twtBPaK/ytww==",
|
||||||
"requires": {}
|
"requires": {}
|
||||||
},
|
},
|
||||||
"react-intersection-observer": {
|
"react-intersection-observer": {
|
||||||
|
@ -11074,6 +11094,12 @@
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
||||||
},
|
},
|
||||||
|
"react-quick-pinch-zoom": {
|
||||||
|
"version": "4.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-quick-pinch-zoom/-/react-quick-pinch-zoom-4.6.0.tgz",
|
||||||
|
"integrity": "sha512-M3woYVzWt8Kh6FCAytBtJJ4KC/5noG98GpI8ZTxTIE2sjR1XRPjV0NpFRhFgxPQpDvD+lkMp63sxP130uhafaw==",
|
||||||
|
"requires": {}
|
||||||
|
},
|
||||||
"react-router": {
|
"react-router": {
|
||||||
"version": "6.6.2",
|
"version": "6.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.6.2.tgz",
|
||||||
|
@ -11204,9 +11230,9 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"rollup": {
|
"rollup": {
|
||||||
"version": "3.12.1",
|
"version": "3.19.1",
|
||||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.12.1.tgz",
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.19.1.tgz",
|
||||||
"integrity": "sha512-t9elERrz2i4UU9z7AwISj3CQcXP39cWxgRWLdf4Tm6aKm1eYrqHIgjzXBgb67GNY1sZckTFFi0oMozh3/S++Ig==",
|
"integrity": "sha512-lAbrdN7neYCg/8WaoWn/ckzCtz+jr70GFfYdlf50OF7387HTg+wiuiqJRFYawwSPpqfqDNYqK7smY/ks2iAudg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"fsevents": "~2.3.2"
|
"fsevents": "~2.3.2"
|
||||||
|
@ -11658,16 +11684,16 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"vite": {
|
"vite": {
|
||||||
"version": "4.1.4",
|
"version": "4.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-4.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-4.2.1.tgz",
|
||||||
"integrity": "sha512-3knk/HsbSTKEin43zHu7jTwYWv81f8kgAL99G5NWBcA1LKvtvcVAC4JjBH1arBunO9kQka+1oGbrMKOjk4ZrBg==",
|
"integrity": "sha512-7MKhqdy0ISo4wnvwtqZkjke6XN4taqQ2TBaTccLIpOKv7Vp2h4Y+NpmWCnGDeSvvn45KxvWgGyb0MkHvY1vgbg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"esbuild": "^0.16.14",
|
"esbuild": "^0.17.5",
|
||||||
"fsevents": "~2.3.2",
|
"fsevents": "~2.3.2",
|
||||||
"postcss": "^8.4.21",
|
"postcss": "^8.4.21",
|
||||||
"resolve": "^1.22.1",
|
"resolve": "^1.22.1",
|
||||||
"rollup": "^3.10.0"
|
"rollup": "^3.18.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"vite-plugin-generate-file": {
|
"vite-plugin-generate-file": {
|
||||||
|
@ -11740,13 +11766,6 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {}
|
"requires": {}
|
||||||
},
|
},
|
||||||
"vite-plugin-html-env": {
|
|
||||||
"version": "1.2.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/vite-plugin-html-env/-/vite-plugin-html-env-1.2.7.tgz",
|
|
||||||
"integrity": "sha512-vdTnKtuBeB8Zp93DCbN0Qjf4odW2msVRq45r7lGKA6nwQGJFj6YemU54u3xPPkvDeZhG8DEEU64xbLwzVEBilQ==",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {}
|
|
||||||
},
|
|
||||||
"vite-plugin-pwa": {
|
"vite-plugin-pwa": {
|
||||||
"version": "0.14.4",
|
"version": "0.14.4",
|
||||||
"resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.14.4.tgz",
|
"resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.14.4.tgz",
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
"@formatjs/intl-localematcher": "~0.2.32",
|
"@formatjs/intl-localematcher": "~0.2.32",
|
||||||
"@github/text-expander-element": "~2.3.0",
|
"@github/text-expander-element": "~2.3.0",
|
||||||
"@iconify-icons/mingcute": "~1.2.4",
|
"@iconify-icons/mingcute": "~1.2.4",
|
||||||
|
"@justinribeiro/lite-youtube": "~1.5.0",
|
||||||
"@szhsin/react-menu": "~3.5.2",
|
"@szhsin/react-menu": "~3.5.2",
|
||||||
"dayjs": "~1.11.7",
|
"dayjs": "~1.11.7",
|
||||||
"dayjs-twitter": "~0.5.0",
|
"dayjs-twitter": "~0.5.0",
|
||||||
|
@ -25,8 +26,9 @@
|
||||||
"p-retry": "~5.1.2",
|
"p-retry": "~5.1.2",
|
||||||
"p-throttle": "~5.0.0",
|
"p-throttle": "~5.0.0",
|
||||||
"preact": "~10.13.1",
|
"preact": "~10.13.1",
|
||||||
"react-hotkeys-hook": "~4.3.7",
|
"react-hotkeys-hook": "~4.3.8",
|
||||||
"react-intersection-observer": "~9.4.3",
|
"react-intersection-observer": "~9.4.3",
|
||||||
|
"react-quick-pinch-zoom": "~4.6.0",
|
||||||
"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",
|
||||||
|
@ -44,10 +46,9 @@
|
||||||
"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.1.4",
|
"vite": "~4.2.1",
|
||||||
"vite-plugin-generate-file": "~0.0.4",
|
"vite-plugin-generate-file": "~0.0.4",
|
||||||
"vite-plugin-html-config": "~1.0.11",
|
"vite-plugin-html-config": "~1.0.11",
|
||||||
"vite-plugin-html-env": "~1.2.7",
|
|
||||||
"vite-plugin-pwa": "~0.14.4",
|
"vite-plugin-pwa": "~0.14.4",
|
||||||
"vite-plugin-remove-console": "~2.1.0",
|
"vite-plugin-remove-console": "~2.1.0",
|
||||||
"workbox-cacheable-response": "~6.5.4",
|
"workbox-cacheable-response": "~6.5.4",
|
||||||
|
|
246
src/app.css
246
src/app.css
|
@ -204,6 +204,9 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
|
||||||
.timeline.contextual > li:last-child {
|
.timeline.contextual > li:last-child {
|
||||||
background-size: 100% 20px;
|
background-size: 100% 20px;
|
||||||
}
|
}
|
||||||
|
.timeline.contextual > li:only-child {
|
||||||
|
background-image: none;
|
||||||
|
}
|
||||||
.timeline.contextual > li.descendant {
|
.timeline.contextual > li.descendant {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
@ -213,21 +216,40 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
|
||||||
.timeline.contextual > li.descendant:not(.thread) > .status-link {
|
.timeline.contextual > li.descendant:not(.thread) > .status-link {
|
||||||
padding-left: 40px;
|
padding-left: 40px;
|
||||||
}
|
}
|
||||||
|
.timeline.contextual .replies[data-comments-level='4'] {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
.timeline.contextual .replies[data-comments-level='4']:has(.replies) {
|
||||||
|
overflow: auto;
|
||||||
|
mask-image: linear-gradient(to left, transparent, black 32px);
|
||||||
|
}
|
||||||
.timeline.contextual
|
.timeline.contextual
|
||||||
> li.descendant.thread
|
.replies[data-comments-level='4']:has(.replies)
|
||||||
> .status-link
|
> .replies-summary {
|
||||||
+ .replies
|
border-top: 2px dashed var(--divider-color);
|
||||||
> summary {
|
}
|
||||||
margin-left: calc(
|
.timeline.contextual
|
||||||
var(--avatar-size) + var(--avatar-margin-start) + var(--avatar-margin-end)
|
.replies[data-comments-level='4']
|
||||||
);
|
.replies[data-comments-level-overflow='true']
|
||||||
|
.status {
|
||||||
|
min-width: min(15em, 75vw);
|
||||||
}
|
}
|
||||||
.timeline.contextual
|
.timeline.contextual
|
||||||
|
> li.descendant.thread
|
||||||
|
> .status-link
|
||||||
|
+ .replies
|
||||||
|
.replies-summary {
|
||||||
|
margin-left: calc(
|
||||||
|
var(--avatar-size) + var(--avatar-margin-start) + var(--avatar-margin-end) +
|
||||||
|
(var(--line-margin-end) * (var(--comments-level) - 1))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
/* .timeline.contextual
|
||||||
> li.descendant.thread
|
> li.descendant.thread
|
||||||
> .status-link
|
> .status-link
|
||||||
+ .replies
|
+ .replies
|
||||||
.replies
|
.replies
|
||||||
> summary {
|
> .replies-summary {
|
||||||
margin-left: calc(
|
margin-left: calc(
|
||||||
var(--avatar-size) + var(--avatar-margin-start) + var(--avatar-margin-end) +
|
var(--avatar-size) + var(--avatar-margin-start) + var(--avatar-margin-end) +
|
||||||
var(--line-margin-end)
|
var(--line-margin-end)
|
||||||
|
@ -239,22 +261,23 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
|
||||||
+ .replies
|
+ .replies
|
||||||
.replies
|
.replies
|
||||||
.replies
|
.replies
|
||||||
> summary {
|
> .replies-summary {
|
||||||
margin-left: calc(
|
margin-left: calc(
|
||||||
var(--avatar-size) + var(--avatar-margin-start) + var(--avatar-margin-end) +
|
var(--avatar-size) + var(--avatar-margin-start) + var(--avatar-margin-end) +
|
||||||
(var(--line-margin-end) * 2)
|
(var(--line-margin-end) * 2)
|
||||||
);
|
);
|
||||||
}
|
} */
|
||||||
.timeline.contextual
|
.timeline.contextual
|
||||||
> li.descendant.thread
|
> li.descendant.thread
|
||||||
> .status-link
|
> .status-link
|
||||||
+ .replies
|
+ .replies
|
||||||
.status-link {
|
.status-link {
|
||||||
padding-left: calc(
|
padding-left: calc(
|
||||||
var(--avatar-size) + var(--avatar-margin-start) + var(--avatar-margin-end)
|
var(--avatar-size) + var(--avatar-margin-start) + var(--avatar-margin-end) +
|
||||||
|
(var(--line-margin-end) * (var(--comments-level) - 1))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
.timeline.contextual
|
/* .timeline.contextual
|
||||||
> li.descendant.thread
|
> li.descendant.thread
|
||||||
> .status-link
|
> .status-link
|
||||||
+ .replies
|
+ .replies
|
||||||
|
@ -276,20 +299,22 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
|
||||||
var(--avatar-size) + var(--avatar-margin-start) + var(--avatar-margin-end) +
|
var(--avatar-size) + var(--avatar-margin-start) + var(--avatar-margin-end) +
|
||||||
(var(--line-margin-end) * 2)
|
(var(--line-margin-end) * 2)
|
||||||
);
|
);
|
||||||
}
|
} */
|
||||||
.timeline.contextual
|
.timeline.contextual
|
||||||
> li.descendant:not(.thread)
|
> li.descendant:not(.thread)
|
||||||
> .status-link
|
> .status-link
|
||||||
+ .replies
|
+ .replies
|
||||||
> summary {
|
.replies-summary {
|
||||||
margin-left: calc(var(--thread-start) + var(--line-margin-end));
|
margin-left: calc(
|
||||||
|
var(--thread-start) + var(--line-margin-end) * var(--comments-level)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
.timeline.contextual
|
/* .timeline.contextual
|
||||||
> li.descendant:not(.thread)
|
> li.descendant:not(.thread)
|
||||||
> .status-link
|
> .status-link
|
||||||
+ .replies
|
+ .replies
|
||||||
.replies
|
.replies
|
||||||
> summary {
|
> .replies-summary {
|
||||||
margin-left: calc(
|
margin-left: calc(
|
||||||
var(--thread-start) + var(--line-margin-end) + var(--line-margin-end)
|
var(--thread-start) + var(--line-margin-end) + var(--line-margin-end)
|
||||||
);
|
);
|
||||||
|
@ -300,19 +325,21 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
|
||||||
+ .replies
|
+ .replies
|
||||||
.replies
|
.replies
|
||||||
.replies
|
.replies
|
||||||
> summary {
|
> .replies-summary {
|
||||||
margin-left: calc(
|
margin-left: calc(
|
||||||
var(--thread-start) + var(--line-margin-end) + (var(--line-margin-end) * 2)
|
var(--thread-start) + var(--line-margin-end) + (var(--line-margin-end) * 2)
|
||||||
);
|
);
|
||||||
}
|
} */
|
||||||
.timeline.contextual
|
.timeline.contextual
|
||||||
> li.descendant:not(.thread)
|
> li.descendant:not(.thread)
|
||||||
> .status-link
|
> .status-link
|
||||||
+ .replies
|
+ .replies
|
||||||
.status-link {
|
.status-link {
|
||||||
padding-left: calc(var(--thread-start) + var(--line-margin-end));
|
padding-left: calc(
|
||||||
|
var(--thread-start) + var(--line-margin-end) * var(--comments-level)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
.timeline.contextual
|
/* .timeline.contextual
|
||||||
> li.descendant:not(.thread)
|
> li.descendant:not(.thread)
|
||||||
> .status-link
|
> .status-link
|
||||||
+ .replies
|
+ .replies
|
||||||
|
@ -328,7 +355,7 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
|
||||||
.replies
|
.replies
|
||||||
.status-link {
|
.status-link {
|
||||||
padding-left: calc(var(--thread-start) + (var(--line-margin-end) * 3));
|
padding-left: calc(var(--thread-start) + (var(--line-margin-end) * 3));
|
||||||
}
|
} */
|
||||||
.timeline.contextual > li.descendant:not(.thread):before {
|
.timeline.contextual > li.descendant:not(.thread):before {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -365,7 +392,7 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
}
|
}
|
||||||
.timeline.contextual > li .replies > summary {
|
.timeline.contextual > li .replies > .replies-summary {
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
background-color: var(--bg-faded-color);
|
background-color: var(--bg-faded-color);
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
@ -381,17 +408,17 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.timeline.contextual > li .replies > summary::-webkit-details-marker {
|
.timeline.contextual > li .replies > .replies-summary::-webkit-details-marker {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
.timeline.contextual > li .replies > summary > * {
|
.timeline.contextual > li .replies > .replies-summary > * {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
.timeline.contextual > li .replies > summary .avatars {
|
.timeline.contextual > li .replies > .replies-summary .avatars {
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
}
|
}
|
||||||
.timeline.contextual > li .replies > summary:active,
|
.timeline.contextual > li .replies > .replies-summary:active,
|
||||||
.timeline.contextual > li .replies[open] > summary {
|
.timeline.contextual > li .replies[open] > .replies-summary {
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
background-color: var(--comment-line-color);
|
background-color: var(--comment-line-color);
|
||||||
background-image: linear-gradient(
|
background-image: linear-gradient(
|
||||||
|
@ -400,17 +427,19 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
|
||||||
var(--bg-faded-color)
|
var(--bg-faded-color)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
.timeline.contextual > li .replies[open] > summary {
|
.timeline.contextual > li .replies[open] > .replies-summary {
|
||||||
border-bottom-left-radius: 0;
|
border-bottom-left-radius: 0;
|
||||||
}
|
}
|
||||||
.timeline.contextual > li .replies summary[hidden] {
|
.timeline.contextual > li .replies .replies-summary[hidden] {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
.timeline.contextual > li .replies li {
|
.timeline.contextual > li .replies li {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
.timeline.contextual > li .replies li {
|
.timeline.contextual > li .replies li {
|
||||||
--line-start: calc(var(--thread-start) + var(--line-margin-end));
|
--line-start: calc(
|
||||||
|
var(--thread-start) + var(--line-margin-end) * var(--comments-level)
|
||||||
|
);
|
||||||
--line-end: calc(var(--line-start) + var(--line-width));
|
--line-end: calc(var(--line-start) + var(--line-width));
|
||||||
background-image: linear-gradient(
|
background-image: linear-gradient(
|
||||||
to right,
|
to right,
|
||||||
|
@ -423,18 +452,19 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
|
||||||
);
|
);
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
}
|
}
|
||||||
.timeline.contextual > li .replies .replies li {
|
/* .timeline.contextual > li .replies .replies li {
|
||||||
--line-start: calc(var(--thread-start) + (var(--line-margin-end) * 2));
|
--line-start: calc(var(--thread-start) + (var(--line-margin-end) * 2));
|
||||||
}
|
}
|
||||||
.timeline.contextual > li .replies .replies .replies li {
|
.timeline.contextual > li .replies .replies .replies li {
|
||||||
--line-start: calc(var(--thread-start) + (var(--line-margin-end) * 3));
|
--line-start: calc(var(--thread-start) + (var(--line-margin-end) * 3));
|
||||||
}
|
} */
|
||||||
.timeline.contextual > li.thread .replies li {
|
.timeline.contextual > li.thread .replies li {
|
||||||
--line-start: calc(
|
--line-start: calc(
|
||||||
var(--avatar-size) + var(--avatar-margin-start) + var(--avatar-margin-end)
|
var(--avatar-size) + var(--avatar-margin-start) + var(--avatar-margin-end) +
|
||||||
|
(var(--line-margin-end) * (var(--comments-level) - 1))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
.timeline.contextual > li.thread .replies .replies li {
|
/* .timeline.contextual > li.thread .replies .replies li {
|
||||||
--line-start: calc(
|
--line-start: calc(
|
||||||
var(--avatar-size) + var(--avatar-margin-start) + var(--avatar-margin-end) +
|
var(--avatar-size) + var(--avatar-margin-start) + var(--avatar-margin-end) +
|
||||||
var(--line-margin-end)
|
var(--line-margin-end)
|
||||||
|
@ -445,7 +475,7 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
|
||||||
var(--avatar-size) + var(--avatar-margin-start) + var(--avatar-margin-end) +
|
var(--avatar-size) + var(--avatar-margin-start) + var(--avatar-margin-end) +
|
||||||
(var(--line-margin-end) * 2)
|
(var(--line-margin-end) * 2)
|
||||||
);
|
);
|
||||||
}
|
} */
|
||||||
.timeline.contextual > li .replies li:last-child {
|
.timeline.contextual > li .replies li:last-child {
|
||||||
background-size: 100% 20px;
|
background-size: 100% 20px;
|
||||||
}
|
}
|
||||||
|
@ -462,7 +492,7 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
|
||||||
border-color: transparent transparent var(--comment-line-color) transparent;
|
border-color: transparent transparent var(--comment-line-color) transparent;
|
||||||
transform: rotate(45deg);
|
transform: rotate(45deg);
|
||||||
}
|
}
|
||||||
.timeline.contextual > li .replies .replies li:before {
|
/* .timeline.contextual > li .replies .replies li:before {
|
||||||
--line-start: calc(
|
--line-start: calc(
|
||||||
var(--thread-start) + var(--line-margin-end) + var(--line-margin-end)
|
var(--thread-start) + var(--line-margin-end) + var(--line-margin-end)
|
||||||
);
|
);
|
||||||
|
@ -471,13 +501,14 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
|
||||||
--line-start: calc(
|
--line-start: calc(
|
||||||
var(--thread-start) + var(--line-margin-end) + (var(--line-margin-end) * 2)
|
var(--thread-start) + var(--line-margin-end) + (var(--line-margin-end) * 2)
|
||||||
);
|
);
|
||||||
}
|
} */
|
||||||
.timeline.contextual > li.thread .replies li:before {
|
.timeline.contextual > li.thread .replies li:before {
|
||||||
--line-start: calc(
|
--line-start: calc(
|
||||||
var(--avatar-size) + var(--avatar-margin-start) + var(--avatar-margin-end)
|
var(--avatar-size) + var(--avatar-margin-start) + var(--avatar-margin-end) +
|
||||||
|
(var(--line-margin-end) * (var(--comments-level) - 1))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
.timeline.contextual > li.thread .replies .replies li:before {
|
/* .timeline.contextual > li.thread .replies .replies li:before {
|
||||||
--line-start: calc(
|
--line-start: calc(
|
||||||
var(--avatar-size) + var(--avatar-margin-start) + var(--avatar-margin-end) +
|
var(--avatar-size) + var(--avatar-margin-start) + var(--avatar-margin-end) +
|
||||||
var(--line-margin-end)
|
var(--line-margin-end)
|
||||||
|
@ -488,7 +519,7 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
|
||||||
var(--avatar-size) + var(--avatar-margin-start) + var(--avatar-margin-end) +
|
var(--avatar-size) + var(--avatar-margin-start) + var(--avatar-margin-end) +
|
||||||
(var(--line-margin-end) * 2)
|
(var(--line-margin-end) * 2)
|
||||||
);
|
);
|
||||||
}
|
} */
|
||||||
.timeline.contextual.loading > li:not(.hero) {
|
.timeline.contextual.loading > li:not(.hero) {
|
||||||
/* opacity: 0.5; */
|
/* opacity: 0.5; */
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
@ -518,6 +549,48 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
|
||||||
margin-bottom: 3em;
|
margin-bottom: 3em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.timeline:not(.flat) > li.timeline-item-container {
|
||||||
|
--line-start: 40px;
|
||||||
|
--line-width: 3px;
|
||||||
|
--line-end: calc(var(--line-start) + var(--line-width));
|
||||||
|
background-image: linear-gradient(
|
||||||
|
to right,
|
||||||
|
transparent,
|
||||||
|
transparent var(--line-start),
|
||||||
|
var(--comment-line-color) var(--line-start),
|
||||||
|
var(--comment-line-color) var(--line-end),
|
||||||
|
transparent var(--line-end),
|
||||||
|
transparent
|
||||||
|
);
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
.timeline:not(.flat) > li.timeline-item-container-start {
|
||||||
|
margin-bottom: 0;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
border-bottom: 0;
|
||||||
|
background-position: 0 16px;
|
||||||
|
}
|
||||||
|
.timeline:not(.flat) > li.timeline-item-container-middle {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
border-bottom: 0;
|
||||||
|
border-top: 0;
|
||||||
|
}
|
||||||
|
.timeline:not(.flat) > li.timeline-item-container-end {
|
||||||
|
margin-top: 0;
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
border-top: 0;
|
||||||
|
background-size: 100% 20px;
|
||||||
|
}
|
||||||
|
.timeline:not(.flat)
|
||||||
|
> li:is(.timeline-item-container-middle, .timeline-item-container-end)
|
||||||
|
.status-reply-to:not(.visibility-direct) {
|
||||||
|
background-image: none;
|
||||||
|
}
|
||||||
|
|
||||||
.status-loading {
|
.status-loading {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: var(--text-insignificant-color);
|
color: var(--text-insignificant-color);
|
||||||
|
@ -615,6 +688,11 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
|
||||||
counter-increment: index;
|
counter-increment: index;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
@media (hover: hover) or (pointer: fine) {
|
||||||
|
.status-carousel ul {
|
||||||
|
scroll-snap-type: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
.status-carousel .content-container .content:only-child {
|
.status-carousel .content-container .content:only-child {
|
||||||
font-size: calc(100% + 25% * max(2 - var(--content-text-weight), 0));
|
font-size: calc(100% + 25% * max(2 - var(--content-text-weight), 0));
|
||||||
}
|
}
|
||||||
|
@ -784,7 +862,7 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
|
||||||
.carousel::-webkit-scrollbar {
|
.carousel::-webkit-scrollbar {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
.carousel > * {
|
.carousel .carousel-item {
|
||||||
scroll-snap-align: center;
|
scroll-snap-align: center;
|
||||||
scroll-snap-stop: always;
|
scroll-snap-stop: always;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
@ -802,7 +880,7 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
|
||||||
transparent 100%
|
transparent 100%
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
.carousel > * :is(img, video) {
|
.carousel .carousel-item :is(img, video) {
|
||||||
width: auto;
|
width: auto;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
|
@ -810,10 +888,24 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
|
||||||
max-height: 100dvh;
|
max-height: 100dvh;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
.carousel > * video {
|
.carousel .carousel-item video {
|
||||||
min-height: 80px;
|
min-height: 80px;
|
||||||
max-height: 80vh; /* prevent other UI elements from obscuring video */
|
max-height: 80vh; /* prevent other UI elements from obscuring video */
|
||||||
}
|
}
|
||||||
|
.carousel .carousel-item .media {
|
||||||
|
background-size: contain;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: env(safe-area-inset-top, 0) env(safe-area-inset-right, 0)
|
||||||
|
env(safe-area-inset-bottom, 0) env(safe-area-inset-left, 0);
|
||||||
|
background-origin: content-box;
|
||||||
|
}
|
||||||
|
|
||||||
.carousel-top-controls {
|
.carousel-top-controls {
|
||||||
top: 0;
|
top: 0;
|
||||||
|
@ -841,6 +933,9 @@ button.carousel-dot {
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
.carousel-top-controls .szh-menu-container {
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
:is(.button, button).carousel-button[hidden] {
|
:is(.button, button).carousel-button[hidden] {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
@ -1074,8 +1169,10 @@ body:has(.status-deck) .media-post-link {
|
||||||
max-width: 90vw;
|
max-width: 90vw;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.szh-menu__item--focusable {
|
.szh-menu[aria-label='Submenu'] {
|
||||||
background-color: transparent;
|
background-color: var(--bg-blur-color);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
box-shadow: 0 3px 24px -3px var(--drop-shadow-color);
|
||||||
}
|
}
|
||||||
.szh-menu__header {
|
.szh-menu__header {
|
||||||
margin: -8px 0 8px;
|
margin: -8px 0 8px;
|
||||||
|
@ -1088,6 +1185,10 @@ body:has(.status-deck) .media-post-link {
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
/* border-bottom: 1px solid var(--outline-color); */
|
/* border-bottom: 1px solid var(--outline-color); */
|
||||||
}
|
}
|
||||||
|
.szh-menu__header.plain {
|
||||||
|
margin-bottom: 0;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
.szh-menu__header * {
|
.szh-menu__header * {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
@ -1095,7 +1196,7 @@ body:has(.status-deck) .media-post-link {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
line-height: 1;
|
line-height: 1.1;
|
||||||
padding: 8px 16px !important;
|
padding: 8px 16px !important;
|
||||||
transition: all 0.1s ease-in-out;
|
transition: all 0.1s ease-in-out;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
@ -1103,6 +1204,9 @@ body:has(.status-deck) .media-post-link {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
.szh-menu .szh-menu__item--focusable {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
.szh-menu .szh-menu__item span {
|
.szh-menu .szh-menu__item span {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
@ -1147,11 +1251,11 @@ body:has(.status-deck) .media-post-link {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.szh-menu .menu-double-lines {
|
.szh-menu .menu-double-lines {
|
||||||
white-space: normal;
|
white-space: normal !important;
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
-webkit-line-clamp: 2;
|
-webkit-line-clamp: 2;
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
overflow: hidden;
|
overflow: hidden !important;
|
||||||
}
|
}
|
||||||
.szh-menu .menu-double-lines span {
|
.szh-menu .menu-double-lines span {
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
|
@ -1183,6 +1287,15 @@ body:has(.status-deck) .media-post-link {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.szh-menu .menu-wrap {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.szh-menu .menu-wrap > * {
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-basis: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
/* GLASS MENU */
|
/* GLASS MENU */
|
||||||
|
|
||||||
.glass-menu {
|
.glass-menu {
|
||||||
|
@ -1251,6 +1364,10 @@ meter.donut:is(.warning, .danger, .explode):after {
|
||||||
meter.donut:is(.danger, .explode):after {
|
meter.donut:is(.danger, .explode):after {
|
||||||
color: var(--red-color);
|
color: var(--red-color);
|
||||||
}
|
}
|
||||||
|
meter.donut[hidden] {
|
||||||
|
display: inline-block;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
/* SHINY PILL */
|
/* SHINY PILL */
|
||||||
|
|
||||||
|
@ -1416,6 +1533,31 @@ ul.link-list li a .icon {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* NAV MENU BUTTON */
|
||||||
|
|
||||||
|
.nav-menu-button.with-avatar {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.nav-menu-button:is(:hover, :focus):not(:active) {
|
||||||
|
filter: none !important;
|
||||||
|
}
|
||||||
|
.nav-menu-button .avatar {
|
||||||
|
transition: box-shadow 0.3s ease-out;
|
||||||
|
}
|
||||||
|
.nav-menu-button:is(:hover, :focus, .active) .avatar {
|
||||||
|
box-shadow: 0 0 0 2px var(--bg-color), 0 0 0 4px var(--link-light-color);
|
||||||
|
}
|
||||||
|
.nav-menu-button.with-avatar .icon {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 4px;
|
||||||
|
right: 8px;
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
.nav-menu-button.with-avatar:hover:not(:active, .active) .icon {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
/* COLUMNS */
|
/* COLUMNS */
|
||||||
|
|
||||||
#columns {
|
#columns {
|
||||||
|
@ -1562,17 +1704,21 @@ ul.link-list li a .icon {
|
||||||
--back-transition: transform 0.4s ease-out;
|
--back-transition: transform 0.4s ease-out;
|
||||||
}
|
}
|
||||||
.timeline:not(.flat) > li > a {
|
.timeline:not(.flat) > li > a {
|
||||||
border-radius: var(--item-radius);
|
border-radius: inherit;
|
||||||
}
|
}
|
||||||
.timeline:not(.flat) > li:not(:has(.status-carousel)) {
|
.timeline:not(.flat) > li:not(:has(.status-carousel)) {
|
||||||
transform: translate3d(0, 0, 0);
|
transform: translate3d(0, 0, 0);
|
||||||
}
|
}
|
||||||
|
.timeline:not(.flat) > li:has(.status-badge) {
|
||||||
|
border-top-right-radius: 8px;
|
||||||
|
}
|
||||||
.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:not(.flat)
|
.timeline:not(.flat)
|
||||||
> li:not(:has(.status-carousel)):has(+ li .status-link.is-active),
|
> li:not(:has(.status-carousel)):has(+ li .status-link.is-active),
|
||||||
|
.timeline:not(.flat) > li.timeline-item-container:has(.status-link.is-active),
|
||||||
.timeline:not(.flat)
|
.timeline:not(.flat)
|
||||||
> li:not(:has(.status-carousel)):has(.status-link.is-active)
|
> li:not(:has(.status-carousel)):has(.status-link.is-active)
|
||||||
+ li {
|
+ li {
|
||||||
|
|
179
src/app.jsx
179
src/app.jsx
|
@ -33,7 +33,6 @@ import FollowedHashtags from './pages/followed-hashtags';
|
||||||
import Following from './pages/following';
|
import Following from './pages/following';
|
||||||
import Hashtag from './pages/hashtag';
|
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 List from './pages/list';
|
||||||
import Lists from './pages/lists';
|
import Lists from './pages/lists';
|
||||||
import Login from './pages/login';
|
import Login from './pages/login';
|
||||||
|
@ -140,10 +139,6 @@ function App() {
|
||||||
let location = useLocation();
|
let location = useLocation();
|
||||||
states.currentLocation = location.pathname;
|
states.currentLocation = location.pathname;
|
||||||
|
|
||||||
const locationDeckMap = {
|
|
||||||
'/': 'home-page',
|
|
||||||
'/notifications': 'notifications-page',
|
|
||||||
};
|
|
||||||
const focusDeck = () => {
|
const focusDeck = () => {
|
||||||
let timer = setTimeout(() => {
|
let timer = setTimeout(() => {
|
||||||
const columns = document.getElementById('columns');
|
const columns = document.getElementById('columns');
|
||||||
|
@ -161,11 +156,6 @@ function App() {
|
||||||
page.focus();
|
page.focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// const page = document.getElementById(locationDeckMap[location.pathname]);
|
|
||||||
// console.debug('FOCUS', location.pathname, page);
|
|
||||||
// if (page) {
|
|
||||||
// page.focus();
|
|
||||||
// }
|
|
||||||
}, 100);
|
}, 100);
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
};
|
};
|
||||||
|
@ -182,59 +172,6 @@ function App() {
|
||||||
if (!showModal) focusDeck();
|
if (!showModal) focusDeck();
|
||||||
}, [showModal]);
|
}, [showModal]);
|
||||||
|
|
||||||
// Notifications service
|
|
||||||
// - WebSocket to receive notifications when page is visible
|
|
||||||
const [visible, setVisible] = useState(true);
|
|
||||||
usePageVisibility(setVisible);
|
|
||||||
const notificationStream = useRef();
|
|
||||||
useEffect(() => {
|
|
||||||
if (isLoggedIn && visible) {
|
|
||||||
const { masto, instance } = api();
|
|
||||||
(async () => {
|
|
||||||
// 1. Get the latest notification
|
|
||||||
if (states.notificationsLast) {
|
|
||||||
const notificationsIterator = masto.v1.notifications.list({
|
|
||||||
limit: 1,
|
|
||||||
since_id: states.notificationsLast.id,
|
|
||||||
});
|
|
||||||
const { value: notifications } = await notificationsIterator.next();
|
|
||||||
if (notifications?.length) {
|
|
||||||
states.notificationsShowNew = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Start streaming
|
|
||||||
notificationStream.current = await masto.ws.stream(
|
|
||||||
'/api/v1/streaming',
|
|
||||||
{
|
|
||||||
stream: 'user:notification',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
console.log('🎏 Streaming notification', notificationStream.current);
|
|
||||||
|
|
||||||
notificationStream.current.on('notification', (notification) => {
|
|
||||||
console.log('🔔🔔 Notification', notification);
|
|
||||||
if (notification.status) {
|
|
||||||
saveStatus(notification.status, instance, {
|
|
||||||
skipThreading: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
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 =
|
const isModalPage =
|
||||||
|
@ -255,34 +192,6 @@ function App() {
|
||||||
return !/^\/(login|welcome)/.test(pathname);
|
return !/^\/(login|welcome)/.test(pathname);
|
||||||
}, [location]);
|
}, [location]);
|
||||||
|
|
||||||
const lastCheckDate = useRef();
|
|
||||||
const checkForUpdates = () => {
|
|
||||||
lastCheckDate.current = Date.now();
|
|
||||||
console.log('✨ Check app update');
|
|
||||||
fetch('./version.json')
|
|
||||||
.then((r) => r.json())
|
|
||||||
.then((info) => {
|
|
||||||
if (info) states.appVersion = info;
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
console.error(e);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
useInterval(() => checkForUpdates, visible && 1000 * 60 * 30); // 30 minutes
|
|
||||||
usePageVisibility((visible) => {
|
|
||||||
if (visible) {
|
|
||||||
if (!lastCheckDate.current) {
|
|
||||||
checkForUpdates();
|
|
||||||
} else {
|
|
||||||
const diff = Date.now() - lastCheckDate.current;
|
|
||||||
if (diff > 1000 * 60 * 60) {
|
|
||||||
// 1 hour
|
|
||||||
checkForUpdates();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Routes location={nonRootLocation || location}>
|
<Routes location={nonRootLocation || location}>
|
||||||
|
@ -306,7 +215,6 @@ function App() {
|
||||||
<Route path="/notifications" element={<Notifications />} />
|
<Route path="/notifications" element={<Notifications />} />
|
||||||
)}
|
)}
|
||||||
{isLoggedIn && <Route path="/following" element={<Following />} />}
|
{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 && (
|
{isLoggedIn && (
|
||||||
|
@ -472,8 +380,95 @@ function App() {
|
||||||
<ShortcutsSettings />
|
<ShortcutsSettings />
|
||||||
</Modal>
|
</Modal>
|
||||||
)}
|
)}
|
||||||
|
<BackgroundService isLoggedIn={isLoggedIn} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function BackgroundService({ isLoggedIn }) {
|
||||||
|
// Notifications service
|
||||||
|
// - WebSocket to receive notifications when page is visible
|
||||||
|
const [visible, setVisible] = useState(true);
|
||||||
|
usePageVisibility(setVisible);
|
||||||
|
const notificationStream = useRef();
|
||||||
|
useEffect(() => {
|
||||||
|
if (isLoggedIn && visible) {
|
||||||
|
const { masto, instance } = api();
|
||||||
|
(async () => {
|
||||||
|
// 1. Get the latest notification
|
||||||
|
if (states.notificationsLast) {
|
||||||
|
const notificationsIterator = masto.v1.notifications.list({
|
||||||
|
limit: 1,
|
||||||
|
since_id: states.notificationsLast.id,
|
||||||
|
});
|
||||||
|
const { value: notifications } = await notificationsIterator.next();
|
||||||
|
if (notifications?.length) {
|
||||||
|
states.notificationsShowNew = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Start streaming
|
||||||
|
notificationStream.current = await masto.ws.stream(
|
||||||
|
'/api/v1/streaming',
|
||||||
|
{
|
||||||
|
stream: 'user:notification',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
console.log('🎏 Streaming notification', notificationStream.current);
|
||||||
|
|
||||||
|
notificationStream.current.on('notification', (notification) => {
|
||||||
|
console.log('🔔🔔 Notification', notification);
|
||||||
|
if (notification.status) {
|
||||||
|
saveStatus(notification.status, instance, {
|
||||||
|
skipThreading: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
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]);
|
||||||
|
|
||||||
|
// Check for updates service
|
||||||
|
const lastCheckDate = useRef();
|
||||||
|
const checkForUpdates = () => {
|
||||||
|
lastCheckDate.current = Date.now();
|
||||||
|
console.log('✨ Check app update');
|
||||||
|
fetch('./version.json')
|
||||||
|
.then((r) => r.json())
|
||||||
|
.then((info) => {
|
||||||
|
if (info) states.appVersion = info;
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
useInterval(() => checkForUpdates, visible && 1000 * 60 * 30); // 30 minutes
|
||||||
|
usePageVisibility((visible) => {
|
||||||
|
if (visible) {
|
||||||
|
if (!lastCheckDate.current) {
|
||||||
|
checkForUpdates();
|
||||||
|
} else {
|
||||||
|
const diff = Date.now() - lastCheckDate.current;
|
||||||
|
if (diff > 1000 * 60 * 60) {
|
||||||
|
// 1 hour
|
||||||
|
checkForUpdates();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
export { App };
|
export { App };
|
||||||
|
|
|
@ -153,10 +153,14 @@
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
min-height: 2.5em;
|
min-height: 2.5em;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
.account-container .actions button {
|
.account-container .actions button {
|
||||||
align-self: flex-end;
|
align-self: flex-end;
|
||||||
}
|
}
|
||||||
|
.account-container .actions .buttons {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
.account-container .profile-metadata {
|
.account-container .profile-metadata {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
@ -1,13 +1,22 @@
|
||||||
import './account-info.css';
|
import './account-info.css';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Menu,
|
||||||
|
MenuDivider,
|
||||||
|
MenuHeader,
|
||||||
|
MenuItem,
|
||||||
|
SubMenu,
|
||||||
|
} from '@szhsin/react-menu';
|
||||||
import { useEffect, useRef, useState } from 'preact/hooks';
|
import { useEffect, useRef, useState } from 'preact/hooks';
|
||||||
|
|
||||||
import { api } from '../utils/api';
|
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 getHTMLText from '../utils/getHTMLText';
|
||||||
import handleContentLinks from '../utils/handle-content-links';
|
import handleContentLinks from '../utils/handle-content-links';
|
||||||
import niceDateTime from '../utils/nice-date-time';
|
import niceDateTime from '../utils/nice-date-time';
|
||||||
import shortenNumber from '../utils/shorten-number';
|
import shortenNumber from '../utils/shorten-number';
|
||||||
|
import showToast from '../utils/show-toast';
|
||||||
import states, { hideAllModals } from '../utils/states';
|
import states, { hideAllModals } from '../utils/states';
|
||||||
import store from '../utils/store';
|
import store from '../utils/store';
|
||||||
|
|
||||||
|
@ -15,6 +24,29 @@ 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 Modal from './modal';
|
||||||
|
import TranslationBlock from './translation-block';
|
||||||
|
|
||||||
|
const MUTE_DURATIONS = [
|
||||||
|
1000 * 60 * 5, // 5 minutes
|
||||||
|
1000 * 60 * 30, // 30 minutes
|
||||||
|
1000 * 60 * 60, // 1 hour
|
||||||
|
1000 * 60 * 60 * 6, // 6 hours
|
||||||
|
1000 * 60 * 60 * 24, // 1 day
|
||||||
|
1000 * 60 * 60 * 24 * 3, // 3 days
|
||||||
|
1000 * 60 * 60 * 24 * 7, // 1 week
|
||||||
|
0, // forever
|
||||||
|
];
|
||||||
|
const MUTE_DURATIONS_LABELS = {
|
||||||
|
0: 'Forever',
|
||||||
|
300_000: '5 minutes',
|
||||||
|
1_800_000: '30 minutes',
|
||||||
|
3_600_000: '1 hour',
|
||||||
|
21_600_000: '6 hours',
|
||||||
|
86_400_000: '1 day',
|
||||||
|
259_200_000: '3 days',
|
||||||
|
604_800_000: '1 week',
|
||||||
|
};
|
||||||
|
|
||||||
function AccountInfo({
|
function AccountInfo({
|
||||||
account,
|
account,
|
||||||
|
@ -359,7 +391,7 @@ function RelatedActions({ info, instance, authenticated }) {
|
||||||
const [relationship, setRelationship] = useState(null);
|
const [relationship, setRelationship] = useState(null);
|
||||||
const [familiarFollowers, setFamiliarFollowers] = useState([]);
|
const [familiarFollowers, setFamiliarFollowers] = useState([]);
|
||||||
|
|
||||||
const { id, locked } = info;
|
const { id, acct, url, username, locked, lastStatusAt, note, fields } = info;
|
||||||
const accountID = useRef(id);
|
const accountID = useRef(id);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
@ -376,6 +408,9 @@ function RelatedActions({ info, instance, authenticated }) {
|
||||||
endorsed,
|
endorsed,
|
||||||
} = relationship || {};
|
} = relationship || {};
|
||||||
|
|
||||||
|
const [currentInfo, setCurrentInfo] = useState(null);
|
||||||
|
const [isSelf, setIsSelf] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (info) {
|
if (info) {
|
||||||
const currentAccount = store.session.get('currentAccount');
|
const currentAccount = store.session.get('currentAccount');
|
||||||
|
@ -394,7 +429,10 @@ function RelatedActions({ info, instance, authenticated }) {
|
||||||
resolve: true,
|
resolve: true,
|
||||||
});
|
});
|
||||||
console.log('🥏 Fetched account from logged-in instance', results);
|
console.log('🥏 Fetched account from logged-in instance', results);
|
||||||
currentID = results.accounts[0].id;
|
if (results.accounts.length) {
|
||||||
|
currentID = results.accounts[0].id;
|
||||||
|
setCurrentInfo(results.accounts[0]);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
|
@ -404,6 +442,7 @@ function RelatedActions({ info, instance, authenticated }) {
|
||||||
|
|
||||||
if (currentAccount === currentID) {
|
if (currentAccount === currentID) {
|
||||||
// It's myself!
|
// It's myself!
|
||||||
|
setIsSelf(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -444,6 +483,11 @@ function RelatedActions({ info, instance, authenticated }) {
|
||||||
}
|
}
|
||||||
}, [info, authenticated]);
|
}, [info, authenticated]);
|
||||||
|
|
||||||
|
const loading = relationshipUIState === 'loading';
|
||||||
|
const menuInstanceRef = useRef(null);
|
||||||
|
|
||||||
|
const [showTranslatedBio, setShowTranslatedBio] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{familiarFollowers?.length > 0 && (
|
{familiarFollowers?.length > 0 && (
|
||||||
|
@ -473,67 +517,329 @@ function RelatedActions({ info, instance, authenticated }) {
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<p class="actions">
|
<p class="actions">
|
||||||
{followedBy ? <span class="tag">Following you</span> : <span />}{' '}
|
{followedBy ? (
|
||||||
{relationshipUIState !== 'loading' && relationship && (
|
<span class="tag">Following you</span>
|
||||||
<button
|
) : !!lastStatusAt ? (
|
||||||
type="button"
|
<span class="insignificant">
|
||||||
class={`${following || requested ? 'light swap' : ''}`}
|
Last status:{' '}
|
||||||
data-swap-state={following || requested ? 'danger' : ''}
|
{niceDateTime(lastStatusAt, {
|
||||||
disabled={relationshipUIState === 'loading'}
|
hideTime: true,
|
||||||
onClick={() => {
|
})}
|
||||||
setRelationshipUIState('loading');
|
</span>
|
||||||
|
) : (
|
||||||
|
<span />
|
||||||
|
)}{' '}
|
||||||
|
<span class="buttons">
|
||||||
|
<Menu
|
||||||
|
instanceRef={menuInstanceRef}
|
||||||
|
portal={{
|
||||||
|
target: document.body,
|
||||||
|
}}
|
||||||
|
containerProps={{
|
||||||
|
style: {
|
||||||
|
// Higher than the backdrop
|
||||||
|
zIndex: 1001,
|
||||||
|
},
|
||||||
|
onClick: (e) => {
|
||||||
|
if (e.target === e.currentTarget) {
|
||||||
|
menuInstanceRef.current?.closeMenu?.();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
align="center"
|
||||||
|
position="anchor"
|
||||||
|
overflow="auto"
|
||||||
|
boundingBoxPadding="8 8 8 8"
|
||||||
|
menuButton={
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
title="More"
|
||||||
|
class="plain"
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
<Icon icon="more" size="l" alt="More" />
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{currentAuthenticated && !isSelf && (
|
||||||
|
<>
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
states.showCompose = {
|
||||||
|
draftStatus: {
|
||||||
|
status: `@${currentInfo?.acct || acct} `,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon="at" />
|
||||||
|
<span>Mention @{username}</span>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
setShowTranslatedBio(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon="translate" />
|
||||||
|
<span>Translate bio</span>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuDivider />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<MenuItem href={url} target="_blank">
|
||||||
|
<Icon icon="external" />
|
||||||
|
<small class="menu-double-lines">{niceAccountURL(url)}</small>
|
||||||
|
</MenuItem>
|
||||||
|
<div class="menu-horizontal">
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
// Copy url to clipboard
|
||||||
|
try {
|
||||||
|
navigator.clipboard.writeText(url);
|
||||||
|
showToast('Link copied');
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
showToast('Unable to copy link');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon="link" />
|
||||||
|
<span>Copy</span>
|
||||||
|
</MenuItem>
|
||||||
|
{navigator?.share &&
|
||||||
|
navigator?.canShare?.({
|
||||||
|
url,
|
||||||
|
}) && (
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
try {
|
||||||
|
navigator.share({
|
||||||
|
url,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
alert("Sharing doesn't seem to work.");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon="share" />
|
||||||
|
<span>Share…</span>
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{!!relationship && (
|
||||||
|
<>
|
||||||
|
<MenuDivider />
|
||||||
|
{muting ? (
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
setRelationshipUIState('loading');
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const newRelationship =
|
||||||
|
await currentMasto.v1.accounts.unmute(
|
||||||
|
currentInfo?.id || id,
|
||||||
|
);
|
||||||
|
console.log('unmuting', newRelationship);
|
||||||
|
setRelationship(newRelationship);
|
||||||
|
setRelationshipUIState('default');
|
||||||
|
showToast(`Unmuted @${username}`);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
setRelationshipUIState('error');
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon="unmute" />
|
||||||
|
<span>Unmute @{username}</span>
|
||||||
|
</MenuItem>
|
||||||
|
) : (
|
||||||
|
<SubMenu
|
||||||
|
openTrigger="clickOnly"
|
||||||
|
direction="bottom"
|
||||||
|
overflow="auto"
|
||||||
|
offsetX={-16}
|
||||||
|
label={
|
||||||
|
<>
|
||||||
|
<Icon icon="mute" />
|
||||||
|
<span class="menu-grow">Mute @{username}…</span>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
textOverflow: 'clip',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon="time" />
|
||||||
|
<Icon icon="chevron-right" />
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div class="menu-wrap">
|
||||||
|
{MUTE_DURATIONS.map((duration) => (
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
setRelationshipUIState('loading');
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const newRelationship =
|
||||||
|
await currentMasto.v1.accounts.mute(
|
||||||
|
currentInfo?.id || id,
|
||||||
|
{
|
||||||
|
duration,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
console.log('muting', newRelationship);
|
||||||
|
setRelationship(newRelationship);
|
||||||
|
setRelationshipUIState('default');
|
||||||
|
showToast(
|
||||||
|
`Muted @${username} for ${MUTE_DURATIONS_LABELS[duration]}`,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
setRelationshipUIState('error');
|
||||||
|
showToast(`Unable to mute @${username}`);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{MUTE_DURATIONS_LABELS[duration]}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</SubMenu>
|
||||||
|
)}
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
if (!blocking && !confirm(`Block @${username}?`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setRelationshipUIState('loading');
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
if (blocking) {
|
||||||
|
const newRelationship =
|
||||||
|
await currentMasto.v1.accounts.unblock(
|
||||||
|
currentInfo?.id || id,
|
||||||
|
);
|
||||||
|
console.log('unblocking', newRelationship);
|
||||||
|
setRelationship(newRelationship);
|
||||||
|
setRelationshipUIState('default');
|
||||||
|
showToast(`Unblocked @${username}`);
|
||||||
|
} else {
|
||||||
|
const newRelationship =
|
||||||
|
await currentMasto.v1.accounts.block(
|
||||||
|
currentInfo?.id || id,
|
||||||
|
);
|
||||||
|
console.log('blocking', newRelationship);
|
||||||
|
setRelationship(newRelationship);
|
||||||
|
setRelationshipUIState('default');
|
||||||
|
showToast(`Blocked @${username}`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
setRelationshipUIState('error');
|
||||||
|
if (blocking) {
|
||||||
|
showToast(`Unable to unblock @${username}`);
|
||||||
|
} else {
|
||||||
|
showToast(`Unable to block @${username}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{blocking ? (
|
||||||
|
<>
|
||||||
|
<Icon icon="unblock" />
|
||||||
|
<span>Unblock @{username}</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Icon icon="block" />
|
||||||
|
<span>Block @{username}…</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</MenuItem>
|
||||||
|
{/* <MenuItem>
|
||||||
|
<Icon icon="flag" />
|
||||||
|
<span>Report @{username}…</span>
|
||||||
|
</MenuItem> */}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Menu>
|
||||||
|
{!!relationship && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class={`${following || requested ? 'light swap' : ''}`}
|
||||||
|
data-swap-state={following || requested ? 'danger' : ''}
|
||||||
|
disabled={loading}
|
||||||
|
onClick={() => {
|
||||||
|
setRelationshipUIState('loading');
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
let newRelationship;
|
||||||
|
|
||||||
(async () => {
|
if (following || requested) {
|
||||||
try {
|
const yes = confirm(
|
||||||
let newRelationship;
|
requested
|
||||||
|
? 'Withdraw follow request?'
|
||||||
|
: `Unfollow @${info.acct || info.username}?`,
|
||||||
|
);
|
||||||
|
|
||||||
if (following || requested) {
|
if (yes) {
|
||||||
const yes = confirm(
|
newRelationship =
|
||||||
requested
|
await currentMasto.v1.accounts.unfollow(
|
||||||
? 'Withdraw follow request?'
|
accountID.current,
|
||||||
: `Unfollow @${info.acct || info.username}?`,
|
);
|
||||||
);
|
}
|
||||||
|
} else {
|
||||||
if (yes) {
|
newRelationship = await currentMasto.v1.accounts.follow(
|
||||||
newRelationship = await currentMasto.v1.accounts.unfollow(
|
|
||||||
accountID.current,
|
accountID.current,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
newRelationship = await currentMasto.v1.accounts.follow(
|
|
||||||
accountID.current,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newRelationship) setRelationship(newRelationship);
|
if (newRelationship) setRelationship(newRelationship);
|
||||||
setRelationshipUIState('default');
|
setRelationshipUIState('default');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert(e);
|
alert(e);
|
||||||
setRelationshipUIState('error');
|
setRelationshipUIState('error');
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{following ? (
|
{following ? (
|
||||||
<>
|
<>
|
||||||
<span>Following</span>
|
<span>Following</span>
|
||||||
<span>Unfollow…</span>
|
<span>Unfollow…</span>
|
||||||
</>
|
</>
|
||||||
) : requested ? (
|
) : requested ? (
|
||||||
<>
|
<>
|
||||||
<span>Requested</span>
|
<span>Requested</span>
|
||||||
<span>Withdraw…</span>
|
<span>Withdraw…</span>
|
||||||
</>
|
</>
|
||||||
) : locked ? (
|
) : locked ? (
|
||||||
<>
|
<>
|
||||||
<Icon icon="lock" /> <span>Follow</span>
|
<Icon icon="lock" /> <span>Follow</span>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
'Follow'
|
'Follow'
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
</span>
|
||||||
</p>
|
</p>
|
||||||
|
{!!showTranslatedBio && (
|
||||||
|
<Modal
|
||||||
|
class="light"
|
||||||
|
onClick={(e) => {
|
||||||
|
if (e.target === e.currentTarget) {
|
||||||
|
setShowTranslatedBio(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TranslatedBioSheet note={note} fields={fields} />
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -554,4 +860,44 @@ function lightenRGB([r, g, b]) {
|
||||||
return [r, g, b, alpha];
|
return [r, g, b, alpha];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function niceAccountURL(url) {
|
||||||
|
if (!url) return;
|
||||||
|
const urlObj = new URL(url);
|
||||||
|
const { host, pathname } = urlObj;
|
||||||
|
const path = pathname.replace(/\/$/, '').replace(/^\//, '');
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<span class="more-insignificant">{host}/</span>
|
||||||
|
<wbr />
|
||||||
|
<span>{path}</span>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TranslatedBioSheet({ note, fields }) {
|
||||||
|
const fieldsText =
|
||||||
|
fields
|
||||||
|
?.map(({ name, value }) => `${name}\n${getHTMLText(value)}`)
|
||||||
|
.join('\n\n') || '';
|
||||||
|
|
||||||
|
const text = getHTMLText(note) + (fieldsText ? `\n\n${fieldsText}` : '');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="sheet">
|
||||||
|
<header>
|
||||||
|
<h2>Translated Bio</h2>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
whiteSpace: 'pre-wrap',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</p>
|
||||||
|
<TranslationBlock forceTranslate text={text} />
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
export default AccountInfo;
|
export default AccountInfo;
|
||||||
|
|
|
@ -16,6 +16,7 @@ const alphaCache = {};
|
||||||
function Avatar({ url, size, alt = '', ...props }) {
|
function Avatar({ url, size, alt = '', ...props }) {
|
||||||
size = SIZES[size] || size || SIZES.m;
|
size = SIZES[size] || size || SIZES.m;
|
||||||
const avatarRef = useRef();
|
const avatarRef = useRef();
|
||||||
|
const isMissing = /missing\.png$/.test(url);
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
ref={avatarRef}
|
ref={avatarRef}
|
||||||
|
@ -34,7 +35,11 @@ function Avatar({ url, size, alt = '', ...props }) {
|
||||||
height={size}
|
height={size}
|
||||||
alt={alt}
|
alt={alt}
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
crossOrigin={alphaCache[url] === undefined ? 'anonymous' : undefined}
|
crossOrigin={
|
||||||
|
alphaCache[url] === undefined && !isMissing
|
||||||
|
? 'anonymous'
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
if (e.target.crossOrigin) {
|
if (e.target.crossOrigin) {
|
||||||
e.target.crossOrigin = null;
|
e.target.crossOrigin = null;
|
||||||
|
@ -43,6 +48,8 @@ function Avatar({ url, size, alt = '', ...props }) {
|
||||||
}}
|
}}
|
||||||
onLoad={(e) => {
|
onLoad={(e) => {
|
||||||
if (avatarRef.current) avatarRef.current.dataset.loaded = true;
|
if (avatarRef.current) avatarRef.current.dataset.loaded = true;
|
||||||
|
if (alphaCache[url] !== undefined) return;
|
||||||
|
if (isMissing) return;
|
||||||
try {
|
try {
|
||||||
// Check if image has alpha channel
|
// Check if image has alpha channel
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
|
@ -65,10 +72,11 @@ function Avatar({ url, size, alt = '', ...props }) {
|
||||||
if (hasAlpha) {
|
if (hasAlpha) {
|
||||||
// console.log('hasAlpha', hasAlpha, allPixels.data);
|
// console.log('hasAlpha', hasAlpha, allPixels.data);
|
||||||
avatarRef.current.classList.add('has-alpha');
|
avatarRef.current.classList.add('has-alpha');
|
||||||
alphaCache[url] = true;
|
|
||||||
}
|
}
|
||||||
|
alphaCache[url] = hasAlpha;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Ignore
|
// Silent fail
|
||||||
|
alphaCache[url] = false;
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,14 +1,18 @@
|
||||||
|
#compose-container-outer {
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
height: 100dvh;
|
||||||
|
overflow: auto;
|
||||||
|
align-self: flex-start;
|
||||||
|
padding: env(safe-area-inset-top) env(safe-area-inset-right)
|
||||||
|
env(safe-area-inset-bottom) env(safe-area-inset-left);
|
||||||
|
}
|
||||||
#compose-container {
|
#compose-container {
|
||||||
|
margin: auto;
|
||||||
width: var(--main-width);
|
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;
|
||||||
max-height: 100vh;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
#compose-container.standalone {
|
|
||||||
max-height: none;
|
|
||||||
margin: auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#compose-container .compose-top {
|
#compose-container .compose-top {
|
||||||
|
@ -26,8 +30,8 @@
|
||||||
#compose-container textarea {
|
#compose-container textarea {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
height: 4em;
|
height: 5em;
|
||||||
min-height: 4em;
|
min-height: 5em;
|
||||||
max-height: 50vh;
|
max-height: 50vh;
|
||||||
resize: vertical;
|
resize: vertical;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
|
@ -41,6 +45,7 @@
|
||||||
#compose-container textarea {
|
#compose-container textarea {
|
||||||
font-size: 150%;
|
font-size: 150%;
|
||||||
font-size: calc(100% + 50% / var(--text-weight));
|
font-size: calc(100% + 50% / var(--text-weight));
|
||||||
|
max-height: 65vh;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,6 +70,9 @@
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
box-shadow: 0 -3px 12px -3px var(--drop-shadow-color);
|
box-shadow: 0 -3px 12px -3px var(--drop-shadow-color);
|
||||||
}
|
}
|
||||||
|
#compose-container .status-preview:has(.status-badge) {
|
||||||
|
border-top-right-radius: 8px;
|
||||||
|
}
|
||||||
#compose-container .status-preview :is(.hashtag, .time) {
|
#compose-container .status-preview :is(.hashtag, .time) {
|
||||||
/* Prevent hashtags from being clickable */
|
/* Prevent hashtags from being clickable */
|
||||||
/* TODO: maybe use a different solution? */
|
/* TODO: maybe use a different solution? */
|
||||||
|
@ -130,6 +138,9 @@
|
||||||
padding: 8px 0;
|
padding: 8px 0;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
#compose-container .toolbar.wrap {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
#compose-container .toolbar.stretch {
|
#compose-container .toolbar.stretch {
|
||||||
justify-content: stretch;
|
justify-content: stretch;
|
||||||
}
|
}
|
||||||
|
@ -140,7 +151,7 @@
|
||||||
#compose-container .toolbar-button {
|
#compose-container .toolbar-button {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
color: var(--link-color);
|
color: var(--link-color);
|
||||||
background-color: var(--bg-blur-color);
|
background-color: transparent;
|
||||||
padding: 0 8px;
|
padding: 0 8px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
min-height: 2.4em;
|
min-height: 2.4em;
|
||||||
|
@ -266,6 +277,19 @@
|
||||||
background-color: var(--bg-color);
|
background-color: var(--bg-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#compose-container .form-visibility-direct {
|
||||||
|
--yellow-stripes: repeating-linear-gradient(
|
||||||
|
-45deg,
|
||||||
|
var(--reply-to-faded-color),
|
||||||
|
var(--reply-to-faded-color) 10px,
|
||||||
|
var(--reply-to-faded-color) 10px,
|
||||||
|
transparent 10px,
|
||||||
|
transparent 20px
|
||||||
|
);
|
||||||
|
/* diagonal stripes of yellow */
|
||||||
|
background-image: var(--yellow-stripes);
|
||||||
|
}
|
||||||
|
|
||||||
#compose-container .media-attachments {
|
#compose-container .media-attachments {
|
||||||
background-color: var(--bg-faded-color);
|
background-color: var(--bg-faded-color);
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
|
@ -502,3 +526,43 @@
|
||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#custom-emojis-sheet {
|
||||||
|
max-height: 50vh;
|
||||||
|
max-height: 50dvh;
|
||||||
|
}
|
||||||
|
#custom-emojis-sheet main {
|
||||||
|
mask-image: none;
|
||||||
|
}
|
||||||
|
#custom-emojis-sheet .custom-emojis-list .section-header {
|
||||||
|
font-size: 80%;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--text-insignificant-color);
|
||||||
|
padding: 8px 0 4px;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
background-color: var(--bg-blur-color);
|
||||||
|
backdrop-filter: blur(1px);
|
||||||
|
}
|
||||||
|
#custom-emojis-sheet .custom-emojis-list section {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
#custom-emojis-sheet .custom-emojis-list button {
|
||||||
|
border-radius: 8px;
|
||||||
|
background-image: radial-gradient(
|
||||||
|
closest-side,
|
||||||
|
var(--img-bg-color),
|
||||||
|
transparent
|
||||||
|
);
|
||||||
|
}
|
||||||
|
#custom-emojis-sheet .custom-emojis-list button:is(:hover, :focus) {
|
||||||
|
filter: none;
|
||||||
|
background-color: var(--bg-faded-color);
|
||||||
|
}
|
||||||
|
#custom-emojis-sheet .custom-emojis-list button img {
|
||||||
|
transition: transform 0.1s ease-out;
|
||||||
|
}
|
||||||
|
#custom-emojis-sheet .custom-emojis-list button:is(:hover, :focus) img {
|
||||||
|
transform: scale(1.5);
|
||||||
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -65,6 +65,15 @@ const ICONS = {
|
||||||
exit: 'mingcute:exit-line',
|
exit: 'mingcute:exit-line',
|
||||||
translate: 'mingcute:translate-line',
|
translate: 'mingcute:translate-line',
|
||||||
play: 'mingcute:play-fill',
|
play: 'mingcute:play-fill',
|
||||||
|
trash: 'mingcute:delete-2-line',
|
||||||
|
mute: 'mingcute:volume-mute-line',
|
||||||
|
unmute: 'mingcute:volume-line',
|
||||||
|
block: 'mingcute:forbid-circle-line',
|
||||||
|
unblock: ['mingcute:forbid-circle-line', '180deg'],
|
||||||
|
flag: 'mingcute:flag-4-line',
|
||||||
|
time: 'mingcute:time-line',
|
||||||
|
refresh: 'mingcute:refresh-2-line',
|
||||||
|
emoji2: 'mingcute:emoji-2-line',
|
||||||
};
|
};
|
||||||
|
|
||||||
const modules = import.meta.glob('/node_modules/@iconify-icons/mingcute/*.js');
|
const modules = import.meta.glob('/node_modules/@iconify-icons/mingcute/*.js');
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import Icon from './icon';
|
import Icon from './icon';
|
||||||
import Link from './link';
|
import Link from './link';
|
||||||
import Media from './media';
|
import Media from './media';
|
||||||
|
import MenuLink from './menu-link';
|
||||||
import Modal from './modal';
|
import Modal from './modal';
|
||||||
import TranslationBlock from './translation-block';
|
import TranslationBlock from './translation-block';
|
||||||
|
|
||||||
|
@ -22,6 +23,17 @@ function MediaModal({
|
||||||
const carouselFocusItem = useRef(null);
|
const carouselFocusItem = useRef(null);
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
carouselFocusItem.current?.scrollIntoView();
|
carouselFocusItem.current?.scrollIntoView();
|
||||||
|
|
||||||
|
history.pushState({ mediaModal: true }, '');
|
||||||
|
const handlePopState = (e) => {
|
||||||
|
if (e.state?.mediaModal) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('popstate', handlePopState);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('popstate', handlePopState);
|
||||||
|
};
|
||||||
}, []);
|
}, []);
|
||||||
const prevStatusID = useRef(statusID);
|
const prevStatusID = useRef(statusID);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -82,7 +94,8 @@ function MediaModal({
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
if (
|
if (
|
||||||
e.target.classList.contains('carousel-item') ||
|
e.target.classList.contains('carousel-item') ||
|
||||||
e.target.classList.contains('media')
|
e.target.classList.contains('media') ||
|
||||||
|
e.target.classList.contains('media-zoom')
|
||||||
) {
|
) {
|
||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
|
@ -166,6 +179,32 @@ function MediaModal({
|
||||||
<span />
|
<span />
|
||||||
)}
|
)}
|
||||||
<span>
|
<span>
|
||||||
|
<Menu
|
||||||
|
overflow="auto"
|
||||||
|
align="end"
|
||||||
|
position="anchor"
|
||||||
|
boundingBoxPadding="8 8 8 8"
|
||||||
|
offsetY={4}
|
||||||
|
menuClassName="glass-menu"
|
||||||
|
menuButton={
|
||||||
|
<button type="button" class="carousel-button plain3">
|
||||||
|
<Icon icon="more" alt="More" />
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<MenuLink
|
||||||
|
href={
|
||||||
|
mediaAttachments[currentIndex]?.remoteUrl ||
|
||||||
|
mediaAttachments[currentIndex]?.url
|
||||||
|
}
|
||||||
|
class="carousel-button plain3"
|
||||||
|
target="_blank"
|
||||||
|
title="Open original media in new window"
|
||||||
|
>
|
||||||
|
<Icon icon="popout" />
|
||||||
|
<span>Open original media</span>
|
||||||
|
</MenuLink>
|
||||||
|
</Menu>{' '}
|
||||||
<Link
|
<Link
|
||||||
to={instance ? `/${instance}/s/${statusID}` : `/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"
|
||||||
|
@ -179,18 +218,7 @@ function MediaModal({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span class="button-label">See post </span>»
|
<span class="button-label">See post </span>»
|
||||||
</Link>{' '}
|
</Link>
|
||||||
<a
|
|
||||||
href={
|
|
||||||
mediaAttachments[currentIndex]?.remoteUrl ||
|
|
||||||
mediaAttachments[currentIndex]?.url
|
|
||||||
}
|
|
||||||
target="_blank"
|
|
||||||
class="button carousel-button plain3"
|
|
||||||
title="Open original media in new window"
|
|
||||||
>
|
|
||||||
<Icon icon="popout" alt="Open original media in new window" />
|
|
||||||
</a>{' '}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{mediaAttachments?.length > 1 && (
|
{mediaAttachments?.length > 1 && (
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { getBlurHashAverageColor } from 'fast-blurhash';
|
import { getBlurHashAverageColor } from 'fast-blurhash';
|
||||||
import { useRef } from 'preact/hooks';
|
import { useCallback, useRef } from 'preact/hooks';
|
||||||
|
import QuickPinchZoom, { make3dTransformValue } from 'react-quick-pinch-zoom';
|
||||||
|
|
||||||
import Icon from './icon';
|
import Icon from './icon';
|
||||||
import { formatDuration } from './status';
|
import { formatDuration } from './status';
|
||||||
|
@ -39,8 +40,40 @@ function Media({ media, showOriginal, autoAnimate, onClick = () => {} }) {
|
||||||
focalBackgroundPosition = `${x.toFixed(0)}% ${y.toFixed(0)}%`;
|
focalBackgroundPosition = `${x.toFixed(0)}% ${y.toFixed(0)}%`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const mediaRef = useRef();
|
||||||
|
const onUpdate = useCallback(({ x, y, scale }) => {
|
||||||
|
const { current: media } = mediaRef;
|
||||||
|
|
||||||
|
if (media) {
|
||||||
|
const value = make3dTransformValue({ x, y, scale });
|
||||||
|
|
||||||
|
media.style.setProperty('transform', value);
|
||||||
|
|
||||||
|
media.closest('.media-zoom').style.touchAction =
|
||||||
|
scale <= 1 ? 'pan-x' : '';
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const quickPinchZoomProps = {
|
||||||
|
draggableUnZoomed: false,
|
||||||
|
inertiaFriction: 0.9,
|
||||||
|
containerProps: {
|
||||||
|
className: 'media-zoom',
|
||||||
|
style: {
|
||||||
|
overflow: 'visible',
|
||||||
|
// width: 'inherit',
|
||||||
|
// height: 'inherit',
|
||||||
|
// justifyContent: 'inherit',
|
||||||
|
// alignItems: 'inherit',
|
||||||
|
// display: 'inherit',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
onUpdate,
|
||||||
|
};
|
||||||
|
|
||||||
if (type === 'image' || (type === 'unknown' && previewUrl && url)) {
|
if (type === 'image' || (type === 'unknown' && previewUrl && url)) {
|
||||||
// Note: type: unknown might not have width/height
|
// Note: type: unknown might not have width/height
|
||||||
|
quickPinchZoomProps.containerProps.style.display = 'inherit';
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
class={`media media-image`}
|
class={`media media-image`}
|
||||||
|
@ -48,42 +81,42 @@ function Media({ media, showOriginal, autoAnimate, onClick = () => {} }) {
|
||||||
style={
|
style={
|
||||||
showOriginal && {
|
showOriginal && {
|
||||||
backgroundImage: `url(${previewUrl})`,
|
backgroundImage: `url(${previewUrl})`,
|
||||||
backgroundSize: 'contain',
|
|
||||||
backgroundRepeat: 'no-repeat',
|
|
||||||
backgroundPosition: 'center',
|
|
||||||
aspectRatio: `${width}/${height}`,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
maxWidth: '100%',
|
|
||||||
maxHeight: '100%',
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<img
|
{showOriginal ? (
|
||||||
src={mediaURL}
|
<QuickPinchZoom {...quickPinchZoomProps}>
|
||||||
alt={description}
|
<img
|
||||||
width={width}
|
ref={mediaRef}
|
||||||
height={height}
|
src={mediaURL}
|
||||||
loading={showOriginal ? 'eager' : 'lazy'}
|
alt={description}
|
||||||
style={
|
width={width}
|
||||||
!showOriginal && {
|
height={height}
|
||||||
|
loading="eager"
|
||||||
|
decoding="async"
|
||||||
|
onLoad={(e) => {
|
||||||
|
e.target.closest('.media-image').style.backgroundImage = '';
|
||||||
|
e.target.closest('.media-zoom').style.display = '';
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</QuickPinchZoom>
|
||||||
|
) : (
|
||||||
|
<img
|
||||||
|
src={mediaURL}
|
||||||
|
alt={description}
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
loading="lazy"
|
||||||
|
style={{
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
rgbAverageColor && `rgb(${rgbAverageColor.join(',')})`,
|
rgbAverageColor && `rgb(${rgbAverageColor.join(',')})`,
|
||||||
backgroundPosition: focalBackgroundPosition || 'center',
|
backgroundPosition: focalBackgroundPosition || 'center',
|
||||||
}
|
}}
|
||||||
}
|
onLoad={(e) => {
|
||||||
onDblClick={() => {
|
e.target.closest('.media-image').style.backgroundImage = '';
|
||||||
// Open original image in new tab
|
}}
|
||||||
window.open(url, '_blank');
|
/>
|
||||||
}}
|
)}
|
||||||
onLoad={(e) => {
|
|
||||||
// Hide background image after image loads
|
|
||||||
e.target.parentElement.style.backgroundImage = 'none';
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (type === 'gifv' || type === 'video') {
|
} else if (type === 'gifv' || type === 'video') {
|
||||||
|
@ -94,6 +127,23 @@ function Media({ media, showOriginal, autoAnimate, onClick = () => {} }) {
|
||||||
const formattedDuration = formatDuration(original.duration);
|
const formattedDuration = formatDuration(original.duration);
|
||||||
const hoverAnimate = !showOriginal && !autoAnimate && isGIF;
|
const hoverAnimate = !showOriginal && !autoAnimate && isGIF;
|
||||||
const autoGIFAnimate = !showOriginal && autoAnimate && isGIF;
|
const autoGIFAnimate = !showOriginal && autoAnimate && isGIF;
|
||||||
|
|
||||||
|
const videoHTML = `
|
||||||
|
<video
|
||||||
|
src="${url}"
|
||||||
|
poster="${previewUrl}"
|
||||||
|
width="${width}"
|
||||||
|
height="${height}"
|
||||||
|
preload="auto"
|
||||||
|
autoplay
|
||||||
|
muted="${isGIF}"
|
||||||
|
${isGIF ? '' : 'controls'}
|
||||||
|
playsinline
|
||||||
|
loop="${loopable}"
|
||||||
|
${isGIF ? 'ondblclick="this.paused ? this.play() : this.pause()"' : ''}
|
||||||
|
></video>
|
||||||
|
`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
class={`media media-${isGIF ? 'gif' : 'video'} ${
|
class={`media media-${isGIF ? 'gif' : 'video'} ${
|
||||||
|
@ -129,33 +179,22 @@ function Media({ media, showOriginal, autoAnimate, onClick = () => {} }) {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{showOriginal || autoGIFAnimate ? (
|
{showOriginal || autoGIFAnimate ? (
|
||||||
<div
|
isGIF ? (
|
||||||
style={{
|
<QuickPinchZoom {...quickPinchZoomProps}>
|
||||||
width: '100%',
|
<div
|
||||||
height: '100%',
|
ref={mediaRef}
|
||||||
}}
|
dangerouslySetInnerHTML={{
|
||||||
dangerouslySetInnerHTML={{
|
__html: videoHTML,
|
||||||
__html: `
|
}}
|
||||||
<video
|
/>
|
||||||
src="${url}"
|
</QuickPinchZoom>
|
||||||
poster="${previewUrl}"
|
) : (
|
||||||
width="${width}"
|
<div
|
||||||
height="${height}"
|
dangerouslySetInnerHTML={{
|
||||||
preload="auto"
|
__html: videoHTML,
|
||||||
autoplay
|
}}
|
||||||
muted="${isGIF}"
|
/>
|
||||||
${isGIF ? '' : 'controls'}
|
)
|
||||||
playsinline
|
|
||||||
loop="${loopable}"
|
|
||||||
${
|
|
||||||
isGIF
|
|
||||||
? 'ondblclick="this.paused ? this.play() : this.pause()"'
|
|
||||||
: ''
|
|
||||||
}
|
|
||||||
></video>
|
|
||||||
`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : isGIF ? (
|
) : isGIF ? (
|
||||||
<video
|
<video
|
||||||
ref={videoRef}
|
ref={videoRef}
|
||||||
|
@ -204,6 +243,11 @@ function Media({ media, showOriginal, autoAnimate, onClick = () => {} }) {
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
{!showOriginal && (
|
||||||
|
<div class="media-play">
|
||||||
|
<Icon icon="play" size="xxl" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +1,23 @@
|
||||||
import { Menu, MenuDivider, MenuItem } from '@szhsin/react-menu';
|
import { Menu, MenuDivider, MenuItem } from '@szhsin/react-menu';
|
||||||
|
import { useLongPress } from 'use-long-press';
|
||||||
import { useSnapshot } from 'valtio';
|
import { useSnapshot } from 'valtio';
|
||||||
|
|
||||||
import { api } from '../utils/api';
|
import { api } from '../utils/api';
|
||||||
import states from '../utils/states';
|
import states from '../utils/states';
|
||||||
import { getCurrentAccount } from '../utils/store-utils';
|
import store from '../utils/store';
|
||||||
|
|
||||||
|
import Avatar from './avatar';
|
||||||
import Icon from './icon';
|
import Icon from './icon';
|
||||||
import MenuLink from './MenuLink';
|
import MenuLink from './menu-link';
|
||||||
|
|
||||||
function NavMenu(props) {
|
function NavMenu(props) {
|
||||||
const snapStates = useSnapshot(states);
|
const snapStates = useSnapshot(states);
|
||||||
const { instance, authenticated } = api();
|
const { instance, authenticated } = api();
|
||||||
const currentAccount = getCurrentAccount();
|
const accounts = store.local.getJSON('accounts') || [];
|
||||||
|
const currentAccount = accounts.find(
|
||||||
|
(account) => account.info.id === store.session.get('currentAccount'),
|
||||||
|
);
|
||||||
|
const moreThanOneAccount = accounts.length > 1;
|
||||||
|
|
||||||
// Home = Following
|
// Home = Following
|
||||||
// But when in multi-column mode, Home becomes columns of anything
|
// But when in multi-column mode, Home becomes columns of anything
|
||||||
|
@ -21,6 +27,16 @@ function NavMenu(props) {
|
||||||
snapStates.settings.shortcutsColumnsMode &&
|
snapStates.settings.shortcutsColumnsMode &&
|
||||||
!snapStates.shortcuts.find((pin) => pin.type === 'following');
|
!snapStates.shortcuts.find((pin) => pin.type === 'following');
|
||||||
|
|
||||||
|
const bindLongPress = useLongPress(
|
||||||
|
() => {
|
||||||
|
states.showAccounts = true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
detect: 'touch',
|
||||||
|
cancelOnMovement: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu
|
<Menu
|
||||||
portal={{
|
portal={{
|
||||||
|
@ -30,11 +46,31 @@ function NavMenu(props) {
|
||||||
overflow="auto"
|
overflow="auto"
|
||||||
viewScroll="close"
|
viewScroll="close"
|
||||||
boundingBoxPadding="8 8 8 8"
|
boundingBoxPadding="8 8 8 8"
|
||||||
menuButton={
|
menuButton={({ open }) => (
|
||||||
<button type="button" class="button plain">
|
<button
|
||||||
<Icon icon="menu" size="l" />
|
type="button"
|
||||||
|
class={`button plain nav-menu-button ${
|
||||||
|
moreThanOneAccount ? 'with-avatar' : ''
|
||||||
|
} ${open ? 'active' : ''}`}
|
||||||
|
style={{ position: 'relative' }}
|
||||||
|
onContextMenu={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
states.showAccounts = true;
|
||||||
|
}}
|
||||||
|
{...bindLongPress()}
|
||||||
|
>
|
||||||
|
{moreThanOneAccount && (
|
||||||
|
<Avatar
|
||||||
|
url={
|
||||||
|
currentAccount?.info?.avatar ||
|
||||||
|
currentAccount?.info?.avatarStatic
|
||||||
|
}
|
||||||
|
size="l"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Icon icon="menu" size={moreThanOneAccount ? 's' : 'l'} />
|
||||||
</button>
|
</button>
|
||||||
}
|
)}
|
||||||
>
|
>
|
||||||
{!!snapStates.appVersion?.commitHash &&
|
{!!snapStates.appVersion?.commitHash &&
|
||||||
__COMMIT_HASH__ !== snapStates.appVersion.commitHash && (
|
__COMMIT_HASH__ !== snapStates.appVersion.commitHash && (
|
||||||
|
|
|
@ -60,6 +60,11 @@
|
||||||
#shortcuts-settings-container .shortcuts-view-mode label img {
|
#shortcuts-settings-container .shortcuts-view-mode label img {
|
||||||
max-height: 64px;
|
max-height: 64px;
|
||||||
}
|
}
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
#shortcuts-settings-container .shortcuts-view-mode label img {
|
||||||
|
filter: invert(0.9) hue-rotate(180deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
#shortcuts-settings-container .shortcuts-view-mode label span {
|
#shortcuts-settings-container .shortcuts-view-mode label span {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 80%;
|
font-size: 80%;
|
||||||
|
@ -68,10 +73,13 @@
|
||||||
position: absolute;
|
position: absolute;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
perspective: 500px;
|
||||||
}
|
}
|
||||||
#shortcuts-settings-container .shortcuts-view-mode label input ~ * {
|
#shortcuts-settings-container .shortcuts-view-mode label input ~ * {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
transition: opacity 0.2s;
|
transform-origin: bottom;
|
||||||
|
transform: scale(0.975);
|
||||||
|
transition: all 0.2s ease-out;
|
||||||
}
|
}
|
||||||
#shortcuts-settings-container .shortcuts-view-mode label:has(input:checked) {
|
#shortcuts-settings-container .shortcuts-view-mode label:has(input:checked) {
|
||||||
box-shadow: inset 0 0 0 3px var(--link-color);
|
box-shadow: inset 0 0 0 3px var(--link-color);
|
||||||
|
@ -81,6 +89,7 @@
|
||||||
label
|
label
|
||||||
input:is(:hover, :active, :checked)
|
input:is(:hover, :active, :checked)
|
||||||
~ * {
|
~ * {
|
||||||
|
transform: scale(1);
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -244,7 +244,8 @@ function ShortcutsSettings() {
|
||||||
states.settings.shortcutsViewMode = e.target.value;
|
states.settings.shortcutsViewMode = e.target.value;
|
||||||
}}
|
}}
|
||||||
/>{' '}
|
/>{' '}
|
||||||
<img src={imgURL} alt="" /> <span>{label}</span>
|
<img src={imgURL} alt="" width="80" height="58" />{' '}
|
||||||
|
<span>{label}</span>
|
||||||
</label>
|
</label>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -12,7 +12,7 @@ import states from '../utils/states';
|
||||||
import AsyncText from './AsyncText';
|
import AsyncText from './AsyncText';
|
||||||
import Icon from './icon';
|
import Icon from './icon';
|
||||||
import Link from './link';
|
import Link from './link';
|
||||||
import MenuLink from './MenuLink';
|
import MenuLink from './menu-link';
|
||||||
|
|
||||||
function Shortcuts() {
|
function Shortcuts() {
|
||||||
const snapStates = useSnapshot(states);
|
const snapStates = useSnapshot(states);
|
||||||
|
|
|
@ -50,9 +50,11 @@
|
||||||
.status-pre-meta .name-text {
|
.status-pre-meta .name-text {
|
||||||
display: inline;
|
display: inline;
|
||||||
}
|
}
|
||||||
.status-pre-meta .icon {
|
.status-pre-meta > * {
|
||||||
color: var(--reblog-color);
|
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
.status-reblog .status-pre-meta .icon {
|
||||||
|
color: var(--reblog-color);
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,6 +67,11 @@
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
@media (min-width: 40em) {
|
||||||
|
.status {
|
||||||
|
padding-bottom: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
.status.large {
|
.status.large {
|
||||||
--fade-in-out-bg: linear-gradient(
|
--fade-in-out-bg: linear-gradient(
|
||||||
to bottom,
|
to bottom,
|
||||||
|
@ -103,6 +110,72 @@
|
||||||
background-color: var(--outline-color);
|
background-color: var(--outline-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.status.filtered {
|
||||||
|
padding-block: 12px;
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.status.filtered .status-filtered-info {
|
||||||
|
pointer-events: none;
|
||||||
|
flex-grow: 1;
|
||||||
|
font-size: 90%;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
mask-image: linear-gradient(to right, black 90%, transparent);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.status.filtered .avatar {
|
||||||
|
opacity: 0.5;
|
||||||
|
transition: opacity 0.7s ease-in;
|
||||||
|
}
|
||||||
|
.status.filtered:is(:hover, :focus, :active) .avatar {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.status.filtered :is(.status-filtered-info-1, .status-filtered-info-2) {
|
||||||
|
transition: all 0.2s ease-out;
|
||||||
|
}
|
||||||
|
.status.filtered:hover :is(.status-filtered-info-1, .status-filtered-info-2) {
|
||||||
|
transition-delay: 0.5s;
|
||||||
|
}
|
||||||
|
.status.filtered .status-filtered-info-1 {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
.status.filtered:is(:hover, :focus, :active) .status-filtered-info-1 {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
.status.filtered .status-filtered-info-2 {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(8px);
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
.status.filtered:is(:hover, :focus, :active) .status-filtered-info-2 {
|
||||||
|
opacity: 0.75;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status.compact-thread {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.status.compact-thread .status-thread-badge {
|
||||||
|
flex-shrink: 0;
|
||||||
|
min-width: 50px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.status.compact-thread .content-compact {
|
||||||
|
overflow: hidden;
|
||||||
|
display: -webkit-box;
|
||||||
|
display: box;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
line-clamp: 2;
|
||||||
|
font-size: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
.status .container {
|
.status .container {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
@ -195,6 +268,52 @@
|
||||||
);
|
);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
.status-filtered-badge {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: var(--text-insignificant-color);
|
||||||
|
/* background: var(--bg-faded-color); */
|
||||||
|
/* border: var(--hairline-width) solid var(--bg-color); */
|
||||||
|
border: var(--hairline-width) dashed var(--text-insignificant-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 4px;
|
||||||
|
font-size: 10px;
|
||||||
|
line-height: 1;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: bold;
|
||||||
|
vertical-align: middle;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.status-filtered-badge.badge-meta {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
top: calc((9px + 2px) / 2 * -1);
|
||||||
|
min-width: 50px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.status-filtered-badge.clickable:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--text-color);
|
||||||
|
border-color: var(--text-color);
|
||||||
|
background: var(--bg-color);
|
||||||
|
}
|
||||||
|
.status-filtered-badge.badge-meta > span:first-child {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.status-filtered-badge.badge-meta > span + span {
|
||||||
|
display: block;
|
||||||
|
font-size: 9px;
|
||||||
|
font-weight: normal;
|
||||||
|
text-transform: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
top: calc(100% + 2px);
|
||||||
|
left: 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
.status.large .content-container {
|
.status.large .content-container {
|
||||||
margin-left: calc(-50px - 16px);
|
margin-left: calc(-50px - 16px);
|
||||||
|
@ -440,6 +559,7 @@ body:has(#modal-container .carousel) .status .media img:hover {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
|
border-radius: inherit;
|
||||||
}
|
}
|
||||||
.status :is(.media-video, .media-audio, .media-gif) {
|
.status :is(.media-video, .media-audio, .media-gif) {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -553,6 +673,7 @@ body:has(#modal-container .carousel) .status .media img:hover {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
font-size: 90%;
|
font-size: 90%;
|
||||||
|
z-index: 1;
|
||||||
}
|
}
|
||||||
.carousel-item button.media-alt .media-alt-desc {
|
.carousel-item button.media-alt .media-alt-desc {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
@ -713,6 +834,7 @@ a.card:is(:hover, :focus) {
|
||||||
|
|
||||||
.poll {
|
.poll {
|
||||||
transition: opacity 0.2s ease-in-out;
|
transition: opacity 0.2s ease-in-out;
|
||||||
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
.poll.loading {
|
.poll.loading {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
|
@ -721,31 +843,62 @@ a.card:is(:hover, :focus) {
|
||||||
.poll.read-only {
|
.poll.read-only {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
.poll-options {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 4px;
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 1px solid var(--outline-color);
|
||||||
|
background-color: var(--bg-faded-color);
|
||||||
|
}
|
||||||
.poll-option {
|
.poll-option {
|
||||||
margin-top: 8px;
|
padding: 4px 8px;
|
||||||
padding: 8px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
background-color: var(--bg-faded-color);
|
|
||||||
background-image: linear-gradient(
|
|
||||||
to right,
|
|
||||||
var(--link-faded-color),
|
|
||||||
var(--link-faded-color) var(--percentage),
|
|
||||||
transparent var(--percentage),
|
|
||||||
transparent
|
|
||||||
);
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid var(--outline-color);
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
text-shadow: 0 1px var(--bg-blur-color);
|
position: relative;
|
||||||
|
}
|
||||||
|
.poll-option > * {
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
.poll-option:after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: var(--link-faded-color);
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
.poll-option:first-child:after {
|
||||||
|
border-top-left-radius: 12px;
|
||||||
|
border-top-right-radius: 12px;
|
||||||
|
}
|
||||||
|
.poll-option:last-child:after {
|
||||||
|
border-bottom-left-radius: 12px;
|
||||||
|
border-bottom-right-radius: 12px;
|
||||||
|
}
|
||||||
|
.poll-option:hover:after {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.poll-option.poll-result:after {
|
||||||
|
width: var(--percentage);
|
||||||
|
opacity: 1;
|
||||||
}
|
}
|
||||||
.poll-label {
|
.poll-label {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
.poll-label input:is([type='radio'], [type='checkbox']) {
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin: 3px;
|
||||||
}
|
}
|
||||||
.poll-option-votes {
|
.poll-option-votes {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
@ -763,6 +916,9 @@ a.card:is(:hover, :focus) {
|
||||||
margin: 8px 0;
|
margin: 8px 0;
|
||||||
font-size: 90%;
|
font-size: 90%;
|
||||||
}
|
}
|
||||||
|
.poll-option-title {
|
||||||
|
text-shadow: 0 1px var(--bg-color);
|
||||||
|
}
|
||||||
.poll-option-title .icon {
|
.poll-option-title .icon {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
@ -780,6 +936,9 @@ a.card:is(:hover, :focus) {
|
||||||
.status .extra-meta a {
|
.status .extra-meta a {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
vertical-align: baseline;
|
||||||
|
text-decoration-thickness: 1px;
|
||||||
|
text-underline-offset: 3px;
|
||||||
}
|
}
|
||||||
.status .extra-meta a:is(:hover, :focus) {
|
.status .extra-meta a:is(:hover, :focus) {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
|
@ -807,6 +966,10 @@ a.card:is(:hover, :focus) {
|
||||||
border-top: var(--hairline-width) solid var(--outline-color);
|
border-top: var(--hairline-width) solid var(--outline-color);
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
.status.large .actions.disabled {
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
.status .action.has-count {
|
.status .action.has-count {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
@ -981,3 +1144,54 @@ a.card:is(:hover, :focus) {
|
||||||
border: 1px solid var(--outline-color);
|
border: 1px solid var(--outline-color);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* DELETED */
|
||||||
|
|
||||||
|
.status-deleted {
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
.status-deleted-tag {
|
||||||
|
color: var(--text-insignificant-color);
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* FILTERED */
|
||||||
|
|
||||||
|
#filtered-status-peek {
|
||||||
|
user-select: none;
|
||||||
|
-webkit-touch-callout: none;
|
||||||
|
-webkit-user-drag: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#filtered-status-peek main > p:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#filtered-status-peek main .heading {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
#filtered-status-peek .status-link {
|
||||||
|
border-radius: 16px;
|
||||||
|
border: var(--hairline-width) dashed var(--text-insignificant-color);
|
||||||
|
max-height: 33vh;
|
||||||
|
max-height: 33dvh;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
#filtered-status-peek .status-link .status {
|
||||||
|
pointer-events: none;
|
||||||
|
font-size: 90%;
|
||||||
|
max-height: 33vh;
|
||||||
|
max-height: 33dvh;
|
||||||
|
overflow: hidden;
|
||||||
|
mask-image: linear-gradient(black 80%, transparent 95%);
|
||||||
|
}
|
||||||
|
#filtered-status-peek .status-post-link {
|
||||||
|
float: right;
|
||||||
|
position: sticky;
|
||||||
|
bottom: 8px;
|
||||||
|
right: 8px;
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import './status.css';
|
import './status.css';
|
||||||
|
|
||||||
|
import { match } from '@formatjs/intl-localematcher';
|
||||||
|
import '@justinribeiro/lite-youtube';
|
||||||
import {
|
import {
|
||||||
ControlledMenu,
|
ControlledMenu,
|
||||||
Menu,
|
Menu,
|
||||||
|
@ -7,6 +9,7 @@ import {
|
||||||
MenuHeader,
|
MenuHeader,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
} from '@szhsin/react-menu';
|
} from '@szhsin/react-menu';
|
||||||
|
import { decodeBlurHash } from 'fast-blurhash';
|
||||||
import mem from 'mem';
|
import mem from 'mem';
|
||||||
import pThrottle from 'p-throttle';
|
import pThrottle from 'p-throttle';
|
||||||
import { memo } from 'preact/compat';
|
import { memo } from 'preact/compat';
|
||||||
|
@ -22,12 +25,14 @@ import NameText from '../components/name-text';
|
||||||
import { api } from '../utils/api';
|
import { api } from '../utils/api';
|
||||||
import enhanceContent from '../utils/enhance-content';
|
import enhanceContent from '../utils/enhance-content';
|
||||||
import getTranslateTargetLanguage from '../utils/get-translate-target-language';
|
import getTranslateTargetLanguage from '../utils/get-translate-target-language';
|
||||||
|
import getHTMLText from '../utils/getHTMLText';
|
||||||
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 niceDateTime from '../utils/nice-date-time';
|
import niceDateTime from '../utils/nice-date-time';
|
||||||
import shortenNumber from '../utils/shorten-number';
|
import shortenNumber from '../utils/shorten-number';
|
||||||
import showToast from '../utils/show-toast';
|
import showToast from '../utils/show-toast';
|
||||||
import states, { saveStatus, statusKey } from '../utils/states';
|
import states, { getStatus, saveStatus, statusKey } from '../utils/states';
|
||||||
|
import statusPeek from '../utils/status-peek';
|
||||||
import store from '../utils/store';
|
import store from '../utils/store';
|
||||||
import visibilityIconsMap from '../utils/visibility-icons-map';
|
import visibilityIconsMap from '../utils/visibility-icons-map';
|
||||||
|
|
||||||
|
@ -35,7 +40,7 @@ import Avatar from './avatar';
|
||||||
import Icon from './icon';
|
import Icon from './icon';
|
||||||
import Link from './link';
|
import Link from './link';
|
||||||
import Media from './media';
|
import Media from './media';
|
||||||
import MenuLink from './MenuLink';
|
import MenuLink from './menu-link';
|
||||||
import RelativeTime from './relative-time';
|
import RelativeTime from './relative-time';
|
||||||
import TranslationBlock from './translation-block';
|
import TranslationBlock from './translation-block';
|
||||||
|
|
||||||
|
@ -71,6 +76,7 @@ function Status({
|
||||||
contentTextWeight,
|
contentTextWeight,
|
||||||
enableTranslate,
|
enableTranslate,
|
||||||
previewMode,
|
previewMode,
|
||||||
|
allowFilters,
|
||||||
}) {
|
}) {
|
||||||
if (skeleton) {
|
if (skeleton) {
|
||||||
return (
|
return (
|
||||||
|
@ -140,10 +146,30 @@ function Status({
|
||||||
// Non-API props
|
// Non-API props
|
||||||
_deleted,
|
_deleted,
|
||||||
_pinned,
|
_pinned,
|
||||||
|
_filtered,
|
||||||
} = status;
|
} = status;
|
||||||
|
|
||||||
console.debug('RENDER Status', id, status?.account.displayName);
|
console.debug('RENDER Status', id, status?.account.displayName);
|
||||||
|
|
||||||
|
const debugHover = (e) => {
|
||||||
|
if (e.shiftKey) {
|
||||||
|
console.log(status);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (allowFilters && size !== 'l' && _filtered) {
|
||||||
|
return (
|
||||||
|
<FilteredStatus
|
||||||
|
status={status}
|
||||||
|
filterInfo={_filtered}
|
||||||
|
instance={instance}
|
||||||
|
containerProps={{
|
||||||
|
onMouseEnter: debugHover,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const createdAtDate = new Date(createdAt);
|
const createdAtDate = new Date(createdAt);
|
||||||
const editedAtDate = new Date(editedAt);
|
const editedAtDate = new Date(editedAt);
|
||||||
|
|
||||||
|
@ -175,13 +201,8 @@ function Status({
|
||||||
|
|
||||||
const showSpoiler = !!snapStates.spoilers[id] || false;
|
const showSpoiler = !!snapStates.spoilers[id] || false;
|
||||||
|
|
||||||
const debugHover = (e) => {
|
|
||||||
if (e.shiftKey) {
|
|
||||||
console.log(status);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (reblog) {
|
if (reblog) {
|
||||||
|
// If has statusID, means useItemID (cached in states)
|
||||||
return (
|
return (
|
||||||
<div class="status-reblog" onMouseEnter={debugHover}>
|
<div class="status-reblog" onMouseEnter={debugHover}>
|
||||||
<div class="status-pre-meta">
|
<div class="status-pre-meta">
|
||||||
|
@ -190,7 +211,8 @@ function Status({
|
||||||
boosted
|
boosted
|
||||||
</div>
|
</div>
|
||||||
<Status
|
<Status
|
||||||
status={reblog}
|
status={statusID ? null : reblog}
|
||||||
|
statusID={statusID ? reblog.id : null}
|
||||||
instance={instance}
|
instance={instance}
|
||||||
size={size}
|
size={size}
|
||||||
contentTextWeight={contentTextWeight}
|
contentTextWeight={contentTextWeight}
|
||||||
|
@ -201,6 +223,8 @@ function Status({
|
||||||
|
|
||||||
const [forceTranslate, setForceTranslate] = useState(false);
|
const [forceTranslate, setForceTranslate] = useState(false);
|
||||||
const targetLanguage = getTranslateTargetLanguage(true);
|
const targetLanguage = getTranslateTargetLanguage(true);
|
||||||
|
const contentTranslationHideLanguages =
|
||||||
|
snapStates.settings.contentTranslationHideLanguages || [];
|
||||||
if (!snapStates.settings.contentTranslation) enableTranslate = false;
|
if (!snapStates.settings.contentTranslation) enableTranslate = false;
|
||||||
|
|
||||||
const [showEdited, setShowEdited] = useState(false);
|
const [showEdited, setShowEdited] = useState(false);
|
||||||
|
@ -543,6 +567,29 @@ function Status({
|
||||||
<Icon icon="pencil" />
|
<Icon icon="pencil" />
|
||||||
<span>Edit</span>
|
<span>Edit</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
{isSizeLarge && (
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
const yes = confirm('Delete this post?');
|
||||||
|
if (yes) {
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
await masto.v1.statuses.remove(id);
|
||||||
|
const cachedStatus = getStatus(id, instance);
|
||||||
|
cachedStatus._deleted = true;
|
||||||
|
showToast('Deleted');
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
showToast('Unable to delete');
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon="trash" />
|
||||||
|
<span>Delete…</span>
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
@ -582,12 +629,13 @@ function Status({
|
||||||
m: 'medium',
|
m: 'medium',
|
||||||
l: 'large',
|
l: 'large',
|
||||||
}[size]
|
}[size]
|
||||||
}`}
|
} ${_deleted ? 'status-deleted' : ''}`}
|
||||||
onMouseEnter={debugHover}
|
onMouseEnter={debugHover}
|
||||||
onContextMenu={(e) => {
|
onContextMenu={(e) => {
|
||||||
if (size === 'l') return;
|
if (size === 'l') return;
|
||||||
if (e.metaKey) return;
|
if (e.metaKey) return;
|
||||||
if (previewMode) return;
|
if (previewMode) return;
|
||||||
|
if (_deleted) return;
|
||||||
// console.log('context menu', e);
|
// console.log('context menu', e);
|
||||||
const link = e.target.closest('a');
|
const link = e.target.closest('a');
|
||||||
if (link && /^https?:\/\//.test(link.getAttribute('href'))) return;
|
if (link && /^https?:\/\//.test(link.getAttribute('href'))) return;
|
||||||
|
@ -672,7 +720,9 @@ function Status({
|
||||||
)} */}
|
)} */}
|
||||||
{/* </span> */}{' '}
|
{/* </span> */}{' '}
|
||||||
{size !== 'l' &&
|
{size !== 'l' &&
|
||||||
(url && !previewMode ? (
|
(_deleted ? (
|
||||||
|
<span class="status-deleted-tag">Deleted</span>
|
||||||
|
) : url && !previewMode ? (
|
||||||
<Menu
|
<Menu
|
||||||
instanceRef={menuInstanceRef}
|
instanceRef={menuInstanceRef}
|
||||||
portal={{
|
portal={{
|
||||||
|
@ -859,7 +909,11 @@ function Status({
|
||||||
{((enableTranslate &&
|
{((enableTranslate &&
|
||||||
!!content.trim() &&
|
!!content.trim() &&
|
||||||
language &&
|
language &&
|
||||||
language !== targetLanguage) ||
|
language !== targetLanguage &&
|
||||||
|
!match([language], [targetLanguage]) &&
|
||||||
|
!contentTranslationHideLanguages.find(
|
||||||
|
(l) => language === l || match([language], [l]),
|
||||||
|
)) ||
|
||||||
forceTranslate) && (
|
forceTranslate) && (
|
||||||
<TranslationBlock
|
<TranslationBlock
|
||||||
forceTranslate={forceTranslate}
|
forceTranslate={forceTranslate}
|
||||||
|
@ -931,29 +985,41 @@ function Status({
|
||||||
{isSizeLarge && (
|
{isSizeLarge && (
|
||||||
<>
|
<>
|
||||||
<div class="extra-meta">
|
<div class="extra-meta">
|
||||||
<Icon icon={visibilityIconsMap[visibility]} alt={visibility} />{' '}
|
{_deleted ? (
|
||||||
<a href={url} target="_blank">
|
<span class="status-deleted-tag">Deleted</span>
|
||||||
<time class="created" datetime={createdAtDate.toISOString()}>
|
) : (
|
||||||
{createdDateText}
|
|
||||||
</time>
|
|
||||||
</a>
|
|
||||||
{editedAt && (
|
|
||||||
<>
|
<>
|
||||||
{' '}
|
<Icon
|
||||||
• <Icon icon="pencil" alt="Edited" />{' '}
|
icon={visibilityIconsMap[visibility]}
|
||||||
<time
|
alt={visibility}
|
||||||
class="edited"
|
/>{' '}
|
||||||
datetime={editedAtDate.toISOString()}
|
<a href={url} target="_blank">
|
||||||
onClick={() => {
|
<time
|
||||||
setShowEdited(id);
|
class="created"
|
||||||
}}
|
datetime={createdAtDate.toISOString()}
|
||||||
>
|
>
|
||||||
{editedDateText}
|
{createdDateText}
|
||||||
</time>
|
</time>
|
||||||
|
</a>
|
||||||
|
{editedAt && (
|
||||||
|
<>
|
||||||
|
{' '}
|
||||||
|
• <Icon icon="pencil" alt="Edited" />{' '}
|
||||||
|
<time
|
||||||
|
class="edited"
|
||||||
|
datetime={editedAtDate.toISOString()}
|
||||||
|
onClick={() => {
|
||||||
|
setShowEdited(id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{editedDateText}
|
||||||
|
</time>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div class="actions">
|
<div class={`actions ${_deleted ? 'disabled' : ''}`}>
|
||||||
<div class="action has-count">
|
<div class="action has-count">
|
||||||
<StatusButton
|
<StatusButton
|
||||||
title="Reply"
|
title="Reply"
|
||||||
|
@ -1105,18 +1171,32 @@ function Card({ card, instance }) {
|
||||||
// );
|
// );
|
||||||
// }
|
// }
|
||||||
|
|
||||||
if (hasText && image) {
|
if (hasText && (image || (!type !== 'photo' && blurhash))) {
|
||||||
const domain = new URL(url).hostname.replace(/^www\./, '');
|
const domain = new URL(url).hostname.replace(/^www\./, '');
|
||||||
|
let blurhashImage;
|
||||||
|
if (!image) {
|
||||||
|
const w = 44;
|
||||||
|
const h = 44;
|
||||||
|
const blurhashPixels = decodeBlurHash(blurhash, w, h);
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = w;
|
||||||
|
canvas.height = h;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
const imageData = ctx.createImageData(w, h);
|
||||||
|
imageData.data.set(blurhashPixels);
|
||||||
|
ctx.putImageData(imageData, 0, 0);
|
||||||
|
blurhashImage = canvas.toDataURL();
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
href={cardStatusURL || url}
|
href={cardStatusURL || url}
|
||||||
target={cardStatusURL ? null : '_blank'}
|
target={cardStatusURL ? null : '_blank'}
|
||||||
rel="nofollow noopener noreferrer"
|
rel="nofollow noopener noreferrer"
|
||||||
class={`card link ${size}`}
|
class={`card link ${blurhashImage ? '' : size}`}
|
||||||
>
|
>
|
||||||
<div class="card-image">
|
<div class="card-image">
|
||||||
<img
|
<img
|
||||||
src={image}
|
src={image || blurhashImage}
|
||||||
width={width}
|
width={width}
|
||||||
height={height}
|
height={height}
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
|
@ -1157,6 +1237,13 @@ function Card({ card, instance }) {
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
} else if (type === 'video') {
|
} else if (type === 'video') {
|
||||||
|
if (/youtube/i.test(providerName)) {
|
||||||
|
// Get ID from e.g. https://www.youtube.com/watch?v=[VIDEO_ID]
|
||||||
|
const videoID = url.match(/watch\?v=([^&]+)/)?.[1];
|
||||||
|
if (videoID) {
|
||||||
|
return <lite-youtube videoid={videoID} nocookie></lite-youtube>;
|
||||||
|
}
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
class="card video"
|
class="card video"
|
||||||
|
@ -1222,53 +1309,64 @@ function Poll({
|
||||||
roundPrecision = 2;
|
roundPrecision = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const [showResults, setShowResults] = useState(false);
|
||||||
|
const optionsHaveVoteCounts = options.every((o) => o.votesCount !== null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
lang={lang}
|
lang={lang}
|
||||||
class={`poll ${readOnly ? 'read-only' : ''} ${
|
class={`poll ${readOnly ? 'read-only' : ''} ${
|
||||||
uiState === 'loading' ? 'loading' : ''
|
uiState === 'loading' ? 'loading' : ''
|
||||||
}`}
|
}`}
|
||||||
|
onDblClick={() => {
|
||||||
|
setShowResults(!showResults);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{voted || expired ? (
|
{(showResults && optionsHaveVoteCounts) || voted || expired ? (
|
||||||
options.map((option, i) => {
|
<div class="poll-options">
|
||||||
const { title, votesCount: optionVotesCount } = option;
|
{options.map((option, i) => {
|
||||||
const percentage = pollVotesCount
|
const { title, votesCount: optionVotesCount } = option;
|
||||||
? ((optionVotesCount / pollVotesCount) * 100).toFixed(
|
const percentage = pollVotesCount
|
||||||
roundPrecision,
|
? ((optionVotesCount / pollVotesCount) * 100).toFixed(
|
||||||
)
|
roundPrecision,
|
||||||
: 0;
|
)
|
||||||
// check if current poll choice is the leading one
|
: 0;
|
||||||
const isLeading =
|
// check if current poll choice is the leading one
|
||||||
optionVotesCount > 0 &&
|
const isLeading =
|
||||||
optionVotesCount === Math.max(...options.map((o) => o.votesCount));
|
optionVotesCount > 0 &&
|
||||||
return (
|
optionVotesCount ===
|
||||||
<div
|
Math.max(...options.map((o) => o.votesCount));
|
||||||
key={`${i}-${title}-${optionVotesCount}`}
|
return (
|
||||||
class={`poll-option ${isLeading ? 'poll-option-leading' : ''}`}
|
|
||||||
style={{
|
|
||||||
'--percentage': `${percentage}%`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div class="poll-option-title">
|
|
||||||
{title}
|
|
||||||
{voted && ownVotes.includes(i) && (
|
|
||||||
<>
|
|
||||||
{' '}
|
|
||||||
<Icon icon="check-circle" />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
class="poll-option-votes"
|
key={`${i}-${title}-${optionVotesCount}`}
|
||||||
title={`${optionVotesCount} vote${
|
class={`poll-option poll-result ${
|
||||||
optionVotesCount === 1 ? '' : 's'
|
isLeading ? 'poll-option-leading' : ''
|
||||||
}`}
|
}`}
|
||||||
|
style={{
|
||||||
|
'--percentage': `${percentage}%`,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{percentage}%
|
<div class="poll-option-title">
|
||||||
|
{title}
|
||||||
|
{voted && ownVotes.includes(i) && (
|
||||||
|
<>
|
||||||
|
{' '}
|
||||||
|
<Icon icon="check-circle" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="poll-option-votes"
|
||||||
|
title={`${optionVotesCount} vote${
|
||||||
|
optionVotesCount === 1 ? '' : 's'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{percentage}%
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
})}
|
||||||
})
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<form
|
<form
|
||||||
onSubmit={async (e) => {
|
onSubmit={async (e) => {
|
||||||
|
@ -1287,23 +1385,25 @@ function Poll({
|
||||||
setUIState('default');
|
setUIState('default');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{options.map((option, i) => {
|
<div class="poll-options">
|
||||||
const { title } = option;
|
{options.map((option, i) => {
|
||||||
return (
|
const { title } = option;
|
||||||
<div class="poll-option">
|
return (
|
||||||
<label class="poll-label">
|
<div class="poll-option">
|
||||||
<input
|
<label class="poll-label">
|
||||||
type={multiple ? 'checkbox' : 'radio'}
|
<input
|
||||||
name="poll"
|
type={multiple ? 'checkbox' : 'radio'}
|
||||||
value={i}
|
name="poll"
|
||||||
disabled={uiState === 'loading'}
|
value={i}
|
||||||
readOnly={readOnly}
|
disabled={uiState === 'loading'}
|
||||||
/>
|
readOnly={readOnly}
|
||||||
<span class="poll-option-title">{title}</span>
|
/>
|
||||||
</label>
|
<span class="poll-option-title">{title}</span>
|
||||||
</div>
|
</label>
|
||||||
);
|
</div>
|
||||||
})}
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
{!readOnly && (
|
{!readOnly && (
|
||||||
<button
|
<button
|
||||||
class="poll-vote-button"
|
class="poll-vote-button"
|
||||||
|
@ -1418,6 +1518,7 @@ function EditedAtModal({
|
||||||
size="s"
|
size="s"
|
||||||
withinContext
|
withinContext
|
||||||
readOnly
|
readOnly
|
||||||
|
previewMode
|
||||||
/>
|
/>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
|
@ -1608,18 +1709,6 @@ function nicePostURL(url) {
|
||||||
|
|
||||||
const unfurlMastodonLink = throttle(_unfurlMastodonLink);
|
const unfurlMastodonLink = throttle(_unfurlMastodonLink);
|
||||||
|
|
||||||
const div = document.createElement('div');
|
|
||||||
function getHTMLText(html) {
|
|
||||||
if (!html) return 0;
|
|
||||||
div.innerHTML = html
|
|
||||||
.replace(/<\/p>/g, '</p>\n\n')
|
|
||||||
.replace(/<\/li>/g, '</li>\n');
|
|
||||||
div.querySelectorAll('br').forEach((br) => {
|
|
||||||
br.replaceWith('\n');
|
|
||||||
});
|
|
||||||
return div.innerText.replace(/[\r\n]{3,}/g, '\n\n').trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
const root = document.documentElement;
|
const root = document.documentElement;
|
||||||
const defaultBoundingBoxPadding = 8;
|
const defaultBoundingBoxPadding = 8;
|
||||||
function safeBoundingBoxPadding() {
|
function safeBoundingBoxPadding() {
|
||||||
|
@ -1641,4 +1730,112 @@ function safeBoundingBoxPadding() {
|
||||||
return str;
|
return str;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function FilteredStatus({ status, filterInfo, instance, containerProps = {} }) {
|
||||||
|
const {
|
||||||
|
account: { avatar, avatarStatic },
|
||||||
|
createdAt,
|
||||||
|
visibility,
|
||||||
|
reblog,
|
||||||
|
} = status;
|
||||||
|
const isReblog = !!reblog;
|
||||||
|
const filterTitleStr = filterInfo?.titlesStr || '';
|
||||||
|
const createdAtDate = new Date(createdAt);
|
||||||
|
const statusPeekText = statusPeek(status.reblog || status);
|
||||||
|
|
||||||
|
const [showPeek, setShowPeek] = useState(false);
|
||||||
|
const bindLongPress = useLongPress(
|
||||||
|
() => {
|
||||||
|
setShowPeek(true);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
captureEvent: true,
|
||||||
|
detect: 'touch',
|
||||||
|
cancelOnMovement: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
class={isReblog ? 'status-reblog' : ''}
|
||||||
|
{...containerProps}
|
||||||
|
title={statusPeekText}
|
||||||
|
onContextMenu={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setShowPeek(true);
|
||||||
|
}}
|
||||||
|
{...bindLongPress()}
|
||||||
|
>
|
||||||
|
<article class="status filtered" tabindex="-1">
|
||||||
|
<b
|
||||||
|
class="status-filtered-badge clickable badge-meta"
|
||||||
|
title={filterTitleStr}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setShowPeek(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>Filtered</span>
|
||||||
|
<span>{filterTitleStr}</span>
|
||||||
|
</b>{' '}
|
||||||
|
<Avatar url={avatarStatic || avatar} />
|
||||||
|
<span class="status-filtered-info">
|
||||||
|
<span class="status-filtered-info-1">
|
||||||
|
<NameText account={status.account} instance={instance} />{' '}
|
||||||
|
<Icon
|
||||||
|
icon={visibilityIconsMap[visibility]}
|
||||||
|
alt={visibilityText[visibility]}
|
||||||
|
size="s"
|
||||||
|
/>{' '}
|
||||||
|
{isReblog ? (
|
||||||
|
'boosted'
|
||||||
|
) : (
|
||||||
|
<RelativeTime datetime={createdAtDate} format="micro" />
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span class="status-filtered-info-2">
|
||||||
|
{isReblog && (
|
||||||
|
<>
|
||||||
|
<Avatar
|
||||||
|
url={reblog.account.avatarStatic || reblog.account.avatar}
|
||||||
|
/>{' '}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{statusPeekText}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</article>
|
||||||
|
{!!showPeek && (
|
||||||
|
<Modal
|
||||||
|
class="light"
|
||||||
|
onClick={(e) => {
|
||||||
|
if (e.target === e.currentTarget) {
|
||||||
|
setShowPeek(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div id="filtered-status-peek" class="sheet">
|
||||||
|
<main tabIndex="-1">
|
||||||
|
<p class="heading">
|
||||||
|
<b class="status-filtered-badge">Filtered</b> {filterTitleStr}
|
||||||
|
</p>
|
||||||
|
<Link
|
||||||
|
class="status-link"
|
||||||
|
to={`/${instance}/s/${status.id}`}
|
||||||
|
onClick={() => {
|
||||||
|
setShowPeek(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Status status={status} instance={instance} size="s" readOnly />
|
||||||
|
<button type="button" class="status-post-link plain3">
|
||||||
|
See post »
|
||||||
|
</button>
|
||||||
|
</Link>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default memo(Status);
|
export default memo(Status);
|
||||||
|
|
|
@ -2,7 +2,11 @@ import { useEffect, 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 { useDebouncedCallback } from 'use-debounce';
|
import { useDebouncedCallback } from 'use-debounce';
|
||||||
|
import { useSnapshot } from 'valtio';
|
||||||
|
|
||||||
|
import states, { statusKey } from '../utils/states';
|
||||||
|
import statusPeek from '../utils/status-peek';
|
||||||
|
import { groupBoosts, groupContext } from '../utils/timeline-utils';
|
||||||
import useInterval from '../utils/useInterval';
|
import useInterval from '../utils/useInterval';
|
||||||
import usePageVisibility from '../utils/usePageVisibility';
|
import usePageVisibility from '../utils/usePageVisibility';
|
||||||
import useScroll from '../utils/useScroll';
|
import useScroll from '../utils/useScroll';
|
||||||
|
@ -28,6 +32,7 @@ function Timeline({
|
||||||
headerStart,
|
headerStart,
|
||||||
headerEnd,
|
headerEnd,
|
||||||
timelineStart,
|
timelineStart,
|
||||||
|
allowFilters,
|
||||||
}) {
|
}) {
|
||||||
const [items, setItems] = useState([]);
|
const [items, setItems] = useState([]);
|
||||||
const [uiState, setUIState] = useState('default');
|
const [uiState, setUIState] = useState('default');
|
||||||
|
@ -48,6 +53,7 @@ function Timeline({
|
||||||
if (boostsCarousel) {
|
if (boostsCarousel) {
|
||||||
value = groupBoosts(value);
|
value = groupBoosts(value);
|
||||||
}
|
}
|
||||||
|
value = groupContext(value);
|
||||||
console.log(value);
|
console.log(value);
|
||||||
if (firstLoad) {
|
if (firstLoad) {
|
||||||
setItems(value);
|
setItems(value);
|
||||||
|
@ -192,24 +198,27 @@ function Timeline({
|
||||||
}, [nearReachEnd, showMore]);
|
}, [nearReachEnd, showMore]);
|
||||||
|
|
||||||
const lastHiddenTime = useRef();
|
const lastHiddenTime = useRef();
|
||||||
usePageVisibility((visible) => {
|
usePageVisibility(
|
||||||
if (visible) {
|
(visible) => {
|
||||||
const timeDiff = Date.now() - lastHiddenTime.current;
|
if (visible) {
|
||||||
if (!lastHiddenTime.current || timeDiff > 1000 * 60) {
|
const timeDiff = Date.now() - lastHiddenTime.current;
|
||||||
(async () => {
|
if (!lastHiddenTime.current || timeDiff > 1000 * 60) {
|
||||||
console.log('✨ Check updates');
|
(async () => {
|
||||||
const hasUpdate = await checkForUpdates();
|
console.log('✨ Check updates');
|
||||||
if (hasUpdate) {
|
const hasUpdate = await checkForUpdates();
|
||||||
console.log('✨ Has new updates');
|
if (hasUpdate) {
|
||||||
setShowNew(true);
|
console.log('✨ Has new updates');
|
||||||
}
|
setShowNew(true);
|
||||||
})();
|
}
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
lastHiddenTime.current = Date.now();
|
||||||
}
|
}
|
||||||
} else {
|
setVisible(visible);
|
||||||
lastHiddenTime.current = Date.now();
|
},
|
||||||
}
|
[checkForUpdates],
|
||||||
setVisible(visible);
|
);
|
||||||
}, []);
|
|
||||||
|
|
||||||
// checkForUpdates interval
|
// checkForUpdates interval
|
||||||
useInterval(
|
useInterval(
|
||||||
|
@ -309,52 +318,117 @@ function Timeline({
|
||||||
} else if (type === 'pinned') {
|
} else if (type === 'pinned') {
|
||||||
title = 'Pinned posts';
|
title = 'Pinned posts';
|
||||||
}
|
}
|
||||||
|
const isCarousel = type === 'boosts' || type === 'pinned';
|
||||||
if (items) {
|
if (items) {
|
||||||
return (
|
if (isCarousel) {
|
||||||
<li key={`timeline-${statusID}`}>
|
// Here, we don't hide filtered posts, but we sort them last
|
||||||
<StatusCarousel title={title} class={`${type}-carousel`}>
|
items.sort((a, b) => {
|
||||||
{items.map((item) => {
|
if (a._filtered && !b._filtered) {
|
||||||
const { id: statusID, reblog } = item;
|
return 1;
|
||||||
const actualStatusID = reblog?.id || statusID;
|
}
|
||||||
const url = instance
|
if (!a._filtered && b._filtered) {
|
||||||
? `/${instance}/s/${actualStatusID}`
|
return -1;
|
||||||
: `/s/${actualStatusID}`;
|
}
|
||||||
return (
|
return 0;
|
||||||
<li key={statusID}>
|
});
|
||||||
<Link
|
return (
|
||||||
class="status-carousel-link timeline-item-alt"
|
<li key={`timeline-${statusID}`}>
|
||||||
to={url}
|
<StatusCarousel
|
||||||
>
|
title={title}
|
||||||
{useItemID ? (
|
class={`${type}-carousel`}
|
||||||
<Status
|
>
|
||||||
statusID={statusID}
|
{items.map((item) => {
|
||||||
instance={instance}
|
const { id: statusID, reblog } = item;
|
||||||
size="s"
|
const actualStatusID = reblog?.id || statusID;
|
||||||
contentTextWeight
|
const url = instance
|
||||||
/>
|
? `/${instance}/s/${actualStatusID}`
|
||||||
) : (
|
: `/s/${actualStatusID}`;
|
||||||
<Status
|
return (
|
||||||
status={item}
|
<li key={statusID}>
|
||||||
instance={instance}
|
<Link
|
||||||
size="s"
|
class="status-carousel-link timeline-item-alt"
|
||||||
contentTextWeight
|
to={url}
|
||||||
/>
|
>
|
||||||
)}
|
{useItemID ? (
|
||||||
</Link>
|
<Status
|
||||||
</li>
|
statusID={statusID}
|
||||||
);
|
instance={instance}
|
||||||
})}
|
size="s"
|
||||||
</StatusCarousel>
|
contentTextWeight
|
||||||
</li>
|
/>
|
||||||
);
|
) : (
|
||||||
|
<Status
|
||||||
|
status={item}
|
||||||
|
instance={instance}
|
||||||
|
size="s"
|
||||||
|
contentTextWeight
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</StatusCarousel>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const manyItems = items.length > 3;
|
||||||
|
return items.map((item, i) => {
|
||||||
|
const { id: statusID } = item;
|
||||||
|
const url = instance
|
||||||
|
? `/${instance}/s/${statusID}`
|
||||||
|
: `/s/${statusID}`;
|
||||||
|
const isMiddle = i > 0 && i < items.length - 1;
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
key={`timeline-${statusID}`}
|
||||||
|
class={`timeline-item-container timeline-item-container-type-${type} timeline-item-container-${
|
||||||
|
i === 0
|
||||||
|
? 'start'
|
||||||
|
: i === items.length - 1
|
||||||
|
? 'end'
|
||||||
|
: 'middle'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Link class="status-link timeline-item" to={url}>
|
||||||
|
{manyItems && isMiddle && type === 'thread' ? (
|
||||||
|
<TimelineStatusCompact
|
||||||
|
status={item}
|
||||||
|
instance={instance}
|
||||||
|
/>
|
||||||
|
) : useItemID ? (
|
||||||
|
<Status
|
||||||
|
statusID={statusID}
|
||||||
|
instance={instance}
|
||||||
|
allowFilters={allowFilters}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Status
|
||||||
|
status={item}
|
||||||
|
instance={instance}
|
||||||
|
allowFilters={allowFilters}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<li key={`timeline-${statusID + _pinned}`}>
|
<li key={`timeline-${statusID + _pinned}`}>
|
||||||
<Link class="status-link timeline-item" to={url}>
|
<Link class="status-link timeline-item" to={url}>
|
||||||
{useItemID ? (
|
{useItemID ? (
|
||||||
<Status statusID={statusID} instance={instance} />
|
<Status
|
||||||
|
statusID={statusID}
|
||||||
|
instance={instance}
|
||||||
|
allowFilters={allowFilters}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Status status={status} instance={instance} />
|
<Status
|
||||||
|
status={status}
|
||||||
|
instance={instance}
|
||||||
|
allowFilters={allowFilters}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
|
@ -430,52 +504,6 @@ function Timeline({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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 }) {
|
function StatusCarousel({ title, class: className, children }) {
|
||||||
const carouselRef = useRef();
|
const carouselRef = useRef();
|
||||||
const { reachStart, reachEnd, init } = useScroll({
|
const { reachStart, reachEnd, init } = useScroll({
|
||||||
|
@ -524,4 +552,26 @@ function StatusCarousel({ title, class: className, children }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function TimelineStatusCompact({ status, instance }) {
|
||||||
|
const snapStates = useSnapshot(states);
|
||||||
|
const { id } = status;
|
||||||
|
const statusPeekText = statusPeek(status);
|
||||||
|
const sKey = statusKey(id, instance);
|
||||||
|
return (
|
||||||
|
<article class="status compact-thread" tabindex="-1">
|
||||||
|
{!!snapStates.statusThreadNumber[sKey] && (
|
||||||
|
<div class="status-thread-badge">
|
||||||
|
<Icon icon="thread" size="s" />
|
||||||
|
{snapStates.statusThreadNumber[sKey]
|
||||||
|
? ` ${snapStates.statusThreadNumber[sKey]}/X`
|
||||||
|
: ''}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div class="content-compact" title={statusPeekText}>
|
||||||
|
{statusPeekText}
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default Timeline;
|
export default Timeline;
|
||||||
|
|
|
@ -58,8 +58,14 @@
|
||||||
0 1px 5px -2px var(--drop-shadow-color);
|
0 1px 5px -2px var(--drop-shadow-color);
|
||||||
text-shadow: 0 1px var(--bg-color);
|
text-shadow: 0 1px var(--bg-color);
|
||||||
}
|
}
|
||||||
|
.status-translation-block .translated-block .translation-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
.status-translation-block .translated-block .translation-info * {
|
.status-translation-block .translated-block .translation-info * {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
.status-translation-block .translated-source-select {
|
.status-translation-block .translated-source-select {
|
||||||
appearance: none;
|
appearance: none;
|
||||||
|
@ -71,16 +77,31 @@
|
||||||
background-color: var(--bg-faded-color);
|
background-color: var(--bg-faded-color);
|
||||||
color: inherit;
|
color: inherit;
|
||||||
width: min-content;
|
width: min-content;
|
||||||
|
min-width: 2em;
|
||||||
|
flex-shrink: 1 !important;
|
||||||
}
|
}
|
||||||
.status-translation-block .translated-block output {
|
.status-translation-block .translated-block output {
|
||||||
display: block;
|
display: block;
|
||||||
margin-top: 1em;
|
margin-top: 0.75em;
|
||||||
}
|
}
|
||||||
.status-translation-block
|
.status-translation-block
|
||||||
.translated-block
|
.translated-block
|
||||||
output.translated-pronunciation-content {
|
output.translated-pronunciation-content {
|
||||||
opacity: 0.75;
|
opacity: 0.75;
|
||||||
padding-bottom: 1em;
|
padding-top: 0.75em;
|
||||||
border-top: var(--hairline-width) solid var(--bg-color);
|
border-top: var(--hairline-width) solid var(--outline-color);
|
||||||
border-bottom: var(--hairline-width) solid var(--outline-color);
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
mask-image: linear-gradient(to bottom, black 3em, transparent);
|
||||||
|
}
|
||||||
|
.status-translation-block
|
||||||
|
.translated-block
|
||||||
|
output.translated-pronunciation-content.expand {
|
||||||
|
display: block;
|
||||||
|
-webkit-line-clamp: unset;
|
||||||
|
-webkit-box-orient: unset;
|
||||||
|
overflow: visible;
|
||||||
|
mask-image: none;
|
||||||
}
|
}
|
||||||
|
|
|
@ -87,7 +87,12 @@ function TranslationBlock({
|
||||||
}, [forceTranslate]);
|
}, [forceTranslate]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="status-translation-block">
|
<div
|
||||||
|
class="status-translation-block"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
}}
|
||||||
|
>
|
||||||
<details ref={detailsRef}>
|
<details ref={detailsRef}>
|
||||||
<summary>
|
<summary>
|
||||||
<button
|
<button
|
||||||
|
@ -134,14 +139,20 @@ function TranslationBlock({
|
||||||
) : (
|
) : (
|
||||||
!!translatedContent && (
|
!!translatedContent && (
|
||||||
<>
|
<>
|
||||||
{!!pronunciationContent && (
|
|
||||||
<output class="translated-pronunciation-content">
|
|
||||||
{pronunciationContent}
|
|
||||||
</output>
|
|
||||||
)}
|
|
||||||
<output class="translated-content" lang={targetLang}>
|
<output class="translated-content" lang={targetLang}>
|
||||||
{translatedContent}
|
{translatedContent}
|
||||||
</output>
|
</output>
|
||||||
|
{!!pronunciationContent && (
|
||||||
|
<output
|
||||||
|
class="translated-pronunciation-content"
|
||||||
|
tabIndex={-1}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.target.classList.toggle('expand');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{pronunciationContent}
|
||||||
|
</output>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import Timeline from '../components/timeline';
|
||||||
import { api } from '../utils/api';
|
import { api } from '../utils/api';
|
||||||
import emojifyText from '../utils/emojify-text';
|
import emojifyText from '../utils/emojify-text';
|
||||||
import states from '../utils/states';
|
import states from '../utils/states';
|
||||||
|
import { saveStatus } from '../utils/states';
|
||||||
import useTitle from '../utils/useTitle';
|
import useTitle from '../utils/useTitle';
|
||||||
|
|
||||||
const LIMIT = 20;
|
const LIMIT = 20;
|
||||||
|
@ -27,6 +28,7 @@ function AccountStatuses() {
|
||||||
if (pinnedStatuses?.length) {
|
if (pinnedStatuses?.length) {
|
||||||
pinnedStatuses.forEach((status) => {
|
pinnedStatuses.forEach((status) => {
|
||||||
status._pinned = true;
|
status._pinned = true;
|
||||||
|
saveStatus(status, instance);
|
||||||
});
|
});
|
||||||
if (pinnedStatuses.length >= 3) {
|
if (pinnedStatuses.length >= 3) {
|
||||||
const pinnedStatusesIds = pinnedStatuses.map((status) => status.id);
|
const pinnedStatusesIds = pinnedStatuses.map((status) => status.id);
|
||||||
|
@ -48,6 +50,10 @@ function AccountStatuses() {
|
||||||
const { value, done } = await accountStatusesIterator.current.next();
|
const { value, done } = await accountStatusesIterator.current.next();
|
||||||
if (value?.length) {
|
if (value?.length) {
|
||||||
results.push(...value);
|
results.push(...value);
|
||||||
|
|
||||||
|
value.forEach((item) => {
|
||||||
|
saveStatus(item, instance);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
value: results,
|
value: results,
|
||||||
|
@ -118,6 +124,7 @@ function AccountStatuses() {
|
||||||
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}
|
||||||
|
useItemID
|
||||||
boostsCarousel={snapStates.settings.boostsCarousel}
|
boostsCarousel={snapStates.settings.boostsCarousel}
|
||||||
timelineStart={TimelineStart}
|
timelineStart={TimelineStart}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import './settings.css';
|
import './settings.css';
|
||||||
|
|
||||||
import { Menu, MenuItem } from '@szhsin/react-menu';
|
import { Menu, MenuDivider, MenuItem } from '@szhsin/react-menu';
|
||||||
import { useReducer, useState } from 'preact/hooks';
|
import { useReducer, useState } from 'preact/hooks';
|
||||||
|
|
||||||
import Avatar from '../components/avatar';
|
import Avatar from '../components/avatar';
|
||||||
|
@ -25,11 +25,6 @@ function Accounts({ onClose }) {
|
||||||
<div id="settings-container" class="sheet" tabIndex="-1">
|
<div id="settings-container" class="sheet" tabIndex="-1">
|
||||||
<header class="header-grid">
|
<header class="header-grid">
|
||||||
<h2>Accounts</h2>
|
<h2>Accounts</h2>
|
||||||
<div class="header-side">
|
|
||||||
<Link to="/login" class="button plain" onClick={onClose}>
|
|
||||||
<Icon icon="plus" /> <span>Account</span>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
<section>
|
<section>
|
||||||
|
@ -66,7 +61,12 @@ function Accounts({ onClose }) {
|
||||||
account={account.info}
|
account={account.info}
|
||||||
showAcct
|
showAcct
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
states.showAccount = `${account.info.username}@${account.instanceURL}`;
|
if (isCurrent) {
|
||||||
|
states.showAccount = `${account.info.username}@${account.instanceURL}`;
|
||||||
|
} else {
|
||||||
|
store.session.set('currentAccount', account.info.id);
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -76,18 +76,6 @@ function Accounts({ onClose }) {
|
||||||
<span class="tag">Default</span>{' '}
|
<span class="tag">Default</span>{' '}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{!isCurrent && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="light"
|
|
||||||
onClick={() => {
|
|
||||||
store.session.set('currentAccount', account.info.id);
|
|
||||||
location.reload();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon icon="transfer" /> Switch
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<Menu
|
<Menu
|
||||||
align="end"
|
align="end"
|
||||||
menuButton={
|
menuButton={
|
||||||
|
@ -100,6 +88,15 @@ function Accounts({ onClose }) {
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
states.showAccount = `${account.info.username}@${account.instanceURL}`;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon="user" />
|
||||||
|
<span>View profile…</span>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuDivider />
|
||||||
{moreThanOneAccount && (
|
{moreThanOneAccount && (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
disabled={isDefault}
|
disabled={isDefault}
|
||||||
|
@ -127,7 +124,7 @@ function Accounts({ onClose }) {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="exit" />
|
<Icon icon="exit" />
|
||||||
<span>Log out</span>
|
<span>Log out…</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</Menu>
|
</Menu>
|
||||||
</div>
|
</div>
|
||||||
|
@ -135,6 +132,11 @@ function Accounts({ onClose }) {
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
|
<p>
|
||||||
|
<Link to="/login" class="button plain2" onClick={onClose}>
|
||||||
|
<Icon icon="plus" /> <span>Add an existing account</span>
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
{moreThanOneAccount && (
|
{moreThanOneAccount && (
|
||||||
<p>
|
<p>
|
||||||
<small>
|
<small>
|
||||||
|
|
|
@ -3,8 +3,10 @@ import { useSnapshot } from 'valtio';
|
||||||
|
|
||||||
import Timeline from '../components/timeline';
|
import Timeline from '../components/timeline';
|
||||||
import { api } from '../utils/api';
|
import { api } from '../utils/api';
|
||||||
|
import { filteredItems } from '../utils/filters';
|
||||||
import states from '../utils/states';
|
import states from '../utils/states';
|
||||||
import { getStatus, saveStatus } from '../utils/states';
|
import { getStatus, saveStatus } from '../utils/states';
|
||||||
|
import { dedupeBoosts } from '../utils/timeline-utils';
|
||||||
import useTitle from '../utils/useTitle';
|
import useTitle from '../utils/useTitle';
|
||||||
|
|
||||||
const LIMIT = 20;
|
const LIMIT = 20;
|
||||||
|
@ -21,15 +23,18 @@ function Following({ title, path, id, ...props }) {
|
||||||
homeIterator.current = masto.v1.timelines.listHome({ limit: LIMIT });
|
homeIterator.current = masto.v1.timelines.listHome({ limit: LIMIT });
|
||||||
}
|
}
|
||||||
const results = await homeIterator.current.next();
|
const results = await homeIterator.current.next();
|
||||||
const { value } = results;
|
let { value } = results;
|
||||||
if (value?.length) {
|
if (value?.length) {
|
||||||
if (firstLoad) {
|
if (firstLoad) {
|
||||||
latestItem.current = value[0].id;
|
latestItem.current = value[0].id;
|
||||||
|
console.log('First load', latestItem.current);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
value = filteredItems(value, 'home');
|
||||||
value.forEach((item) => {
|
value.forEach((item) => {
|
||||||
saveStatus(item, instance);
|
saveStatus(item, instance);
|
||||||
});
|
});
|
||||||
|
value = dedupeBoosts(value, instance);
|
||||||
|
|
||||||
// ENFORCE sort by datetime (Latest first)
|
// ENFORCE sort by datetime (Latest first)
|
||||||
value.sort((a, b) => {
|
value.sort((a, b) => {
|
||||||
|
@ -49,10 +54,15 @@ function Following({ title, path, id, ...props }) {
|
||||||
since_id: latestItem.current,
|
since_id: latestItem.current,
|
||||||
})
|
})
|
||||||
.next();
|
.next();
|
||||||
const { value } = results;
|
let { value } = results;
|
||||||
console.log('checkForUpdates', latestItem.current, value);
|
console.log('checkForUpdates', latestItem.current, value);
|
||||||
if (value?.length && value.some((item) => !item.reblog)) {
|
if (value?.length) {
|
||||||
return true;
|
latestItem.current = value[0].id;
|
||||||
|
value = dedupeBoosts(value, instance);
|
||||||
|
value = filteredItems(value, 'home');
|
||||||
|
if (value.some((item) => !item.reblog)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -119,6 +129,7 @@ function Following({ title, path, id, ...props }) {
|
||||||
useItemID
|
useItemID
|
||||||
boostsCarousel={snapStates.settings.boostsCarousel}
|
boostsCarousel={snapStates.settings.boostsCarousel}
|
||||||
{...props}
|
{...props}
|
||||||
|
allowFilters
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@ import Timeline from '../components/timeline';
|
||||||
import { api } from '../utils/api';
|
import { api } from '../utils/api';
|
||||||
import showToast from '../utils/show-toast';
|
import showToast from '../utils/show-toast';
|
||||||
import states from '../utils/states';
|
import states from '../utils/states';
|
||||||
|
import { saveStatus } from '../utils/states';
|
||||||
import useTitle from '../utils/useTitle';
|
import useTitle from '../utils/useTitle';
|
||||||
|
|
||||||
const LIMIT = 20;
|
const LIMIT = 20;
|
||||||
|
@ -52,6 +53,10 @@ function Hashtags(props) {
|
||||||
if (firstLoad) {
|
if (firstLoad) {
|
||||||
latestItem.current = value[0].id;
|
latestItem.current = value[0].id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
value.forEach((item) => {
|
||||||
|
saveStatus(item, instance);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
@ -110,6 +115,7 @@ function Hashtags(props) {
|
||||||
errorText="Unable to load posts with this tag"
|
errorText="Unable to load posts with this tag"
|
||||||
fetchItems={fetchHashtags}
|
fetchItems={fetchHashtags}
|
||||||
checkForUpdates={checkForUpdates}
|
checkForUpdates={checkForUpdates}
|
||||||
|
useItemID
|
||||||
headerEnd={
|
headerEnd={
|
||||||
<Menu
|
<Menu
|
||||||
portal={{
|
portal={{
|
||||||
|
|
|
@ -1,546 +0,0 @@
|
||||||
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);
|
|
|
@ -5,6 +5,8 @@ import Icon from '../components/icon';
|
||||||
import Link from '../components/link';
|
import Link from '../components/link';
|
||||||
import Timeline from '../components/timeline';
|
import Timeline from '../components/timeline';
|
||||||
import { api } from '../utils/api';
|
import { api } from '../utils/api';
|
||||||
|
import { filteredItems } from '../utils/filters';
|
||||||
|
import { saveStatus } from '../utils/states';
|
||||||
import useTitle from '../utils/useTitle';
|
import useTitle from '../utils/useTitle';
|
||||||
|
|
||||||
const LIMIT = 20;
|
const LIMIT = 20;
|
||||||
|
@ -22,11 +24,16 @@ function List(props) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const results = await listIterator.current.next();
|
const results = await listIterator.current.next();
|
||||||
const { value } = results;
|
let { value } = results;
|
||||||
if (value?.length) {
|
if (value?.length) {
|
||||||
if (firstLoad) {
|
if (firstLoad) {
|
||||||
latestItem.current = value[0].id;
|
latestItem.current = value[0].id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
value = filteredItems(value, 'home');
|
||||||
|
value.forEach((item) => {
|
||||||
|
saveStatus(item, instance);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
@ -37,7 +44,8 @@ function List(props) {
|
||||||
limit: 1,
|
limit: 1,
|
||||||
since_id: latestItem.current,
|
since_id: latestItem.current,
|
||||||
});
|
});
|
||||||
const { value } = results;
|
let { value } = results;
|
||||||
|
value = filteredItems(value, 'home');
|
||||||
if (value?.length) {
|
if (value?.length) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -69,7 +77,9 @@ function List(props) {
|
||||||
instance={instance}
|
instance={instance}
|
||||||
fetchItems={fetchList}
|
fetchItems={fetchList}
|
||||||
checkForUpdates={checkForUpdates}
|
checkForUpdates={checkForUpdates}
|
||||||
|
useItemID
|
||||||
boostsCarousel
|
boostsCarousel
|
||||||
|
allowFilters
|
||||||
headerStart={
|
headerStart={
|
||||||
<Link to="/l" class="button plain">
|
<Link to="/l" class="button plain">
|
||||||
<Icon icon="list" size="l" />
|
<Icon icon="list" size="l" />
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
animation: appear 0.2s ease-out;
|
animation: appear 0.2s ease-out;
|
||||||
}
|
}
|
||||||
.notification.mention {
|
.notification.notification-mention {
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
}
|
}
|
||||||
.only-mentions .notification:not(.mention),
|
.only-mentions .notification:not(.mention),
|
||||||
|
|
|
@ -318,7 +318,7 @@ function Notification({ notification, instance }) {
|
||||||
: contentText[type];
|
: contentText[type];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={`notification ${type}`} tabIndex="0">
|
<div class={`notification notification-${type}`} tabIndex="0">
|
||||||
<div
|
<div
|
||||||
class={`notification-type notification-${type}`}
|
class={`notification-type notification-${type}`}
|
||||||
title={new Date(notification.createdAt).toLocaleString()}
|
title={new Date(notification.createdAt).toLocaleString()}
|
||||||
|
|
|
@ -6,7 +6,9 @@ import { useSnapshot } from 'valtio';
|
||||||
import Icon from '../components/icon';
|
import Icon from '../components/icon';
|
||||||
import Timeline from '../components/timeline';
|
import Timeline from '../components/timeline';
|
||||||
import { api } from '../utils/api';
|
import { api } from '../utils/api';
|
||||||
|
import { filteredItems } from '../utils/filters';
|
||||||
import states from '../utils/states';
|
import states from '../utils/states';
|
||||||
|
import { saveStatus } from '../utils/states';
|
||||||
import useTitle from '../utils/useTitle';
|
import useTitle from '../utils/useTitle';
|
||||||
|
|
||||||
const LIMIT = 20;
|
const LIMIT = 20;
|
||||||
|
@ -32,11 +34,16 @@ function Public({ local, ...props }) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const results = await publicIterator.current.next();
|
const results = await publicIterator.current.next();
|
||||||
const { value } = results;
|
let { value } = results;
|
||||||
if (value?.length) {
|
if (value?.length) {
|
||||||
if (firstLoad) {
|
if (firstLoad) {
|
||||||
latestItem.current = value[0].id;
|
latestItem.current = value[0].id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
value = filteredItems(value, 'public');
|
||||||
|
value.forEach((item) => {
|
||||||
|
saveStatus(item, instance);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
@ -50,7 +57,8 @@ function Public({ local, ...props }) {
|
||||||
since_id: latestItem.current,
|
since_id: latestItem.current,
|
||||||
})
|
})
|
||||||
.next();
|
.next();
|
||||||
const { value } = results;
|
let { value } = results;
|
||||||
|
value = filteredItems(value, 'public');
|
||||||
if (value?.length) {
|
if (value?.length) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -76,8 +84,10 @@ function Public({ local, ...props }) {
|
||||||
errorText="Unable to load posts"
|
errorText="Unable to load posts"
|
||||||
fetchItems={fetchPublic}
|
fetchItems={fetchPublic}
|
||||||
checkForUpdates={checkForUpdates}
|
checkForUpdates={checkForUpdates}
|
||||||
|
useItemID
|
||||||
headerStart={<></>}
|
headerStart={<></>}
|
||||||
boostsCarousel={snapStates.settings.boostsCarousel}
|
boostsCarousel={snapStates.settings.boostsCarousel}
|
||||||
|
allowFilters
|
||||||
headerEnd={
|
headerEnd={
|
||||||
<Menu
|
<Menu
|
||||||
portal={{
|
portal={{
|
||||||
|
|
|
@ -123,3 +123,22 @@
|
||||||
#settings-container .range-group input[type='range'] {
|
#settings-container .range-group input[type='range'] {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#settings-container .checkbox-fields {
|
||||||
|
border: 1px solid var(--outline-color);
|
||||||
|
background-color: var(--bg-faded-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 8px 0;
|
||||||
|
max-height: 6.5em;
|
||||||
|
overflow: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
font-size: 90%;
|
||||||
|
}
|
||||||
|
#settings-container .checkbox-fieldset label {
|
||||||
|
flex: 1 0 10em;
|
||||||
|
padding: 4px;
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
|
@ -194,6 +194,50 @@ function Settings({ onClose }) {
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
|
<p class="checkbox-fieldset">
|
||||||
|
<small>
|
||||||
|
Hide "Translate" button for
|
||||||
|
{snapStates.settings.contentTranslationHideLanguages
|
||||||
|
.length > 0 && (
|
||||||
|
<>
|
||||||
|
{' '}
|
||||||
|
(
|
||||||
|
{
|
||||||
|
snapStates.settings.contentTranslationHideLanguages
|
||||||
|
.length
|
||||||
|
}
|
||||||
|
)
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
:
|
||||||
|
</small>
|
||||||
|
<div class="checkbox-fields">
|
||||||
|
{targetLanguages.map((lang) => (
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={snapStates.settings.contentTranslationHideLanguages.includes(
|
||||||
|
lang.code,
|
||||||
|
)}
|
||||||
|
onChange={(e) => {
|
||||||
|
const { checked } = e.target;
|
||||||
|
if (checked) {
|
||||||
|
states.settings.contentTranslationHideLanguages.push(
|
||||||
|
lang.code,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
states.settings.contentTranslationHideLanguages =
|
||||||
|
snapStates.settings.contentTranslationHideLanguages.filter(
|
||||||
|
(code) => code !== lang.code,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>{' '}
|
||||||
|
{lang.name}
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<small>
|
<small>
|
||||||
Note: This feature uses an external API to translate,
|
Note: This feature uses an external API to translate,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import './status.css';
|
import './status.css';
|
||||||
|
|
||||||
import { Menu, MenuItem } from '@szhsin/react-menu';
|
import { Menu, MenuDivider, MenuHeader, MenuItem } from '@szhsin/react-menu';
|
||||||
import debounce from 'just-debounce-it';
|
import debounce from 'just-debounce-it';
|
||||||
import pRetry from 'p-retry';
|
import pRetry from 'p-retry';
|
||||||
import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
|
import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
|
||||||
|
@ -25,6 +25,7 @@ import states, {
|
||||||
statusKey,
|
statusKey,
|
||||||
threadifyStatus,
|
threadifyStatus,
|
||||||
} from '../utils/states';
|
} from '../utils/states';
|
||||||
|
import statusPeek from '../utils/status-peek';
|
||||||
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';
|
||||||
|
@ -32,6 +33,7 @@ import useTitle from '../utils/useTitle';
|
||||||
const LIMIT = 40;
|
const LIMIT = 40;
|
||||||
const THREAD_LIMIT = 20;
|
const THREAD_LIMIT = 20;
|
||||||
|
|
||||||
|
let cachedRepliesToggle = {};
|
||||||
let cachedStatusesMap = {};
|
let cachedStatusesMap = {};
|
||||||
function resetScrollPosition(id) {
|
function resetScrollPosition(id) {
|
||||||
delete cachedStatusesMap[id];
|
delete cachedStatusesMap[id];
|
||||||
|
@ -168,6 +170,16 @@ function StatusPage() {
|
||||||
|
|
||||||
console.log({ ancestors, descendants, nestedDescendants });
|
console.log({ ancestors, descendants, nestedDescendants });
|
||||||
|
|
||||||
|
function expandReplies(_replies) {
|
||||||
|
return _replies?.map((_r) => ({
|
||||||
|
id: _r.id,
|
||||||
|
account: _r.account,
|
||||||
|
repliesCount: _r.repliesCount,
|
||||||
|
content: _r.content,
|
||||||
|
replies: expandReplies(_r.__replies),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
const allStatuses = [
|
const allStatuses = [
|
||||||
...ancestors.map((s) => ({
|
...ancestors.map((s) => ({
|
||||||
id: s.id,
|
id: s.id,
|
||||||
|
@ -180,26 +192,7 @@ function StatusPage() {
|
||||||
accountID: s.account.id,
|
accountID: s.account.id,
|
||||||
descendant: true,
|
descendant: true,
|
||||||
thread: s.account.id === heroStatus.account.id,
|
thread: s.account.id === heroStatus.account.id,
|
||||||
replies: s.__replies?.map((r) => ({
|
replies: expandReplies(s.__replies),
|
||||||
id: r.id,
|
|
||||||
account: r.account,
|
|
||||||
repliesCount: r.repliesCount,
|
|
||||||
content: r.content,
|
|
||||||
replies: r.__replies?.map((r2) => ({
|
|
||||||
// Level 3
|
|
||||||
id: r2.id,
|
|
||||||
account: r2.account,
|
|
||||||
repliesCount: r2.repliesCount,
|
|
||||||
content: r2.content,
|
|
||||||
replies: r2.__replies?.map((r3) => ({
|
|
||||||
// Level 4
|
|
||||||
id: r3.id,
|
|
||||||
account: r3.account,
|
|
||||||
repliesCount: r3.repliesCount,
|
|
||||||
content: r3.content,
|
|
||||||
})),
|
|
||||||
})),
|
|
||||||
})),
|
|
||||||
})),
|
})),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -292,6 +285,7 @@ function StatusPage() {
|
||||||
states.scrollPositions = {};
|
states.scrollPositions = {};
|
||||||
states.reloadStatusPage = 0;
|
states.reloadStatusPage = 0;
|
||||||
cachedStatusesMap = {};
|
cachedStatusesMap = {};
|
||||||
|
cachedRepliesToggle = {};
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
@ -306,15 +300,7 @@ function StatusPage() {
|
||||||
}, [heroStatus]);
|
}, [heroStatus]);
|
||||||
const heroContentText = useMemo(() => {
|
const heroContentText = useMemo(() => {
|
||||||
if (!heroStatus) return '';
|
if (!heroStatus) return '';
|
||||||
const { spoilerText, content } = heroStatus;
|
let text = statusPeek(heroStatus);
|
||||||
let text;
|
|
||||||
if (spoilerText) {
|
|
||||||
text = spoilerText;
|
|
||||||
} else {
|
|
||||||
const div = document.createElement('div');
|
|
||||||
div.innerHTML = content;
|
|
||||||
text = div.innerText.trim();
|
|
||||||
}
|
|
||||||
if (text.length > 64) {
|
if (text.length > 64) {
|
||||||
// "The title should ideally be less than 64 characters in length"
|
// "The title should ideally be less than 64 characters in length"
|
||||||
// https://www.w3.org/Provider/Style/TITLE.html
|
// https://www.w3.org/Provider/Style/TITLE.html
|
||||||
|
@ -329,6 +315,17 @@ function StatusPage() {
|
||||||
'/:instance?/s/:id',
|
'/:instance?/s/:id',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const postInstance = useMemo(() => {
|
||||||
|
if (!heroStatus) return;
|
||||||
|
const { url } = heroStatus;
|
||||||
|
if (!url) return;
|
||||||
|
return new URL(url).hostname;
|
||||||
|
}, [heroStatus]);
|
||||||
|
const postSameInstance = useMemo(() => {
|
||||||
|
if (!postInstance) return;
|
||||||
|
return postInstance === instance;
|
||||||
|
}, [postInstance, instance]);
|
||||||
|
|
||||||
const closeLink = useMemo(() => {
|
const closeLink = useMemo(() => {
|
||||||
const { prevLocation } = snapStates;
|
const { prevLocation } = snapStates;
|
||||||
const pathname =
|
const pathname =
|
||||||
|
@ -562,6 +559,15 @@ function StatusPage() {
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
<MenuItem
|
||||||
|
disabled={uiState === 'loading'}
|
||||||
|
onClick={() => {
|
||||||
|
states.reloadStatusPage++;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon="refresh" />
|
||||||
|
<span>Refresh</span>
|
||||||
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
// Click all buttons with class .spoiler but not .spoiling
|
// Click all buttons with class .spoiler but not .spoiling
|
||||||
|
@ -578,6 +584,24 @@ function StatusPage() {
|
||||||
<Icon icon="eye-open" />{' '}
|
<Icon icon="eye-open" />{' '}
|
||||||
<span>Show all sensitive content</span>
|
<span>Show all sensitive content</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
<MenuDivider />
|
||||||
|
<MenuHeader className="plain">Experimental</MenuHeader>
|
||||||
|
<MenuItem
|
||||||
|
disabled={postSameInstance}
|
||||||
|
onClick={() => {
|
||||||
|
const statusURL = getInstanceStatusURL(heroStatus.url);
|
||||||
|
if (statusURL) {
|
||||||
|
navigate(statusURL);
|
||||||
|
} else {
|
||||||
|
alert('Unable to switch');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon="transfer" />
|
||||||
|
<small class="menu-double-lines">
|
||||||
|
Switch to post's instance (<b>{postInstance}</b>)
|
||||||
|
</small>
|
||||||
|
</MenuItem>
|
||||||
</Menu>
|
</Menu>
|
||||||
)}
|
)}
|
||||||
<Link
|
<Link
|
||||||
|
@ -721,6 +745,7 @@ function StatusPage() {
|
||||||
hasManyStatuses={hasManyStatuses}
|
hasManyStatuses={hasManyStatuses}
|
||||||
replies={replies}
|
replies={replies}
|
||||||
hasParentThread={thread}
|
hasParentThread={thread}
|
||||||
|
level={1}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{uiState === 'loading' &&
|
{uiState === 'loading' &&
|
||||||
|
@ -800,7 +825,13 @@ function StatusPage() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SubComments({ hasManyStatuses, replies, instance, hasParentThread }) {
|
function SubComments({
|
||||||
|
hasManyStatuses,
|
||||||
|
replies,
|
||||||
|
instance,
|
||||||
|
hasParentThread,
|
||||||
|
level,
|
||||||
|
}) {
|
||||||
// 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
|
||||||
|
@ -839,10 +870,24 @@ function SubComments({ hasManyStatuses, replies, instance, hasParentThread }) {
|
||||||
|
|
||||||
const open =
|
const open =
|
||||||
(!hasParentThread || replies.length === 1) && (isBrief || !hasManyStatuses);
|
(!hasParentThread || replies.length === 1) && (isBrief || !hasManyStatuses);
|
||||||
|
const openBefore = cachedRepliesToggle[replies[0].id];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<details class="replies" open={open}>
|
<details
|
||||||
<summary hidden={open}>
|
class="replies"
|
||||||
|
open={openBefore || open}
|
||||||
|
onToggle={(e) => {
|
||||||
|
const { open } = e.target;
|
||||||
|
// use first reply as ID
|
||||||
|
cachedRepliesToggle[replies[0].id] = open;
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
'--comments-level': level,
|
||||||
|
}}
|
||||||
|
data-comments-level={level}
|
||||||
|
data-comments-level-overflow={level > 4}
|
||||||
|
>
|
||||||
|
<summary class="replies-summary" hidden={open}>
|
||||||
<span class="avatars">
|
<span class="avatars">
|
||||||
{accounts.map((a) => (
|
{accounts.map((a) => (
|
||||||
<Avatar
|
<Avatar
|
||||||
|
@ -900,6 +945,7 @@ function SubComments({ hasManyStatuses, replies, instance, hasParentThread }) {
|
||||||
instance={instance}
|
instance={instance}
|
||||||
hasManyStatuses={hasManyStatuses}
|
hasManyStatuses={hasManyStatuses}
|
||||||
replies={r.replies}
|
replies={r.replies}
|
||||||
|
level={level + 1}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</li>
|
</li>
|
||||||
|
@ -909,4 +955,14 @@ function SubComments({ hasManyStatuses, replies, instance, hasParentThread }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getInstanceStatusURL(url) {
|
||||||
|
// Regex /:username/:id, where username = @username or @username@domain, id = anything
|
||||||
|
const statusRegex = /\/@([^@\/]+)@?([^\/]+)?\/([^\/]+)\/?$/i;
|
||||||
|
const { hostname, pathname } = new URL(url);
|
||||||
|
const [, username, domain, id] = pathname.match(statusRegex) || [];
|
||||||
|
if (id) {
|
||||||
|
return `/${hostname}/s/${id}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default StatusPage;
|
export default StatusPage;
|
||||||
|
|
36
src/utils/filters.jsx
Normal file
36
src/utils/filters.jsx
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import store from './store';
|
||||||
|
|
||||||
|
export function filteredItem(item, filterContext, currentAccountID) {
|
||||||
|
const { filtered } = item;
|
||||||
|
if (!filtered?.length) return true;
|
||||||
|
const isSelf = currentAccountID && item.account?.id === currentAccountID;
|
||||||
|
if (isSelf) return true;
|
||||||
|
const appliedFilters = filtered.filter((f) => {
|
||||||
|
const { filter } = f;
|
||||||
|
const hasContext = filter.context.includes(filterContext);
|
||||||
|
if (!hasContext) return false;
|
||||||
|
if (!filter.expiresAt) return hasContext;
|
||||||
|
return new Date(filter.expiresAt) > new Date();
|
||||||
|
});
|
||||||
|
if (!appliedFilters.length) return true;
|
||||||
|
const isHidden = appliedFilters.some((f) => f.filter.filterAction === 'hide');
|
||||||
|
console.log({ isHidden, filtered, appliedFilters, item });
|
||||||
|
if (isHidden) return false;
|
||||||
|
const isWarn = appliedFilters.some((f) => f.filter.filterAction === 'warn');
|
||||||
|
if (isWarn) {
|
||||||
|
const filterTitles = appliedFilters.map((f) => f.filter.title);
|
||||||
|
item._filtered = {
|
||||||
|
titles: filterTitles,
|
||||||
|
titlesStr: filterTitles.join(' • '),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return isWarn;
|
||||||
|
}
|
||||||
|
export function filteredItems(items, filterContext) {
|
||||||
|
if (!items?.length) return [];
|
||||||
|
if (!filterContext) return items;
|
||||||
|
const currentAccountID = store.session.get('currentAccount');
|
||||||
|
return items.filter((item) =>
|
||||||
|
filteredItem(item, filterContext, currentAccountID),
|
||||||
|
);
|
||||||
|
}
|
13
src/utils/getHTMLText.jsx
Normal file
13
src/utils/getHTMLText.jsx
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
const div = document.createElement('div');
|
||||||
|
function getHTMLText(html) {
|
||||||
|
if (!html) return '';
|
||||||
|
div.innerHTML = html
|
||||||
|
.replace(/<\/p>/g, '</p>\n\n')
|
||||||
|
.replace(/<\/li>/g, '</li>\n');
|
||||||
|
div.querySelectorAll('br').forEach((br) => {
|
||||||
|
br.replaceWith('\n');
|
||||||
|
});
|
||||||
|
return div.innerText.replace(/[\r\n]{3,}/g, '\n\n').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
export default getHTMLText;
|
|
@ -48,6 +48,8 @@ const states = proxy({
|
||||||
store.account.get('settings-contentTranslation') ?? true,
|
store.account.get('settings-contentTranslation') ?? true,
|
||||||
contentTranslationTargetLanguage:
|
contentTranslationTargetLanguage:
|
||||||
store.account.get('settings-contentTranslationTargetLanguage') || null,
|
store.account.get('settings-contentTranslationTargetLanguage') || null,
|
||||||
|
contentTranslationHideLanguages:
|
||||||
|
store.account.get('settings-contentTranslationHideLanguages') || [],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -76,6 +78,12 @@ subscribe(states, (changes) => {
|
||||||
console.log('SET', value);
|
console.log('SET', value);
|
||||||
store.account.set('settings-contentTranslationTargetLanguage', value);
|
store.account.set('settings-contentTranslationTargetLanguage', value);
|
||||||
}
|
}
|
||||||
|
if (/^settings\.contentTranslationHideLanguages/i.test(path.join('.'))) {
|
||||||
|
store.account.set(
|
||||||
|
'settings-contentTranslationHideLanguages',
|
||||||
|
states.settings.contentTranslationHideLanguages,
|
||||||
|
);
|
||||||
|
}
|
||||||
if (path?.[0] === 'shortcuts') {
|
if (path?.[0] === 'shortcuts') {
|
||||||
store.account.set('shortcuts', states.shortcuts);
|
store.account.set('shortcuts', states.shortcuts);
|
||||||
}
|
}
|
||||||
|
@ -114,8 +122,11 @@ export function saveStatus(status, instance, opts) {
|
||||||
opts,
|
opts,
|
||||||
);
|
);
|
||||||
if (!status) return;
|
if (!status) return;
|
||||||
if (!override && getStatus(status.id)) return;
|
const oldStatus = getStatus(status.id, instance);
|
||||||
|
if (!override && oldStatus) return;
|
||||||
const key = statusKey(status.id, instance);
|
const key = statusKey(status.id, instance);
|
||||||
|
if (oldStatus?._pinned) status._pinned = oldStatus._pinned;
|
||||||
|
if (oldStatus?._filtered) status._filtered = oldStatus._filtered;
|
||||||
states.statuses[key] = status;
|
states.statuses[key] = status;
|
||||||
if (status.reblog) {
|
if (status.reblog) {
|
||||||
const key = statusKey(status.reblog.id, instance);
|
const key = statusKey(status.reblog.id, instance);
|
||||||
|
|
34
src/utils/status-peek.jsx
Normal file
34
src/utils/status-peek.jsx
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import getHTMLText from './getHTMLText';
|
||||||
|
|
||||||
|
function statusPeek(status) {
|
||||||
|
const { spoilerText, content, poll, mediaAttachments } = status;
|
||||||
|
let text = '';
|
||||||
|
if (spoilerText?.trim()) {
|
||||||
|
text += spoilerText;
|
||||||
|
} else {
|
||||||
|
text += getHTMLText(content);
|
||||||
|
}
|
||||||
|
text = text.trim();
|
||||||
|
if (poll) {
|
||||||
|
text += ' 📊';
|
||||||
|
}
|
||||||
|
if (mediaAttachments?.length) {
|
||||||
|
text +=
|
||||||
|
' ' +
|
||||||
|
mediaAttachments
|
||||||
|
.map(
|
||||||
|
(m) =>
|
||||||
|
({
|
||||||
|
image: '🖼️',
|
||||||
|
gifv: '🎞️',
|
||||||
|
video: '📹',
|
||||||
|
audio: '🎵',
|
||||||
|
unknown: '',
|
||||||
|
}[m.type] || ''),
|
||||||
|
)
|
||||||
|
.join('');
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default statusPeek;
|
123
src/utils/timeline-utils.jsx
Normal file
123
src/utils/timeline-utils.jsx
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
import { getStatus } from './states';
|
||||||
|
|
||||||
|
export 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function dedupeBoosts(items, instance) {
|
||||||
|
return items.filter((item) => {
|
||||||
|
if (!item.reblog) return true;
|
||||||
|
const s = getStatus(item.reblog.id, instance);
|
||||||
|
if (s) {
|
||||||
|
console.warn(
|
||||||
|
`🚫 Duplicate boost by ${item.account.displayName}`,
|
||||||
|
item,
|
||||||
|
s,
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const s2 = getStatus(item.id, instance);
|
||||||
|
if (s2) {
|
||||||
|
console.warn('🚫 Re-boosted boost', item);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function groupContext(items) {
|
||||||
|
const contexts = [];
|
||||||
|
let contextIndex = 0;
|
||||||
|
items.forEach((item) => {
|
||||||
|
for (let i = 0; i < contexts.length; i++) {
|
||||||
|
if (contexts[i].find((t) => t.id === item.id)) return;
|
||||||
|
if (
|
||||||
|
contexts[i].find((t) => t.id === item.inReplyToId) ||
|
||||||
|
contexts[i].find((t) => t.inReplyToId === item.id)
|
||||||
|
) {
|
||||||
|
contexts[i].push(item);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const repliedItem = items.find((i) => i.id === item.inReplyToId);
|
||||||
|
if (repliedItem) {
|
||||||
|
contexts[contextIndex++] = [item, repliedItem];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (contexts.length) console.log('🧵 Contexts', contexts);
|
||||||
|
|
||||||
|
const newItems = [];
|
||||||
|
const appliedContextIndices = [];
|
||||||
|
items.forEach((item) => {
|
||||||
|
if (item.reblog) {
|
||||||
|
newItems.push(item);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (let i = 0; i < contexts.length; i++) {
|
||||||
|
if (contexts[i].find((t) => t.id === item.id)) {
|
||||||
|
if (appliedContextIndices.includes(i)) return;
|
||||||
|
const contextItems = contexts[i];
|
||||||
|
contextItems.sort((a, b) => {
|
||||||
|
const aDate = new Date(a.createdAt);
|
||||||
|
const bDate = new Date(b.createdAt);
|
||||||
|
return aDate - bDate;
|
||||||
|
});
|
||||||
|
const firstItemAccountID = contextItems[0].account.id;
|
||||||
|
newItems.push({
|
||||||
|
id: contextItems.map((i) => i.id),
|
||||||
|
items: contextItems,
|
||||||
|
type: contextItems.every((it) => it.account.id === firstItemAccountID)
|
||||||
|
? 'thread'
|
||||||
|
: 'conversation',
|
||||||
|
});
|
||||||
|
appliedContextIndices.push(i);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newItems.push(item);
|
||||||
|
});
|
||||||
|
|
||||||
|
return newItems;
|
||||||
|
}
|
|
@ -5,7 +5,6 @@ import { resolve } from 'path';
|
||||||
import { defineConfig, loadEnv, splitVendorChunkPlugin } from 'vite';
|
import { defineConfig, loadEnv, splitVendorChunkPlugin } from 'vite';
|
||||||
import generateFile from 'vite-plugin-generate-file';
|
import generateFile from 'vite-plugin-generate-file';
|
||||||
import htmlPlugin from 'vite-plugin-html-config';
|
import htmlPlugin from 'vite-plugin-html-config';
|
||||||
import VitePluginHtmlEnv from 'vite-plugin-html-env';
|
|
||||||
import { VitePWA } from 'vite-plugin-pwa';
|
import { VitePWA } from 'vite-plugin-pwa';
|
||||||
import removeConsole from 'vite-plugin-remove-console';
|
import removeConsole from 'vite-plugin-remove-console';
|
||||||
|
|
||||||
|
@ -31,7 +30,6 @@ export default defineConfig({
|
||||||
plugins: [
|
plugins: [
|
||||||
preact(),
|
preact(),
|
||||||
splitVendorChunkPlugin(),
|
splitVendorChunkPlugin(),
|
||||||
VitePluginHtmlEnv(),
|
|
||||||
removeConsole({
|
removeConsole({
|
||||||
includes: ['log', 'debug', 'info', 'warn', 'error'],
|
includes: ['log', 'debug', 'info', 'warn', 'error'],
|
||||||
}),
|
}),
|
||||||
|
@ -88,6 +86,7 @@ export default defineConfig({
|
||||||
build: {
|
build: {
|
||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
|
treeshake: false,
|
||||||
input: {
|
input: {
|
||||||
main: resolve(__dirname, 'index.html'),
|
main: resolve(__dirname, 'index.html'),
|
||||||
compose: resolve(__dirname, 'compose/index.html'),
|
compose: resolve(__dirname, 'compose/index.html'),
|
||||||
|
|
Loading…
Reference in a new issue