1
0
Fork 0

Compare commits

...

44 commits

Author SHA1 Message Date
Alexander Yakovlev a4240732d6
Merge remote-tracking branch 'origin/main' 2024-06-04 10:28:55 +06:00
Chee Aun 6d7eddc568
Merge pull request #557 from kevquirk/patch-1
Added social.qrk.one to self-hosted instances
2024-06-03 22:37:31 +08:00
Kev Quirk dac2af4334
Added social.qrk.one to self-hosted instances 2024-06-03 14:54:09 +01:00
Lim Chee Aun 2099953b68 Remove spaces between buttons 2024-06-03 18:01:49 +08:00
Lim Chee Aun 5931ebb8fc Reduce visual clutter for grouped notification
30 instead of 50 as limit. No more tiny avatars as they don't help much.
2024-06-02 22:52:47 +08:00
Lim Chee Aun adcb87679b Upgrade dependencies
Try bump text-expander too as it might have fixed its bugs
2024-06-01 16:33:13 +08:00
Lim Chee Aun 5ead17a093 Disable GIF button if exceed max media limit or has poll 2024-06-01 11:51:58 +08:00
Lim Chee Aun 224cad4d7f Utilise the new batch fetch on Mastodon v4.3 2024-05-31 17:11:40 +08:00
Lim Chee Aun e08817d611 Attempt to rewrite this part 2024-05-31 16:56:13 +08:00
Lim Chee Aun 1ffc1c257a Use setTimeout instead 2024-05-29 18:46:42 +08:00
Lim Chee Aun 098014a109 Fix possible error 2024-05-29 18:46:14 +08:00
Lim Chee Aun 7546b42c7c Further improve lang detection perf 2024-05-29 15:26:58 +08:00
Lim Chee Aun f9a73777e7 Perf over function 2024-05-29 10:23:46 +08:00
Lim Chee Aun d5584f8dd4 Delay preload 2024-05-29 08:58:17 +08:00
Lim Chee Aun 563b06e680 Break the tasks 2024-05-28 22:22:14 +08:00
Lim Chee Aun b6a64b66c7 Fix wrong logic for highlighting Languages select 2024-05-28 21:03:05 +08:00
Lim Chee Aun 0a4aae51b7 It's time for MVP-ish language auto-detection 2024-05-28 17:59:17 +08:00
Lim Chee Aun d16221e296 Test fix Pixelfed home timeline not showing reblogs 2024-05-28 13:44:24 +08:00
Lim Chee Aun ed712d15f1 Test fix notification toast appearing after loaded 2024-05-28 13:44:02 +08:00
Lim Chee Aun bd8817e61b Show warning if exceed file size or matrix limit 2024-05-27 19:19:34 +08:00
Lim Chee Aun ef712c62a9 Add one more username ≈ display name logic 2024-05-27 19:02:19 +08:00
Lim Chee Aun 9aa2bac685 Try fix toast width again 2024-05-27 19:01:41 +08:00
Lim Chee Aun 34077e8467 Don't show 'More…' for hashtag autosuggest 2024-05-26 18:15:37 +08:00
Lim Chee Aun b473061845 Show compose button above post modal when minimized 2024-05-26 00:13:20 +08:00
Lim Chee Aun 64c7b5b4f0 Rewrite polyfill suspense for Composer with preload
Hopefully this works
2024-05-25 20:43:15 +08:00
Lim Chee Aun c11bbbb2b3 Handle modifiers when clicking on account links 2024-05-25 13:52:25 +08:00
Lim Chee Aun 2c1a6c8cb5 Restyle the composer controls UI 2024-05-25 13:39:11 +08:00
Lim Chee Aun 67a85e1eef Forgot Mobile Safari always need 16px for input fields 2024-05-25 13:16:22 +08:00
Lim Chee Aun 2e0ef6494b Extend at-mentions with dedicated UI 2024-05-25 11:06:58 +08:00
Lim Chee Aun 012b86d7ce Try not hide compose button if loading 2024-05-25 11:06:03 +08:00
Lim Chee Aun 0c45f515f0 Don't add space if empty string 2024-05-25 09:16:03 +08:00
Lim Chee Aun 9cc590be1b Extra check if the composer is publishing 2024-05-25 09:15:43 +08:00
Lim Chee Aun 7589ec8803 Downgrade text-expander
Possibly might fix autosuggest position bug on Mobile Safari
2024-05-25 09:15:13 +08:00
Lim Chee Aun cd17ca0b42 Experiment: allow minimize composer 2024-05-24 12:30:20 +08:00
Lim Chee Aun 8aab997900 Upgrade dependencies 2024-05-23 20:25:14 +08:00
Lim Chee Aun 96c44ed485 Fix composer not overwritten by restored composer window 2024-05-23 14:14:23 +08:00
Lim Chee Aun 7053fcc96a Experimental 'More…' for custom emojis suggestions
Also includes small fixes and improvements
2024-05-22 19:12:13 +08:00
Lim Chee Aun ad7cb46547 Experiment auto-expand spoiler in hero status 2024-05-19 18:46:27 +08:00
Lim Chee Aun 1b1af67064 Experiment non-English description generation 2024-05-19 16:27:59 +08:00
Lim Chee Aun bdd238de0e Test using inert to control text searchability 2024-05-19 16:26:15 +08:00
Lim Chee Aun ced4dc86aa Forgot passing blankCopy 2024-05-19 16:24:29 +08:00
Lim Chee Aun 7be1e589ab Support Pleroma's /notice unfurl 2024-05-19 16:23:12 +08:00
Lim Chee Aun 7da1745cca Respect expand spoiler setting in Catchup 2024-05-19 16:22:18 +08:00
Lim Chee Aun 025a5429cc Set limit to 80 for notifications 2024-05-17 18:32:12 +08:00
36 changed files with 1560 additions and 325 deletions

