Compare commits
44 commits
531df4cafa
...
a4240732d6
Author | SHA1 | Date | |
---|---|---|---|
|
a4240732d6 | ||
|
6d7eddc568 | ||
|
dac2af4334 | ||
|
2099953b68 | ||
|
5931ebb8fc | ||
|
adcb87679b | ||
|
5ead17a093 | ||
|
224cad4d7f | ||
|
e08817d611 | ||
|
1ffc1c257a | ||
|
098014a109 | ||
|
7546b42c7c | ||
|
f9a73777e7 | ||
|
d5584f8dd4 | ||
|
563b06e680 | ||
|
b6a64b66c7 | ||
|
0a4aae51b7 | ||
|
d16221e296 | ||
|
ed712d15f1 | ||
|
bd8817e61b | ||
|
ef712c62a9 | ||
|
9aa2bac685 | ||
|
34077e8467 | ||
|
b473061845 | ||
|
64c7b5b4f0 | ||
|
c11bbbb2b3 | ||
|
2c1a6c8cb5 | ||
|
67a85e1eef | ||
|
2e0ef6494b | ||
|
012b86d7ce | ||
|
0c45f515f0 | ||
|
9cc590be1b | ||
|
7589ec8803 | ||
|
cd17ca0b42 | ||
|
8aab997900 | ||
|
96c44ed485 | ||
|
7053fcc96a | ||
|
ad7cb46547 | ||
|
1b1af67064 | ||
|
bdd238de0e | ||
|
ced4dc86aa | ||
|
7be1e589ab | ||
|
7da1745cca | ||
|
025a5429cc |
|
@ -209,6 +209,7 @@ These are self-hosted by other wonderful folks.
|
|||
- [phanpy.fulda.social](https://phanpy.fulda.social) by [@Ganneff@fulda.social](https://fulda.social/@Ganneff)
|
||||
- [phanpy.crmbl.uk](https://phanpy.crmbl.uk) by [@snail@crmbl.uk](https://mstdn.crmbl.uk/@snail)
|
||||
- [halo.mookiesplace.com](https://halo.mookiesplace.com) by [@mookie@mookiesplace.com](https://mookiesplace.com/@mookie)
|
||||
- [social.qrk.one](https://social.qrk.one) by [@kev@fosstodon.org](https://fosstodon.org/@kev)
|
||||
|
||||
> Note: Add yours by creating a pull request.
|
||||
|
||||
|
|
139
package-lock.json
generated
139
package-lock.json
generated
|
@ -9,9 +9,9 @@
|
|||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@formatjs/intl-localematcher": "~0.5.4",
|
||||
"@formatjs/intl-segmenter": "~11.5.5",
|
||||
"@formatjs/intl-segmenter": "~11.5.7",
|
||||
"@formkit/auto-animate": "~0.8.2",
|
||||
"@github/text-expander-element": "~2.6.1",
|
||||
"@github/text-expander-element": "~2.7.1",
|
||||
"@iconify-icons/mingcute": "~1.2.9",
|
||||
"@justinribeiro/lite-youtube": "~1.5.0",
|
||||
"@szhsin/react-menu": "~4.1.0",
|
||||
|
@ -30,7 +30,7 @@
|
|||
"moize": "~6.1.6",
|
||||
"p-retry": "~6.2.0",
|
||||
"p-throttle": "~6.1.0",
|
||||
"preact": "~10.21.0",
|
||||
"preact": "~10.22.0",
|
||||
"punycode": "~2.3.1",
|
||||
"react-hotkeys-hook": "~4.5.0",
|
||||
"react-intersection-observer": "~9.10.2",
|
||||
|
@ -38,9 +38,10 @@
|
|||
"react-router-dom": "6.6.2",
|
||||
"string-length": "6.0.0",
|
||||
"swiped-events": "~1.2.0",
|
||||
"tinyld": "~1.3.4",
|
||||
"toastify-js": "~1.12.0",
|
||||
"uid": "~2.0.2",
|
||||
"use-debounce": "~10.0.0",
|
||||
"use-debounce": "~10.0.1",
|
||||
"use-long-press": "~3.2.0",
|
||||
"use-resize-observer": "~9.1.0",
|
||||
"valtio": "1.13.2"
|
||||
|
@ -50,9 +51,9 @@
|
|||
"@trivago/prettier-plugin-sort-imports": "~4.3.0",
|
||||
"postcss": "~8.4.38",
|
||||
"postcss-dark-theme-class": "~1.3.0",
|
||||
"postcss-preset-env": "~9.5.11",
|
||||
"postcss-preset-env": "~9.5.14",
|
||||
"twitter-text": "~3.1.0",
|
||||
"vite": "~5.2.11",
|
||||
"vite": "~5.2.12",
|
||||
"vite-plugin-generate-file": "~0.1.1",
|
||||
"vite-plugin-html-config": "~1.0.11",
|
||||
"vite-plugin-pwa": "~0.20.0",
|
||||
|
@ -2036,9 +2037,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@csstools/postcss-cascade-layers": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-4.0.4.tgz",
|
||||
"integrity": "sha512-MKErv8lpEwVmAcAwidY1Kfd3oWrh2Q14kxHs9xn26XzjP/PrcdngWq63lJsZeMlBY7o+WlEOeE+FP6zPzeY2uw==",
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-4.0.6.tgz",
|
||||
"integrity": "sha512-Xt00qGAQyqAODFiFEJNkTpSUz5VfYqnDLECdlA/Vv17nl/OIV5QfTRHGAXrBGG5YcJyHpJ+GF9gF/RZvOQz4oA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
@ -2051,7 +2052,7 @@
|
|||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"@csstools/selector-specificity": "^3.0.3",
|
||||
"@csstools/selector-specificity": "^3.1.1",
|
||||
"postcss-selector-parser": "^6.0.13"
|
||||
},
|
||||
"engines": {
|
||||
|
@ -2307,9 +2308,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@csstools/postcss-is-pseudo-class": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-4.0.6.tgz",
|
||||
"integrity": "sha512-HilOhAsMpFheMYkuaREZx+CGa4hsG6kQdzwXSsuqKDFzYz2eIMP213+3dH/vUbPXaWrzqLKr8m3i0dgYPoh7vg==",
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-4.0.8.tgz",
|
||||
"integrity": "sha512-0aj591yGlq5Qac+plaWCbn5cpjs5Sh0daovYUKJUOMjIp70prGH/XPLp7QjxtbFXz3CTvb0H9a35dpEuIuUi3Q==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
@ -2322,7 +2323,7 @@
|
|||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"@csstools/selector-specificity": "^3.0.3",
|
||||
"@csstools/selector-specificity": "^3.1.1",
|
||||
"postcss-selector-parser": "^6.0.13"
|
||||
},
|
||||
"engines": {
|
||||
|
@ -2816,9 +2817,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@csstools/selector-specificity": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-3.0.3.tgz",
|
||||
"integrity": "sha512-KEPNw4+WW5AVEIyzC80rTbWEUatTW2lXpN8+8ILC8PiPeWPjwUzrPZDIOZ2wwqDmeqOYTdSGyL3+vE5GC3FB3Q==",
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-3.1.1.tgz",
|
||||
"integrity": "sha512-a7cxGcJ2wIlMFLlh8z2ONm+715QkPHiyJcxwQlKOz/03GPw1COpfhcmC9wm4xlZfp//jWHNNMwzjtqHXVWU9KA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
@ -3228,9 +3229,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@formatjs/ecma402-abstract": {
|
||||
"version": "1.18.2",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.18.2.tgz",
|
||||
"integrity": "sha512-+QoPW4csYALsQIl8GbN14igZzDbuwzcpWrku9nyMXlaqAlwRBgl5V+p0vWMGFqHOw37czNXaP/lEk4wbLgcmtA==",
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.0.0.tgz",
|
||||
"integrity": "sha512-rRqXOqdFmk7RYvj4khklyqzcfQl9vEL/usogncBHRZfZBDOwMGuSRNFl02fu5KGHXdbinju+YXyuR+Nk8xlr/g==",
|
||||
"dependencies": {
|
||||
"@formatjs/intl-localematcher": "0.5.4",
|
||||
"tslib": "^2.4.0"
|
||||
|
@ -3245,11 +3246,11 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@formatjs/intl-segmenter": {
|
||||
"version": "11.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/intl-segmenter/-/intl-segmenter-11.5.5.tgz",
|
||||
"integrity": "sha512-mMbJKFGzwYJBcwfL9EfqFje75Ce5WPar5rSi7wWvFtBPFY2Zi1cWIss7FSm2MNNM9l1BycBAsBQuXFt+Hd+0tQ==",
|
||||
"version": "11.5.7",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/intl-segmenter/-/intl-segmenter-11.5.7.tgz",
|
||||
"integrity": "sha512-MPvUKOURPY1aHc/d3YtLKp4hamrJtdBRc/AZVt9zRitrNeRszSwpIIYDHka9chQJTRIJlIfS4S9FGMdA1PE3Xw==",
|
||||
"dependencies": {
|
||||
"@formatjs/ecma402-abstract": "1.18.2",
|
||||
"@formatjs/ecma402-abstract": "2.0.0",
|
||||
"@formatjs/intl-localematcher": "0.5.4",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
|
@ -3266,12 +3267,12 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@github/text-expander-element": {
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@github/text-expander-element/-/text-expander-element-2.6.1.tgz",
|
||||
"integrity": "sha512-i6krPGXJRABfKXut0WArFd365Je4PT0MljtDoXUoCOEp+lGrmdosDMxmO0EfOYc97jBn+Hd2XO1mMsuI5+fwmQ==",
|
||||
"license": "MIT",
|
||||
"version": "2.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@github/text-expander-element/-/text-expander-element-2.7.1.tgz",
|
||||
"integrity": "sha512-CWxfYxJRkeWVCUhJveproLs6pHsPrWtK8TsjL8ByYVcSCs8CJmNzF8b7ZawrUgfai0F2jb4aIdw2FoBTykj9XA==",
|
||||
"dependencies": {
|
||||
"@github/combobox-nav": "^2.0.2"
|
||||
"@github/combobox-nav": "^2.0.2",
|
||||
"dom-input-range": "^1.1.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@iconify-icons/mingcute": {
|
||||
|
@ -4499,9 +4500,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/css-has-pseudo": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/css-has-pseudo/-/css-has-pseudo-6.0.3.tgz",
|
||||
"integrity": "sha512-qIsDxK/z0byH/mpNsv5hzQ5NOl8m1FRmOLgZpx4bG5uYHnOlO2XafeMI4mFIgNSViHwoUWcxSJZyyijaAmbs+A==",
|
||||
"version": "6.0.5",
|
||||
"resolved": "https://registry.npmjs.org/css-has-pseudo/-/css-has-pseudo-6.0.5.tgz",
|
||||
"integrity": "sha512-ZTv6RlvJJZKp32jPYnAJVhowDCrRrHUTAxsYSuUPBEDJjzws6neMnzkRblxtgmv1RgcV5dhH2gn7E3wA9Wt6lw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
@ -4514,7 +4515,7 @@
|
|||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"@csstools/selector-specificity": "^3.0.3",
|
||||
"@csstools/selector-specificity": "^3.1.1",
|
||||
"postcss-selector-parser": "^6.0.13",
|
||||
"postcss-value-parser": "^4.2.0"
|
||||
},
|
||||
|
@ -4737,6 +4738,14 @@
|
|||
"valtio": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/dom-input-range": {
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmjs.org/dom-input-range/-/dom-input-range-1.1.6.tgz",
|
||||
"integrity": "sha512-4o/SkTpscD0n81BeErrrtmE58lG8vTks++92vk//ld0NmkQTb4AVJ2rexh2yor6rtBf5IMte26u+fF3EgCppPQ==",
|
||||
"workspaces": [
|
||||
"demos"
|
||||
]
|
||||
},
|
||||
"node_modules/dom-serializer": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
||||
|
@ -6925,9 +6934,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/postcss-nesting": {
|
||||
"version": "12.1.2",
|
||||
"resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-12.1.2.tgz",
|
||||
"integrity": "sha512-FUmTHGDNundodutB4PUBxt/EPuhgtpk8FJGRsBhOuy+6FnkR2A8RZWIsyyy6XmhvX2DZQQWIkvu+HB4IbJm+Ew==",
|
||||
"version": "12.1.5",
|
||||
"resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-12.1.5.tgz",
|
||||
"integrity": "sha512-N1NgI1PDCiAGWPTYrwqm8wpjv0bgDmkYHH72pNsqTCv9CObxjxftdYu6AKtGN+pnJa7FQjMm3v4sp8QJbFsYdQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
@ -6941,8 +6950,8 @@
|
|||
],
|
||||
"dependencies": {
|
||||
"@csstools/selector-resolve-nested": "^1.1.0",
|
||||
"@csstools/selector-specificity": "^3.0.3",
|
||||
"postcss-selector-parser": "^6.0.13"
|
||||
"@csstools/selector-specificity": "^3.1.1",
|
||||
"postcss-selector-parser": "^6.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14 || ^16 || >=18"
|
||||
|
@ -7035,9 +7044,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/postcss-preset-env": {
|
||||
"version": "9.5.11",
|
||||
"resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-9.5.11.tgz",
|
||||
"integrity": "sha512-rPFnftk1vQAaR45UmsuXhKd/IZrTj39dIc4usu8qbfxyNevHnG+FB8E50U7vs0v2OxBqBt5u0J5+cwb4newzGA==",
|
||||
"version": "9.5.14",
|
||||
"resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-9.5.14.tgz",
|
||||
"integrity": "sha512-gTMi+3kENN/mN+K59aR+vEOjlkujTmmXJcM9rnAqGh9Y/euQ/ypdp9rd8mO1eoIjAD8vNS15+xbkBxoi+65BqQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
@ -7050,7 +7059,7 @@
|
|||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"@csstools/postcss-cascade-layers": "^4.0.4",
|
||||
"@csstools/postcss-cascade-layers": "^4.0.6",
|
||||
"@csstools/postcss-color-function": "^3.0.16",
|
||||
"@csstools/postcss-color-mix-function": "^2.0.16",
|
||||
"@csstools/postcss-exponential-functions": "^1.0.7",
|
||||
|
@ -7060,7 +7069,7 @@
|
|||
"@csstools/postcss-hwb-function": "^3.0.15",
|
||||
"@csstools/postcss-ic-unit": "^3.0.6",
|
||||
"@csstools/postcss-initial": "^1.0.1",
|
||||
"@csstools/postcss-is-pseudo-class": "^4.0.6",
|
||||
"@csstools/postcss-is-pseudo-class": "^4.0.8",
|
||||
"@csstools/postcss-light-dark-function": "^1.0.5",
|
||||
"@csstools/postcss-logical-float-and-clear": "^2.0.1",
|
||||
"@csstools/postcss-logical-overflow": "^1.0.1",
|
||||
|
@ -7082,7 +7091,7 @@
|
|||
"autoprefixer": "^10.4.19",
|
||||
"browserslist": "^4.22.3",
|
||||
"css-blank-pseudo": "^6.0.2",
|
||||
"css-has-pseudo": "^6.0.3",
|
||||
"css-has-pseudo": "^6.0.5",
|
||||
"css-prefers-color-scheme": "^9.0.1",
|
||||
"cssdb": "^8.0.0",
|
||||
"postcss-attribute-case-insensitive": "^6.0.3",
|
||||
|
@ -7102,7 +7111,7 @@
|
|||
"postcss-image-set-function": "^6.0.3",
|
||||
"postcss-lab-function": "^6.0.16",
|
||||
"postcss-logical": "^7.0.1",
|
||||
"postcss-nesting": "^12.1.2",
|
||||
"postcss-nesting": "^12.1.5",
|
||||
"postcss-opacity-percentage": "^2.0.0",
|
||||
"postcss-overflow-shorthand": "^5.0.1",
|
||||
"postcss-page-break": "^3.0.4",
|
||||
|
@ -7179,9 +7188,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/postcss-selector-parser": {
|
||||
"version": "6.0.15",
|
||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.15.tgz",
|
||||
"integrity": "sha512-rEYkQOMUCEMhsKbK66tbEU9QVIxbhN18YiniAwA7XQYTVBqrBy+P2p5JcdqsHgKM2zWylp8d7J6eszocfds5Sw==",
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.0.tgz",
|
||||
"integrity": "sha512-UMz42UD0UY0EApS0ZL9o1XnLhSTtvvvLe5Dc2H2O56fvRZi+KulDyf5ctDhhtYJBGKStV2FL1fy6253cmLgqVQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"cssesc": "^3.0.0",
|
||||
|
@ -7199,9 +7208,9 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/preact": {
|
||||
"version": "10.21.0",
|
||||
"resolved": "https://registry.npmjs.org/preact/-/preact-10.21.0.tgz",
|
||||
"integrity": "sha512-aQAIxtzWEwH8ou+OovWVSVNlFImL7xUCwJX3YMqA3U8iKCNC34999fFOnWjYNsylgfPgMexpbk7WYOLtKr/mxg==",
|
||||
"version": "10.22.0",
|
||||
"resolved": "https://registry.npmjs.org/preact/-/preact-10.22.0.tgz",
|
||||
"integrity": "sha512-RRurnSjJPj4rp5K6XoP45Ui33ncb7e4H7WiOHVpjbkvqvA3U+N8Z6Qbo0AE6leGYBV66n8EhEaFixvIu3SkxFw==",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/preact"
|
||||
|
@ -8010,6 +8019,21 @@
|
|||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyld": {
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/tinyld/-/tinyld-1.3.4.tgz",
|
||||
"integrity": "sha512-u26CNoaInA4XpDU+8s/6Cq8xHc2T5M4fXB3ICfXPokUQoLzmPgSZU02TAkFwFMJCWTjk53gtkS8pETTreZwCqw==",
|
||||
"bin": {
|
||||
"tinyld": "bin/tinyld.js",
|
||||
"tinyld-heavy": "bin/tinyld-heavy.js",
|
||||
"tinyld-light": "bin/tinyld-light.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12.10.0",
|
||||
"npm": ">= 6.12.0",
|
||||
"yarn": ">= 1.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/to-fast-properties": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
|
||||
|
@ -8331,10 +8355,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/use-debounce": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-10.0.0.tgz",
|
||||
"integrity": "sha512-XRjvlvCB46bah9IBXVnq/ACP2lxqXyZj0D9hj4K5OzNroMDpTEBg8Anuh1/UfRTRs7pLhQ+RiNxxwZu9+MVl1A==",
|
||||
"license": "MIT",
|
||||
"version": "10.0.1",
|
||||
"resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-10.0.1.tgz",
|
||||
"integrity": "sha512-0uUXjOfm44e6z4LZ/woZvkM8FwV1wiuoB6xnrrOmeAEjRDDzTLQNRFtYHvqUsJdrz1X37j0rVGIVp144GLHGKg==",
|
||||
"engines": {
|
||||
"node": ">= 16.0.0"
|
||||
},
|
||||
|
@ -8405,9 +8428,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "5.2.11",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.2.11.tgz",
|
||||
"integrity": "sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ==",
|
||||
"version": "5.2.12",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.2.12.tgz",
|
||||
"integrity": "sha512-/gC8GxzxMK5ntBwb48pR32GGhENnjtY30G4A0jemunsBkiEZFw60s8InGpN8gkhHEkjnRK1aSAxeQgwvFhUHAA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.20.1",
|
||||
|
|
13
package.json
13
package.json
|
@ -11,9 +11,9 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@formatjs/intl-localematcher": "~0.5.4",
|
||||
"@formatjs/intl-segmenter": "~11.5.5",
|
||||
"@formatjs/intl-segmenter": "~11.5.7",
|
||||
"@formkit/auto-animate": "~0.8.2",
|
||||
"@github/text-expander-element": "~2.6.1",
|
||||
"@github/text-expander-element": "~2.7.1",
|
||||
"@iconify-icons/mingcute": "~1.2.9",
|
||||
"@justinribeiro/lite-youtube": "~1.5.0",
|
||||
"@szhsin/react-menu": "~4.1.0",
|
||||
|
@ -32,7 +32,7 @@
|
|||
"moize": "~6.1.6",
|
||||
"p-retry": "~6.2.0",
|
||||
"p-throttle": "~6.1.0",
|
||||
"preact": "~10.21.0",
|
||||
"preact": "~10.22.0",
|
||||
"punycode": "~2.3.1",
|
||||
"react-hotkeys-hook": "~4.5.0",
|
||||
"react-intersection-observer": "~9.10.2",
|
||||
|
@ -40,9 +40,10 @@
|
|||
"react-router-dom": "6.6.2",
|
||||
"string-length": "6.0.0",
|
||||
"swiped-events": "~1.2.0",
|
||||
"tinyld": "~1.3.4",
|
||||
"toastify-js": "~1.12.0",
|
||||
"uid": "~2.0.2",
|
||||
"use-debounce": "~10.0.0",
|
||||
"use-debounce": "~10.0.1",
|
||||
"use-long-press": "~3.2.0",
|
||||
"use-resize-observer": "~9.1.0",
|
||||
"valtio": "1.13.2"
|
||||
|
@ -52,9 +53,9 @@
|
|||
"@trivago/prettier-plugin-sort-imports": "~4.3.0",
|
||||
"postcss": "~8.4.38",
|
||||
"postcss-dark-theme-class": "~1.3.0",
|
||||
"postcss-preset-env": "~9.5.11",
|
||||
"postcss-preset-env": "~9.5.14",
|
||||
"twitter-text": "~3.1.0",
|
||||
"vite": "~5.2.11",
|
||||
"vite": "~5.2.12",
|
||||
"vite-plugin-generate-file": "~0.1.1",
|
||||
"vite-plugin-html-config": "~1.0.11",
|
||||
"vite-plugin-pwa": "~0.20.0",
|
||||
|
|
48
src/app.css
48
src/app.css
|
@ -1580,8 +1580,8 @@ body:has(.media-modal-container + .status-deck) .media-post-link {
|
|||
0 10px 36px -4px var(--button-bg-blur-color);
|
||||
transition: all 0.3s ease-in-out;
|
||||
}
|
||||
.deck-container:has(header[hidden]) ~ #compose-button,
|
||||
#compose-button[hidden] {
|
||||
.deck-container:has(header[hidden]) ~ #compose-button:not(.loading),
|
||||
#compose-button[hidden]:not(.loading) {
|
||||
transform: translateY(200%);
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
|
@ -1610,6 +1610,48 @@ body:has(.media-modal-container + .status-deck) .media-post-link {
|
|||
bottom: calc(16px + env(safe-area-inset-bottom) + 52px);
|
||||
}
|
||||
}
|
||||
#compose-button {
|
||||
&.min {
|
||||
outline: 2px solid var(--button-text-color);
|
||||
z-index: 1001; /* Higher than modal */
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--button-bg-color);
|
||||
border: 2px solid var(--button-text-color);
|
||||
box-shadow: 0 2px 8px var(--drop-shadow-color);
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease-out 0.5s;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&.loading {
|
||||
outline-color: var(--button-bg-blur-color);
|
||||
|
||||
&:before {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
content: '';
|
||||
border-radius: 50%;
|
||||
animation: spin 5s linear infinite;
|
||||
border: 2px dashed var(--button-text-color);
|
||||
}
|
||||
}
|
||||
|
||||
&.error {
|
||||
&:after {
|
||||
background-color: var(--red-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* SHEET */
|
||||
|
||||
|
@ -2184,6 +2226,8 @@ body > .szh-menu-container {
|
|||
box-shadow: 0 3px 8px -1px var(--drop-shadow-color),
|
||||
0 10px 36px -4px var(--button-bg-blur-color);
|
||||
text-align: center;
|
||||
width: fit-content;
|
||||
max-width: calc(100vw - 32px);
|
||||
}
|
||||
.toastify-bottom {
|
||||
margin-bottom: env(safe-area-inset-bottom);
|
||||
|
|
|
@ -126,13 +126,13 @@ setInterval(() => {
|
|||
// Related: https://github.com/vitejs/vite/issues/10600
|
||||
setTimeout(() => {
|
||||
for (const icon in ICONS) {
|
||||
queueMicrotask(() => {
|
||||
setTimeout(() => {
|
||||
if (Array.isArray(ICONS[icon])) {
|
||||
ICONS[icon][0]?.();
|
||||
} else {
|
||||
ICONS[icon]?.();
|
||||
}
|
||||
});
|
||||
}, 1);
|
||||
}
|
||||
}, 5000);
|
||||
|
||||
|
|
|
@ -108,4 +108,5 @@ export const ICONS = {
|
|||
settings: () => import('@iconify-icons/mingcute/settings-6-line'),
|
||||
'heart-break': () => import('@iconify-icons/mingcute/heart-crack-line'),
|
||||
'user-x': () => import('@iconify-icons/mingcute/user-x-line'),
|
||||
minimize: () => import('@iconify-icons/mingcute/arrows-down-line'),
|
||||
};
|
||||
|
|
|
@ -133,21 +133,18 @@ function AccountBlock({
|
|||
)}
|
||||
</span>
|
||||
{showActivity && (
|
||||
<>
|
||||
<br />
|
||||
<small class="last-status-at insignificant">
|
||||
Posts: {statusesCount}
|
||||
{!!lastStatusAt && (
|
||||
<>
|
||||
{' '}
|
||||
· Last posted:{' '}
|
||||
{niceDateTime(lastStatusAt, {
|
||||
hideTime: true,
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</small>
|
||||
</>
|
||||
<div class="account-block-stats">
|
||||
Posts: {shortenNumber(statusesCount)}
|
||||
{!!lastStatusAt && (
|
||||
<>
|
||||
{' '}
|
||||
· Last posted:{' '}
|
||||
{niceDateTime(lastStatusAt, {
|
||||
hideTime: true,
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{showStats && (
|
||||
<div class="account-block-stats">
|
||||
|
|
|
@ -19,6 +19,7 @@ import { getLists } from '../utils/lists';
|
|||
import niceDateTime from '../utils/nice-date-time';
|
||||
import pmem from '../utils/pmem';
|
||||
import shortenNumber from '../utils/shorten-number';
|
||||
import showCompose from '../utils/show-compose';
|
||||
import showToast from '../utils/show-toast';
|
||||
import states, { hideAllModals } from '../utils/states';
|
||||
import store from '../utils/store';
|
||||
|
@ -1081,11 +1082,11 @@ function RelatedActions({
|
|||
<>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
states.showCompose = {
|
||||
showCompose({
|
||||
draftStatus: {
|
||||
status: `@${currentInfo?.acct || acct} `,
|
||||
},
|
||||
};
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Icon icon="at" />
|
||||
|
|
|
@ -63,7 +63,7 @@ function Avatar({ url, size, alt = '', squircle, ...props }) {
|
|||
if (avatarRef.current) avatarRef.current.dataset.loaded = true;
|
||||
if (alphaCache[url] !== undefined) return;
|
||||
if (isMissing) return;
|
||||
queueMicrotask(() => {
|
||||
setTimeout(() => {
|
||||
try {
|
||||
// Check if image has alpha channel
|
||||
const { width, height } = e.target;
|
||||
|
@ -88,7 +88,7 @@ function Avatar({ url, size, alt = '', squircle, ...props }) {
|
|||
// Silent fail
|
||||
alphaCache[url] = false;
|
||||
}
|
||||
});
|
||||
}, 1);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useSnapshot } from 'valtio';
|
||||
|
||||
import openCompose from '../utils/open-compose';
|
||||
import openOSK from '../utils/open-osk';
|
||||
|
@ -7,7 +8,15 @@ import states from '../utils/states';
|
|||
import Icon from './icon';
|
||||
|
||||
export default function ComposeButton() {
|
||||
const snapStates = useSnapshot(states);
|
||||
|
||||
function handleButton(e) {
|
||||
if (snapStates.composerState.minimized) {
|
||||
states.composerState.minimized = false;
|
||||
openOSK();
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.shiftKey) {
|
||||
const newWin = openCompose();
|
||||
|
||||
|
@ -28,7 +37,14 @@ export default function ComposeButton() {
|
|||
});
|
||||
|
||||
return (
|
||||
<button type="button" id="compose-button" onClick={handleButton}>
|
||||
<button
|
||||
type="button"
|
||||
id="compose-button"
|
||||
onClick={handleButton}
|
||||
class={`${snapStates.composerState.minimized ? 'min' : ''} ${
|
||||
snapStates.composerState.publishing ? 'loading' : ''
|
||||
} ${snapStates.composerState.publishingError ? 'error' : ''}`}
|
||||
>
|
||||
<Icon icon="quill" size="xl" alt="Compose" />
|
||||
</button>
|
||||
);
|
||||
|
|
48
src/components/compose-suspense.jsx
Normal file
48
src/components/compose-suspense.jsx
Normal file
|
@ -0,0 +1,48 @@
|
|||
import { shouldPolyfill } from '@formatjs/intl-segmenter/should-polyfill';
|
||||
import { useEffect, useState } from 'preact/hooks';
|
||||
|
||||
import Loader from './loader';
|
||||
|
||||
const supportsIntlSegmenter = !shouldPolyfill();
|
||||
|
||||
function importIntlSegmenter() {
|
||||
if (!supportsIntlSegmenter) {
|
||||
return import('@formatjs/intl-segmenter/polyfill-force').catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
function importCompose() {
|
||||
return import('./compose');
|
||||
}
|
||||
|
||||
export async function preload() {
|
||||
try {
|
||||
await importIntlSegmenter();
|
||||
importCompose();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
export default function ComposeSuspense(props) {
|
||||
const [Compose, setCompose] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
if (supportsIntlSegmenter) {
|
||||
const component = await importCompose();
|
||||
setCompose(component);
|
||||
} else {
|
||||
await importIntlSegmenter();
|
||||
const component = await importCompose();
|
||||
setCompose(component);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
return Compose?.default ? <Compose.default {...props} /> : <Loader />;
|
||||
}
|
|
@ -298,14 +298,20 @@
|
|||
height: 2.2em;
|
||||
}
|
||||
#compose-container .text-expander-menu li:is(:hover, :focus, [aria-selected]) {
|
||||
color: var(--bg-color);
|
||||
background-color: var(--link-color);
|
||||
}
|
||||
#compose-container
|
||||
.text-expander-menu:hover
|
||||
li[aria-selected]:not(:hover, :focus) {
|
||||
background-color: var(--link-bg-color);
|
||||
color: var(--text-color);
|
||||
background-color: var(--bg-color);
|
||||
}
|
||||
#compose-container .text-expander-menu li[aria-selected] {
|
||||
box-shadow: inset 4px 0 0 0 var(--button-bg-color);
|
||||
}
|
||||
#compose-container .text-expander-menu li[data-more] {
|
||||
&:not(:hover, :focus, [aria-selected]) {
|
||||
color: var(--text-insignificant-color);
|
||||
background-color: var(--bg-faded-color);
|
||||
}
|
||||
|
||||
font-size: 0.8em;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#compose-container .form-visibility-direct {
|
||||
|
@ -334,6 +340,21 @@
|
|||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: stretch;
|
||||
|
||||
.media-error {
|
||||
padding: 2px;
|
||||
color: var(--orange-fg-color);
|
||||
background-color: transparent;
|
||||
border: 1.5px dashed transparent;
|
||||
line-height: 1;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
|
||||
&:is(:hover, :focus) {
|
||||
background-color: var(--bg-color);
|
||||
border-color: var(--orange-fg-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
#compose-container .media-preview {
|
||||
flex-shrink: 0;
|
||||
|
@ -594,6 +615,75 @@
|
|||
} */
|
||||
}
|
||||
|
||||
#mention-sheet {
|
||||
height: 50vh;
|
||||
|
||||
.accounts-list {
|
||||
--list-gap: 1px;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 8px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: var(--list-gap);
|
||||
|
||||
&.loading {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
/* align-items: center; */
|
||||
margin: 0 -8px;
|
||||
padding: 8px;
|
||||
gap: 8px;
|
||||
position: relative;
|
||||
justify-content: space-between;
|
||||
border-radius: 8px;
|
||||
/* align-items: center; */
|
||||
|
||||
&:hover {
|
||||
background-image: linear-gradient(
|
||||
to right,
|
||||
transparent 75%,
|
||||
var(--link-bg-color)
|
||||
);
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background-image: linear-gradient(
|
||||
to right,
|
||||
var(--bg-faded-color) 75%,
|
||||
var(--link-bg-color)
|
||||
);
|
||||
}
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
display: block;
|
||||
border-top: var(--hairline-width) solid var(--divider-color);
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 58px;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
&:has(+ li:is(.selected, :hover)):before,
|
||||
&:is(.selected, :hover):before {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
> button {
|
||||
border-radius: 4px;
|
||||
&:hover {
|
||||
outline: 2px solid var(--button-bg-blur-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#custom-emojis-sheet {
|
||||
max-height: 50vh;
|
||||
max-height: 50dvh;
|
||||
|
@ -609,7 +699,6 @@
|
|||
input {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -379,26 +379,26 @@ function Media({
|
|||
const autoGIFAnimate = !showOriginal && autoAnimate && isGIF;
|
||||
const showProgress = original.duration > 5;
|
||||
|
||||
const videoHTML = `
|
||||
<video
|
||||
src="${url}"
|
||||
poster="${previewUrl}"
|
||||
width="${width}"
|
||||
height="${height}"
|
||||
data-orientation="${orientation}"
|
||||
preload="auto"
|
||||
autoplay
|
||||
${isGIF ? 'muted' : ''}
|
||||
${isGIF ? '' : 'controls'}
|
||||
playsinline
|
||||
loop="${loopable}"
|
||||
${isGIF ? 'ondblclick="this.paused ? this.play() : this.pause()"' : ''}
|
||||
${
|
||||
isGIF && showProgress
|
||||
? "ontimeupdate=\"this.closest('.media-gif') && this.closest('.media-gif').style.setProperty('--progress', `${~~((this.currentTime / this.duration) * 100)}%`)\""
|
||||
: ''
|
||||
}
|
||||
></video>
|
||||
// This string is only for autoplay + muted to work on Mobile Safari
|
||||
const gifHTML = `
|
||||
<video
|
||||
src="${url}"
|
||||
poster="${previewUrl}"
|
||||
width="${width}"
|
||||
height="${height}"
|
||||
data-orientation="${orientation}"
|
||||
preload="auto"
|
||||
autoplay
|
||||
muted
|
||||
playsinline
|
||||
loop="${loopable}"
|
||||
ondblclick="this.paused ? this.play() : this.pause()"
|
||||
${
|
||||
showProgress
|
||||
? "ontimeupdate=\"this.closest('.media-gif') && this.closest('.media-gif').style.setProperty('--progress', `${~~((this.currentTime / this.duration) * 100)}%`)\""
|
||||
: ''
|
||||
}
|
||||
></video>
|
||||
`;
|
||||
|
||||
return (
|
||||
|
@ -461,17 +461,33 @@ function Media({
|
|||
<div
|
||||
ref={mediaRef}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: videoHTML,
|
||||
__html: gifHTML,
|
||||
}}
|
||||
/>
|
||||
</QuickPinchZoom>
|
||||
) : (
|
||||
) : isGIF ? (
|
||||
<div
|
||||
class="video-container"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: videoHTML,
|
||||
__html: gifHTML,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div class="video-container">
|
||||
<video
|
||||
slot="media"
|
||||
src={url}
|
||||
poster={previewUrl}
|
||||
width={width}
|
||||
height={height}
|
||||
data-orientation={orientation}
|
||||
preload="auto"
|
||||
autoplay
|
||||
playsinline
|
||||
loop={loopable}
|
||||
controls
|
||||
></video>
|
||||
</div>
|
||||
)
|
||||
) : isGIF ? (
|
||||
<video
|
||||
|
|
|
@ -10,17 +10,56 @@
|
|||
align-items: center;
|
||||
background-color: var(--backdrop-color);
|
||||
animation: appear 0.5s var(--timing-function) both;
|
||||
transition: all 0.5s var(--timing-function);
|
||||
|
||||
&.solid {
|
||||
background-color: var(--backdrop-solid-color);
|
||||
}
|
||||
|
||||
--compose-button-dimension: 56px;
|
||||
--compose-button-dimension-half: calc(var(--compose-button-dimension) / 2);
|
||||
--compose-button-dimension-margin: 16px;
|
||||
|
||||
&.min {
|
||||
/* Minimized */
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
overflow: hidden;
|
||||
transform: scale(0);
|
||||
--right: max(
|
||||
var(--compose-button-dimension-margin),
|
||||
env(safe-area-inset-right)
|
||||
);
|
||||
--bottom: max(
|
||||
var(--compose-button-dimension-margin),
|
||||
env(safe-area-inset-bottom)
|
||||
);
|
||||
--origin-right: calc(
|
||||
100% - var(--compose-button-dimension-half) - var(--right)
|
||||
);
|
||||
--origin-bottom: calc(
|
||||
100% - var(--compose-button-dimension-half) - var(--bottom)
|
||||
);
|
||||
transform-origin: var(--origin-right) var(--origin-bottom);
|
||||
}
|
||||
|
||||
.sheet {
|
||||
transition: transform 0.3s var(--timing-function);
|
||||
transform-origin: center bottom;
|
||||
transform-origin: 80% 80%;
|
||||
}
|
||||
|
||||
&:has(~ div) .sheet {
|
||||
transform: scale(0.975);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: calc(40em - 1px)) {
|
||||
#app[data-shortcuts-view-mode='tab-menu-bar'] ~ #modal-container > div.min {
|
||||
border: 2px solid red;
|
||||
|
||||
--bottom: calc(
|
||||
var(--compose-button-dimension-margin) + env(safe-area-inset-bottom) +
|
||||
52px
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ import useCloseWatcher from '../utils/useCloseWatcher';
|
|||
|
||||
const $modalContainer = document.getElementById('modal-container');
|
||||
|
||||
function Modal({ children, onClose, onClick, class: className }) {
|
||||
function Modal({ children, onClose, onClick, class: className, minimized }) {
|
||||
if (!children) return null;
|
||||
|
||||
const modalRef = useRef();
|
||||
|
@ -41,6 +41,33 @@ function Modal({ children, onClose, onClick, class: className }) {
|
|||
);
|
||||
useCloseWatcher(onClose, [onClose]);
|
||||
|
||||
useEffect(() => {
|
||||
const $deckContainers = document.querySelectorAll('.deck-container');
|
||||
if (minimized) {
|
||||
// Similar to focusDeck in focus-deck.jsx
|
||||
// Focus last deck
|
||||
const page = $deckContainers[$deckContainers.length - 1]; // last one
|
||||
if (page && page.tabIndex === -1) {
|
||||
page.focus();
|
||||
}
|
||||
} else {
|
||||
if (children) {
|
||||
$deckContainers.forEach(($deckContainer) => {
|
||||
$deckContainer.setAttribute('inert', '');
|
||||
});
|
||||
} else {
|
||||
$deckContainers.forEach(($deckContainer) => {
|
||||
$deckContainer.removeAttribute('inert');
|
||||
});
|
||||
}
|
||||
}
|
||||
return () => {
|
||||
$deckContainers.forEach(($deckContainer) => {
|
||||
$deckContainer.removeAttribute('inert');
|
||||
});
|
||||
};
|
||||
}, [children, minimized]);
|
||||
|
||||
const Modal = (
|
||||
<div
|
||||
ref={(node) => {
|
||||
|
@ -54,7 +81,8 @@ function Modal({ children, onClose, onClick, class: className }) {
|
|||
onClose?.(e);
|
||||
}
|
||||
}}
|
||||
tabIndex="-1"
|
||||
tabIndex={minimized ? 0 : '-1'}
|
||||
inert={minimized}
|
||||
onFocus={(e) => {
|
||||
try {
|
||||
if (e.target === e.currentTarget) {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { lazy } from 'preact/compat';
|
||||
import { useEffect } from 'preact/hooks';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { subscribe, useSnapshot } from 'valtio';
|
||||
|
||||
|
@ -9,19 +9,16 @@ import showToast from '../utils/show-toast';
|
|||
import states from '../utils/states';
|
||||
|
||||
import AccountSheet from './account-sheet';
|
||||
// import Compose from './compose';
|
||||
import ComposeSuspense, { preload } from './compose-suspense';
|
||||
import Drafts from './drafts';
|
||||
import EmbedModal from './embed-modal';
|
||||
import GenericAccounts from './generic-accounts';
|
||||
import IntlSegmenterSuspense from './intl-segmenter-suspense';
|
||||
import MediaAltModal from './media-alt-modal';
|
||||
import MediaModal from './media-modal';
|
||||
import Modal from './modal';
|
||||
import ReportModal from './report-modal';
|
||||
import ShortcutsSettings from './shortcuts-settings';
|
||||
|
||||
const Compose = lazy(() => import('./compose'));
|
||||
|
||||
subscribe(states, (changes) => {
|
||||
for (const [action, path, value, prevValue] of changes) {
|
||||
// When closing modal, focus on deck
|
||||
|
@ -36,55 +33,60 @@ export default function Modals() {
|
|||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(preload, 1000);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
{!!snapStates.showCompose && (
|
||||
<Modal class="solid">
|
||||
<IntlSegmenterSuspense>
|
||||
<Compose
|
||||
replyToStatus={
|
||||
typeof snapStates.showCompose !== 'boolean'
|
||||
? snapStates.showCompose.replyToStatus
|
||||
: window.__COMPOSE__?.replyToStatus || null
|
||||
<Modal
|
||||
class={`solid ${snapStates.composerState.minimized ? 'min' : ''}`}
|
||||
minimized={!!snapStates.composerState.minimized}
|
||||
>
|
||||
<ComposeSuspense
|
||||
replyToStatus={
|
||||
typeof snapStates.showCompose !== 'boolean'
|
||||
? snapStates.showCompose.replyToStatus
|
||||
: window.__COMPOSE__?.replyToStatus || null
|
||||
}
|
||||
editStatus={
|
||||
states.showCompose?.editStatus ||
|
||||
window.__COMPOSE__?.editStatus ||
|
||||
null
|
||||
}
|
||||
draftStatus={
|
||||
states.showCompose?.draftStatus ||
|
||||
window.__COMPOSE__?.draftStatus ||
|
||||
null
|
||||
}
|
||||
onClose={(results) => {
|
||||
const { newStatus, instance, type } = results || {};
|
||||
states.showCompose = false;
|
||||
window.__COMPOSE__ = null;
|
||||
if (newStatus) {
|
||||
states.reloadStatusPage++;
|
||||
showToast({
|
||||
text: {
|
||||
post: 'Post published. Check it out.',
|
||||
reply: 'Reply posted. Check it out.',
|
||||
edit: 'Post updated. Check it out.',
|
||||
}[type || 'post'],
|
||||
delay: 1000,
|
||||
duration: 10_000, // 10 seconds
|
||||
onClick: (toast) => {
|
||||
toast.hideToast();
|
||||
states.prevLocation = location;
|
||||
navigate(
|
||||
instance
|
||||
? `/${instance}/s/${newStatus.id}`
|
||||
: `/s/${newStatus.id}`,
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
editStatus={
|
||||
states.showCompose?.editStatus ||
|
||||
window.__COMPOSE__?.editStatus ||
|
||||
null
|
||||
}
|
||||
draftStatus={
|
||||
states.showCompose?.draftStatus ||
|
||||
window.__COMPOSE__?.draftStatus ||
|
||||
null
|
||||
}
|
||||
onClose={(results) => {
|
||||
const { newStatus, instance, type } = results || {};
|
||||
states.showCompose = false;
|
||||
window.__COMPOSE__ = null;
|
||||
if (newStatus) {
|
||||
states.reloadStatusPage++;
|
||||
showToast({
|
||||
text: {
|
||||
post: 'Post published. Check it out.',
|
||||
reply: 'Reply posted. Check it out.',
|
||||
edit: 'Post updated. Check it out.',
|
||||
}[type || 'post'],
|
||||
delay: 1000,
|
||||
duration: 10_000, // 10 seconds
|
||||
onClick: (toast) => {
|
||||
toast.hideToast();
|
||||
states.prevLocation = location;
|
||||
navigate(
|
||||
instance
|
||||
? `/${instance}/s/${newStatus.id}`
|
||||
: `/s/${newStatus.id}`,
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</IntlSegmenterSuspense>
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
)}
|
||||
{!!snapStates.showSettings && (
|
||||
|
@ -187,6 +189,7 @@ export default function Modals() {
|
|||
}
|
||||
postID={snapStates.showGenericAccounts.postID}
|
||||
onClose={() => (states.showGenericAccounts = false)}
|
||||
blankCopy={snapStates.showGenericAccounts.blankCopy}
|
||||
/>
|
||||
</Modal>
|
||||
)}
|
||||
|
|
|
@ -31,16 +31,17 @@ function NameText({
|
|||
.replace(/(\:(\w|\+|\-)+\:)(?=|[\!\.\?]|$)/g, '') // Remove shortcodes, regex from https://regex101.com/r/iE9uV0/1
|
||||
.replace(/\s+/g, ''); // E.g. "My name" === "myname"
|
||||
const shortenedAlphaNumericDisplayName = shortenedDisplayName.replace(
|
||||
/[^a-z0-9]/gi,
|
||||
/[^a-z0-9@\.]/gi,
|
||||
'',
|
||||
); // Remove non-alphanumeric characters
|
||||
|
||||
if (
|
||||
!short &&
|
||||
(trimmedUsername === trimmedDisplayName ||
|
||||
trimmedUsername === shortenedDisplayName ||
|
||||
trimmedUsername === shortenedAlphaNumericDisplayName ||
|
||||
nameCollator.compare(trimmedUsername, shortenedDisplayName) === 0)
|
||||
(!short &&
|
||||
(trimmedUsername === trimmedDisplayName ||
|
||||
trimmedUsername === shortenedDisplayName ||
|
||||
trimmedUsername === shortenedAlphaNumericDisplayName ||
|
||||
nameCollator.compare(trimmedUsername, shortenedDisplayName) === 0)) ||
|
||||
shortenedAlphaNumericDisplayName === acct.toLowerCase()
|
||||
) {
|
||||
username = null;
|
||||
}
|
||||
|
@ -57,9 +58,15 @@ function NameText({
|
|||
}
|
||||
onClick={(e) => {
|
||||
if (external) return;
|
||||
if (e.shiftKey) return; // Save link? 🤷♂️
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (onClick) return onClick(e);
|
||||
if (e.metaKey || e.ctrlKey || e.shiftKey || e.which === 2) {
|
||||
const internalURL = `#/${instance}/a/${id}`;
|
||||
window.open(internalURL, '_blank');
|
||||
return;
|
||||
}
|
||||
states.showAccount = {
|
||||
account,
|
||||
instance,
|
||||
|
|
|
@ -132,7 +132,7 @@ const MODERATION_WARNING_TEXT = {
|
|||
suspend: 'Your account has been suspended.',
|
||||
};
|
||||
|
||||
const AVATARS_LIMIT = 50;
|
||||
const AVATARS_LIMIT = 30;
|
||||
|
||||
function Notification({
|
||||
notification,
|
||||
|
@ -374,11 +374,7 @@ function Notification({
|
|||
? 'xxl'
|
||||
: _accounts.length < 20
|
||||
? 'xl'
|
||||
: _accounts.length < 30
|
||||
? 'l'
|
||||
: _accounts.length < 40
|
||||
? 'm'
|
||||
: 's' // My god, this person is popular!
|
||||
: 'l'
|
||||
}
|
||||
key={account.id}
|
||||
alt={`${account.displayName} @${account.acct}`}
|
||||
|
|
|
@ -23,6 +23,7 @@ import {
|
|||
} from 'preact/hooks';
|
||||
import punycode from 'punycode';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { detectAll } from 'tinyld/light';
|
||||
import { useLongPress } from 'use-long-press';
|
||||
import { useSnapshot } from 'valtio';
|
||||
|
||||
|
@ -46,11 +47,13 @@ import handleContentLinks from '../utils/handle-content-links';
|
|||
import htmlContentLength from '../utils/html-content-length';
|
||||
import isMastodonLinkMaybe from '../utils/isMastodonLinkMaybe';
|
||||
import localeMatch from '../utils/locale-match';
|
||||
import mem from '../utils/mem';
|
||||
import niceDateTime from '../utils/nice-date-time';
|
||||
import openCompose from '../utils/open-compose';
|
||||
import pmem from '../utils/pmem';
|
||||
import safeBoundingBoxPadding from '../utils/safe-bounding-box-padding';
|
||||
import shortenNumber from '../utils/shorten-number';
|
||||
import showCompose from '../utils/show-compose';
|
||||
import showToast from '../utils/show-toast';
|
||||
import { speak, supportsTTS } from '../utils/speech';
|
||||
import states, { getStatus, saveStatus, statusKey } from '../utils/states';
|
||||
|
@ -157,6 +160,25 @@ const SIZE_CLASS = {
|
|||
l: 'large',
|
||||
};
|
||||
|
||||
const detectLang = mem((text) => {
|
||||
text = text?.trim();
|
||||
|
||||
// Ref: https://github.com/komodojp/tinyld/blob/develop/docs/benchmark.md
|
||||
// 500 should be enough for now, also the default max chars for Mastodon
|
||||
if (text?.length > 500) {
|
||||
return null;
|
||||
}
|
||||
const langs = detectAll(text);
|
||||
const lang = langs[0];
|
||||
if (lang?.lang && lang?.accuracy > 0.5) {
|
||||
// If > 50% accurate, use it
|
||||
// It can be accurate if < 50% but better be safe
|
||||
// Though > 50% also can be inaccurate 🤷♂️
|
||||
return lang.lang;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
function Status({
|
||||
statusID,
|
||||
status,
|
||||
|
@ -241,7 +263,7 @@ function Status({
|
|||
sensitive,
|
||||
spoilerText,
|
||||
visibility, // public, unlisted, private, direct
|
||||
language,
|
||||
language: _language,
|
||||
editedAt,
|
||||
filtered,
|
||||
card,
|
||||
|
@ -264,6 +286,42 @@ function Status({
|
|||
emojiReactions,
|
||||
} = status;
|
||||
|
||||
const [languageAutoDetected, setLanguageAutoDetected] = useState(null);
|
||||
useEffect(() => {
|
||||
if (!content) return;
|
||||
if (_language) return;
|
||||
let timer;
|
||||
timer = setTimeout(() => {
|
||||
let detected = detectLang(
|
||||
getHTMLText(content, {
|
||||
preProcess: (dom) => {
|
||||
// Remove anything that can skew the language detection
|
||||
|
||||
// Remove .mention, .hashtag, pre, code, a:has(.invisible)
|
||||
dom
|
||||
.querySelectorAll(
|
||||
'.mention, .hashtag, pre, code, a:has(.invisible)',
|
||||
)
|
||||
.forEach((a) => {
|
||||
a.remove();
|
||||
});
|
||||
|
||||
// Remove links that contains text that starts with https?://
|
||||
dom.querySelectorAll('a').forEach((a) => {
|
||||
const text = a.innerText.trim();
|
||||
if (text.startsWith('https://') || text.startsWith('http://')) {
|
||||
a.remove();
|
||||
}
|
||||
});
|
||||
},
|
||||
}),
|
||||
);
|
||||
setLanguageAutoDetected(detected);
|
||||
}, 1000);
|
||||
return () => clearTimeout(timer);
|
||||
}, [content, _language]);
|
||||
const language = _language || languageAutoDetected;
|
||||
|
||||
// if (!mediaAttachments?.length) mediaFirst = false;
|
||||
const hasMediaAttachments = !!mediaAttachments?.length;
|
||||
if (mediaFirst && hasMediaAttachments) size = 's';
|
||||
|
@ -524,9 +582,9 @@ function Status({
|
|||
});
|
||||
if (newWin) return;
|
||||
}
|
||||
states.showCompose = {
|
||||
showCompose({
|
||||
replyToStatus: status,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
// Check if media has no descriptions
|
||||
|
@ -771,11 +829,11 @@ function Status({
|
|||
menuExtras={
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
states.showCompose = {
|
||||
showCompose({
|
||||
draftStatus: {
|
||||
status: `\n${url}`,
|
||||
},
|
||||
};
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Icon icon="quote" />
|
||||
|
@ -1092,9 +1150,9 @@ function Status({
|
|||
{supports('@mastodon/post-edit') && (
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
states.showCompose = {
|
||||
showCompose({
|
||||
editStatus: status,
|
||||
};
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Icon icon="pencil" />
|
||||
|
@ -1897,6 +1955,7 @@ function Status({
|
|||
forceTranslate={forceTranslate || inlineTranslate}
|
||||
mini={!isSizeLarge && !withinContext}
|
||||
sourceLanguage={language}
|
||||
autoDetected={languageAutoDetected}
|
||||
text={getPostText(status)}
|
||||
/>
|
||||
)}
|
||||
|
@ -2125,11 +2184,11 @@ function Status({
|
|||
menuExtras={
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
states.showCompose = {
|
||||
showCompose({
|
||||
draftStatus: {
|
||||
status: `\n${url}`,
|
||||
},
|
||||
};
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Icon icon="quote" />
|
||||
|
@ -3124,7 +3183,7 @@ function StatusCompact({ sKey }) {
|
|||
const {
|
||||
sensitive,
|
||||
spoilerText,
|
||||
account: { avatar, avatarStatic, bot },
|
||||
account: { avatar, avatarStatic, bot } = {},
|
||||
visibility,
|
||||
content,
|
||||
language,
|
||||
|
|
|
@ -77,6 +77,7 @@ function TranslationBlock({
|
|||
onTranslate,
|
||||
text = '',
|
||||
mini,
|
||||
autoDetected,
|
||||
}) {
|
||||
const targetLang = getTranslateTargetLanguage(true);
|
||||
const [uiState, setUIState] = useState('default');
|
||||
|
@ -187,7 +188,9 @@ function TranslationBlock({
|
|||
{uiState === 'loading'
|
||||
? 'Translating…'
|
||||
: sourceLanguage && sourceLangText && !detectedLang
|
||||
? `Translate from ${sourceLangText}`
|
||||
? autoDetected
|
||||
? `Translate from ${sourceLangText} (auto-detected)`
|
||||
: `Translate from ${sourceLangText}`
|
||||
: `Translate`}
|
||||
</span>
|
||||
</button>
|
||||
|
|
|
@ -3,16 +3,12 @@ import './index.css';
|
|||
import './app.css';
|
||||
|
||||
import { render } from 'preact';
|
||||
import { lazy } from 'preact/compat';
|
||||
import { useEffect, useState } from 'preact/hooks';
|
||||
|
||||
import IntlSegmenterSuspense from './components/intl-segmenter-suspense';
|
||||
import ComposeSuspense from './components/compose-suspense';
|
||||
import { initStates } from './utils/states';
|
||||
// import Compose from './components/compose';
|
||||
import useTitle from './utils/useTitle';
|
||||
|
||||
const Compose = lazy(() => import('./components/compose'));
|
||||
|
||||
if (window.opener) {
|
||||
console = window.opener.console;
|
||||
}
|
||||
|
@ -66,25 +62,23 @@ function App() {
|
|||
console.debug('OPEN COMPOSE');
|
||||
|
||||
return (
|
||||
<IntlSegmenterSuspense>
|
||||
<Compose
|
||||
editStatus={editStatus}
|
||||
replyToStatus={replyToStatus}
|
||||
draftStatus={draftStatus}
|
||||
standalone
|
||||
hasOpener={window.opener}
|
||||
onClose={(results) => {
|
||||
const { newStatus, fn = () => {} } = results || {};
|
||||
try {
|
||||
if (newStatus) {
|
||||
window.opener.__STATES__.reloadStatusPage++;
|
||||
}
|
||||
fn();
|
||||
setUIState('closed');
|
||||
} catch (e) {}
|
||||
}}
|
||||
/>
|
||||
</IntlSegmenterSuspense>
|
||||
<ComposeSuspense
|
||||
editStatus={editStatus}
|
||||
replyToStatus={replyToStatus}
|
||||
draftStatus={draftStatus}
|
||||
standalone
|
||||
hasOpener={window.opener}
|
||||
onClose={(results) => {
|
||||
const { newStatus, fn = () => {} } = results || {};
|
||||
try {
|
||||
if (newStatus) {
|
||||
window.opener.__STATES__.reloadStatusPage++;
|
||||
}
|
||||
fn();
|
||||
setUIState('closed');
|
||||
} catch (e) {}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"@mastodon/edit-media-attributes": ">=4.1",
|
||||
"@mastodon/list-exclusive": ">=4.2",
|
||||
"@mastodon/filtered-notifications": "~4.3 || >=4.3"
|
||||
"@mastodon/filtered-notifications": "~4.3 || >=4.3",
|
||||
"@mastodon/fetch-multiple-statuses": "~4.3 || >=4.3"
|
||||
}
|
||||
|
|
|
@ -547,3 +547,9 @@ kbd {
|
|||
.shazam-container-horizontal[hidden] {
|
||||
grid-template-columns: 0fr;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,6 +41,7 @@ import states, { statusKey } from '../utils/states';
|
|||
import statusPeek from '../utils/status-peek';
|
||||
import store from '../utils/store';
|
||||
import { getCurrentAccountID, getCurrentAccountNS } from '../utils/store-utils';
|
||||
import supports from '../utils/supports';
|
||||
import { assignFollowedTags } from '../utils/timeline-utils';
|
||||
import useTitle from '../utils/useTitle';
|
||||
|
||||
|
@ -116,6 +117,8 @@ function Catchup() {
|
|||
}, []);
|
||||
const isSelf = (accountID) => accountID === currentAccount;
|
||||
|
||||
const supportsPixelfed = supports('@pixelfed/home-include-reblogs');
|
||||
|
||||
async function fetchHome({ maxCreatedAt }) {
|
||||
const maxCreatedAtDate = maxCreatedAt ? new Date(maxCreatedAt) : null;
|
||||
console.debug('fetchHome', maxCreatedAtDate);
|
||||
|
@ -123,6 +126,13 @@ function Catchup() {
|
|||
const homeIterator = masto.v1.timelines.home.list({ limit: 40 });
|
||||
mainloop: while (true) {
|
||||
try {
|
||||
if (supportsPixelfed && homeIterator.nextParams) {
|
||||
if (typeof homeIterator.nextParams === 'string') {
|
||||
homeIterator.nextParams += '&include_reblogs=true';
|
||||
} else {
|
||||
homeIterator.nextParams.include_reblogs = true;
|
||||
}
|
||||
}
|
||||
const results = await homeIterator.next();
|
||||
const { value } = results;
|
||||
if (value?.length) {
|
||||
|
@ -1677,63 +1687,70 @@ function PostPeek({ post, filterInfo }) {
|
|||
} = post;
|
||||
const isThread =
|
||||
(inReplyToId && inReplyToAccountId === account.id) || !!_thread;
|
||||
const showMedia = !spoilerText && !sensitive;
|
||||
|
||||
const readingExpandSpoilers = useMemo(() => {
|
||||
const prefs = store.account.get('preferences') || {};
|
||||
return !!prefs['reading:expand:spoilers'];
|
||||
}, []);
|
||||
// const readingExpandSpoilers = true;
|
||||
const showMedia = readingExpandSpoilers || (!spoilerText && !sensitive);
|
||||
const postText = content ? statusPeek(post) : '';
|
||||
|
||||
const showPostContent = !spoilerText || readingExpandSpoilers;
|
||||
|
||||
return (
|
||||
<div class="post-peek" title={!spoilerText ? postText : ''}>
|
||||
<span class="post-peek-content">
|
||||
{isThread && !showPostContent && (
|
||||
<>
|
||||
<span class="post-peek-tag post-peek-thread">Thread</span>{' '}
|
||||
</>
|
||||
)}
|
||||
{!!filterInfo ? (
|
||||
<>
|
||||
{isThread && (
|
||||
<>
|
||||
<span class="post-peek-tag post-peek-thread">Thread</span>{' '}
|
||||
</>
|
||||
)}
|
||||
<span class="post-peek-filtered">
|
||||
Filtered{filterInfo?.titlesStr ? `: ${filterInfo.titlesStr}` : ''}
|
||||
</span>
|
||||
</>
|
||||
) : !!spoilerText ? (
|
||||
<>
|
||||
{isThread && (
|
||||
<>
|
||||
<span class="post-peek-tag post-peek-thread">Thread</span>{' '}
|
||||
</>
|
||||
)}
|
||||
<span class="post-peek-spoiler">
|
||||
<Icon icon="eye-close" /> {spoilerText}
|
||||
</span>
|
||||
</>
|
||||
<span class="post-peek-filtered">
|
||||
Filtered{filterInfo?.titlesStr ? `: ${filterInfo.titlesStr}` : ''}
|
||||
</span>
|
||||
) : (
|
||||
<div class="post-peek-html">
|
||||
{isThread && (
|
||||
<>
|
||||
<span class="post-peek-tag post-peek-thread">Thread</span>{' '}
|
||||
</>
|
||||
<>
|
||||
{!!spoilerText && (
|
||||
<span class="post-peek-spoiler">
|
||||
<Icon
|
||||
icon={`${readingExpandSpoilers ? 'eye-open' : 'eye-close'}`}
|
||||
/>{' '}
|
||||
{spoilerText}
|
||||
</span>
|
||||
)}
|
||||
{!!content && (
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: emojifyText(content, emojis),
|
||||
}}
|
||||
/>
|
||||
{showPostContent && (
|
||||
<div class="post-peek-html">
|
||||
{isThread && (
|
||||
<>
|
||||
<span class="post-peek-tag post-peek-thread">Thread</span>{' '}
|
||||
</>
|
||||
)}
|
||||
{!!content && (
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: emojifyText(content, emojis),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{!!poll?.options?.length &&
|
||||
poll.options.map((o) => (
|
||||
<div>
|
||||
{poll.multiple ? '▪️' : '•'} {o.title}
|
||||
</div>
|
||||
))}
|
||||
{!content &&
|
||||
mediaAttachments?.length === 1 &&
|
||||
mediaAttachments[0].description && (
|
||||
<>
|
||||
<span class="post-peek-tag post-peek-alt">ALT</span>{' '}
|
||||
<div>{mediaAttachments[0].description}</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!!poll?.options?.length &&
|
||||
poll.options.map((o) => (
|
||||
<div>
|
||||
{poll.multiple ? '▪️' : '•'} {o.title}
|
||||
</div>
|
||||
))}
|
||||
{!content &&
|
||||
mediaAttachments?.length === 1 &&
|
||||
mediaAttachments[0].description && (
|
||||
<>
|
||||
<span class="post-peek-tag post-peek-alt">ALT</span>{' '}
|
||||
<div>{mediaAttachments[0].description}</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
{!filterInfo && (
|
||||
|
|
|
@ -6,6 +6,7 @@ import { api } from '../utils/api';
|
|||
import { filteredItems } from '../utils/filters';
|
||||
import states from '../utils/states';
|
||||
import { getStatus, saveStatus } from '../utils/states';
|
||||
import supports from '../utils/supports';
|
||||
import {
|
||||
assignFollowedTags,
|
||||
clearFollowedTagsState,
|
||||
|
@ -23,11 +24,19 @@ function Following({ title, path, id, ...props }) {
|
|||
const latestItem = useRef();
|
||||
|
||||
console.debug('RENDER Following', title, id);
|
||||
const supportsPixelfed = supports('@pixelfed/home-include-reblogs');
|
||||
|
||||
async function fetchHome(firstLoad) {
|
||||
if (firstLoad || !homeIterator.current) {
|
||||
homeIterator.current = masto.v1.timelines.home.list({ limit: LIMIT });
|
||||
}
|
||||
if (supportsPixelfed && homeIterator.current?.nextParams) {
|
||||
if (typeof homeIterator.current.nextParams === 'string') {
|
||||
homeIterator.current.nextParams += '&include_reblogs=true';
|
||||
} else {
|
||||
homeIterator.current.nextParams.include_reblogs = true;
|
||||
}
|
||||
}
|
||||
const results = await homeIterator.current.next();
|
||||
let { value } = results;
|
||||
if (value?.length) {
|
||||
|
@ -63,12 +72,14 @@ function Following({ title, path, id, ...props }) {
|
|||
|
||||
async function checkForUpdates() {
|
||||
try {
|
||||
const results = await masto.v1.timelines.home
|
||||
.list({
|
||||
limit: 5,
|
||||
since_id: latestItem.current,
|
||||
})
|
||||
.next();
|
||||
const opts = {
|
||||
limit: 5,
|
||||
since_id: latestItem.current,
|
||||
};
|
||||
if (supports('@pixelfed/home-include-reblogs')) {
|
||||
opts.include_reblogs = true;
|
||||
}
|
||||
const results = await masto.v1.timelines.home.list(opts).next();
|
||||
let { value } = results;
|
||||
console.log('checkForUpdates', latestItem.current, value);
|
||||
const valueContainsLatestItem = value[0]?.id === latestItem.current; // since_id might not be supported
|
||||
|
|
|
@ -84,7 +84,7 @@ function NotificationsLink() {
|
|||
);
|
||||
}
|
||||
|
||||
const NOTIFICATIONS_LIMIT = 30;
|
||||
const NOTIFICATIONS_LIMIT = 80;
|
||||
const NOTIFICATIONS_DISPLAY_LIMIT = 5;
|
||||
function NotificationsMenu({ anchorRef, state, onClose }) {
|
||||
const { masto, instance } = api();
|
||||
|
|
|
@ -33,7 +33,7 @@ import usePageVisibility from '../utils/usePageVisibility';
|
|||
import useScroll from '../utils/useScroll';
|
||||
import useTitle from '../utils/useTitle';
|
||||
|
||||
const LIMIT = 30; // 30 is the maximum limit :(
|
||||
const LIMIT = 80;
|
||||
const emptySearchParams = new URLSearchParams();
|
||||
|
||||
const scrollIntoViewOptions = {
|
||||
|
@ -292,8 +292,13 @@ function Notifications({ columnMode }) {
|
|||
}
|
||||
}
|
||||
});
|
||||
const firstLoad = useRef(true);
|
||||
useEffect(() => {
|
||||
let unsub = subscribeKey(states, 'notificationsShowNew', (v) => {
|
||||
if (firstLoad.current) {
|
||||
firstLoad.current = false;
|
||||
return;
|
||||
}
|
||||
if (uiState === 'loading') return;
|
||||
if (v) loadUpdates();
|
||||
setShowNew(v);
|
||||
|
|
|
@ -23,12 +23,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.hero-heading {
|
||||
font-size: var(--text-size);
|
||||
display: inline-block;
|
||||
|
|
|
@ -153,6 +153,18 @@ function StatusPage(params) {
|
|||
return () => clearTimeout(timer);
|
||||
}, [showMediaOnly]);
|
||||
|
||||
useEffect(() => {
|
||||
const $deckContainers = document.querySelectorAll('.deck-container');
|
||||
$deckContainers.forEach(($deckContainer) => {
|
||||
$deckContainer.setAttribute('inert', '');
|
||||
});
|
||||
return () => {
|
||||
$deckContainers.forEach(($deckContainer) => {
|
||||
$deckContainer.removeAttribute('inert');
|
||||
});
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div class="deck-backdrop">
|
||||
{showMedia ? (
|
||||
|
@ -972,6 +984,18 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
|
|||
[statuses, limit, renderStatus],
|
||||
);
|
||||
|
||||
// If there's spoiler in hero status, auto-expand it
|
||||
useEffect(() => {
|
||||
let timer = setTimeout(() => {
|
||||
if (!heroStatusRef.current) return;
|
||||
const spoilerButton = heroStatusRef.current.querySelector(
|
||||
'.spoiler-button:not(.spoiling), .spoiler-media-button:not(.spoiling)',
|
||||
);
|
||||
if (spoilerButton) spoilerButton.click();
|
||||
}, 1000);
|
||||
return () => clearTimeout(timer);
|
||||
}, [id]);
|
||||
|
||||
return (
|
||||
<div
|
||||
tabIndex="-1"
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import mem from './mem';
|
||||
|
||||
const div = document.createElement('div');
|
||||
function getHTMLText(html) {
|
||||
function getHTMLText(html, opts) {
|
||||
if (!html) return '';
|
||||
const { preProcess } = opts || {};
|
||||
|
||||
div.innerHTML = html
|
||||
.replace(/<\/p>/g, '</p>\n\n')
|
||||
.replace(/<\/li>/g, '</li>\n');
|
||||
|
@ -10,6 +12,8 @@ function getHTMLText(html) {
|
|||
br.replaceWith('\n');
|
||||
});
|
||||
|
||||
preProcess?.(div);
|
||||
|
||||
// MASTODON-SPECIFIC classes
|
||||
// Remove .invisible
|
||||
div.querySelectorAll('.invisible').forEach((el) => {
|
||||
|
|
27
src/utils/show-compose.js
Normal file
27
src/utils/show-compose.js
Normal file
|
@ -0,0 +1,27 @@
|
|||
import openOSK from './open-osk';
|
||||
import showToast from './show-toast';
|
||||
import states from './states';
|
||||
|
||||
const TOAST_DURATION = 5_000; // 5 seconds
|
||||
|
||||
export default function showCompose(opts) {
|
||||
if (!opts) opts = true;
|
||||
|
||||
if (states.showCompose) {
|
||||
if (states.composerState.minimized) {
|
||||
showToast({
|
||||
duration: TOAST_DURATION,
|
||||
text: `A draft post is currently minimized. Post or discard it before creating a new one.`,
|
||||
});
|
||||
} else {
|
||||
showToast({
|
||||
duration: TOAST_DURATION,
|
||||
text: `A post is currently open. Post or discard it before creating a new one.`,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
openOSK();
|
||||
states.showCompose = opts;
|
||||
}
|
|
@ -40,6 +40,7 @@ const states = proxy({
|
|||
statusReply: {},
|
||||
accounts: {},
|
||||
routeNotification: null,
|
||||
composerState: {},
|
||||
// Modals
|
||||
showCompose: false,
|
||||
showSettings: false,
|
||||
|
|
|
@ -18,6 +18,7 @@ const platformFeatures = {
|
|||
'@mastodon/profile-edit': notContainPixelfed,
|
||||
'@mastodon/profile-private-note': notContainPixelfed,
|
||||
'@pixelfed/trending': containPixelfed,
|
||||
'@pixelfed/home-include-reblogs': containPixelfed,
|
||||
};
|
||||
const supportsCache = {};
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import pmem from './pmem';
|
|||
import { fetchRelationships } from './relationships';
|
||||
import states, { saveStatus, statusKey } from './states';
|
||||
import store from './store';
|
||||
import supports from './supports';
|
||||
|
||||
export function groupBoosts(values) {
|
||||
let newValues = [];
|
||||
|
@ -149,6 +150,7 @@ export function groupContext(items, instance) {
|
|||
|
||||
const newItems = [];
|
||||
const appliedContextIndices = [];
|
||||
const inReplyToIds = [];
|
||||
items.forEach((item) => {
|
||||
if (item.reblog) {
|
||||
newItems.push(item);
|
||||
|
@ -176,17 +178,53 @@ export function groupContext(items, instance) {
|
|||
}
|
||||
}
|
||||
|
||||
// PREPARE FOR REPLY HINTS
|
||||
if (item.inReplyToId && item.inReplyToAccountId !== item.account.id) {
|
||||
const sKey = statusKey(item.id, instance);
|
||||
if (!states.statusReply[sKey]) {
|
||||
// If it's a reply and not a thread
|
||||
queueMicrotask(async () => {
|
||||
inReplyToIds.push({
|
||||
sKey,
|
||||
inReplyToId: item.inReplyToId,
|
||||
});
|
||||
// queueMicrotask(async () => {
|
||||
// try {
|
||||
// const { masto } = api({ instance });
|
||||
// // const replyToStatus = await masto.v1.statuses
|
||||
// // .$select(item.inReplyToId)
|
||||
// // .fetch();
|
||||
// const replyToStatus = await fetchStatus(item.inReplyToId, masto);
|
||||
// saveStatus(replyToStatus, instance, {
|
||||
// skipThreading: true,
|
||||
// skipUnfurling: true,
|
||||
// });
|
||||
// states.statusReply[sKey] = {
|
||||
// id: replyToStatus.id,
|
||||
// instance,
|
||||
// };
|
||||
// } catch (e) {
|
||||
// // Silently fail
|
||||
// console.error(e);
|
||||
// }
|
||||
// });
|
||||
}
|
||||
}
|
||||
|
||||
newItems.push(item);
|
||||
});
|
||||
|
||||
// FETCH AND SHOW REPLY HINTS
|
||||
if (inReplyToIds?.length) {
|
||||
queueMicrotask(() => {
|
||||
const { masto } = api({ instance });
|
||||
console.log('REPLYHINT', inReplyToIds);
|
||||
|
||||
// Fallback if batch fetch fails or returns nothing or not supported
|
||||
async function fallbackFetch() {
|
||||
for (let i = 0; i < inReplyToIds.length; i++) {
|
||||
const { sKey, inReplyToId } = inReplyToIds[i];
|
||||
try {
|
||||
const { masto } = api({ instance });
|
||||
// const replyToStatus = await masto.v1.statuses
|
||||
// .$select(item.inReplyToId)
|
||||
// .fetch();
|
||||
const replyToStatus = await fetchStatus(item.inReplyToId, masto);
|
||||
const replyToStatus = await fetchStatus(inReplyToId, masto);
|
||||
saveStatus(replyToStatus, instance, {
|
||||
skipThreading: true,
|
||||
skipUnfurling: true,
|
||||
|
@ -195,16 +233,52 @@ export function groupContext(items, instance) {
|
|||
id: replyToStatus.id,
|
||||
instance,
|
||||
};
|
||||
// Pause 1s
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
} catch (e) {
|
||||
// Silently fail
|
||||
console.error(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
newItems.push(item);
|
||||
});
|
||||
if (supports('@mastodon/fetch-multiple-statuses')) {
|
||||
// This is batch fetching yooo, woot
|
||||
// Limit 20, returns 422 if exceeded https://github.com/mastodon/mastodon/pull/27871
|
||||
const ids = inReplyToIds.map(({ inReplyToId }) => inReplyToId);
|
||||
(async () => {
|
||||
try {
|
||||
const replyToStatuses = await masto.v1.statuses.list({ id: ids });
|
||||
if (replyToStatuses?.length) {
|
||||
for (const replyToStatus of replyToStatuses) {
|
||||
saveStatus(replyToStatus, instance, {
|
||||
skipThreading: true,
|
||||
skipUnfurling: true,
|
||||
});
|
||||
const sKey = inReplyToIds.find(
|
||||
({ inReplyToId }) => inReplyToId === replyToStatus.id,
|
||||
)?.sKey;
|
||||
if (sKey) {
|
||||
states.statusReply[sKey] = {
|
||||
id: replyToStatus.id,
|
||||
instance,
|
||||
};
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fallbackFetch();
|
||||
}
|
||||
} catch (e) {
|
||||
// Silently fail
|
||||
console.error(e);
|
||||
fallbackFetch();
|
||||
}
|
||||
})();
|
||||
} else {
|
||||
fallbackFetch();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return newItems;
|
||||
}
|
||||
|
|
|
@ -9,6 +9,20 @@ export const throttle = pThrottle({
|
|||
interval: 1000,
|
||||
});
|
||||
|
||||
const STATUS_ID_REGEXES = [
|
||||
/\/@[^@\/]+@?[^\/]+?\/(\d+)$/i, // Mastodon
|
||||
/\/notice\/(\w+)$/i, // Pleroma
|
||||
];
|
||||
function getStatusID(path) {
|
||||
for (let i = 0; i < STATUS_ID_REGEXES.length; i++) {
|
||||
const statusMatchID = path.match(STATUS_ID_REGEXES[i])?.[1];
|
||||
if (statusMatchID) {
|
||||
return statusMatchID;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const denylistDomains = /(twitter|github)\.com/i;
|
||||
const failedUnfurls = {};
|
||||
function _unfurlMastodonLink(instance, url) {
|
||||
|
@ -53,11 +67,11 @@ function _unfurlMastodonLink(instance, url) {
|
|||
}
|
||||
const domain = urlObj.hostname;
|
||||
const path = urlObj.pathname;
|
||||
// Regex /:username/:id, where username = @username or @username@domain, id = number
|
||||
const statusRegex = /\/@([^@\/]+)@?([^\/]+)?\/(\d+)$/i;
|
||||
const statusMatch = statusRegex.exec(path);
|
||||
if (statusMatch) {
|
||||
const id = statusMatch[3];
|
||||
// Regex /:username/:id, where username = @username or @username@domain, id = post ID
|
||||
let statusMatchID = getStatusID(path);
|
||||
|
||||
if (statusMatchID) {
|
||||
const id = statusMatchID;
|
||||
const { masto } = api({ instance: domain });
|
||||
remoteInstanceFetch = masto.v1.statuses
|
||||
.$select(id)
|
||||
|
|
Loading…
Reference in a new issue