mirror of
https://github.com/cheeaun/phanpy.git
synced 2025-02-02 14:16:39 +01:00
commit
74991c326d
55 changed files with 2447 additions and 1143 deletions
179
package-lock.json
generated
179
package-lock.json
generated
|
@ -15,6 +15,7 @@
|
|||
"@justinribeiro/lite-youtube": "~1.5.0",
|
||||
"@szhsin/react-menu": "~4.1.0",
|
||||
"@uidotdev/usehooks": "~2.4.1",
|
||||
"compare-versions": "~6.1.0",
|
||||
"dayjs": "~1.11.10",
|
||||
"dayjs-twitter": "~0.5.0",
|
||||
"fast-blurhash": "~1.1.2",
|
||||
|
@ -22,11 +23,11 @@
|
|||
"idb-keyval": "~6.2.1",
|
||||
"just-debounce-it": "~3.2.0",
|
||||
"lz-string": "~1.5.0",
|
||||
"masto": "~6.3.3",
|
||||
"masto": "~6.4.2",
|
||||
"moize": "~6.1.6",
|
||||
"p-retry": "~6.1.0",
|
||||
"p-throttle": "~5.1.0",
|
||||
"preact": "~10.18.1",
|
||||
"preact": "~10.18.2",
|
||||
"react-hotkeys-hook": "~4.4.1",
|
||||
"react-intersection-observer": "~9.5.2",
|
||||
"react-quick-pinch-zoom": "~5.0.0",
|
||||
|
@ -45,12 +46,12 @@
|
|||
"@trivago/prettier-plugin-sort-imports": "~4.2.1",
|
||||
"postcss": "~8.4.31",
|
||||
"postcss-dark-theme-class": "~1.0.0",
|
||||
"postcss-preset-env": "~9.2.0",
|
||||
"postcss-preset-env": "~9.3.0",
|
||||
"twitter-text": "~3.1.0",
|
||||
"vite": "~4.5.0",
|
||||
"vite-plugin-generate-file": "~0.0.4",
|
||||
"vite-plugin-html-config": "~1.0.11",
|
||||
"vite-plugin-pwa": "~0.16.5",
|
||||
"vite-plugin-pwa": "~0.16.6",
|
||||
"vite-plugin-remove-console": "~2.1.1",
|
||||
"workbox-cacheable-response": "~7.0.0",
|
||||
"workbox-expiration": "~7.0.0",
|
||||
|
@ -1988,9 +1989,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@csstools/postcss-cascade-layers": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-4.0.0.tgz",
|
||||
"integrity": "sha512-dVPVVqQG0FixjM9CG/+8eHTsCAxRKqmNh6H69IpruolPlnEF1611f2AoLK8TijTSAsqBSclKd4WHs1KUb/LdJw==",
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-4.0.1.tgz",
|
||||
"integrity": "sha512-UYFuFL9GgVnftg9v7tBvVEBRLaBeAD66euD+yYy5fYCUld9ZIWTJNCE30hm6STMEdt6FL5xzeVw1lAZ1tpvUEg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
@ -2299,6 +2300,50 @@
|
|||
"postcss": "^8.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/postcss-logical-overflow": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/postcss-logical-overflow/-/postcss-logical-overflow-1.0.0.tgz",
|
||||
"integrity": "sha512-cIrZ8f7bGGvr+W53nEuMspcwaeaI2YTmz6LZ4yiAO5z14/PQgOOv+Pn+qjvPOPoadeY2BmpaoTzZKvdAQuM17w==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"engines": {
|
||||
"node": "^14 || ^16 || >=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"postcss": "^8.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/postcss-logical-overscroll-behavior": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/postcss-logical-overscroll-behavior/-/postcss-logical-overscroll-behavior-1.0.0.tgz",
|
||||
"integrity": "sha512-e89S2LWjnxf0SB2wNUAbqDyFb/Fow/tlOe1XqOLbNx4rf3LrQokM9qldVx7sarnddml3ORE5LDUmlKpPOOeJTA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"engines": {
|
||||
"node": "^14 || ^16 || >=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"postcss": "^8.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/postcss-logical-resize": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/postcss-logical-resize/-/postcss-logical-resize-2.0.0.tgz",
|
||||
|
@ -3870,6 +3915,11 @@
|
|||
"node": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/compare-versions": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-6.1.0.tgz",
|
||||
"integrity": "sha512-LNZQXhqUvqUTotpZ00qLSaify3b4VFD588aRr8MKFw4CMUr98ytzCW5wDH5qx/DEY5kCDXcbcRuCqL0szEf2tg=="
|
||||
},
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
|
@ -3997,9 +4047,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/cssdb": {
|
||||
"version": "7.8.0",
|
||||
"resolved": "https://registry.npmjs.org/cssdb/-/cssdb-7.8.0.tgz",
|
||||
"integrity": "sha512-SkeezZOQr5AHt9MgJgSFNyiuJwg1p8AwoVln6JwaQJsyxduRW9QJ+HP/gAQzbsz8SIqINtYvpJKjxTRI67zxLg==",
|
||||
"version": "7.9.0",
|
||||
"resolved": "https://registry.npmjs.org/cssdb/-/cssdb-7.9.0.tgz",
|
||||
"integrity": "sha512-WPMT9seTQq6fPAa1yN4zjgZZeoTriSN2LqW9C+otjar12DQIWA4LuSfFrvFJiKp4oD0xIk1vumDLw8K9ur4NBw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
@ -4264,9 +4314,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/events-to-async": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/events-to-async/-/events-to-async-2.0.0.tgz",
|
||||
"integrity": "sha512-NiZEr4g51nI4/lz/6NdwMqK/TLIctlnp9TQ3wCJjlRp47VgrthUZE4nrk2UhfZ8VzoQ/Xyth+G6MKioLCt0FVA=="
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/events-to-async/-/events-to-async-2.0.1.tgz",
|
||||
"integrity": "sha512-RtnLYrMbXp4JkZIoZu+3VTqV21bNVBlJBZ4NmtwvMNqSE3qouhxv2gvLE4JJDaQc54ioPkrX74V6x+hp/hqjkQ=="
|
||||
},
|
||||
"node_modules/fast-blurhash": {
|
||||
"version": "1.1.2",
|
||||
|
@ -5257,12 +5307,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/masto": {
|
||||
"version": "6.3.3",
|
||||
"resolved": "https://registry.npmjs.org/masto/-/masto-6.3.3.tgz",
|
||||
"integrity": "sha512-hmDsiscImeZfpkS+5oEWk3w5mkbxERFKN/UpuaoKZpVWWoGWCNnO7iPfQHygs/phP7PQqS6pVHlE5ylqSylf6A==",
|
||||
"version": "6.4.2",
|
||||
"resolved": "https://registry.npmjs.org/masto/-/masto-6.4.2.tgz",
|
||||
"integrity": "sha512-aIxhsTkl0pc755Sf9NMuglY0VM9HuU/tec60e4oLsyyzAkE7ELwufh6anErO84n8g3eWu/u0LGucTujIiDaAhg==",
|
||||
"dependencies": {
|
||||
"change-case": "^4.1.2",
|
||||
"events-to-async": "^2.0.0",
|
||||
"events-to-async": "^2.0.1",
|
||||
"isomorphic-ws": "^5.0.0",
|
||||
"ts-custom-error": "^3.3.1",
|
||||
"ws": "^8.13.0"
|
||||
|
@ -6087,9 +6137,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/postcss-preset-env": {
|
||||
"version": "9.2.0",
|
||||
"resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-9.2.0.tgz",
|
||||
"integrity": "sha512-Lnr4C5gb7t5Cc8akQMJzNdJkqw7s7s7BHUaQSgsuf+CTY9Lsz5lqQTft5yNZr59JyCLz0aFNSAqSLm/xRtcTpg==",
|
||||
"version": "9.3.0",
|
||||
"resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-9.3.0.tgz",
|
||||
"integrity": "sha512-ycw6doPrqV6QxDCtgiyGDef61bEfiSc59HGM4gOw/wxQxmKnhuEery61oOC/5ViENz/ycpRsuhTexs1kUBTvVw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
@ -6102,7 +6152,7 @@
|
|||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"@csstools/postcss-cascade-layers": "^4.0.0",
|
||||
"@csstools/postcss-cascade-layers": "^4.0.1",
|
||||
"@csstools/postcss-color-function": "^3.0.7",
|
||||
"@csstools/postcss-color-mix-function": "^2.0.7",
|
||||
"@csstools/postcss-exponential-functions": "^1.0.1",
|
||||
|
@ -6114,6 +6164,8 @@
|
|||
"@csstools/postcss-initial": "^1.0.0",
|
||||
"@csstools/postcss-is-pseudo-class": "^4.0.3",
|
||||
"@csstools/postcss-logical-float-and-clear": "^2.0.0",
|
||||
"@csstools/postcss-logical-overflow": "^1.0.0",
|
||||
"@csstools/postcss-logical-overscroll-behavior": "^1.0.0",
|
||||
"@csstools/postcss-logical-resize": "^2.0.0",
|
||||
"@csstools/postcss-logical-viewport-units": "^2.0.3",
|
||||
"@csstools/postcss-media-minmax": "^1.1.0",
|
||||
|
@ -6133,7 +6185,7 @@
|
|||
"css-blank-pseudo": "^6.0.0",
|
||||
"css-has-pseudo": "^6.0.0",
|
||||
"css-prefers-color-scheme": "^9.0.0",
|
||||
"cssdb": "^7.8.0",
|
||||
"cssdb": "^7.9.0",
|
||||
"postcss-attribute-case-insensitive": "^6.0.2",
|
||||
"postcss-clamp": "^4.1.0",
|
||||
"postcss-color-functional-notation": "^6.0.2",
|
||||
|
@ -6241,9 +6293,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"node_modules/preact": {
|
||||
"version": "10.18.1",
|
||||
"resolved": "https://registry.npmjs.org/preact/-/preact-10.18.1.tgz",
|
||||
"integrity": "sha512-mKUD7RRkQQM6s7Rkmi7IFkoEHjuFqRQUaXamO61E6Nn7vqF/bo7EZCmSyrUnp2UWHw0O7XjZ2eeXis+m7tf4lg==",
|
||||
"version": "10.18.2",
|
||||
"resolved": "https://registry.npmjs.org/preact/-/preact-10.18.2.tgz",
|
||||
"integrity": "sha512-X/K43vocUHDg0XhWVmTTMbec4LT/iBMh+csCEqJk+pJqegaXsvjdqN80ZZ3L+93azWCnWCZ+WGwYb8SplxeNjA==",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/preact"
|
||||
|
@ -7394,9 +7446,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/vite-plugin-pwa": {
|
||||
"version": "0.16.5",
|
||||
"resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.16.5.tgz",
|
||||
"integrity": "sha512-Ahol4dwhMP2UHPQXkllSlXbihOaDFnvBIDPmAxoSZ1EObBUJGP4CMRyCyAVkIHjd6/H+//vH0DM2ON+XxHr81g==",
|
||||
"version": "0.16.6",
|
||||
"resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.16.6.tgz",
|
||||
"integrity": "sha512-bQPDOWvhPMwydMoWqohXvIzvrq4X8iuCF+q95qEiaM4yC0ybViGKWMnWcpWp0vcnoLk7QvxHDlK65KUZvqB3Sg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"debug": "^4.3.4",
|
||||
|
@ -7412,7 +7464,7 @@
|
|||
"url": "https://github.com/sponsors/antfu"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vite": "^3.1.0 || ^4.0.0",
|
||||
"vite": "^3.1.0 || ^4.0.0 || ^5.0.0-0",
|
||||
"workbox-build": "^7.0.0",
|
||||
"workbox-window": "^7.0.0"
|
||||
}
|
||||
|
@ -9113,9 +9165,9 @@
|
|||
"requires": {}
|
||||
},
|
||||
"@csstools/postcss-cascade-layers": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-4.0.0.tgz",
|
||||
"integrity": "sha512-dVPVVqQG0FixjM9CG/+8eHTsCAxRKqmNh6H69IpruolPlnEF1611f2AoLK8TijTSAsqBSclKd4WHs1KUb/LdJw==",
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-4.0.1.tgz",
|
||||
"integrity": "sha512-UYFuFL9GgVnftg9v7tBvVEBRLaBeAD66euD+yYy5fYCUld9ZIWTJNCE30hm6STMEdt6FL5xzeVw1lAZ1tpvUEg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@csstools/selector-specificity": "^3.0.0",
|
||||
|
@ -9234,6 +9286,20 @@
|
|||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
"@csstools/postcss-logical-overflow": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/postcss-logical-overflow/-/postcss-logical-overflow-1.0.0.tgz",
|
||||
"integrity": "sha512-cIrZ8f7bGGvr+W53nEuMspcwaeaI2YTmz6LZ4yiAO5z14/PQgOOv+Pn+qjvPOPoadeY2BmpaoTzZKvdAQuM17w==",
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
"@csstools/postcss-logical-overscroll-behavior": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/postcss-logical-overscroll-behavior/-/postcss-logical-overscroll-behavior-1.0.0.tgz",
|
||||
"integrity": "sha512-e89S2LWjnxf0SB2wNUAbqDyFb/Fow/tlOe1XqOLbNx4rf3LrQokM9qldVx7sarnddml3ORE5LDUmlKpPOOeJTA==",
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
"@csstools/postcss-logical-resize": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/postcss-logical-resize/-/postcss-logical-resize-2.0.0.tgz",
|
||||
|
@ -10185,6 +10251,11 @@
|
|||
"integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==",
|
||||
"dev": true
|
||||
},
|
||||
"compare-versions": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-6.1.0.tgz",
|
||||
"integrity": "sha512-LNZQXhqUvqUTotpZ00qLSaify3b4VFD588aRr8MKFw4CMUr98ytzCW5wDH5qx/DEY5kCDXcbcRuCqL0szEf2tg=="
|
||||
},
|
||||
"concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
|
@ -10256,9 +10327,9 @@
|
|||
"requires": {}
|
||||
},
|
||||
"cssdb": {
|
||||
"version": "7.8.0",
|
||||
"resolved": "https://registry.npmjs.org/cssdb/-/cssdb-7.8.0.tgz",
|
||||
"integrity": "sha512-SkeezZOQr5AHt9MgJgSFNyiuJwg1p8AwoVln6JwaQJsyxduRW9QJ+HP/gAQzbsz8SIqINtYvpJKjxTRI67zxLg==",
|
||||
"version": "7.9.0",
|
||||
"resolved": "https://registry.npmjs.org/cssdb/-/cssdb-7.9.0.tgz",
|
||||
"integrity": "sha512-WPMT9seTQq6fPAa1yN4zjgZZeoTriSN2LqW9C+otjar12DQIWA4LuSfFrvFJiKp4oD0xIk1vumDLw8K9ur4NBw==",
|
||||
"dev": true
|
||||
},
|
||||
"cssesc": {
|
||||
|
@ -10453,9 +10524,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"events-to-async": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/events-to-async/-/events-to-async-2.0.0.tgz",
|
||||
"integrity": "sha512-NiZEr4g51nI4/lz/6NdwMqK/TLIctlnp9TQ3wCJjlRp47VgrthUZE4nrk2UhfZ8VzoQ/Xyth+G6MKioLCt0FVA=="
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/events-to-async/-/events-to-async-2.0.1.tgz",
|
||||
"integrity": "sha512-RtnLYrMbXp4JkZIoZu+3VTqV21bNVBlJBZ4NmtwvMNqSE3qouhxv2gvLE4JJDaQc54ioPkrX74V6x+hp/hqjkQ=="
|
||||
},
|
||||
"fast-blurhash": {
|
||||
"version": "1.1.2",
|
||||
|
@ -11188,12 +11259,12 @@
|
|||
}
|
||||
},
|
||||
"masto": {
|
||||
"version": "6.3.3",
|
||||
"resolved": "https://registry.npmjs.org/masto/-/masto-6.3.3.tgz",
|
||||
"integrity": "sha512-hmDsiscImeZfpkS+5oEWk3w5mkbxERFKN/UpuaoKZpVWWoGWCNnO7iPfQHygs/phP7PQqS6pVHlE5ylqSylf6A==",
|
||||
"version": "6.4.2",
|
||||
"resolved": "https://registry.npmjs.org/masto/-/masto-6.4.2.tgz",
|
||||
"integrity": "sha512-aIxhsTkl0pc755Sf9NMuglY0VM9HuU/tec60e4oLsyyzAkE7ELwufh6anErO84n8g3eWu/u0LGucTujIiDaAhg==",
|
||||
"requires": {
|
||||
"change-case": "^4.1.2",
|
||||
"events-to-async": "^2.0.0",
|
||||
"events-to-async": "^2.0.1",
|
||||
"isomorphic-ws": "^5.0.0",
|
||||
"ts-custom-error": "^3.3.1",
|
||||
"ws": "^8.13.0"
|
||||
|
@ -11620,12 +11691,12 @@
|
|||
}
|
||||
},
|
||||
"postcss-preset-env": {
|
||||
"version": "9.2.0",
|
||||
"resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-9.2.0.tgz",
|
||||
"integrity": "sha512-Lnr4C5gb7t5Cc8akQMJzNdJkqw7s7s7BHUaQSgsuf+CTY9Lsz5lqQTft5yNZr59JyCLz0aFNSAqSLm/xRtcTpg==",
|
||||
"version": "9.3.0",
|
||||
"resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-9.3.0.tgz",
|
||||
"integrity": "sha512-ycw6doPrqV6QxDCtgiyGDef61bEfiSc59HGM4gOw/wxQxmKnhuEery61oOC/5ViENz/ycpRsuhTexs1kUBTvVw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@csstools/postcss-cascade-layers": "^4.0.0",
|
||||
"@csstools/postcss-cascade-layers": "^4.0.1",
|
||||
"@csstools/postcss-color-function": "^3.0.7",
|
||||
"@csstools/postcss-color-mix-function": "^2.0.7",
|
||||
"@csstools/postcss-exponential-functions": "^1.0.1",
|
||||
|
@ -11637,6 +11708,8 @@
|
|||
"@csstools/postcss-initial": "^1.0.0",
|
||||
"@csstools/postcss-is-pseudo-class": "^4.0.3",
|
||||
"@csstools/postcss-logical-float-and-clear": "^2.0.0",
|
||||
"@csstools/postcss-logical-overflow": "^1.0.0",
|
||||
"@csstools/postcss-logical-overscroll-behavior": "^1.0.0",
|
||||
"@csstools/postcss-logical-resize": "^2.0.0",
|
||||
"@csstools/postcss-logical-viewport-units": "^2.0.3",
|
||||
"@csstools/postcss-media-minmax": "^1.1.0",
|
||||
|
@ -11656,7 +11729,7 @@
|
|||
"css-blank-pseudo": "^6.0.0",
|
||||
"css-has-pseudo": "^6.0.0",
|
||||
"css-prefers-color-scheme": "^9.0.0",
|
||||
"cssdb": "^7.8.0",
|
||||
"cssdb": "^7.9.0",
|
||||
"postcss-attribute-case-insensitive": "^6.0.2",
|
||||
"postcss-clamp": "^4.1.0",
|
||||
"postcss-color-functional-notation": "^6.0.2",
|
||||
|
@ -11727,9 +11800,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"preact": {
|
||||
"version": "10.18.1",
|
||||
"resolved": "https://registry.npmjs.org/preact/-/preact-10.18.1.tgz",
|
||||
"integrity": "sha512-mKUD7RRkQQM6s7Rkmi7IFkoEHjuFqRQUaXamO61E6Nn7vqF/bo7EZCmSyrUnp2UWHw0O7XjZ2eeXis+m7tf4lg=="
|
||||
"version": "10.18.2",
|
||||
"resolved": "https://registry.npmjs.org/preact/-/preact-10.18.2.tgz",
|
||||
"integrity": "sha512-X/K43vocUHDg0XhWVmTTMbec4LT/iBMh+csCEqJk+pJqegaXsvjdqN80ZZ3L+93azWCnWCZ+WGwYb8SplxeNjA=="
|
||||
},
|
||||
"prettier": {
|
||||
"version": "2.8.0",
|
||||
|
@ -12507,9 +12580,9 @@
|
|||
"requires": {}
|
||||
},
|
||||
"vite-plugin-pwa": {
|
||||
"version": "0.16.5",
|
||||
"resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.16.5.tgz",
|
||||
"integrity": "sha512-Ahol4dwhMP2UHPQXkllSlXbihOaDFnvBIDPmAxoSZ1EObBUJGP4CMRyCyAVkIHjd6/H+//vH0DM2ON+XxHr81g==",
|
||||
"version": "0.16.6",
|
||||
"resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.16.6.tgz",
|
||||
"integrity": "sha512-bQPDOWvhPMwydMoWqohXvIzvrq4X8iuCF+q95qEiaM4yC0ybViGKWMnWcpWp0vcnoLk7QvxHDlK65KUZvqB3Sg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"debug": "^4.3.4",
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
"@justinribeiro/lite-youtube": "~1.5.0",
|
||||
"@szhsin/react-menu": "~4.1.0",
|
||||
"@uidotdev/usehooks": "~2.4.1",
|
||||
"compare-versions": "~6.1.0",
|
||||
"dayjs": "~1.11.10",
|
||||
"dayjs-twitter": "~0.5.0",
|
||||
"fast-blurhash": "~1.1.2",
|
||||
|
@ -24,11 +25,11 @@
|
|||
"idb-keyval": "~6.2.1",
|
||||
"just-debounce-it": "~3.2.0",
|
||||
"lz-string": "~1.5.0",
|
||||
"masto": "~6.3.3",
|
||||
"masto": "~6.4.2",
|
||||
"moize": "~6.1.6",
|
||||
"p-retry": "~6.1.0",
|
||||
"p-throttle": "~5.1.0",
|
||||
"preact": "~10.18.1",
|
||||
"preact": "~10.18.2",
|
||||
"react-hotkeys-hook": "~4.4.1",
|
||||
"react-intersection-observer": "~9.5.2",
|
||||
"react-quick-pinch-zoom": "~5.0.0",
|
||||
|
@ -47,12 +48,12 @@
|
|||
"@trivago/prettier-plugin-sort-imports": "~4.2.1",
|
||||
"postcss": "~8.4.31",
|
||||
"postcss-dark-theme-class": "~1.0.0",
|
||||
"postcss-preset-env": "~9.2.0",
|
||||
"postcss-preset-env": "~9.3.0",
|
||||
"twitter-text": "~3.1.0",
|
||||
"vite": "~4.5.0",
|
||||
"vite-plugin-generate-file": "~0.0.4",
|
||||
"vite-plugin-html-config": "~1.0.11",
|
||||
"vite-plugin-pwa": "~0.16.5",
|
||||
"vite-plugin-pwa": "~0.16.6",
|
||||
"vite-plugin-remove-console": "~2.1.1",
|
||||
"workbox-cacheable-response": "~7.0.0",
|
||||
"workbox-expiration": "~7.0.0",
|
||||
|
|
200
src/app.css
200
src/app.css
|
@ -56,6 +56,7 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
|
|||
overscroll-behavior: contain;
|
||||
scroll-behavior: smooth;
|
||||
background-color: var(--bg-color);
|
||||
flex-grow: 1;
|
||||
/* This `transform` fixes carousel blocking vertical scrolling for pointer devices on iPad */
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
@ -79,6 +80,21 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
|
|||
scroll-padding-top: 3em;
|
||||
}
|
||||
|
||||
:is(#home-page, #welcome, #columns, #loader-root) ~ .deck-container {
|
||||
z-index: 10;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
}
|
||||
:is(#home-page, #welcome, #columns, #loader-root):has(~ .deck-container) {
|
||||
display: block;
|
||||
position: absolute;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
/* This causes scrollTop to be reset to 0 when the page is hidden */
|
||||
/* content-visibility: hidden; */
|
||||
}
|
||||
|
||||
.deck {
|
||||
min-height: 100vh;
|
||||
min-height: 100dvh;
|
||||
|
@ -110,6 +126,16 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
|
|||
user-select: none;
|
||||
transition: transform 0.5s ease-in-out;
|
||||
user-select: none;
|
||||
|
||||
.header-double-lines {
|
||||
font-size: 90% !important;
|
||||
cursor: pointer;
|
||||
|
||||
div {
|
||||
font-weight: normal;
|
||||
color: var(--text-insignificant-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
.deck > header[hidden] {
|
||||
display: block;
|
||||
|
@ -209,6 +235,64 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
|
|||
.timeline {
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
|
||||
&.timeline-media {
|
||||
--grid-gap: 8px;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
grid-auto-rows: fit-content;
|
||||
gap: var(--grid-gap);
|
||||
padding: var(--grid-gap);
|
||||
|
||||
&:not(#columns &) {
|
||||
background-color: var(--bg-faded-color);
|
||||
}
|
||||
|
||||
@media (min-width: 320px) {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
@media (min-width: 40em) {
|
||||
&:not(#columns &) {
|
||||
--grid-gap: 16px;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
|
||||
@media (min-width: 40em) {
|
||||
width: 95vw;
|
||||
max-width: calc(320px * 3.3);
|
||||
transform: translateX(calc(-50% + var(--main-width) / 2));
|
||||
}
|
||||
}
|
||||
|
||||
#columns & {
|
||||
padding-inline: 0;
|
||||
}
|
||||
}
|
||||
|
||||
li {
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
border: 0 !important;
|
||||
overflow: visible !important;
|
||||
background-color: transparent !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
@supports (grid-template-rows: masonry) {
|
||||
grid-template-rows: masonry;
|
||||
masonry-auto-flow: pack;
|
||||
|
||||
.media-post a {
|
||||
aspect-ratio: revert !important;
|
||||
|
||||
video,
|
||||
img,
|
||||
audio {
|
||||
min-height: 88px; /* for extreme dimensions */
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.timeline.grow {
|
||||
/* min-height: 100vh;
|
||||
|
@ -275,10 +359,7 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
|
|||
box-shadow: inset 0 -3px var(--comment-line-color),
|
||||
inset 0 3px var(--comment-line-color);
|
||||
overscroll-behavior-x: contain;
|
||||
|
||||
.status-link {
|
||||
width: fit-content;
|
||||
}
|
||||
touch-action: pan-x;
|
||||
}
|
||||
.timeline.contextual .replies[data-comments-level='4'] {
|
||||
overflow-x: auto;
|
||||
|
@ -471,7 +552,6 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
|
|||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
text-transform: uppercase;
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
color: var(--text-insignificant-color);
|
||||
user-select: none;
|
||||
|
@ -479,6 +559,11 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
|
|||
position: relative;
|
||||
list-style: none;
|
||||
white-space: nowrap;
|
||||
|
||||
b {
|
||||
font-weight: 500;
|
||||
color: var(--text-color);
|
||||
}
|
||||
}
|
||||
.timeline.contextual > li .replies > .replies-summary::-webkit-details-marker {
|
||||
display: none;
|
||||
|
@ -488,6 +573,10 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
|
|||
}
|
||||
.timeline.contextual > li .replies > .replies-summary .avatars {
|
||||
margin-right: 8px;
|
||||
|
||||
> *:not(:first-child) {
|
||||
margin: 0 0 0 -4px;
|
||||
}
|
||||
}
|
||||
.timeline.contextual > li .replies > .replies-summary:active,
|
||||
.timeline.contextual > li .replies[open] > .replies-summary {
|
||||
|
@ -928,10 +1017,13 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
|
|||
.deck-backdrop .deck .status {
|
||||
max-width: var(--main-width);
|
||||
}
|
||||
.deck-backdrop .deck .menu-switch-view {
|
||||
.deck-backdrop .deck :is(.button-switch-view, .menu-switch-view) {
|
||||
display: none;
|
||||
}
|
||||
@media (min-width: 40em) {
|
||||
.deck-backdrop .deck .button-switch-view {
|
||||
display: inline-block;
|
||||
}
|
||||
.deck-backdrop .deck .menu-switch-view {
|
||||
display: flex;
|
||||
}
|
||||
|
@ -1218,6 +1310,9 @@ button.carousel-dot:is(.active, [disabled].active) .icon {
|
|||
body:has(.status-deck) .media-post-link {
|
||||
display: none;
|
||||
}
|
||||
.media-modal-count-1 .button-label {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
/* ✨ New */
|
||||
body:has(.media-modal-container + .status-deck) .media-post-link {
|
||||
|
@ -1675,6 +1770,24 @@ body > .szh-menu-container {
|
|||
opacity: 1;
|
||||
}
|
||||
|
||||
.szh-menu
|
||||
.szh-menu__item--type-checkbox:not(.szh-menu__item--disabled):not(
|
||||
.szh-menu__item--hover
|
||||
) {
|
||||
.icon {
|
||||
opacity: 0.15;
|
||||
}
|
||||
|
||||
&.szh-menu__item--checked {
|
||||
color: var(--link-color);
|
||||
|
||||
.icon {
|
||||
opacity: 1;
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.szh-menu .menu-wrap {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
@ -1843,25 +1956,6 @@ meter.donut[hidden] {
|
|||
}
|
||||
}
|
||||
|
||||
.deck-container {
|
||||
width: 100%;
|
||||
flex-grow: 1;
|
||||
}
|
||||
:is(#home-page, #welcome, #columns, #loader-root) ~ .deck-container {
|
||||
z-index: 10;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
}
|
||||
:is(#home-page, #welcome, #columns, #loader-root):has(~ .deck-container) {
|
||||
display: block;
|
||||
position: absolute;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
/* This causes scrollTop to be reset to 0 when the page is hidden */
|
||||
/* content-visibility: hidden; */
|
||||
}
|
||||
|
||||
/* 404 */
|
||||
|
||||
#not-found-page {
|
||||
|
@ -1882,13 +1976,23 @@ meter.donut[hidden] {
|
|||
|
||||
/* ACCOUNT STATUSES */
|
||||
|
||||
.header-account {
|
||||
font-size: 90% !important;
|
||||
cursor: pointer;
|
||||
@keyframes peekaboo-header {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10%);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
.header-account div {
|
||||
font-weight: normal;
|
||||
color: var(--text-insignificant-color);
|
||||
|
||||
@supports (animation-timeline: scroll()) {
|
||||
.header-account {
|
||||
animation: peekaboo-header 1s linear both;
|
||||
animation-timeline: scroll();
|
||||
animation-range: 0 150px;
|
||||
}
|
||||
}
|
||||
|
||||
/* LINK LISTS? */
|
||||
|
@ -1917,6 +2021,21 @@ ul.link-list li a {
|
|||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.count {
|
||||
font-size: 80%;
|
||||
display: inline-block;
|
||||
color: var(--text-insignificant-color);
|
||||
min-width: 16px;
|
||||
min-height: 16px;
|
||||
padding: 4px;
|
||||
background-color: var(--bg-color);
|
||||
border-radius: 4px;
|
||||
|
||||
@media (min-width: 40em) {
|
||||
background-color: var(--bg-faded-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
ul.link-list li:first-child a {
|
||||
border-top-left-radius: var(--radius);
|
||||
|
@ -2009,13 +2128,18 @@ ul.link-list li a .icon {
|
|||
#columns .header-grid input {
|
||||
pointer-events: none;
|
||||
}
|
||||
#columns
|
||||
.header-grid
|
||||
.header-side:first-of-type
|
||||
:is(button, .button)
|
||||
~ :is(button, .button),
|
||||
#columns .deck-container:not(:first-of-type) .header-grid .header-side > * {
|
||||
display: none;
|
||||
#columns {
|
||||
/* Any buttons except nav menu button on first header-side, on 1st column */
|
||||
.deck-container:first-of-type
|
||||
.header-grid
|
||||
.header-side:first-of-type
|
||||
> *:not(.nav-menu-button),
|
||||
/* Any buttons on last header-side, on 1st column */
|
||||
.deck-container:first-of-type .header-grid .header-side:last-of-type > *,
|
||||
/* Any buttons on any header-side, on columns after 1st */
|
||||
.deck-container:not(:first-of-type) .header-grid .header-side > * {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@media (min-width: 40em) {
|
||||
#columns {
|
||||
|
|
88
src/app.jsx
88
src/app.jsx
|
@ -10,7 +10,7 @@ import {
|
|||
} from 'preact/hooks';
|
||||
import { matchPath, Route, Routes, useLocation } from 'react-router-dom';
|
||||
import 'swiped-events';
|
||||
import { subscribe, useSnapshot } from 'valtio';
|
||||
import { subscribe } from 'valtio';
|
||||
|
||||
import BackgroundService from './components/background-service';
|
||||
import ComposeButton from './components/compose-button';
|
||||
|
@ -49,12 +49,70 @@ import {
|
|||
} from './utils/api';
|
||||
import { getAccessToken } from './utils/auth';
|
||||
import focusDeck from './utils/focus-deck';
|
||||
import states, { initStates } from './utils/states';
|
||||
import states, { initStates, statusKey } from './utils/states';
|
||||
import store from './utils/store';
|
||||
import { getCurrentAccount } from './utils/store-utils';
|
||||
import './utils/toast-alert';
|
||||
|
||||
window.__STATES__ = states;
|
||||
window.__STATES_STATS__ = () => {
|
||||
const keys = [
|
||||
'statuses',
|
||||
'accounts',
|
||||
'spoilers',
|
||||
'unfurledLinks',
|
||||
'statusQuotes',
|
||||
];
|
||||
const counts = {};
|
||||
keys.forEach((key) => {
|
||||
counts[key] = Object.keys(states[key]).length;
|
||||
});
|
||||
console.warn('STATE stats', counts);
|
||||
|
||||
const { statuses } = states;
|
||||
const unmountedPosts = [];
|
||||
for (const key in statuses) {
|
||||
const $post = document.querySelector(`[data-state-post-id="${key}"]`);
|
||||
if (!$post) {
|
||||
unmountedPosts.push(key);
|
||||
}
|
||||
}
|
||||
console.warn('Unmounted posts', unmountedPosts.length, unmountedPosts);
|
||||
};
|
||||
|
||||
// Experimental "garbage collection" for states
|
||||
// Every 15 minutes
|
||||
// Only posts for now
|
||||
setInterval(() => {
|
||||
if (!window.__IDLE__) return;
|
||||
const { statuses, unfurledLinks, notifications } = states;
|
||||
let keysCount = 0;
|
||||
const { instance } = api();
|
||||
for (const key in statuses) {
|
||||
try {
|
||||
const $post = document.querySelector(`[data-state-post-id~="${key}"]`);
|
||||
const postInNotifications = notifications.some(
|
||||
(n) => key === statusKey(n.status?.id, instance),
|
||||
);
|
||||
if (!$post && !postInNotifications) {
|
||||
delete states.statuses[key];
|
||||
delete states.statusQuotes[key];
|
||||
for (const link in unfurledLinks) {
|
||||
const unfurled = unfurledLinks[link];
|
||||
const sKey = statusKey(unfurled.id, unfurled.instance);
|
||||
if (sKey === key) {
|
||||
delete states.unfurledLinks[link];
|
||||
break;
|
||||
}
|
||||
}
|
||||
keysCount++;
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
if (keysCount) {
|
||||
console.info(`GC: Removed ${keysCount} keys`);
|
||||
}
|
||||
}, 15 * 60 * 1000);
|
||||
|
||||
// Preload icons
|
||||
// There's probably a better way to do this
|
||||
|
@ -70,7 +128,7 @@ setTimeout(() => {
|
|||
}, 5000);
|
||||
|
||||
(() => {
|
||||
window.__IDLE__ = false;
|
||||
window.__IDLE__ = true;
|
||||
const nonIdleEvents = [
|
||||
'mousemove',
|
||||
'mousedown',
|
||||
|
@ -81,13 +139,14 @@ setTimeout(() => {
|
|||
'pointermove',
|
||||
'wheel',
|
||||
];
|
||||
const IDLE_TIME = 5_000; // 5 seconds
|
||||
const setIdle = debounce(() => {
|
||||
const setIdle = () => {
|
||||
window.__IDLE__ = true;
|
||||
}, IDLE_TIME);
|
||||
};
|
||||
const IDLE_TIME = 3_000; // 3 seconds
|
||||
const debouncedSetIdle = debounce(setIdle, IDLE_TIME);
|
||||
const onNonIdle = () => {
|
||||
window.__IDLE__ = false;
|
||||
setIdle();
|
||||
debouncedSetIdle();
|
||||
};
|
||||
nonIdleEvents.forEach((event) => {
|
||||
window.addEventListener(event, onNonIdle, {
|
||||
|
@ -95,6 +154,21 @@ setTimeout(() => {
|
|||
capture: true,
|
||||
});
|
||||
});
|
||||
window.addEventListener('blur', setIdle, {
|
||||
passive: true,
|
||||
});
|
||||
// When cursor leaves the window, set idle
|
||||
document.documentElement.addEventListener(
|
||||
'mouseleave',
|
||||
(e) => {
|
||||
if (!e.relatedTarget && !e.toElement) {
|
||||
setIdle();
|
||||
}
|
||||
},
|
||||
{
|
||||
passive: true,
|
||||
},
|
||||
);
|
||||
// document.addEventListener(
|
||||
// 'visibilitychange',
|
||||
// () => {
|
||||
|
|
|
@ -11,7 +11,7 @@ body.cloak,
|
|||
.status .content-compact,
|
||||
.account-container :is(header, main > *:not(.actions)),
|
||||
.account-container :is(header, main > *:not(.actions)) *,
|
||||
.header-account,
|
||||
.header-double-lines,
|
||||
.account-block {
|
||||
text-decoration-thickness: 1.1em;
|
||||
text-decoration-line: line-through;
|
||||
|
@ -25,6 +25,7 @@ body.cloak,
|
|||
}
|
||||
|
||||
.status :is(img, video, audio),
|
||||
.media-post .media,
|
||||
.avatar,
|
||||
.emoji,
|
||||
.header-banner {
|
||||
|
|
|
@ -82,7 +82,7 @@ function AccountBlock({
|
|||
}}
|
||||
>
|
||||
<Avatar url={avatar} size={avatarSize} squircle={bot} />
|
||||
<span>
|
||||
<span class="account-block-content">
|
||||
{!hideDisplayName && (
|
||||
<>
|
||||
{displayName ? (
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
position: absolute;
|
||||
top: 8px;
|
||||
inset-inline: 8px;
|
||||
z-index: 2;
|
||||
z-index: 3;
|
||||
border: 1px solid var(--outline-color);
|
||||
box-shadow: 0 8px 16px var(--drop-shadow-color);
|
||||
border-radius: calc(16px - 8px);
|
||||
|
@ -47,7 +47,7 @@
|
|||
|
||||
~ * {
|
||||
/* pointer-events: none; */
|
||||
filter: grayscale(0.75) brightness(0.75);
|
||||
filter: grayscale(0.75) opacity(0.75);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -254,6 +254,7 @@
|
|||
.account-container .note {
|
||||
font-size: 95%;
|
||||
line-height: 1.4;
|
||||
text-wrap: pretty;
|
||||
}
|
||||
.account-container .note:not(:has(p)):not(:empty) {
|
||||
/* Some notes don't have <p> tags, so we need to add some padding */
|
||||
|
@ -408,6 +409,7 @@
|
|||
|
||||
.timeline-start .account-container {
|
||||
border-bottom: 1px solid var(--outline-color);
|
||||
position: relative;
|
||||
}
|
||||
.timeline-start .account-container header {
|
||||
padding: 16px 16px 1px;
|
||||
|
@ -424,18 +426,46 @@
|
|||
display: none;
|
||||
}
|
||||
|
||||
@keyframes bye-banner {
|
||||
20% {
|
||||
filter: blur(0) opacity(1);
|
||||
}
|
||||
100% {
|
||||
filter: blur(16px) opacity(0.2);
|
||||
}
|
||||
}
|
||||
@keyframes surface-header {
|
||||
0% {
|
||||
border-bottom-color: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
100% {
|
||||
border-bottom-color: var(--outline-color);
|
||||
box-shadow: 0 8px 16px -8px var(--drop-shadow-color);
|
||||
}
|
||||
}
|
||||
@keyframes shrink-avatar {
|
||||
0% {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
}
|
||||
100% {
|
||||
width: 2.5em;
|
||||
height: 2.5em;
|
||||
}
|
||||
}
|
||||
.sheet .account-container {
|
||||
border-radius: 16px 16px 0 0;
|
||||
overflow-x: hidden;
|
||||
max-height: 75vh;
|
||||
overscroll-behavior: none;
|
||||
scroll-timeline: --account-scroll;
|
||||
|
||||
header {
|
||||
padding-bottom: 16px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
/* --bg-color: red; */
|
||||
background-image: linear-gradient(
|
||||
to bottom,
|
||||
transparent 30%,
|
||||
|
@ -443,6 +473,14 @@
|
|||
var(--bg-color) calc(100% - 8px),
|
||||
transparent
|
||||
);
|
||||
|
||||
.account-block-content {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
line-clamp: 3;
|
||||
-webkit-line-clamp: 3;
|
||||
}
|
||||
}
|
||||
|
||||
.faux-header-bg {
|
||||
|
@ -455,6 +493,32 @@
|
|||
margin-top: calc(-1 * var(--banner-overlap));
|
||||
}
|
||||
|
||||
@supports (animation-timeline: scroll()) {
|
||||
.header-banner:not(.header-is-avatar):not(:hover):not(:active) {
|
||||
animation: bye-banner 1s linear both;
|
||||
animation-timeline: view();
|
||||
animation-range: contain 100% cover 100%;
|
||||
}
|
||||
|
||||
header {
|
||||
background-image: linear-gradient(
|
||||
to bottom,
|
||||
transparent 30%,
|
||||
var(--bg-color) var(--banner-overlap)
|
||||
);
|
||||
border-bottom: 1px solid transparent;
|
||||
animation: surface-header 1s linear both;
|
||||
animation-timeline: --account-scroll;
|
||||
animation-range: 0 150px;
|
||||
}
|
||||
|
||||
header .avatar {
|
||||
animation: shrink-avatar 1s linear both;
|
||||
animation-timeline: --account-scroll;
|
||||
animation-range: 0 150px;
|
||||
}
|
||||
}
|
||||
|
||||
main {
|
||||
margin-top: -8px;
|
||||
padding-top: 1px;
|
||||
|
@ -612,43 +676,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
@keyframes shine {
|
||||
0% {
|
||||
left: -100%;
|
||||
}
|
||||
100% {
|
||||
left: 100%;
|
||||
}
|
||||
}
|
||||
.timeline-start .account-container {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.timeline-start .account-container:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image: linear-gradient(
|
||||
100deg,
|
||||
rgba(255, 255, 255, 0) 30%,
|
||||
rgba(255, 255, 255, 0.25),
|
||||
rgba(255, 255, 255, 0) 70%
|
||||
);
|
||||
top: 0;
|
||||
left: -100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.timeline-start .account-container:before {
|
||||
opacity: 0.25;
|
||||
}
|
||||
}
|
||||
.timeline-start .account-container:hover:before {
|
||||
animation: shine 1s ease-in-out 1s;
|
||||
}
|
||||
|
||||
#list-add-remove-container .list-add-remove {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
|
@ -126,19 +126,14 @@ function AccountInfo({
|
|||
const { masto } = api({
|
||||
instance,
|
||||
});
|
||||
const { masto: currentMasto } = api();
|
||||
const { masto: currentMasto, instance: currentInstance } = api();
|
||||
const [uiState, setUIState] = useState('default');
|
||||
const isString = typeof account === 'string';
|
||||
const [info, setInfo] = useState(isString ? null : account);
|
||||
|
||||
const isSelf = useMemo(
|
||||
() => account.id === store.session.get('currentAccount'),
|
||||
[account?.id],
|
||||
);
|
||||
|
||||
const sameCurrentInstance = useMemo(
|
||||
() => instance === api().instance,
|
||||
[instance],
|
||||
() => instance === currentInstance,
|
||||
[instance, currentInstance],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -198,6 +193,37 @@ function AccountInfo({
|
|||
}
|
||||
}
|
||||
|
||||
const isSelf = useMemo(
|
||||
() => id === store.session.get('currentAccount'),
|
||||
[id],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const infoHasEssentials = !!(
|
||||
info?.id &&
|
||||
info?.username &&
|
||||
info?.acct &&
|
||||
info?.avatar &&
|
||||
info?.avatarStatic &&
|
||||
info?.displayName &&
|
||||
info?.url
|
||||
);
|
||||
if (isSelf && instance && infoHasEssentials) {
|
||||
const accounts = store.local.getJSON('accounts');
|
||||
let updated = false;
|
||||
accounts.forEach((account) => {
|
||||
if (account.info.id === info.id && account.instanceURL === instance) {
|
||||
account.info = info;
|
||||
updated = true;
|
||||
}
|
||||
});
|
||||
if (updated) {
|
||||
console.log('Updated account info', info);
|
||||
store.local.setJSON('accounts', accounts);
|
||||
}
|
||||
}
|
||||
}, [isSelf, info, instance]);
|
||||
|
||||
const accountInstance = useMemo(() => {
|
||||
if (!url) return null;
|
||||
const domain = new URL(url).hostname;
|
||||
|
@ -304,12 +330,13 @@ function AccountInfo({
|
|||
({ relationship, currentID }) => {
|
||||
if (!relationship.following) {
|
||||
renderFamiliarFollowers(currentID);
|
||||
if (!standalone) {
|
||||
if (!standalone && statusesCount > 0) {
|
||||
// Only render posting stats if not standalone and has posts
|
||||
renderPostingStats();
|
||||
}
|
||||
}
|
||||
},
|
||||
[standalone, id],
|
||||
[standalone, id, statusesCount],
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -534,7 +561,7 @@ function AccountInfo({
|
|||
class="note"
|
||||
dir="auto"
|
||||
onClick={handleContentLinks({
|
||||
instance,
|
||||
instance: currentInstance,
|
||||
})}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: enhanceContent(note, { emojis }),
|
||||
|
|
|
@ -8,24 +8,31 @@
|
|||
box-shadow: 0 0 0 1px var(--bg-blur-color);
|
||||
flex-shrink: 0;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.avatar.has-alpha {
|
||||
border-radius: 0;
|
||||
}
|
||||
.avatar:not(.has-alpha).squircle {
|
||||
border-radius: 25%;
|
||||
}
|
||||
|
||||
.avatar img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
background-color: var(--img-bg-color);
|
||||
contain: none;
|
||||
}
|
||||
&.has-alpha {
|
||||
border-radius: 0;
|
||||
background-color: transparent;
|
||||
box-shadow: none;
|
||||
|
||||
.avatar[data-loaded],
|
||||
.avatar[data-loaded] img {
|
||||
box-shadow: none;
|
||||
background-color: transparent;
|
||||
img {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
&:not(.has-alpha).squircle {
|
||||
border-radius: 25%;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
background-color: var(--img-bg-color);
|
||||
contain: none;
|
||||
}
|
||||
|
||||
&[data-loaded],
|
||||
&[data-loaded] img {
|
||||
box-shadow: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,29 +1,32 @@
|
|||
import { memo } from 'preact/compat';
|
||||
import { useEffect, useRef, useState } from 'preact/hooks';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
|
||||
import { api } from '../utils/api';
|
||||
import showToast from '../utils/show-toast';
|
||||
import states, { saveStatus } from '../utils/states';
|
||||
import useInterval from '../utils/useInterval';
|
||||
import usePageVisibility from '../utils/usePageVisibility';
|
||||
|
||||
const STREAMING_TIMEOUT = 1000 * 3; // 3 seconds
|
||||
const POLL_INTERVAL = 15_000; // 15 seconds
|
||||
|
||||
export default memo(function BackgroundService({ isLoggedIn }) {
|
||||
// Notifications service
|
||||
// - WebSocket to receive notifications when page is visible
|
||||
const [visible, setVisible] = useState(true);
|
||||
usePageVisibility(setVisible);
|
||||
const subRef = useRef();
|
||||
const debouncedStartNotifications = useDebouncedCallback(() => {
|
||||
const { masto, streaming, instance } = api();
|
||||
(async () => {
|
||||
// 1. Get the latest notification
|
||||
if (states.notificationsLast) {
|
||||
const notificationsIterator = masto.v1.notifications.list({
|
||||
limit: 1,
|
||||
since_id: states.notificationsLast.id,
|
||||
});
|
||||
const { value: notifications } = await notificationsIterator.next();
|
||||
if (notifications?.length) {
|
||||
const checkLatestNotification = async (masto, instance, skipCheckMarkers) => {
|
||||
if (states.notificationsLast) {
|
||||
const notificationsIterator = masto.v1.notifications.list({
|
||||
limit: 1,
|
||||
sinceId: states.notificationsLast.id,
|
||||
});
|
||||
const { value: notifications } = await notificationsIterator.next();
|
||||
if (notifications?.length) {
|
||||
if (skipCheckMarkers) {
|
||||
states.notificationsShowNew = true;
|
||||
} else {
|
||||
let lastReadId;
|
||||
try {
|
||||
const markers = await masto.v1.markers.fetch({
|
||||
|
@ -38,36 +41,60 @@ export default memo(function BackgroundService({ isLoggedIn }) {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 2. Start streaming
|
||||
if (streaming) {
|
||||
let sub = (subRef.current = streaming.user.notification.subscribe());
|
||||
console.log('🎏 Streaming notification', sub);
|
||||
for await (const entry of sub) {
|
||||
if (!sub) break;
|
||||
console.log('🔔🔔 Notification entry', entry);
|
||||
if (entry.event === 'notification') {
|
||||
console.log('🔔🔔 Notification', entry);
|
||||
saveStatus(entry.payload, instance, {
|
||||
skipThreading: true,
|
||||
});
|
||||
}
|
||||
states.notificationsShowNew = true;
|
||||
}
|
||||
}
|
||||
})();
|
||||
}, 3000);
|
||||
useEffect(() => {
|
||||
// let sub;
|
||||
let sub;
|
||||
let pollNotifications;
|
||||
if (isLoggedIn && visible) {
|
||||
debouncedStartNotifications();
|
||||
const { masto, streaming, instance } = api();
|
||||
(async () => {
|
||||
// 1. Get the latest notification
|
||||
await checkLatestNotification(masto, instance);
|
||||
|
||||
let hasStreaming = false;
|
||||
// 2. Start streaming
|
||||
if (streaming) {
|
||||
pollNotifications = setTimeout(() => {
|
||||
(async () => {
|
||||
try {
|
||||
hasStreaming = true;
|
||||
sub = streaming.user.notification.subscribe();
|
||||
console.log('🎏 Streaming notification', sub);
|
||||
for await (const entry of sub) {
|
||||
if (!sub) break;
|
||||
if (!visible) break;
|
||||
console.log('🔔🔔 Notification entry', entry);
|
||||
if (entry.event === 'notification') {
|
||||
console.log('🔔🔔 Notification', entry);
|
||||
saveStatus(entry.payload, instance, {
|
||||
skipThreading: true,
|
||||
});
|
||||
}
|
||||
states.notificationsShowNew = true;
|
||||
}
|
||||
} catch (e) {
|
||||
hasStreaming = false;
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
if (!hasStreaming) {
|
||||
console.log('🎏 Streaming failed, fallback to polling');
|
||||
pollNotifications = setInterval(() => {
|
||||
checkLatestNotification(masto, instance, true);
|
||||
}, POLL_INTERVAL);
|
||||
}
|
||||
})();
|
||||
}, STREAMING_TIMEOUT);
|
||||
}
|
||||
})();
|
||||
}
|
||||
return () => {
|
||||
// sub?.unsubscribe?.();
|
||||
// sub = null;
|
||||
debouncedStartNotifications?.cancel?.();
|
||||
subRef.current?.unsubscribe?.();
|
||||
subRef.current = null;
|
||||
sub?.unsubscribe?.();
|
||||
sub = null;
|
||||
clearTimeout(pollNotifications);
|
||||
clearInterval(pollNotifications);
|
||||
};
|
||||
}, [visible, isLoggedIn]);
|
||||
|
||||
|
@ -100,5 +127,14 @@ export default memo(function BackgroundService({ isLoggedIn }) {
|
|||
}
|
||||
});
|
||||
|
||||
// Global keyboard shortcuts "service"
|
||||
useHotkeys('shift+alt+k', () => {
|
||||
const currentCloakMode = states.settings.cloakMode;
|
||||
states.settings.cloakMode = !currentCloakMode;
|
||||
showToast({
|
||||
text: `Cloak mode ${currentCloakMode ? 'disabled' : 'enabled'}`,
|
||||
});
|
||||
});
|
||||
|
||||
return null;
|
||||
});
|
||||
|
|
|
@ -49,7 +49,24 @@ function Columns() {
|
|||
}
|
||||
});
|
||||
|
||||
return <div id="columns">{components}</div>;
|
||||
return (
|
||||
<div
|
||||
id="columns"
|
||||
onContextMenu={(e) => {
|
||||
// If right-click on header, but not links or buttons
|
||||
if (
|
||||
e.target.closest('.deck > header') &&
|
||||
!e.target.closest('a') &&
|
||||
!e.target.closest('button')
|
||||
) {
|
||||
e.preventDefault();
|
||||
states.showShortcutsSettings = true;
|
||||
}
|
||||
}}
|
||||
>
|
||||
{components}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Columns;
|
||||
|
|
|
@ -11,7 +11,6 @@ export default function ComposeButton() {
|
|||
const newWin = openCompose();
|
||||
|
||||
if (!newWin) {
|
||||
alert('Looks like your browser is blocking popups.');
|
||||
states.showCompose = true;
|
||||
}
|
||||
} else {
|
||||
|
|
|
@ -40,28 +40,6 @@
|
|||
border-radius: 9999px;
|
||||
}
|
||||
|
||||
#compose-container textarea {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
height: 5em;
|
||||
min-height: 5em;
|
||||
max-height: 50vh;
|
||||
resize: vertical;
|
||||
line-height: 1.4;
|
||||
border-color: transparent;
|
||||
}
|
||||
#compose-container textarea:hover {
|
||||
border-color: var(--divider-color);
|
||||
}
|
||||
|
||||
@media (min-width: 40em) {
|
||||
#compose-container textarea {
|
||||
/* font-size: 150%;
|
||||
font-size: calc(100% + 50% / var(--text-weight)); */
|
||||
max-height: 65vh;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes appear-up {
|
||||
0% {
|
||||
opacity: 0;
|
||||
|
@ -129,19 +107,53 @@
|
|||
}
|
||||
|
||||
#compose-container form {
|
||||
border-radius: 16px;
|
||||
padding: 4px 12px;
|
||||
--form-padding-inline: 12px;
|
||||
--form-padding-block: 8px;
|
||||
/* border-radius: 16px; */
|
||||
padding: var(--form-padding-block) var(--form-padding-inline);
|
||||
background-color: var(--bg-blur-color);
|
||||
/* background-image: linear-gradient(var(--bg-color) 85%, transparent); */
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
--drop-shadow: 0 3px 6px -3px var(--drop-shadow-color);
|
||||
box-shadow: var(--drop-shadow);
|
||||
|
||||
@media (min-width: 40em) {
|
||||
border-radius: 16px;
|
||||
}
|
||||
}
|
||||
#compose-container .status-preview ~ form {
|
||||
box-shadow: var(--drop-shadow), 0 -3px 6px -3px var(--drop-shadow-color);
|
||||
}
|
||||
|
||||
#compose-container textarea {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
height: 5em;
|
||||
min-height: 5em;
|
||||
max-height: 50vh;
|
||||
resize: vertical;
|
||||
line-height: 1.4;
|
||||
border-color: transparent;
|
||||
|
||||
&.compose-field {
|
||||
@media (width < 30em) {
|
||||
margin-inline: calc(-1 * var(--form-padding-inline));
|
||||
width: 100vw !important;
|
||||
max-width: 100vw;
|
||||
border-radius: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 40em) {
|
||||
max-height: 65vh;
|
||||
}
|
||||
}
|
||||
}
|
||||
#compose-container textarea:hover {
|
||||
border-color: var(--divider-color);
|
||||
}
|
||||
|
||||
#compose-container .toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
@ -269,6 +281,15 @@
|
|||
gap: 8px;
|
||||
align-items: center;
|
||||
font-size: 90%;
|
||||
|
||||
.grow {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.count {
|
||||
font-size: 80%;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
#compose-container .text-expander-menu li b img {
|
||||
/* The shortcode emojis */
|
||||
|
|
|
@ -17,6 +17,7 @@ import db from '../utils/db';
|
|||
import emojifyText from '../utils/emojify-text';
|
||||
import localeMatch from '../utils/locale-match';
|
||||
import openCompose from '../utils/open-compose';
|
||||
import shortenNumber from '../utils/shorten-number';
|
||||
import states, { saveStatus } from '../utils/states';
|
||||
import store from '../utils/store';
|
||||
import {
|
||||
|
@ -521,6 +522,34 @@ function Compose({
|
|||
|
||||
const [showEmoji2Picker, setShowEmoji2Picker] = useState(false);
|
||||
|
||||
const [topSupportedLanguages, restSupportedLanguages] = useMemo(() => {
|
||||
const topLanguages = [];
|
||||
const restLanguages = [];
|
||||
const { contentTranslationHideLanguages = [] } = states.settings;
|
||||
supportedLanguages.forEach((l) => {
|
||||
const [code] = l;
|
||||
if (
|
||||
code === language ||
|
||||
code === prevLanguage.current ||
|
||||
code === DEFAULT_LANG ||
|
||||
contentTranslationHideLanguages.includes(code)
|
||||
) {
|
||||
topLanguages.push(l);
|
||||
} else {
|
||||
restLanguages.push(l);
|
||||
}
|
||||
});
|
||||
topLanguages.sort(([codeA, commonA], [codeB, commonB]) => {
|
||||
if (codeA === language) return -1;
|
||||
if (codeB === language) return 1;
|
||||
return commonA.localeCompare(commonB);
|
||||
});
|
||||
restLanguages.sort(([codeA, commonA], [codeB, commonB]) =>
|
||||
commonA.localeCompare(commonB),
|
||||
);
|
||||
return [topLanguages, restLanguages];
|
||||
}, [language]);
|
||||
|
||||
return (
|
||||
<div id="compose-container-outer">
|
||||
<div id="compose-container" class={standalone ? 'standalone' : ''}>
|
||||
|
@ -578,7 +607,6 @@ function Compose({
|
|||
});
|
||||
|
||||
if (!newWin) {
|
||||
alert('Looks like your browser is blocking popups.');
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1125,32 +1153,17 @@ function Compose({
|
|||
}}
|
||||
disabled={uiState === 'loading'}
|
||||
>
|
||||
{supportedLanguages
|
||||
.sort(([codeA, commonA], [codeB, commonB]) => {
|
||||
const { contentTranslationHideLanguages = [] } =
|
||||
states.settings;
|
||||
// Sort codes that same as language, prevLanguage, DEFAULT_LANGUAGE and all the ones in states.settings.contentTranslationHideLanguages, to the top
|
||||
if (
|
||||
codeA === language ||
|
||||
codeA === prevLanguage ||
|
||||
codeA === DEFAULT_LANG ||
|
||||
contentTranslationHideLanguages?.includes(codeA)
|
||||
)
|
||||
return -1;
|
||||
if (
|
||||
codeB === language ||
|
||||
codeB === prevLanguage ||
|
||||
codeB === DEFAULT_LANG ||
|
||||
contentTranslationHideLanguages?.includes(codeB)
|
||||
)
|
||||
return 1;
|
||||
return commonA.localeCompare(commonB);
|
||||
})
|
||||
.map(([code, common, native]) => (
|
||||
<option value={code}>
|
||||
{common} ({native})
|
||||
</option>
|
||||
))}
|
||||
{topSupportedLanguages.map(([code, common, native]) => (
|
||||
<option value={code} key={code}>
|
||||
{common} ({native})
|
||||
</option>
|
||||
))}
|
||||
<hr />
|
||||
{restSupportedLanguages.map(([code, common, native]) => (
|
||||
<option value={code} key={code}>
|
||||
{common} ({native})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>{' '}
|
||||
<button
|
||||
|
@ -1306,6 +1319,7 @@ const Textarea = forwardRef((props, ref) => {
|
|||
username,
|
||||
acct,
|
||||
emojis,
|
||||
history,
|
||||
} = result;
|
||||
const displayNameWithEmoji = emojifyText(displayName, emojis);
|
||||
// const item = menuItem.cloneNode();
|
||||
|
@ -1324,9 +1338,18 @@ const Textarea = forwardRef((props, ref) => {
|
|||
</li>
|
||||
`;
|
||||
} else {
|
||||
const total = history?.reduce?.(
|
||||
(acc, cur) => acc + +cur.uses,
|
||||
0,
|
||||
);
|
||||
html += `
|
||||
<li role="option" data-value="${encodeHTML(name)}">
|
||||
<span>#<b>${encodeHTML(name)}</b></span>
|
||||
<span class="grow">#<b>${encodeHTML(name)}</b></span>
|
||||
${
|
||||
total
|
||||
? `<span class="count">${shortenNumber(total)}</span>`
|
||||
: ''
|
||||
}
|
||||
</li>
|
||||
`;
|
||||
}
|
||||
|
@ -1393,6 +1416,7 @@ const Textarea = forwardRef((props, ref) => {
|
|||
return (
|
||||
<text-expander ref={textExpanderRef} keys="@ # :">
|
||||
<textarea
|
||||
class="compose-field"
|
||||
autoCapitalize="sentences"
|
||||
autoComplete="on"
|
||||
autoCorrect="on"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import './generic-accounts.css';
|
||||
|
||||
import { useEffect, useState } from 'preact/hooks';
|
||||
import { useEffect, useRef, useState } from 'preact/hooks';
|
||||
import { InView } from 'react-intersection-observer';
|
||||
import { useSnapshot } from 'valtio';
|
||||
|
||||
|
@ -56,15 +56,18 @@ export default function GenericAccounts({ onClose = () => {} }) {
|
|||
})();
|
||||
};
|
||||
|
||||
const firstLoad = useRef(true);
|
||||
useEffect(() => {
|
||||
if (staticAccounts?.length > 0) {
|
||||
setAccounts(staticAccounts);
|
||||
} else {
|
||||
loadAccounts(true);
|
||||
firstLoad.current = false;
|
||||
}
|
||||
}, [staticAccounts, fetchAccounts]);
|
||||
|
||||
useEffect(() => {
|
||||
if (firstLoad.current) return;
|
||||
// reloadGenericAccounts contains value like {id: 'mute', counter: 1}
|
||||
// We only need to reload if the id matches
|
||||
if (snapStates.reloadGenericAccounts?.id === id) {
|
||||
|
|
|
@ -102,6 +102,7 @@ export const ICONS = {
|
|||
keyboard: () => import('@iconify-icons/mingcute/keyboard-line'),
|
||||
cloud: () => import('@iconify-icons/mingcute/cloud-line'),
|
||||
month: () => import('@iconify-icons/mingcute/calendar-month-line'),
|
||||
media: () => import('@iconify-icons/mingcute/photo-album-line'),
|
||||
};
|
||||
|
||||
function Icon({
|
||||
|
|
|
@ -117,6 +117,15 @@ export default memo(function KeyboardShortcutsHelp() {
|
|||
action: 'Compose new post',
|
||||
keys: <kbd>c</kbd>,
|
||||
},
|
||||
{
|
||||
action: 'Compose new post (new window)',
|
||||
className: 'insignificant',
|
||||
keys: (
|
||||
<>
|
||||
<kbd>Shift</kbd> + <kbd>c</kbd>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
action: 'Send post',
|
||||
keys: (
|
||||
|
@ -134,6 +143,15 @@ export default memo(function KeyboardShortcutsHelp() {
|
|||
action: 'Reply',
|
||||
keys: <kbd>r</kbd>,
|
||||
},
|
||||
{
|
||||
action: 'Reply (new window)',
|
||||
className: 'insignificant',
|
||||
keys: (
|
||||
<>
|
||||
<kbd>Shift</kbd> + <kbd>r</kbd>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
action: 'Like (favourite)',
|
||||
keys: (
|
||||
|
@ -154,9 +172,17 @@ export default memo(function KeyboardShortcutsHelp() {
|
|||
action: 'Bookmark',
|
||||
keys: <kbd>d</kbd>,
|
||||
},
|
||||
].map(({ action, keys }) => (
|
||||
{
|
||||
action: 'Toggle Cloak mode',
|
||||
keys: (
|
||||
<>
|
||||
<kbd>Shift</kbd> + <kbd>Alt</kbd> + <kbd>k</kbd>
|
||||
</>
|
||||
),
|
||||
},
|
||||
].map(({ action, className, keys }) => (
|
||||
<tr key={action}>
|
||||
<th>{action}</th>
|
||||
<th class={className}>{action}</th>
|
||||
<td>{keys}</td>
|
||||
</tr>
|
||||
))}
|
||||
|
|
|
@ -57,6 +57,7 @@ export default function MediaAltModal({ alt, lang, onClose }) {
|
|||
<p
|
||||
style={{
|
||||
whiteSpace: 'pre-wrap',
|
||||
textWrap: 'pretty',
|
||||
}}
|
||||
>
|
||||
{alt}
|
||||
|
|
|
@ -103,7 +103,9 @@ function MediaModal({
|
|||
}, []);
|
||||
|
||||
return (
|
||||
<div class="media-modal-container">
|
||||
<div
|
||||
class={`media-modal-container media-modal-count-${mediaAttachments?.length}`}
|
||||
>
|
||||
<div
|
||||
ref={carouselRef}
|
||||
tabIndex="0"
|
||||
|
@ -142,7 +144,11 @@ function MediaModal({
|
|||
key={media.id}
|
||||
ref={i === currentIndex ? carouselFocusItem : null}
|
||||
onClick={(e) => {
|
||||
if (e.target !== e.currentTarget) {
|
||||
// console.log(e);
|
||||
// if (e.target !== e.currentTarget) {
|
||||
// setShowControls(!showControls);
|
||||
// }
|
||||
if (!e.target.classList.contains('media')) {
|
||||
setShowControls(!showControls);
|
||||
}
|
||||
}}
|
||||
|
@ -248,7 +254,7 @@ function MediaModal({
|
|||
// }
|
||||
// }}
|
||||
>
|
||||
<span class="button-label">See post </span>»
|
||||
<span class="button-label">View post </span>»
|
||||
</Link>
|
||||
</span>
|
||||
</div>
|
||||
|
|
107
src/components/media-post.css
Normal file
107
src/components/media-post.css
Normal file
|
@ -0,0 +1,107 @@
|
|||
.media-post {
|
||||
--item-radius: 16px;
|
||||
position: relative;
|
||||
animation: appear-smooth 1s ease-out;
|
||||
|
||||
&:is(.filtered, .has-spoiler) :is(img, video) {
|
||||
filter: blur(32px);
|
||||
image-rendering: crisp-edges;
|
||||
image-rendering: pixelated;
|
||||
animation: none !important;
|
||||
}
|
||||
|
||||
&.filtered[data-filtered-text]:before {
|
||||
content: attr(data-filtered-text);
|
||||
}
|
||||
&.has-spoiler[data-spoiler-text]:before {
|
||||
content: attr(data-spoiler-text);
|
||||
}
|
||||
|
||||
&.filtered[data-filtered-text]:before,
|
||||
&.has-spoiler[data-spoiler-text]:before {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
background-color: var(--bg-blur-color);
|
||||
margin: 8px;
|
||||
padding: 4px 6px;
|
||||
border-radius: calc(var(--item-radius) / 2);
|
||||
font-size: 90%;
|
||||
border: var(--hairline-width) dashed var(--bg-color);
|
||||
word-break: break-word;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
mix-blend-mode: luminosity;
|
||||
-webkit-line-clamp: 3;
|
||||
line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
box-orient: vertical;
|
||||
display: -webkit-box;
|
||||
display: box;
|
||||
overflow: hidden;
|
||||
z-index: 2;
|
||||
|
||||
> * {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.media {
|
||||
border-radius: var(--item-radius);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
display: block;
|
||||
aspect-ratio: 1 !important;
|
||||
|
||||
&:before {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
content: '';
|
||||
border: 1px solid var(--outline-color);
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
&:not(.media-audio) {
|
||||
background-color: var(--average-color, var(--media-bg-color));
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
&:hover {
|
||||
--drop-shadow: var(--drop-shadow-color);
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
box-shadow: 0 8px 16px -4px var(--drop-shadow),
|
||||
0 4px 8px var(--drop-shadow);
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
--drop-shadow: var(--link-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:active:not(:has(button:active)) {
|
||||
box-shadow: none;
|
||||
filter: brightness(0.8);
|
||||
transform: scale(0.99);
|
||||
}
|
||||
|
||||
video,
|
||||
img,
|
||||
audio {
|
||||
border-radius: 16px;
|
||||
/* object-fit: scale-down; */
|
||||
object-fit: cover;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
:not(.filtered, .has-spoiler) &:is(:hover, :focus) img {
|
||||
/* Less delay here to make it feel more responsive */
|
||||
animation: position-object 5s ease-in-out 0.1s 5;
|
||||
animation-duration: var(--anim-duration, 5s);
|
||||
}
|
||||
}
|
||||
}
|
150
src/components/media-post.jsx
Normal file
150
src/components/media-post.jsx
Normal file
|
@ -0,0 +1,150 @@
|
|||
import './media-post.css';
|
||||
|
||||
import { memo } from 'preact/compat';
|
||||
import { useContext, useMemo } from 'preact/hooks';
|
||||
import { useSnapshot } from 'valtio';
|
||||
|
||||
import FilterContext from '../utils/filter-context';
|
||||
import { isFiltered } from '../utils/filters';
|
||||
import states, { statusKey } from '../utils/states';
|
||||
import store from '../utils/store';
|
||||
|
||||
import Media from './media';
|
||||
|
||||
function MediaPost({
|
||||
class: className,
|
||||
statusID,
|
||||
status,
|
||||
instance,
|
||||
parent,
|
||||
// allowFilters,
|
||||
onMediaClick,
|
||||
}) {
|
||||
let sKey = statusKey(statusID, instance);
|
||||
const snapStates = useSnapshot(states);
|
||||
if (!status) {
|
||||
status = snapStates.statuses[sKey] || snapStates.statuses[statusID];
|
||||
sKey = statusKey(status?.id, instance);
|
||||
}
|
||||
if (!status) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {
|
||||
account: {
|
||||
acct,
|
||||
avatar,
|
||||
avatarStatic,
|
||||
id: accountId,
|
||||
url: accountURL,
|
||||
displayName,
|
||||
username,
|
||||
emojis: accountEmojis,
|
||||
bot,
|
||||
group,
|
||||
},
|
||||
id,
|
||||
repliesCount,
|
||||
reblogged,
|
||||
reblogsCount,
|
||||
favourited,
|
||||
favouritesCount,
|
||||
bookmarked,
|
||||
poll,
|
||||
muted,
|
||||
sensitive,
|
||||
spoilerText,
|
||||
visibility, // public, unlisted, private, direct
|
||||
language,
|
||||
editedAt,
|
||||
filtered,
|
||||
card,
|
||||
createdAt,
|
||||
inReplyToId,
|
||||
inReplyToAccountId,
|
||||
content,
|
||||
mentions,
|
||||
mediaAttachments,
|
||||
reblog,
|
||||
uri,
|
||||
url,
|
||||
emojis,
|
||||
// Non-API props
|
||||
_deleted,
|
||||
_pinned,
|
||||
// _filtered,
|
||||
} = status;
|
||||
|
||||
if (!mediaAttachments?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const debugHover = (e) => {
|
||||
if (e.shiftKey) {
|
||||
console.log({
|
||||
...status,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const currentAccount = useMemo(() => {
|
||||
return store.session.get('currentAccount');
|
||||
}, []);
|
||||
const isSelf = useMemo(() => {
|
||||
return currentAccount && currentAccount === accountId;
|
||||
}, [accountId, currentAccount]);
|
||||
|
||||
const filterContext = useContext(FilterContext);
|
||||
const filterInfo = !isSelf && isFiltered(filtered, filterContext);
|
||||
|
||||
if (filterInfo?.action === 'hide') {
|
||||
return null;
|
||||
}
|
||||
|
||||
console.debug('RENDER Media post', id, status?.account.displayName);
|
||||
|
||||
// const readingExpandSpoilers = useMemo(() => {
|
||||
// const prefs = store.account.get('preferences') || {};
|
||||
// return !!prefs['reading:expand:spoilers'];
|
||||
// }, []);
|
||||
const hasSpoiler = spoilerText || sensitive;
|
||||
|
||||
const Parent = parent || 'div';
|
||||
|
||||
return mediaAttachments.map((media, i) => {
|
||||
const mediaKey = `${sKey}-${media.id}`;
|
||||
const filterTitleStr = filterInfo?.titlesStr;
|
||||
return (
|
||||
<Parent
|
||||
data-state-post-id={sKey}
|
||||
onMouseEnter={debugHover}
|
||||
key={mediaKey}
|
||||
data-spoiler-text={
|
||||
spoilerText || (sensitive ? 'Sensitive media' : undefined)
|
||||
}
|
||||
data-filtered-text={
|
||||
filterInfo
|
||||
? `Filtered${filterTitleStr ? `: ${filterTitleStr}` : ''}`
|
||||
: undefined
|
||||
}
|
||||
class={`
|
||||
media-post
|
||||
${filterInfo ? 'filtered' : ''}
|
||||
${hasSpoiler ? 'has-spoiler' : ''}
|
||||
`}
|
||||
>
|
||||
<Media
|
||||
class={className}
|
||||
media={media}
|
||||
lang={language}
|
||||
to={`/${instance}/s/${id}?media-only=${i + 1}`}
|
||||
onClick={
|
||||
onMediaClick ? (e) => onMediaClick(e, i, media, status) : undefined
|
||||
}
|
||||
/>
|
||||
</Parent>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export default memo(MediaPost);
|
|
@ -62,6 +62,7 @@ export const isMediaCaptionLong = mem((caption) =>
|
|||
);
|
||||
|
||||
function Media({
|
||||
class: className = '',
|
||||
media,
|
||||
to,
|
||||
lang,
|
||||
|
@ -170,6 +171,9 @@ function Media({
|
|||
const maxAspectHeight =
|
||||
window.innerHeight * (orientation === 'portrait' ? 0.45 : 0.33);
|
||||
const maxHeight = orientation === 'portrait' ? 0 : 160;
|
||||
const averageColorStyle = {
|
||||
'--average-color': rgbAverageColor && `rgb(${rgbAverageColor.join(',')})`,
|
||||
};
|
||||
const mediaStyles =
|
||||
width && height
|
||||
? {
|
||||
|
@ -180,8 +184,11 @@ function Media({
|
|||
(width / height) * Math.max(maxHeight, maxAspectHeight)
|
||||
}px`,
|
||||
aspectRatio: `${width} / ${height}`,
|
||||
...averageColorStyle,
|
||||
}
|
||||
: {};
|
||||
: {
|
||||
...averageColorStyle,
|
||||
};
|
||||
|
||||
const longDesc = isMediaCaptionLong(description);
|
||||
const showInlineDesc =
|
||||
|
@ -233,7 +240,7 @@ function Media({
|
|||
<Figure>
|
||||
<Parent
|
||||
ref={parentRef}
|
||||
class={`media media-image`}
|
||||
class={`media media-image ${className}`}
|
||||
onClick={onClick}
|
||||
data-orientation={orientation}
|
||||
data-has-alt={!showInlineDesc}
|
||||
|
@ -244,6 +251,7 @@ function Media({
|
|||
backgroundSize: imageSmallerThanParent
|
||||
? `${width}px ${height}px`
|
||||
: undefined,
|
||||
...averageColorStyle,
|
||||
}
|
||||
: mediaStyles
|
||||
}
|
||||
|
@ -341,11 +349,13 @@ function Media({
|
|||
return (
|
||||
<Figure>
|
||||
<Parent
|
||||
class={`media media-${isGIF ? 'gif' : 'video'} ${
|
||||
class={`media ${className} media-${isGIF ? 'gif' : 'video'} ${
|
||||
autoGIFAnimate ? 'media-contain' : ''
|
||||
}`}
|
||||
data-orientation={orientation}
|
||||
data-formatted-duration={formattedDuration}
|
||||
data-formatted-duration={
|
||||
!showOriginal ? formattedDuration : undefined
|
||||
}
|
||||
data-label={isGIF && !showOriginal && !autoGIFAnimate ? 'GIF' : ''}
|
||||
data-has-alt={!showInlineDesc}
|
||||
// style={{
|
||||
|
@ -448,8 +458,10 @@ function Media({
|
|||
return (
|
||||
<Figure>
|
||||
<Parent
|
||||
class="media media-audio"
|
||||
data-formatted-duration={formattedDuration}
|
||||
class={`media media-audio ${className}`}
|
||||
data-formatted-duration={
|
||||
!showOriginal ? formattedDuration : undefined
|
||||
}
|
||||
data-has-alt={!showInlineDesc}
|
||||
onClick={onClick}
|
||||
style={!showOriginal && mediaStyles}
|
||||
|
|
|
@ -35,8 +35,9 @@ function NavMenu(props) {
|
|||
// User may choose pin or not to pin Following
|
||||
// If user doesn't pin Following, we show it in the menu
|
||||
const showFollowing =
|
||||
(snapStates.settings.shortcutsColumnsMode ||
|
||||
snapStates.settings.shortcutsViewMode === 'multi-column') &&
|
||||
(snapStates.settings.shortcutsViewMode === 'multi-column' ||
|
||||
(!snapStates.settings.shortcutsViewMode &&
|
||||
snapStates.settings.shortcutsColumnsMode)) &&
|
||||
!snapStates.shortcuts.find((pin) => pin.type === 'following');
|
||||
|
||||
const bindLongPress = useLongPress(
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { Fragment } from 'preact';
|
||||
import { memo } from 'preact/compat';
|
||||
|
||||
import shortenNumber from '../utils/shorten-number';
|
||||
|
@ -221,9 +222,10 @@ function Notification({ notification, instance, isStatic }) {
|
|||
)}
|
||||
{_accounts?.length > 1 && (
|
||||
<p class="avatars-stack">
|
||||
{_accounts.slice(0, AVATARS_LIMIT).map((account, i) => (
|
||||
<>
|
||||
{_accounts.slice(0, AVATARS_LIMIT).map((account) => (
|
||||
<Fragment key={account.id}>
|
||||
<a
|
||||
key={account.id}
|
||||
href={account.url}
|
||||
rel="noopener noreferrer"
|
||||
class="account-avatar-stack"
|
||||
|
@ -261,7 +263,7 @@ function Notification({ notification, instance, isStatic }) {
|
|||
</div>
|
||||
)}
|
||||
</a>{' '}
|
||||
</>
|
||||
</Fragment>
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
|
|
|
@ -86,7 +86,8 @@
|
|||
transition: all 0.2s ease-out;
|
||||
}
|
||||
#shortcuts-settings-container .shortcuts-view-mode label.checked {
|
||||
box-shadow: inset 0 0 0 3px var(--link-color);
|
||||
box-shadow: inset 0 0 0 3px var(--link-color),
|
||||
inset 0 0 32px var(--link-faded-color);
|
||||
}
|
||||
#shortcuts-settings-container
|
||||
.shortcuts-view-mode
|
||||
|
|
|
@ -22,7 +22,7 @@ import Icon from './icon';
|
|||
import MenuConfirm from './menu-confirm';
|
||||
import Modal from './modal';
|
||||
|
||||
const SHORTCUTS_LIMIT = 9;
|
||||
export const SHORTCUTS_LIMIT = 9;
|
||||
|
||||
const TYPES = [
|
||||
'following',
|
||||
|
@ -104,6 +104,11 @@ const TYPE_PARAMS = {
|
|||
placeholder: 'e.g. PixelArt (Max 5, space-separated)',
|
||||
pattern: '[^#]+',
|
||||
},
|
||||
{
|
||||
text: 'Media only',
|
||||
name: 'media',
|
||||
type: 'checkbox',
|
||||
},
|
||||
{
|
||||
text: 'Instance',
|
||||
name: 'instance',
|
||||
|
@ -113,6 +118,14 @@ const TYPE_PARAMS = {
|
|||
},
|
||||
],
|
||||
};
|
||||
const fetchListTitle = pmem(async ({ id }) => {
|
||||
const list = await api().masto.v1.lists.$select(id).fetch();
|
||||
return list.title;
|
||||
});
|
||||
const fetchAccountTitle = pmem(async ({ id }) => {
|
||||
const account = await api().masto.v1.accounts.$select(id).fetch();
|
||||
return account.username || account.acct || account.displayName;
|
||||
});
|
||||
export const SHORTCUTS_META = {
|
||||
following: {
|
||||
id: 'home',
|
||||
|
@ -134,10 +147,7 @@ export const SHORTCUTS_META = {
|
|||
},
|
||||
list: {
|
||||
id: 'list',
|
||||
title: pmem(async ({ id }) => {
|
||||
const list = await api().masto.v1.lists.$select(id).fetch();
|
||||
return list.title;
|
||||
}),
|
||||
title: fetchListTitle,
|
||||
path: ({ id }) => `/l/${id}`,
|
||||
icon: 'list',
|
||||
},
|
||||
|
@ -163,10 +173,7 @@ export const SHORTCUTS_META = {
|
|||
},
|
||||
'account-statuses': {
|
||||
id: 'account-statuses',
|
||||
title: pmem(async ({ id }) => {
|
||||
const account = await api().masto.v1.accounts.$select(id).fetch();
|
||||
return account.username || account.acct || account.displayName;
|
||||
}),
|
||||
title: fetchAccountTitle,
|
||||
path: ({ id }) => `/a/${id}`,
|
||||
icon: 'user',
|
||||
},
|
||||
|
@ -186,51 +193,22 @@ export const SHORTCUTS_META = {
|
|||
id: 'hashtag',
|
||||
title: ({ hashtag }) => hashtag,
|
||||
subtitle: ({ instance }) => instance || api().instance,
|
||||
path: ({ hashtag, instance }) =>
|
||||
`${instance ? `/${instance}` : ''}/t/${hashtag.split(/\s+/).join('+')}`,
|
||||
path: ({ hashtag, instance, media }) =>
|
||||
`${instance ? `/${instance}` : ''}/t/${hashtag.split(/\s+/).join('+')}${
|
||||
media ? '?media=1' : ''
|
||||
}`,
|
||||
icon: 'hashtag',
|
||||
},
|
||||
};
|
||||
|
||||
function ShortcutsSettings({ onClose }) {
|
||||
const snapStates = useSnapshot(states);
|
||||
const { masto } = api();
|
||||
const { shortcuts } = snapStates;
|
||||
|
||||
const [lists, setLists] = useState([]);
|
||||
const [followedHashtags, setFollowedHashtags] = useState([]);
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
const [showImportExport, setShowImportExport] = useState(false);
|
||||
|
||||
const [shortcutsListParent] = useAutoAnimate();
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const lists = await masto.v1.lists.list();
|
||||
lists.sort((a, b) => a.title.localeCompare(b.title));
|
||||
setLists(lists);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
})();
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const iterator = masto.v1.followedTags.list();
|
||||
const tags = [];
|
||||
do {
|
||||
const { value, done } = await iterator.next();
|
||||
if (done || value?.length === 0) break;
|
||||
tags.push(...value);
|
||||
} while (true);
|
||||
setFollowedHashtags(tags);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div id="shortcuts-settings-container" class="sheet" tabindex="-1">
|
||||
{!!onClose && (
|
||||
|
@ -293,33 +271,6 @@ function ShortcutsSettings({ onClose }) {
|
|||
);
|
||||
})}
|
||||
</div>
|
||||
{/* <select
|
||||
value={snapStates.settings.shortcutsViewMode || 'float-button'}
|
||||
onChange={(e) => {
|
||||
states.settings.shortcutsViewMode = e.target.value;
|
||||
}}
|
||||
>
|
||||
<option value="float-button">Floating button</option>
|
||||
<option value="multi-column">Multi-column</option>
|
||||
<option value="tab-menu-bar">Tab/Menu bar </option>
|
||||
</select> */}
|
||||
{/* <p>
|
||||
<details>
|
||||
<summary class="insignificant">
|
||||
Experimental Multi-column mode
|
||||
</summary>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={snapStates.settings.shortcutsColumnsMode}
|
||||
onChange={(e) => {
|
||||
states.settings.shortcutsColumnsMode = e.target.checked;
|
||||
}}
|
||||
/>{' '}
|
||||
Show shortcuts in multiple columns instead of the floating button.
|
||||
</label>
|
||||
</details>
|
||||
</p> */}
|
||||
{shortcuts.length > 0 ? (
|
||||
<ol class="shortcuts-list" ref={shortcutsListParent}>
|
||||
{shortcuts.filter(Boolean).map((shortcut, i) => {
|
||||
|
@ -474,8 +425,6 @@ function ShortcutsSettings({ onClose }) {
|
|||
<ShortcutForm
|
||||
shortcut={showForm.shortcut}
|
||||
shortcutIndex={showForm.shortcutIndex}
|
||||
lists={lists}
|
||||
followedHashtags={followedHashtags}
|
||||
onSubmit={({ result, mode }) => {
|
||||
console.log('onSubmit', result);
|
||||
if (mode === 'edit') {
|
||||
|
@ -507,9 +456,27 @@ function ShortcutsSettings({ onClose }) {
|
|||
);
|
||||
}
|
||||
|
||||
const FETCH_MAX_AGE = 1000 * 60; // 1 minute
|
||||
const fetchLists = pmem(
|
||||
() => {
|
||||
const { masto } = api();
|
||||
return masto.v1.lists.list();
|
||||
},
|
||||
{
|
||||
maxAge: FETCH_MAX_AGE,
|
||||
},
|
||||
);
|
||||
const fetchFollowedHashtags = pmem(
|
||||
() => {
|
||||
const { masto } = api();
|
||||
return masto.v1.followedTags.list();
|
||||
},
|
||||
{
|
||||
maxAge: FETCH_MAX_AGE,
|
||||
},
|
||||
);
|
||||
|
||||
function ShortcutForm({
|
||||
lists,
|
||||
followedHashtags,
|
||||
onSubmit,
|
||||
disabled,
|
||||
shortcut,
|
||||
|
@ -520,6 +487,41 @@ function ShortcutForm({
|
|||
const editMode = !!shortcut;
|
||||
const [currentType, setCurrentType] = useState(shortcut?.type || null);
|
||||
|
||||
const [uiState, setUIState] = useState('default');
|
||||
const [lists, setLists] = useState([]);
|
||||
const [followedHashtags, setFollowedHashtags] = useState([]);
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (currentType !== 'list') return;
|
||||
try {
|
||||
setUIState('loading');
|
||||
const lists = await fetchLists();
|
||||
lists.sort((a, b) => a.title.localeCompare(b.title));
|
||||
setLists(lists);
|
||||
setUIState('default');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setUIState('error');
|
||||
}
|
||||
})();
|
||||
|
||||
(async () => {
|
||||
if (currentType !== 'hashtag') return;
|
||||
try {
|
||||
const iterator = fetchFollowedHashtags();
|
||||
const tags = [];
|
||||
do {
|
||||
const { value, done } = await iterator.next();
|
||||
if (done || value?.length === 0) break;
|
||||
tags.push(...value);
|
||||
} while (true);
|
||||
setFollowedHashtags(tags);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
})();
|
||||
}, [currentType]);
|
||||
|
||||
const formRef = useRef();
|
||||
useEffect(() => {
|
||||
if (editMode && currentType && TYPE_PARAMS[currentType]) {
|
||||
|
@ -608,7 +610,8 @@ function ShortcutForm({
|
|||
<select
|
||||
name="id"
|
||||
required={!notRequired}
|
||||
disabled={disabled}
|
||||
disabled={disabled || uiState === 'loading'}
|
||||
defaultValue={editMode ? shortcut.id : undefined}
|
||||
>
|
||||
{lists.map((list) => (
|
||||
<option value={list.id}>{list.title}</option>
|
||||
|
@ -653,7 +656,11 @@ function ShortcutForm({
|
|||
},
|
||||
)}
|
||||
<footer>
|
||||
<button type="submit" class="block" disabled={disabled}>
|
||||
<button
|
||||
type="submit"
|
||||
class="block"
|
||||
disabled={disabled || uiState === 'loading'}
|
||||
>
|
||||
{editMode ? 'Save' : 'Add'}
|
||||
</button>
|
||||
{editMode && (
|
||||
|
|
|
@ -152,7 +152,7 @@ shortcuts .tab-bar[hidden] {
|
|||
}
|
||||
}
|
||||
|
||||
@media (min-width: 40em) {
|
||||
@media (min-width: 40em) and (hover: hover) {
|
||||
#app[data-shortcuts-view-mode='tab-menu-bar'] .timeline-deck {
|
||||
margin-top: 44px;
|
||||
}
|
||||
|
|
|
@ -25,8 +25,8 @@ function Shortcuts() {
|
|||
return null;
|
||||
}
|
||||
if (
|
||||
settings.shortcutsColumnsMode ||
|
||||
settings.shortcutsViewMode === 'multi-column'
|
||||
settings.shortcutsViewMode === 'multi-column' ||
|
||||
(!settings.shortcutsViewMode && settings.shortcutsColumnsMode)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
@ -90,7 +90,13 @@ function Shortcuts() {
|
|||
return (
|
||||
<div id="shortcuts">
|
||||
{snapStates.settings.shortcutsViewMode === 'tab-menu-bar' ? (
|
||||
<nav class="tab-bar">
|
||||
<nav
|
||||
class="tab-bar"
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault();
|
||||
states.showShortcutsSettings = true;
|
||||
}}
|
||||
>
|
||||
<ul>
|
||||
{formattedShortcuts.map(
|
||||
({ id, path, title, subtitle, icon }, i) => {
|
||||
|
@ -146,6 +152,10 @@ function Shortcuts() {
|
|||
type="button"
|
||||
id="shortcuts-button"
|
||||
class="plain"
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault();
|
||||
states.showShortcutsSettings = true;
|
||||
}}
|
||||
onTransitionStart={(e) => {
|
||||
// Close menu if the button disappears
|
||||
try {
|
||||
|
|
|
@ -603,6 +603,7 @@
|
|||
margin-block: min(0.75em, 12px);
|
||||
white-space: pre-wrap;
|
||||
tab-size: 2;
|
||||
text-wrap: pretty;
|
||||
}
|
||||
.status .content p:first-child {
|
||||
margin-block-start: 0;
|
||||
|
@ -726,6 +727,7 @@
|
|||
white-space: pre-line;
|
||||
flex-basis: 15em;
|
||||
flex-grow: 1;
|
||||
text-wrap: pretty;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -846,7 +848,7 @@
|
|||
object-fit: cover;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.status .media {
|
||||
:is(.status, .media-post) .media {
|
||||
cursor: pointer;
|
||||
|
||||
&[data-has-alt] {
|
||||
|
@ -885,7 +887,7 @@ body:has(#modal-container .carousel) .status .media img:hover {
|
|||
position: relative;
|
||||
background-clip: padding-box;
|
||||
}
|
||||
.status :is(.media-video, .media-audio) .media-play {
|
||||
:is(.status, .media-post) :is(.media-video, .media-audio) .media-play {
|
||||
pointer-events: none;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
|
@ -902,10 +904,13 @@ body:has(#modal-container .carousel) .status .media img:hover {
|
|||
border-radius: 70px;
|
||||
transition: transform 0.2s ease-in-out;
|
||||
}
|
||||
.status :is(.media-video, .media-audio):hover:not(:active) .media-play {
|
||||
:is(.status, .media-post)
|
||||
:is(.media-video, .media-audio):hover:not(:active)
|
||||
.media-play {
|
||||
transform: translate(-50%, -50%) scale(1.1);
|
||||
}
|
||||
.status :is(.media-video, .media-audio)[data-formatted-duration]:after {
|
||||
:is(.status, .media-post)
|
||||
:is(.media-video, .media-audio)[data-formatted-duration]:after {
|
||||
font-size: 12px;
|
||||
pointer-events: none;
|
||||
content: attr(data-formatted-duration);
|
||||
|
@ -918,10 +923,10 @@ body:has(#modal-container .carousel) .status .media img:hover {
|
|||
border-radius: 4px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
.status .media-audio[data-formatted-duration]:after {
|
||||
:is(.status, .media-post) .media-audio[data-formatted-duration]:after {
|
||||
content: '♬ ' attr(data-formatted-duration);
|
||||
}
|
||||
.status .media-gif[data-label]:not(:hover):after {
|
||||
:is(.status, .media-post) .media-gif[data-label]:not(:hover):after {
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
pointer-events: none;
|
||||
|
@ -953,12 +958,13 @@ body:has(#modal-container .carousel) .status .media img:hover {
|
|||
.status .media-audio audio {
|
||||
width: 100%;
|
||||
} */
|
||||
.status .media-audio {
|
||||
:is(.status, .media-post) .media-audio {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 88px;
|
||||
background-image: radial-gradient(
|
||||
circle at center center,
|
||||
var(--bg-color),
|
||||
transparent,
|
||||
var(--bg-faded-color)
|
||||
),
|
||||
repeating-radial-gradient(
|
||||
|
@ -1078,7 +1084,6 @@ body:has(#modal-container .carousel) .status .media img:hover {
|
|||
font-size: 90%;
|
||||
z-index: 1;
|
||||
text-shadow: 0 var(--hairline-width) var(--bg-color);
|
||||
mix-blend-mode: luminosity;
|
||||
white-space: pre-line;
|
||||
|
||||
&:is(:hover, :focus) {
|
||||
|
@ -1202,6 +1207,7 @@ a:focus-visible .card img {
|
|||
box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
text-wrap: balance;
|
||||
}
|
||||
.card .meta {
|
||||
font-size: smaller;
|
||||
|
|
|
@ -13,6 +13,7 @@ import pThrottle from 'p-throttle';
|
|||
import { memo } from 'preact/compat';
|
||||
import {
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
|
@ -34,6 +35,8 @@ import Poll from '../components/poll';
|
|||
import { api } from '../utils/api';
|
||||
import emojifyText from '../utils/emojify-text';
|
||||
import enhanceContent from '../utils/enhance-content';
|
||||
import FilterContext from '../utils/filter-context';
|
||||
import { isFiltered } from '../utils/filters';
|
||||
import getTranslateTargetLanguage from '../utils/get-translate-target-language';
|
||||
import getHTMLText from '../utils/getHTMLText';
|
||||
import handleContentLinks from '../utils/handle-content-links';
|
||||
|
@ -41,6 +44,7 @@ import htmlContentLength from '../utils/html-content-length';
|
|||
import isMastodonLinkMaybe from '../utils/isMastodonLinkMaybe';
|
||||
import localeMatch from '../utils/locale-match';
|
||||
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';
|
||||
|
@ -78,6 +82,10 @@ const visibilityText = {
|
|||
direct: 'Private mention',
|
||||
};
|
||||
|
||||
const isIOS =
|
||||
window.ontouchstart !== undefined &&
|
||||
/iPad|iPhone|iPod/.test(navigator.userAgent);
|
||||
|
||||
function Status({
|
||||
statusID,
|
||||
status,
|
||||
|
@ -90,7 +98,7 @@ function Status({
|
|||
enableTranslate,
|
||||
forceTranslate: _forceTranslate,
|
||||
previewMode,
|
||||
allowFilters,
|
||||
// allowFilters,
|
||||
onMediaClick,
|
||||
quoted,
|
||||
onStatusLinkClick = () => {},
|
||||
|
@ -166,9 +174,24 @@ function Status({
|
|||
// Non-API props
|
||||
_deleted,
|
||||
_pinned,
|
||||
_filtered,
|
||||
// _filtered,
|
||||
} = status;
|
||||
|
||||
const currentAccount = useMemo(() => {
|
||||
return store.session.get('currentAccount');
|
||||
}, []);
|
||||
const isSelf = useMemo(() => {
|
||||
return currentAccount && currentAccount === accountId;
|
||||
}, [accountId, currentAccount]);
|
||||
|
||||
const filterContext = useContext(FilterContext);
|
||||
const filterInfo =
|
||||
!isSelf && !readOnly && !previewMode && isFiltered(filtered, filterContext);
|
||||
|
||||
if (filterInfo?.action === 'hide') {
|
||||
return null;
|
||||
}
|
||||
|
||||
console.debug('RENDER Status', id, status?.account.displayName, quoted);
|
||||
|
||||
const debugHover = (e) => {
|
||||
|
@ -179,11 +202,11 @@ function Status({
|
|||
}
|
||||
};
|
||||
|
||||
if (allowFilters && size !== 'l' && _filtered) {
|
||||
if (/*allowFilters && */ size !== 'l' && filterInfo) {
|
||||
return (
|
||||
<FilteredStatus
|
||||
status={status}
|
||||
filterInfo={_filtered}
|
||||
filterInfo={filterInfo}
|
||||
instance={instance}
|
||||
containerProps={{
|
||||
onMouseEnter: debugHover,
|
||||
|
@ -195,13 +218,6 @@ function Status({
|
|||
const createdAtDate = new Date(createdAt);
|
||||
const editedAtDate = new Date(editedAt);
|
||||
|
||||
const currentAccount = useMemo(() => {
|
||||
return store.session.get('currentAccount');
|
||||
}, []);
|
||||
const isSelf = useMemo(() => {
|
||||
return currentAccount && currentAccount === accountId;
|
||||
}, [accountId, currentAccount]);
|
||||
|
||||
let inReplyToAccountRef = mentions?.find(
|
||||
(mention) => mention.id === inReplyToAccountId,
|
||||
);
|
||||
|
@ -238,7 +254,11 @@ function Status({
|
|||
|
||||
if (group) {
|
||||
return (
|
||||
<div class="status-group" onMouseEnter={debugHover}>
|
||||
<div
|
||||
data-state-post-id={sKey}
|
||||
class="status-group"
|
||||
onMouseEnter={debugHover}
|
||||
>
|
||||
<div class="status-pre-meta">
|
||||
<Icon icon="group" size="l" alt="Group" />{' '}
|
||||
<NameText account={status.account} instance={instance} showAvatar />
|
||||
|
@ -249,13 +269,18 @@ function Status({
|
|||
instance={instance}
|
||||
size={size}
|
||||
contentTextWeight={contentTextWeight}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="status-reblog" onMouseEnter={debugHover}>
|
||||
<div
|
||||
data-state-post-id={sKey}
|
||||
class="status-reblog"
|
||||
onMouseEnter={debugHover}
|
||||
>
|
||||
<div class="status-pre-meta">
|
||||
<Icon icon="rocket" size="l" />{' '}
|
||||
<NameText account={status.account} instance={instance} showAvatar />{' '}
|
||||
|
@ -267,6 +292,7 @@ function Status({
|
|||
instance={instance}
|
||||
size={size}
|
||||
contentTextWeight={contentTextWeight}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -348,10 +374,17 @@ function Status({
|
|||
canBoost = true;
|
||||
}
|
||||
|
||||
const replyStatus = () => {
|
||||
const replyStatus = (e) => {
|
||||
if (!sameInstance || !authenticated) {
|
||||
return alert(unauthInteractionErrorMessage);
|
||||
}
|
||||
// syntheticEvent comes from MenuItem
|
||||
if (e?.shiftKey || e?.syntheticEvent?.shiftKey) {
|
||||
const newWin = openCompose({
|
||||
replyToStatus: status,
|
||||
});
|
||||
if (newWin) return;
|
||||
}
|
||||
states.showCompose = {
|
||||
replyToStatus: status,
|
||||
};
|
||||
|
@ -795,13 +828,13 @@ function Status({
|
|||
const contextMenuRef = useRef();
|
||||
const [isContextMenuOpen, setIsContextMenuOpen] = useState(false);
|
||||
const [contextMenuProps, setContextMenuProps] = useState({});
|
||||
const isIOS =
|
||||
window.ontouchstart !== undefined &&
|
||||
/iPad|iPhone|iPod/.test(navigator.userAgent);
|
||||
|
||||
const showContextMenu = !isSizeLarge && !previewMode && !_deleted && !quoted;
|
||||
|
||||
// Only iOS/iPadOS browsers don't support contextmenu
|
||||
// Some comments report iPadOS might support contextmenu if a mouse is connected
|
||||
const bindLongPressContext = useLongPress(
|
||||
isIOS
|
||||
isIOS && showContextMenu
|
||||
? (e) => {
|
||||
if (e.pointerType === 'mouse') return;
|
||||
// There's 'pen' too, but not sure if contextmenu event would trigger from a pen
|
||||
|
@ -829,10 +862,8 @@ function Status({
|
|||
},
|
||||
);
|
||||
|
||||
const showContextMenu = size !== 'l' && !previewMode && !_deleted && !quoted;
|
||||
|
||||
const hotkeysEnabled = !readOnly && !previewMode;
|
||||
const rRef = useHotkeys('r', replyStatus, {
|
||||
const rRef = useHotkeys('r, shift+r', replyStatus, {
|
||||
enabled: hotkeysEnabled,
|
||||
});
|
||||
const fRef = useHotkeys(
|
||||
|
@ -960,6 +991,7 @@ function Status({
|
|||
|
||||
return (
|
||||
<article
|
||||
data-state-post-id={sKey}
|
||||
ref={(node) => {
|
||||
statusRef.current = node;
|
||||
// Use parent node if it's in focus
|
||||
|
@ -1091,6 +1123,15 @@ function Status({
|
|||
<Link
|
||||
to={instance ? `/${instance}/s/${id}` : `/s/${id}`}
|
||||
onClick={(e) => {
|
||||
if (
|
||||
e.metaKey ||
|
||||
e.ctrlKey ||
|
||||
e.shiftKey ||
|
||||
e.altKey ||
|
||||
e.which === 2
|
||||
) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onStatusLinkClick?.(e, status);
|
||||
|
@ -1457,6 +1498,7 @@ function Status({
|
|||
{' '}
|
||||
• <Icon icon="pencil" alt="Edited" />{' '}
|
||||
<time
|
||||
tabIndex="0"
|
||||
class="edited"
|
||||
datetime={editedAtDate.toISOString()}
|
||||
onClick={() => {
|
||||
|
@ -1576,10 +1618,11 @@ function Status({
|
|||
</div>
|
||||
{!!showEdited && (
|
||||
<Modal
|
||||
class="light"
|
||||
onClick={(e) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
setShowEdited(false);
|
||||
statusRef.current?.focus();
|
||||
// statusRef.current?.focus();
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
@ -1691,7 +1734,9 @@ function Card({ card, instance }) {
|
|||
if (snapStates.unfurledLinks[url]) return null;
|
||||
|
||||
if (hasText && (image || (type === 'photo' && blurhash))) {
|
||||
const domain = new URL(url).hostname.replace(/^www\./, '');
|
||||
const domain = new URL(url).hostname
|
||||
.replace(/^www\./, '')
|
||||
.replace(/\/$/, '');
|
||||
let blurhashImage;
|
||||
if (!image) {
|
||||
const w = 44;
|
||||
|
@ -1737,7 +1782,10 @@ function Card({ card, instance }) {
|
|||
{title}
|
||||
</p>
|
||||
<p class="meta" dir="auto">
|
||||
{description || providerName || authorName}
|
||||
{description ||
|
||||
(!!publishedAt && (
|
||||
<RelativeTime datetime={publishedAt} format="micro" />
|
||||
))}
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
|
@ -2120,10 +2168,24 @@ function _unfurlMastodonLink(instance, url) {
|
|||
|
||||
let remoteInstanceFetch;
|
||||
let theURL = url;
|
||||
if (/\/\/elk\.[^\/]+\/[^.]+\.[^.]+/i.test(theURL)) {
|
||||
// E.g. https://elk.zone/domain.com/@stest/123 -> https://domain.com/@stest/123
|
||||
|
||||
// https://elk.zone/domain.com/@stest/123 -> https://domain.com/@stest/123
|
||||
if (/\/\/elk\.[^\/]+\/[^\/]+\.[^\/]+/i.test(theURL)) {
|
||||
theURL = theURL.replace(/elk\.[^\/]+\//i, '');
|
||||
}
|
||||
|
||||
// https://trunks.social/status/domain.com/@stest/123 -> https://domain.com/@stest/123
|
||||
if (/\/\/trunks\.[^\/]+\/status\/[^\/]+\.[^\/]+/i.test(theURL)) {
|
||||
theURL = theURL.replace(/trunks\.[^\/]+\/status\//i, '');
|
||||
}
|
||||
|
||||
// https://phanpy.social/#/domain.com/s/123 -> https://domain.com/statuses/123
|
||||
if (/\/#\/[^\/]+\.[^\/]+\/s\/.+/i.test(theURL)) {
|
||||
const urlAfterHash = theURL.split('/#/')[1];
|
||||
const finalURL = urlAfterHash.replace(/\/s\//i, '/@fakeUsername/');
|
||||
theURL = `https://${finalURL}`;
|
||||
}
|
||||
|
||||
const urlObj = new URL(theURL);
|
||||
const domain = urlObj.hostname;
|
||||
const path = urlObj.pathname;
|
||||
|
@ -2151,7 +2213,7 @@ function _unfurlMastodonLink(instance, url) {
|
|||
const { masto } = api({ instance });
|
||||
const mastoSearchFetch = masto.v2.search
|
||||
.fetch({
|
||||
q: url,
|
||||
q: theURL,
|
||||
type: 'statuses',
|
||||
resolve: true,
|
||||
limit: 1,
|
||||
|
@ -2224,6 +2286,7 @@ const unfurlMastodonLink = throttle(_unfurlMastodonLink);
|
|||
|
||||
function FilteredStatus({ status, filterInfo, instance, containerProps = {} }) {
|
||||
const {
|
||||
id: statusID,
|
||||
account: { avatar, avatarStatic, bot, group },
|
||||
createdAt,
|
||||
visibility,
|
||||
|
@ -2248,6 +2311,15 @@ function FilteredStatus({ status, filterInfo, instance, containerProps = {} }) {
|
|||
);
|
||||
|
||||
const statusPeekRef = useTruncated();
|
||||
const sKey =
|
||||
statusKey(status.id, instance) +
|
||||
' ' +
|
||||
(statusKey(reblog?.id, instance) || '');
|
||||
|
||||
const actualStatusID = reblog?.id || statusID;
|
||||
const url = instance
|
||||
? `/${instance}/s/${actualStatusID}`
|
||||
: `/s/${actualStatusID}`;
|
||||
|
||||
return (
|
||||
<div
|
||||
|
@ -2260,7 +2332,7 @@ function FilteredStatus({ status, filterInfo, instance, containerProps = {} }) {
|
|||
}}
|
||||
{...bindLongPressPeek()}
|
||||
>
|
||||
<article class="status filtered" tabindex="-1">
|
||||
<article data-state-post-id={sKey} class="status filtered" tabindex="-1">
|
||||
<b
|
||||
class="status-filtered-badge clickable badge-meta"
|
||||
title={filterTitleStr}
|
||||
|
@ -2324,7 +2396,7 @@ function FilteredStatus({ status, filterInfo, instance, containerProps = {} }) {
|
|||
<Link
|
||||
ref={statusPeekRef}
|
||||
class="status-link"
|
||||
to={`/${instance}/s/${status.id}`}
|
||||
to={url}
|
||||
onClick={() => {
|
||||
setShowPeek(false);
|
||||
}}
|
||||
|
|
|
@ -4,6 +4,8 @@ import { InView } from 'react-intersection-observer';
|
|||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import { useSnapshot } from 'valtio';
|
||||
|
||||
import FilterContext from '../utils/filter-context';
|
||||
import { isFiltered } from '../utils/filters';
|
||||
import states, { statusKey } from '../utils/states';
|
||||
import statusPeek from '../utils/status-peek';
|
||||
import { groupBoosts, groupContext } from '../utils/timeline-utils';
|
||||
|
@ -13,6 +15,7 @@ import useScroll from '../utils/useScroll';
|
|||
|
||||
import Icon from './icon';
|
||||
import Link from './link';
|
||||
import MediaPost from './media-post';
|
||||
import NavMenu from './nav-menu';
|
||||
import Status from './status';
|
||||
|
||||
|
@ -33,12 +36,14 @@ function Timeline({
|
|||
boostsCarousel,
|
||||
fetchItems = () => {},
|
||||
checkForUpdates = () => {},
|
||||
checkForUpdatesInterval = 60_000, // 1 minute
|
||||
checkForUpdatesInterval = 15_000, // 15 seconds
|
||||
headerStart,
|
||||
headerEnd,
|
||||
timelineStart,
|
||||
allowFilters,
|
||||
// allowFilters,
|
||||
refresh,
|
||||
view,
|
||||
filterContext,
|
||||
}) {
|
||||
const snapStates = useSnapshot(states);
|
||||
const [items, setItems] = useState([]);
|
||||
|
@ -50,6 +55,7 @@ function Timeline({
|
|||
|
||||
console.debug('RENDER Timeline', id, refresh);
|
||||
|
||||
const allowGrouping = view !== 'media';
|
||||
const loadItems = useDebouncedCallback(
|
||||
(firstLoad) => {
|
||||
setShowNew(false);
|
||||
|
@ -59,10 +65,12 @@ function Timeline({
|
|||
try {
|
||||
let { done, value } = await fetchItems(firstLoad);
|
||||
if (Array.isArray(value)) {
|
||||
if (boostsCarousel) {
|
||||
value = groupBoosts(value);
|
||||
if (allowGrouping) {
|
||||
if (boostsCarousel) {
|
||||
value = groupBoosts(value);
|
||||
}
|
||||
value = groupContext(value);
|
||||
}
|
||||
value = groupContext(value);
|
||||
console.log(value);
|
||||
if (firstLoad) {
|
||||
setItems(value);
|
||||
|
@ -210,25 +218,38 @@ function Timeline({
|
|||
}
|
||||
}, [nearReachEnd, showMore]);
|
||||
|
||||
const prevView = useRef(view);
|
||||
useEffect(() => {
|
||||
if (prevView.current !== view) {
|
||||
prevView.current = view;
|
||||
setItems([]);
|
||||
}
|
||||
}, [view]);
|
||||
|
||||
const loadOrCheckUpdates = useCallback(
|
||||
async ({ disableIdleCheck = false } = {}) => {
|
||||
console.log('✨ Load or check updates', {
|
||||
const noPointers = scrollableRef.current
|
||||
? getComputedStyle(scrollableRef.current).pointerEvents === 'none'
|
||||
: false;
|
||||
console.log('✨ Load or check updates', id, {
|
||||
autoRefresh: snapStates.settings.autoRefresh,
|
||||
scrollTop: scrollableRef.current.scrollTop,
|
||||
disableIdleCheck,
|
||||
idle: window.__IDLE__,
|
||||
inBackground: inBackground(),
|
||||
noPointers,
|
||||
});
|
||||
if (
|
||||
snapStates.settings.autoRefresh &&
|
||||
scrollableRef.current.scrollTop === 0 &&
|
||||
scrollableRef.current.scrollTop < 16 &&
|
||||
(disableIdleCheck || window.__IDLE__) &&
|
||||
!inBackground()
|
||||
!inBackground() &&
|
||||
!noPointers
|
||||
) {
|
||||
console.log('✨ Load updates', snapStates.settings.autoRefresh);
|
||||
console.log('✨ Load updates', id, snapStates.settings.autoRefresh);
|
||||
loadItems(true);
|
||||
} else {
|
||||
console.log('✨ Check updates', snapStates.settings.autoRefresh);
|
||||
console.log('✨ Check updates', id, snapStates.settings.autoRefresh);
|
||||
const hasUpdate = await checkForUpdates();
|
||||
if (hasUpdate) {
|
||||
console.log('✨ Has new updates', id);
|
||||
|
@ -238,10 +259,6 @@ function Timeline({
|
|||
},
|
||||
[id, loadItems, checkForUpdates, snapStates.settings.autoRefresh],
|
||||
);
|
||||
const debouncedLoadOrCheckUpdates = useDebouncedCallback(
|
||||
loadOrCheckUpdates,
|
||||
3000,
|
||||
);
|
||||
|
||||
const lastHiddenTime = useRef();
|
||||
usePageVisibility(
|
||||
|
@ -249,14 +266,12 @@ function Timeline({
|
|||
if (visible) {
|
||||
const timeDiff = Date.now() - lastHiddenTime.current;
|
||||
if (!lastHiddenTime.current || timeDiff > 1000 * 60) {
|
||||
// 1 minute
|
||||
debouncedLoadOrCheckUpdates({
|
||||
loadOrCheckUpdates({
|
||||
disableIdleCheck: true,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
lastHiddenTime.current = Date.now();
|
||||
debouncedLoadOrCheckUpdates.cancel();
|
||||
}
|
||||
setVisible(visible);
|
||||
},
|
||||
|
@ -272,293 +287,334 @@ function Timeline({
|
|||
const hiddenUI = scrollDirection === 'end' && !nearReachStart;
|
||||
|
||||
return (
|
||||
<div
|
||||
id={`${id}-page`}
|
||||
class="deck-container"
|
||||
ref={(node) => {
|
||||
scrollableRef.current = node;
|
||||
jRef.current = node;
|
||||
kRef.current = node;
|
||||
oRef.current = node;
|
||||
}}
|
||||
tabIndex="-1"
|
||||
>
|
||||
<div class="timeline-deck deck">
|
||||
<header
|
||||
hidden={hiddenUI}
|
||||
onClick={(e) => {
|
||||
if (!e.target.closest('a, button')) {
|
||||
scrollableRef.current?.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}
|
||||
}}
|
||||
onDblClick={(e) => {
|
||||
if (!e.target.closest('a, button')) {
|
||||
loadItems(true);
|
||||
}
|
||||
}}
|
||||
class={uiState === 'loading' ? 'loading' : ''}
|
||||
>
|
||||
<div class="header-grid">
|
||||
<div class="header-side">
|
||||
<NavMenu />
|
||||
{headerStart !== null && headerStart !== undefined ? (
|
||||
headerStart
|
||||
) : (
|
||||
<Link to="/" class="button plain home-button">
|
||||
<Icon icon="home" size="l" />
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
{title && (titleComponent ? titleComponent : <h1>{title}</h1>)}
|
||||
<div class="header-side">
|
||||
{/* <Loader hidden={uiState !== 'loading'} /> */}
|
||||
{!!headerEnd && headerEnd}
|
||||
</div>
|
||||
</div>
|
||||
{items.length > 0 &&
|
||||
uiState !== 'loading' &&
|
||||
!hiddenUI &&
|
||||
showNew && (
|
||||
<button
|
||||
class="updates-button shiny-pill"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
loadItems(true);
|
||||
scrollableRef.current?.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Icon icon="arrow-up" /> New posts
|
||||
</button>
|
||||
)}
|
||||
</header>
|
||||
{!!timelineStart && (
|
||||
<div
|
||||
class={`timeline-start ${uiState === 'loading' ? 'loading' : ''}`}
|
||||
<FilterContext.Provider value={filterContext}>
|
||||
<div
|
||||
id={`${id}-page`}
|
||||
class="deck-container"
|
||||
ref={(node) => {
|
||||
scrollableRef.current = node;
|
||||
jRef.current = node;
|
||||
kRef.current = node;
|
||||
oRef.current = node;
|
||||
}}
|
||||
tabIndex="-1"
|
||||
>
|
||||
<div class="timeline-deck deck">
|
||||
<header
|
||||
hidden={hiddenUI}
|
||||
onClick={(e) => {
|
||||
if (!e.target.closest('a, button')) {
|
||||
scrollableRef.current?.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}
|
||||
}}
|
||||
onDblClick={(e) => {
|
||||
if (!e.target.closest('a, button')) {
|
||||
loadItems(true);
|
||||
}
|
||||
}}
|
||||
class={uiState === 'loading' ? 'loading' : ''}
|
||||
>
|
||||
{timelineStart}
|
||||
</div>
|
||||
)}
|
||||
{!!items.length ? (
|
||||
<>
|
||||
<ul class="timeline">
|
||||
{items.map((status) => {
|
||||
const { id: statusID, reblog, items, type, _pinned } = status;
|
||||
const actualStatusID = reblog?.id || statusID;
|
||||
const url = instance
|
||||
? `/${instance}/s/${actualStatusID}`
|
||||
: `/s/${actualStatusID}`;
|
||||
let title = '';
|
||||
if (type === 'boosts') {
|
||||
title = `${items.length} Boosts`;
|
||||
} else if (type === 'pinned') {
|
||||
title = 'Pinned posts';
|
||||
}
|
||||
const isCarousel = type === 'boosts' || type === 'pinned';
|
||||
if (items) {
|
||||
if (isCarousel) {
|
||||
// Here, we don't hide filtered posts, but we sort them last
|
||||
items.sort((a, b) => {
|
||||
if (a._filtered && !b._filtered) {
|
||||
return 1;
|
||||
}
|
||||
if (!a._filtered && b._filtered) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
<div class="header-grid">
|
||||
<div class="header-side">
|
||||
<NavMenu />
|
||||
{headerStart !== null && headerStart !== undefined ? (
|
||||
headerStart
|
||||
) : (
|
||||
<Link to="/" class="button plain home-button">
|
||||
<Icon icon="home" size="l" />
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
{title && (titleComponent ? titleComponent : <h1>{title}</h1>)}
|
||||
<div class="header-side">
|
||||
{/* <Loader hidden={uiState !== 'loading'} /> */}
|
||||
{!!headerEnd && headerEnd}
|
||||
</div>
|
||||
</div>
|
||||
{items.length > 0 &&
|
||||
uiState !== 'loading' &&
|
||||
!hiddenUI &&
|
||||
showNew && (
|
||||
<button
|
||||
class="updates-button shiny-pill"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
loadItems(true);
|
||||
scrollableRef.current?.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
return (
|
||||
<li
|
||||
key={`timeline-${statusID}`}
|
||||
class="timeline-item-carousel"
|
||||
>
|
||||
<StatusCarousel
|
||||
title={title}
|
||||
class={`${type}-carousel`}
|
||||
>
|
||||
{items.map((item) => {
|
||||
const { id: statusID, reblog } = item;
|
||||
const actualStatusID = reblog?.id || statusID;
|
||||
const url = instance
|
||||
? `/${instance}/s/${actualStatusID}`
|
||||
: `/s/${actualStatusID}`;
|
||||
return (
|
||||
<li key={statusID}>
|
||||
<Link
|
||||
class="status-carousel-link timeline-item-alt"
|
||||
to={url}
|
||||
>
|
||||
{useItemID ? (
|
||||
<Status
|
||||
statusID={statusID}
|
||||
instance={instance}
|
||||
size="s"
|
||||
contentTextWeight
|
||||
allowFilters={allowFilters}
|
||||
/>
|
||||
) : (
|
||||
<Status
|
||||
status={item}
|
||||
instance={instance}
|
||||
size="s"
|
||||
contentTextWeight
|
||||
allowFilters={allowFilters}
|
||||
/>
|
||||
)}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</StatusCarousel>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
const manyItems = items.length > 3;
|
||||
return items.map((item, i) => {
|
||||
const { id: statusID, _differentAuthor } = item;
|
||||
const url = instance
|
||||
? `/${instance}/s/${statusID}`
|
||||
: `/s/${statusID}`;
|
||||
const isMiddle = i > 0 && i < items.length - 1;
|
||||
const isSpoiler = item.sensitive && !!item.spoilerText;
|
||||
const showCompact =
|
||||
(!_differentAuthor && isSpoiler && i > 0) ||
|
||||
(manyItems &&
|
||||
isMiddle &&
|
||||
(type === 'thread' ||
|
||||
(type === 'conversation' &&
|
||||
!_differentAuthor &&
|
||||
!items[i - 1]._differentAuthor &&
|
||||
!items[i + 1]._differentAuthor)));
|
||||
return (
|
||||
<li
|
||||
key={`timeline-${statusID}`}
|
||||
class={`timeline-item-container timeline-item-container-type-${type} timeline-item-container-${
|
||||
i === 0
|
||||
? 'start'
|
||||
: i === items.length - 1
|
||||
? 'end'
|
||||
: 'middle'
|
||||
} ${
|
||||
_differentAuthor ? 'timeline-item-diff-author' : ''
|
||||
}`}
|
||||
>
|
||||
<Link class="status-link timeline-item" to={url}>
|
||||
{showCompact ? (
|
||||
<TimelineStatusCompact
|
||||
status={item}
|
||||
instance={instance}
|
||||
/>
|
||||
) : useItemID ? (
|
||||
<Status
|
||||
statusID={statusID}
|
||||
instance={instance}
|
||||
allowFilters={allowFilters}
|
||||
/>
|
||||
) : (
|
||||
<Status
|
||||
status={item}
|
||||
instance={instance}
|
||||
allowFilters={allowFilters}
|
||||
/>
|
||||
)}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
});
|
||||
}
|
||||
return (
|
||||
<li key={`timeline-${statusID + _pinned}`}>
|
||||
<Link class="status-link timeline-item" to={url}>
|
||||
{useItemID ? (
|
||||
<Status
|
||||
statusID={statusID}
|
||||
instance={instance}
|
||||
allowFilters={allowFilters}
|
||||
/>
|
||||
) : (
|
||||
<Status
|
||||
status={status}
|
||||
instance={instance}
|
||||
allowFilters={allowFilters}
|
||||
/>
|
||||
)}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
{showMore && uiState === 'loading' && (
|
||||
<>
|
||||
<li
|
||||
style={{
|
||||
height: '20vh',
|
||||
}}
|
||||
>
|
||||
<Status skeleton />
|
||||
</li>
|
||||
<li
|
||||
style={{
|
||||
height: '25vh',
|
||||
}}
|
||||
>
|
||||
<Status skeleton />
|
||||
</li>
|
||||
</>
|
||||
)}
|
||||
</ul>
|
||||
{uiState === 'default' &&
|
||||
(showMore ? (
|
||||
<InView
|
||||
onChange={(inView) => {
|
||||
if (inView) {
|
||||
loadItems();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="plain block"
|
||||
onClick={() => loadItems()}
|
||||
style={{ marginBlockEnd: '6em' }}
|
||||
>
|
||||
Show more…
|
||||
</button>
|
||||
</InView>
|
||||
) : (
|
||||
<p class="ui-state insignificant">The end.</p>
|
||||
))}
|
||||
</>
|
||||
) : uiState === 'loading' ? (
|
||||
<ul class="timeline">
|
||||
{Array.from({ length: 5 }).map((_, i) => (
|
||||
<li key={i}>
|
||||
<Status skeleton />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
uiState !== 'error' && <p class="ui-state">{emptyText}</p>
|
||||
)}
|
||||
{uiState === 'error' && (
|
||||
<p class="ui-state">
|
||||
{errorText}
|
||||
<br />
|
||||
<br />
|
||||
<button
|
||||
class="button plain"
|
||||
onClick={() => loadItems(!items.length)}
|
||||
<Icon icon="arrow-up" /> New posts
|
||||
</button>
|
||||
)}
|
||||
</header>
|
||||
{!!timelineStart && (
|
||||
<div
|
||||
class={`timeline-start ${uiState === 'loading' ? 'loading' : ''}`}
|
||||
>
|
||||
Try again
|
||||
</button>
|
||||
</p>
|
||||
)}
|
||||
{timelineStart}
|
||||
</div>
|
||||
)}
|
||||
{!!items.length ? (
|
||||
<>
|
||||
<ul class={`timeline ${view ? `timeline-${view}` : ''}`}>
|
||||
{items.map((status) => (
|
||||
<TimelineItem
|
||||
status={status}
|
||||
instance={instance}
|
||||
useItemID={useItemID}
|
||||
// allowFilters={allowFilters}
|
||||
filterContext={filterContext}
|
||||
key={status.id + status?._pinned}
|
||||
view={view}
|
||||
/>
|
||||
))}
|
||||
{showMore &&
|
||||
uiState === 'loading' &&
|
||||
(view === 'media' ? null : (
|
||||
<>
|
||||
<li
|
||||
style={{
|
||||
height: '20vh',
|
||||
}}
|
||||
>
|
||||
<Status skeleton />
|
||||
</li>
|
||||
<li
|
||||
style={{
|
||||
height: '25vh',
|
||||
}}
|
||||
>
|
||||
<Status skeleton />
|
||||
</li>
|
||||
</>
|
||||
))}
|
||||
</ul>
|
||||
{uiState === 'default' &&
|
||||
(showMore ? (
|
||||
<InView
|
||||
onChange={(inView) => {
|
||||
if (inView) {
|
||||
loadItems();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="plain block"
|
||||
onClick={() => loadItems()}
|
||||
style={{ marginBlockEnd: '6em' }}
|
||||
>
|
||||
Show more…
|
||||
</button>
|
||||
</InView>
|
||||
) : (
|
||||
<p class="ui-state insignificant">The end.</p>
|
||||
))}
|
||||
</>
|
||||
) : uiState === 'loading' ? (
|
||||
<ul class="timeline">
|
||||
{Array.from({ length: 5 }).map((_, i) =>
|
||||
view === 'media' ? (
|
||||
<div
|
||||
style={{
|
||||
height: '50vh',
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<li key={i}>
|
||||
<Status skeleton />
|
||||
</li>
|
||||
),
|
||||
)}
|
||||
</ul>
|
||||
) : (
|
||||
uiState !== 'error' && <p class="ui-state">{emptyText}</p>
|
||||
)}
|
||||
{uiState === 'error' && (
|
||||
<p class="ui-state">
|
||||
{errorText}
|
||||
<br />
|
||||
<br />
|
||||
<button type="button" onClick={() => loadItems(!items.length)}>
|
||||
Try again
|
||||
</button>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FilterContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function TimelineItem({
|
||||
status,
|
||||
instance,
|
||||
useItemID,
|
||||
// allowFilters,
|
||||
filterContext,
|
||||
view,
|
||||
}) {
|
||||
const { id: statusID, reblog, items, type, _pinned } = status;
|
||||
const actualStatusID = reblog?.id || statusID;
|
||||
const url = instance
|
||||
? `/${instance}/s/${actualStatusID}`
|
||||
: `/s/${actualStatusID}`;
|
||||
let title = '';
|
||||
if (type === 'boosts') {
|
||||
title = `${items.length} Boosts`;
|
||||
} else if (type === 'pinned') {
|
||||
title = 'Pinned posts';
|
||||
}
|
||||
const isCarousel = type === 'boosts' || type === 'pinned';
|
||||
if (items) {
|
||||
if (isCarousel) {
|
||||
// Here, we don't hide filtered posts, but we sort them last
|
||||
items.sort((a, b) => {
|
||||
// if (a._filtered && !b._filtered) {
|
||||
// return 1;
|
||||
// }
|
||||
// if (!a._filtered && b._filtered) {
|
||||
// return -1;
|
||||
// }
|
||||
const aFiltered = isFiltered(a.filtered, filterContext);
|
||||
const bFiltered = isFiltered(b.filtered, filterContext);
|
||||
if (aFiltered && !bFiltered) {
|
||||
return 1;
|
||||
}
|
||||
if (!aFiltered && bFiltered) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
return (
|
||||
<li key={`timeline-${statusID}`} class="timeline-item-carousel">
|
||||
<StatusCarousel title={title} class={`${type}-carousel`}>
|
||||
{items.map((item) => {
|
||||
const { id: statusID, reblog } = item;
|
||||
const actualStatusID = reblog?.id || statusID;
|
||||
const url = instance
|
||||
? `/${instance}/s/${actualStatusID}`
|
||||
: `/s/${actualStatusID}`;
|
||||
return (
|
||||
<li key={statusID}>
|
||||
<Link class="status-carousel-link timeline-item-alt" to={url}>
|
||||
{useItemID ? (
|
||||
<Status
|
||||
statusID={statusID}
|
||||
instance={instance}
|
||||
size="s"
|
||||
contentTextWeight
|
||||
// allowFilters={allowFilters}
|
||||
/>
|
||||
) : (
|
||||
<Status
|
||||
status={item}
|
||||
instance={instance}
|
||||
size="s"
|
||||
contentTextWeight
|
||||
// allowFilters={allowFilters}
|
||||
/>
|
||||
)}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</StatusCarousel>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
const manyItems = items.length > 3;
|
||||
return items.map((item, i) => {
|
||||
const { id: statusID, _differentAuthor } = item;
|
||||
const url = instance ? `/${instance}/s/${statusID}` : `/s/${statusID}`;
|
||||
const isMiddle = i > 0 && i < items.length - 1;
|
||||
const isSpoiler = item.sensitive && !!item.spoilerText;
|
||||
const showCompact =
|
||||
(!_differentAuthor && isSpoiler && i > 0) ||
|
||||
(manyItems &&
|
||||
isMiddle &&
|
||||
(type === 'thread' ||
|
||||
(type === 'conversation' &&
|
||||
!_differentAuthor &&
|
||||
!items[i - 1]._differentAuthor &&
|
||||
!items[i + 1]._differentAuthor)));
|
||||
return (
|
||||
<li
|
||||
key={`timeline-${statusID}`}
|
||||
class={`timeline-item-container timeline-item-container-type-${type} timeline-item-container-${
|
||||
i === 0 ? 'start' : i === items.length - 1 ? 'end' : 'middle'
|
||||
} ${_differentAuthor ? 'timeline-item-diff-author' : ''}`}
|
||||
>
|
||||
<Link class="status-link timeline-item" to={url}>
|
||||
{showCompact ? (
|
||||
<TimelineStatusCompact status={item} instance={instance} />
|
||||
) : useItemID ? (
|
||||
<Status
|
||||
statusID={statusID}
|
||||
instance={instance}
|
||||
// allowFilters={allowFilters}
|
||||
/>
|
||||
) : (
|
||||
<Status
|
||||
status={item}
|
||||
instance={instance}
|
||||
// allowFilters={allowFilters}
|
||||
/>
|
||||
)}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const itemKey = `timeline-${statusID + _pinned}`;
|
||||
|
||||
if (view === 'media') {
|
||||
return useItemID ? (
|
||||
<MediaPost
|
||||
class="timeline-item"
|
||||
parent="li"
|
||||
key={itemKey}
|
||||
statusID={statusID}
|
||||
instance={instance}
|
||||
// allowFilters={allowFilters}
|
||||
/>
|
||||
) : (
|
||||
<MediaPost
|
||||
class="timeline-item"
|
||||
parent="li"
|
||||
key={itemKey}
|
||||
status={status}
|
||||
instance={instance}
|
||||
// allowFilters={allowFilters}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<li key={itemKey}>
|
||||
<Link class="status-link timeline-item" to={url}>
|
||||
{useItemID ? (
|
||||
<Status
|
||||
statusID={statusID}
|
||||
instance={instance}
|
||||
// allowFilters={allowFilters}
|
||||
/>
|
||||
) : (
|
||||
<Status
|
||||
status={status}
|
||||
instance={instance}
|
||||
// allowFilters={allowFilters}
|
||||
/>
|
||||
)}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -83,6 +83,7 @@
|
|||
.status-translation-block .translated-block output {
|
||||
display: block;
|
||||
margin-top: 0.75em;
|
||||
text-wrap: pretty;
|
||||
}
|
||||
.status-translation-block
|
||||
.translated-block
|
||||
|
|
|
@ -1,5 +1,11 @@
|
|||
import { MenuItem } from '@szhsin/react-menu';
|
||||
import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'preact/hooks';
|
||||
import { useParams, useSearchParams } from 'react-router-dom';
|
||||
import { useSnapshot } from 'valtio';
|
||||
|
||||
|
@ -51,6 +57,7 @@ function AccountStatuses() {
|
|||
const tagged = searchParams.get('tagged');
|
||||
const media = !!searchParams.get('media');
|
||||
const { masto, instance, authenticated } = api({ instance: params.instance });
|
||||
const { masto: currentMasto, instance: currentInstance } = api();
|
||||
const accountStatusesIterator = useRef();
|
||||
|
||||
const allSearchParams = [month, excludeReplies, excludeBoosts, tagged, media];
|
||||
|
@ -61,8 +68,8 @@ function AccountStatuses() {
|
|||
}, allSearchParams);
|
||||
|
||||
const sameCurrentInstance = useMemo(
|
||||
() => instance === api().instance,
|
||||
[instance],
|
||||
() => instance === currentInstance,
|
||||
[instance, currentInstance],
|
||||
);
|
||||
const [searchEnabled, setSearchEnabled] = useState(false);
|
||||
useEffect(() => {
|
||||
|
@ -153,8 +160,8 @@ function AccountStatuses() {
|
|||
.next();
|
||||
if (pinnedStatuses?.length && !tagged && !media) {
|
||||
pinnedStatuses.forEach((status) => {
|
||||
status._pinned = true;
|
||||
saveStatus(status, instance);
|
||||
status._pinned = true;
|
||||
});
|
||||
if (pinnedStatuses.length >= 3) {
|
||||
const pinnedStatusesIds = pinnedStatuses.map((status) => status.id);
|
||||
|
@ -195,15 +202,41 @@ function AccountStatuses() {
|
|||
|
||||
const [featuredTags, setFeaturedTags] = useState([]);
|
||||
useTitle(
|
||||
`${account?.displayName ? account.displayName + ' ' : ''}@${
|
||||
account?.acct ? account.acct : 'Account posts'
|
||||
}`,
|
||||
account?.acct
|
||||
? `${account?.displayName ? account.displayName + ' ' : ''}@${
|
||||
account.acct
|
||||
}${
|
||||
!excludeReplies
|
||||
? ' (+ Replies)'
|
||||
: excludeBoosts
|
||||
? ' (- Boosts)'
|
||||
: tagged
|
||||
? ` (#${tagged})`
|
||||
: media
|
||||
? ' (Media)'
|
||||
: month
|
||||
? ` (${new Date(month).toLocaleString('default', {
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
})})`
|
||||
: ''
|
||||
}`
|
||||
: 'Account posts',
|
||||
'/:instance?/a/:id',
|
||||
);
|
||||
|
||||
const fetchAccountPromiseRef = useRef();
|
||||
const fetchAccount = useCallback(() => {
|
||||
const fetchPromise =
|
||||
fetchAccountPromiseRef.current || masto.v1.accounts.$select(id).fetch();
|
||||
fetchAccountPromiseRef.current = fetchPromise;
|
||||
return fetchPromise;
|
||||
}, [id, masto]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const acc = await masto.v1.accounts.$select(id).fetch();
|
||||
const acc = await fetchAccount();
|
||||
console.log(acc);
|
||||
setAccount(acc);
|
||||
} catch (e) {
|
||||
|
@ -223,21 +256,34 @@ function AccountStatuses() {
|
|||
|
||||
const { displayName, acct, emojis } = account || {};
|
||||
|
||||
const accountInfoMemo = useMemo(() => {
|
||||
const cachedAccount = snapStates.accounts[`${id}@${instance}`];
|
||||
return (
|
||||
<AccountInfo
|
||||
instance={instance}
|
||||
account={cachedAccount || id}
|
||||
fetchAccount={fetchAccount}
|
||||
authenticated={authenticated}
|
||||
standalone
|
||||
/>
|
||||
);
|
||||
}, [id, instance, authenticated, fetchAccount]);
|
||||
|
||||
const filterBarRef = useRef();
|
||||
const TimelineStart = useMemo(() => {
|
||||
const cachedAccount = snapStates.accounts[`${id}@${instance}`];
|
||||
const filtered =
|
||||
!excludeReplies || excludeBoosts || tagged || media || !!month;
|
||||
|
||||
return (
|
||||
<>
|
||||
<AccountInfo
|
||||
instance={instance}
|
||||
account={cachedAccount || id}
|
||||
fetchAccount={() => masto.v1.accounts.$select(id).fetch()}
|
||||
authenticated={authenticated}
|
||||
standalone
|
||||
/>
|
||||
<div class="filter-bar" ref={filterBarRef}>
|
||||
{accountInfoMemo}
|
||||
<div
|
||||
class="filter-bar"
|
||||
ref={filterBarRef}
|
||||
style={{
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
{filtered ? (
|
||||
<Link
|
||||
to={`/${instance}/a/${id}`}
|
||||
|
@ -328,6 +374,15 @@ function AccountStatuses() {
|
|||
}
|
||||
: {},
|
||||
);
|
||||
showToast(
|
||||
`Showing posts in ${new Date(value).toLocaleString(
|
||||
'default',
|
||||
{
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
},
|
||||
)}`,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
|
@ -392,7 +447,7 @@ function AccountStatuses() {
|
|||
title={`${account?.acct ? '@' + account.acct : 'Posts'}`}
|
||||
titleComponent={
|
||||
<h1
|
||||
class="header-account"
|
||||
class="header-double-lines header-account"
|
||||
// onClick={() => {
|
||||
// states.showAccount = {
|
||||
// account,
|
||||
|
@ -414,6 +469,7 @@ function AccountStatuses() {
|
|||
errorText="Unable to load posts"
|
||||
fetchItems={fetchAccountStatuses}
|
||||
useItemID
|
||||
view={media ? 'media' : undefined}
|
||||
boostsCarousel={snapStates.settings.boostsCarousel}
|
||||
timelineStart={TimelineStart}
|
||||
refresh={[
|
||||
|
@ -461,6 +517,29 @@ function AccountStatuses() {
|
|||
Switch to account's instance (<b>{accountInstance}</b>)
|
||||
</small>
|
||||
</MenuItem>
|
||||
{!sameCurrentInstance && (
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
(async () => {
|
||||
try {
|
||||
const acc = await currentMasto.v1.accounts.lookup({
|
||||
acct: account.acct + '@' + instance,
|
||||
});
|
||||
const { id } = acc;
|
||||
location.hash = `/${currentInstance}/a/${id}`;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert('Unable to fetch account info');
|
||||
}
|
||||
})();
|
||||
}}
|
||||
>
|
||||
<Icon icon="transfer" />{' '}
|
||||
<small class="menu-double-lines">
|
||||
Switch to my instance (<b>{currentInstance}</b>)
|
||||
</small>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Menu2>
|
||||
}
|
||||
/>
|
||||
|
|
|
@ -32,7 +32,7 @@ function Following({ title, path, id, ...props }) {
|
|||
console.log('First load', latestItem.current);
|
||||
}
|
||||
|
||||
value = filteredItems(value, 'home');
|
||||
// value = filteredItems(value, 'home');
|
||||
value.forEach((item) => {
|
||||
saveStatus(item, instance);
|
||||
});
|
||||
|
@ -115,7 +115,8 @@ function Following({ title, path, id, ...props }) {
|
|||
useItemID
|
||||
boostsCarousel={snapStates.settings.boostsCarousel}
|
||||
{...props}
|
||||
allowFilters
|
||||
// allowFilters
|
||||
filterContext="home"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -2,16 +2,19 @@ import {
|
|||
FocusableItem,
|
||||
MenuDivider,
|
||||
MenuGroup,
|
||||
MenuHeader,
|
||||
MenuItem,
|
||||
} from '@szhsin/react-menu';
|
||||
import { useEffect, useRef, useState } from 'preact/hooks';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
||||
|
||||
import Icon from '../components/icon';
|
||||
import Menu2 from '../components/menu2';
|
||||
import MenuConfirm from '../components/menu-confirm';
|
||||
import { SHORTCUTS_LIMIT } from '../components/shortcuts-settings';
|
||||
import Timeline from '../components/timeline';
|
||||
import { api } from '../utils/api';
|
||||
import { filteredItems } from '../utils/filters';
|
||||
import showToast from '../utils/show-toast';
|
||||
import states from '../utils/states';
|
||||
import { saveStatus } from '../utils/states';
|
||||
|
@ -25,20 +28,30 @@ const LIMIT = 20;
|
|||
const TAGS_LIMIT_PER_MODE = 4;
|
||||
const TOTAL_TAGS_LIMIT = TAGS_LIMIT_PER_MODE + 1;
|
||||
|
||||
function Hashtags({ columnMode, ...props }) {
|
||||
function Hashtags({ media: mediaView, columnMode, ...props }) {
|
||||
// const navigate = useNavigate();
|
||||
let { hashtag, ...params } = columnMode ? {} : useParams();
|
||||
if (props.hashtag) hashtag = props.hashtag;
|
||||
let hashtags = hashtag.trim().split(/[\s+]+/);
|
||||
hashtags.sort();
|
||||
hashtag = hashtags[0];
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const media = mediaView || !!searchParams.get('media');
|
||||
const linkParams = media ? '?media=1' : '';
|
||||
|
||||
const { masto, instance, authenticated } = api({
|
||||
instance: props?.instance || params.instance,
|
||||
});
|
||||
const { authenticated: currentAuthenticated } = api();
|
||||
const {
|
||||
masto: currentMasto,
|
||||
instance: currentInstance,
|
||||
authenticated: currentAuthenticated,
|
||||
} = api();
|
||||
const hashtagTitle = hashtags.map((t) => `#${t}`).join(' ');
|
||||
const title = instance ? `${hashtagTitle} on ${instance}` : hashtagTitle;
|
||||
const hashtagPostTitle = media ? ` (Media only)` : '';
|
||||
const title = instance
|
||||
? `${hashtagTitle}${hashtagPostTitle} on ${instance}`
|
||||
: `${hashtagTitle}${hashtagPostTitle}`;
|
||||
useTitle(title, `/:instance?/t/:hashtag`);
|
||||
const latestItem = useRef();
|
||||
|
||||
|
@ -60,16 +73,20 @@ function Hashtags({ columnMode, ...props }) {
|
|||
limit: LIMIT,
|
||||
any: hashtags.slice(1),
|
||||
maxId: firstLoad ? undefined : maxID.current,
|
||||
onlyMedia: media,
|
||||
})
|
||||
.next();
|
||||
const { value } = results;
|
||||
let { value } = results;
|
||||
if (value?.length) {
|
||||
if (firstLoad) {
|
||||
latestItem.current = value[0].id;
|
||||
}
|
||||
|
||||
// value = filteredItems(value, 'public');
|
||||
value.forEach((item) => {
|
||||
saveStatus(item, instance);
|
||||
saveStatus(item, instance, {
|
||||
skipThreading: media, // If media view, no need to form threads
|
||||
});
|
||||
});
|
||||
|
||||
maxID.current = value[value.length - 1].id;
|
||||
|
@ -88,9 +105,11 @@ function Hashtags({ columnMode, ...props }) {
|
|||
limit: 1,
|
||||
any: hashtags.slice(1),
|
||||
since_id: latestItem.current,
|
||||
onlyMedia: media,
|
||||
})
|
||||
.next();
|
||||
const { value } = results;
|
||||
let { value } = results;
|
||||
value = filteredItems(value, 'public');
|
||||
if (value?.length) {
|
||||
return true;
|
||||
}
|
||||
|
@ -123,7 +142,7 @@ function Hashtags({ columnMode, ...props }) {
|
|||
title={title}
|
||||
titleComponent={
|
||||
!!instance && (
|
||||
<h1 class="header-account">
|
||||
<h1 class="header-double-lines">
|
||||
<b>{hashtagTitle}</b>
|
||||
<div>{instance}</div>
|
||||
</h1>
|
||||
|
@ -136,6 +155,10 @@ function Hashtags({ columnMode, ...props }) {
|
|||
fetchItems={fetchHashtags}
|
||||
checkForUpdates={checkForUpdates}
|
||||
useItemID
|
||||
view={media ? 'media' : undefined}
|
||||
refresh={media}
|
||||
// allowFilters
|
||||
filterContext="public"
|
||||
headerEnd={
|
||||
<Menu2
|
||||
portal
|
||||
|
@ -209,6 +232,23 @@ function Hashtags({ columnMode, ...props }) {
|
|||
<MenuDivider />
|
||||
</>
|
||||
)}
|
||||
<MenuHeader className="plain">Filters</MenuHeader>
|
||||
<MenuItem
|
||||
type="checkbox"
|
||||
checked={!!media}
|
||||
onClick={() => {
|
||||
if (media) {
|
||||
searchParams.delete('media');
|
||||
} else {
|
||||
searchParams.set('media', '1');
|
||||
}
|
||||
setSearchParams(searchParams);
|
||||
}}
|
||||
>
|
||||
<Icon icon="check-circle" />{' '}
|
||||
<span class="menu-grow">Media only</span>
|
||||
</MenuItem>
|
||||
<MenuDivider />
|
||||
<FocusableItem className="menu-field" disabled={reachLimit}>
|
||||
{({ ref }) => (
|
||||
<form
|
||||
|
@ -231,7 +271,7 @@ function Hashtags({ columnMode, ...props }) {
|
|||
// );
|
||||
location.hash = instance
|
||||
? `/${instance}/t/${hashtags.join('+')}`
|
||||
: `/t/${hashtags.join('+')}`;
|
||||
: `/t/${hashtags.join('+')}${linkParams}`;
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
@ -267,8 +307,8 @@ function Hashtags({ columnMode, ...props }) {
|
|||
// : `/t/${hashtags.join('+')}`,
|
||||
// );
|
||||
location.hash = instance
|
||||
? `/${instance}/t/${hashtags.join('+')}`
|
||||
: `/t/${hashtags.join('+')}`;
|
||||
? `/${instance}/t/${hashtags.join('+')}${linkParams}`
|
||||
: `/t/${hashtags.join('+')}${linkParams}`;
|
||||
}}
|
||||
>
|
||||
<Icon icon="x" alt="Remove hashtag" class="danger-icon" />
|
||||
|
@ -283,10 +323,17 @@ function Hashtags({ columnMode, ...props }) {
|
|||
<MenuItem
|
||||
disabled={!currentAuthenticated}
|
||||
onClick={() => {
|
||||
if (states.shortcuts.length >= SHORTCUTS_LIMIT) {
|
||||
alert(
|
||||
`Max ${SHORTCUTS_LIMIT} shortcuts reached. Unable to add shortcut.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
const shortcut = {
|
||||
type: 'hashtag',
|
||||
hashtag: hashtags.join(' '),
|
||||
instance,
|
||||
media: media ? 'on' : undefined,
|
||||
};
|
||||
// Check if already exists
|
||||
const exists = states.shortcuts.some(
|
||||
|
@ -300,7 +347,8 @@ function Hashtags({ columnMode, ...props }) {
|
|||
.split(/[\s+]+/)
|
||||
.sort()
|
||||
.join(' ') &&
|
||||
(s.instance ? s.instance === shortcut.instance : true),
|
||||
(s.instance ? s.instance === shortcut.instance : true) &&
|
||||
(s.media ? !!s.media === !!shortcut.media : true),
|
||||
);
|
||||
if (exists) {
|
||||
alert('This shortcut already exists');
|
||||
|
@ -324,12 +372,28 @@ function Hashtags({ columnMode, ...props }) {
|
|||
if (newInstance) {
|
||||
newInstance = newInstance.toLowerCase().trim();
|
||||
// navigate(`/${newInstance}/t/${hashtags.join('+')}`);
|
||||
location.hash = `/${newInstance}/t/${hashtags.join('+')}`;
|
||||
location.hash = `/${newInstance}/t/${hashtags.join(
|
||||
'+',
|
||||
)}${linkParams}`;
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Icon icon="bus" /> <span>Go to another instance…</span>
|
||||
</MenuItem>
|
||||
{currentInstance !== instance && (
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
location.hash = `/${currentInstance}/t/${hashtags.join(
|
||||
'+',
|
||||
)}${linkParams}`;
|
||||
}}
|
||||
>
|
||||
<Icon icon="bus" />{' '}
|
||||
<small class="menu-double-lines">
|
||||
Go to my instance (<b>{currentInstance}</b>)
|
||||
</small>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Menu2>
|
||||
}
|
||||
/>
|
||||
|
|
|
@ -35,8 +35,9 @@ function Home() {
|
|||
|
||||
return (
|
||||
<>
|
||||
{(snapStates.settings.shortcutsColumnsMode ||
|
||||
snapStates.settings.shortcutsViewMode === 'multi-column') &&
|
||||
{(snapStates.settings.shortcutsViewMode === 'multi-column' ||
|
||||
(!snapStates.settings.shortcutsViewMode &&
|
||||
snapStates.settings.shortcutsColumnsMode)) &&
|
||||
!!snapStates.shortcuts?.length ? (
|
||||
<Columns />
|
||||
) : (
|
||||
|
|
|
@ -43,7 +43,7 @@ function List(props) {
|
|||
latestItem.current = value[0].id;
|
||||
}
|
||||
|
||||
value = filteredItems(value, 'home');
|
||||
// value = filteredItems(value, 'home');
|
||||
value.forEach((item) => {
|
||||
saveStatus(item, instance);
|
||||
});
|
||||
|
@ -102,7 +102,8 @@ function List(props) {
|
|||
checkForUpdates={checkForUpdates}
|
||||
useItemID
|
||||
boostsCarousel={snapStates.settings.boostsCarousel}
|
||||
allowFilters
|
||||
// allowFilters
|
||||
filterContext="home"
|
||||
// refresh={reloadCount}
|
||||
headerStart={
|
||||
<Link to="/l" class="button plain">
|
||||
|
|
|
@ -11,11 +11,11 @@ const LIMIT = 20;
|
|||
const emptySearchParams = new URLSearchParams();
|
||||
|
||||
function Mentions({ columnMode, ...props }) {
|
||||
useTitle('Mentions', '/mentions');
|
||||
const { masto, instance } = api();
|
||||
const [searchParams] = columnMode ? [emptySearchParams] : useSearchParams();
|
||||
const [stateType, setStateType] = useState(null);
|
||||
const type = props?.type || searchParams.get('type') || stateType;
|
||||
useTitle(`Mentions${type === 'private' ? ' (Private)' : ''}`, '/mentions');
|
||||
|
||||
const mentionsIterator = useRef();
|
||||
const latestItem = useRef();
|
||||
|
|
|
@ -162,9 +162,10 @@
|
|||
flex-grow: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.notification-content p:first-child {
|
||||
.notification-content > p:first-child {
|
||||
margin-top: 0;
|
||||
margin-bottom: 8px;
|
||||
text-wrap: pretty;
|
||||
}
|
||||
|
||||
.notification-group-statuses {
|
||||
|
@ -407,6 +408,7 @@
|
|||
margin-block: min(0.75em, 12px);
|
||||
white-space: pre-wrap;
|
||||
tab-size: 2;
|
||||
text-wrap: pretty;
|
||||
}
|
||||
.announcements .announcement-reactions:not(:hidden) {
|
||||
display: flex;
|
||||
|
|
|
@ -6,6 +6,7 @@ import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
|
|||
import { InView } from 'react-intersection-observer';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { useSnapshot } from 'valtio';
|
||||
import { subscribeKey } from 'valtio/utils';
|
||||
|
||||
import AccountBlock from '../components/account-block';
|
||||
import FollowRequestButtons from '../components/follow-request-buttons';
|
||||
|
@ -23,6 +24,7 @@ import { getRegistration } from '../utils/push-notifications';
|
|||
import shortenNumber from '../utils/shorten-number';
|
||||
import states, { saveStatus } from '../utils/states';
|
||||
import { getCurrentInstance } from '../utils/store-utils';
|
||||
import usePageVisibility from '../utils/usePageVisibility';
|
||||
import useScroll from '../utils/useScroll';
|
||||
import useTitle from '../utils/useTitle';
|
||||
|
||||
|
@ -132,20 +134,25 @@ function Notifications({ columnMode }) {
|
|||
(async () => {
|
||||
try {
|
||||
const fetchNotificationsPromise = fetchNotifications(firstLoad);
|
||||
const fetchFollowRequestsPromise = fetchFollowRequests();
|
||||
const fetchAnnouncementsPromise = fetchAnnouncements();
|
||||
|
||||
if (firstLoad) {
|
||||
const announcements = await fetchAnnouncementsPromise;
|
||||
announcements.sort((a, b) => {
|
||||
// Sort by updatedAt first, then createdAt
|
||||
const aDate = new Date(a.updatedAt || a.createdAt);
|
||||
const bDate = new Date(b.updatedAt || b.createdAt);
|
||||
return bDate - aDate;
|
||||
});
|
||||
setAnnouncements(announcements);
|
||||
const requests = await fetchFollowRequestsPromise;
|
||||
setFollowRequests(requests);
|
||||
fetchAnnouncements()
|
||||
.then((announcements) => {
|
||||
announcements.sort((a, b) => {
|
||||
// Sort by updatedAt first, then createdAt
|
||||
const aDate = new Date(a.updatedAt || a.createdAt);
|
||||
const bDate = new Date(b.updatedAt || b.createdAt);
|
||||
return bDate - aDate;
|
||||
});
|
||||
setAnnouncements(announcements);
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
fetchFollowRequests()
|
||||
.then((requests) => {
|
||||
setFollowRequests(requests);
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
const { done } = await fetchNotificationsPromise;
|
||||
|
@ -173,30 +180,59 @@ function Notifications({ columnMode }) {
|
|||
// }
|
||||
// }, [nearReachEnd, showMore]);
|
||||
|
||||
const loadUpdates = useCallback(() => {
|
||||
console.log('✨ Load updates', {
|
||||
autoRefresh: snapStates.settings.autoRefresh,
|
||||
scrollTop: scrollableRef.current?.scrollTop === 0,
|
||||
inBackground: inBackground(),
|
||||
notificationsShowNew: snapStates.notificationsShowNew,
|
||||
uiState,
|
||||
});
|
||||
if (
|
||||
snapStates.settings.autoRefresh &&
|
||||
scrollableRef.current?.scrollTop === 0 &&
|
||||
window.__IDLE__ &&
|
||||
!inBackground() &&
|
||||
snapStates.notificationsShowNew &&
|
||||
uiState !== 'loading'
|
||||
) {
|
||||
loadNotifications(true);
|
||||
const [showNew, setShowNew] = useState(false);
|
||||
|
||||
const loadUpdates = useCallback(
|
||||
({ disableIdleCheck = false } = {}) => {
|
||||
if (uiState === 'loading') {
|
||||
return;
|
||||
}
|
||||
console.log('✨ Load updates', {
|
||||
autoRefresh: snapStates.settings.autoRefresh,
|
||||
scrollTop: scrollableRef.current?.scrollTop,
|
||||
inBackground: inBackground(),
|
||||
disableIdleCheck,
|
||||
notificationsShowNew: snapStates.notificationsShowNew,
|
||||
});
|
||||
if (
|
||||
snapStates.settings.autoRefresh &&
|
||||
scrollableRef.current?.scrollTop < 16 &&
|
||||
(disableIdleCheck || window.__IDLE__) &&
|
||||
!inBackground() &&
|
||||
snapStates.notificationsShowNew
|
||||
) {
|
||||
setShowNew(false);
|
||||
loadNotifications(true);
|
||||
} else {
|
||||
setShowNew(snapStates.notificationsShowNew);
|
||||
}
|
||||
},
|
||||
[snapStates.notificationsShowNew, snapStates.settings.autoRefresh, uiState],
|
||||
);
|
||||
// useEffect(loadUpdates, [snapStates.notificationsShowNew]);
|
||||
|
||||
const lastHiddenTime = useRef();
|
||||
usePageVisibility((visible) => {
|
||||
let unsub;
|
||||
if (visible) {
|
||||
const timeDiff = Date.now() - lastHiddenTime.current;
|
||||
if (!lastHiddenTime.current || timeDiff > 1000 * 60) {
|
||||
loadUpdates({
|
||||
disableIdleCheck: true,
|
||||
});
|
||||
} else {
|
||||
lastHiddenTime.current = Date.now();
|
||||
}
|
||||
unsub = subscribeKey(states, 'notificationsShowNew', (v) => {
|
||||
if (v) {
|
||||
loadUpdates();
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [
|
||||
snapStates.notificationsShowNew,
|
||||
snapStates.settings.autoRefresh,
|
||||
uiState,
|
||||
]);
|
||||
useEffect(loadUpdates, [snapStates.notificationsShowNew]);
|
||||
return () => {
|
||||
unsub?.();
|
||||
};
|
||||
});
|
||||
|
||||
const todayDate = new Date();
|
||||
const yesterdayDate = new Date(todayDate - 24 * 60 * 60 * 1000);
|
||||
|
@ -265,7 +301,7 @@ function Notifications({ columnMode }) {
|
|||
{/* <Loader hidden={uiState !== 'loading'} /> */}
|
||||
</div>
|
||||
</div>
|
||||
{snapStates.notificationsShowNew && uiState !== 'loading' && (
|
||||
{showNew && uiState !== 'loading' && (
|
||||
<button
|
||||
class="updates-button shiny-pill"
|
||||
type="button"
|
||||
|
|
|
@ -21,6 +21,7 @@ function Public({ local, columnMode, ...props }) {
|
|||
const { masto, instance } = api({
|
||||
instance: props?.instance || params.instance,
|
||||
});
|
||||
const { masto: currentMasto, instance: currentInstance } = api();
|
||||
const title = `${isLocal ? 'Local' : 'Federated'} timeline (${instance})`;
|
||||
useTitle(title, isLocal ? `/:instance?/p/l` : `/:instance?/p`);
|
||||
// const navigate = useNavigate();
|
||||
|
@ -41,7 +42,7 @@ function Public({ local, columnMode, ...props }) {
|
|||
latestItem.current = value[0].id;
|
||||
}
|
||||
|
||||
value = filteredItems(value, 'public');
|
||||
// value = filteredItems(value, 'public');
|
||||
value.forEach((item) => {
|
||||
saveStatus(item, instance);
|
||||
});
|
||||
|
@ -77,7 +78,7 @@ function Public({ local, columnMode, ...props }) {
|
|||
key={instance + isLocal}
|
||||
title={title}
|
||||
titleComponent={
|
||||
<h1 class="header-account">
|
||||
<h1 class="header-double-lines">
|
||||
<b>{isLocal ? 'Local timeline' : 'Federated timeline'}</b>
|
||||
<div>{instance}</div>
|
||||
</h1>
|
||||
|
@ -91,7 +92,8 @@ function Public({ local, columnMode, ...props }) {
|
|||
useItemID
|
||||
headerStart={<></>}
|
||||
boostsCarousel={snapStates.settings.boostsCarousel}
|
||||
allowFilters
|
||||
// allowFilters
|
||||
filterContext="public"
|
||||
headerEnd={
|
||||
<Menu2
|
||||
portal
|
||||
|
@ -137,6 +139,20 @@ function Public({ local, columnMode, ...props }) {
|
|||
>
|
||||
<Icon icon="bus" /> <span>Go to another instance…</span>
|
||||
</MenuItem>
|
||||
{currentInstance !== instance && (
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
location.hash = isLocal
|
||||
? `/${currentInstance}/p/l`
|
||||
: `/${currentInstance}/p`;
|
||||
}}
|
||||
>
|
||||
<Icon icon="bus" />{' '}
|
||||
<small class="menu-double-lines">
|
||||
Go to my instance (<b>{currentInstance}</b>)
|
||||
</small>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Menu2>
|
||||
}
|
||||
/>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import './search.css';
|
||||
|
||||
import { useAutoAnimate } from '@formkit/auto-animate/preact';
|
||||
import { useEffect, useLayoutEffect, useRef, useState } from 'preact/hooks';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { InView } from 'react-intersection-observer';
|
||||
|
@ -13,6 +14,7 @@ import NavMenu from '../components/nav-menu';
|
|||
import SearchForm from '../components/search-form';
|
||||
import Status from '../components/status';
|
||||
import { api } from '../utils/api';
|
||||
import shortenNumber from '../utils/shorten-number';
|
||||
import useTitle from '../utils/useTitle';
|
||||
|
||||
const SHORT_LIMIT = 5;
|
||||
|
@ -144,6 +146,8 @@ function Search(props) {
|
|||
},
|
||||
);
|
||||
|
||||
const [filterBarParent] = useAutoAnimate();
|
||||
|
||||
return (
|
||||
<div id="search-page" class="deck-container" ref={scrollableRef}>
|
||||
<div class="timeline-deck deck">
|
||||
|
@ -158,7 +162,10 @@ function Search(props) {
|
|||
</header>
|
||||
<main>
|
||||
{!!q && (
|
||||
<div class={`filter-bar ${uiState === 'loading' ? 'loading' : ''}`}>
|
||||
<div
|
||||
ref={filterBarParent}
|
||||
class={`filter-bar ${uiState === 'loading' ? 'loading' : ''}`}
|
||||
>
|
||||
{!!type && (
|
||||
<Link to={`/search${q ? `?q=${encodeURIComponent(q)}` : ''}`}>
|
||||
‹ All
|
||||
|
@ -244,20 +251,32 @@ function Search(props) {
|
|||
{hashtagResults.length > 0 ? (
|
||||
<>
|
||||
<ul class="link-list hashtag-list">
|
||||
{hashtagResults.map((hashtag) => (
|
||||
<li key={hashtag.name}>
|
||||
<Link
|
||||
to={
|
||||
instance
|
||||
? `/${instance}/t/${hashtag.name}`
|
||||
: `/t/${hashtag.name}`
|
||||
}
|
||||
>
|
||||
<Icon icon="hashtag" />
|
||||
<span>{hashtag.name}</span>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
{hashtagResults.map((hashtag) => {
|
||||
const { name, history } = hashtag;
|
||||
const total = history.reduce(
|
||||
(acc, cur) => acc + +cur.uses,
|
||||
0,
|
||||
);
|
||||
return (
|
||||
<li key={hashtag.name}>
|
||||
<Link
|
||||
to={
|
||||
instance
|
||||
? `/${instance}/t/${hashtag.name}`
|
||||
: `/t/${hashtag.name}`
|
||||
}
|
||||
>
|
||||
<Icon icon="hashtag" />
|
||||
<span>{hashtag.name}</span>
|
||||
{!!total && (
|
||||
<span class="count">
|
||||
{shortenNumber(total)}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
{type !== 'hashtags' && (
|
||||
<div class="ui-state">
|
||||
|
|
|
@ -60,6 +60,14 @@ const scrollIntoViewOptions = {
|
|||
behavior: 'smooth',
|
||||
};
|
||||
|
||||
// Select all statuses except those inside collapsed details/summary
|
||||
// Hat-tip to @AmeliaBR@front-end.social
|
||||
// https://front-end.social/@AmeliaBR/109784776146144471
|
||||
const STATUSES_SELECTOR =
|
||||
'.status-link:not(details:not([open]) > summary ~ *, details:not([open]) > summary ~ * *), .status-focus:not(details:not([open]) > summary ~ *, details:not([open]) > summary ~ * *)';
|
||||
|
||||
const STATUS_URL_REGEX = /\/s\//i;
|
||||
|
||||
function StatusPage(params) {
|
||||
const { id } = params;
|
||||
const { masto, instance } = api({ instance: params.instance });
|
||||
|
@ -167,8 +175,13 @@ function StatusPage(params) {
|
|||
function StatusThread({ id, closeLink = '/', instance: propInstance }) {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const mediaParam = searchParams.get('media');
|
||||
const mediaStatusID = searchParams.get('mediaStatusID');
|
||||
const showMedia = parseInt(mediaParam, 10) > 0;
|
||||
const firstLoad = useRef(!states.prevLocation && history.length === 1);
|
||||
const firstLoad = useRef(
|
||||
!states.prevLocation &&
|
||||
(history.length === 1 ||
|
||||
('navigation' in window && navigation?.entries?.()?.length === 1)),
|
||||
);
|
||||
const [viewMode, setViewMode] = useState(
|
||||
searchParams.get('view') || firstLoad.current ? 'full' : null,
|
||||
);
|
||||
|
@ -554,12 +567,7 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
|
|||
);
|
||||
const activeStatusRect = activeStatus?.getBoundingClientRect();
|
||||
const allStatusLinks = Array.from(
|
||||
// Select all statuses except those inside collapsed details/summary
|
||||
// Hat-tip to @AmeliaBR@front-end.social
|
||||
// https://front-end.social/@AmeliaBR/109784776146144471
|
||||
scrollableRef.current.querySelectorAll(
|
||||
'.status-link:not(details:not([open]) > summary ~ *, details:not([open]) > summary ~ * *), .status-focus:not(details:not([open]) > summary ~ *, details:not([open]) > summary ~ * *)',
|
||||
),
|
||||
scrollableRef.current.querySelectorAll(STATUSES_SELECTOR),
|
||||
);
|
||||
console.log({ allStatusLinks });
|
||||
if (
|
||||
|
@ -592,9 +600,7 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
|
|||
);
|
||||
const activeStatusRect = activeStatus?.getBoundingClientRect();
|
||||
const allStatusLinks = Array.from(
|
||||
scrollableRef.current.querySelectorAll(
|
||||
'.status-link:not(details:not([open]) > summary ~ *, details:not([open]) > summary ~ * *), .status-focus:not(details:not([open]) > summary ~ *, details:not([open]) > summary ~ * *)',
|
||||
),
|
||||
scrollableRef.current.querySelectorAll(STATUSES_SELECTOR),
|
||||
);
|
||||
if (
|
||||
activeStatus &&
|
||||
|
@ -657,139 +663,159 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
|
|||
resetScrollPosition(status.id);
|
||||
}, []);
|
||||
|
||||
const renderStatus = (status) => {
|
||||
const {
|
||||
id: statusID,
|
||||
ancestor,
|
||||
isThread,
|
||||
descendant,
|
||||
thread,
|
||||
replies,
|
||||
repliesCount,
|
||||
weight,
|
||||
} = status;
|
||||
const isHero = statusID === id;
|
||||
// const StatusParent = useCallback(
|
||||
// (props) =>
|
||||
// isThread || thread || ancestor ? (
|
||||
// <Link
|
||||
// class="status-link"
|
||||
// to={
|
||||
// instance ? `/${instance}/s/${statusID}` : `/s/${statusID}`
|
||||
// }
|
||||
// onClick={() => {
|
||||
// resetScrollPosition(statusID);
|
||||
// }}
|
||||
// {...props}
|
||||
// />
|
||||
// ) : (
|
||||
// <div class="status-focus" tabIndex={0} {...props} />
|
||||
// ),
|
||||
// [isThread, thread],
|
||||
// );
|
||||
return (
|
||||
<li
|
||||
key={statusID}
|
||||
ref={isHero ? heroStatusRef : null}
|
||||
class={`${ancestor ? 'ancestor' : ''} ${
|
||||
descendant ? 'descendant' : ''
|
||||
} ${thread ? 'thread' : ''} ${isHero ? 'hero' : ''}`}
|
||||
>
|
||||
{isHero ? (
|
||||
<>
|
||||
<InView
|
||||
threshold={0.1}
|
||||
onChange={onView}
|
||||
class="status-focus"
|
||||
tabIndex={0}
|
||||
useEffect(() => {
|
||||
let timer;
|
||||
if (mediaStatusID && showMedia) {
|
||||
timer = setTimeout(() => {
|
||||
const status = scrollableRef.current?.querySelector(
|
||||
`.status-link[href*="/${mediaStatusID}"]`,
|
||||
);
|
||||
if (status) {
|
||||
status.scrollIntoView(scrollIntoViewOptions);
|
||||
}
|
||||
}, 400); // After CSS transition
|
||||
}
|
||||
return () => {
|
||||
clearTimeout(timer);
|
||||
};
|
||||
}, [mediaStatusID, showMedia]);
|
||||
|
||||
const renderStatus = useCallback(
|
||||
(status) => {
|
||||
const {
|
||||
id: statusID,
|
||||
ancestor,
|
||||
isThread,
|
||||
descendant,
|
||||
thread,
|
||||
replies,
|
||||
repliesCount,
|
||||
weight,
|
||||
} = status;
|
||||
const isHero = statusID === id;
|
||||
// const StatusParent = useCallback(
|
||||
// (props) =>
|
||||
// isThread || thread || ancestor ? (
|
||||
// <Link
|
||||
// class="status-link"
|
||||
// to={
|
||||
// instance ? `/${instance}/s/${statusID}` : `/s/${statusID}`
|
||||
// }
|
||||
// onClick={() => {
|
||||
// resetScrollPosition(statusID);
|
||||
// }}
|
||||
// {...props}
|
||||
// />
|
||||
// ) : (
|
||||
// <div class="status-focus" tabIndex={0} {...props} />
|
||||
// ),
|
||||
// [isThread, thread],
|
||||
// );
|
||||
return (
|
||||
<li
|
||||
key={statusID}
|
||||
ref={isHero ? heroStatusRef : null}
|
||||
class={`${ancestor ? 'ancestor' : ''} ${
|
||||
descendant ? 'descendant' : ''
|
||||
} ${thread ? 'thread' : ''} ${isHero ? 'hero' : ''}`}
|
||||
>
|
||||
{isHero ? (
|
||||
<>
|
||||
<InView
|
||||
threshold={0.1}
|
||||
onChange={onView}
|
||||
class="status-focus"
|
||||
tabIndex={0}
|
||||
>
|
||||
<Status
|
||||
statusID={statusID}
|
||||
instance={instance}
|
||||
withinContext
|
||||
size="l"
|
||||
enableTranslate
|
||||
forceTranslate={translate}
|
||||
/>
|
||||
</InView>
|
||||
{uiState !== 'loading' && !authenticated ? (
|
||||
<div class="post-status-banner">
|
||||
<p>
|
||||
You're not logged in. Interactions (reply, boost, etc) are
|
||||
not possible.
|
||||
</p>
|
||||
<Link to="/login" class="button">
|
||||
Log in
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
!sameInstance && (
|
||||
<div class="post-status-banner">
|
||||
<p>
|
||||
This post is from another instance (<b>{instance}</b>).
|
||||
Interactions (reply, boost, etc) are not possible.
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
disabled={uiState === 'loading'}
|
||||
onClick={() => {
|
||||
setUIState('loading');
|
||||
(async () => {
|
||||
try {
|
||||
const results = await currentMasto.v2.search.fetch({
|
||||
q: heroStatus.url,
|
||||
type: 'statuses',
|
||||
resolve: true,
|
||||
limit: 1,
|
||||
});
|
||||
if (results.statuses.length) {
|
||||
const status = results.statuses[0];
|
||||
location.hash = currentInstance
|
||||
? `/${currentInstance}/s/${status.id}`
|
||||
: `/s/${status.id}`;
|
||||
} else {
|
||||
throw new Error('No results');
|
||||
}
|
||||
} catch (e) {
|
||||
setUIState('default');
|
||||
alert('Error: ' + e);
|
||||
console.error(e);
|
||||
}
|
||||
})();
|
||||
}}
|
||||
>
|
||||
<Icon icon="transfer" /> Switch to my instance to enable
|
||||
interactions
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
// <StatusParent>
|
||||
<Link
|
||||
class="status-link"
|
||||
to={instance ? `/${instance}/s/${statusID}` : `/s/${statusID}`}
|
||||
onClick={() => {
|
||||
resetScrollPosition(statusID);
|
||||
}}
|
||||
>
|
||||
<Status
|
||||
statusID={statusID}
|
||||
instance={instance}
|
||||
withinContext
|
||||
size="l"
|
||||
size={thread || ancestor ? 'm' : 's'}
|
||||
enableTranslate
|
||||
forceTranslate={translate}
|
||||
onMediaClick={handleMediaClick}
|
||||
onStatusLinkClick={handleStatusLinkClick}
|
||||
/>
|
||||
</InView>
|
||||
{uiState !== 'loading' && !authenticated ? (
|
||||
<div class="post-status-banner">
|
||||
<p>
|
||||
You're not logged in. Interactions (reply, boost, etc) are not
|
||||
possible.
|
||||
</p>
|
||||
<Link to="/login" class="button">
|
||||
Log in
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
!sameInstance && (
|
||||
<div class="post-status-banner">
|
||||
<p>
|
||||
This post is from another instance (<b>{instance}</b>).
|
||||
Interactions (reply, boost, etc) are not possible.
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
disabled={uiState === 'loading'}
|
||||
onClick={() => {
|
||||
setUIState('loading');
|
||||
(async () => {
|
||||
try {
|
||||
const results = await currentMasto.v2.search.fetch({
|
||||
q: heroStatus.url,
|
||||
type: 'statuses',
|
||||
resolve: true,
|
||||
limit: 1,
|
||||
});
|
||||
if (results.statuses.length) {
|
||||
const status = results.statuses[0];
|
||||
location.hash = currentInstance
|
||||
? `/${currentInstance}/s/${status.id}`
|
||||
: `/s/${status.id}`;
|
||||
} else {
|
||||
throw new Error('No results');
|
||||
}
|
||||
} catch (e) {
|
||||
setUIState('default');
|
||||
alert('Error: ' + e);
|
||||
console.error(e);
|
||||
}
|
||||
})();
|
||||
}}
|
||||
>
|
||||
<Icon icon="transfer" /> Switch to my instance to enable
|
||||
interactions
|
||||
</button>
|
||||
{ancestor && isThread && repliesCount > 1 && (
|
||||
<div class="replies-link">
|
||||
<Icon icon="comment" />{' '}
|
||||
<span title={repliesCount}>
|
||||
{shortenNumber(repliesCount)}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
// <StatusParent>
|
||||
<Link
|
||||
class="status-link"
|
||||
to={instance ? `/${instance}/s/${statusID}` : `/s/${statusID}`}
|
||||
onClick={() => {
|
||||
resetScrollPosition(statusID);
|
||||
}}
|
||||
>
|
||||
<Status
|
||||
statusID={statusID}
|
||||
instance={instance}
|
||||
withinContext
|
||||
size={thread || ancestor ? 'm' : 's'}
|
||||
enableTranslate
|
||||
onMediaClick={handleMediaClick}
|
||||
onStatusLinkClick={handleStatusLinkClick}
|
||||
/>
|
||||
{ancestor && isThread && repliesCount > 1 && (
|
||||
<div class="replies-link">
|
||||
<Icon icon="comment" />{' '}
|
||||
<span title={repliesCount}>{shortenNumber(repliesCount)}</span>
|
||||
</div>
|
||||
)}{' '}
|
||||
{/* {replies?.length > LIMIT && (
|
||||
)}{' '}
|
||||
{/* {replies?.length > LIMIT && (
|
||||
<div class="replies-link">
|
||||
<Icon icon="comment" />{' '}
|
||||
<span title={replies.length}>
|
||||
|
@ -797,48 +823,71 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
|
|||
</span>
|
||||
</div>
|
||||
)} */}
|
||||
{/* </StatusParent> */}
|
||||
</Link>
|
||||
)}
|
||||
{descendant && replies?.length > 0 && (
|
||||
<SubComments
|
||||
instance={instance}
|
||||
replies={replies}
|
||||
hasParentThread={thread}
|
||||
level={1}
|
||||
accWeight={weight}
|
||||
openAll={totalDescendants.current < SUBCOMMENTS_OPEN_ALL_LIMIT}
|
||||
/>
|
||||
)}
|
||||
{uiState === 'loading' &&
|
||||
isHero &&
|
||||
!!heroStatus?.repliesCount &&
|
||||
!hasDescendants && (
|
||||
<div class="status-loading">
|
||||
<Loader />
|
||||
</div>
|
||||
{/* </StatusParent> */}
|
||||
</Link>
|
||||
)}
|
||||
{uiState === 'error' &&
|
||||
isHero &&
|
||||
!!heroStatus?.repliesCount &&
|
||||
!hasDescendants && (
|
||||
<div class="status-error">
|
||||
Unable to load replies.
|
||||
<br />
|
||||
<button
|
||||
type="button"
|
||||
class="plain"
|
||||
onClick={() => {
|
||||
states.reloadStatusPage++;
|
||||
}}
|
||||
>
|
||||
Try again
|
||||
</button>
|
||||
</div>
|
||||
{descendant && replies?.length > 0 && (
|
||||
<SubComments
|
||||
instance={instance}
|
||||
replies={replies}
|
||||
hasParentThread={thread}
|
||||
level={1}
|
||||
accWeight={weight}
|
||||
openAll={totalDescendants.current < SUBCOMMENTS_OPEN_ALL_LIMIT}
|
||||
/>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
};
|
||||
{uiState === 'loading' &&
|
||||
isHero &&
|
||||
!!heroStatus?.repliesCount &&
|
||||
!hasDescendants && (
|
||||
<div class="status-loading">
|
||||
<Loader />
|
||||
</div>
|
||||
)}
|
||||
{uiState === 'error' &&
|
||||
isHero &&
|
||||
!!heroStatus?.repliesCount &&
|
||||
!hasDescendants && (
|
||||
<div class="status-error">
|
||||
Unable to load replies.
|
||||
<br />
|
||||
<button
|
||||
type="button"
|
||||
class="plain"
|
||||
onClick={() => {
|
||||
states.reloadStatusPage++;
|
||||
}}
|
||||
>
|
||||
Try again
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
},
|
||||
[
|
||||
id,
|
||||
instance,
|
||||
uiState,
|
||||
authenticated,
|
||||
sameInstance,
|
||||
translate,
|
||||
handleMediaClick,
|
||||
handleStatusLinkClick,
|
||||
hasDescendants,
|
||||
],
|
||||
);
|
||||
|
||||
const prevLocationIsStatusPage = useMemo(() => {
|
||||
// Navigation API
|
||||
if ('navigation' in window && navigation?.entries) {
|
||||
const prevEntry = navigation.entries()[navigation.currentEntry.index - 1];
|
||||
if (prevEntry?.url) {
|
||||
return STATUS_URL_REGEX.test(prevEntry.url);
|
||||
}
|
||||
}
|
||||
return STATUS_URL_REGEX.test(states.prevLocation?.pathname);
|
||||
}, [sKey]);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
@ -876,7 +925,7 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
|
|||
</div> */}
|
||||
<div class="header-grid header-grid-2">
|
||||
<h1>
|
||||
{!!/\/s\//i.test(snapStates.prevLocation?.pathname) && (
|
||||
{prevLocationIsStatusPage && (
|
||||
<button
|
||||
type="button"
|
||||
class="plain deck-back"
|
||||
|
@ -970,7 +1019,7 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
|
|||
<div class="header-side">
|
||||
<button
|
||||
type="button"
|
||||
class="plain4"
|
||||
class="plain4 button-switch-view"
|
||||
style={{
|
||||
display: viewMode === 'full' ? '' : 'none',
|
||||
}}
|
||||
|
@ -1231,11 +1280,11 @@ function SubComments({
|
|||
/>
|
||||
))}
|
||||
</span>
|
||||
<span>
|
||||
<b>
|
||||
<span title={replies.length}>{shortenNumber(replies.length)}</span>{' '}
|
||||
repl
|
||||
{replies.length === 1 ? 'y' : 'ies'}
|
||||
</span>
|
||||
</b>
|
||||
{!sameCount && totalComments > 1 && (
|
||||
<>
|
||||
{' '}
|
||||
|
|
|
@ -15,6 +15,7 @@ import { api } from '../utils/api';
|
|||
import { oklab2rgb, rgb2oklab } from '../utils/color-utils';
|
||||
import { filteredItems } from '../utils/filters';
|
||||
import pmem from '../utils/pmem';
|
||||
import shortenNumber from '../utils/shorten-number';
|
||||
import states from '../utils/states';
|
||||
import { saveStatus } from '../utils/states';
|
||||
import useTitle from '../utils/useTitle';
|
||||
|
@ -37,6 +38,7 @@ function Trending({ columnMode, ...props }) {
|
|||
const { masto, instance } = api({
|
||||
instance: props?.instance || params.instance,
|
||||
});
|
||||
const { masto: currentMasto, instance: currentInstance } = api();
|
||||
const title = `Trending (${instance})`;
|
||||
useTitle(title, `/:instance?/trending`);
|
||||
// const navigate = useNavigate();
|
||||
|
@ -84,7 +86,7 @@ function Trending({ columnMode, ...props }) {
|
|||
latestItem.current = value[0].id;
|
||||
}
|
||||
|
||||
value = filteredItems(value, 'public'); // Might not work here
|
||||
// value = filteredItems(value, 'public'); // Might not work here
|
||||
value.forEach((item) => {
|
||||
saveStatus(item, instance);
|
||||
});
|
||||
|
@ -131,7 +133,7 @@ function Trending({ columnMode, ...props }) {
|
|||
<span class="more-insignificant">#</span>
|
||||
{name}
|
||||
</span>
|
||||
<span class="filter-count">{total.toLocaleString()}</span>
|
||||
<span class="filter-count">{shortenNumber(total)}</span>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
|
@ -241,7 +243,7 @@ function Trending({ columnMode, ...props }) {
|
|||
key={instance}
|
||||
title={title}
|
||||
titleComponent={
|
||||
<h1 class="header-account">
|
||||
<h1 class="header-double-lines">
|
||||
<b>Trending</b>
|
||||
<div>{instance}</div>
|
||||
</h1>
|
||||
|
@ -256,7 +258,8 @@ function Trending({ columnMode, ...props }) {
|
|||
useItemID
|
||||
headerStart={<></>}
|
||||
boostsCarousel={snapStates.settings.boostsCarousel}
|
||||
allowFilters
|
||||
// allowFilters
|
||||
filterContext="public"
|
||||
timelineStart={TimelineStart}
|
||||
headerEnd={
|
||||
<Menu2
|
||||
|
@ -289,6 +292,18 @@ function Trending({ columnMode, ...props }) {
|
|||
>
|
||||
<Icon icon="bus" /> <span>Go to another instance…</span>
|
||||
</MenuItem>
|
||||
{currentInstance !== instance && (
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
location.hash = `/${currentInstance}/trending`;
|
||||
}}
|
||||
>
|
||||
<Icon icon="bus" />{' '}
|
||||
<small class="menu-double-lines">
|
||||
Go to my instance (<b>{currentInstance}</b>)
|
||||
</small>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Menu2>
|
||||
}
|
||||
/>
|
||||
|
|
|
@ -1,40 +1,3 @@
|
|||
#welcome {
|
||||
text-align: center;
|
||||
background-image: radial-gradient(
|
||||
circle at center,
|
||||
var(--bg-color),
|
||||
transparent 16em
|
||||
),
|
||||
radial-gradient(circle at center, var(--bg-color), transparent 8em);
|
||||
background-repeat: no-repeat;
|
||||
background-attachment: fixed;
|
||||
padding: 16px;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
#welcome .hero-container {
|
||||
padding-block: 60px;
|
||||
height: 100vh;
|
||||
height: 100svh;
|
||||
max-height: 1024px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#welcome h1 {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: 5em;
|
||||
line-height: 1;
|
||||
letter-spacing: -1px;
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
mix-blend-mode: multiply;
|
||||
}
|
||||
@keyframes shine2 {
|
||||
0% {
|
||||
left: -100%;
|
||||
|
@ -46,92 +9,176 @@
|
|||
left: 100%;
|
||||
}
|
||||
}
|
||||
#welcome h1:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image: linear-gradient(
|
||||
100deg,
|
||||
rgba(255, 255, 255, 0) 30%,
|
||||
rgba(255, 255, 255, 0.4),
|
||||
rgba(255, 255, 255, 0) 70%
|
||||
);
|
||||
top: 0;
|
||||
left: -100%;
|
||||
pointer-events: none;
|
||||
animation: shine2 5s ease-in-out 1s infinite;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
#welcome {
|
||||
|
||||
#welcome {
|
||||
text-align: center;
|
||||
background-image: radial-gradient(
|
||||
circle at center,
|
||||
var(--bg-color),
|
||||
transparent 16em
|
||||
),
|
||||
radial-gradient(circle at center, var(--bg-color), transparent 8em);
|
||||
background-repeat: no-repeat;
|
||||
background-attachment: fixed;
|
||||
cursor: default;
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
background-image: none;
|
||||
}
|
||||
#welcome h1 {
|
||||
mix-blend-mode: normal;
|
||||
}
|
||||
#welcome h1:before {
|
||||
content: none;
|
||||
}
|
||||
}
|
||||
#welcome img {
|
||||
vertical-align: top;
|
||||
transition: transform 0.3s ease-out;
|
||||
}
|
||||
#welcome h1 img {
|
||||
filter: drop-shadow(-1px -1px var(--bg-blur-color))
|
||||
drop-shadow(0 -1px 1px #fff)
|
||||
drop-shadow(0 16px 32px var(--drop-shadow-color));
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
#welcome h1 img {
|
||||
filter: none;
|
||||
}
|
||||
}
|
||||
|
||||
#welcome h1:hover img {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
#welcome .desc {
|
||||
font-size: 1.4em;
|
||||
text-wrap: balance;
|
||||
opacity: 0.7;
|
||||
}
|
||||
#welcome .hero-container > p {
|
||||
margin-top: 0;
|
||||
}
|
||||
.hero-container {
|
||||
padding: 16px;
|
||||
height: 100vh;
|
||||
height: 100svh;
|
||||
max-height: 800px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
#why-container .sections {
|
||||
padding-inline: 16px;
|
||||
}
|
||||
#why-container .sections section {
|
||||
text-align: start;
|
||||
max-width: 480px;
|
||||
background-color: var(--bg-color);
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
box-shadow: 17px 20px 40px var(--drop-shadow-color);
|
||||
margin-bottom: 48px;
|
||||
}
|
||||
#why-container .sections section h4 {
|
||||
margin: 0;
|
||||
padding: 30px 30px 0;
|
||||
font-size: 1.4em;
|
||||
font-weight: 600;
|
||||
}
|
||||
#why-container .sections section p {
|
||||
margin-inline: 30px;
|
||||
margin-bottom: 30px;
|
||||
opacity: 0.7;
|
||||
text-wrap: balance;
|
||||
}
|
||||
#why-container .sections section img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border-bottom: 1px solid var(--outline-color);
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
#why-container .sections section img {
|
||||
filter: invert(0.85) hue-rotate(180deg);
|
||||
a {
|
||||
color: inherit;
|
||||
|
||||
&:hover {
|
||||
color: var(--link-text-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: 5em;
|
||||
line-height: 1;
|
||||
letter-spacing: -1px;
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
mix-blend-mode: multiply;
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
mix-blend-mode: normal;
|
||||
}
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image: linear-gradient(
|
||||
100deg,
|
||||
rgba(255, 255, 255, 0) 30%,
|
||||
rgba(255, 255, 255, 0.4),
|
||||
rgba(255, 255, 255, 0) 70%
|
||||
);
|
||||
top: 0;
|
||||
left: -100%;
|
||||
pointer-events: none;
|
||||
animation: shine2 5s ease-in-out 1s infinite;
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
content: none;
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
filter: drop-shadow(-1px -1px var(--bg-blur-color))
|
||||
drop-shadow(0 -1px 1px #fff)
|
||||
drop-shadow(0 16px 32px var(--drop-shadow-color));
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
filter: none;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover img {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
vertical-align: top;
|
||||
transition: transform 0.3s ease-out;
|
||||
}
|
||||
|
||||
.desc {
|
||||
font-size: 1.4em;
|
||||
text-wrap: balance;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.hero-container > p {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
#why-container {
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.sections {
|
||||
padding-inline: 16px;
|
||||
|
||||
section {
|
||||
text-align: start;
|
||||
max-width: 480px;
|
||||
background-color: var(--bg-color);
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
box-shadow: 17px 20px 40px var(--drop-shadow-color);
|
||||
margin-bottom: 48px;
|
||||
|
||||
h4 {
|
||||
margin: 0;
|
||||
padding: 30px 30px 0;
|
||||
font-size: 1.4em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-inline: 30px;
|
||||
margin-bottom: 30px;
|
||||
opacity: 0.7;
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border-bottom: 1px solid var(--outline-color);
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
filter: invert(0.85) hue-rotate(180deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (width > 40em) {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-rows: 1fr auto;
|
||||
height: 100vh;
|
||||
height: 100svh;
|
||||
|
||||
.hero-container {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
#why-container {
|
||||
padding: 32px;
|
||||
overflow: auto;
|
||||
mask-image: linear-gradient(to top, transparent 16px, black 64px);
|
||||
}
|
||||
|
||||
footer {
|
||||
grid-row: 2;
|
||||
grid-column: 1 / span 2;
|
||||
}
|
||||
}
|
||||
|
||||
& ~ :is(#compose-button, #shortcuts) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -98,31 +98,33 @@ function Welcome() {
|
|||
</section>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<p>
|
||||
<a href="https://github.com/cheeaun/phanpy" target="_blank">
|
||||
Built
|
||||
</a>{' '}
|
||||
by{' '}
|
||||
<a
|
||||
href="https://mastodon.social/@cheeaun"
|
||||
target="_blank"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
states.showAccount = 'cheeaun@mastodon.social';
|
||||
}}
|
||||
>
|
||||
@cheeaun
|
||||
</a>
|
||||
.{' '}
|
||||
<a
|
||||
href="https://github.com/cheeaun/phanpy/blob/main/PRIVACY.MD"
|
||||
target="_blank"
|
||||
>
|
||||
Privacy Policy
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
<footer>
|
||||
<hr />
|
||||
<p>
|
||||
<a href="https://github.com/cheeaun/phanpy" target="_blank">
|
||||
Built
|
||||
</a>{' '}
|
||||
by{' '}
|
||||
<a
|
||||
href="https://mastodon.social/@cheeaun"
|
||||
target="_blank"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
states.showAccount = 'cheeaun@mastodon.social';
|
||||
}}
|
||||
>
|
||||
@cheeaun
|
||||
</a>
|
||||
.{' '}
|
||||
<a
|
||||
href="https://github.com/cheeaun/phanpy/blob/main/PRIVACY.MD"
|
||||
target="_blank"
|
||||
>
|
||||
Privacy Policy
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</footer>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -221,8 +221,34 @@ export function api({ instance, accessToken, accountID, account } = {}) {
|
|||
}
|
||||
}
|
||||
|
||||
const currentAccount = getCurrentAccount();
|
||||
|
||||
// If only instance is provided, get the masto instance for that instance
|
||||
if (instance) {
|
||||
if (currentAccountApi?.instance === instance) {
|
||||
return {
|
||||
masto: currentAccountApi.masto,
|
||||
streaming: currentAccountApi.streaming,
|
||||
client: currentAccountApi,
|
||||
authenticated: true,
|
||||
instance,
|
||||
};
|
||||
}
|
||||
|
||||
if (currentAccount?.instanceURL === instance) {
|
||||
const { accessToken } = currentAccount;
|
||||
currentAccountApi =
|
||||
accountApis[instance]?.[accessToken] ||
|
||||
initClient({ instance, accessToken });
|
||||
return {
|
||||
masto: currentAccountApi.masto,
|
||||
streaming: currentAccountApi.streaming,
|
||||
client: currentAccountApi,
|
||||
authenticated: true,
|
||||
instance,
|
||||
};
|
||||
}
|
||||
|
||||
const client = apis[instance] || initClient({ instance });
|
||||
const { masto, streaming, accessToken } = client;
|
||||
return {
|
||||
|
@ -244,7 +270,6 @@ export function api({ instance, accessToken, accountID, account } = {}) {
|
|||
instance: currentAccountApi.instance,
|
||||
};
|
||||
}
|
||||
const currentAccount = getCurrentAccount();
|
||||
if (currentAccount) {
|
||||
const { accessToken, instanceURL: instance } = currentAccount;
|
||||
currentAccountApi =
|
||||
|
|
4
src/utils/filter-context.js
Normal file
4
src/utils/filter-context.js
Normal file
|
@ -0,0 +1,4 @@
|
|||
import { createContext } from 'preact';
|
||||
|
||||
const FilterContext = createContext();
|
||||
export default FilterContext;
|
|
@ -1,10 +1,8 @@
|
|||
import mem from './mem';
|
||||
import store from './store';
|
||||
|
||||
export function filteredItem(item, filterContext, currentAccountID) {
|
||||
const { filtered } = item;
|
||||
if (!filtered?.length) return true;
|
||||
const isSelf = currentAccountID && item.account?.id === currentAccountID;
|
||||
if (isSelf) return true;
|
||||
function _isFiltered(filtered, filterContext) {
|
||||
if (!filtered?.length) return false;
|
||||
const appliedFilters = filtered.filter((f) => {
|
||||
const { filter } = f;
|
||||
const hasContext = filter.context.includes(filterContext);
|
||||
|
@ -12,19 +10,35 @@ export function filteredItem(item, filterContext, currentAccountID) {
|
|||
if (!filter.expiresAt) return hasContext;
|
||||
return new Date(filter.expiresAt) > new Date();
|
||||
});
|
||||
if (!appliedFilters.length) return true;
|
||||
if (!appliedFilters.length) return false;
|
||||
const isHidden = appliedFilters.some((f) => f.filter.filterAction === 'hide');
|
||||
console.log({ isHidden, filtered, appliedFilters, item });
|
||||
if (isHidden) return false;
|
||||
if (isHidden)
|
||||
return {
|
||||
action: 'hide',
|
||||
};
|
||||
const isWarn = appliedFilters.some((f) => f.filter.filterAction === 'warn');
|
||||
if (isWarn) {
|
||||
const filterTitles = appliedFilters.map((f) => f.filter.title);
|
||||
item._filtered = {
|
||||
return {
|
||||
action: 'warn',
|
||||
titles: filterTitles,
|
||||
titlesStr: filterTitles.join(' • '),
|
||||
};
|
||||
}
|
||||
return isWarn;
|
||||
return false;
|
||||
}
|
||||
export const isFiltered = mem(_isFiltered);
|
||||
|
||||
export function filteredItem(item, filterContext, currentAccountID) {
|
||||
const { filtered } = item;
|
||||
if (!filtered?.length) return true;
|
||||
const isSelf = currentAccountID && item.account?.id === currentAccountID;
|
||||
if (isSelf) return true;
|
||||
const filterState = isFiltered(filtered, filterContext);
|
||||
if (!filterState) return true;
|
||||
if (filterState.action === 'hide') return false;
|
||||
// item._filtered = filterState;
|
||||
return true;
|
||||
}
|
||||
export function filteredItems(items, filterContext) {
|
||||
if (!items?.length) return [];
|
||||
|
|
|
@ -1,4 +1,37 @@
|
|||
// This is like very lame "type-checking" lol
|
||||
const notificationTypeKeys = {
|
||||
mention: ['account', 'status'],
|
||||
status: ['account', 'status'],
|
||||
reblog: ['account', 'status'],
|
||||
follow: ['account'],
|
||||
follow_request: ['account'],
|
||||
favourite: ['account', 'status'],
|
||||
poll: ['status'],
|
||||
update: ['status'],
|
||||
};
|
||||
function fixNotifications(notifications) {
|
||||
return notifications.filter((notification) => {
|
||||
const { type, id, createdAt } = notification;
|
||||
if (!type) {
|
||||
console.warn('Notification missing type', notification);
|
||||
return false;
|
||||
}
|
||||
if (!id || !createdAt) {
|
||||
console.warn('Notification missing id or createdAt', notification);
|
||||
// Continue processing this despite missing id or createdAt
|
||||
}
|
||||
const keys = notificationTypeKeys[type];
|
||||
if (keys?.length) {
|
||||
return keys.every((key) => !!notification[key]);
|
||||
}
|
||||
return true; // skip other types
|
||||
});
|
||||
}
|
||||
|
||||
function groupNotifications(notifications) {
|
||||
// Filter out invalid notifications
|
||||
notifications = fixNotifications(notifications);
|
||||
|
||||
// Create new flat list of notifications
|
||||
// Combine sibling notifications based on type and status id
|
||||
// Concat all notification.account into an array of _accounts
|
||||
|
@ -7,7 +40,7 @@ function groupNotifications(notifications) {
|
|||
for (let i = 0, j = 0; i < notifications.length; i++) {
|
||||
const notification = notifications[i];
|
||||
const { id, status, account, type, createdAt } = notification;
|
||||
const date = new Date(createdAt).toLocaleDateString();
|
||||
const date = createdAt ? new Date(createdAt).toLocaleDateString() : '';
|
||||
let virtualType = type;
|
||||
if (type === 'favourite' || type === 'reblog') {
|
||||
virtualType = 'favourite+reblog';
|
||||
|
@ -50,7 +83,7 @@ function groupNotifications(notifications) {
|
|||
for (let i = 0, j = 0; i < cleanNotifications.length; i++) {
|
||||
const notification = cleanNotifications[i];
|
||||
const { id, account, _accounts, type, createdAt } = notification;
|
||||
const date = new Date(createdAt).toLocaleDateString();
|
||||
const date = createdAt ? new Date(createdAt).toLocaleDateString() : '';
|
||||
if (type === 'favourite+reblog' && account && _accounts.length === 1) {
|
||||
const key = `${account?.id}-${type}-${date}`;
|
||||
const mappedNotification = notificationsMap2[key];
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
export default function isMastodonLinkMaybe(url) {
|
||||
const { pathname } = new URL(url);
|
||||
const { pathname, hash } = new URL(url);
|
||||
return (
|
||||
/^\/.*\/\d+$/i.test(pathname) ||
|
||||
/^\/@[^/]+\/(statuses|posts)\/\w+\/?$/i.test(pathname) || // GoToSocial, Takahe
|
||||
/^\/notes\/[a-z0-9]+$/i.test(pathname) || // Misskey, Calckey
|
||||
/^\/(notice|objects)\/[a-z0-9-]+$/i.test(pathname) // Pleroma
|
||||
/^\/notes\/[a-z0-9]+$/i.test(pathname) || // Misskey, Calckey
|
||||
/^\/(notice|objects)\/[a-z0-9-]+$/i.test(pathname) || // Pleroma
|
||||
/#\/[^\/]+\.[^\/]+\/s\/.+/i.test(hash) // Phanpy 🫣
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import moize from 'moize';
|
||||
|
||||
window._moize = moize;
|
||||
|
||||
export default function mem(fn, opts = {}) {
|
||||
return moize(fn, { ...opts, maxSize: 100 });
|
||||
return moize(fn, { ...opts, maxSize: 50, isDeepEqual: true });
|
||||
}
|
||||
|
|
|
@ -18,6 +18,8 @@ export default function openCompose(opts) {
|
|||
// }
|
||||
|
||||
newWin.__COMPOSE__ = opts;
|
||||
} else {
|
||||
alert('Looks like your browser is blocking popups.');
|
||||
}
|
||||
|
||||
return newWin;
|
||||
|
|
|
@ -22,7 +22,6 @@ const states = proxy({
|
|||
notificationsNew: [],
|
||||
notificationsShowNew: false,
|
||||
notificationsLastFetchTime: null,
|
||||
accounts: {},
|
||||
reloadStatusPage: 0,
|
||||
reloadGenericAccounts: {
|
||||
id: null,
|
||||
|
@ -72,8 +71,9 @@ export function initStates() {
|
|||
store.account.get('settings-autoRefresh') ?? false;
|
||||
states.settings.shortcutsViewMode =
|
||||
store.account.get('settings-shortcutsViewMode') ?? null;
|
||||
states.settings.shortcutsColumnsMode =
|
||||
store.account.get('settings-shortcutsColumnsMode') ?? false;
|
||||
if (store.account.get('settings-shortcutsColumnsMode')) {
|
||||
states.settings.shortcutsColumnsMode = true;
|
||||
}
|
||||
states.settings.boostsCarousel =
|
||||
store.account.get('settings-boostsCarousel') ?? true;
|
||||
states.settings.contentTranslation =
|
||||
|
@ -100,9 +100,6 @@ subscribe(states, (changes) => {
|
|||
if (path.join('.') === 'settings.boostsCarousel') {
|
||||
store.account.set('settings-boostsCarousel', !!value);
|
||||
}
|
||||
if (path.join('.') === 'settings.shortcutsColumnsMode') {
|
||||
store.account.set('settings-shortcutsColumnsMode', !!value);
|
||||
}
|
||||
if (path.join('.') === 'settings.shortcutsViewMode') {
|
||||
store.account.set('settings-shortcutsViewMode', value);
|
||||
}
|
||||
|
@ -171,7 +168,7 @@ export function saveStatus(status, instance, opts) {
|
|||
if (!override && oldStatus) return;
|
||||
const key = statusKey(status.id, instance);
|
||||
if (oldStatus?._pinned) status._pinned = oldStatus._pinned;
|
||||
if (oldStatus?._filtered) status._filtered = oldStatus._filtered;
|
||||
// if (oldStatus?._filtered) status._filtered = oldStatus._filtered;
|
||||
states.statuses[key] = status;
|
||||
if (status.reblog) {
|
||||
const key = statusKey(status.reblog.id, instance);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { satisfies } from 'semver';
|
||||
import { satisfies } from 'compare-versions';
|
||||
|
||||
import features from '../data/features.json';
|
||||
|
||||
|
|
Loading…
Reference in a new issue