diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..dd84ea78 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -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. diff --git a/README.md b/README.md index db514c4b..c78a79d7 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/package-lock.json b/package-lock.json index 2f718061..53bac186 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index dbde5c5c..4d4ecf31 100644 --- a/package.json +++ b/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", diff --git a/public/sw.js b/public/sw.js index 44af1ed5..8bf163f7 100644 --- a/public/sw.js +++ b/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); diff --git a/scripts/fetch-instances-list.js b/scripts/fetch-instances-list.js index 7b39126c..f90a2fb7 100644 --- a/scripts/fetch-instances-list.js +++ b/scripts/fetch-instances-list.js @@ -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', }); diff --git a/src/app.css b/src/app.css index f3817a1d..0e50fd3b 100644 --- a/src/app.css +++ b/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 { diff --git a/src/app.jsx b/src/app.jsx index c8cccc91..903f8d3c 100644 --- a/src/app.jsx +++ b/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 && ( - <> - -
- {/* Home will never be unmounted */} -
- +
+ {/* Home will never be unmounted */} +
)} {!isLoggedIn && uiState === 'loading' && } { const { newStatus } = results || {}; states.showCompose = false; + window.__COMPOSE__ = null; if (newStatus) { states.reloadStatusPage++; setTimeout(() => { diff --git a/src/components/account.jsx b/src/components/account.jsx index 831e93c2..075421fb 100644 --- a/src/components/account.jsx +++ b/src/components/account.jsx @@ -197,17 +197,19 @@ function Account({ account }) { {relationshipUIState !== 'loading' && relationship && ( )}

diff --git a/src/components/compose.css b/src/components/compose.css index 28930557..91787445 100644 --- a/src/components/compose.css +++ b/src/components/compose.css @@ -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; +} diff --git a/src/components/compose.jsx b/src/components/compose.jsx index e2c408ba..d5d17e8d 100644 --- a/src/components/compose.jsx +++ b/src/components/compose.jsx @@ -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 += ` -
  • - - :${encodeHTML(shortcode)}: -
  • `; - }); - // 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 += ` -
  • - - - - - ${displayNameWithEmoji || username} -
    @${encodeHTML(acct)} -
    -
  • - `; - } else { - html += ` -
  • - #${encodeHTML(name)} -
  • - `; - } - 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 (
    @@ -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,41 +707,22 @@ function Compose({ {' '}
    - - - + )} -
    - + {suffixType === 'image' ? ( + + ) : suffixType === 'video' || suffixType === 'gifv' ? ( +
    + {descTextarea} +
    + +
    - + {showModal && ( + { + if (e.target === e.currentTarget) { + setShowModal(false); + } + }} + > +
    +
    +

    + { + { + image: 'Edit image description', + video: 'Edit video description', + audio: 'Edit audio description', + }[suffixType] + } +

    +
    +
    +
    + {suffixType === 'image' ? ( + + ) : suffixType === 'video' || suffixType === 'gifv' ? ( +
    + {descTextarea} +
    +
    +
    + )} + ); } @@ -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) { diff --git a/src/components/name-text.jsx b/src/components/name-text.jsx index b4dafee1..3a9b9aea 100644 --- a/src/components/name-text.jsx +++ b/src/components/name-text.jsx @@ -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() ) { diff --git a/src/components/relative-time.jsx b/src/components/relative-time.jsx new file mode 100644 index 00000000..60d8147f --- /dev/null +++ b/src/components/relative-time.jsx @@ -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 ( + + ); +} diff --git a/src/components/status.css b/src/components/status.css index ae65237a..36c878f1 100644 --- a/src/components/status.css +++ b/src/components/status.css @@ -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; diff --git a/src/components/status.jsx b/src/components/status.jsx index 5f8df8af..425859e9 100644 --- a/src/components/status.jsx +++ b/src/components/status.jsx @@ -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" />{' '} - - {createdAtDate.toLocaleString()} - + ) : ( @@ -268,37 +263,34 @@ function Status({ alt={visibility} size="s" />{' '} - - {createdAtDate.toLocaleString()} - + ))} - {inReplyToAccountId && !withinContext && size !== 's' && ( - <> - {inReplyToAccountId === status.account.id ? ( -
    - - Thread -
    - ) : ( - !!inReplyToAccount && - !mentions.find((mention) => { - return mention.id === inReplyToAccountId; - }) && ( -
    - {' '} - + {!!inReplyToId && + !!inReplyToAccountId && + !withinContext && + size !== 's' && ( + <> + {inReplyToAccountId === status.account.id ? ( +
    + + Thread
    - ) - )} - - )} + ) : ( + !!inReplyToAccount && + (!!spoilerText || + !mentions.find((mention) => { + return mention.id === inReplyToAccountId; + })) && ( +
    + {' '} + +
    + ) + )} + + )}
    { e.preventDefault(); e.stopPropagation(); @@ -440,8 +433,10 @@ function Status({
    )} {!!card && - (size === 'l' || - (size === 'm' && !poll && !mediaAttachments.length)) && ( + !sensitive && + !spoilerText && + !poll && + !mediaAttachments.length && ( { setShowMediaModal(false); - statusRef.current?.focus(); }} /> @@ -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 (
    { - if (!showOriginal && isGIF) { + if (hoverAnimate) { try { videoRef.current.pause(); } catch (e) {} @@ -780,37 +781,41 @@ 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 ? (
    - `, + + `, }} /> ) : isGIF ? ( @@ -1130,9 +1135,7 @@ function Poll({ poll, lang, readOnly, onUpdate = () => {} }) { )}{' '} • {expired ? 'Ended' : 'Ending'}{' '} - {!!expiresAtDate && ( - - )} + {!!expiresAtDate && }

    )}
    @@ -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; diff --git a/src/compose.jsx b/src/compose.jsx index c9dbe4ca..757502eb 100644 --- a/src/compose.jsx +++ b/src/compose.jsx @@ -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'; diff --git a/src/data/instances.json b/src/data/instances.json index dd5018f8..a936f9ad 100644 --- a/src/data/instances.json +++ b/src/data/instances.json @@ -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" ] \ No newline at end of file diff --git a/src/index.css b/src/index.css index e4887fba..d8230b9c 100644 --- a/src/index.css +++ b/src/index.css @@ -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; diff --git a/src/main.jsx b/src/main.jsx index 74bdd119..ad1d5cb9 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -1,6 +1,5 @@ import './index.css'; -import '@github/relative-time-element'; import { render } from 'preact'; import { App } from './app'; diff --git a/src/pages/home.jsx b/src/pages/home.jsx index 4b2ec205..d3abe301 100644 --- a/src/pages/home.jsx +++ b/src/pages/home.jsx @@ -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 (