commit
a7a3d5605b
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal 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.
|
|
@ -79,10 +79,13 @@ It's been **more than 15 years**.
|
|||
|
||||
And here I am. Building a Mastodon web client.
|
||||
|
||||
## Alternative clients
|
||||
## Alternative web clients
|
||||
|
||||
- [Pinafore](https://pinafore.social/)
|
||||
- [Soapbox](https://fe.soapbox.pub/)
|
||||
- [Elk](https://m.webtoo.ls/@elk)
|
||||
- [Mastodeck](https://mastodeck.com/)
|
||||
-
|
||||
- [More...](https://github.com/tleb/awesome-mastodon#clients)
|
||||
|
||||
## License
|
||||
|
|
228
package-lock.json
generated
228
package-lock.json
generated
|
@ -8,13 +8,14 @@
|
|||
"name": "phanpy",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@github/relative-time-element": "~4.1.5",
|
||||
"@github/text-expander-element": "~2.3.0",
|
||||
"dayjs": "~1.11.7",
|
||||
"dayjs-twitter": "~0.5.0",
|
||||
"fast-blurhash": "~1.1.2",
|
||||
"history": "~5.3.0",
|
||||
"iconify-icon": "~1.0.2",
|
||||
"just-debounce-it": "~3.2.0",
|
||||
"masto": "~5.1.0",
|
||||
"masto": "~5.1.1",
|
||||
"mem": "~9.0.2",
|
||||
"preact": "~10.11.3",
|
||||
"preact-router": "~4.1.0",
|
||||
|
@ -24,7 +25,7 @@
|
|||
"swiped-events": "~1.1.7",
|
||||
"toastify-js": "~1.12.0",
|
||||
"use-resize-observer": "~9.1.0",
|
||||
"valtio": "~1.8.0"
|
||||
"valtio": "~1.8.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@preact/preset-vite": "~2.5.0",
|
||||
|
@ -33,10 +34,11 @@
|
|||
"postcss": "~8.4.20",
|
||||
"postcss-dark-theme-class": "~0.7.3",
|
||||
"twitter-text": "~3.1.0",
|
||||
"vite": "~4.0.3",
|
||||
"vite": "~4.0.4",
|
||||
"vite-plugin-html-config": "~1.0.11",
|
||||
"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-expiration": "~6.5.4",
|
||||
"workbox-routing": "~6.5.4",
|
||||
|
@ -311,7 +313,7 @@
|
|||
"version": "7.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz",
|
||||
"integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.18.6"
|
||||
},
|
||||
|
@ -433,7 +435,7 @@
|
|||
"version": "7.19.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz",
|
||||
"integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
|
@ -442,7 +444,7 @@
|
|||
"version": "7.19.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz",
|
||||
"integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
|
@ -1694,7 +1696,7 @@
|
|||
"version": "7.20.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.5.tgz",
|
||||
"integrity": "sha512-c9fst/h2/dcF7H+MJKZ2T0KjEQ8hY/BNnDk/H3XY8C4Aw/eWQXWn/lWntHF9ooUBnGmEvbfGrTgLWc+um0YDUg==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.19.4",
|
||||
"@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",
|
||||
"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": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@github/text-expander-element/-/text-expander-element-2.3.0.tgz",
|
||||
|
@ -3000,6 +2997,19 @@
|
|||
"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": {
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||
|
@ -3059,6 +3069,11 @@
|
|||
"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": {
|
||||
"version": "3.1.8",
|
||||
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.8.tgz",
|
||||
|
@ -3140,7 +3155,7 @@
|
|||
"version": "0.16.7",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.16.7.tgz",
|
||||
"integrity": "sha512-P6OBFYFSQOGzfApqCeYKqfKRRbCIRsdppTXFo4aAvtiW3o8TTyiIplBvHJI171saPAiy3WlawJHCveJVIOIx1A==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
|
@ -3368,7 +3383,7 @@
|
|||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
|
||||
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
|
||||
"devOptional": true
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/function.prototype.name": {
|
||||
"version": "1.1.5",
|
||||
|
@ -3505,7 +3520,7 @@
|
|||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
|
||||
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.1"
|
||||
},
|
||||
|
@ -3678,7 +3693,7 @@
|
|||
"version": "2.11.0",
|
||||
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz",
|
||||
"integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"has": "^1.0.3"
|
||||
},
|
||||
|
@ -4153,9 +4168,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/masto": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/masto/-/masto-5.1.0.tgz",
|
||||
"integrity": "sha512-/Rvi44BKv9AGGv08Oo63dA2WHE3kwCUtNb1/W0brK9alLaCSboOwTjoWtK46ovjmsm8TugNtKqj2lscxwcFhDQ==",
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/masto/-/masto-5.1.1.tgz",
|
||||
"integrity": "sha512-IvfdpCiayM4tM58aTf/tfkSq0MGW1kKEAwJvgVRbzmwlE4PBt1WnGvZXQg6CiLkcKBMTQaDjLR0sBaGmPrVGCQ==",
|
||||
"dependencies": {
|
||||
"@mastojs/ponyfills": "^1.0.4",
|
||||
"change-case": "^4.1.2",
|
||||
|
@ -4274,7 +4289,7 @@
|
|||
"version": "3.3.4",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz",
|
||||
"integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
|
@ -4437,13 +4452,13 @@
|
|||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
|
||||
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
|
||||
"devOptional": true
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
|
||||
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
|
||||
"devOptional": true
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
|
@ -4461,7 +4476,7 @@
|
|||
"version": "8.4.20",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.20.tgz",
|
||||
"integrity": "sha512-6Q04AXR1212bXr5fh03u8aAwbLxAQNGQ/Q1LNa0VfOI06ZAlhPHtQvE4OIdpj4kLThXilalPnmDSOD65DcHt+g==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
|
@ -4549,9 +4564,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/proxy-compare": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-compare/-/proxy-compare-2.3.0.tgz",
|
||||
"integrity": "sha512-c3L2CcAi7f7pvlD0D7xsF+2CQIW8C3HaYx2Pfgq8eA4HAl3GAH6/dVYsyBbYF/0XJs2ziGLrzmz5fmzPm6A0pQ=="
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-compare/-/proxy-compare-2.4.0.tgz",
|
||||
"integrity": "sha512-FD8KmQUQD6Mfpd0hywCOzcon/dbkFP8XBd9F1ycbKtvVsfv6TsFUKJ2eC0Iz2y+KzlkdT1Z8SY6ZSgm07zOyqg=="
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.1.1",
|
||||
|
@ -4739,7 +4754,7 @@
|
|||
"version": "1.22.1",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz",
|
||||
"integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"is-core-module": "^2.9.0",
|
||||
"path-parse": "^1.0.7",
|
||||
|
@ -4766,7 +4781,7 @@
|
|||
"version": "3.7.4",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.7.4.tgz",
|
||||
"integrity": "sha512-jN9rx3k5pfg9H9al0r0y1EYKSeiRANZRYX32SuNXAnKzh6cVyf4LZVto1KAuDnbHT03E1CpsgqDKaqQ8FZtgxw==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"rollup": "dist/bin/rollup"
|
||||
},
|
||||
|
@ -4908,7 +4923,7 @@
|
|||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
|
||||
"integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
|
@ -5044,7 +5059,7 @@
|
|||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
|
||||
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
|
@ -5106,7 +5121,7 @@
|
|||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
|
||||
"integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
|
@ -5336,50 +5351,30 @@
|
|||
}
|
||||
},
|
||||
"node_modules/valtio": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/valtio/-/valtio-1.8.0.tgz",
|
||||
"integrity": "sha512-lNw7wM0Qb9iBzXMju+XCn+UiIlf5uCe5pcI8XRqcvxEZ/mnRXyKXoOodPDKB8cIAVekA3Q3zWA7rboCdS4ea7g==",
|
||||
"version": "1.8.2",
|
||||
"resolved": "https://registry.npmjs.org/valtio/-/valtio-1.8.2.tgz",
|
||||
"integrity": "sha512-ypFWPi3aY04tojWAFPbTYBDw5iFaCDbKAJ2XqhmY2XOSorNtaCZJNg++FSssv8gMJwmPXfrU/RjncQtsoOHbUg==",
|
||||
"dependencies": {
|
||||
"proxy-compare": "2.3.0",
|
||||
"proxy-compare": "2.4.0",
|
||||
"use-sync-external-store": "1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.7.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/helper-module-imports": ">=7.12",
|
||||
"@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"
|
||||
"react": ">=16.8"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@babel/helper-module-imports": {
|
||||
"optional": true
|
||||
},
|
||||
"@babel/types": {
|
||||
"optional": true
|
||||
},
|
||||
"aslemammad-vite-plugin-macro": {
|
||||
"optional": true
|
||||
},
|
||||
"babel-plugin-macros": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"vite": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-4.0.3.tgz",
|
||||
"integrity": "sha512-HvuNv1RdE7deIfQb8mPk51UKjqptO/4RXZ5yXSAvurd5xOckwS/gg8h9Tky3uSbnjYTgUm0hVCet1cyhKd73ZA==",
|
||||
"devOptional": true,
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-4.0.4.tgz",
|
||||
"integrity": "sha512-xevPU7M8FU0i/80DMR+YhgrzR5KS2ORy1B4xcX/cXLsvnUWvfHuqMmVU6N0YiJ4JWGRJJsLCgjEzKjG9/GKoSw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.16.3",
|
||||
"postcss": "^8.4.20",
|
||||
|
@ -5449,9 +5444,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/vite-plugin-pwa": {
|
||||
"version": "0.14.0",
|
||||
"resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.14.0.tgz",
|
||||
"integrity": "sha512-3wZx47PLWTckOQhc8Y6YZjAbNZ89Ovh4TdCT97MGhgl7aFd2LUekVnAmIgFwgMqyxzJ93nmkPF/ALpEW/i2qCg==",
|
||||
"version": "0.14.1",
|
||||
"resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.14.1.tgz",
|
||||
"integrity": "sha512-5zx7yhQ8RTLwV71+GA9YsQQ63ALKG8XXIMqRJDdZkR8ZYftFcRgnzM7wOWmQZ/DATspyhPih5wCdcZnAIsM+mA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@rollup/plugin-replace": "^5.0.1",
|
||||
|
@ -5471,6 +5466,12 @@
|
|||
"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": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz",
|
||||
|
@ -6068,7 +6069,7 @@
|
|||
"version": "7.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz",
|
||||
"integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/types": "^7.18.6"
|
||||
}
|
||||
|
@ -6160,13 +6161,13 @@
|
|||
"version": "7.19.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz",
|
||||
"integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==",
|
||||
"devOptional": true
|
||||
"dev": true
|
||||
},
|
||||
"@babel/helper-validator-identifier": {
|
||||
"version": "7.19.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz",
|
||||
"integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==",
|
||||
"devOptional": true
|
||||
"dev": true
|
||||
},
|
||||
"@babel/helper-validator-option": {
|
||||
"version": "7.18.6",
|
||||
|
@ -7010,7 +7011,7 @@
|
|||
"version": "7.20.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.5.tgz",
|
||||
"integrity": "sha512-c9fst/h2/dcF7H+MJKZ2T0KjEQ8hY/BNnDk/H3XY8C4Aw/eWQXWn/lWntHF9ooUBnGmEvbfGrTgLWc+um0YDUg==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-string-parser": "^7.19.4",
|
||||
"@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",
|
||||
"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": {
|
||||
"version": "2.3.0",
|
||||
"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==",
|
||||
"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": {
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||
|
@ -7957,6 +7966,11 @@
|
|||
"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": {
|
||||
"version": "3.1.8",
|
||||
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.8.tgz",
|
||||
|
@ -8020,7 +8034,7 @@
|
|||
"version": "0.16.7",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.16.7.tgz",
|
||||
"integrity": "sha512-P6OBFYFSQOGzfApqCeYKqfKRRbCIRsdppTXFo4aAvtiW3o8TTyiIplBvHJI171saPAiy3WlawJHCveJVIOIx1A==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@esbuild/android-arm": "0.16.7",
|
||||
"@esbuild/android-arm64": "0.16.7",
|
||||
|
@ -8202,7 +8216,7 @@
|
|||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
|
||||
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
|
||||
"devOptional": true
|
||||
"dev": true
|
||||
},
|
||||
"function.prototype.name": {
|
||||
"version": "1.1.5",
|
||||
|
@ -8303,7 +8317,7 @@
|
|||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
|
||||
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"function-bind": "^1.1.1"
|
||||
}
|
||||
|
@ -8431,7 +8445,7 @@
|
|||
"version": "2.11.0",
|
||||
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz",
|
||||
"integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"has": "^1.0.3"
|
||||
}
|
||||
|
@ -8777,9 +8791,9 @@
|
|||
}
|
||||
},
|
||||
"masto": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/masto/-/masto-5.1.0.tgz",
|
||||
"integrity": "sha512-/Rvi44BKv9AGGv08Oo63dA2WHE3kwCUtNb1/W0brK9alLaCSboOwTjoWtK46ovjmsm8TugNtKqj2lscxwcFhDQ==",
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/masto/-/masto-5.1.1.tgz",
|
||||
"integrity": "sha512-IvfdpCiayM4tM58aTf/tfkSq0MGW1kKEAwJvgVRbzmwlE4PBt1WnGvZXQg6CiLkcKBMTQaDjLR0sBaGmPrVGCQ==",
|
||||
"requires": {
|
||||
"@mastojs/ponyfills": "^1.0.4",
|
||||
"change-case": "^4.1.2",
|
||||
|
@ -8867,7 +8881,7 @@
|
|||
"version": "3.3.4",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz",
|
||||
"integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==",
|
||||
"devOptional": true
|
||||
"dev": true
|
||||
},
|
||||
"no-case": {
|
||||
"version": "3.0.4",
|
||||
|
@ -8994,13 +9008,13 @@
|
|||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
|
||||
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
|
||||
"devOptional": true
|
||||
"dev": true
|
||||
},
|
||||
"picocolors": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
|
||||
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
|
||||
"devOptional": true
|
||||
"dev": true
|
||||
},
|
||||
"picomatch": {
|
||||
"version": "2.3.1",
|
||||
|
@ -9012,7 +9026,7 @@
|
|||
"version": "8.4.20",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.20.tgz",
|
||||
"integrity": "sha512-6Q04AXR1212bXr5fh03u8aAwbLxAQNGQ/Q1LNa0VfOI06ZAlhPHtQvE4OIdpj4kLThXilalPnmDSOD65DcHt+g==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"nanoid": "^3.3.4",
|
||||
"picocolors": "^1.0.0",
|
||||
|
@ -9057,9 +9071,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"proxy-compare": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-compare/-/proxy-compare-2.3.0.tgz",
|
||||
"integrity": "sha512-c3L2CcAi7f7pvlD0D7xsF+2CQIW8C3HaYx2Pfgq8eA4HAl3GAH6/dVYsyBbYF/0XJs2ziGLrzmz5fmzPm6A0pQ=="
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-compare/-/proxy-compare-2.4.0.tgz",
|
||||
"integrity": "sha512-FD8KmQUQD6Mfpd0hywCOzcon/dbkFP8XBd9F1ycbKtvVsfv6TsFUKJ2eC0Iz2y+KzlkdT1Z8SY6ZSgm07zOyqg=="
|
||||
},
|
||||
"punycode": {
|
||||
"version": "2.1.1",
|
||||
|
@ -9200,7 +9214,7 @@
|
|||
"version": "1.22.1",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz",
|
||||
"integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-core-module": "^2.9.0",
|
||||
"path-parse": "^1.0.7",
|
||||
|
@ -9217,7 +9231,7 @@
|
|||
"version": "3.7.4",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.7.4.tgz",
|
||||
"integrity": "sha512-jN9rx3k5pfg9H9al0r0y1EYKSeiRANZRYX32SuNXAnKzh6cVyf4LZVto1KAuDnbHT03E1CpsgqDKaqQ8FZtgxw==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
|
@ -9312,7 +9326,7 @@
|
|||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
|
||||
"integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
|
||||
"devOptional": true
|
||||
"dev": true
|
||||
},
|
||||
"source-map-support": {
|
||||
"version": "0.5.21",
|
||||
|
@ -9415,7 +9429,7 @@
|
|||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
|
||||
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
|
||||
"devOptional": true
|
||||
"dev": true
|
||||
},
|
||||
"swiped-events": {
|
||||
"version": "1.1.7",
|
||||
|
@ -9456,7 +9470,7 @@
|
|||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
|
||||
"integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==",
|
||||
"devOptional": true
|
||||
"dev": true
|
||||
},
|
||||
"to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
|
@ -9629,19 +9643,19 @@
|
|||
"requires": {}
|
||||
},
|
||||
"valtio": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/valtio/-/valtio-1.8.0.tgz",
|
||||
"integrity": "sha512-lNw7wM0Qb9iBzXMju+XCn+UiIlf5uCe5pcI8XRqcvxEZ/mnRXyKXoOodPDKB8cIAVekA3Q3zWA7rboCdS4ea7g==",
|
||||
"version": "1.8.2",
|
||||
"resolved": "https://registry.npmjs.org/valtio/-/valtio-1.8.2.tgz",
|
||||
"integrity": "sha512-ypFWPi3aY04tojWAFPbTYBDw5iFaCDbKAJ2XqhmY2XOSorNtaCZJNg++FSssv8gMJwmPXfrU/RjncQtsoOHbUg==",
|
||||
"requires": {
|
||||
"proxy-compare": "2.3.0",
|
||||
"proxy-compare": "2.4.0",
|
||||
"use-sync-external-store": "1.2.0"
|
||||
}
|
||||
},
|
||||
"vite": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-4.0.3.tgz",
|
||||
"integrity": "sha512-HvuNv1RdE7deIfQb8mPk51UKjqptO/4RXZ5yXSAvurd5xOckwS/gg8h9Tky3uSbnjYTgUm0hVCet1cyhKd73ZA==",
|
||||
"devOptional": true,
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-4.0.4.tgz",
|
||||
"integrity": "sha512-xevPU7M8FU0i/80DMR+YhgrzR5KS2ORy1B4xcX/cXLsvnUWvfHuqMmVU6N0YiJ4JWGRJJsLCgjEzKjG9/GKoSw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"esbuild": "^0.16.3",
|
||||
"fsevents": "~2.3.2",
|
||||
|
@ -9665,9 +9679,9 @@
|
|||
"requires": {}
|
||||
},
|
||||
"vite-plugin-pwa": {
|
||||
"version": "0.14.0",
|
||||
"resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.14.0.tgz",
|
||||
"integrity": "sha512-3wZx47PLWTckOQhc8Y6YZjAbNZ89Ovh4TdCT97MGhgl7aFd2LUekVnAmIgFwgMqyxzJ93nmkPF/ALpEW/i2qCg==",
|
||||
"version": "0.14.1",
|
||||
"resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.14.1.tgz",
|
||||
"integrity": "sha512-5zx7yhQ8RTLwV71+GA9YsQQ63ALKG8XXIMqRJDdZkR8ZYftFcRgnzM7wOWmQZ/DATspyhPih5wCdcZnAIsM+mA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@rollup/plugin-replace": "^5.0.1",
|
||||
|
@ -9679,6 +9693,12 @@
|
|||
"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": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz",
|
||||
|
|
14
package.json
14
package.json
|
@ -7,16 +7,17 @@
|
|||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"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": {
|
||||
"@github/relative-time-element": "~4.1.5",
|
||||
"@github/text-expander-element": "~2.3.0",
|
||||
"dayjs": "~1.11.7",
|
||||
"dayjs-twitter": "~0.5.0",
|
||||
"fast-blurhash": "~1.1.2",
|
||||
"history": "~5.3.0",
|
||||
"iconify-icon": "~1.0.2",
|
||||
"just-debounce-it": "~3.2.0",
|
||||
"masto": "~5.1.0",
|
||||
"masto": "~5.1.1",
|
||||
"mem": "~9.0.2",
|
||||
"preact": "~10.11.3",
|
||||
"preact-router": "~4.1.0",
|
||||
|
@ -26,7 +27,7 @@
|
|||
"swiped-events": "~1.1.7",
|
||||
"toastify-js": "~1.12.0",
|
||||
"use-resize-observer": "~9.1.0",
|
||||
"valtio": "~1.8.0"
|
||||
"valtio": "~1.8.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@preact/preset-vite": "~2.5.0",
|
||||
|
@ -35,10 +36,11 @@
|
|||
"postcss": "~8.4.20",
|
||||
"postcss-dark-theme-class": "~0.7.3",
|
||||
"twitter-text": "~3.1.0",
|
||||
"vite": "~4.0.3",
|
||||
"vite": "~4.0.4",
|
||||
"vite-plugin-html-config": "~1.0.11",
|
||||
"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-expiration": "~6.5.4",
|
||||
"workbox-routing": "~6.5.4",
|
||||
|
|
36
public/sw.js
36
public/sw.js
|
@ -3,6 +3,8 @@ import { ExpirationPlugin } from 'workbox-expiration';
|
|||
import { RegExpRoute, registerRoute, Route } from 'workbox-routing';
|
||||
import { CacheFirst, StaleWhileRevalidate } from 'workbox-strategies';
|
||||
|
||||
self.__WB_DISABLE_DEV_LOGS = true;
|
||||
|
||||
const imageRoute = new Route(
|
||||
({ request, sameOrigin }) => {
|
||||
const isRemote = !sameOrigin;
|
||||
|
@ -44,20 +46,20 @@ const apiExtendedRoute = new RegExpRoute(
|
|||
);
|
||||
registerRoute(apiExtendedRoute);
|
||||
|
||||
// Not caching API requests, doesn't seem to be necessary fo now
|
||||
//
|
||||
// const apiRoute = new RegExpRoute(
|
||||
// /^https?:\/\/[^\/]+\/api\//,
|
||||
// new StaleWhileRevalidate({
|
||||
// cacheName: 'api',
|
||||
// plugins: [
|
||||
// new ExpirationPlugin({
|
||||
// maxAgeSeconds: 60, // 1 minute
|
||||
// }),
|
||||
// new CacheableResponsePlugin({
|
||||
// statuses: [0, 200],
|
||||
// }),
|
||||
// ],
|
||||
// }),
|
||||
// );
|
||||
// registerRoute(apiRoute);
|
||||
const apiRoute = new RegExpRoute(
|
||||
// Matches:
|
||||
// - statuses/:id/context - some contexts are really huge
|
||||
/^https?:\/\/[^\/]+\/api\/v\d+\/(statuses\/\d+\/context)/,
|
||||
new StaleWhileRevalidate({
|
||||
cacheName: 'api',
|
||||
plugins: [
|
||||
new ExpirationPlugin({
|
||||
maxAgeSeconds: 5 * 60, // 5 minutes
|
||||
}),
|
||||
new CacheableResponsePlugin({
|
||||
statuses: [0, 200],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
);
|
||||
registerRoute(apiRoute);
|
||||
|
|
|
@ -3,7 +3,8 @@ import fs from 'fs';
|
|||
const { INSTANCES_SOCIAL_SECRET_TOKEN } = process.env;
|
||||
|
||||
const params = new URLSearchParams({
|
||||
count: 200,
|
||||
count: 0,
|
||||
min_users: 1_000,
|
||||
sort_by: 'active_users',
|
||||
sort_order: 'desc',
|
||||
});
|
||||
|
|
48
src/app.css
48
src/app.css
|
@ -90,6 +90,13 @@ a.mention span {
|
|||
grid-template-columns: 1fr 1fr 1fr;
|
||||
align-items: center;
|
||||
user-select: none;
|
||||
transition: transform 0.5s ease-in-out;
|
||||
user-select: none;
|
||||
}
|
||||
.deck header[hidden] {
|
||||
transform: translateY(-100%);
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
.deck header > .header-side:last-of-type {
|
||||
text-align: right;
|
||||
|
@ -348,8 +355,10 @@ a.mention span {
|
|||
display: block;
|
||||
text-decoration-line: none;
|
||||
color: inherit;
|
||||
user-select: none;
|
||||
transition: background-color 0.2s ease-out;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
animation: appear 0.2s ease-out;
|
||||
}
|
||||
.status-link:is(:hover, :focus) {
|
||||
background-color: var(--link-bg-hover-color);
|
||||
|
@ -357,7 +366,6 @@ a.mention span {
|
|||
}
|
||||
.status-link:active {
|
||||
filter: brightness(0.95);
|
||||
transform: translateY(0.5px);
|
||||
}
|
||||
|
||||
.ui-state {
|
||||
|
@ -374,18 +382,17 @@ a.mention span {
|
|||
z-index: 1000;
|
||||
display: flex;
|
||||
background-color: var(--backdrop-color);
|
||||
animation: appear 0.2s ease-out;
|
||||
}
|
||||
.deck-backdrop > a {
|
||||
flex-grow: 1;
|
||||
backdrop-filter: saturate(0.75);
|
||||
/* backdrop-filter: saturate(0.75); */
|
||||
}
|
||||
@keyframes slide-in {
|
||||
0% {
|
||||
opacity: 0.5;
|
||||
transform: translate3d(100%, 0, 0);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
@ -402,7 +409,6 @@ a.mention span {
|
|||
|
||||
.decks {
|
||||
flex-grow: 1;
|
||||
transition: transform 0.5s var(--timing-function);
|
||||
}
|
||||
|
||||
.deck-close {
|
||||
|
@ -436,7 +442,7 @@ a.mention span {
|
|||
}
|
||||
.updates-button {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
z-index: 2;
|
||||
animation: fade-from-top 2s ease-out;
|
||||
left: 50%;
|
||||
margin-top: 8px;
|
||||
|
@ -602,7 +608,18 @@ button.carousel-dot[disabled].active {
|
|||
z-index: 1;
|
||||
box-shadow: 0 3px 8px -1px var(--bg-faded-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) {
|
||||
background-color: var(--button-bg-color);
|
||||
|
@ -610,7 +627,6 @@ button.carousel-dot[disabled].active {
|
|||
}
|
||||
#compose-button:active {
|
||||
filter: brightness(0.75);
|
||||
transform: translateY(1px);
|
||||
}
|
||||
#compose-button .icon {
|
||||
filter: drop-shadow(0 1px 2px var(--button-bg-color));
|
||||
|
@ -637,6 +653,10 @@ button.carousel-dot[disabled].active {
|
|||
padding: 16px 16px 8px;
|
||||
padding-left: max(16px, env(safe-area-inset-left));
|
||||
padding-right: max(16px, env(safe-area-inset-right));
|
||||
user-select: none;
|
||||
}
|
||||
.sheet header :is(h1, h2, h3) {
|
||||
margin: 0;
|
||||
}
|
||||
.sheet main {
|
||||
overflow: auto;
|
||||
|
@ -800,6 +820,14 @@ meter.donut:is(.danger, .explode):after {
|
|||
filter: brightness(0.8);
|
||||
}
|
||||
|
||||
/* AVATARS STACK */
|
||||
|
||||
.avatars-stack {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
@media (min-width: 40em) {
|
||||
html,
|
||||
body {
|
||||
|
@ -808,7 +836,11 @@ meter.donut:is(.danger, .explode):after {
|
|||
#app {
|
||||
display: flex;
|
||||
}
|
||||
.decks {
|
||||
transition: transform 0.4s var(--timing-function);
|
||||
}
|
||||
.decks:has(~ .deck-backdrop) {
|
||||
transition: transform 0.4s ease-out;
|
||||
transform: translate3d(-5vw, 0, 0);
|
||||
}
|
||||
.deck-backdrop .deck {
|
||||
|
|
38
src/app.jsx
38
src/app.jsx
|
@ -82,7 +82,7 @@ function App() {
|
|||
let account = accounts.find((a) => a.info.id === mastoAccount.id);
|
||||
if (account) {
|
||||
account.info = mastoAccount;
|
||||
account.instanceURL = instanceURL;
|
||||
account.instanceURL = instanceURL.toLowerCase();
|
||||
account.accessToken = accessToken;
|
||||
} else {
|
||||
account = {
|
||||
|
@ -166,7 +166,7 @@ function App() {
|
|||
console.log(info);
|
||||
const { uri, domain } = info;
|
||||
const instances = store.local.getJSON('instances') || {};
|
||||
instances[domain || uri] = info;
|
||||
instances[(domain || uri).toLowerCase()] = info;
|
||||
store.local.setJSON('instances', instances);
|
||||
})();
|
||||
});
|
||||
|
@ -177,31 +177,12 @@ function App() {
|
|||
return (
|
||||
<>
|
||||
{isLoggedIn && currentDeck && (
|
||||
<>
|
||||
<button
|
||||
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="decks">
|
||||
{/* Home will never be unmounted */}
|
||||
<Home hidden={currentDeck !== 'home'} />
|
||||
{/* Notifications can be unmounted */}
|
||||
{currentDeck === 'notifications' && <Notifications />}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{!isLoggedIn && uiState === 'loading' && <Loader />}
|
||||
<Router
|
||||
|
@ -237,13 +218,22 @@ function App() {
|
|||
replyToStatus={
|
||||
typeof snapStates.showCompose !== 'boolean'
|
||||
? 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) => {
|
||||
const { newStatus } = results || {};
|
||||
states.showCompose = false;
|
||||
window.__COMPOSE__ = null;
|
||||
if (newStatus) {
|
||||
states.reloadStatusPage++;
|
||||
setTimeout(() => {
|
||||
|
|
|
@ -197,17 +197,19 @@ function Account({ account }) {
|
|||
{relationshipUIState !== 'loading' && relationship && (
|
||||
<button
|
||||
type="button"
|
||||
class={`${following ? 'light' : ''} swap`}
|
||||
data-swap-state={following ? 'danger' : ''}
|
||||
class={`${following || requested ? 'light swap' : ''}`}
|
||||
data-swap-state={following || requested ? 'danger' : ''}
|
||||
disabled={relationshipUIState === 'loading'}
|
||||
onClick={() => {
|
||||
setRelationshipUIState('loading');
|
||||
(async () => {
|
||||
try {
|
||||
let newRelationship;
|
||||
if (following) {
|
||||
if (following || requested) {
|
||||
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) {
|
||||
newRelationship = await masto.v1.accounts.unfollow(
|
||||
|
@ -231,10 +233,18 @@ function Account({ account }) {
|
|||
<span>Following</span>
|
||||
<span>Unfollow…</span>
|
||||
</>
|
||||
) : requested ? (
|
||||
<>
|
||||
<span>Requested</span>
|
||||
<span>Withdraw…</span>
|
||||
</>
|
||||
) : locked ? (
|
||||
<>
|
||||
<Icon icon="lock" /> <span>Follow</span>
|
||||
</>
|
||||
) : (
|
||||
'Follow'
|
||||
)}
|
||||
{/* {following ? 'Unfollow…' : 'Follow'} */}
|
||||
</button>
|
||||
)}
|
||||
</p>
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
max-width: 100%;
|
||||
height: 4em;
|
||||
min-height: 4em;
|
||||
max-height: 10em;
|
||||
max-height: 50vh;
|
||||
resize: vertical;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
@ -255,16 +255,34 @@
|
|||
align-items: stretch;
|
||||
}
|
||||
#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 > * {
|
||||
min-width: 80px;
|
||||
width: 80px !important;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
object-fit: contain;
|
||||
background-color: var(--img-bg-color);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--outline-color);
|
||||
vertical-align: middle;
|
||||
pointer-events: none;
|
||||
}
|
||||
#compose-container .media-preview:hover {
|
||||
box-shadow: 0 0 0 2px var(--link-light-color);
|
||||
cursor: pointer;
|
||||
}
|
||||
#compose-container .media-attachment textarea {
|
||||
height: 80px;
|
||||
|
@ -389,3 +407,39 @@
|
|||
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;
|
||||
}
|
||||
|
|
|
@ -1,19 +1,25 @@
|
|||
import './compose.css';
|
||||
|
||||
import '@github/text-expander-element';
|
||||
import { forwardRef } from 'preact/compat';
|
||||
import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import stringLength from 'string-length';
|
||||
import { useSnapshot } from 'valtio';
|
||||
|
||||
import supportedLanguages from '../data/status-supported-languages';
|
||||
import urlRegex from '../data/url-regex';
|
||||
import emojifyText from '../utils/emojify-text';
|
||||
import openCompose from '../utils/open-compose';
|
||||
import states from '../utils/states';
|
||||
import store from '../utils/store';
|
||||
import useDebouncedCallback from '../utils/useDebouncedCallback';
|
||||
import visibilityIconsMap from '../utils/visibility-icons-map';
|
||||
|
||||
import Avatar from './avatar';
|
||||
import Icon from './icon';
|
||||
import Loader from './loader';
|
||||
import Modal from './modal';
|
||||
import Status from './status';
|
||||
|
||||
const supportedLanguagesMap = supportedLanguages.reduce((acc, l) => {
|
||||
|
@ -53,6 +59,16 @@ menu.className = 'text-expander-menu';
|
|||
|
||||
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({
|
||||
onClose,
|
||||
replyToStatus,
|
||||
|
@ -61,6 +77,7 @@ function Compose({
|
|||
standalone,
|
||||
hasOpener,
|
||||
}) {
|
||||
console.warn('RENDER COMPOSER');
|
||||
const [uiState, setUIState] = useState('default');
|
||||
|
||||
const accounts = store.local.getJSON('accounts');
|
||||
|
@ -72,9 +89,9 @@ function Compose({
|
|||
const configuration = useMemo(() => {
|
||||
try {
|
||||
const instances = store.local.getJSON('instances');
|
||||
const currentInstance = accounts.find(
|
||||
(a) => a.info.id === currentAccount,
|
||||
).instanceURL;
|
||||
const currentInstance = accounts
|
||||
.find((a) => a.info.id === currentAccount)
|
||||
.instanceURL.toLowerCase();
|
||||
const config = instances[currentInstance].configuration;
|
||||
console.log(config);
|
||||
return config;
|
||||
|
@ -222,130 +239,6 @@ function Compose({
|
|||
}
|
||||
}, [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 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 { value } = textareaRef.current;
|
||||
const { value: spoilerText } = spoilerTextRef.current;
|
||||
return stringLength(countableText(value)) + stringLength(spoilerText);
|
||||
};
|
||||
const updateCharCount = () => {
|
||||
setCharCount(getCharCount());
|
||||
const count = getCharCount();
|
||||
states.composerCharacterCount = count;
|
||||
};
|
||||
useEffect(updateCharCount, []);
|
||||
|
||||
useHotkeys(
|
||||
'esc',
|
||||
() => {
|
||||
if (!standalone && confirmClose()) {
|
||||
onClose();
|
||||
}
|
||||
},
|
||||
{
|
||||
enableOnFormTags: true,
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
<div id="compose-container" class={standalone ? 'standalone' : ''}>
|
||||
|
@ -463,21 +365,21 @@ function Compose({
|
|||
disabled={uiState === 'loading'}
|
||||
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
|
||||
const containNonIDMediaAttachments =
|
||||
mediaAttachments.length > 0 &&
|
||||
mediaAttachments.some((media) => !media.id);
|
||||
if (containNonIDMediaAttachments) {
|
||||
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?',
|
||||
);
|
||||
if (!yes) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// const containNonIDMediaAttachments =
|
||||
// mediaAttachments.length > 0 &&
|
||||
// mediaAttachments.some((media) => !media.id);
|
||||
// if (containNonIDMediaAttachments) {
|
||||
// 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?',
|
||||
// );
|
||||
// if (!yes) {
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
|
||||
const mediaAttachmentsWithIDs = mediaAttachments.filter(
|
||||
(media) => media.id,
|
||||
);
|
||||
// const mediaAttachmentsWithIDs = mediaAttachments.filter(
|
||||
// (media) => media.id,
|
||||
// );
|
||||
|
||||
const newWin = openCompose({
|
||||
editStatus,
|
||||
|
@ -489,7 +391,7 @@ function Compose({
|
|||
language,
|
||||
sensitive,
|
||||
poll,
|
||||
mediaAttachments: mediaAttachmentsWithIDs,
|
||||
mediaAttachments,
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -524,17 +426,17 @@ function Compose({
|
|||
disabled={uiState === 'loading'}
|
||||
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
|
||||
const containNonIDMediaAttachments =
|
||||
mediaAttachments.length > 0 &&
|
||||
mediaAttachments.some((media) => !media.id);
|
||||
if (containNonIDMediaAttachments) {
|
||||
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?',
|
||||
);
|
||||
if (!yes) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// const containNonIDMediaAttachments =
|
||||
// mediaAttachments.length > 0 &&
|
||||
// mediaAttachments.some((media) => !media.id);
|
||||
// if (containNonIDMediaAttachments) {
|
||||
// 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?',
|
||||
// );
|
||||
// if (!yes) {
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
|
||||
if (!window.opener) {
|
||||
alert('Looks like you closed the parent window.');
|
||||
|
@ -548,13 +450,13 @@ function Compose({
|
|||
if (!yes) return;
|
||||
}
|
||||
|
||||
const mediaAttachmentsWithIDs = mediaAttachments.filter(
|
||||
(media) => media.id,
|
||||
);
|
||||
// const mediaAttachmentsWithIDs = mediaAttachments.filter(
|
||||
// (media) => media.id,
|
||||
// );
|
||||
|
||||
onClose({
|
||||
fn: () => {
|
||||
window.opener.__STATES__.showCompose = {
|
||||
const passData = {
|
||||
editStatus,
|
||||
replyToStatus,
|
||||
draftStatus: {
|
||||
|
@ -564,9 +466,11 @@ function Compose({
|
|||
language,
|
||||
sensitive,
|
||||
poll,
|
||||
mediaAttachments: mediaAttachmentsWithIDs,
|
||||
mediaAttachments,
|
||||
},
|
||||
};
|
||||
window.opener.__COMPOSE__ = passData;
|
||||
window.opener.__STATES__.showCompose = true;
|
||||
},
|
||||
});
|
||||
}}
|
||||
|
@ -766,7 +670,7 @@ function Compose({
|
|||
name="sensitive"
|
||||
type="checkbox"
|
||||
checked={sensitive}
|
||||
disabled={uiState === 'loading' || !!editStatus}
|
||||
disabled={uiState === 'loading'}
|
||||
onChange={(e) => {
|
||||
const sensitive = e.target.checked;
|
||||
setSensitive(sensitive);
|
||||
|
@ -803,8 +707,7 @@ function Compose({
|
|||
</select>
|
||||
</label>{' '}
|
||||
</div>
|
||||
<text-expander ref={textExpanderRef} keys="@ # :">
|
||||
<textarea
|
||||
<Textarea
|
||||
ref={textareaRef}
|
||||
placeholder={
|
||||
replyToStatus
|
||||
|
@ -814,30 +717,12 @@ function Compose({
|
|||
: 'What are you doing?'
|
||||
}
|
||||
required={mediaAttachments.length === 0}
|
||||
autoCapitalize="sentences"
|
||||
autoComplete="on"
|
||||
autoCorrect="on"
|
||||
spellCheck="true"
|
||||
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;
|
||||
onInput={() => {
|
||||
updateCharCount();
|
||||
}}
|
||||
style={{
|
||||
maxHeight: `${maxCharacters / 50}em`,
|
||||
'--text-weight': (1 + charCount / 140).toFixed(1) || 1,
|
||||
}}
|
||||
></textarea>
|
||||
</text-expander>
|
||||
maxCharacters={maxCharacters}
|
||||
/>
|
||||
{mediaAttachments.length > 0 && (
|
||||
<div class="media-attachments">
|
||||
{mediaAttachments.map((attachment, i) => {
|
||||
|
@ -942,26 +827,8 @@ function Compose({
|
|||
</button>{' '}
|
||||
<div class="spacer" />
|
||||
{uiState === 'loading' && <Loader abrupt />}{' '}
|
||||
{uiState !== 'loading' && charCount > maxCharacters / 2 && (
|
||||
<>
|
||||
<meter
|
||||
class={`donut ${
|
||||
leftChars <= -10
|
||||
? 'explode'
|
||||
: leftChars <= 0
|
||||
? 'danger'
|
||||
: leftChars <= 20
|
||||
? 'warning'
|
||||
: ''
|
||||
}`}
|
||||
value={charCount}
|
||||
max={maxCharacters}
|
||||
data-left={leftChars}
|
||||
style={{
|
||||
'--percentage': (charCount / maxCharacters) * 100,
|
||||
}}
|
||||
/>{' '}
|
||||
</>
|
||||
{uiState !== 'loading' && (
|
||||
<CharCountMeter maxCharacters={maxCharacters} />
|
||||
)}
|
||||
<label class="toolbar-button">
|
||||
<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({
|
||||
attachment,
|
||||
disabled,
|
||||
onDescriptionChange = () => {},
|
||||
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];
|
||||
return (
|
||||
<div class="media-attachment">
|
||||
<div class="media-preview">
|
||||
{suffixType === 'image' ? (
|
||||
<img src={url} alt="" />
|
||||
) : suffixType === 'video' || suffixType === 'gifv' ? (
|
||||
<video src={url} playsinline muted />
|
||||
) : suffixType === 'audio' ? (
|
||||
<audio src={url} controls />
|
||||
) : null}
|
||||
</div>
|
||||
const debouncedOnDescriptionChange = useDebouncedCallback(
|
||||
onDescriptionChange,
|
||||
500,
|
||||
);
|
||||
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const textareaRef = useRef(null);
|
||||
useEffect(() => {
|
||||
let timer;
|
||||
if (showModal && textareaRef.current) {
|
||||
timer = setTimeout(() => {
|
||||
textareaRef.current.focus();
|
||||
}, 100);
|
||||
}
|
||||
return () => {
|
||||
clearTimeout(timer);
|
||||
};
|
||||
}, [showModal]);
|
||||
|
||||
const descTextarea = (
|
||||
<>
|
||||
{!!id ? (
|
||||
<div class="media-desc">
|
||||
<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>
|
||||
) : (
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
value={description || ''}
|
||||
placeholder={
|
||||
{
|
||||
|
@ -1041,10 +1145,32 @@ function MediaAttachment({
|
|||
// TODO: Un-hard-code this maxlength, ref: https://github.com/mastodon/mastodon/blob/b59fb28e90bc21d6fd1a6bafd13cfbd81ab5be54/app/models/media_attachment.rb#L39
|
||||
onInput={(e) => {
|
||||
const { value } = e.target;
|
||||
onDescriptionChange(value);
|
||||
setDescription(value);
|
||||
debouncedOnDescriptionChange(value);
|
||||
}}
|
||||
></textarea>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div class="media-attachment">
|
||||
<div
|
||||
class="media-preview"
|
||||
onClick={() => {
|
||||
setShowModal(true);
|
||||
}}
|
||||
>
|
||||
{suffixType === 'image' ? (
|
||||
<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"
|
||||
|
@ -1056,6 +1182,42 @@ function MediaAttachment({
|
|||
</button>
|
||||
</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) {
|
||||
for (let key in obj) {
|
||||
if (obj[key] === null || obj[key] === undefined) {
|
||||
|
|
|
@ -16,6 +16,8 @@ function NameText({ account, showAvatar, showAcct, short, external, onClick }) {
|
|||
username.toLowerCase().trim() ===
|
||||
(displayName || '')
|
||||
.replace(/(\:(\w|\+|\-)+\:)(?=|[\!\.\?]|$)/g, '') // Remove shortcodes, regex from https://regex101.com/r/iE9uV0/1
|
||||
.replace(/\s+/g, '') // E.g. "My name" === "myname"
|
||||
.replace(/[^a-z0-9]/gi, '') // Remove non-alphanumeric characters
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
) {
|
||||
|
|
58
src/components/relative-time.jsx
Normal file
58
src/components/relative-time.jsx
Normal 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>
|
||||
);
|
||||
}
|
|
@ -103,6 +103,9 @@
|
|||
justify-content: space-between;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.status.small > .container > .meta {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.status > .container > .meta > * {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
|
@ -179,33 +182,56 @@
|
|||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
.status .content-container.has-spoiler .spoiler ~ * {
|
||||
/* filter: blur(6px) invert(0.5); */
|
||||
filter: url(#spoiler);
|
||||
transform: translate3d(-5px, -5px, 0);
|
||||
.status
|
||||
.content-container.has-spoiler
|
||||
.spoiler
|
||||
~ *: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;
|
||||
user-select: none;
|
||||
contain: layout;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.status .content-container.has-spoiler .spoiler ~ * {
|
||||
.status .content-container.has-spoiler .spoiler ~ .media-container .media > *,
|
||||
.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);
|
||||
}
|
||||
}
|
||||
.status .content-container.has-spoiler .spoiler ~ .content ~ * {
|
||||
opacity: 0.5;
|
||||
}
|
||||
} */
|
||||
.status .content-container.show-spoiler .spoiler {
|
||||
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;
|
||||
transform: none;
|
||||
pointer-events: auto;
|
||||
user-select: auto;
|
||||
text-rendering: auto;
|
||||
image-rendering: auto;
|
||||
}
|
||||
.status .content-container.has-spoiler .spoiler ~ .content ~ * {
|
||||
opacity: 1;
|
||||
.status .content-container.show-spoiler .spoiler ~ .media-container .media > *,
|
||||
.status .content-container.show-spoiler .spoiler ~ .card > img {
|
||||
filter: none;
|
||||
image-rendering: auto;
|
||||
}
|
||||
|
||||
.timeline-deck .status .content {
|
||||
|
@ -353,48 +379,67 @@
|
|||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
.status .media-video {
|
||||
.status .media-video,
|
||||
.status .media-gif {
|
||||
position: relative;
|
||||
background-clip: padding-box;
|
||||
}
|
||||
.status .media-video:before {
|
||||
/* draw a circle in the middle */
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
.status .media-video[data-formatted-duration]:before {
|
||||
pointer-events: none;
|
||||
content: '⏵';
|
||||
width: 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);
|
||||
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 {
|
||||
/* show play icon */
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
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);
|
||||
.status .media-video[data-formatted-duration]:hover:before {
|
||||
color: var(--text-color);
|
||||
}
|
||||
.status .media-video[data-formatted-duration]:after {
|
||||
font-size: 12px;
|
||||
pointer-events: none;
|
||||
opacity: 0.75;
|
||||
z-index: 2;
|
||||
content: attr(data-formatted-duration);
|
||||
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 {
|
||||
opacity: 1;
|
||||
.status .media-gif[data-label]:not(:hover):after {
|
||||
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 {
|
||||
object-fit: cover;
|
||||
pointer-events: none;
|
||||
}
|
||||
.status .media-contain video {
|
||||
object-fit: contain !important;
|
||||
}
|
||||
.status .media-audio {
|
||||
border: 0;
|
||||
min-height: 0;
|
||||
|
@ -434,6 +479,7 @@
|
|||
width: 100%;
|
||||
max-height: 50vh;
|
||||
border-inline-end: 0;
|
||||
border-block-end: 1px solid var(--outline-color);
|
||||
}
|
||||
.card:is(:hover, :focus) .image {
|
||||
animation: position-object 5s ease-in-out 1s 5;
|
||||
|
@ -447,6 +493,9 @@
|
|||
flex-grow: 1;
|
||||
align-self: center;
|
||||
}
|
||||
.card.large .meta-container {
|
||||
align-self: flex-start;
|
||||
}
|
||||
.card .title {
|
||||
line-height: 1.25;
|
||||
font-weight: normal;
|
||||
|
|
|
@ -28,6 +28,7 @@ import visibilityIconsMap from '../utils/visibility-icons-map';
|
|||
|
||||
import Avatar from './avatar';
|
||||
import Icon from './icon';
|
||||
import RelativeTime from './relative-time';
|
||||
|
||||
function fetchAccount(id) {
|
||||
return masto.v1.accounts.fetch(id);
|
||||
|
@ -94,6 +95,7 @@ function Status({
|
|||
filtered,
|
||||
card,
|
||||
createdAt,
|
||||
inReplyToId,
|
||||
inReplyToAccountId,
|
||||
content,
|
||||
mentions,
|
||||
|
@ -252,14 +254,7 @@ function Status({
|
|||
alt={visibility}
|
||||
size="s"
|
||||
/>{' '}
|
||||
<relative-time
|
||||
datetime={createdAtDate.toISOString()}
|
||||
format="micro"
|
||||
threshold="P1D"
|
||||
prefix=""
|
||||
>
|
||||
{createdAtDate.toLocaleString()}
|
||||
</relative-time>
|
||||
<RelativeTime datetime={createdAtDate} format="micro" />
|
||||
</a>
|
||||
) : (
|
||||
<span class="time">
|
||||
|
@ -268,18 +263,14 @@ function Status({
|
|||
alt={visibility}
|
||||
size="s"
|
||||
/>{' '}
|
||||
<relative-time
|
||||
datetime={createdAtDate.toISOString()}
|
||||
format="micro"
|
||||
threshold="P1D"
|
||||
prefix=""
|
||||
>
|
||||
{createdAtDate.toLocaleString()}
|
||||
</relative-time>
|
||||
<RelativeTime datetime={createdAtDate} format="micro" />
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
{inReplyToAccountId && !withinContext && size !== 's' && (
|
||||
{!!inReplyToId &&
|
||||
!!inReplyToAccountId &&
|
||||
!withinContext &&
|
||||
size !== 's' && (
|
||||
<>
|
||||
{inReplyToAccountId === status.account.id ? (
|
||||
<div class="status-thread-badge">
|
||||
|
@ -288,9 +279,10 @@ function Status({
|
|||
</div>
|
||||
) : (
|
||||
!!inReplyToAccount &&
|
||||
(!!spoilerText ||
|
||||
!mentions.find((mention) => {
|
||||
return mention.id === inReplyToAccountId;
|
||||
}) && (
|
||||
})) && (
|
||||
<div class="status-reply-badge">
|
||||
<Icon icon="reply" />{' '}
|
||||
<NameText account={inReplyToAccount} short />
|
||||
|
@ -430,6 +422,7 @@ function Status({
|
|||
<Media
|
||||
key={media.id}
|
||||
media={media}
|
||||
autoAnimate={size === 'l'}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
@ -440,8 +433,10 @@ function Status({
|
|||
</div>
|
||||
)}
|
||||
{!!card &&
|
||||
(size === 'l' ||
|
||||
(size === 'm' && !poll && !mediaAttachments.length)) && (
|
||||
!sensitive &&
|
||||
!spoilerText &&
|
||||
!poll &&
|
||||
!mediaAttachments.length && (
|
||||
<Card
|
||||
card={card}
|
||||
size={!poll && !mediaAttachments.length ? 'l' : 'm'}
|
||||
|
@ -658,7 +653,6 @@ function Status({
|
|||
index={showMediaModal}
|
||||
onClose={() => {
|
||||
setShowMediaModal(false);
|
||||
statusRef.current?.focus();
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
|
@ -695,7 +689,7 @@ video = Video clip
|
|||
audio = Audio track
|
||||
*/
|
||||
|
||||
function Media({ media, showOriginal, onClick = () => {} }) {
|
||||
function Media({ media, showOriginal, autoAnimate, onClick = () => {} }) {
|
||||
const { blurhash, description, meta, previewUrl, remoteUrl, url, type } =
|
||||
media;
|
||||
const { original, small, focus } = meta || {};
|
||||
|
@ -748,7 +742,7 @@ function Media({ media, showOriginal, onClick = () => {} }) {
|
|||
alt={description}
|
||||
width={width}
|
||||
height={height}
|
||||
loading="lazy"
|
||||
loading={showOriginal ? 'eager' : 'lazy'}
|
||||
style={
|
||||
!showOriginal && {
|
||||
backgroundColor:
|
||||
|
@ -762,17 +756,24 @@ function Media({ media, showOriginal, onClick = () => {} }) {
|
|||
} else if (type === 'gifv' || type === 'video') {
|
||||
// 20 seconds, treat as a gif
|
||||
const shortDuration = original.duration <= 20;
|
||||
const isGIF = type === 'gifv' || shortDuration;
|
||||
const isGIFV = type === 'gifv';
|
||||
const isGIF = isGIFV || shortDuration;
|
||||
const loopable = original.duration <= 60;
|
||||
const formattedDuration = formatDuration(original.duration);
|
||||
const hoverAnimate = !showOriginal && !autoAnimate && isGIF;
|
||||
return (
|
||||
<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={{
|
||||
backgroundColor:
|
||||
rgbAverageColor && `rgb(${rgbAverageColor.join(',')})`,
|
||||
}}
|
||||
onClick={(e) => {
|
||||
if (!showOriginal && isGIF) {
|
||||
if (hoverAnimate) {
|
||||
try {
|
||||
videoRef.current.pause();
|
||||
} catch (e) {}
|
||||
|
@ -780,22 +781,26 @@ function Media({ media, showOriginal, onClick = () => {} }) {
|
|||
onClick(e);
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
if (!showOriginal && isGIF) {
|
||||
if (hoverAnimate) {
|
||||
try {
|
||||
videoRef.current.play();
|
||||
} catch (e) {}
|
||||
}
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
if (!showOriginal && isGIF) {
|
||||
if (hoverAnimate) {
|
||||
try {
|
||||
videoRef.current.pause();
|
||||
} catch (e) {}
|
||||
}
|
||||
}}
|
||||
>
|
||||
{showOriginal ? (
|
||||
{showOriginal || autoAnimate ? (
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
<video
|
||||
|
@ -806,7 +811,7 @@ function Media({ media, showOriginal, onClick = () => {} }) {
|
|||
preload="auto"
|
||||
autoplay
|
||||
muted="${isGIF}"
|
||||
${isGIF ? '' : 'controls'}
|
||||
${isGIFV ? '' : 'controls'}
|
||||
playsinline
|
||||
loop="${loopable}"
|
||||
></video>
|
||||
|
@ -1130,9 +1135,7 @@ function Poll({ poll, lang, readOnly, onUpdate = () => {} }) {
|
|||
</>
|
||||
)}{' '}
|
||||
• {expired ? 'Ended' : 'Ending'}{' '}
|
||||
{!!expiresAtDate && (
|
||||
<relative-time datetime={expiresAtDate.toISOString()} />
|
||||
)}
|
||||
{!!expiresAtDate && <RelativeTime datetime={expiresAtDate} />}
|
||||
</p>
|
||||
)}
|
||||
</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;
|
||||
|
|
|
@ -2,7 +2,6 @@ import './index.css';
|
|||
|
||||
import './app.css';
|
||||
|
||||
import '@github/relative-time-element';
|
||||
import { login } from 'masto';
|
||||
import { render } from 'preact';
|
||||
import { useEffect, useState } from 'preact/hooks';
|
||||
|
|
|
@ -1,202 +1,435 @@
|
|||
[
|
||||
"mastodon.social",
|
||||
"mstdn.social",
|
||||
"mastodon.world",
|
||||
"mas.to",
|
||||
"pawoo.net",
|
||||
"mastodon.online",
|
||||
"infosec.exchange",
|
||||
"mstdn.jp",
|
||||
"mastodonapp.uk",
|
||||
"hachyderm.io",
|
||||
"techhub.social",
|
||||
"fosstodon.org",
|
||||
"universeodon.com",
|
||||
"mastodon.lol",
|
||||
"mastodonapp.uk",
|
||||
"infosec.exchange",
|
||||
"mastodon.uno",
|
||||
"techhub.social",
|
||||
"mastodon.sdf.org",
|
||||
"fosstodon.org",
|
||||
"troet.cafe",
|
||||
"masto.ai",
|
||||
"mastodon.uno",
|
||||
"mastodon.nl",
|
||||
"mstdn.party",
|
||||
"c.im",
|
||||
"hachyderm.io",
|
||||
"m.cmx.im",
|
||||
"masto.ai",
|
||||
"mstdn.ca",
|
||||
"sfba.social",
|
||||
"home.social",
|
||||
"c.im",
|
||||
"kolektiva.social",
|
||||
"mastodon.scot",
|
||||
"ohai.social",
|
||||
"m.cmx.im",
|
||||
"sfba.social",
|
||||
"fedibird.com",
|
||||
"piaille.fr",
|
||||
"home.social",
|
||||
"mindly.social",
|
||||
"mastodon.nl",
|
||||
"toot.community",
|
||||
"aus.social",
|
||||
"thu.closed.social",
|
||||
"mastodon.gamedev.place",
|
||||
"nerdculture.de",
|
||||
"mastodon.scot",
|
||||
"mindly.social",
|
||||
"ohai.social",
|
||||
"mastodon.cloud",
|
||||
"mastodon.ie",
|
||||
"toot.community",
|
||||
"det.social",
|
||||
"mastodon.au",
|
||||
"aus.social",
|
||||
"nrw.social",
|
||||
"mastodon.art",
|
||||
"chaos.social",
|
||||
"social.vivaldi.net",
|
||||
"mastodon.ie",
|
||||
"norden.social",
|
||||
"sueden.social",
|
||||
"mastodon.top",
|
||||
"mastodon.au",
|
||||
"mastodontech.de",
|
||||
"mas.todon.de",
|
||||
"ioc.exchange",
|
||||
"alive.bar",
|
||||
"tkz.one",
|
||||
"sueden.social",
|
||||
"mastodon.nu",
|
||||
"mastodon.top",
|
||||
"mastouille.fr",
|
||||
"mastodontech.de",
|
||||
"o3o.ca",
|
||||
"social.tchncs.de",
|
||||
"mastodon.nu",
|
||||
"social.cologne",
|
||||
"mastouille.fr",
|
||||
"o3o.ca",
|
||||
"mathstodon.xyz",
|
||||
"noagendasocial.com",
|
||||
"newsie.social",
|
||||
"masto.es",
|
||||
"planet.moe",
|
||||
"social.vivaldi.net",
|
||||
"ravenation.club",
|
||||
"wxw.moe",
|
||||
"mathstodon.xyz",
|
||||
"social.cologne",
|
||||
"mastodon.nz",
|
||||
"qoto.org",
|
||||
"hessen.social",
|
||||
"sigmoid.social",
|
||||
"mastodon.com.tr",
|
||||
"ruhr.social",
|
||||
"hessen.social",
|
||||
"muenchen.social",
|
||||
"mamot.fr",
|
||||
"twit.social",
|
||||
"dice.camp",
|
||||
"meow.social",
|
||||
"www.masto.pt",
|
||||
"social.anoxinon.de",
|
||||
"www.sociale.network",
|
||||
"masto.es",
|
||||
"masto.nu",
|
||||
"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",
|
||||
"fediscience.org",
|
||||
"toot.io",
|
||||
"masthead.social",
|
||||
"glasgow.social",
|
||||
"ieji.de",
|
||||
"social.dev-wiki.de",
|
||||
"mastodont.cat",
|
||||
"toot.wales",
|
||||
"ieji.de",
|
||||
"ecoevo.social",
|
||||
"ro-mastodon.puyo.jp",
|
||||
"noc.social",
|
||||
"indieweb.social",
|
||||
"zirk.us",
|
||||
"twingyeo.kr",
|
||||
"noc.social",
|
||||
"social.linux.pizza",
|
||||
"mastodont.cat",
|
||||
"social.dev-wiki.de",
|
||||
"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",
|
||||
"cyberplace.social",
|
||||
"indieweb.social",
|
||||
"mastodonners.nl",
|
||||
"muenster.im",
|
||||
"lor.sh",
|
||||
"phpc.social",
|
||||
"pewtix.com",
|
||||
"social.librem.one",
|
||||
"convo.casa",
|
||||
"twingyeo.kr",
|
||||
"sself.co",
|
||||
"urbanists.social",
|
||||
"glasgow.social",
|
||||
"botsin.space",
|
||||
"eldritch.cafe",
|
||||
"climatejustice.social",
|
||||
"theblower.au",
|
||||
"framapiaf.org",
|
||||
"artsio.com",
|
||||
"mastodon.iriseden.eu",
|
||||
"socel.net",
|
||||
"g0v.social",
|
||||
"mastodonczech.cz",
|
||||
"mastodontti.fi",
|
||||
"wandering.shop",
|
||||
"thu.closed.social",
|
||||
"mastodon.bida.im",
|
||||
"geekdom.social",
|
||||
"stranger.social",
|
||||
"cupoftea.social",
|
||||
"bildung.social",
|
||||
"awscommunity.social",
|
||||
"mas.town",
|
||||
"ruby.social",
|
||||
"sciences.social",
|
||||
"wien.rocks",
|
||||
"respublicae.eu",
|
||||
"metalhead.club",
|
||||
"pouet.chapril.org",
|
||||
"genomic.social",
|
||||
"dju.social",
|
||||
"101010.pl",
|
||||
"graphics.social",
|
||||
"defcon.social",
|
||||
"mastodon.xyz",
|
||||
"bark.lgbt",
|
||||
"witches.live",
|
||||
"climatejustice.rocks",
|
||||
"rollenspiel.social",
|
||||
"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",
|
||||
"mastodon.fun",
|
||||
"me.ns.ci",
|
||||
"mastodon.eus",
|
||||
"phpc.social",
|
||||
"mast.lat",
|
||||
"muenster.im",
|
||||
"mastodon.chasem.dev",
|
||||
"tooot.im",
|
||||
"musician.social",
|
||||
"dresden.network",
|
||||
"hostux.social",
|
||||
"scholar.social",
|
||||
"freiburg.social",
|
||||
"todon.eu",
|
||||
"writing.exchange",
|
||||
"swiss.social",
|
||||
"h4.io",
|
||||
"toot.aquilenet.fr",
|
||||
"digitalcourage.social",
|
||||
"rheinneckar.social",
|
||||
"discuss.systems",
|
||||
"defcon.social",
|
||||
"snabelen.no",
|
||||
"toad.social",
|
||||
"poweredbygay.social",
|
||||
"hostux.social",
|
||||
"mastodon.se",
|
||||
"mastodon.me.uk",
|
||||
"rubber.social",
|
||||
"fulda.social",
|
||||
"vis.social",
|
||||
"toot.funami.tech",
|
||||
"mast.dragon-fly.club",
|
||||
"pewtix.com",
|
||||
"mastodon.berlin",
|
||||
"lor.sh",
|
||||
"mastodon.fun",
|
||||
"me.ns.ci",
|
||||
"snabelen.no",
|
||||
"freiburg.social",
|
||||
"disabled.social",
|
||||
"medibubble.org",
|
||||
"mastodon.technology",
|
||||
"spore.social",
|
||||
"qdon.space",
|
||||
"beta.qdon.space",
|
||||
"scholar.social",
|
||||
"vmst.io",
|
||||
"mstdn.io",
|
||||
"equestria.social",
|
||||
"vocalodon.net",
|
||||
"mastodon.ml",
|
||||
"libretooth.gr",
|
||||
"astrodon.social",
|
||||
"masto.nobigtech.es",
|
||||
"hci.social",
|
||||
"mastodon.eus",
|
||||
"todon.eu",
|
||||
"discuss.systems",
|
||||
"tooting.ch",
|
||||
"dizl.de",
|
||||
"best-friends.chat",
|
||||
"romancelandia.club",
|
||||
"queer.party",
|
||||
"tilde.zone",
|
||||
"xarxa.cloud",
|
||||
"abdl.link",
|
||||
"bitcoinhackers.org",
|
||||
"photog.social",
|
||||
"macaw.social",
|
||||
"paquita.masto.host",
|
||||
"fulda.social",
|
||||
"lile.cl",
|
||||
"medibubble.org",
|
||||
"writing.exchange",
|
||||
"historians.social",
|
||||
"vocalodon.net",
|
||||
"vis.social",
|
||||
"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",
|
||||
"eupolicy.social",
|
||||
"gruene.social",
|
||||
"artisan.chat",
|
||||
"graz.social",
|
||||
"bitcoinhackers.org",
|
||||
"uiuxdev.social",
|
||||
"queer.party",
|
||||
"mastodon.ml",
|
||||
"aethy.com",
|
||||
"abdl.link",
|
||||
"mastodon.com.py",
|
||||
"mapstodon.space",
|
||||
"typo.social",
|
||||
"cryptodon.lol",
|
||||
"tilde.zone",
|
||||
"computerfairi.es",
|
||||
"social.coop",
|
||||
"mstdn.id",
|
||||
"social.sciences.re",
|
||||
"ludosphere.fr",
|
||||
"social.politicaconciencia.org",
|
||||
"oslo.town",
|
||||
"scicomm.xyz",
|
||||
"mast.dragon-fly.club",
|
||||
"dragon-fly.club",
|
||||
"floss.social",
|
||||
"creators.social",
|
||||
"tabletop.social",
|
||||
"photog.social",
|
||||
"bonn.social",
|
||||
"openbiblio.social",
|
||||
"mastodon.la",
|
||||
"halifaxsocial.ca",
|
||||
"sciencemastodon.com",
|
||||
"mastodon.coffee",
|
||||
"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",
|
||||
"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",
|
||||
"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"
|
||||
]
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
--blue-color: royalblue;
|
||||
--purple-color: blueviolet;
|
||||
--green-color: green;
|
||||
--green-color: darkgreen;
|
||||
--orange-color: darkorange;
|
||||
--red-color: orangered;
|
||||
--bg-color: #fff;
|
||||
|
@ -41,7 +41,7 @@
|
|||
:root {
|
||||
--blue-color: CornflowerBlue;
|
||||
--purple-color: mediumpurple;
|
||||
--green-color: limegreen;
|
||||
--green-color: lightgreen;
|
||||
--orange-color: orange;
|
||||
--bg-color: #242526;
|
||||
--bg-faded-color: #18191a;
|
||||
|
@ -123,6 +123,7 @@ button,
|
|||
line-height: 1;
|
||||
vertical-align: middle;
|
||||
text-decoration: none;
|
||||
user-select: none;
|
||||
}
|
||||
:is(button, .button) > * {
|
||||
vertical-align: middle;
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import './index.css';
|
||||
|
||||
import '@github/relative-time-element';
|
||||
import { render } from 'preact';
|
||||
|
||||
import { App } from './app';
|
||||
|
|
|
@ -8,6 +8,8 @@ import Icon from '../components/icon';
|
|||
import Loader from '../components/loader';
|
||||
import Status from '../components/status';
|
||||
import states from '../utils/states';
|
||||
import useDebouncedCallback from '../utils/useDebouncedCallback';
|
||||
import useScroll from '../utils/useScroll';
|
||||
|
||||
const LIMIT = 20;
|
||||
|
||||
|
@ -27,6 +29,7 @@ function Home({ hidden }) {
|
|||
homeIterator.current = masto.v1.timelines.listHome({
|
||||
limit: LIMIT,
|
||||
});
|
||||
states.homeNew = [];
|
||||
}
|
||||
const allStatuses = await homeIterator.current.next();
|
||||
if (allStatuses.value <= 0) {
|
||||
|
@ -52,7 +55,10 @@ function Home({ hidden }) {
|
|||
return allStatuses;
|
||||
}
|
||||
|
||||
const loadStatuses = (firstLoad) => {
|
||||
const loadingStatuses = useRef(false);
|
||||
const loadStatuses = useDebouncedCallback((firstLoad) => {
|
||||
if (loadingStatuses.current) return;
|
||||
loadingStatuses.current = true;
|
||||
setUIState('loading');
|
||||
(async () => {
|
||||
try {
|
||||
|
@ -62,9 +68,11 @@ function Home({ hidden }) {
|
|||
} catch (e) {
|
||||
console.warn(e);
|
||||
setUIState('error');
|
||||
} finally {
|
||||
loadingStatuses.current = false;
|
||||
}
|
||||
})();
|
||||
};
|
||||
}, 1000);
|
||||
|
||||
useEffect(() => {
|
||||
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 (
|
||||
<div
|
||||
id="home-page"
|
||||
|
@ -162,8 +189,27 @@ function Home({ hidden }) {
|
|||
ref={scrollableRef}
|
||||
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">
|
||||
<header
|
||||
hidden={scrollDirection === 'down' && !nearReachTop}
|
||||
onClick={() => {
|
||||
scrollableRef.current?.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}}
|
||||
|
@ -200,7 +246,10 @@ function Home({ hidden }) {
|
|||
</a>
|
||||
</div>
|
||||
</header>
|
||||
{snapStates.homeNew.length > 0 && (
|
||||
{snapStates.homeNew.length > 0 &&
|
||||
scrollDirection === 'up' &&
|
||||
!nearReachTop &&
|
||||
!nearReachBottom && (
|
||||
<button
|
||||
class="updates-button"
|
||||
type="button"
|
||||
|
@ -240,7 +289,7 @@ function Home({ hidden }) {
|
|||
})}
|
||||
{showMore && (
|
||||
<>
|
||||
<InView
|
||||
{/* <InView
|
||||
as="li"
|
||||
style={{
|
||||
height: '20vh',
|
||||
|
@ -250,9 +299,15 @@ function Home({ hidden }) {
|
|||
}}
|
||||
root={scrollableRef.current}
|
||||
rootMargin="100px 0px"
|
||||
> */}
|
||||
<li
|
||||
style={{
|
||||
height: '20vh',
|
||||
}}
|
||||
>
|
||||
<Status skeleton />
|
||||
</InView>
|
||||
</li>
|
||||
{/* </InView> */}
|
||||
<li
|
||||
style={{
|
||||
height: '25vh',
|
||||
|
|
|
@ -3,7 +3,7 @@ import './login.css';
|
|||
import { useEffect, useRef, useState } from 'preact/hooks';
|
||||
|
||||
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 store from '../utils/store';
|
||||
import useTitle from '../utils/useTitle';
|
||||
|
@ -14,16 +14,30 @@ function Login() {
|
|||
const cachedInstanceURL = store.local.get('instanceURL');
|
||||
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(() => {
|
||||
if (cachedInstanceURL) {
|
||||
instanceURLRef.current.value = cachedInstanceURL;
|
||||
instanceURLRef.current.value = cachedInstanceURL.toLowerCase();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const onSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
const { elements } = e.target;
|
||||
let instanceURL = elements.instanceURL.value;
|
||||
let instanceURL = elements.instanceURL.value.toLowerCase();
|
||||
// Remove protocol from instance URL
|
||||
instanceURL = instanceURL.replace(/(^\w+:|^)\/\//, '');
|
||||
store.local.set('instanceURL', instanceURL);
|
||||
|
@ -68,6 +82,10 @@ function Login() {
|
|||
ref={instanceURLRef}
|
||||
disabled={uiState === 'loading'}
|
||||
list="instances-list"
|
||||
autocorrect="off"
|
||||
autocapitalize="off"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
/>
|
||||
<datalist id="instances-list">
|
||||
{instancesList.map((instance) => (
|
||||
|
|
|
@ -20,41 +20,56 @@
|
|||
opacity: 0.75;
|
||||
color: var(--text-insignificant-color);
|
||||
}
|
||||
.notification-type.notification-mention {
|
||||
color: var(--reply-to-color);
|
||||
}
|
||||
.notification-type.notification-favourite {
|
||||
color: var(--favourite-color);
|
||||
}
|
||||
.notification-type.notification-reblog {
|
||||
color: var(--reblog-color);
|
||||
}
|
||||
.notification-type.notification-poll,
|
||||
.notification-type.notification-mention {
|
||||
.notification-type.notification-poll {
|
||||
color: var(--link-color);
|
||||
}
|
||||
|
||||
.notification .status-link {
|
||||
border-radius: 8px 8px 0 0;
|
||||
border-radius: 8px;
|
||||
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;
|
||||
overflow: hidden;
|
||||
/* fade out mask gradient bottom */
|
||||
mask-image: linear-gradient(
|
||||
rgba(0, 0, 0, 1),
|
||||
rgba(0, 0, 0, 1) 50%,
|
||||
transparent
|
||||
rgba(0, 0, 0, 1) 130px,
|
||||
rgba(0, 0, 0, 0.5) 145px,
|
||||
transparent 159px
|
||||
);
|
||||
filter: saturate(0.25);
|
||||
}
|
||||
.notification .status-link.status-type-mention {
|
||||
max-height: 320px;
|
||||
filter: none;
|
||||
background-color: var(--bg-color);
|
||||
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) {
|
||||
background-color: var(--bg-blur-color);
|
||||
filter: saturate(1);
|
||||
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 > * {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import Avatar from '../components/avatar';
|
|||
import Icon from '../components/icon';
|
||||
import Loader from '../components/loader';
|
||||
import NameText from '../components/name-text';
|
||||
import RelativeTime from '../components/relative-time';
|
||||
import Status from '../components/status';
|
||||
import states from '../utils/states';
|
||||
import store from '../utils/store';
|
||||
|
@ -102,18 +103,16 @@ function Notification({ notification }) {
|
|||
<span class="insignificant">
|
||||
{' '}
|
||||
•{' '}
|
||||
<relative-time
|
||||
<RelativeTime
|
||||
datetime={notification.createdAt}
|
||||
format="micro"
|
||||
threshold="P1D"
|
||||
prefix=""
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
{_accounts?.length > 1 && (
|
||||
<p>
|
||||
<p class="avatars-stack">
|
||||
{_accounts.map((account, i) => (
|
||||
<>
|
||||
<a
|
||||
|
@ -127,7 +126,7 @@ function Notification({ notification }) {
|
|||
<Avatar
|
||||
url={account.avatarStatic}
|
||||
size={
|
||||
_accounts.length < 10
|
||||
_accounts.length < 30
|
||||
? 'xl'
|
||||
: _accounts.length < 100
|
||||
? 'l'
|
||||
|
@ -164,28 +163,23 @@ function NotificationsList({ notifications, emptyCopy }) {
|
|||
// Create new flat list of notifications
|
||||
// Combine sibling notifications based on type and status id, ignore the id
|
||||
// Concat all notification.account into an array of _accounts
|
||||
const cleanNotifications = [
|
||||
{
|
||||
...notifications[0],
|
||||
_accounts: [notifications[0].account],
|
||||
},
|
||||
];
|
||||
for (let i = 1, j = 0; i < notifications.length; i++) {
|
||||
const notificationsMap = {};
|
||||
const cleanNotifications = [];
|
||||
for (let i = 0, j = 0; i < notifications.length; i++) {
|
||||
const notification = notifications[i];
|
||||
const cleanNotification = cleanNotifications[j];
|
||||
const { status, account, type } = notification;
|
||||
if (
|
||||
account &&
|
||||
cleanNotification?.account &&
|
||||
cleanNotification?.status?.id === status?.id &&
|
||||
cleanNotification?.type === type
|
||||
) {
|
||||
cleanNotification._accounts.push(account);
|
||||
// const cleanNotification = cleanNotifications[j];
|
||||
const { status, account, type, created_at } = notification;
|
||||
const createdAt = new Date(created_at).toLocaleDateString();
|
||||
const key = `${status?.id}-${type}-${createdAt}`;
|
||||
const mappedNotification = notificationsMap[key];
|
||||
if (mappedNotification?.account) {
|
||||
mappedNotification._accounts.push(account);
|
||||
} else {
|
||||
cleanNotifications[++j] = {
|
||||
let n = (notificationsMap[key] = {
|
||||
...notification,
|
||||
_accounts: [account],
|
||||
};
|
||||
});
|
||||
cleanNotifications[j++] = n;
|
||||
}
|
||||
}
|
||||
// console.log({ notifications, cleanNotifications });
|
||||
|
@ -222,6 +216,7 @@ function Notifications() {
|
|||
notificationsIterator.current = masto.v1.notifications.list({
|
||||
limit: LIMIT,
|
||||
});
|
||||
states.notificationsNew = [];
|
||||
}
|
||||
const allNotifications = await notificationsIterator.current.next();
|
||||
if (allNotifications.value <= 0) {
|
||||
|
@ -257,7 +252,6 @@ function Notifications() {
|
|||
|
||||
useEffect(() => {
|
||||
loadNotifications(true);
|
||||
states.notificationsNew = [];
|
||||
}, []);
|
||||
|
||||
const scrollableRef = useRef();
|
||||
|
|
|
@ -5,6 +5,7 @@ import { useRef, useState } from 'preact/hooks';
|
|||
import Avatar from '../components/avatar';
|
||||
import Icon from '../components/icon';
|
||||
import NameText from '../components/name-text';
|
||||
import RelativeTime from '../components/relative-time';
|
||||
import states from '../utils/states';
|
||||
import store from '../utils/store';
|
||||
|
||||
|
@ -196,8 +197,7 @@ function Settings({ onClose }) {
|
|||
</p>
|
||||
{__BUILD_TIME__ && (
|
||||
<p>
|
||||
Last build:{' '}
|
||||
<relative-time datetime={new Date(__BUILD_TIME__).toISOString()} />{' '}
|
||||
Last build: <RelativeTime datetime={new Date(__BUILD_TIME__)} />{' '}
|
||||
{__COMMIT_HASH__ && (
|
||||
<>
|
||||
(
|
||||
|
|
|
@ -17,6 +17,7 @@ import { useSnapshot } from 'valtio';
|
|||
import Icon from '../components/icon';
|
||||
import Loader from '../components/loader';
|
||||
import NameText from '../components/name-text';
|
||||
import RelativeTime from '../components/relative-time';
|
||||
import Status from '../components/status';
|
||||
import htmlContentLength from '../utils/html-content-length';
|
||||
import shortenNumber from '../utils/shorten-number';
|
||||
|
@ -54,7 +55,7 @@ function StatusPage({ id }) {
|
|||
};
|
||||
}, [id]);
|
||||
|
||||
useEffect(() => {
|
||||
const initContext = () => {
|
||||
setUIState('loading');
|
||||
let heroTimer;
|
||||
|
||||
|
@ -173,7 +174,30 @@ function StatusPage({ id }) {
|
|||
return () => {
|
||||
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);
|
||||
|
||||
|
@ -280,7 +304,7 @@ function StatusPage({ id }) {
|
|||
}, [heroInView]);
|
||||
|
||||
useHotkeys(['esc', 'backspace'], () => {
|
||||
route(closeLink);
|
||||
location.hash = closeLink;
|
||||
});
|
||||
|
||||
return (
|
||||
|
@ -325,11 +349,9 @@ function StatusPage({ id }) {
|
|||
<NameText showAvatar account={heroStatus.account} short />{' '}
|
||||
<span class="insignificant">
|
||||
•{' '}
|
||||
<relative-time
|
||||
<RelativeTime
|
||||
datetime={heroStatus.createdAt}
|
||||
format="micro"
|
||||
threshold="P1D"
|
||||
prefix=""
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
|
|
|
@ -18,4 +18,5 @@ export default proxy({
|
|||
showCompose: false,
|
||||
showSettings: false,
|
||||
showAccount: false,
|
||||
composeCharacterCount: 0,
|
||||
});
|
||||
|
|
43
src/utils/useScroll.js
Normal file
43
src/utils/useScroll.js
Normal 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 };
|
||||
}
|
|
@ -6,12 +6,11 @@ import { defineConfig, loadEnv, splitVendorChunkPlugin } from 'vite';
|
|||
import htmlPlugin from 'vite-plugin-html-config';
|
||||
import VitePluginHtmlEnv from 'vite-plugin-html-env';
|
||||
import { VitePWA } from 'vite-plugin-pwa';
|
||||
import removeConsole from 'vite-plugin-remove-console';
|
||||
|
||||
const {
|
||||
VITE_CLIENT_NAME: CLIENT_NAME,
|
||||
NODE_ENV,
|
||||
VITE_APP_ERROR_LOGGING,
|
||||
} = loadEnv('production', process.cwd());
|
||||
const { NODE_ENV } = process.env;
|
||||
const { VITE_CLIENT_NAME: CLIENT_NAME, VITE_APP_ERROR_LOGGING: ERROR_LOGGING } =
|
||||
loadEnv('production', process.cwd());
|
||||
|
||||
const commitHash = execSync('git rev-parse --short HEAD').toString().trim();
|
||||
|
||||
|
@ -31,8 +30,9 @@ export default defineConfig({
|
|||
preact(),
|
||||
splitVendorChunkPlugin(),
|
||||
VitePluginHtmlEnv(),
|
||||
removeConsole(),
|
||||
htmlPlugin({
|
||||
headScripts: VITE_APP_ERROR_LOGGING ? [rollbarCode] : [],
|
||||
headScripts: ERROR_LOGGING ? [rollbarCode] : [],
|
||||
}),
|
||||
VitePWA({
|
||||
manifest: {
|
||||
|
|
Loading…
Reference in a new issue