Merge pull request #42 from cheeaun/main

Update from main
This commit is contained in:
Chee Aun 2023-01-06 23:24:13 +08:00 committed by GitHub
commit a7a3d5605b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 1565 additions and 754 deletions

38
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View file

@ -0,0 +1,38 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.

View file

@ -79,10 +79,13 @@ It's been **more than 15 years**.
And here I am. Building a Mastodon web client. And here I am. Building a Mastodon web client.
## Alternative clients ## Alternative web clients
- [Pinafore](https://pinafore.social/) - [Pinafore](https://pinafore.social/)
- [Soapbox](https://fe.soapbox.pub/)
- [Elk](https://m.webtoo.ls/@elk) - [Elk](https://m.webtoo.ls/@elk)
- [Mastodeck](https://mastodeck.com/)
-
- [More...](https://github.com/tleb/awesome-mastodon#clients) - [More...](https://github.com/tleb/awesome-mastodon#clients)
## License ## License

228
package-lock.json generated
View file

@ -8,13 +8,14 @@
"name": "phanpy", "name": "phanpy",
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"@github/relative-time-element": "~4.1.5",
"@github/text-expander-element": "~2.3.0", "@github/text-expander-element": "~2.3.0",
"dayjs": "~1.11.7",
"dayjs-twitter": "~0.5.0",
"fast-blurhash": "~1.1.2", "fast-blurhash": "~1.1.2",
"history": "~5.3.0", "history": "~5.3.0",
"iconify-icon": "~1.0.2", "iconify-icon": "~1.0.2",
"just-debounce-it": "~3.2.0", "just-debounce-it": "~3.2.0",
"masto": "~5.1.0", "masto": "~5.1.1",
"mem": "~9.0.2", "mem": "~9.0.2",
"preact": "~10.11.3", "preact": "~10.11.3",
"preact-router": "~4.1.0", "preact-router": "~4.1.0",
@ -24,7 +25,7 @@
"swiped-events": "~1.1.7", "swiped-events": "~1.1.7",
"toastify-js": "~1.12.0", "toastify-js": "~1.12.0",
"use-resize-observer": "~9.1.0", "use-resize-observer": "~9.1.0",
"valtio": "~1.8.0" "valtio": "~1.8.2"
}, },
"devDependencies": { "devDependencies": {
"@preact/preset-vite": "~2.5.0", "@preact/preset-vite": "~2.5.0",
@ -33,10 +34,11 @@
"postcss": "~8.4.20", "postcss": "~8.4.20",
"postcss-dark-theme-class": "~0.7.3", "postcss-dark-theme-class": "~0.7.3",
"twitter-text": "~3.1.0", "twitter-text": "~3.1.0",
"vite": "~4.0.3", "vite": "~4.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-html-env": "~1.2.7",
"vite-plugin-pwa": "~0.14.0", "vite-plugin-pwa": "~0.14.1",
"vite-plugin-remove-console": "~1.3.0",
"workbox-cacheable-response": "~6.5.4", "workbox-cacheable-response": "~6.5.4",
"workbox-expiration": "~6.5.4", "workbox-expiration": "~6.5.4",
"workbox-routing": "~6.5.4", "workbox-routing": "~6.5.4",
@ -311,7 +313,7 @@
"version": "7.18.6", "version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz",
"integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==", "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==",
"devOptional": true, "dev": true,
"dependencies": { "dependencies": {
"@babel/types": "^7.18.6" "@babel/types": "^7.18.6"
}, },
@ -433,7 +435,7 @@
"version": "7.19.4", "version": "7.19.4",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz",
"integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==", "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==",
"devOptional": true, "dev": true,
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
} }
@ -442,7 +444,7 @@
"version": "7.19.1", "version": "7.19.1",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz",
"integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==",
"devOptional": true, "dev": true,
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
} }
@ -1694,7 +1696,7 @@
"version": "7.20.5", "version": "7.20.5",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.5.tgz", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.5.tgz",
"integrity": "sha512-c9fst/h2/dcF7H+MJKZ2T0KjEQ8hY/BNnDk/H3XY8C4Aw/eWQXWn/lWntHF9ooUBnGmEvbfGrTgLWc+um0YDUg==", "integrity": "sha512-c9fst/h2/dcF7H+MJKZ2T0KjEQ8hY/BNnDk/H3XY8C4Aw/eWQXWn/lWntHF9ooUBnGmEvbfGrTgLWc+um0YDUg==",
"devOptional": true, "dev": true,
"dependencies": { "dependencies": {
"@babel/helper-string-parser": "^7.19.4", "@babel/helper-string-parser": "^7.19.4",
"@babel/helper-validator-identifier": "^7.19.1", "@babel/helper-validator-identifier": "^7.19.1",
@ -2061,11 +2063,6 @@
"resolved": "https://registry.npmjs.org/@github/combobox-nav/-/combobox-nav-2.1.5.tgz", "resolved": "https://registry.npmjs.org/@github/combobox-nav/-/combobox-nav-2.1.5.tgz",
"integrity": "sha512-dmG1PuppNKHnBBEcfylWDwj9SSxd/E/qd8mC1G/klQC3s7ps5q6JZ034mwkkG0LKfI+Y+UgEua/ROD776N400w==" "integrity": "sha512-dmG1PuppNKHnBBEcfylWDwj9SSxd/E/qd8mC1G/klQC3s7ps5q6JZ034mwkkG0LKfI+Y+UgEua/ROD776N400w=="
}, },
"node_modules/@github/relative-time-element": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/@github/relative-time-element/-/relative-time-element-4.1.5.tgz",
"integrity": "sha512-WAf1EQV5Sn6jGuAIQur/ztKlEV9R+VHDNwqEbeaOb6s9fiwM5z7+ujlWNZtgFkDp3lF0H8D/f0vdiPlfHz0ZTQ=="
},
"node_modules/@github/text-expander-element": { "node_modules/@github/text-expander-element": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/@github/text-expander-element/-/text-expander-element-2.3.0.tgz", "resolved": "https://registry.npmjs.org/@github/text-expander-element/-/text-expander-element-2.3.0.tgz",
@ -3000,6 +2997,19 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/dayjs": {
"version": "1.11.7",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.7.tgz",
"integrity": "sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ=="
},
"node_modules/dayjs-twitter": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/dayjs-twitter/-/dayjs-twitter-0.5.0.tgz",
"integrity": "sha512-SZ7qEUISstBLUXdlGAbLrwr6zfRM9kaCfbq4uVTerM/HXzuHiiGzzUqAJVhxt+3tf69E+ocmQdP6YYpOINv05w==",
"dependencies": {
"duration-js": "^4.0.0"
}
},
"node_modules/debug": { "node_modules/debug": {
"version": "4.3.4", "version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
@ -3059,6 +3069,11 @@
"tslib": "^2.0.3" "tslib": "^2.0.3"
} }
}, },
"node_modules/duration-js": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/duration-js/-/duration-js-4.0.0.tgz",
"integrity": "sha512-qoXjOsH97r+NrOa6sK5V2cwBOouVG/LI9jwgwKvjVkyqGpZ72yilWjjzFJYPqqbvNZDwpRMaLEUFE+PTefvOEA=="
},
"node_modules/ejs": { "node_modules/ejs": {
"version": "3.1.8", "version": "3.1.8",
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.8.tgz", "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.8.tgz",
@ -3140,7 +3155,7 @@
"version": "0.16.7", "version": "0.16.7",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.16.7.tgz", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.16.7.tgz",
"integrity": "sha512-P6OBFYFSQOGzfApqCeYKqfKRRbCIRsdppTXFo4aAvtiW3o8TTyiIplBvHJI171saPAiy3WlawJHCveJVIOIx1A==", "integrity": "sha512-P6OBFYFSQOGzfApqCeYKqfKRRbCIRsdppTXFo4aAvtiW3o8TTyiIplBvHJI171saPAiy3WlawJHCveJVIOIx1A==",
"devOptional": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"bin": { "bin": {
"esbuild": "bin/esbuild" "esbuild": "bin/esbuild"
@ -3368,7 +3383,7 @@
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
"devOptional": true "dev": true
}, },
"node_modules/function.prototype.name": { "node_modules/function.prototype.name": {
"version": "1.1.5", "version": "1.1.5",
@ -3505,7 +3520,7 @@
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
"devOptional": true, "dev": true,
"dependencies": { "dependencies": {
"function-bind": "^1.1.1" "function-bind": "^1.1.1"
}, },
@ -3678,7 +3693,7 @@
"version": "2.11.0", "version": "2.11.0",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz",
"integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==",
"devOptional": true, "dev": true,
"dependencies": { "dependencies": {
"has": "^1.0.3" "has": "^1.0.3"
}, },
@ -4153,9 +4168,9 @@
} }
}, },
"node_modules/masto": { "node_modules/masto": {
"version": "5.1.0", "version": "5.1.1",
"resolved": "https://registry.npmjs.org/masto/-/masto-5.1.0.tgz", "resolved": "https://registry.npmjs.org/masto/-/masto-5.1.1.tgz",
"integrity": "sha512-/Rvi44BKv9AGGv08Oo63dA2WHE3kwCUtNb1/W0brK9alLaCSboOwTjoWtK46ovjmsm8TugNtKqj2lscxwcFhDQ==", "integrity": "sha512-IvfdpCiayM4tM58aTf/tfkSq0MGW1kKEAwJvgVRbzmwlE4PBt1WnGvZXQg6CiLkcKBMTQaDjLR0sBaGmPrVGCQ==",
"dependencies": { "dependencies": {
"@mastojs/ponyfills": "^1.0.4", "@mastojs/ponyfills": "^1.0.4",
"change-case": "^4.1.2", "change-case": "^4.1.2",
@ -4274,7 +4289,7 @@
"version": "3.3.4", "version": "3.3.4",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz",
"integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==",
"devOptional": true, "dev": true,
"bin": { "bin": {
"nanoid": "bin/nanoid.cjs" "nanoid": "bin/nanoid.cjs"
}, },
@ -4437,13 +4452,13 @@
"version": "1.0.7", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"devOptional": true "dev": true
}, },
"node_modules/picocolors": { "node_modules/picocolors": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
"devOptional": true "dev": true
}, },
"node_modules/picomatch": { "node_modules/picomatch": {
"version": "2.3.1", "version": "2.3.1",
@ -4461,7 +4476,7 @@
"version": "8.4.20", "version": "8.4.20",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.20.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.20.tgz",
"integrity": "sha512-6Q04AXR1212bXr5fh03u8aAwbLxAQNGQ/Q1LNa0VfOI06ZAlhPHtQvE4OIdpj4kLThXilalPnmDSOD65DcHt+g==", "integrity": "sha512-6Q04AXR1212bXr5fh03u8aAwbLxAQNGQ/Q1LNa0VfOI06ZAlhPHtQvE4OIdpj4kLThXilalPnmDSOD65DcHt+g==",
"devOptional": true, "dev": true,
"funding": [ "funding": [
{ {
"type": "opencollective", "type": "opencollective",
@ -4549,9 +4564,9 @@
} }
}, },
"node_modules/proxy-compare": { "node_modules/proxy-compare": {
"version": "2.3.0", "version": "2.4.0",
"resolved": "https://registry.npmjs.org/proxy-compare/-/proxy-compare-2.3.0.tgz", "resolved": "https://registry.npmjs.org/proxy-compare/-/proxy-compare-2.4.0.tgz",
"integrity": "sha512-c3L2CcAi7f7pvlD0D7xsF+2CQIW8C3HaYx2Pfgq8eA4HAl3GAH6/dVYsyBbYF/0XJs2ziGLrzmz5fmzPm6A0pQ==" "integrity": "sha512-FD8KmQUQD6Mfpd0hywCOzcon/dbkFP8XBd9F1ycbKtvVsfv6TsFUKJ2eC0Iz2y+KzlkdT1Z8SY6ZSgm07zOyqg=="
}, },
"node_modules/punycode": { "node_modules/punycode": {
"version": "2.1.1", "version": "2.1.1",
@ -4739,7 +4754,7 @@
"version": "1.22.1", "version": "1.22.1",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz",
"integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==",
"devOptional": true, "dev": true,
"dependencies": { "dependencies": {
"is-core-module": "^2.9.0", "is-core-module": "^2.9.0",
"path-parse": "^1.0.7", "path-parse": "^1.0.7",
@ -4766,7 +4781,7 @@
"version": "3.7.4", "version": "3.7.4",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.7.4.tgz", "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.7.4.tgz",
"integrity": "sha512-jN9rx3k5pfg9H9al0r0y1EYKSeiRANZRYX32SuNXAnKzh6cVyf4LZVto1KAuDnbHT03E1CpsgqDKaqQ8FZtgxw==", "integrity": "sha512-jN9rx3k5pfg9H9al0r0y1EYKSeiRANZRYX32SuNXAnKzh6cVyf4LZVto1KAuDnbHT03E1CpsgqDKaqQ8FZtgxw==",
"devOptional": true, "dev": true,
"bin": { "bin": {
"rollup": "dist/bin/rollup" "rollup": "dist/bin/rollup"
}, },
@ -4908,7 +4923,7 @@
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
"integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
"devOptional": true, "dev": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@ -5044,7 +5059,7 @@
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"devOptional": true, "dev": true,
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
}, },
@ -5106,7 +5121,7 @@
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
"integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==",
"devOptional": true, "dev": true,
"engines": { "engines": {
"node": ">=4" "node": ">=4"
} }
@ -5336,50 +5351,30 @@
} }
}, },
"node_modules/valtio": { "node_modules/valtio": {
"version": "1.8.0", "version": "1.8.2",
"resolved": "https://registry.npmjs.org/valtio/-/valtio-1.8.0.tgz", "resolved": "https://registry.npmjs.org/valtio/-/valtio-1.8.2.tgz",
"integrity": "sha512-lNw7wM0Qb9iBzXMju+XCn+UiIlf5uCe5pcI8XRqcvxEZ/mnRXyKXoOodPDKB8cIAVekA3Q3zWA7rboCdS4ea7g==", "integrity": "sha512-ypFWPi3aY04tojWAFPbTYBDw5iFaCDbKAJ2XqhmY2XOSorNtaCZJNg++FSssv8gMJwmPXfrU/RjncQtsoOHbUg==",
"dependencies": { "dependencies": {
"proxy-compare": "2.3.0", "proxy-compare": "2.4.0",
"use-sync-external-store": "1.2.0" "use-sync-external-store": "1.2.0"
}, },
"engines": { "engines": {
"node": ">=12.7.0" "node": ">=12.7.0"
}, },
"peerDependencies": { "peerDependencies": {
"@babel/helper-module-imports": ">=7.12", "react": ">=16.8"
"@babel/types": ">=7.13",
"aslemammad-vite-plugin-macro": ">=1.0.0-alpha.1",
"babel-plugin-macros": ">=3.0",
"react": ">=16.8",
"vite": ">=2.8.6"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
"@babel/helper-module-imports": {
"optional": true
},
"@babel/types": {
"optional": true
},
"aslemammad-vite-plugin-macro": {
"optional": true
},
"babel-plugin-macros": {
"optional": true
},
"react": { "react": {
"optional": true "optional": true
},
"vite": {
"optional": true
} }
} }
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "4.0.3", "version": "4.0.4",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.0.3.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-4.0.4.tgz",
"integrity": "sha512-HvuNv1RdE7deIfQb8mPk51UKjqptO/4RXZ5yXSAvurd5xOckwS/gg8h9Tky3uSbnjYTgUm0hVCet1cyhKd73ZA==", "integrity": "sha512-xevPU7M8FU0i/80DMR+YhgrzR5KS2ORy1B4xcX/cXLsvnUWvfHuqMmVU6N0YiJ4JWGRJJsLCgjEzKjG9/GKoSw==",
"devOptional": true, "dev": true,
"dependencies": { "dependencies": {
"esbuild": "^0.16.3", "esbuild": "^0.16.3",
"postcss": "^8.4.20", "postcss": "^8.4.20",
@ -5449,9 +5444,9 @@
} }
}, },
"node_modules/vite-plugin-pwa": { "node_modules/vite-plugin-pwa": {
"version": "0.14.0", "version": "0.14.1",
"resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.14.0.tgz", "resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.14.1.tgz",
"integrity": "sha512-3wZx47PLWTckOQhc8Y6YZjAbNZ89Ovh4TdCT97MGhgl7aFd2LUekVnAmIgFwgMqyxzJ93nmkPF/ALpEW/i2qCg==", "integrity": "sha512-5zx7yhQ8RTLwV71+GA9YsQQ63ALKG8XXIMqRJDdZkR8ZYftFcRgnzM7wOWmQZ/DATspyhPih5wCdcZnAIsM+mA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@rollup/plugin-replace": "^5.0.1", "@rollup/plugin-replace": "^5.0.1",
@ -5471,6 +5466,12 @@
"workbox-window": "^6.5.4" "workbox-window": "^6.5.4"
} }
}, },
"node_modules/vite-plugin-remove-console": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/vite-plugin-remove-console/-/vite-plugin-remove-console-1.3.0.tgz",
"integrity": "sha512-5a/OLYB6yNRHMuHj9rBQRYMQ1NBKffxA8BaD77urUBLcGOWMHFHALjh6C26wZfZd41KytSwLp6DhvNKU78mNJg==",
"dev": true
},
"node_modules/webidl-conversions": { "node_modules/webidl-conversions": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz",
@ -6068,7 +6069,7 @@
"version": "7.18.6", "version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz",
"integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==", "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==",
"devOptional": true, "dev": true,
"requires": { "requires": {
"@babel/types": "^7.18.6" "@babel/types": "^7.18.6"
} }
@ -6160,13 +6161,13 @@
"version": "7.19.4", "version": "7.19.4",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz",
"integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==", "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==",
"devOptional": true "dev": true
}, },
"@babel/helper-validator-identifier": { "@babel/helper-validator-identifier": {
"version": "7.19.1", "version": "7.19.1",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz",
"integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==",
"devOptional": true "dev": true
}, },
"@babel/helper-validator-option": { "@babel/helper-validator-option": {
"version": "7.18.6", "version": "7.18.6",
@ -7010,7 +7011,7 @@
"version": "7.20.5", "version": "7.20.5",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.5.tgz", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.5.tgz",
"integrity": "sha512-c9fst/h2/dcF7H+MJKZ2T0KjEQ8hY/BNnDk/H3XY8C4Aw/eWQXWn/lWntHF9ooUBnGmEvbfGrTgLWc+um0YDUg==", "integrity": "sha512-c9fst/h2/dcF7H+MJKZ2T0KjEQ8hY/BNnDk/H3XY8C4Aw/eWQXWn/lWntHF9ooUBnGmEvbfGrTgLWc+um0YDUg==",
"devOptional": true, "dev": true,
"requires": { "requires": {
"@babel/helper-string-parser": "^7.19.4", "@babel/helper-string-parser": "^7.19.4",
"@babel/helper-validator-identifier": "^7.19.1", "@babel/helper-validator-identifier": "^7.19.1",
@ -7176,11 +7177,6 @@
"resolved": "https://registry.npmjs.org/@github/combobox-nav/-/combobox-nav-2.1.5.tgz", "resolved": "https://registry.npmjs.org/@github/combobox-nav/-/combobox-nav-2.1.5.tgz",
"integrity": "sha512-dmG1PuppNKHnBBEcfylWDwj9SSxd/E/qd8mC1G/klQC3s7ps5q6JZ034mwkkG0LKfI+Y+UgEua/ROD776N400w==" "integrity": "sha512-dmG1PuppNKHnBBEcfylWDwj9SSxd/E/qd8mC1G/klQC3s7ps5q6JZ034mwkkG0LKfI+Y+UgEua/ROD776N400w=="
}, },
"@github/relative-time-element": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/@github/relative-time-element/-/relative-time-element-4.1.5.tgz",
"integrity": "sha512-WAf1EQV5Sn6jGuAIQur/ztKlEV9R+VHDNwqEbeaOb6s9fiwM5z7+ujlWNZtgFkDp3lF0H8D/f0vdiPlfHz0ZTQ=="
},
"@github/text-expander-element": { "@github/text-expander-element": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/@github/text-expander-element/-/text-expander-element-2.3.0.tgz", "resolved": "https://registry.npmjs.org/@github/text-expander-element/-/text-expander-element-2.3.0.tgz",
@ -7918,6 +7914,19 @@
"integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==",
"dev": true "dev": true
}, },
"dayjs": {
"version": "1.11.7",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.7.tgz",
"integrity": "sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ=="
},
"dayjs-twitter": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/dayjs-twitter/-/dayjs-twitter-0.5.0.tgz",
"integrity": "sha512-SZ7qEUISstBLUXdlGAbLrwr6zfRM9kaCfbq4uVTerM/HXzuHiiGzzUqAJVhxt+3tf69E+ocmQdP6YYpOINv05w==",
"requires": {
"duration-js": "^4.0.0"
}
},
"debug": { "debug": {
"version": "4.3.4", "version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
@ -7957,6 +7966,11 @@
"tslib": "^2.0.3" "tslib": "^2.0.3"
} }
}, },
"duration-js": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/duration-js/-/duration-js-4.0.0.tgz",
"integrity": "sha512-qoXjOsH97r+NrOa6sK5V2cwBOouVG/LI9jwgwKvjVkyqGpZ72yilWjjzFJYPqqbvNZDwpRMaLEUFE+PTefvOEA=="
},
"ejs": { "ejs": {
"version": "3.1.8", "version": "3.1.8",
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.8.tgz", "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.8.tgz",
@ -8020,7 +8034,7 @@
"version": "0.16.7", "version": "0.16.7",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.16.7.tgz", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.16.7.tgz",
"integrity": "sha512-P6OBFYFSQOGzfApqCeYKqfKRRbCIRsdppTXFo4aAvtiW3o8TTyiIplBvHJI171saPAiy3WlawJHCveJVIOIx1A==", "integrity": "sha512-P6OBFYFSQOGzfApqCeYKqfKRRbCIRsdppTXFo4aAvtiW3o8TTyiIplBvHJI171saPAiy3WlawJHCveJVIOIx1A==",
"devOptional": true, "dev": true,
"requires": { "requires": {
"@esbuild/android-arm": "0.16.7", "@esbuild/android-arm": "0.16.7",
"@esbuild/android-arm64": "0.16.7", "@esbuild/android-arm64": "0.16.7",
@ -8202,7 +8216,7 @@
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
"devOptional": true "dev": true
}, },
"function.prototype.name": { "function.prototype.name": {
"version": "1.1.5", "version": "1.1.5",
@ -8303,7 +8317,7 @@
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
"devOptional": true, "dev": true,
"requires": { "requires": {
"function-bind": "^1.1.1" "function-bind": "^1.1.1"
} }
@ -8431,7 +8445,7 @@
"version": "2.11.0", "version": "2.11.0",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz",
"integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==",
"devOptional": true, "dev": true,
"requires": { "requires": {
"has": "^1.0.3" "has": "^1.0.3"
} }
@ -8777,9 +8791,9 @@
} }
}, },
"masto": { "masto": {
"version": "5.1.0", "version": "5.1.1",
"resolved": "https://registry.npmjs.org/masto/-/masto-5.1.0.tgz", "resolved": "https://registry.npmjs.org/masto/-/masto-5.1.1.tgz",
"integrity": "sha512-/Rvi44BKv9AGGv08Oo63dA2WHE3kwCUtNb1/W0brK9alLaCSboOwTjoWtK46ovjmsm8TugNtKqj2lscxwcFhDQ==", "integrity": "sha512-IvfdpCiayM4tM58aTf/tfkSq0MGW1kKEAwJvgVRbzmwlE4PBt1WnGvZXQg6CiLkcKBMTQaDjLR0sBaGmPrVGCQ==",
"requires": { "requires": {
"@mastojs/ponyfills": "^1.0.4", "@mastojs/ponyfills": "^1.0.4",
"change-case": "^4.1.2", "change-case": "^4.1.2",
@ -8867,7 +8881,7 @@
"version": "3.3.4", "version": "3.3.4",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz",
"integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==",
"devOptional": true "dev": true
}, },
"no-case": { "no-case": {
"version": "3.0.4", "version": "3.0.4",
@ -8994,13 +9008,13 @@
"version": "1.0.7", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"devOptional": true "dev": true
}, },
"picocolors": { "picocolors": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
"devOptional": true "dev": true
}, },
"picomatch": { "picomatch": {
"version": "2.3.1", "version": "2.3.1",
@ -9012,7 +9026,7 @@
"version": "8.4.20", "version": "8.4.20",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.20.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.20.tgz",
"integrity": "sha512-6Q04AXR1212bXr5fh03u8aAwbLxAQNGQ/Q1LNa0VfOI06ZAlhPHtQvE4OIdpj4kLThXilalPnmDSOD65DcHt+g==", "integrity": "sha512-6Q04AXR1212bXr5fh03u8aAwbLxAQNGQ/Q1LNa0VfOI06ZAlhPHtQvE4OIdpj4kLThXilalPnmDSOD65DcHt+g==",
"devOptional": true, "dev": true,
"requires": { "requires": {
"nanoid": "^3.3.4", "nanoid": "^3.3.4",
"picocolors": "^1.0.0", "picocolors": "^1.0.0",
@ -9057,9 +9071,9 @@
"dev": true "dev": true
}, },
"proxy-compare": { "proxy-compare": {
"version": "2.3.0", "version": "2.4.0",
"resolved": "https://registry.npmjs.org/proxy-compare/-/proxy-compare-2.3.0.tgz", "resolved": "https://registry.npmjs.org/proxy-compare/-/proxy-compare-2.4.0.tgz",
"integrity": "sha512-c3L2CcAi7f7pvlD0D7xsF+2CQIW8C3HaYx2Pfgq8eA4HAl3GAH6/dVYsyBbYF/0XJs2ziGLrzmz5fmzPm6A0pQ==" "integrity": "sha512-FD8KmQUQD6Mfpd0hywCOzcon/dbkFP8XBd9F1ycbKtvVsfv6TsFUKJ2eC0Iz2y+KzlkdT1Z8SY6ZSgm07zOyqg=="
}, },
"punycode": { "punycode": {
"version": "2.1.1", "version": "2.1.1",
@ -9200,7 +9214,7 @@
"version": "1.22.1", "version": "1.22.1",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz",
"integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==",
"devOptional": true, "dev": true,
"requires": { "requires": {
"is-core-module": "^2.9.0", "is-core-module": "^2.9.0",
"path-parse": "^1.0.7", "path-parse": "^1.0.7",
@ -9217,7 +9231,7 @@
"version": "3.7.4", "version": "3.7.4",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.7.4.tgz", "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.7.4.tgz",
"integrity": "sha512-jN9rx3k5pfg9H9al0r0y1EYKSeiRANZRYX32SuNXAnKzh6cVyf4LZVto1KAuDnbHT03E1CpsgqDKaqQ8FZtgxw==", "integrity": "sha512-jN9rx3k5pfg9H9al0r0y1EYKSeiRANZRYX32SuNXAnKzh6cVyf4LZVto1KAuDnbHT03E1CpsgqDKaqQ8FZtgxw==",
"devOptional": true, "dev": true,
"requires": { "requires": {
"fsevents": "~2.3.2" "fsevents": "~2.3.2"
} }
@ -9312,7 +9326,7 @@
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
"integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
"devOptional": true "dev": true
}, },
"source-map-support": { "source-map-support": {
"version": "0.5.21", "version": "0.5.21",
@ -9415,7 +9429,7 @@
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"devOptional": true "dev": true
}, },
"swiped-events": { "swiped-events": {
"version": "1.1.7", "version": "1.1.7",
@ -9456,7 +9470,7 @@
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
"integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==",
"devOptional": true "dev": true
}, },
"to-regex-range": { "to-regex-range": {
"version": "5.0.1", "version": "5.0.1",
@ -9629,19 +9643,19 @@
"requires": {} "requires": {}
}, },
"valtio": { "valtio": {
"version": "1.8.0", "version": "1.8.2",
"resolved": "https://registry.npmjs.org/valtio/-/valtio-1.8.0.tgz", "resolved": "https://registry.npmjs.org/valtio/-/valtio-1.8.2.tgz",
"integrity": "sha512-lNw7wM0Qb9iBzXMju+XCn+UiIlf5uCe5pcI8XRqcvxEZ/mnRXyKXoOodPDKB8cIAVekA3Q3zWA7rboCdS4ea7g==", "integrity": "sha512-ypFWPi3aY04tojWAFPbTYBDw5iFaCDbKAJ2XqhmY2XOSorNtaCZJNg++FSssv8gMJwmPXfrU/RjncQtsoOHbUg==",
"requires": { "requires": {
"proxy-compare": "2.3.0", "proxy-compare": "2.4.0",
"use-sync-external-store": "1.2.0" "use-sync-external-store": "1.2.0"
} }
}, },
"vite": { "vite": {
"version": "4.0.3", "version": "4.0.4",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.0.3.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-4.0.4.tgz",
"integrity": "sha512-HvuNv1RdE7deIfQb8mPk51UKjqptO/4RXZ5yXSAvurd5xOckwS/gg8h9Tky3uSbnjYTgUm0hVCet1cyhKd73ZA==", "integrity": "sha512-xevPU7M8FU0i/80DMR+YhgrzR5KS2ORy1B4xcX/cXLsvnUWvfHuqMmVU6N0YiJ4JWGRJJsLCgjEzKjG9/GKoSw==",
"devOptional": true, "dev": true,
"requires": { "requires": {
"esbuild": "^0.16.3", "esbuild": "^0.16.3",
"fsevents": "~2.3.2", "fsevents": "~2.3.2",
@ -9665,9 +9679,9 @@
"requires": {} "requires": {}
}, },
"vite-plugin-pwa": { "vite-plugin-pwa": {
"version": "0.14.0", "version": "0.14.1",
"resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.14.0.tgz", "resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.14.1.tgz",
"integrity": "sha512-3wZx47PLWTckOQhc8Y6YZjAbNZ89Ovh4TdCT97MGhgl7aFd2LUekVnAmIgFwgMqyxzJ93nmkPF/ALpEW/i2qCg==", "integrity": "sha512-5zx7yhQ8RTLwV71+GA9YsQQ63ALKG8XXIMqRJDdZkR8ZYftFcRgnzM7wOWmQZ/DATspyhPih5wCdcZnAIsM+mA==",
"dev": true, "dev": true,
"requires": { "requires": {
"@rollup/plugin-replace": "^5.0.1", "@rollup/plugin-replace": "^5.0.1",
@ -9679,6 +9693,12 @@
"workbox-window": "^6.5.4" "workbox-window": "^6.5.4"
} }
}, },
"vite-plugin-remove-console": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/vite-plugin-remove-console/-/vite-plugin-remove-console-1.3.0.tgz",
"integrity": "sha512-5a/OLYB6yNRHMuHj9rBQRYMQ1NBKffxA8BaD77urUBLcGOWMHFHALjh6C26wZfZd41KytSwLp6DhvNKU78mNJg==",
"dev": true
},
"webidl-conversions": { "webidl-conversions": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz",

View file

@ -7,16 +7,17 @@
"build": "vite build", "build": "vite build",
"preview": "vite preview", "preview": "vite preview",
"fetch-instances": "env $(cat .env.dev | grep -v \"#\" | xargs) node scripts/fetch-instances-list.js", "fetch-instances": "env $(cat .env.dev | grep -v \"#\" | xargs) node scripts/fetch-instances-list.js",
"source-map-explorer": "npx source-map-explorer dist/assets/*.js" "sourcemap": "npx source-map-explorer dist/assets/*.js"
}, },
"dependencies": { "dependencies": {
"@github/relative-time-element": "~4.1.5",
"@github/text-expander-element": "~2.3.0", "@github/text-expander-element": "~2.3.0",
"dayjs": "~1.11.7",
"dayjs-twitter": "~0.5.0",
"fast-blurhash": "~1.1.2", "fast-blurhash": "~1.1.2",
"history": "~5.3.0", "history": "~5.3.0",
"iconify-icon": "~1.0.2", "iconify-icon": "~1.0.2",
"just-debounce-it": "~3.2.0", "just-debounce-it": "~3.2.0",
"masto": "~5.1.0", "masto": "~5.1.1",
"mem": "~9.0.2", "mem": "~9.0.2",
"preact": "~10.11.3", "preact": "~10.11.3",
"preact-router": "~4.1.0", "preact-router": "~4.1.0",
@ -26,7 +27,7 @@
"swiped-events": "~1.1.7", "swiped-events": "~1.1.7",
"toastify-js": "~1.12.0", "toastify-js": "~1.12.0",
"use-resize-observer": "~9.1.0", "use-resize-observer": "~9.1.0",
"valtio": "~1.8.0" "valtio": "~1.8.2"
}, },
"devDependencies": { "devDependencies": {
"@preact/preset-vite": "~2.5.0", "@preact/preset-vite": "~2.5.0",
@ -35,10 +36,11 @@
"postcss": "~8.4.20", "postcss": "~8.4.20",
"postcss-dark-theme-class": "~0.7.3", "postcss-dark-theme-class": "~0.7.3",
"twitter-text": "~3.1.0", "twitter-text": "~3.1.0",
"vite": "~4.0.3", "vite": "~4.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-html-env": "~1.2.7",
"vite-plugin-pwa": "~0.14.0", "vite-plugin-pwa": "~0.14.1",
"vite-plugin-remove-console": "~1.3.0",
"workbox-cacheable-response": "~6.5.4", "workbox-cacheable-response": "~6.5.4",
"workbox-expiration": "~6.5.4", "workbox-expiration": "~6.5.4",
"workbox-routing": "~6.5.4", "workbox-routing": "~6.5.4",

View file

@ -3,6 +3,8 @@ import { ExpirationPlugin } from 'workbox-expiration';
import { RegExpRoute, registerRoute, Route } from 'workbox-routing'; import { RegExpRoute, registerRoute, Route } from 'workbox-routing';
import { CacheFirst, StaleWhileRevalidate } from 'workbox-strategies'; import { CacheFirst, StaleWhileRevalidate } from 'workbox-strategies';
self.__WB_DISABLE_DEV_LOGS = true;
const imageRoute = new Route( const imageRoute = new Route(
({ request, sameOrigin }) => { ({ request, sameOrigin }) => {
const isRemote = !sameOrigin; const isRemote = !sameOrigin;
@ -44,20 +46,20 @@ const apiExtendedRoute = new RegExpRoute(
); );
registerRoute(apiExtendedRoute); registerRoute(apiExtendedRoute);
// Not caching API requests, doesn't seem to be necessary fo now const apiRoute = new RegExpRoute(
// // Matches:
// const apiRoute = new RegExpRoute( // - statuses/:id/context - some contexts are really huge
// /^https?:\/\/[^\/]+\/api\//, /^https?:\/\/[^\/]+\/api\/v\d+\/(statuses\/\d+\/context)/,
// new StaleWhileRevalidate({ new StaleWhileRevalidate({
// cacheName: 'api', cacheName: 'api',
// plugins: [ plugins: [
// new ExpirationPlugin({ new ExpirationPlugin({
// maxAgeSeconds: 60, // 1 minute maxAgeSeconds: 5 * 60, // 5 minutes
// }), }),
// new CacheableResponsePlugin({ new CacheableResponsePlugin({
// statuses: [0, 200], statuses: [0, 200],
// }), }),
// ], ],
// }), }),
// ); );
// registerRoute(apiRoute); registerRoute(apiRoute);

View file

@ -3,7 +3,8 @@ import fs from 'fs';
const { INSTANCES_SOCIAL_SECRET_TOKEN } = process.env; const { INSTANCES_SOCIAL_SECRET_TOKEN } = process.env;
const params = new URLSearchParams({ const params = new URLSearchParams({
count: 200, count: 0,
min_users: 1_000,
sort_by: 'active_users', sort_by: 'active_users',
sort_order: 'desc', sort_order: 'desc',
}); });

View file

@ -90,6 +90,13 @@ a.mention span {
grid-template-columns: 1fr 1fr 1fr; grid-template-columns: 1fr 1fr 1fr;
align-items: center; align-items: center;
user-select: none; user-select: none;
transition: transform 0.5s ease-in-out;
user-select: none;
}
.deck header[hidden] {
transform: translateY(-100%);
pointer-events: none;
user-select: none;
} }
.deck header > .header-side:last-of-type { .deck header > .header-side:last-of-type {
text-align: right; text-align: right;
@ -348,8 +355,10 @@ a.mention span {
display: block; display: block;
text-decoration-line: none; text-decoration-line: none;
color: inherit; color: inherit;
user-select: none;
transition: background-color 0.2s ease-out; transition: background-color 0.2s ease-out;
-webkit-tap-highlight-color: transparent; -webkit-tap-highlight-color: transparent;
animation: appear 0.2s ease-out;
} }
.status-link:is(:hover, :focus) { .status-link:is(:hover, :focus) {
background-color: var(--link-bg-hover-color); background-color: var(--link-bg-hover-color);
@ -357,7 +366,6 @@ a.mention span {
} }
.status-link:active { .status-link:active {
filter: brightness(0.95); filter: brightness(0.95);
transform: translateY(0.5px);
} }
.ui-state { .ui-state {
@ -374,18 +382,17 @@ a.mention span {
z-index: 1000; z-index: 1000;
display: flex; display: flex;
background-color: var(--backdrop-color); background-color: var(--backdrop-color);
animation: appear 0.2s ease-out;
} }
.deck-backdrop > a { .deck-backdrop > a {
flex-grow: 1; flex-grow: 1;
backdrop-filter: saturate(0.75); /* backdrop-filter: saturate(0.75); */
} }
@keyframes slide-in { @keyframes slide-in {
0% { 0% {
opacity: 0.5;
transform: translate3d(100%, 0, 0); transform: translate3d(100%, 0, 0);
} }
100% { 100% {
opacity: 1;
transform: translate3d(0, 0, 0); transform: translate3d(0, 0, 0);
} }
} }
@ -402,7 +409,6 @@ a.mention span {
.decks { .decks {
flex-grow: 1; flex-grow: 1;
transition: transform 0.5s var(--timing-function);
} }
.deck-close { .deck-close {
@ -436,7 +442,7 @@ a.mention span {
} }
.updates-button { .updates-button {
position: absolute; position: absolute;
z-index: 1; z-index: 2;
animation: fade-from-top 2s ease-out; animation: fade-from-top 2s ease-out;
left: 50%; left: 50%;
margin-top: 8px; margin-top: 8px;
@ -602,7 +608,18 @@ button.carousel-dot[disabled].active {
z-index: 1; z-index: 1;
box-shadow: 0 3px 8px -1px var(--bg-faded-blur-color), box-shadow: 0 3px 8px -1px var(--bg-faded-blur-color),
0 10px 36px -4px var(--button-bg-blur-color); 0 10px 36px -4px var(--button-bg-blur-color);
transition: background-color 0.2s ease-in-out; transition: all 0.3s ease-in-out;
}
#compose-button[hidden] {
transform: translateY(200%);
pointer-events: none;
user-select: none;
}
#compose-button .icon {
transition: transform 0.3s ease-in-out;
}
#compose-button[hidden] .icon {
transform: rotate(90deg);
} }
#compose-button:is(:hover, :focus) { #compose-button:is(:hover, :focus) {
background-color: var(--button-bg-color); background-color: var(--button-bg-color);
@ -610,7 +627,6 @@ button.carousel-dot[disabled].active {
} }
#compose-button:active { #compose-button:active {
filter: brightness(0.75); filter: brightness(0.75);
transform: translateY(1px);
} }
#compose-button .icon { #compose-button .icon {
filter: drop-shadow(0 1px 2px var(--button-bg-color)); filter: drop-shadow(0 1px 2px var(--button-bg-color));
@ -637,6 +653,10 @@ button.carousel-dot[disabled].active {
padding: 16px 16px 8px; padding: 16px 16px 8px;
padding-left: max(16px, env(safe-area-inset-left)); padding-left: max(16px, env(safe-area-inset-left));
padding-right: max(16px, env(safe-area-inset-right)); padding-right: max(16px, env(safe-area-inset-right));
user-select: none;
}
.sheet header :is(h1, h2, h3) {
margin: 0;
} }
.sheet main { .sheet main {
overflow: auto; overflow: auto;
@ -800,6 +820,14 @@ meter.donut:is(.danger, .explode):after {
filter: brightness(0.8); filter: brightness(0.8);
} }
/* AVATARS STACK */
.avatars-stack {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
@media (min-width: 40em) { @media (min-width: 40em) {
html, html,
body { body {
@ -808,7 +836,11 @@ meter.donut:is(.danger, .explode):after {
#app { #app {
display: flex; display: flex;
} }
.decks {
transition: transform 0.4s var(--timing-function);
}
.decks:has(~ .deck-backdrop) { .decks:has(~ .deck-backdrop) {
transition: transform 0.4s ease-out;
transform: translate3d(-5vw, 0, 0); transform: translate3d(-5vw, 0, 0);
} }
.deck-backdrop .deck { .deck-backdrop .deck {

View file

@ -82,7 +82,7 @@ function App() {
let account = accounts.find((a) => a.info.id === mastoAccount.id); let account = accounts.find((a) => a.info.id === mastoAccount.id);
if (account) { if (account) {
account.info = mastoAccount; account.info = mastoAccount;
account.instanceURL = instanceURL; account.instanceURL = instanceURL.toLowerCase();
account.accessToken = accessToken; account.accessToken = accessToken;
} else { } else {
account = { account = {
@ -166,7 +166,7 @@ function App() {
console.log(info); console.log(info);
const { uri, domain } = info; const { uri, domain } = info;
const instances = store.local.getJSON('instances') || {}; const instances = store.local.getJSON('instances') || {};
instances[domain || uri] = info; instances[(domain || uri).toLowerCase()] = info;
store.local.setJSON('instances', instances); store.local.setJSON('instances', instances);
})(); })();
}); });
@ -177,31 +177,12 @@ function App() {
return ( return (
<> <>
{isLoggedIn && currentDeck && ( {isLoggedIn && currentDeck && (
<> <div class="decks">
<button {/* Home will never be unmounted */}
type="button" <Home hidden={currentDeck !== 'home'} />
id="compose-button" {/* Notifications can be unmounted */}
onClick={(e) => { {currentDeck === 'notifications' && <Notifications />}
if (e.shiftKey) { </div>
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>
<div class="decks">
{/* Home will never be unmounted */}
<Home hidden={currentDeck !== 'home'} />
{/* Notifications can be unmounted */}
{currentDeck === 'notifications' && <Notifications />}
</div>
</>
)} )}
{!isLoggedIn && uiState === 'loading' && <Loader />} {!isLoggedIn && uiState === 'loading' && <Loader />}
<Router <Router
@ -237,13 +218,22 @@ function App() {
replyToStatus={ replyToStatus={
typeof snapStates.showCompose !== 'boolean' typeof snapStates.showCompose !== 'boolean'
? snapStates.showCompose.replyToStatus ? snapStates.showCompose.replyToStatus
: null : window.__COMPOSE__?.replyToStatus || null
}
editStatus={
states.showCompose?.editStatus ||
window.__COMPOSE__?.editStatus ||
null
}
draftStatus={
states.showCompose?.draftStatus ||
window.__COMPOSE__?.draftStatus ||
null
} }
editStatus={states.showCompose?.editStatus || null}
draftStatus={states.showCompose?.draftStatus || null}
onClose={(results) => { onClose={(results) => {
const { newStatus } = results || {}; const { newStatus } = results || {};
states.showCompose = false; states.showCompose = false;
window.__COMPOSE__ = null;
if (newStatus) { if (newStatus) {
states.reloadStatusPage++; states.reloadStatusPage++;
setTimeout(() => { setTimeout(() => {

View file

@ -197,17 +197,19 @@ function Account({ account }) {
{relationshipUIState !== 'loading' && relationship && ( {relationshipUIState !== 'loading' && relationship && (
<button <button
type="button" type="button"
class={`${following ? 'light' : ''} swap`} class={`${following || requested ? 'light swap' : ''}`}
data-swap-state={following ? 'danger' : ''} data-swap-state={following || requested ? 'danger' : ''}
disabled={relationshipUIState === 'loading'} disabled={relationshipUIState === 'loading'}
onClick={() => { onClick={() => {
setRelationshipUIState('loading'); setRelationshipUIState('loading');
(async () => { (async () => {
try { try {
let newRelationship; let newRelationship;
if (following) { if (following || requested) {
const yes = confirm( const yes = confirm(
'Are you sure that you want to unfollow this account?', requested
? 'Are you sure that you want to withdraw follow request?'
: 'Are you sure that you want to unfollow this account?',
); );
if (yes) { if (yes) {
newRelationship = await masto.v1.accounts.unfollow( newRelationship = await masto.v1.accounts.unfollow(
@ -231,10 +233,18 @@ function Account({ account }) {
<span>Following</span> <span>Following</span>
<span>Unfollow</span> <span>Unfollow</span>
</> </>
) : requested ? (
<>
<span>Requested</span>
<span>Withdraw</span>
</>
) : locked ? (
<>
<Icon icon="lock" /> <span>Follow</span>
</>
) : ( ) : (
'Follow' 'Follow'
)} )}
{/* {following ? 'Unfollow…' : 'Follow'} */}
</button> </button>
)} )}
</p> </p>

View file

@ -28,7 +28,7 @@
max-width: 100%; max-width: 100%;
height: 4em; height: 4em;
min-height: 4em; min-height: 4em;
max-height: 10em; max-height: 50vh;
resize: vertical; resize: vertical;
line-height: 1.4; line-height: 1.4;
} }
@ -255,16 +255,34 @@
align-items: stretch; align-items: stretch;
} }
#compose-container .media-preview { #compose-container .media-preview {
flex-shrink: 1; flex-shrink: 0;
border: 1px solid var(--outline-color);
border-radius: 4px;
overflow: hidden;
width: 80px;
height: 80px;
/* checkerboard background */
background-image: linear-gradient(
45deg,
var(--img-bg-color) 25%,
transparent 25%
),
linear-gradient(-45deg, var(--img-bg-color) 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, var(--img-bg-color) 75%),
linear-gradient(-45deg, transparent 75%, var(--img-bg-color) 75%);
background-size: 10px 10px;
background-position: 0 0, 0 5px, 5px -5px, -5px 0px;
} }
#compose-container .media-preview > * { #compose-container .media-preview > * {
min-width: 80px; width: 80px;
width: 80px !important;
height: 80px; height: 80px;
object-fit: contain; object-fit: contain;
background-color: var(--img-bg-color); vertical-align: middle;
border-radius: 8px; pointer-events: none;
border: 1px solid var(--outline-color); }
#compose-container .media-preview:hover {
box-shadow: 0 0 0 2px var(--link-light-color);
cursor: pointer;
} }
#compose-container .media-attachment textarea { #compose-container .media-attachment textarea {
height: 80px; height: 80px;
@ -389,3 +407,39 @@
display: none; display: none;
} }
} }
#media-sheet main {
padding-top: 8px;
display: flex;
flex-direction: column;
flex: 1;
}
#media-sheet textarea {
width: 100%;
height: 10em;
margin-top: 8px;
}
#media-sheet .media-preview {
border: 2px solid var(--outline-color);
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 16px var(--img-bg-color);
/* checkerboard background */
background-image: linear-gradient(
45deg,
var(--img-bg-color) 25%,
transparent 25%
),
linear-gradient(-45deg, var(--img-bg-color) 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, var(--img-bg-color) 75%),
linear-gradient(-45deg, transparent 75%, var(--img-bg-color) 75%);
background-size: 20px 20px;
background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
}
#media-sheet .media-preview > * {
width: 100%;
height: 100%;
max-height: 50vh;
object-fit: contain;
vertical-align: middle;
}

View file

@ -1,19 +1,25 @@
import './compose.css'; import './compose.css';
import '@github/text-expander-element'; import '@github/text-expander-element';
import { forwardRef } from 'preact/compat';
import { useEffect, useMemo, useRef, useState } from 'preact/hooks'; import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
import { useHotkeys } from 'react-hotkeys-hook';
import stringLength from 'string-length'; import stringLength from 'string-length';
import { useSnapshot } from 'valtio';
import supportedLanguages from '../data/status-supported-languages'; import supportedLanguages from '../data/status-supported-languages';
import urlRegex from '../data/url-regex'; import urlRegex from '../data/url-regex';
import emojifyText from '../utils/emojify-text'; import emojifyText from '../utils/emojify-text';
import openCompose from '../utils/open-compose'; import openCompose from '../utils/open-compose';
import states from '../utils/states';
import store from '../utils/store'; import store from '../utils/store';
import useDebouncedCallback from '../utils/useDebouncedCallback';
import visibilityIconsMap from '../utils/visibility-icons-map'; import visibilityIconsMap from '../utils/visibility-icons-map';
import Avatar from './avatar'; import Avatar from './avatar';
import Icon from './icon'; import Icon from './icon';
import Loader from './loader'; import Loader from './loader';
import Modal from './modal';
import Status from './status'; import Status from './status';
const supportedLanguagesMap = supportedLanguages.reduce((acc, l) => { const supportedLanguagesMap = supportedLanguages.reduce((acc, l) => {
@ -53,6 +59,16 @@ menu.className = 'text-expander-menu';
const DEFAULT_LANG = 'en'; const DEFAULT_LANG = 'en';
// https://github.com/mastodon/mastodon/blob/c4a429ed47e85a6bbf0d470a41cc2f64cf120c19/app/javascript/mastodon/features/compose/util/counter.js
const urlRegexObj = new RegExp(urlRegex.source, urlRegex.flags);
const usernameRegex = /(^|[^\/\w])@(([a-z0-9_]+)@[a-z0-9\.\-]+[a-z0-9]+)/gi;
const urlPlaceholder = '$2xxxxxxxxxxxxxxxxxxxxxxx';
function countableText(inputText) {
return inputText
.replace(urlRegexObj, urlPlaceholder)
.replace(usernameRegex, '$1@$3');
}
function Compose({ function Compose({
onClose, onClose,
replyToStatus, replyToStatus,
@ -61,6 +77,7 @@ function Compose({
standalone, standalone,
hasOpener, hasOpener,
}) { }) {
console.warn('RENDER COMPOSER');
const [uiState, setUIState] = useState('default'); const [uiState, setUIState] = useState('default');
const accounts = store.local.getJSON('accounts'); const accounts = store.local.getJSON('accounts');
@ -72,9 +89,9 @@ function Compose({
const configuration = useMemo(() => { const configuration = useMemo(() => {
try { try {
const instances = store.local.getJSON('instances'); const instances = store.local.getJSON('instances');
const currentInstance = accounts.find( const currentInstance = accounts
(a) => a.info.id === currentAccount, .find((a) => a.info.id === currentAccount)
).instanceURL; .instanceURL.toLowerCase();
const config = instances[currentInstance].configuration; const config = instances[currentInstance].configuration;
console.log(config); console.log(config);
return config; return config;
@ -222,130 +239,6 @@ function Compose({
} }
}, [draftStatus, editStatus, replyToStatus]); }, [draftStatus, editStatus, replyToStatus]);
const textExpanderRef = useRef();
const textExpanderTextRef = useRef('');
useEffect(() => {
if (textExpanderRef.current) {
const handleChange = (e) => {
// console.log('text-expander-change', e);
const { key, provide, text } = e.detail;
textExpanderTextRef.current = text;
if (text === '') {
provide(
Promise.resolve({
matched: false,
}),
);
return;
}
if (key === ':') {
// const emojis = customEmojis.current.filter((emoji) =>
// emoji.shortcode.startsWith(text),
// );
const emojis = filterShortcodes(customEmojis.current, text);
let html = '';
emojis.forEach((emoji) => {
const { shortcode, url } = emoji;
html += `
<li role="option" data-value="${encodeHTML(shortcode)}">
<img src="${encodeHTML(
url,
)}" width="16" height="16" alt="" loading="lazy" />
:${encodeHTML(shortcode)}:
</li>`;
});
// console.log({ emojis, html });
menu.innerHTML = html;
provide(
Promise.resolve({
matched: emojis.length > 0,
fragment: menu,
}),
);
return;
}
const type = {
'@': 'accounts',
'#': 'hashtags',
}[key];
provide(
new Promise((resolve) => {
const searchResults = masto.v2.search({
type,
q: text,
limit: 5,
});
searchResults.then((value) => {
if (text !== textExpanderTextRef.current) {
return;
}
console.log({ value, type, v: value[type] });
const results = value[type];
console.log('RESULTS', value, results);
let html = '';
results.forEach((result) => {
const {
name,
avatarStatic,
displayName,
username,
acct,
emojis,
} = result;
const displayNameWithEmoji = emojifyText(displayName, emojis);
// const item = menuItem.cloneNode();
if (acct) {
html += `
<li role="option" data-value="${encodeHTML(acct)}">
<span class="avatar">
<img src="${encodeHTML(
avatarStatic,
)}" width="16" height="16" alt="" loading="lazy" />
</span>
<span>
<b>${displayNameWithEmoji || username}</b>
<br>@${encodeHTML(acct)}
</span>
</li>
`;
} else {
html += `
<li role="option" data-value="${encodeHTML(name)}">
<span>#<b>${encodeHTML(name)}</b></span>
</li>
`;
}
menu.innerHTML = html;
});
console.log('MENU', results, menu);
resolve({
matched: results.length > 0,
fragment: menu,
});
});
}),
);
};
textExpanderRef.current.addEventListener(
'text-expander-change',
handleChange,
);
textExpanderRef.current.addEventListener('text-expander-value', (e) => {
const { key, item } = e.detail;
if (key === ':') {
e.detail.value = `:${item.dataset.value}:`;
} else {
e.detail.value = `${key}${item.dataset.value}`;
}
});
}
}, []);
const formRef = useRef(); const formRef = useRef();
const beforeUnloadCopy = const beforeUnloadCopy =
@ -431,19 +324,28 @@ function Compose({
}); });
}, []); }, []);
const [charCount, setCharCount] = useState(
textareaRef.current?.value?.length +
spoilerTextRef.current?.value?.length || 0,
);
const leftChars = maxCharacters - charCount;
const getCharCount = () => { const getCharCount = () => {
const { value } = textareaRef.current; const { value } = textareaRef.current;
const { value: spoilerText } = spoilerTextRef.current; const { value: spoilerText } = spoilerTextRef.current;
return stringLength(countableText(value)) + stringLength(spoilerText); return stringLength(countableText(value)) + stringLength(spoilerText);
}; };
const updateCharCount = () => { const updateCharCount = () => {
setCharCount(getCharCount()); const count = getCharCount();
states.composerCharacterCount = count;
}; };
useEffect(updateCharCount, []);
useHotkeys(
'esc',
() => {
if (!standalone && confirmClose()) {
onClose();
}
},
{
enableOnFormTags: true,
},
);
return ( return (
<div id="compose-container" class={standalone ? 'standalone' : ''}> <div id="compose-container" class={standalone ? 'standalone' : ''}>
@ -463,21 +365,21 @@ function Compose({
disabled={uiState === 'loading'} disabled={uiState === 'loading'}
onClick={() => { onClick={() => {
// If there are non-ID media attachments (not yet uploaded), show confirmation dialog because they are not going to be passed to the new window // If there are non-ID media attachments (not yet uploaded), show confirmation dialog because they are not going to be passed to the new window
const containNonIDMediaAttachments = // const containNonIDMediaAttachments =
mediaAttachments.length > 0 && // mediaAttachments.length > 0 &&
mediaAttachments.some((media) => !media.id); // mediaAttachments.some((media) => !media.id);
if (containNonIDMediaAttachments) { // if (containNonIDMediaAttachments) {
const yes = confirm( // const yes = confirm(
'You have media attachments that are not yet uploaded. Opening a new window will discard them and you will need to re-attach them. Are you sure you want to continue?', // 'You have media attachments that are not yet uploaded. Opening a new window will discard them and you will need to re-attach them. Are you sure you want to continue?',
); // );
if (!yes) { // if (!yes) {
return; // return;
} // }
} // }
const mediaAttachmentsWithIDs = mediaAttachments.filter( // const mediaAttachmentsWithIDs = mediaAttachments.filter(
(media) => media.id, // (media) => media.id,
); // );
const newWin = openCompose({ const newWin = openCompose({
editStatus, editStatus,
@ -489,7 +391,7 @@ function Compose({
language, language,
sensitive, sensitive,
poll, poll,
mediaAttachments: mediaAttachmentsWithIDs, mediaAttachments,
}, },
}); });
@ -524,17 +426,17 @@ function Compose({
disabled={uiState === 'loading'} disabled={uiState === 'loading'}
onClick={() => { onClick={() => {
// If there are non-ID media attachments (not yet uploaded), show confirmation dialog because they are not going to be passed to the new window // If there are non-ID media attachments (not yet uploaded), show confirmation dialog because they are not going to be passed to the new window
const containNonIDMediaAttachments = // const containNonIDMediaAttachments =
mediaAttachments.length > 0 && // mediaAttachments.length > 0 &&
mediaAttachments.some((media) => !media.id); // mediaAttachments.some((media) => !media.id);
if (containNonIDMediaAttachments) { // if (containNonIDMediaAttachments) {
const yes = confirm( // const yes = confirm(
'You have media attachments that are not yet uploaded. Opening a new window will discard them and you will need to re-attach them. Are you sure you want to continue?', // 'You have media attachments that are not yet uploaded. Opening a new window will discard them and you will need to re-attach them. Are you sure you want to continue?',
); // );
if (!yes) { // if (!yes) {
return; // return;
} // }
} // }
if (!window.opener) { if (!window.opener) {
alert('Looks like you closed the parent window.'); alert('Looks like you closed the parent window.');
@ -548,13 +450,13 @@ function Compose({
if (!yes) return; if (!yes) return;
} }
const mediaAttachmentsWithIDs = mediaAttachments.filter( // const mediaAttachmentsWithIDs = mediaAttachments.filter(
(media) => media.id, // (media) => media.id,
); // );
onClose({ onClose({
fn: () => { fn: () => {
window.opener.__STATES__.showCompose = { const passData = {
editStatus, editStatus,
replyToStatus, replyToStatus,
draftStatus: { draftStatus: {
@ -564,9 +466,11 @@ function Compose({
language, language,
sensitive, sensitive,
poll, poll,
mediaAttachments: mediaAttachmentsWithIDs, mediaAttachments,
}, },
}; };
window.opener.__COMPOSE__ = passData;
window.opener.__STATES__.showCompose = true;
}, },
}); });
}} }}
@ -766,7 +670,7 @@ function Compose({
name="sensitive" name="sensitive"
type="checkbox" type="checkbox"
checked={sensitive} checked={sensitive}
disabled={uiState === 'loading' || !!editStatus} disabled={uiState === 'loading'}
onChange={(e) => { onChange={(e) => {
const sensitive = e.target.checked; const sensitive = e.target.checked;
setSensitive(sensitive); setSensitive(sensitive);
@ -803,41 +707,22 @@ function Compose({
</select> </select>
</label>{' '} </label>{' '}
</div> </div>
<text-expander ref={textExpanderRef} keys="@ # :"> <Textarea
<textarea ref={textareaRef}
ref={textareaRef} placeholder={
placeholder={ replyToStatus
replyToStatus ? 'Post your reply'
? 'Post your reply' : editStatus
: editStatus ? 'Edit your status'
? 'Edit your status' : 'What are you doing?'
: 'What are you doing?' }
} required={mediaAttachments.length === 0}
required={mediaAttachments.length === 0} disabled={uiState === 'loading'}
autoCapitalize="sentences" onInput={() => {
autoComplete="on" updateCharCount();
autoCorrect="on" }}
spellCheck="true" maxCharacters={maxCharacters}
dir="auto" />
rows="6"
cols="50"
name="status"
disabled={uiState === 'loading'}
onInput={(e) => {
const { scrollHeight, offsetHeight, clientHeight, value } =
e.target;
const offset = offsetHeight - clientHeight;
e.target.style.height = value
? scrollHeight + offset + 'px'
: null;
updateCharCount();
}}
style={{
maxHeight: `${maxCharacters / 50}em`,
'--text-weight': (1 + charCount / 140).toFixed(1) || 1,
}}
></textarea>
</text-expander>
{mediaAttachments.length > 0 && ( {mediaAttachments.length > 0 && (
<div class="media-attachments"> <div class="media-attachments">
{mediaAttachments.map((attachment, i) => { {mediaAttachments.map((attachment, i) => {
@ -942,26 +827,8 @@ function Compose({
</button>{' '} </button>{' '}
<div class="spacer" /> <div class="spacer" />
{uiState === 'loading' && <Loader abrupt />}{' '} {uiState === 'loading' && <Loader abrupt />}{' '}
{uiState !== 'loading' && charCount > maxCharacters / 2 && ( {uiState !== 'loading' && (
<> <CharCountMeter maxCharacters={maxCharacters} />
<meter
class={`donut ${
leftChars <= -10
? 'explode'
: leftChars <= 0
? 'danger'
: leftChars <= 20
? 'warning'
: ''
}`}
value={charCount}
max={maxCharacters}
data-left={leftChars}
style={{
'--percentage': (charCount / maxCharacters) * 100,
}}
/>{' '}
</>
)} )}
<label class="toolbar-button"> <label class="toolbar-button">
<span class="icon-text"> <span class="icon-text">
@ -997,32 +864,269 @@ function Compose({
); );
} }
const Textarea = forwardRef((props, ref) => {
const [text, setText] = useState(ref.current?.value || '');
const { maxCharacters, ...textareaProps } = props;
const snapStates = useSnapshot(states);
const charCount = snapStates.composerCharacterCount;
const textExpanderRef = useRef();
const textExpanderTextRef = useRef('');
useEffect(() => {
let handleChange, handleValue, handleCommited;
if (textExpanderRef.current) {
handleChange = (e) => {
// console.log('text-expander-change', e);
const { key, provide, text } = e.detail;
textExpanderTextRef.current = text;
if (text === '') {
provide(
Promise.resolve({
matched: false,
}),
);
return;
}
if (key === ':') {
// const emojis = customEmojis.current.filter((emoji) =>
// emoji.shortcode.startsWith(text),
// );
const emojis = filterShortcodes(customEmojis.current, text);
let html = '';
emojis.forEach((emoji) => {
const { shortcode, url } = emoji;
html += `
<li role="option" data-value="${encodeHTML(shortcode)}">
<img src="${encodeHTML(
url,
)}" width="16" height="16" alt="" loading="lazy" />
:${encodeHTML(shortcode)}:
</li>`;
});
// console.log({ emojis, html });
menu.innerHTML = html;
provide(
Promise.resolve({
matched: emojis.length > 0,
fragment: menu,
}),
);
return;
}
const type = {
'@': 'accounts',
'#': 'hashtags',
}[key];
provide(
new Promise((resolve) => {
const searchResults = masto.v2.search({
type,
q: text,
limit: 5,
});
searchResults.then((value) => {
if (text !== textExpanderTextRef.current) {
return;
}
console.log({ value, type, v: value[type] });
const results = value[type];
console.log('RESULTS', value, results);
let html = '';
results.forEach((result) => {
const {
name,
avatarStatic,
displayName,
username,
acct,
emojis,
} = result;
const displayNameWithEmoji = emojifyText(displayName, emojis);
// const item = menuItem.cloneNode();
if (acct) {
html += `
<li role="option" data-value="${encodeHTML(acct)}">
<span class="avatar">
<img src="${encodeHTML(
avatarStatic,
)}" width="16" height="16" alt="" loading="lazy" />
</span>
<span>
<b>${displayNameWithEmoji || username}</b>
<br>@${encodeHTML(acct)}
</span>
</li>
`;
} else {
html += `
<li role="option" data-value="${encodeHTML(name)}">
<span>#<b>${encodeHTML(name)}</b></span>
</li>
`;
}
menu.innerHTML = html;
});
console.log('MENU', results, menu);
resolve({
matched: results.length > 0,
fragment: menu,
});
});
}),
);
};
textExpanderRef.current.addEventListener(
'text-expander-change',
handleChange,
);
handleValue = (e) => {
const { key, item } = e.detail;
if (key === ':') {
e.detail.value = `:${item.dataset.value}:`;
} else {
e.detail.value = `${key}${item.dataset.value}`;
}
};
textExpanderRef.current.addEventListener(
'text-expander-value',
handleValue,
);
handleCommited = (e) => {
const { input } = e.detail;
setText(input.value);
};
textExpanderRef.current.addEventListener(
'text-expander-committed',
handleCommited,
);
}
return () => {
if (textExpanderRef.current) {
textExpanderRef.current.removeEventListener(
'text-expander-change',
handleChange,
);
textExpanderRef.current.removeEventListener(
'text-expander-value',
handleValue,
);
textExpanderRef.current.removeEventListener(
'text-expander-committed',
handleCommited,
);
}
};
}, []);
return (
<text-expander ref={textExpanderRef} keys="@ # :">
<textarea
autoCapitalize="sentences"
autoComplete="on"
autoCorrect="on"
spellCheck="true"
dir="auto"
rows="6"
cols="50"
{...textareaProps}
ref={ref}
name="status"
value={text}
onInput={(e) => {
const { scrollHeight, offsetHeight, clientHeight, value } = e.target;
setText(value);
const offset = offsetHeight - clientHeight;
e.target.style.height = value ? scrollHeight + offset + 'px' : null;
props.onInput?.(e);
}}
style={{
width: '100%',
height: '4em',
'--text-weight': (1 + charCount / 140).toFixed(1) || 1,
}}
/>
</text-expander>
);
});
function CharCountMeter({ maxCharacters = 500 }) {
const snapStates = useSnapshot(states);
const charCount = snapStates.composerCharacterCount;
const leftChars = maxCharacters - charCount;
if (charCount <= maxCharacters / 2) {
return null;
}
return (
<meter
class={`donut ${
leftChars <= -10
? 'explode'
: leftChars <= 0
? 'danger'
: leftChars <= 20
? 'warning'
: ''
}`}
value={charCount}
max={maxCharacters}
data-left={leftChars}
style={{
'--percentage': (charCount / maxCharacters) * 100,
}}
/>
);
}
function MediaAttachment({ function MediaAttachment({
attachment, attachment,
disabled, disabled,
onDescriptionChange = () => {}, onDescriptionChange = () => {},
onRemove = () => {}, onRemove = () => {},
}) { }) {
const { url, type, id, description } = attachment; const { url, type, id } = attachment;
console.log({ attachment });
const [description, setDescription] = useState(attachment.description);
const suffixType = type.split('/')[0]; const suffixType = type.split('/')[0];
return ( const debouncedOnDescriptionChange = useDebouncedCallback(
<div class="media-attachment"> onDescriptionChange,
<div class="media-preview"> 500,
{suffixType === 'image' ? ( );
<img src={url} alt="" />
) : suffixType === 'video' || suffixType === 'gifv' ? ( const [showModal, setShowModal] = useState(false);
<video src={url} playsinline muted /> const textareaRef = useRef(null);
) : suffixType === 'audio' ? ( useEffect(() => {
<audio src={url} controls /> let timer;
) : null} if (showModal && textareaRef.current) {
</div> timer = setTimeout(() => {
textareaRef.current.focus();
}, 100);
}
return () => {
clearTimeout(timer);
};
}, [showModal]);
const descTextarea = (
<>
{!!id ? ( {!!id ? (
<div class="media-desc"> <div class="media-desc">
<span class="tag">Uploaded</span> <span class="tag">Uploaded</span>
<p title={description}>{description || <i>No description</i>}</p> <p title={description}>
{attachment.description || <i>No description</i>}
</p>
</div> </div>
) : ( ) : (
<textarea <textarea
ref={textareaRef}
value={description || ''} value={description || ''}
placeholder={ placeholder={
{ {
@ -1041,21 +1145,79 @@ function MediaAttachment({
// TODO: Un-hard-code this maxlength, ref: https://github.com/mastodon/mastodon/blob/b59fb28e90bc21d6fd1a6bafd13cfbd81ab5be54/app/models/media_attachment.rb#L39 // TODO: Un-hard-code this maxlength, ref: https://github.com/mastodon/mastodon/blob/b59fb28e90bc21d6fd1a6bafd13cfbd81ab5be54/app/models/media_attachment.rb#L39
onInput={(e) => { onInput={(e) => {
const { value } = e.target; const { value } = e.target;
onDescriptionChange(value); setDescription(value);
debouncedOnDescriptionChange(value);
}} }}
></textarea> ></textarea>
)} )}
<div class="media-aside"> </>
<button );
type="button"
class="plain close-button" return (
disabled={disabled} <>
onClick={onRemove} <div class="media-attachment">
<div
class="media-preview"
onClick={() => {
setShowModal(true);
}}
> >
<Icon icon="x" /> {suffixType === 'image' ? (
</button> <img src={url} alt="" />
) : suffixType === 'video' || suffixType === 'gifv' ? (
<video src={url} playsinline muted />
) : suffixType === 'audio' ? (
<audio src={url} controls />
) : null}
</div>
{descTextarea}
<div class="media-aside">
<button
type="button"
class="plain close-button"
disabled={disabled}
onClick={onRemove}
>
<Icon icon="x" />
</button>
</div>
</div> </div>
</div> {showModal && (
<Modal
onClick={(e) => {
if (e.target === e.currentTarget) {
setShowModal(false);
}
}}
>
<div id="media-sheet" class="sheet">
<header>
<h2>
{
{
image: 'Edit image description',
video: 'Edit video description',
audio: 'Edit audio description',
}[suffixType]
}
</h2>
</header>
<main tabIndex="-1">
<div class="media-preview">
{suffixType === 'image' ? (
<img src={url} alt="" />
) : suffixType === 'video' || suffixType === 'gifv' ? (
<video src={url} playsinline controls />
) : suffixType === 'audio' ? (
<audio src={url} controls />
) : null}
</div>
{descTextarea}
</main>
</div>
</Modal>
)}
</>
); );
} }
@ -1205,16 +1367,6 @@ function encodeHTML(str) {
}); });
} }
// https://github.com/mastodon/mastodon/blob/c4a429ed47e85a6bbf0d470a41cc2f64cf120c19/app/javascript/mastodon/features/compose/util/counter.js
const urlRegexObj = new RegExp(urlRegex.source, urlRegex.flags);
const usernameRegex = /(^|[^\/\w])@(([a-z0-9_]+)@[a-z0-9\.\-]+[a-z0-9]+)/gi;
const urlPlaceholder = '$2xxxxxxxxxxxxxxxxxxxxxxx';
function countableText(inputText) {
return inputText
.replace(urlRegexObj, urlPlaceholder)
.replace(usernameRegex, '$1@$3');
}
function removeNullUndefined(obj) { function removeNullUndefined(obj) {
for (let key in obj) { for (let key in obj) {
if (obj[key] === null || obj[key] === undefined) { if (obj[key] === null || obj[key] === undefined) {

View file

@ -16,6 +16,8 @@ function NameText({ account, showAvatar, showAcct, short, external, onClick }) {
username.toLowerCase().trim() === username.toLowerCase().trim() ===
(displayName || '') (displayName || '')
.replace(/(\:(\w|\+|\-)+\:)(?=|[\!\.\?]|$)/g, '') // Remove shortcodes, regex from https://regex101.com/r/iE9uV0/1 .replace(/(\:(\w|\+|\-)+\:)(?=|[\!\.\?]|$)/g, '') // Remove shortcodes, regex from https://regex101.com/r/iE9uV0/1
.replace(/\s+/g, '') // E.g. "My name" === "myname"
.replace(/[^a-z0-9]/gi, '') // Remove non-alphanumeric characters
.toLowerCase() .toLowerCase()
.trim() .trim()
) { ) {

View file

@ -0,0 +1,58 @@
// Twitter-style relative time component
// Seconds = 1s
// Minutes = 1m
// Hours = 1h
// Days = 1d
// After 7 days, use DD/MM/YYYY or MM/DD/YYYY
import dayjs from 'dayjs';
import dayjsTwitter from 'dayjs-twitter';
import localizedFormat from 'dayjs/plugin/localizedFormat';
import relativeTime from 'dayjs/plugin/relativeTime';
import { useEffect, useState } from 'preact/hooks';
dayjs.extend(dayjsTwitter);
dayjs.extend(localizedFormat);
dayjs.extend(relativeTime);
const dtf = new Intl.DateTimeFormat();
export default function RelativeTime({ datetime, format }) {
if (!datetime) return null;
const date = dayjs(datetime);
const [dateStr, setDateStr] = useState('');
useEffect(() => {
let timer, raf;
const update = () => {
raf = requestAnimationFrame(() => {
let str;
if (format === 'micro') {
// If date <= 1 day ago or day is within this year
const now = dayjs();
const dayDiff = now.diff(date, 'day');
if (dayDiff <= 1 || now.year() === date.year()) {
str = date.twitter();
} else {
str = dtf.format(date.toDate());
}
} else {
str = date.fromNow();
}
setDateStr(str);
timer = setTimeout(update, 30_000);
});
};
raf = requestAnimationFrame(update);
return () => {
clearTimeout(timer);
cancelAnimationFrame(raf);
};
}, [date]);
return (
<time datetime={date.toISOString()} title={date.format('LLLL')}>
{dateStr}
</time>
);
}

View file

@ -103,6 +103,9 @@
justify-content: space-between; justify-content: space-between;
white-space: nowrap; white-space: nowrap;
} }
.status.small > .container > .meta {
margin-bottom: 4px;
}
.status > .container > .meta > * { .status > .container > .meta > * {
min-width: 0; min-width: 0;
overflow: hidden; overflow: hidden;
@ -179,33 +182,56 @@
gap: 8px; gap: 8px;
align-items: center; align-items: center;
} }
.status .content-container.has-spoiler .spoiler ~ * { .status
/* filter: blur(6px) invert(0.5); */ .content-container.has-spoiler
filter: url(#spoiler); .spoiler
transform: translate3d(-5px, -5px, 0); ~ *:not(.media-container, .card),
.status .content-container.has-spoiler .spoiler ~ .card .meta-container {
filter: blur(6px) invert(0.5);
/* filter: url(#spoiler); */
text-rendering: optimizeSpeed;
image-rendering: crisp-edges;
image-rendering: pixelated;
/* transform: translate3d(-5px, -5px, 0); */
pointer-events: none; pointer-events: none;
user-select: none; user-select: none;
contain: layout; contain: layout;
} }
@media (prefers-color-scheme: dark) { .status .content-container.has-spoiler .spoiler ~ .media-container .media > *,
.status .content-container.has-spoiler .spoiler ~ * { .status .content-container.has-spoiler .spoiler ~ .card > img {
filter: blur(32px);
image-rendering: crisp-edges;
image-rendering: pixelated;
animation: none !important;
}
/* @media (prefers-color-scheme: dark) {
.status
.content-container.has-spoiler
.spoiler
~ *:not(.media-container, .card),
.status .content-container.has-spoiler .spoiler ~ .card .meta-container {
filter: url(#spoiler-dark); filter: url(#spoiler-dark);
} }
} } */
.status .content-container.has-spoiler .spoiler ~ .content ~ * {
opacity: 0.5;
}
.status .content-container.show-spoiler .spoiler { .status .content-container.show-spoiler .spoiler {
border-style: dotted; border-style: dotted;
} }
.status .content-container.show-spoiler .spoiler ~ * { .status
.content-container.show-spoiler
.spoiler
~ *:not(.media-container, .card),
.status .content-container.show-spoiler .spoiler ~ .card .meta-container {
filter: none !important; filter: none !important;
transform: none; transform: none;
pointer-events: auto; pointer-events: auto;
user-select: auto; user-select: auto;
text-rendering: auto;
image-rendering: auto;
} }
.status .content-container.has-spoiler .spoiler ~ .content ~ * { .status .content-container.show-spoiler .spoiler ~ .media-container .media > *,
opacity: 1; .status .content-container.show-spoiler .spoiler ~ .card > img {
filter: none;
image-rendering: auto;
} }
.timeline-deck .status .content { .timeline-deck .status .content {
@ -353,48 +379,67 @@
height: 100%; height: 100%;
object-fit: contain; object-fit: contain;
} }
.status .media-video { .status .media-video,
.status .media-gif {
position: relative; position: relative;
background-clip: padding-box; background-clip: padding-box;
} }
.status .media-video:before { .status .media-video[data-formatted-duration]:before {
/* draw a circle in the middle */ pointer-events: none;
content: ''; content: '⏵';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 70px; width: 70px;
height: 70px; height: 70px;
border-radius: 50%; font-size: 50px;
position: absolute;
text-indent: 3px;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
color: var(--text-insignificant-color);
background-color: var(--bg-blur-color); background-color: var(--bg-blur-color);
backdrop-filter: blur(6px) saturate(3) invert(0.2); backdrop-filter: blur(6px) saturate(3) invert(0.2);
z-index: 1; display: flex;
place-content: center;
place-items: center;
border-radius: 70px;
} }
.status .media-video:after { .status .media-video[data-formatted-duration]:hover:before {
/* show play icon */ color: var(--text-color);
content: ''; }
position: absolute; .status .media-video[data-formatted-duration]:after {
top: 50%; font-size: 12px;
left: 50%;
transform: translate(-35%, -50%);
width: 0;
height: 0;
border-style: solid;
border-width: 15px 0 15px 26px;
border-color: transparent transparent transparent
var(--text-insignificant-color);
pointer-events: none; pointer-events: none;
opacity: 0.75; content: attr(data-formatted-duration);
z-index: 2; position: absolute;
bottom: 8px;
left: 8px;
color: var(--bg-color);
background-color: var(--text-color);
backdrop-filter: blur(6px) saturate(3) invert(0.2);
border-radius: 4px;
padding: 0 4px;
} }
.status .media-video:hover:after { .status .media-gif[data-label]:not(:hover):after {
opacity: 1; font-size: 12px;
font-weight: bold;
pointer-events: none;
content: attr(data-label);
position: absolute;
bottom: 8px;
left: 8px;
color: var(--bg-faded-color);
background-color: var(--text-insignificant-color);
backdrop-filter: blur(6px) saturate(3) invert(0.2);
border-radius: 4px;
padding: 0 4px;
} }
.status .media-gif video { .status .media-gif video {
object-fit: cover; object-fit: cover;
pointer-events: none; pointer-events: none;
} }
.status .media-contain video {
object-fit: contain !important;
}
.status .media-audio { .status .media-audio {
border: 0; border: 0;
min-height: 0; min-height: 0;
@ -434,6 +479,7 @@
width: 100%; width: 100%;
max-height: 50vh; max-height: 50vh;
border-inline-end: 0; border-inline-end: 0;
border-block-end: 1px solid var(--outline-color);
} }
.card:is(:hover, :focus) .image { .card:is(:hover, :focus) .image {
animation: position-object 5s ease-in-out 1s 5; animation: position-object 5s ease-in-out 1s 5;
@ -447,6 +493,9 @@
flex-grow: 1; flex-grow: 1;
align-self: center; align-self: center;
} }
.card.large .meta-container {
align-self: flex-start;
}
.card .title { .card .title {
line-height: 1.25; line-height: 1.25;
font-weight: normal; font-weight: normal;

View file

@ -28,6 +28,7 @@ import visibilityIconsMap from '../utils/visibility-icons-map';
import Avatar from './avatar'; import Avatar from './avatar';
import Icon from './icon'; import Icon from './icon';
import RelativeTime from './relative-time';
function fetchAccount(id) { function fetchAccount(id) {
return masto.v1.accounts.fetch(id); return masto.v1.accounts.fetch(id);
@ -94,6 +95,7 @@ function Status({
filtered, filtered,
card, card,
createdAt, createdAt,
inReplyToId,
inReplyToAccountId, inReplyToAccountId,
content, content,
mentions, mentions,
@ -252,14 +254,7 @@ function Status({
alt={visibility} alt={visibility}
size="s" size="s"
/>{' '} />{' '}
<relative-time <RelativeTime datetime={createdAtDate} format="micro" />
datetime={createdAtDate.toISOString()}
format="micro"
threshold="P1D"
prefix=""
>
{createdAtDate.toLocaleString()}
</relative-time>
</a> </a>
) : ( ) : (
<span class="time"> <span class="time">
@ -268,37 +263,34 @@ function Status({
alt={visibility} alt={visibility}
size="s" size="s"
/>{' '} />{' '}
<relative-time <RelativeTime datetime={createdAtDate} format="micro" />
datetime={createdAtDate.toISOString()}
format="micro"
threshold="P1D"
prefix=""
>
{createdAtDate.toLocaleString()}
</relative-time>
</span> </span>
))} ))}
</div> </div>
{inReplyToAccountId && !withinContext && size !== 's' && ( {!!inReplyToId &&
<> !!inReplyToAccountId &&
{inReplyToAccountId === status.account.id ? ( !withinContext &&
<div class="status-thread-badge"> size !== 's' && (
<Icon icon="thread" size="s" /> <>
Thread {inReplyToAccountId === status.account.id ? (
</div> <div class="status-thread-badge">
) : ( <Icon icon="thread" size="s" />
!!inReplyToAccount && Thread
!mentions.find((mention) => {
return mention.id === inReplyToAccountId;
}) && (
<div class="status-reply-badge">
<Icon icon="reply" />{' '}
<NameText account={inReplyToAccount} short />
</div> </div>
) ) : (
)} !!inReplyToAccount &&
</> (!!spoilerText ||
)} !mentions.find((mention) => {
return mention.id === inReplyToAccountId;
})) && (
<div class="status-reply-badge">
<Icon icon="reply" />{' '}
<NameText account={inReplyToAccount} short />
</div>
)
)}
</>
)}
<div <div
class={`content-container ${ class={`content-container ${
sensitive || spoilerText ? 'has-spoiler' : '' sensitive || spoilerText ? 'has-spoiler' : ''
@ -430,6 +422,7 @@ function Status({
<Media <Media
key={media.id} key={media.id}
media={media} media={media}
autoAnimate={size === 'l'}
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
@ -440,8 +433,10 @@ function Status({
</div> </div>
)} )}
{!!card && {!!card &&
(size === 'l' || !sensitive &&
(size === 'm' && !poll && !mediaAttachments.length)) && ( !spoilerText &&
!poll &&
!mediaAttachments.length && (
<Card <Card
card={card} card={card}
size={!poll && !mediaAttachments.length ? 'l' : 'm'} size={!poll && !mediaAttachments.length ? 'l' : 'm'}
@ -658,7 +653,6 @@ function Status({
index={showMediaModal} index={showMediaModal}
onClose={() => { onClose={() => {
setShowMediaModal(false); setShowMediaModal(false);
statusRef.current?.focus();
}} }}
/> />
</Modal> </Modal>
@ -695,7 +689,7 @@ video = Video clip
audio = Audio track audio = Audio track
*/ */
function Media({ media, showOriginal, onClick = () => {} }) { function Media({ media, showOriginal, autoAnimate, onClick = () => {} }) {
const { blurhash, description, meta, previewUrl, remoteUrl, url, type } = const { blurhash, description, meta, previewUrl, remoteUrl, url, type } =
media; media;
const { original, small, focus } = meta || {}; const { original, small, focus } = meta || {};
@ -748,7 +742,7 @@ function Media({ media, showOriginal, onClick = () => {} }) {
alt={description} alt={description}
width={width} width={width}
height={height} height={height}
loading="lazy" loading={showOriginal ? 'eager' : 'lazy'}
style={ style={
!showOriginal && { !showOriginal && {
backgroundColor: backgroundColor:
@ -762,17 +756,24 @@ function Media({ media, showOriginal, onClick = () => {} }) {
} else if (type === 'gifv' || type === 'video') { } else if (type === 'gifv' || type === 'video') {
// 20 seconds, treat as a gif // 20 seconds, treat as a gif
const shortDuration = original.duration <= 20; const shortDuration = original.duration <= 20;
const isGIF = type === 'gifv' || shortDuration; const isGIFV = type === 'gifv';
const isGIF = isGIFV || shortDuration;
const loopable = original.duration <= 60; const loopable = original.duration <= 60;
const formattedDuration = formatDuration(original.duration);
const hoverAnimate = !showOriginal && !autoAnimate && isGIF;
return ( return (
<div <div
class={`media media-${isGIF ? 'gif' : 'video'}`} class={`media media-${isGIF ? 'gif' : 'video'} ${
autoAnimate ? 'media-contain' : ''
}`}
data-formatted-duration={formattedDuration}
data-label={isGIF && !showOriginal && !autoAnimate ? 'GIF' : ''}
style={{ style={{
backgroundColor: backgroundColor:
rgbAverageColor && `rgb(${rgbAverageColor.join(',')})`, rgbAverageColor && `rgb(${rgbAverageColor.join(',')})`,
}} }}
onClick={(e) => { onClick={(e) => {
if (!showOriginal && isGIF) { if (hoverAnimate) {
try { try {
videoRef.current.pause(); videoRef.current.pause();
} catch (e) {} } catch (e) {}
@ -780,37 +781,41 @@ function Media({ media, showOriginal, onClick = () => {} }) {
onClick(e); onClick(e);
}} }}
onMouseEnter={() => { onMouseEnter={() => {
if (!showOriginal && isGIF) { if (hoverAnimate) {
try { try {
videoRef.current.play(); videoRef.current.play();
} catch (e) {} } catch (e) {}
} }
}} }}
onMouseLeave={() => { onMouseLeave={() => {
if (!showOriginal && isGIF) { if (hoverAnimate) {
try { try {
videoRef.current.pause(); videoRef.current.pause();
} catch (e) {} } catch (e) {}
} }
}} }}
> >
{showOriginal ? ( {showOriginal || autoAnimate ? (
<div <div
style={{
width: '100%',
height: '100%',
}}
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: ` __html: `
<video <video
src="${url}" src="${url}"
poster="${previewUrl}" poster="${previewUrl}"
width="${width}" width="${width}"
height="${height}" height="${height}"
preload="auto" preload="auto"
autoplay autoplay
muted="${isGIF}" muted="${isGIF}"
${isGIF ? '' : 'controls'} ${isGIFV ? '' : 'controls'}
playsinline playsinline
loop="${loopable}" loop="${loopable}"
></video> ></video>
`, `,
}} }}
/> />
) : isGIF ? ( ) : isGIF ? (
@ -1130,9 +1135,7 @@ function Poll({ poll, lang, readOnly, onUpdate = () => {} }) {
</> </>
)}{' '} )}{' '}
&bull; {expired ? 'Ended' : 'Ending'}{' '} &bull; {expired ? 'Ended' : 'Ending'}{' '}
{!!expiresAtDate && ( {!!expiresAtDate && <RelativeTime datetime={expiresAtDate} />}
<relative-time datetime={expiresAtDate.toISOString()} />
)}
</p> </p>
)} )}
</div> </div>
@ -1423,4 +1426,19 @@ function Carousel({ mediaAttachments, index = 0, onClose = () => {} }) {
); );
} }
function formatDuration(time) {
if (!time) return;
let hours = Math.floor(time / 3600);
let minutes = Math.floor((time % 3600) / 60);
let seconds = Math.round(time % 60);
if (hours === 0) {
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
} else {
return `${hours}:${minutes.toString().padStart(2, '0')}:${seconds
.toString()
.padStart(2, '0')}`;
}
}
export default Status; export default Status;

View file

@ -2,7 +2,6 @@ import './index.css';
import './app.css'; import './app.css';
import '@github/relative-time-element';
import { login } from 'masto'; import { login } from 'masto';
import { render } from 'preact'; import { render } from 'preact';
import { useEffect, useState } from 'preact/hooks'; import { useEffect, useState } from 'preact/hooks';

View file

@ -1,202 +1,435 @@
[ [
"mastodon.social", "mastodon.social",
"mstdn.social",
"mastodon.world", "mastodon.world",
"mas.to", "mas.to",
"pawoo.net", "pawoo.net",
"mastodon.online", "mastodon.online",
"infosec.exchange",
"mstdn.jp", "mstdn.jp",
"mastodonapp.uk",
"hachyderm.io",
"techhub.social",
"fosstodon.org",
"universeodon.com", "universeodon.com",
"mastodon.lol", "mastodon.lol",
"mastodonapp.uk",
"infosec.exchange",
"mastodon.uno",
"techhub.social",
"mastodon.sdf.org", "mastodon.sdf.org",
"fosstodon.org",
"troet.cafe", "troet.cafe",
"masto.ai", "mastodon.uno",
"mastodon.nl",
"mstdn.party", "mstdn.party",
"c.im", "masto.ai",
"hachyderm.io",
"m.cmx.im",
"mstdn.ca", "mstdn.ca",
"sfba.social", "home.social",
"c.im",
"kolektiva.social", "kolektiva.social",
"mastodon.scot", "m.cmx.im",
"ohai.social", "sfba.social",
"fedibird.com", "fedibird.com",
"piaille.fr", "piaille.fr",
"home.social",
"mindly.social",
"mastodon.nl",
"toot.community",
"aus.social",
"thu.closed.social",
"mastodon.gamedev.place", "mastodon.gamedev.place",
"nerdculture.de", "mastodon.scot",
"mindly.social",
"ohai.social",
"mastodon.cloud", "mastodon.cloud",
"mastodon.ie", "toot.community",
"det.social", "det.social",
"mastodon.au", "aus.social",
"nrw.social", "nrw.social",
"mastodon.art", "mastodon.art",
"chaos.social", "chaos.social",
"social.vivaldi.net",
"mastodon.ie",
"norden.social", "norden.social",
"sueden.social",
"mastodon.top",
"mastodon.au",
"mastodontech.de",
"mas.todon.de",
"ioc.exchange", "ioc.exchange",
"alive.bar", "alive.bar",
"tkz.one",
"sueden.social",
"mastodon.nu",
"mastodon.top",
"mastouille.fr",
"mastodontech.de",
"o3o.ca",
"social.tchncs.de", "social.tchncs.de",
"mastodon.nu",
"social.cologne",
"mastouille.fr",
"o3o.ca",
"mathstodon.xyz",
"noagendasocial.com", "noagendasocial.com",
"newsie.social", "newsie.social",
"masto.es", "sigmoid.social",
"planet.moe",
"social.vivaldi.net",
"ravenation.club",
"wxw.moe",
"mathstodon.xyz",
"social.cologne",
"mastodon.nz",
"qoto.org",
"hessen.social",
"mastodon.com.tr", "mastodon.com.tr",
"ruhr.social", "hessen.social",
"muenchen.social", "muenchen.social",
"mamot.fr",
"twit.social",
"dice.camp",
"meow.social", "meow.social",
"www.masto.pt", "masto.es",
"social.anoxinon.de", "masto.nu",
"www.sociale.network",
"tech.lgbt", "tech.lgbt",
"ruhr.social",
"mastodon.green",
"mstdn.plus",
"wxw.moe",
"qoto.org",
"mamot.fr",
"tkz.one",
"dice.camp",
"social.anoxinon.de",
"mastodon.nz",
"twit.social",
"ravenation.club",
"planet.moe",
"mstdn.science",
"med-mastodon.com",
"econtwitter.net", "econtwitter.net",
"fediscience.org",
"toot.io",
"masthead.social", "masthead.social",
"glasgow.social", "social.dev-wiki.de",
"ieji.de", "mastodont.cat",
"toot.wales", "toot.wales",
"ieji.de",
"ecoevo.social", "ecoevo.social",
"ro-mastodon.puyo.jp", "ro-mastodon.puyo.jp",
"noc.social",
"indieweb.social",
"zirk.us", "zirk.us",
"twingyeo.kr", "noc.social",
"social.linux.pizza", "social.linux.pizza",
"mastodont.cat", "cyberplace.social",
"social.dev-wiki.de", "indieweb.social",
"mastodonczech.cz",
"climatejustice.social",
"eldritch.cafe",
"g0v.social",
"socel.net",
"dju.social",
"mastodontti.fi",
"101010.pl",
"framapiaf.org",
"wien.rocks",
"botsin.space",
"mastodon.bida.im",
"bildung.social",
"pouet.chapril.org",
"urbanists.social",
"wandering.shop",
"masto.pt",
"union.place",
"metalhead.club",
"ruby.social",
"hiveway.net",
"h4.io",
"genomic.social",
"mastodon-belgium.be",
"mastodon.xyz",
"octodon.social",
"pol.social",
"tooot.im",
"berlin.social",
"sciences.social",
"mstdn.guru",
"qdon.space",
"mastodon.radio",
"lile.cl",
"masto.nu",
"witches.live",
"mastodonners.nl", "mastodonners.nl",
"muenster.im", "convo.casa",
"lor.sh", "twingyeo.kr",
"phpc.social", "sself.co",
"pewtix.com", "urbanists.social",
"social.librem.one", "glasgow.social",
"botsin.space",
"eldritch.cafe",
"climatejustice.social",
"theblower.au",
"framapiaf.org",
"artsio.com",
"mastodon.iriseden.eu",
"socel.net",
"g0v.social",
"mastodonczech.cz",
"mastodontti.fi",
"wandering.shop",
"thu.closed.social",
"mastodon.bida.im",
"geekdom.social",
"stranger.social",
"cupoftea.social",
"bildung.social",
"awscommunity.social",
"mas.town",
"ruby.social",
"sciences.social",
"wien.rocks",
"respublicae.eu",
"metalhead.club",
"pouet.chapril.org",
"genomic.social",
"dju.social",
"101010.pl",
"graphics.social",
"defcon.social",
"mastodon.xyz",
"bark.lgbt",
"witches.live",
"climatejustice.rocks",
"rollenspiel.social", "rollenspiel.social",
"peoplemaking.games", "berlin.social",
"masto.pt",
"litmind.club",
"livellosegreto.it",
"mstdn.guru",
"nerdculture.de",
"journa.host",
"octodon.social",
"union.place",
"mastodon-belgium.be",
"mastodon.radio",
"pol.social",
"rheinneckar.social",
"hometech.social",
"androiddev.social",
"social.librem.one",
"kinky.business", "kinky.business",
"mastodon.fun", "phpc.social",
"me.ns.ci", "mast.lat",
"mastodon.eus", "muenster.im",
"mastodon.chasem.dev",
"tooot.im",
"musician.social",
"dresden.network", "dresden.network",
"hostux.social", "swiss.social",
"scholar.social", "h4.io",
"freiburg.social",
"todon.eu",
"writing.exchange",
"toot.aquilenet.fr", "toot.aquilenet.fr",
"digitalcourage.social", "digitalcourage.social",
"rheinneckar.social", "toad.social",
"discuss.systems", "poweredbygay.social",
"defcon.social", "hostux.social",
"snabelen.no",
"mastodon.se", "mastodon.se",
"mastodon.me.uk",
"rubber.social", "rubber.social",
"fulda.social", "pewtix.com",
"vis.social", "mastodon.berlin",
"toot.funami.tech", "lor.sh",
"mast.dragon-fly.club", "mastodon.fun",
"me.ns.ci",
"snabelen.no",
"freiburg.social",
"disabled.social", "disabled.social",
"medibubble.org", "spore.social",
"mastodon.technology", "qdon.space",
"beta.qdon.space",
"scholar.social",
"vmst.io", "vmst.io",
"mstdn.io", "astrodon.social",
"equestria.social", "masto.nobigtech.es",
"vocalodon.net", "hci.social",
"mastodon.ml", "mastodon.eus",
"libretooth.gr", "todon.eu",
"discuss.systems",
"tooting.ch", "tooting.ch",
"dizl.de", "paquita.masto.host",
"best-friends.chat", "fulda.social",
"romancelandia.club", "lile.cl",
"queer.party", "medibubble.org",
"tilde.zone", "writing.exchange",
"xarxa.cloud", "historians.social",
"abdl.link", "vocalodon.net",
"bitcoinhackers.org", "vis.social",
"photog.social",
"macaw.social",
"yiff.life", "yiff.life",
"sociale.network", "fur.lgbt",
"peoplemaking.games",
"hcommons.social",
"mstdn.io",
"libretooth.gr",
"m.sclo.nl",
"pettingzoo.co",
"mastodon.zaclys.com",
"equestria.social",
"best-friends.chat",
"ursal.zone", "ursal.zone",
"eupolicy.social", "bitcoinhackers.org",
"gruene.social", "uiuxdev.social",
"artisan.chat", "queer.party",
"graz.social", "mastodon.ml",
"aethy.com",
"abdl.link",
"mastodon.com.py",
"mapstodon.space",
"typo.social",
"cryptodon.lol",
"tilde.zone",
"computerfairi.es",
"social.coop", "social.coop",
"mstdn.id", "mast.dragon-fly.club",
"social.sciences.re", "dragon-fly.club",
"ludosphere.fr",
"social.politicaconciencia.org",
"oslo.town",
"scicomm.xyz",
"floss.social", "floss.social",
"creators.social", "photog.social",
"tabletop.social",
"bonn.social", "bonn.social",
"openbiblio.social", "sciencemastodon.com",
"mastodon.la", "mastodon.coffee",
"halifaxsocial.ca", "mastorol.es",
"federated.press",
"toot.funami.tech",
"mastodon.gal",
"tabletop.social",
"shakedown.social",
"dizl.de",
"romancelandia.club",
"oslo.town",
"graz.social",
"sociale.network",
"todon.nl",
"nofan.xyz",
"data-folks.masto.host",
"scicomm.xyz",
"layer8.space",
"artisan.chat",
"freeradical.zone", "freeradical.zone",
"toot.cat",
"fandom.ink",
"twiukraine.com",
"eupolicy.social",
"xarxa.cloud",
"bsd.network",
"weirder.earth",
"linuxrocks.online",
"mastodon.cat",
"girlcock.club",
"bolha.us",
"zeroes.ca",
"douchi.space",
"cybre.space",
"mastodon.la",
"sunny.garden",
"bbq.snoot.com",
"liker.social",
"vulpine.club",
"imastodon.net",
"mstdn.maud.io",
"freeatlantis.com",
"is.nota.live",
"mastodon.org.uk",
"mastodon.arch-linux.cz",
"mona.do",
"tyrol.social",
"mstdn.id",
"mastodon.uy",
"mastodon.in.th",
"kurry.social",
"toot.cafe",
"shelter.moe",
"social.politicaconciencia.org",
"h-net.social",
"mstdn.mx",
"kopiti.am",
"mastodon.vlaanderen",
"mao.mastodonhub.com",
"cloud-native.social",
"mograph.social",
"oc.todon.fr",
"ura-mstdn.com",
"uri.life",
"liberdon.com",
"kinkyelephant.com",
"nojack.easydns.ca",
"mastodon.be",
"podcastindex.social",
"blacktwitter.io",
"awoo.space",
"woof.group",
"ani.work",
"colorid.es",
"seo.chat",
"mental.social",
"plural.cafe",
"ika.queloud.net",
"mastodon.com.br",
"mstdn.tokyocameraclub.com",
"donphan.social",
"gensokyo.town",
"ichiji.social",
"sunbeam.city",
"mstdn.kemono-friends.info",
"littlefo.rest",
"kirakiratter.com",
"uwu.social",
"elekk.xyz",
"hispagatos.space",
"hello.2heng.xin",
"the.fores.top",
"mstdn.fr",
"mastodon.mnetwork.co.kr",
"mastodon.gougere.fr",
"dobbs.town",
"gameliberty.club",
"gensokyo.social",
"mathtod.online",
"mastodon.cc",
"iztasocial.site",
"mastodon.pirateparty.be",
"dingdash.com",
"mastodon.partipirate.org",
"oulipo.social",
"anticapitalist.party",
"kemonodon.club",
"toot.turbo.chat",
"photodn.net",
"otogamer.me",
"bear.community",
"tablegame.mstdn.cloud",
"anarchism.space",
"ffxiv-mastodon.com",
"lgbt.io",
"lou.lt",
"social.chinwag.org",
"chinwag.org",
"aleph.land",
"social.slat.org",
"mastodon.juggler.jp",
"eigadon.net",
"vocalounge.cafe",
"acg.mn",
"acg.debula.ml",
"eletusk.club",
"otoya.space",
"social.coletivos.org",
"mastodon.cipherbliss.com",
"truthsocial.co.in",
"mstdn.osaka",
"social.targaryen.house",
"catdon.life",
"stereodon.social",
"social.opendesktop.org",
"nasface.cz",
"toot.site",
"fetswing.org",
"vipgirlfriend.xxx",
"mastodon.elte.hu",
"bgme.me",
"kinbaku.club",
"m.rthome.me",
"animalliberation.social",
"mastodon.librelabucm.org",
"mastodon.gza.jp",
"med-mammoth.com",
"hearthtodon.com",
"counter.social",
"kfem.cat", "kfem.cat",
"federated.press" "pet123.club",
"beta.woof.group",
"explosion.party",
"id.cc",
"freespeechextremist.com",
"cawfee.club",
"1234.as",
"fedi.absturztau.be",
"fsmi.social",
"go5.dev",
"poa.st",
"patriot.online",
"seaofog.com",
"libranet.de",
"tea.codes",
"pixelfed.social",
"shitposter.club",
"squeet.me",
"shared.graphics",
"glindr.org",
"pxlmo.com",
"pixel.tchncs.de",
"love.alicecomplex.com",
"friendica.eskimo.com",
"meatbag.app",
"fediverse.bbad.com",
"pix.toot.wales",
"fgc.network",
"bookrastinating.com",
"pixey.org",
"pixelfed.tokyo",
"chudbuds.lol",
"freeframe.masto.host",
"varishangout.net",
"friendica.vrije-mens.org",
"bae.st",
"brighteon.social",
"pixelfed.uno",
"helladoge.com",
"donotban.com",
"bookwyrm.social",
"spinster.xyz",
"pixelfed.de",
"metapixl.com",
"venera.social",
"blob.cat",
"onevery.ignorelist.com",
"cliq.buzz",
"pxl.roflcopter.fr",
"p.1069-3.com",
"www2.patriot.online",
"gc2.jp",
"soap.shitposter.club",
"www.mastodon.scot"
] ]

View file

@ -3,7 +3,7 @@
--blue-color: royalblue; --blue-color: royalblue;
--purple-color: blueviolet; --purple-color: blueviolet;
--green-color: green; --green-color: darkgreen;
--orange-color: darkorange; --orange-color: darkorange;
--red-color: orangered; --red-color: orangered;
--bg-color: #fff; --bg-color: #fff;
@ -41,7 +41,7 @@
:root { :root {
--blue-color: CornflowerBlue; --blue-color: CornflowerBlue;
--purple-color: mediumpurple; --purple-color: mediumpurple;
--green-color: limegreen; --green-color: lightgreen;
--orange-color: orange; --orange-color: orange;
--bg-color: #242526; --bg-color: #242526;
--bg-faded-color: #18191a; --bg-faded-color: #18191a;
@ -123,6 +123,7 @@ button,
line-height: 1; line-height: 1;
vertical-align: middle; vertical-align: middle;
text-decoration: none; text-decoration: none;
user-select: none;
} }
:is(button, .button) > * { :is(button, .button) > * {
vertical-align: middle; vertical-align: middle;

View file

@ -1,6 +1,5 @@
import './index.css'; import './index.css';
import '@github/relative-time-element';
import { render } from 'preact'; import { render } from 'preact';
import { App } from './app'; import { App } from './app';

View file

@ -8,6 +8,8 @@ import Icon from '../components/icon';
import Loader from '../components/loader'; import Loader from '../components/loader';
import Status from '../components/status'; import Status from '../components/status';
import states from '../utils/states'; import states from '../utils/states';
import useDebouncedCallback from '../utils/useDebouncedCallback';
import useScroll from '../utils/useScroll';
const LIMIT = 20; const LIMIT = 20;
@ -27,6 +29,7 @@ function Home({ hidden }) {
homeIterator.current = masto.v1.timelines.listHome({ homeIterator.current = masto.v1.timelines.listHome({
limit: LIMIT, limit: LIMIT,
}); });
states.homeNew = [];
} }
const allStatuses = await homeIterator.current.next(); const allStatuses = await homeIterator.current.next();
if (allStatuses.value <= 0) { if (allStatuses.value <= 0) {
@ -52,7 +55,10 @@ function Home({ hidden }) {
return allStatuses; return allStatuses;
} }
const loadStatuses = (firstLoad) => { const loadingStatuses = useRef(false);
const loadStatuses = useDebouncedCallback((firstLoad) => {
if (loadingStatuses.current) return;
loadingStatuses.current = true;
setUIState('loading'); setUIState('loading');
(async () => { (async () => {
try { try {
@ -62,9 +68,11 @@ function Home({ hidden }) {
} catch (e) { } catch (e) {
console.warn(e); console.warn(e);
setUIState('error'); setUIState('error');
} finally {
loadingStatuses.current = false;
} }
})(); })();
}; }, 1000);
useEffect(() => { useEffect(() => {
loadStatuses(true); loadStatuses(true);
@ -154,6 +162,25 @@ function Home({ hidden }) {
} }
}); });
const { scrollDirection, reachTop, nearReachTop, nearReachBottom } =
useScroll({
scrollableElement: scrollableRef.current,
distanceFromTop: 0.1,
distanceFromBottom: 0.15,
});
useEffect(() => {
if (nearReachBottom && showMore) {
loadStatuses();
}
}, [nearReachBottom]);
useEffect(() => {
if (reachTop) {
loadStatuses(true);
}
}, [reachTop]);
return ( return (
<div <div
id="home-page" id="home-page"
@ -162,8 +189,27 @@ function Home({ hidden }) {
ref={scrollableRef} ref={scrollableRef}
tabIndex="-1" tabIndex="-1"
> >
<button
hidden={scrollDirection === 'down' && !nearReachTop}
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>
<div class="timeline-deck deck"> <div class="timeline-deck deck">
<header <header
hidden={scrollDirection === 'down' && !nearReachTop}
onClick={() => { onClick={() => {
scrollableRef.current?.scrollTo({ top: 0, behavior: 'smooth' }); scrollableRef.current?.scrollTo({ top: 0, behavior: 'smooth' });
}} }}
@ -200,27 +246,30 @@ function Home({ hidden }) {
</a> </a>
</div> </div>
</header> </header>
{snapStates.homeNew.length > 0 && ( {snapStates.homeNew.length > 0 &&
<button scrollDirection === 'up' &&
class="updates-button" !nearReachTop &&
type="button" !nearReachBottom && (
onClick={() => { <button
const uniqueHomeNew = snapStates.homeNew.filter( class="updates-button"
(status) => !states.home.some((s) => s.id === status.id), type="button"
); onClick={() => {
states.home.unshift(...uniqueHomeNew); const uniqueHomeNew = snapStates.homeNew.filter(
loadStatuses(true); (status) => !states.home.some((s) => s.id === status.id),
states.homeNew = []; );
states.home.unshift(...uniqueHomeNew);
loadStatuses(true);
states.homeNew = [];
scrollableRef.current?.scrollTo({ scrollableRef.current?.scrollTo({
top: 0, top: 0,
behavior: 'smooth', behavior: 'smooth',
}); });
}} }}
> >
<Icon icon="arrow-up" /> New posts <Icon icon="arrow-up" /> New posts
</button> </button>
)} )}
{snapStates.home.length ? ( {snapStates.home.length ? (
<> <>
<ul class="timeline"> <ul class="timeline">
@ -240,7 +289,7 @@ function Home({ hidden }) {
})} })}
{showMore && ( {showMore && (
<> <>
<InView {/* <InView
as="li" as="li"
style={{ style={{
height: '20vh', height: '20vh',
@ -250,9 +299,15 @@ function Home({ hidden }) {
}} }}
root={scrollableRef.current} root={scrollableRef.current}
rootMargin="100px 0px" rootMargin="100px 0px"
> */}
<li
style={{
height: '20vh',
}}
> >
<Status skeleton /> <Status skeleton />
</InView> </li>
{/* </InView> */}
<li <li
style={{ style={{
height: '25vh', height: '25vh',

View file

@ -3,7 +3,7 @@ import './login.css';
import { useEffect, useRef, useState } from 'preact/hooks'; import { useEffect, useRef, useState } from 'preact/hooks';
import Loader from '../components/loader'; import Loader from '../components/loader';
import instancesList from '../data/instances.json'; import instancesListURL from '../data/instances.json?url';
import { getAuthorizationURL, registerApplication } from '../utils/auth'; import { getAuthorizationURL, registerApplication } from '../utils/auth';
import store from '../utils/store'; import store from '../utils/store';
import useTitle from '../utils/useTitle'; import useTitle from '../utils/useTitle';
@ -14,16 +14,30 @@ function Login() {
const cachedInstanceURL = store.local.get('instanceURL'); const cachedInstanceURL = store.local.get('instanceURL');
const [uiState, setUIState] = useState('default'); const [uiState, setUIState] = useState('default');
const [instancesList, setInstancesList] = useState([]);
useEffect(() => {
(async () => {
try {
const res = await fetch(instancesListURL);
const data = await res.json();
setInstancesList(data);
} catch (e) {
// Silently fail
console.error(e);
}
})();
}, []);
useEffect(() => { useEffect(() => {
if (cachedInstanceURL) { if (cachedInstanceURL) {
instanceURLRef.current.value = cachedInstanceURL; instanceURLRef.current.value = cachedInstanceURL.toLowerCase();
} }
}, []); }, []);
const onSubmit = (e) => { const onSubmit = (e) => {
e.preventDefault(); e.preventDefault();
const { elements } = e.target; const { elements } = e.target;
let instanceURL = elements.instanceURL.value; let instanceURL = elements.instanceURL.value.toLowerCase();
// Remove protocol from instance URL // Remove protocol from instance URL
instanceURL = instanceURL.replace(/(^\w+:|^)\/\//, ''); instanceURL = instanceURL.replace(/(^\w+:|^)\/\//, '');
store.local.set('instanceURL', instanceURL); store.local.set('instanceURL', instanceURL);
@ -68,6 +82,10 @@ function Login() {
ref={instanceURLRef} ref={instanceURLRef}
disabled={uiState === 'loading'} disabled={uiState === 'loading'}
list="instances-list" list="instances-list"
autocorrect="off"
autocapitalize="off"
autocomplete="off"
spellcheck="false"
/> />
<datalist id="instances-list"> <datalist id="instances-list">
{instancesList.map((instance) => ( {instancesList.map((instance) => (

View file

@ -20,41 +20,56 @@
opacity: 0.75; opacity: 0.75;
color: var(--text-insignificant-color); color: var(--text-insignificant-color);
} }
.notification-type.notification-mention {
color: var(--reply-to-color);
}
.notification-type.notification-favourite { .notification-type.notification-favourite {
color: var(--favourite-color); color: var(--favourite-color);
} }
.notification-type.notification-reblog { .notification-type.notification-reblog {
color: var(--reblog-color); color: var(--reblog-color);
} }
.notification-type.notification-poll, .notification-type.notification-poll {
.notification-type.notification-mention {
color: var(--link-color); color: var(--link-color);
} }
.notification .status-link { .notification .status-link {
border-radius: 8px 8px 0 0; border-radius: 8px;
border: 1px solid var(--outline-color); border: 1px solid var(--outline-color);
max-height: 160px;
overflow: hidden;
filter: saturate(0.25);
}
.notification .status-link:not(.status-type-mention) > .status {
max-height: 160px; max-height: 160px;
overflow: hidden; overflow: hidden;
/* fade out mask gradient bottom */ /* fade out mask gradient bottom */
mask-image: linear-gradient( mask-image: linear-gradient(
rgba(0, 0, 0, 1), rgba(0, 0, 0, 1) 130px,
rgba(0, 0, 0, 1) 50%, rgba(0, 0, 0, 0.5) 145px,
transparent transparent 159px
); );
filter: saturate(0.25);
} }
.notification .status-link.status-type-mention { .notification .status-link.status-type-mention {
max-height: 320px; max-height: 320px;
filter: none; filter: none;
background-color: var(--bg-color); background-color: var(--bg-color);
margin-top: calc(-16px - 1px); margin-top: calc(-16px - 1px);
border-color: var(--reply-to-color);
box-shadow: 0 0 0 3px var(--reply-to-faded-color);
} }
.notification .status-link:is(:hover, :focus) { .notification .status-link:is(:hover, :focus) {
background-color: var(--bg-blur-color); background-color: var(--bg-blur-color);
filter: saturate(1); filter: saturate(1);
border-color: var(--outline-hover-color); border-color: var(--outline-hover-color);
} }
.notification .status-link.status-type-mention:is(:hover, :focus) {
border-color: var(--reply-to-color);
box-shadow: 0 0 5px var(--reply-to-color);
}
.notification .status-link:active {
filter: brightness(0.95);
}
.notification .status-link > * { .notification .status-link > * {
pointer-events: none; pointer-events: none;
} }

View file

@ -8,6 +8,7 @@ import Avatar from '../components/avatar';
import Icon from '../components/icon'; import Icon from '../components/icon';
import Loader from '../components/loader'; import Loader from '../components/loader';
import NameText from '../components/name-text'; import NameText from '../components/name-text';
import RelativeTime from '../components/relative-time';
import Status from '../components/status'; import Status from '../components/status';
import states from '../utils/states'; import states from '../utils/states';
import store from '../utils/store'; import store from '../utils/store';
@ -102,18 +103,16 @@ function Notification({ notification }) {
<span class="insignificant"> <span class="insignificant">
{' '} {' '}
{' '} {' '}
<relative-time <RelativeTime
datetime={notification.createdAt} datetime={notification.createdAt}
format="micro" format="micro"
threshold="P1D"
prefix=""
/> />
</span> </span>
)} )}
</p> </p>
)} )}
{_accounts?.length > 1 && ( {_accounts?.length > 1 && (
<p> <p class="avatars-stack">
{_accounts.map((account, i) => ( {_accounts.map((account, i) => (
<> <>
<a <a
@ -127,7 +126,7 @@ function Notification({ notification }) {
<Avatar <Avatar
url={account.avatarStatic} url={account.avatarStatic}
size={ size={
_accounts.length < 10 _accounts.length < 30
? 'xl' ? 'xl'
: _accounts.length < 100 : _accounts.length < 100
? 'l' ? 'l'
@ -164,28 +163,23 @@ function NotificationsList({ notifications, emptyCopy }) {
// Create new flat list of notifications // Create new flat list of notifications
// Combine sibling notifications based on type and status id, ignore the id // Combine sibling notifications based on type and status id, ignore the id
// Concat all notification.account into an array of _accounts // Concat all notification.account into an array of _accounts
const cleanNotifications = [ const notificationsMap = {};
{ const cleanNotifications = [];
...notifications[0], for (let i = 0, j = 0; i < notifications.length; i++) {
_accounts: [notifications[0].account],
},
];
for (let i = 1, j = 0; i < notifications.length; i++) {
const notification = notifications[i]; const notification = notifications[i];
const cleanNotification = cleanNotifications[j]; // const cleanNotification = cleanNotifications[j];
const { status, account, type } = notification; const { status, account, type, created_at } = notification;
if ( const createdAt = new Date(created_at).toLocaleDateString();
account && const key = `${status?.id}-${type}-${createdAt}`;
cleanNotification?.account && const mappedNotification = notificationsMap[key];
cleanNotification?.status?.id === status?.id && if (mappedNotification?.account) {
cleanNotification?.type === type mappedNotification._accounts.push(account);
) {
cleanNotification._accounts.push(account);
} else { } else {
cleanNotifications[++j] = { let n = (notificationsMap[key] = {
...notification, ...notification,
_accounts: [account], _accounts: [account],
}; });
cleanNotifications[j++] = n;
} }
} }
// console.log({ notifications, cleanNotifications }); // console.log({ notifications, cleanNotifications });
@ -222,6 +216,7 @@ function Notifications() {
notificationsIterator.current = masto.v1.notifications.list({ notificationsIterator.current = masto.v1.notifications.list({
limit: LIMIT, limit: LIMIT,
}); });
states.notificationsNew = [];
} }
const allNotifications = await notificationsIterator.current.next(); const allNotifications = await notificationsIterator.current.next();
if (allNotifications.value <= 0) { if (allNotifications.value <= 0) {
@ -257,7 +252,6 @@ function Notifications() {
useEffect(() => { useEffect(() => {
loadNotifications(true); loadNotifications(true);
states.notificationsNew = [];
}, []); }, []);
const scrollableRef = useRef(); const scrollableRef = useRef();

View file

@ -5,6 +5,7 @@ import { useRef, useState } from 'preact/hooks';
import Avatar from '../components/avatar'; import Avatar from '../components/avatar';
import Icon from '../components/icon'; import Icon from '../components/icon';
import NameText from '../components/name-text'; import NameText from '../components/name-text';
import RelativeTime from '../components/relative-time';
import states from '../utils/states'; import states from '../utils/states';
import store from '../utils/store'; import store from '../utils/store';
@ -196,8 +197,7 @@ function Settings({ onClose }) {
</p> </p>
{__BUILD_TIME__ && ( {__BUILD_TIME__ && (
<p> <p>
Last build:{' '} Last build: <RelativeTime datetime={new Date(__BUILD_TIME__)} />{' '}
<relative-time datetime={new Date(__BUILD_TIME__).toISOString()} />{' '}
{__COMMIT_HASH__ && ( {__COMMIT_HASH__ && (
<> <>
( (

View file

@ -17,6 +17,7 @@ import { useSnapshot } from 'valtio';
import Icon from '../components/icon'; import Icon from '../components/icon';
import Loader from '../components/loader'; import Loader from '../components/loader';
import NameText from '../components/name-text'; import NameText from '../components/name-text';
import RelativeTime from '../components/relative-time';
import Status from '../components/status'; import Status from '../components/status';
import htmlContentLength from '../utils/html-content-length'; import htmlContentLength from '../utils/html-content-length';
import shortenNumber from '../utils/shorten-number'; import shortenNumber from '../utils/shorten-number';
@ -54,7 +55,7 @@ function StatusPage({ id }) {
}; };
}, [id]); }, [id]);
useEffect(() => { const initContext = () => {
setUIState('loading'); setUIState('loading');
let heroTimer; let heroTimer;
@ -173,7 +174,30 @@ function StatusPage({ id }) {
return () => { return () => {
clearTimeout(heroTimer); clearTimeout(heroTimer);
}; };
}, [id, snapStates.reloadStatusPage]); };
useEffect(initContext, [id]);
useEffect(() => {
// Delete the cache for the context
(async () => {
try {
const accounts = store.local.getJSON('accounts') || [];
const currentAccount = store.session.get('currentAccount');
const account =
accounts.find((a) => a.info.id === currentAccount) || accounts[0];
const instanceURL = account.instanceURL;
const contextURL = `https://${instanceURL}/api/v1/statuses/${id}/context`;
console.log('Clear cache', contextURL);
const apiCache = await caches.open('api');
await apiCache.delete(contextURL, { ignoreVary: true });
return initContext();
} catch (e) {
console.error(e);
}
})();
}, [snapStates.reloadStatusPage]);
const firstLoad = useRef(true); const firstLoad = useRef(true);
@ -280,7 +304,7 @@ function StatusPage({ id }) {
}, [heroInView]); }, [heroInView]);
useHotkeys(['esc', 'backspace'], () => { useHotkeys(['esc', 'backspace'], () => {
route(closeLink); location.hash = closeLink;
}); });
return ( return (
@ -325,11 +349,9 @@ function StatusPage({ id }) {
<NameText showAvatar account={heroStatus.account} short />{' '} <NameText showAvatar account={heroStatus.account} short />{' '}
<span class="insignificant"> <span class="insignificant">
&bull;{' '} &bull;{' '}
<relative-time <RelativeTime
datetime={heroStatus.createdAt} datetime={heroStatus.createdAt}
format="micro" format="micro"
threshold="P1D"
prefix=""
/> />
</span> </span>
</span> </span>

View file

@ -18,4 +18,5 @@ export default proxy({
showCompose: false, showCompose: false,
showSettings: false, showSettings: false,
showAccount: false, showAccount: false,
composeCharacterCount: 0,
}); });

43
src/utils/useScroll.js Normal file
View file

@ -0,0 +1,43 @@
import { useEffect, useState } from 'preact/hooks';
export default function useScroll({
scrollableElement = window,
distanceFromTop = 0,
distanceFromBottom = 0,
scrollThreshold = 10,
} = {}) {
const [scrollDirection, setScrollDirection] = useState(null);
const [reachTop, setReachTop] = useState(false);
const [nearReachTop, setNearReachTop] = useState(false);
const [nearReachBottom, setNearReachBottom] = useState(false);
useEffect(() => {
let previousScrollTop = scrollableElement.scrollTop;
function onScroll() {
const { scrollTop, scrollHeight, clientHeight } = scrollableElement;
const scrollDistance = Math.abs(scrollTop - previousScrollTop);
const distanceFromTopPx =
scrollHeight * Math.min(1, Math.max(0, distanceFromTop));
const distanceFromBottomPx =
scrollHeight * Math.min(1, Math.max(0, distanceFromBottom));
if (scrollDistance >= scrollThreshold) {
setScrollDirection(previousScrollTop < scrollTop ? 'down' : 'up');
previousScrollTop = scrollTop;
}
setReachTop(scrollTop === 0);
setNearReachTop(scrollTop <= distanceFromTopPx);
setNearReachBottom(
scrollTop + clientHeight >= scrollHeight - distanceFromBottomPx,
);
}
scrollableElement.addEventListener('scroll', onScroll, { passive: true });
return () => scrollableElement.removeEventListener('scroll', onScroll);
}, [scrollableElement, distanceFromTop, distanceFromBottom, scrollThreshold]);
return { scrollDirection, reachTop, nearReachTop, nearReachBottom };
}

View file

@ -6,12 +6,11 @@ import { defineConfig, loadEnv, splitVendorChunkPlugin } from 'vite';
import htmlPlugin from 'vite-plugin-html-config'; import htmlPlugin from 'vite-plugin-html-config';
import VitePluginHtmlEnv from 'vite-plugin-html-env'; 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';
const { const { NODE_ENV } = process.env;
VITE_CLIENT_NAME: CLIENT_NAME, const { VITE_CLIENT_NAME: CLIENT_NAME, VITE_APP_ERROR_LOGGING: ERROR_LOGGING } =
NODE_ENV, loadEnv('production', process.cwd());
VITE_APP_ERROR_LOGGING,
} = loadEnv('production', process.cwd());
const commitHash = execSync('git rev-parse --short HEAD').toString().trim(); const commitHash = execSync('git rev-parse --short HEAD').toString().trim();
@ -31,8 +30,9 @@ export default defineConfig({
preact(), preact(),
splitVendorChunkPlugin(), splitVendorChunkPlugin(),
VitePluginHtmlEnv(), VitePluginHtmlEnv(),
removeConsole(),
htmlPlugin({ htmlPlugin({
headScripts: VITE_APP_ERROR_LOGGING ? [rollbarCode] : [], headScripts: ERROR_LOGGING ? [rollbarCode] : [],
}), }),
VitePWA({ VitePWA({
manifest: { manifest: {