View file

@ -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
View file

@ -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",

View file

@ -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",

View file

@ -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);

View file

@ -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);

View file

@ -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'),
};

View file

@ -133,21 +133,18 @@ function AccountBlock({
)}
</span>
{showActivity && (
<>
<br />
<small class="last-status-at insignificant">
Posts: {statusesCount}
{!!lastStatusAt && (
<>
{' '}
&middot; Last posted:{' '}
{niceDateTime(lastStatusAt, {
hideTime: true,
})}
</>
)}
</small>
</>
<div class="account-block-stats">
Posts: {shortenNumber(statusesCount)}
{!!lastStatusAt && (
<>
{' '}
&middot; Last posted:{' '}
{niceDateTime(lastStatusAt, {
hideTime: true,
})}
</>
)}
</div>
)}
{showStats && (
<div class="account-block-stats">

View file

@ -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" />

View file

@ -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);
}}
/>
)}

View file

@ -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>
);

View 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 />;
}

View file

@ -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

View file

@ -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

View file

@ -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
);
}
}

View file

@ -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) {

View file

@ -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>
)}

View file

@ -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,

View file

@ -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}`}

View file

@ -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,

View file

@ -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>

View file

@ -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) {}
}}
/>
);
}

View file

@ -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"
}

View file

@ -547,3 +547,9 @@ kbd {
.shazam-container-horizontal[hidden] {
grid-template-columns: 0fr;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}

View file

@ -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 && (

View file

@ -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

View file

@ -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();

View file

@ -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);

View file

@ -23,12 +23,6 @@
}
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.hero-heading {
font-size: var(--text-size);
display: inline-block;

View file

@ -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"

View file

@ -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
View 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;
}

View file

@ -40,6 +40,7 @@ const states = proxy({
statusReply: {},
accounts: {},
routeNotification: null,
composerState: {},
// Modals
showCompose: false,
showSettings: false,

View file

@ -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 = {};

View file

@ -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;
}

View file

@ -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)