kopia lustrzana https://github.com/cheeaun/phanpy
Porównaj commity
46 Commity
65d51b077f
...
b6cf53c221
Autor | SHA1 | Data |
---|---|---|
Chee Aun | b6cf53c221 | |
Lim Chee Aun | b9058c6e3d | |
Lim Chee Aun | 55ad6500bc | |
Lim Chee Aun | f4b95d254c | |
Lim Chee Aun | effbe189e1 | |
Lim Chee Aun | 44e910b8c9 | |
Lim Chee Aun | a68dccd7cf | |
Lim Chee Aun | 9a6364a674 | |
Lim Chee Aun | e2f39596f0 | |
Lim Chee Aun | 701b9e99b3 | |
Lim Chee Aun | 294ab2bf00 | |
Lim Chee Aun | 304ce5a3e8 | |
Lim Chee Aun | 57390a291b | |
Lim Chee Aun | cd5920114f | |
Lim Chee Aun | 06c6360cae | |
Lim Chee Aun | afdfdb86da | |
Lim Chee Aun | 6f8f3e4fd0 | |
Lim Chee Aun | 342ff20986 | |
Lim Chee Aun | 94996d098e | |
Lim Chee Aun | c286562ee8 | |
Lim Chee Aun | 5babdc9d63 | |
Lim Chee Aun | 260bb8746d | |
Lim Chee Aun | 7be620808f | |
Lim Chee Aun | df3aca70fa | |
Lim Chee Aun | ec65163c89 | |
Lim Chee Aun | 6f22ec3842 | |
Lim Chee Aun | 2faf9b4c20 | |
Lim Chee Aun | 501e43207b | |
Lim Chee Aun | e782cc0dde | |
Lim Chee Aun | aefda31c2a | |
Lim Chee Aun | 9285a0ba9a | |
Chee Aun | 7fb56d9f6c | |
steve mookie kong | f7c69e56e9 | |
Lim Chee Aun | c3bcf3d595 | |
Lim Chee Aun | 0efa39b825 | |
Lim Chee Aun | a0d2037007 | |
Lim Chee Aun | 6e73728e2b | |
Lim Chee Aun | 60920966d6 | |
Lim Chee Aun | 5083463942 | |
Lim Chee Aun | 8b5fee3dfd | |
Lim Chee Aun | c9124bf150 | |
Lim Chee Aun | b85174155c | |
Lim Chee Aun | 5c9f6bae3c | |
Lim Chee Aun | 4e5940900e | |
Lim Chee Aun | 7fa0b4f076 | |
Lim Chee Aun | ecfcc68f15 |
|
@ -179,6 +179,9 @@ Available variables:
|
|||
- May specify a self-hosted Lingva instance, powered by either [lingva-translate](https://github.com/thedaviddelta/lingva-translate) or [lingva-api](https://github.com/cheeaun/lingva-api)
|
||||
- List of fallback instances hard-coded in `/.env`
|
||||
- [↗️ List of lingva-translate instances](https://github.com/thedaviddelta/lingva-translate?tab=readme-ov-file#instances)
|
||||
- `PHANPY_IMG_ALT_API_URL` (optional, no defaults):
|
||||
- API endpoint for self-hosted instance of [img-alt-api](https://github.com/cheeaun/img-alt-api).
|
||||
- If provided, a setting will appear for users to enable the image description generator in the composer. Disabled by default.
|
||||
- `PHANPY_GIPHY_API_KEY` (optional, no defaults):
|
||||
- API key for [GIPHY](https://developers.giphy.com/). See [API docs](https://developers.giphy.com/docs/api/).
|
||||
- If provided, a setting will appear for users to enable the GIF picker in the composer. Disabled by default.
|
||||
|
@ -205,6 +208,7 @@ These are self-hosted by other wonderful folks.
|
|||
- [phanpy.hear-me.social](https://phanpy.hear-me.social) by [@admin@hear-me.social](https://hear-me.social/@admin)
|
||||
- [phanpy.fulda.social](https://phanpy.fulda.social) by [@Ganneff@fulda.social](https://fulda.social/@Ganneff)
|
||||
- [phanpy.crmbl.uk](https://phanpy.crmbl.uk) by [@snail@crmbl.uk](https://mstdn.crmbl.uk/@snail)
|
||||
- [halo.mookiesplace.com](https://halo.mookiesplace.com) by [@mookie@mookiesplace.com](https://mookiesplace.com/@mookie)
|
||||
|
||||
> Note: Add yours by creating a pull request.
|
||||
|
||||
|
@ -256,6 +260,7 @@ And here I am. Building a Mastodon web client.
|
|||
- [Tusked](https://tusked.app/)
|
||||
- [Mastodon Glitch Edition (standalone frontend)](https://iceshrimp.dev/iceshrimp/masto-fe-standalone)
|
||||
- [Mangane](https://github.com/BDX-town/Mangane)
|
||||
- [TheDesk](https://github.com/cutls/TheDesk)
|
||||
- [More...](https://github.com/hueyy/awesome-mastodon/#clients)
|
||||
|
||||
## 💁♂️ Notice to all other social media client developers
|
||||
|
|
|
@ -49,9 +49,9 @@
|
|||
"@trivago/prettier-plugin-sort-imports": "~4.3.0",
|
||||
"postcss": "~8.4.38",
|
||||
"postcss-dark-theme-class": "~1.2.1",
|
||||
"postcss-preset-env": "~9.5.2",
|
||||
"postcss-preset-env": "~9.5.4",
|
||||
"twitter-text": "~3.1.0",
|
||||
"vite": "~5.2.6",
|
||||
"vite": "~5.2.8",
|
||||
"vite-plugin-generate-file": "~0.1.1",
|
||||
"vite-plugin-html-config": "~1.0.11",
|
||||
"vite-plugin-pwa": "~0.19.7",
|
||||
|
@ -1974,9 +1974,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@csstools/color-helpers": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-4.0.0.tgz",
|
||||
"integrity": "sha512-wjyXB22/h2OvxAr3jldPB7R7kjTUEzopvjitS8jWtyd8fN6xJ8vy1HnHu0ZNfEkqpBJgQ76Q+sBDshWcMvTa/w==",
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-4.1.0.tgz",
|
||||
"integrity": "sha512-pWRKF6cDwget8HowIIf2MqEmqIca/cf8/jO4b3PRtUF5EfQXYMtBIKycXB4yXTCUmwLKOoRZAzh/hjnc7ywOIg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
@ -2016,9 +2016,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@csstools/css-color-parser": {
|
||||
"version": "1.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-1.6.2.tgz",
|
||||
"integrity": "sha512-mlt0PomBlDXMGcbPAqCG36Fw35LZTtaSgCQCHEs4k8QTv1cUKe0rJDlFSJMHtqrgQiLC7LAAS9+s9kKQp2ou/Q==",
|
||||
"version": "1.6.3",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-1.6.3.tgz",
|
||||
"integrity": "sha512-pQPUPo32HW3/NuZxrwr3VJHE+vGqSTVI5gK4jGbuJ7eOFUrsTmZikXcVdInCVWOvuxK5xbCzwDWoTlZUCAKN+A==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
@ -2031,7 +2031,7 @@
|
|||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"@csstools/color-helpers": "^4.0.0",
|
||||
"@csstools/color-helpers": "^4.1.0",
|
||||
"@csstools/css-calc": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
|
@ -2107,9 +2107,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@csstools/postcss-cascade-layers": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-4.0.3.tgz",
|
||||
"integrity": "sha512-RbkQoOH23yGhWVetgBTwFgIOHEyU2tKMN7blTz/YAKKabR6tr9pP7mYS23Q9snFY2hr8WSaV8Le64KdM9BtUSA==",
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-4.0.4.tgz",
|
||||
"integrity": "sha512-MKErv8lpEwVmAcAwidY1Kfd3oWrh2Q14kxHs9xn26XzjP/PrcdngWq63lJsZeMlBY7o+WlEOeE+FP6zPzeY2uw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
@ -2122,7 +2122,7 @@
|
|||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"@csstools/selector-specificity": "^3.0.2",
|
||||
"@csstools/selector-specificity": "^3.0.3",
|
||||
"postcss-selector-parser": "^6.0.13"
|
||||
},
|
||||
"engines": {
|
||||
|
@ -2133,9 +2133,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@csstools/postcss-color-function": {
|
||||
"version": "3.0.12",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/postcss-color-function/-/postcss-color-function-3.0.12.tgz",
|
||||
"integrity": "sha512-amPGGDI4Xmgu7VN2ciKQe0pP/j5raaETT50nzbnkydp9FMw7imKxSUnXdVQU4NmRgpLKIc5Q7jox0MFhMBImIg==",
|
||||
"version": "3.0.13",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/postcss-color-function/-/postcss-color-function-3.0.13.tgz",
|
||||
"integrity": "sha512-gM24cIPU45HSPJ2zllz7VKjS1OKQS1sKOMI7Wsw8gFyXSGAGrxhYo++McylOqOXd8ecMaKxKQMUJqJVibvJYig==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
@ -2148,10 +2148,10 @@
|
|||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"@csstools/css-color-parser": "^1.6.2",
|
||||
"@csstools/css-color-parser": "^1.6.3",
|
||||
"@csstools/css-parser-algorithms": "^2.6.1",
|
||||
"@csstools/css-tokenizer": "^2.2.4",
|
||||
"@csstools/postcss-progressive-custom-properties": "^3.1.1",
|
||||
"@csstools/postcss-progressive-custom-properties": "^3.2.0",
|
||||
"@csstools/utilities": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
|
@ -2162,9 +2162,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@csstools/postcss-color-mix-function": {
|
||||
"version": "2.0.12",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/postcss-color-mix-function/-/postcss-color-mix-function-2.0.12.tgz",
|
||||
"integrity": "sha512-qpAEGwVVqHSa88i3gLb43IMpT4/LyZEE8HzZylQKKXFVJ7XykXaORTmXySxyH6H+flT+NyCnutKG2fegCVyCug==",
|
||||
"version": "2.0.13",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/postcss-color-mix-function/-/postcss-color-mix-function-2.0.13.tgz",
|
||||
"integrity": "sha512-mD8IIfGVeWkN1H1wfCqYePOg4cDnVrOXm4P0OlYcvKriq6sImGCGShv/2D88q6s3iUlLXfUBES+DUjLVjDMhnw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
@ -2177,10 +2177,10 @@
|
|||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"@csstools/css-color-parser": "^1.6.2",
|
||||
"@csstools/css-color-parser": "^1.6.3",
|
||||
"@csstools/css-parser-algorithms": "^2.6.1",
|
||||
"@csstools/css-tokenizer": "^2.2.4",
|
||||
"@csstools/postcss-progressive-custom-properties": "^3.1.1",
|
||||
"@csstools/postcss-progressive-custom-properties": "^3.2.0",
|
||||
"@csstools/utilities": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
|
@ -2244,9 +2244,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@csstools/postcss-gamut-mapping": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/postcss-gamut-mapping/-/postcss-gamut-mapping-1.0.5.tgz",
|
||||
"integrity": "sha512-AJ74/4nHXgghLWY4/ydEhu3mzwN8c56EjIGrJsoEhKaNuGBAOtUfE5qbkc9XQQ0G2FMhHggqE+9eRrApeK7ebQ==",
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/postcss-gamut-mapping/-/postcss-gamut-mapping-1.0.6.tgz",
|
||||
"integrity": "sha512-qGFpHU9cRf9qqkbHh9cWMTlBtGi/ujPgP/znQdwkbB4TgDR1ddI5wRRrksBsx64sfoUSlIEd70bxXzD9FtfdLg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
@ -2259,7 +2259,7 @@
|
|||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"@csstools/css-color-parser": "^1.6.2",
|
||||
"@csstools/css-color-parser": "^1.6.3",
|
||||
"@csstools/css-parser-algorithms": "^2.6.1",
|
||||
"@csstools/css-tokenizer": "^2.2.4"
|
||||
},
|
||||
|
@ -2271,9 +2271,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@csstools/postcss-gradients-interpolation-method": {
|
||||
"version": "4.0.13",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/postcss-gradients-interpolation-method/-/postcss-gradients-interpolation-method-4.0.13.tgz",
|
||||
"integrity": "sha512-dBbyxs9g+mrIzmEH+UtrqJUmvcJB/60j0ijhBcVJMHCgl/rKjj8ey6r/pJOI0EhkVsckOu3Prc9AGzH88C+1pQ==",
|
||||
"version": "4.0.14",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/postcss-gradients-interpolation-method/-/postcss-gradients-interpolation-method-4.0.14.tgz",
|
||||
"integrity": "sha512-VMWC3xtpchHJoRBb/fs1gJR/5nHopX+0GwwmgdCI1DjROtfWUKIW0nv8occ922Gv0/Lk93XBtYBv8JttVBMZUQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
@ -2286,10 +2286,10 @@
|
|||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"@csstools/css-color-parser": "^1.6.2",
|
||||
"@csstools/css-color-parser": "^1.6.3",
|
||||
"@csstools/css-parser-algorithms": "^2.6.1",
|
||||
"@csstools/css-tokenizer": "^2.2.4",
|
||||
"@csstools/postcss-progressive-custom-properties": "^3.1.1",
|
||||
"@csstools/postcss-progressive-custom-properties": "^3.2.0",
|
||||
"@csstools/utilities": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
|
@ -2300,9 +2300,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@csstools/postcss-hwb-function": {
|
||||
"version": "3.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/postcss-hwb-function/-/postcss-hwb-function-3.0.11.tgz",
|
||||
"integrity": "sha512-c36FtMFptwGn5CmsfdONA40IlWG2lHeoC/TDyED/7lwiTht5okxe6iLAa9t2LjBBo5AHQSHfeMvOASdXk/SHog==",
|
||||
"version": "3.0.12",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/postcss-hwb-function/-/postcss-hwb-function-3.0.12.tgz",
|
||||
"integrity": "sha512-90kIs+FsM6isAXLVoFHTTl4h0J6g1J1M6ahpIjAs6/k7a2A9FB/q+l0MHpLre0ZiPlBf2y3e1j4L+79vml7kJw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
@ -2315,10 +2315,10 @@
|
|||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"@csstools/css-color-parser": "^1.6.2",
|
||||
"@csstools/css-color-parser": "^1.6.3",
|
||||
"@csstools/css-parser-algorithms": "^2.6.1",
|
||||
"@csstools/css-tokenizer": "^2.2.4",
|
||||
"@csstools/postcss-progressive-custom-properties": "^3.1.1",
|
||||
"@csstools/postcss-progressive-custom-properties": "^3.2.0",
|
||||
"@csstools/utilities": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
|
@ -2329,9 +2329,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@csstools/postcss-ic-unit": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/postcss-ic-unit/-/postcss-ic-unit-3.0.5.tgz",
|
||||
"integrity": "sha512-9CriM/zvKXa/lDARlxs/MgeyKE6ZmmX4V77VLD7VUxKLVSt0Go3NCy/gRMbwGzxbrk3iaHFXnFbc2lNw+/7jcg==",
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/postcss-ic-unit/-/postcss-ic-unit-3.0.6.tgz",
|
||||
"integrity": "sha512-fHaU9C/sZPauXMrzPitZ/xbACbvxbkPpHoUgB9Kw5evtsBWdVkVrajOyiT9qX7/c+G1yjApoQjP1fQatldsy9w==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
@ -2344,7 +2344,7 @@
|
|||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"@csstools/postcss-progressive-custom-properties": "^3.1.1",
|
||||
"@csstools/postcss-progressive-custom-properties": "^3.2.0",
|
||||
"@csstools/utilities": "^1.0.0",
|
||||
"postcss-value-parser": "^4.2.0"
|
||||
},
|
||||
|
@ -2378,9 +2378,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@csstools/postcss-is-pseudo-class": {
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-4.0.5.tgz",
|
||||
"integrity": "sha512-qG3MI7IN3KY9UwdaE9E7G7sFydscVW7nAj5OGwaBP9tQPEEVdxXTGI+l1ZW5EUpZFSj+u3q/22fH5+8HI72+Bg==",
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-4.0.6.tgz",
|
||||
"integrity": "sha512-HilOhAsMpFheMYkuaREZx+CGa4hsG6kQdzwXSsuqKDFzYz2eIMP213+3dH/vUbPXaWrzqLKr8m3i0dgYPoh7vg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
@ -2393,7 +2393,7 @@
|
|||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"@csstools/selector-specificity": "^3.0.2",
|
||||
"@csstools/selector-specificity": "^3.0.3",
|
||||
"postcss-selector-parser": "^6.0.13"
|
||||
},
|
||||
"engines": {
|
||||
|
@ -2404,9 +2404,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@csstools/postcss-light-dark-function": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/postcss-light-dark-function/-/postcss-light-dark-function-1.0.1.tgz",
|
||||
"integrity": "sha512-CJOcp+m7Njbu91HtYMMoYuZznsvNSpJtLiR/7BO8/bHTXYPiuAZfxunh7wXLkMbHd5dRBgAVAQZ+H4iFqrvWZw==",
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/postcss-light-dark-function/-/postcss-light-dark-function-1.0.3.tgz",
|
||||
"integrity": "sha512-izW8hvhOqJlarLcGXO5PSylW9pQS3fytmhRdx2/e1oZFi15vs7ZShOHcREHJ3FfGdYqDA10cP9uhH0A3hmm1Rw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
@ -2421,7 +2421,7 @@
|
|||
"dependencies": {
|
||||
"@csstools/css-parser-algorithms": "^2.6.1",
|
||||
"@csstools/css-tokenizer": "^2.2.4",
|
||||
"@csstools/postcss-progressive-custom-properties": "^3.1.1",
|
||||
"@csstools/postcss-progressive-custom-properties": "^3.2.0",
|
||||
"@csstools/utilities": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
|
@ -2655,9 +2655,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@csstools/postcss-oklab-function": {
|
||||
"version": "3.0.12",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/postcss-oklab-function/-/postcss-oklab-function-3.0.12.tgz",
|
||||
"integrity": "sha512-RNitTHamFvUUh8x+MJuPd2tCekYexUrylGKfUoor5D2GGcgzY1WB6Bl3pIj9t8bAq5h/lcacKaB2wmvUOTfGgQ==",
|
||||
"version": "3.0.13",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/postcss-oklab-function/-/postcss-oklab-function-3.0.13.tgz",
|
||||
"integrity": "sha512-xbzMmukDFAwCt2+279io7ZiamZj87s6cnU3UgKB3G+NMpRX9A6uvN8xlnTLCe384hqg6hix5vlOmwkxqACb5pg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
@ -2670,10 +2670,10 @@
|
|||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"@csstools/css-color-parser": "^1.6.2",
|
||||
"@csstools/css-color-parser": "^1.6.3",
|
||||
"@csstools/css-parser-algorithms": "^2.6.1",
|
||||
"@csstools/css-tokenizer": "^2.2.4",
|
||||
"@csstools/postcss-progressive-custom-properties": "^3.1.1",
|
||||
"@csstools/postcss-progressive-custom-properties": "^3.2.0",
|
||||
"@csstools/utilities": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
|
@ -2684,9 +2684,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@csstools/postcss-progressive-custom-properties": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/postcss-progressive-custom-properties/-/postcss-progressive-custom-properties-3.1.1.tgz",
|
||||
"integrity": "sha512-cx/bZgj+MK8SpRZNTu2zGeVFMCQfhsaeuDhukAhfA53yykvIXaTIwLi5shW9hfkvPrkqBeFoiRAzq/qogxeHTA==",
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/postcss-progressive-custom-properties/-/postcss-progressive-custom-properties-3.2.0.tgz",
|
||||
"integrity": "sha512-BZlirVxCRgKlE7yVme+Xvif72eTn1MYXj8oZ4Knb+jwaH4u3AN1DjbhM7j86RP5vvuAOexJ4JwfifYYKWMN/QQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
@ -2709,9 +2709,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@csstools/postcss-relative-color-syntax": {
|
||||
"version": "2.0.12",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/postcss-relative-color-syntax/-/postcss-relative-color-syntax-2.0.12.tgz",
|
||||
"integrity": "sha512-VreDGDgE634niwCytLtkoE5kRxfva7bnMzSoyok7Eh9VPYFOm8CK/oJXt9y3df71Bxc9PG4KC8RA3CxTknudnw==",
|
||||
"version": "2.0.13",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/postcss-relative-color-syntax/-/postcss-relative-color-syntax-2.0.13.tgz",
|
||||
"integrity": "sha512-mENWPNcHdiEYtjHFfZP9U1jNukQgFpSQ7wvTvwiadK3qgNBiSl0vMSinM9kKsGsJLTHQ0LEAqWLHurU52I4Jeg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
@ -2724,10 +2724,10 @@
|
|||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"@csstools/css-color-parser": "^1.6.2",
|
||||
"@csstools/css-color-parser": "^1.6.3",
|
||||
"@csstools/css-parser-algorithms": "^2.6.1",
|
||||
"@csstools/css-tokenizer": "^2.2.4",
|
||||
"@csstools/postcss-progressive-custom-properties": "^3.1.1",
|
||||
"@csstools/postcss-progressive-custom-properties": "^3.2.0",
|
||||
"@csstools/utilities": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
|
@ -2790,9 +2790,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@csstools/postcss-text-decoration-shorthand": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/postcss-text-decoration-shorthand/-/postcss-text-decoration-shorthand-3.0.4.tgz",
|
||||
"integrity": "sha512-yUZmbnUemgQmja7SpOZeU45+P49wNEgQguRdyTktFkZsHf7Gof+ZIYfvF6Cm+LsU1PwSupy4yUeEKKjX5+k6cQ==",
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/postcss-text-decoration-shorthand/-/postcss-text-decoration-shorthand-3.0.5.tgz",
|
||||
"integrity": "sha512-qKxXpD0TYINkUtWDN1RHdeWKtZCzEv5j3UMT/ZGqyY27icwCFw7iKO0bUeLSHjYFBqhurCWvoOsa9REqLdrNDw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
@ -2805,7 +2805,7 @@
|
|||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"@csstools/color-helpers": "^4.0.0",
|
||||
"@csstools/color-helpers": "^4.1.0",
|
||||
"postcss-value-parser": "^4.2.0"
|
||||
},
|
||||
"engines": {
|
||||
|
@ -2887,9 +2887,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@csstools/selector-specificity": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-3.0.2.tgz",
|
||||
"integrity": "sha512-RpHaZ1h9LE7aALeQXmXrJkRG84ZxIsctEN2biEUmFyKpzFM3zZ35eUMcIzZFsw/2olQE6v69+esEqU2f1MKycg==",
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-3.0.3.tgz",
|
||||
"integrity": "sha512-KEPNw4+WW5AVEIyzC80rTbWEUatTW2lXpN8+8ILC8PiPeWPjwUzrPZDIOZ2wwqDmeqOYTdSGyL3+vE5GC3FB3Q==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
@ -4091,9 +4091,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/autoprefixer": {
|
||||
"version": "10.4.18",
|
||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.18.tgz",
|
||||
"integrity": "sha512-1DKbDfsr6KUElM6wg+0zRNkB/Q7WcKYAaK+pzXn+Xqmszm/5Xa9coeNdtP88Vi+dPzZnMjhge8GIV49ZQkDa+g==",
|
||||
"version": "10.4.19",
|
||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz",
|
||||
"integrity": "sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
@ -4111,7 +4111,7 @@
|
|||
],
|
||||
"dependencies": {
|
||||
"browserslist": "^4.23.0",
|
||||
"caniuse-lite": "^1.0.30001591",
|
||||
"caniuse-lite": "^1.0.30001599",
|
||||
"fraction.js": "^4.3.7",
|
||||
"normalize-range": "^0.1.2",
|
||||
"picocolors": "^1.0.0",
|
||||
|
@ -4306,9 +4306,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001597",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001597.tgz",
|
||||
"integrity": "sha512-7LjJvmQU6Sj7bL0j5b5WY/3n7utXUJvAe1lxhsHDbLmwX9mdL86Yjtr+5SRCyf8qME4M7pU2hswj0FpyBVCv9w==",
|
||||
"version": "1.0.30001605",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001605.tgz",
|
||||
"integrity": "sha512-nXwGlFWo34uliI9z3n6Qc0wZaf7zaZWA1CPZ169La5mV3I/gem7bst0vr5XQH5TJXZIMfDeZyOrZnSlVzKxxHQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
@ -4494,9 +4494,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/css-has-pseudo": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/css-has-pseudo/-/css-has-pseudo-6.0.2.tgz",
|
||||
"integrity": "sha512-Z2Qm5yyOvJRTy6THdUlnGIX6PW/1wOc4FHWlfkcBkfkpZ3oz6lPdG+h+J7t1HZHT4uSSVR8XatXiMpqMUADXow==",
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/css-has-pseudo/-/css-has-pseudo-6.0.3.tgz",
|
||||
"integrity": "sha512-qIsDxK/z0byH/mpNsv5hzQ5NOl8m1FRmOLgZpx4bG5uYHnOlO2XafeMI4mFIgNSViHwoUWcxSJZyyijaAmbs+A==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
@ -4509,7 +4509,7 @@
|
|||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"@csstools/selector-specificity": "^3.0.2",
|
||||
"@csstools/selector-specificity": "^3.0.3",
|
||||
"postcss-selector-parser": "^6.0.13",
|
||||
"postcss-value-parser": "^4.2.0"
|
||||
},
|
||||
|
@ -4571,9 +4571,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/cssdb": {
|
||||
"version": "7.11.2",
|
||||
"resolved": "https://registry.npmjs.org/cssdb/-/cssdb-7.11.2.tgz",
|
||||
"integrity": "sha512-lhQ32TFkc1X4eTefGfYPvgovRSzIMofHkigfH8nWtyRL4XJLsRhJFreRvEgKzept7x1rjBuy3J/MurXLaFxW/A==",
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cssdb/-/cssdb-8.0.0.tgz",
|
||||
"integrity": "sha512-hfpm8VXc7/dhcEWpLvKDLwImOSk1sa2DxL36OEiY/4h2MGfKjPYIMZo4hnEEl+TCJr2GwcX46jF5TafRASDe9w==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
@ -6424,9 +6424,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/postcss-color-functional-notation": {
|
||||
"version": "6.0.7",
|
||||
"resolved": "https://registry.npmjs.org/postcss-color-functional-notation/-/postcss-color-functional-notation-6.0.7.tgz",
|
||||
"integrity": "sha512-VwzaVfu1kEYDK2yM8ixeKA/QbgQ8k0uxpRevLH9Wam+R3C1sg68vnRB7m2AMhYfjqb5khp4p0EQk5aO90ASAkw==",
|
||||
"version": "6.0.8",
|
||||
"resolved": "https://registry.npmjs.org/postcss-color-functional-notation/-/postcss-color-functional-notation-6.0.8.tgz",
|
||||
"integrity": "sha512-BilFPTHcfWEnuQeqL83nbSPVK3tcU57S60aOrqgditarNDzOojyF0Gdc2Ur5L+zox366QjrCe0rOBLDO2pNvRQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
@ -6439,10 +6439,10 @@
|
|||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"@csstools/css-color-parser": "^1.6.2",
|
||||
"@csstools/css-color-parser": "^1.6.3",
|
||||
"@csstools/css-parser-algorithms": "^2.6.1",
|
||||
"@csstools/css-tokenizer": "^2.2.4",
|
||||
"@csstools/postcss-progressive-custom-properties": "^3.1.1",
|
||||
"@csstools/postcss-progressive-custom-properties": "^3.2.0",
|
||||
"@csstools/utilities": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
|
@ -6637,9 +6637,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/postcss-double-position-gradients": {
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/postcss-double-position-gradients/-/postcss-double-position-gradients-5.0.5.tgz",
|
||||
"integrity": "sha512-26Tx4BfoxMNO9C89Nk56bfGv4jAwdDVgrQOyHZOP/6/D+xuOBf306KzTjHC2oBzaIIVtX+famOWHv4raxMjJMQ==",
|
||||
"version": "5.0.6",
|
||||
"resolved": "https://registry.npmjs.org/postcss-double-position-gradients/-/postcss-double-position-gradients-5.0.6.tgz",
|
||||
"integrity": "sha512-QJ+089FKMaqDxOhhIHsJrh4IP7h4PIHNC5jZP5PMmnfUScNu8Hji2lskqpFWCvu+5sj+2EJFyzKd13sLEWOZmQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
@ -6652,7 +6652,7 @@
|
|||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"@csstools/postcss-progressive-custom-properties": "^3.1.1",
|
||||
"@csstools/postcss-progressive-custom-properties": "^3.2.0",
|
||||
"@csstools/utilities": "^1.0.0",
|
||||
"postcss-value-parser": "^4.2.0"
|
||||
},
|
||||
|
@ -6772,9 +6772,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/postcss-lab-function": {
|
||||
"version": "6.0.12",
|
||||
"resolved": "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-6.0.12.tgz",
|
||||
"integrity": "sha512-flHW2jdRCRe8ClhMgrylR1BCiyyqLLvp1qKfO5wuAclUihldfRsoDIFQWFVW7rJbruil9/LCoHNUvY9JwTlLPw==",
|
||||
"version": "6.0.13",
|
||||
"resolved": "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-6.0.13.tgz",
|
||||
"integrity": "sha512-tzEThi3prSyomnVqaAU+k/YJib4rxeeTKVfMt+mPcEugFgp0t6xRjoc7fzaWCoEwYLC6GxGLD8/Ugx8COCqabw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
@ -6787,10 +6787,10 @@
|
|||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"@csstools/css-color-parser": "^1.6.2",
|
||||
"@csstools/css-color-parser": "^1.6.3",
|
||||
"@csstools/css-parser-algorithms": "^2.6.1",
|
||||
"@csstools/css-tokenizer": "^2.2.4",
|
||||
"@csstools/postcss-progressive-custom-properties": "^3.1.1",
|
||||
"@csstools/postcss-progressive-custom-properties": "^3.2.0",
|
||||
"@csstools/utilities": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
|
@ -6826,9 +6826,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/postcss-nesting": {
|
||||
"version": "12.1.0",
|
||||
"resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-12.1.0.tgz",
|
||||
"integrity": "sha512-QOYnosaZ+mlP6plQrAxFw09UUp2Sgtxj1BVHN+rSVbtV0Yx48zRt9/9F/ZOoxOKBBEsaJk2MYhhVRjeRRw5yuw==",
|
||||
"version": "12.1.1",
|
||||
"resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-12.1.1.tgz",
|
||||
"integrity": "sha512-qc74KvIAQNa5ujZKG1UV286dhaDW6basbUy2i9AzNU/T8C9hpvGu9NZzm1SfePe2yP7sPYgpA8d4sPVopn2Hhw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
@ -6842,7 +6842,7 @@
|
|||
],
|
||||
"dependencies": {
|
||||
"@csstools/selector-resolve-nested": "^1.1.0",
|
||||
"@csstools/selector-specificity": "^3.0.2",
|
||||
"@csstools/selector-specificity": "^3.0.3",
|
||||
"postcss-selector-parser": "^6.0.13"
|
||||
},
|
||||
"engines": {
|
||||
|
@ -6936,9 +6936,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/postcss-preset-env": {
|
||||
"version": "9.5.2",
|
||||
"resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-9.5.2.tgz",
|
||||
"integrity": "sha512-/KIAHELdg5BxsKA/Vc6Nok/66EM7lps8NulKcQWX2S52HdzxAqh+6HcuAFj7trRSW587vlOA4zCjlRFgR+W6Ag==",
|
||||
"version": "9.5.4",
|
||||
"resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-9.5.4.tgz",
|
||||
"integrity": "sha512-o/jOlJjhm4f6rI5q1f+4Og3tz1cjaO50er9ndk7ZdcXHjWOH49kMAhqDC/nQifypQkOAiAmF46dPt3pZM+Cwbg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
@ -6951,18 +6951,18 @@
|
|||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"@csstools/postcss-cascade-layers": "^4.0.3",
|
||||
"@csstools/postcss-color-function": "^3.0.12",
|
||||
"@csstools/postcss-color-mix-function": "^2.0.12",
|
||||
"@csstools/postcss-cascade-layers": "^4.0.4",
|
||||
"@csstools/postcss-color-function": "^3.0.13",
|
||||
"@csstools/postcss-color-mix-function": "^2.0.13",
|
||||
"@csstools/postcss-exponential-functions": "^1.0.5",
|
||||
"@csstools/postcss-font-format-keywords": "^3.0.2",
|
||||
"@csstools/postcss-gamut-mapping": "^1.0.5",
|
||||
"@csstools/postcss-gradients-interpolation-method": "^4.0.13",
|
||||
"@csstools/postcss-hwb-function": "^3.0.11",
|
||||
"@csstools/postcss-ic-unit": "^3.0.5",
|
||||
"@csstools/postcss-gamut-mapping": "^1.0.6",
|
||||
"@csstools/postcss-gradients-interpolation-method": "^4.0.14",
|
||||
"@csstools/postcss-hwb-function": "^3.0.12",
|
||||
"@csstools/postcss-ic-unit": "^3.0.6",
|
||||
"@csstools/postcss-initial": "^1.0.1",
|
||||
"@csstools/postcss-is-pseudo-class": "^4.0.5",
|
||||
"@csstools/postcss-light-dark-function": "^1.0.1",
|
||||
"@csstools/postcss-is-pseudo-class": "^4.0.6",
|
||||
"@csstools/postcss-light-dark-function": "^1.0.3",
|
||||
"@csstools/postcss-logical-float-and-clear": "^2.0.1",
|
||||
"@csstools/postcss-logical-overflow": "^1.0.1",
|
||||
"@csstools/postcss-logical-overscroll-behavior": "^1.0.1",
|
||||
|
@ -6972,38 +6972,38 @@
|
|||
"@csstools/postcss-media-queries-aspect-ratio-number-values": "^2.0.7",
|
||||
"@csstools/postcss-nested-calc": "^3.0.2",
|
||||
"@csstools/postcss-normalize-display-values": "^3.0.2",
|
||||
"@csstools/postcss-oklab-function": "^3.0.12",
|
||||
"@csstools/postcss-progressive-custom-properties": "^3.1.1",
|
||||
"@csstools/postcss-relative-color-syntax": "^2.0.12",
|
||||
"@csstools/postcss-oklab-function": "^3.0.13",
|
||||
"@csstools/postcss-progressive-custom-properties": "^3.2.0",
|
||||
"@csstools/postcss-relative-color-syntax": "^2.0.13",
|
||||
"@csstools/postcss-scope-pseudo-class": "^3.0.1",
|
||||
"@csstools/postcss-stepped-value-functions": "^3.0.6",
|
||||
"@csstools/postcss-text-decoration-shorthand": "^3.0.4",
|
||||
"@csstools/postcss-text-decoration-shorthand": "^3.0.5",
|
||||
"@csstools/postcss-trigonometric-functions": "^3.0.6",
|
||||
"@csstools/postcss-unset-value": "^3.0.1",
|
||||
"autoprefixer": "^10.4.18",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"browserslist": "^4.22.3",
|
||||
"css-blank-pseudo": "^6.0.1",
|
||||
"css-has-pseudo": "^6.0.2",
|
||||
"css-has-pseudo": "^6.0.3",
|
||||
"css-prefers-color-scheme": "^9.0.1",
|
||||
"cssdb": "^7.11.1",
|
||||
"cssdb": "^8.0.0",
|
||||
"postcss-attribute-case-insensitive": "^6.0.3",
|
||||
"postcss-clamp": "^4.1.0",
|
||||
"postcss-color-functional-notation": "^6.0.7",
|
||||
"postcss-color-functional-notation": "^6.0.8",
|
||||
"postcss-color-hex-alpha": "^9.0.4",
|
||||
"postcss-color-rebeccapurple": "^9.0.3",
|
||||
"postcss-custom-media": "^10.0.4",
|
||||
"postcss-custom-properties": "^13.3.6",
|
||||
"postcss-custom-selectors": "^7.1.8",
|
||||
"postcss-dir-pseudo-class": "^8.0.1",
|
||||
"postcss-double-position-gradients": "^5.0.5",
|
||||
"postcss-double-position-gradients": "^5.0.6",
|
||||
"postcss-focus-visible": "^9.0.1",
|
||||
"postcss-focus-within": "^8.0.1",
|
||||
"postcss-font-variant": "^5.0.0",
|
||||
"postcss-gap-properties": "^5.0.1",
|
||||
"postcss-image-set-function": "^6.0.3",
|
||||
"postcss-lab-function": "^6.0.12",
|
||||
"postcss-lab-function": "^6.0.13",
|
||||
"postcss-logical": "^7.0.1",
|
||||
"postcss-nesting": "^12.1.0",
|
||||
"postcss-nesting": "^12.1.1",
|
||||
"postcss-opacity-percentage": "^2.0.0",
|
||||
"postcss-overflow-shorthand": "^5.0.1",
|
||||
"postcss-page-break": "^3.0.4",
|
||||
|
@ -8207,13 +8207,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "5.2.6",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.2.6.tgz",
|
||||
"integrity": "sha512-FPtnxFlSIKYjZ2eosBQamz4CbyrTizbZ3hnGJlh/wMtCrlp1Hah6AzBLjGI5I2urTfNnpovpHdrL6YRuBOPnCA==",
|
||||
"version": "5.2.8",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.2.8.tgz",
|
||||
"integrity": "sha512-OyZR+c1CE8yeHw5V5t59aXsUPPVTHMDjEZz8MgguLL/Q7NblxhZUlTu9xSPqlsUO/y+X7dlU05jdhvyycD55DA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.20.1",
|
||||
"postcss": "^8.4.36",
|
||||
"postcss": "^8.4.38",
|
||||
"rollup": "^4.13.0"
|
||||
},
|
||||
"bin": {
|
||||
|
|
|
@ -51,9 +51,9 @@
|
|||
"@trivago/prettier-plugin-sort-imports": "~4.3.0",
|
||||
"postcss": "~8.4.38",
|
||||
"postcss-dark-theme-class": "~1.2.1",
|
||||
"postcss-preset-env": "~9.5.2",
|
||||
"postcss-preset-env": "~9.5.4",
|
||||
"twitter-text": "~3.1.0",
|
||||
"vite": "~5.2.6",
|
||||
"vite": "~5.2.8",
|
||||
"vite-plugin-generate-file": "~0.1.1",
|
||||
"vite-plugin-html-config": "~1.0.11",
|
||||
"vite-plugin-pwa": "~0.19.7",
|
||||
|
|
38
src/app.css
38
src/app.css
|
@ -301,6 +301,41 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.deck-container-media-first {
|
||||
.timeline {
|
||||
> li:not(.timeline-item-carousel, .timeline-item-container) {
|
||||
&:has(.status-media-first) {
|
||||
@media (min-width: 40em) {
|
||||
width: fit-content;
|
||||
max-width: min(480px, 100%);
|
||||
}
|
||||
|
||||
background-color: transparent !important;
|
||||
border: 0 !important;
|
||||
box-shadow: none !important;
|
||||
margin-inline: auto !important;
|
||||
|
||||
&:not(:first-child) {
|
||||
margin-block: 32px;
|
||||
}
|
||||
|
||||
&:has(.skeleton) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&:has(.media[data-orientation='landscape']) {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.status-link:has(.status-media-first):hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.timeline.grow {
|
||||
/* min-height: 100vh;
|
||||
min-height: 100dvh; */
|
||||
|
@ -1882,7 +1917,8 @@ body > .szh-menu-container {
|
|||
/* two columns only */
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
.szh-menu .menu-horizontal:has(> .szh-menu__item:only-child) {
|
||||
.szh-menu .menu-horizontal:has(> .szh-menu__item:only-child),
|
||||
.szh-menu .menu-horizontal:has(> .szh-menu__submenu:only-child) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.szh-menu .menu-horizontal > .szh-menu__item:not(:only-child):first-child,
|
||||
|
|
|
@ -53,7 +53,7 @@ import { getAccessToken } from './utils/auth';
|
|||
import focusDeck from './utils/focus-deck';
|
||||
import states, { initStates, statusKey } from './utils/states';
|
||||
import store from './utils/store';
|
||||
import { getCurrentAccount } from './utils/store-utils';
|
||||
import { getCurrentAccount, setCurrentAccountID } from './utils/store-utils';
|
||||
import './utils/toast-alert';
|
||||
|
||||
window.__STATES__ = states;
|
||||
|
@ -338,7 +338,7 @@ function App() {
|
|||
window.__IGNORE_GET_ACCOUNT_ERROR__ = true;
|
||||
const account = getCurrentAccount();
|
||||
if (account) {
|
||||
store.session.set('currentAccount', account.info.id);
|
||||
setCurrentAccountID(account.info.id);
|
||||
const { client } = api({ account });
|
||||
const { instance } = client;
|
||||
// console.log('masto', masto);
|
||||
|
|
|
@ -22,7 +22,8 @@ import shortenNumber from '../utils/shorten-number';
|
|||
import showToast from '../utils/show-toast';
|
||||
import states, { hideAllModals } from '../utils/states';
|
||||
import store from '../utils/store';
|
||||
import { updateAccount } from '../utils/store-utils';
|
||||
import { getCurrentAccountID, updateAccount } from '../utils/store-utils';
|
||||
import supports from '../utils/supports';
|
||||
|
||||
import AccountBlock from './account-block';
|
||||
import Avatar from './avatar';
|
||||
|
@ -198,10 +199,7 @@ function AccountInfo({
|
|||
}
|
||||
}
|
||||
|
||||
const isSelf = useMemo(
|
||||
() => id === store.session.get('currentAccount'),
|
||||
[id],
|
||||
);
|
||||
const isSelf = useMemo(() => id === getCurrentAccountID(), [id]);
|
||||
|
||||
useEffect(() => {
|
||||
const infoHasEssentials = !!(
|
||||
|
@ -254,12 +252,13 @@ function AccountInfo({
|
|||
// On first load, fetch familiar followers, merge to top of results' `value`
|
||||
// Remove dups on every fetch
|
||||
if (firstLoad) {
|
||||
const familiarFollowers = await masto.v1.accounts.familiarFollowers.fetch(
|
||||
{
|
||||
let familiarFollowers = [];
|
||||
try {
|
||||
familiarFollowers = await masto.v1.accounts.familiarFollowers.fetch({
|
||||
id: [id],
|
||||
},
|
||||
);
|
||||
familiarFollowersCache.current = familiarFollowers[0].accounts;
|
||||
});
|
||||
} catch (e) {}
|
||||
familiarFollowersCache.current = familiarFollowers?.[0]?.accounts || [];
|
||||
newValue = [
|
||||
...familiarFollowersCache.current,
|
||||
...value.filter(
|
||||
|
@ -919,7 +918,7 @@ function RelatedActions({
|
|||
|
||||
useEffect(() => {
|
||||
if (info) {
|
||||
const currentAccount = store.session.get('currentAccount');
|
||||
const currentAccount = getCurrentAccountID();
|
||||
let currentID;
|
||||
(async () => {
|
||||
if (sameInstance && authenticated) {
|
||||
|
@ -1093,16 +1092,18 @@ function RelatedActions({
|
|||
<Icon icon="translate" />
|
||||
<span>Translate bio</span>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
setShowPrivateNoteModal(true);
|
||||
}}
|
||||
>
|
||||
<Icon icon="pencil" />
|
||||
<span>
|
||||
{privateNote ? 'Edit private note' : 'Add private note'}
|
||||
</span>
|
||||
</MenuItem>
|
||||
{supports('@mastodon/profile-private-note') && (
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
setShowPrivateNoteModal(true);
|
||||
}}
|
||||
>
|
||||
<Icon icon="pencil" />
|
||||
<span>
|
||||
{privateNote ? 'Edit private note' : 'Add private note'}
|
||||
</span>
|
||||
</MenuItem>
|
||||
)}
|
||||
{following && !!relationship && (
|
||||
<>
|
||||
<MenuItem
|
||||
|
@ -1451,19 +1452,22 @@ function RelatedActions({
|
|||
</MenuItem>
|
||||
</>
|
||||
)}
|
||||
{currentAuthenticated && isSelf && standalone && (
|
||||
<>
|
||||
<MenuDivider />
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
setShowEditProfile(true);
|
||||
}}
|
||||
>
|
||||
<Icon icon="pencil" />
|
||||
<span>Edit profile</span>
|
||||
</MenuItem>
|
||||
</>
|
||||
)}
|
||||
{currentAuthenticated &&
|
||||
isSelf &&
|
||||
standalone &&
|
||||
supports('@mastodon/profile-edit') && (
|
||||
<>
|
||||
<MenuDivider />
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
setShowEditProfile(true);
|
||||
}}
|
||||
>
|
||||
<Icon icon="pencil" />
|
||||
<span>Edit profile</span>
|
||||
</MenuItem>
|
||||
</>
|
||||
)}
|
||||
{import.meta.env.DEV && currentAuthenticated && isSelf && (
|
||||
<>
|
||||
<MenuDivider />
|
||||
|
|
|
@ -310,7 +310,7 @@
|
|||
|
||||
#compose-container .form-visibility-direct {
|
||||
--yellow-stripes: repeating-linear-gradient(
|
||||
-45deg,
|
||||
135deg,
|
||||
var(--reply-to-faded-color),
|
||||
var(--reply-to-faded-color) 10px,
|
||||
var(--reply-to-faded-color) 10px,
|
||||
|
|
|
@ -124,7 +124,7 @@ const MENTION_RE = new RegExp(
|
|||
|
||||
// AI-generated, all other regexes are too complicated
|
||||
const HASHTAG_RE = new RegExp(
|
||||
`(^|[^=\\/\\w])(#[a-z0-9_]+([a-z0-9_.-]+[a-z0-9_]+)?)(?![\\/\\w])`,
|
||||
`(^|[^=\\/\\w])(#[a-z0-9_]+([a-z0-9_.]+[a-z0-9_]+)?)(?![\\/\\w])`,
|
||||
'ig',
|
||||
);
|
||||
|
||||
|
@ -988,7 +988,11 @@ function Compose({
|
|||
} else {
|
||||
try {
|
||||
newStatus = await masto.v1.statuses.create(params, {
|
||||
idempotencyKey: UID.current,
|
||||
requestInit: {
|
||||
headers: {
|
||||
'Idempotency-Key': UID.current,
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (_) {
|
||||
// If idempotency key fails, try again without it
|
||||
|
@ -2370,6 +2374,10 @@ function GIFPickerModal({ onClose = () => {}, onSelect = () => {} }) {
|
|||
qRef.current?.focus();
|
||||
}, []);
|
||||
|
||||
const debouncedOnInput = useDebouncedCallback(() => {
|
||||
fetchGIFs({ offset: 0 });
|
||||
}, 1000);
|
||||
|
||||
return (
|
||||
<div id="gif-picker-sheet" class="sheet">
|
||||
{!!onClose && (
|
||||
|
@ -2396,6 +2404,7 @@ function GIFPickerModal({ onClose = () => {}, onSelect = () => {} }) {
|
|||
autocapitalize="off"
|
||||
spellCheck="false"
|
||||
dir="auto"
|
||||
onInput={debouncedOnInput}
|
||||
/>
|
||||
<input
|
||||
type="image"
|
||||
|
|
|
@ -8,6 +8,7 @@ import FilterContext from '../utils/filter-context';
|
|||
import { isFiltered } from '../utils/filters';
|
||||
import states, { statusKey } from '../utils/states';
|
||||
import store from '../utils/store';
|
||||
import { getCurrentAccountID } from '../utils/store-utils';
|
||||
|
||||
import Media from './media';
|
||||
|
||||
|
@ -88,7 +89,7 @@ function MediaPost({
|
|||
};
|
||||
|
||||
const currentAccount = useMemo(() => {
|
||||
return store.session.get('currentAccount');
|
||||
return getCurrentAccountID();
|
||||
}, []);
|
||||
const isSelf = useMemo(() => {
|
||||
return currentAccount && currentAccount === accountId;
|
||||
|
|
|
@ -74,7 +74,7 @@ function Media({
|
|||
altIndex,
|
||||
onClick = () => {},
|
||||
}) {
|
||||
const {
|
||||
let {
|
||||
blurhash,
|
||||
description,
|
||||
meta,
|
||||
|
@ -84,15 +84,27 @@ function Media({
|
|||
url,
|
||||
type,
|
||||
} = media;
|
||||
if (/no\-preview\./i.test(previewUrl)) {
|
||||
previewUrl = null;
|
||||
}
|
||||
const { original = {}, small, focus } = meta || {};
|
||||
|
||||
const width = showOriginal ? original?.width : small?.width;
|
||||
const height = showOriginal ? original?.height : small?.height;
|
||||
const width = showOriginal
|
||||
? original?.width
|
||||
: small?.width || original?.width;
|
||||
const height = showOriginal
|
||||
? original?.height
|
||||
: small?.height || original?.height;
|
||||
const mediaURL = showOriginal ? url : previewUrl || url;
|
||||
const remoteMediaURL = showOriginal
|
||||
? remoteUrl
|
||||
: previewRemoteUrl || remoteUrl;
|
||||
const orientation = width >= height ? 'landscape' : 'portrait';
|
||||
const hasDimensions = width && height;
|
||||
const orientation = hasDimensions
|
||||
? width > height
|
||||
? 'landscape'
|
||||
: 'portrait'
|
||||
: null;
|
||||
|
||||
const rgbAverageColor = blurhash ? getBlurHashAverageColor(blurhash) : null;
|
||||
|
||||
|
@ -133,7 +145,8 @@ function Media({
|
|||
enabled: pinchZoomEnabled,
|
||||
draggableUnZoomed: false,
|
||||
inertiaFriction: 0.9,
|
||||
doubleTapZoomOutOnMaxScale: true,
|
||||
tapZoomFactor: 2,
|
||||
doubleTapToggleZoom: true,
|
||||
containerProps: {
|
||||
className: 'media-zoom',
|
||||
style: {
|
||||
|
@ -290,7 +303,11 @@ function Media({
|
|||
}}
|
||||
onError={(e) => {
|
||||
const { src } = e.target;
|
||||
if (src === mediaURL && mediaURL !== remoteMediaURL) {
|
||||
if (
|
||||
src === mediaURL &&
|
||||
remoteMediaURL &&
|
||||
mediaURL !== remoteMediaURL
|
||||
) {
|
||||
e.target.src = remoteMediaURL;
|
||||
}
|
||||
}}
|
||||
|
@ -321,6 +338,20 @@ function Media({
|
|||
onLoad={(e) => {
|
||||
// e.target.closest('.media-image').style.backgroundImage = '';
|
||||
e.target.dataset.loaded = true;
|
||||
if (!hasDimensions) {
|
||||
const $media = e.target.closest('.media');
|
||||
if ($media) {
|
||||
const { naturalWidth, naturalHeight } = e.target;
|
||||
$media.dataset.orientation =
|
||||
naturalWidth > naturalHeight ? 'landscape' : 'portrait';
|
||||
$media.style.setProperty('--width', `${naturalWidth}px`);
|
||||
$media.style.setProperty(
|
||||
'--height',
|
||||
`${naturalHeight}px`,
|
||||
);
|
||||
$media.style.aspectRatio = `${naturalWidth}/${naturalHeight}`;
|
||||
}
|
||||
}
|
||||
}}
|
||||
onError={(e) => {
|
||||
const { src } = e.target;
|
||||
|
@ -338,6 +369,7 @@ function Media({
|
|||
</Figure>
|
||||
);
|
||||
} else if (type === 'gifv' || type === 'video' || isVideoMaybe) {
|
||||
const hasDuration = original.duration > 0;
|
||||
const shortDuration = original.duration < 31;
|
||||
const isGIF = type === 'gifv' && shortDuration;
|
||||
// If GIF is too long, treat it as a video
|
||||
|
@ -473,14 +505,61 @@ function Media({
|
|||
/>
|
||||
) : (
|
||||
<>
|
||||
<img
|
||||
src={previewUrl}
|
||||
alt={showInlineDesc ? '' : description}
|
||||
width={width}
|
||||
height={height}
|
||||
data-orientation={orientation}
|
||||
loading="lazy"
|
||||
/>
|
||||
{previewUrl ? (
|
||||
<img
|
||||
src={previewUrl}
|
||||
alt={showInlineDesc ? '' : description}
|
||||
width={width}
|
||||
height={height}
|
||||
data-orientation={orientation}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
onLoad={(e) => {
|
||||
if (!hasDimensions) {
|
||||
const $media = e.target.closest('.media');
|
||||
if ($media) {
|
||||
const { naturalHeight, naturalWidth } = e.target;
|
||||
$media.dataset.orientation =
|
||||
naturalWidth > naturalHeight
|
||||
? 'landscape'
|
||||
: 'portrait';
|
||||
$media.style.setProperty(
|
||||
'--width',
|
||||
`${naturalWidth}px`,
|
||||
);
|
||||
$media.style.setProperty(
|
||||
'--height',
|
||||
`${naturalHeight}px`,
|
||||
);
|
||||
$media.style.aspectRatio = `${naturalWidth}/${naturalHeight}`;
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<video
|
||||
src={url + '#t=0.1'} // Make Safari show 1st-frame preview
|
||||
width={width}
|
||||
height={height}
|
||||
data-orientation={orientation}
|
||||
preload="metadata"
|
||||
muted
|
||||
disablePictureInPicture
|
||||
onLoadedMetadata={(e) => {
|
||||
if (!hasDuration) {
|
||||
const { duration } = e.target;
|
||||
if (duration) {
|
||||
const formattedDuration = formatDuration(duration);
|
||||
const container = e.target.closest('.media-video');
|
||||
if (container) {
|
||||
container.dataset.formattedDuration =
|
||||
formattedDuration;
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div class="media-play">
|
||||
<Icon icon="play" size="xl" />
|
||||
</div>
|
||||
|
|
|
@ -11,6 +11,8 @@ import { getLists } from '../utils/lists';
|
|||
import safeBoundingBoxPadding from '../utils/safe-bounding-box-padding';
|
||||
import states from '../utils/states';
|
||||
import store from '../utils/store';
|
||||
import { getCurrentAccountID } from '../utils/store-utils';
|
||||
import supports from '../utils/supports';
|
||||
|
||||
import Avatar from './avatar';
|
||||
import Icon from './icon';
|
||||
|
@ -24,9 +26,8 @@ function NavMenu(props) {
|
|||
const [currentAccount, moreThanOneAccount] = useMemo(() => {
|
||||
const accounts = store.local.getJSON('accounts') || [];
|
||||
const acc =
|
||||
accounts.find(
|
||||
(account) => account.info.id === store.session.get('currentAccount'),
|
||||
) || accounts[0];
|
||||
accounts.find((account) => account.info.id === getCurrentAccountID()) ||
|
||||
accounts[0];
|
||||
return [acc, accounts.length > 1];
|
||||
}, []);
|
||||
|
||||
|
@ -83,8 +84,10 @@ function NavMenu(props) {
|
|||
return results;
|
||||
}
|
||||
|
||||
const supportsLists = supports('@mastodon/lists');
|
||||
const [lists, setLists] = useState([]);
|
||||
useEffect(() => {
|
||||
if (!supportsLists) return;
|
||||
if (menuState === 'open') {
|
||||
getLists().then(setLists);
|
||||
}
|
||||
|
@ -186,9 +189,11 @@ function NavMenu(props) {
|
|||
<Icon icon="history2" size="l" />
|
||||
<span>Catch-up</span>
|
||||
</MenuLink>
|
||||
<MenuLink to="/mentions">
|
||||
<Icon icon="at" size="l" /> <span>Mentions</span>
|
||||
</MenuLink>
|
||||
{supports('@mastodon/mentions') && (
|
||||
<MenuLink to="/mentions">
|
||||
<Icon icon="at" size="l" /> <span>Mentions</span>
|
||||
</MenuLink>
|
||||
)}
|
||||
<MenuLink to="/notifications">
|
||||
<Icon icon="notification" size="l" /> <span>Notifications</span>
|
||||
{snapStates.notificationsShowNew && (
|
||||
|
@ -232,10 +237,12 @@ function NavMenu(props) {
|
|||
)}
|
||||
</SubMenu2>
|
||||
) : (
|
||||
<MenuLink to="/l">
|
||||
<Icon icon="list" size="l" />
|
||||
<span>Lists</span>
|
||||
</MenuLink>
|
||||
supportsLists && (
|
||||
<MenuLink to="/l">
|
||||
<Icon icon="list" size="l" />
|
||||
<span>Lists</span>
|
||||
</MenuLink>
|
||||
)
|
||||
)}
|
||||
<MenuLink to="/b">
|
||||
<Icon icon="bookmark" size="l" /> <span>Bookmarks</span>
|
||||
|
@ -260,10 +267,12 @@ function NavMenu(props) {
|
|||
<span>Followed Hashtags</span>
|
||||
</MenuLink>
|
||||
<MenuDivider />
|
||||
<MenuLink to="/ft">
|
||||
<Icon icon="filters" size="l" />
|
||||
Filters
|
||||
</MenuLink>
|
||||
{supports('@mastodon/filters') && (
|
||||
<MenuLink to="/ft">
|
||||
<Icon icon="filters" size="l" />
|
||||
Filters
|
||||
</MenuLink>
|
||||
)}
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
states.showGenericAccounts = {
|
||||
|
|
|
@ -4,6 +4,7 @@ import { memo } from 'preact/compat';
|
|||
import shortenNumber from '../utils/shorten-number';
|
||||
import states, { statusKey } from '../utils/states';
|
||||
import store from '../utils/store';
|
||||
import { getCurrentAccountID } from '../utils/store-utils';
|
||||
import useTruncated from '../utils/useTruncated';
|
||||
|
||||
import Avatar from './avatar';
|
||||
|
@ -132,7 +133,7 @@ function Notification({
|
|||
const actualStatus = status?.reblog || status;
|
||||
const actualStatusID = actualStatus?.id;
|
||||
|
||||
const currentAccount = store.session.get('currentAccount');
|
||||
const currentAccount = getCurrentAccountID();
|
||||
const isSelf = currentAccount === account?.id;
|
||||
const isVoted = status?.poll?.voted;
|
||||
const isReplyToOthers =
|
||||
|
|
|
@ -19,6 +19,7 @@ import pmem from '../utils/pmem';
|
|||
import showToast from '../utils/show-toast';
|
||||
import states from '../utils/states';
|
||||
import store from '../utils/store';
|
||||
import { getCurrentAccountID } from '../utils/store-utils';
|
||||
|
||||
import AsyncText from './AsyncText';
|
||||
import Icon from './icon';
|
||||
|
@ -787,7 +788,7 @@ function ImportExport({ shortcuts, onClose }) {
|
|||
disabled={importUIState === 'cloud-downloading'}
|
||||
onClick={async () => {
|
||||
setImportUIState('cloud-downloading');
|
||||
const currentAccount = store.session.get('currentAccount');
|
||||
const currentAccount = getCurrentAccountID();
|
||||
showToast(
|
||||
'Downloading saved shortcuts from instance server…',
|
||||
);
|
||||
|
@ -1043,7 +1044,7 @@ function ImportExport({ shortcuts, onClose }) {
|
|||
disabled={importUIState === 'cloud-uploading'}
|
||||
onClick={async () => {
|
||||
setImportUIState('cloud-uploading');
|
||||
const currentAccount = store.session.get('currentAccount');
|
||||
const currentAccount = getCurrentAccountID();
|
||||
try {
|
||||
const relationships =
|
||||
await masto.v1.accounts.relationships.fetch({
|
||||
|
|
|
@ -47,7 +47,7 @@
|
|||
}
|
||||
.visibility-direct {
|
||||
--yellow-stripes: repeating-linear-gradient(
|
||||
-45deg,
|
||||
135deg,
|
||||
var(--reply-to-faded-color),
|
||||
var(--reply-to-faded-color) 10px,
|
||||
var(--reply-to-faded-color) 10px,
|
||||
|
@ -160,7 +160,7 @@
|
|||
display: block;
|
||||
position: relative;
|
||||
|
||||
&:after {
|
||||
&[data-read-more]:after {
|
||||
content: attr(data-read-more);
|
||||
line-height: 1;
|
||||
display: inline-block;
|
||||
|
@ -365,6 +365,10 @@
|
|||
background-image: var(--yellow-stripes);
|
||||
}
|
||||
|
||||
.status-pre-meta + & {
|
||||
background-image: none;
|
||||
}
|
||||
|
||||
> * {
|
||||
opacity: 0.65;
|
||||
transition: opacity 1s ease-out;
|
||||
|
@ -618,6 +622,7 @@
|
|||
~ *:not(
|
||||
.content.truncated,
|
||||
.media-container,
|
||||
.media-first-container,
|
||||
.card,
|
||||
.media-figure-multiple,
|
||||
.spoiler-media-button
|
||||
|
@ -638,6 +643,7 @@
|
|||
|
||||
~ *:not(
|
||||
.media-container,
|
||||
.media-first-container,
|
||||
.card,
|
||||
.media-figure-multiple,
|
||||
.spoiler-media-button
|
||||
|
@ -708,11 +714,12 @@
|
|||
}
|
||||
}
|
||||
|
||||
~ :is(.media-container, .media-figure-multiple) .media {
|
||||
~ :is(.media-container, .media-first-container, .media-figure-multiple)
|
||||
.media {
|
||||
background-image: radial-gradient(
|
||||
circle at 50% 50%,
|
||||
var(--average-color, var(--bg-faded-color)),
|
||||
var(--bg-color) 20em
|
||||
var(--bg-color) 25em
|
||||
);
|
||||
|
||||
> *:not(.media-play, .alt-badge) {
|
||||
|
@ -790,7 +797,9 @@
|
|||
black 1.5em
|
||||
);
|
||||
}
|
||||
.timeline-deck .status:not(.truncated .status) .content.truncated:after {
|
||||
.timeline-deck
|
||||
.status:not(.truncated .status)
|
||||
.content.truncated[data-read-more]:after {
|
||||
content: attr(data-read-more);
|
||||
line-height: 1;
|
||||
display: inline-block;
|
||||
|
@ -1314,6 +1323,258 @@ body:has(#modal-container .carousel) .status .media img:hover {
|
|||
background-blend-mode: multiply;
|
||||
}
|
||||
|
||||
.status.skeleton .media-first-container {
|
||||
min-height: 320px;
|
||||
background-color: var(--outline-color);
|
||||
}
|
||||
|
||||
@keyframes media-carousel-slide {
|
||||
0% {
|
||||
transform: translateX(calc(var(--dots-count, 1) * 2.5px));
|
||||
}
|
||||
100% {
|
||||
transform: translateX(calc(var(--dots-count, 1) * -2.5px));
|
||||
}
|
||||
}
|
||||
|
||||
.status-media-first {
|
||||
timeline-scope: --media-carousel;
|
||||
|
||||
.meta-name {
|
||||
opacity: 0.65;
|
||||
transition: opacity 0.5s ease-in-out;
|
||||
|
||||
b + i {
|
||||
opacity: 0;
|
||||
transition: opacity 0.5s ease-in-out;
|
||||
}
|
||||
}
|
||||
:is(:hover, :focus) > & .meta-name {
|
||||
opacity: 1;
|
||||
b + i {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.media-first-spoiler-content {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 100%;
|
||||
transition: opacity 0.5s ease-in-out;
|
||||
opacity: 0.5;
|
||||
}
|
||||
&:hover .media-first-spoiler-content {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.media-first-spoiler-button {
|
||||
display: inline-flex !important;
|
||||
}
|
||||
|
||||
.media-first-container {
|
||||
position: relative;
|
||||
margin-top: 8px;
|
||||
margin-inline: -16px;
|
||||
|
||||
@media (min-width: 40em) {
|
||||
margin-inline: 0;
|
||||
}
|
||||
|
||||
.media-carousel-controls {
|
||||
flex-shrink: 0;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.carousel-indexer {
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
color: var(--media-fg-color);
|
||||
background-color: var(--media-bg-color);
|
||||
padding: 2px 8px;
|
||||
border-radius: 16px;
|
||||
font-size: 0.8em;
|
||||
font-variant-numeric: tabular-nums;
|
||||
opacity: 0.6;
|
||||
transition: opacity 1s ease-in-out 0.3s;
|
||||
border: var(--hairline-width) solid var(--media-outline-color);
|
||||
}
|
||||
|
||||
.media-carousel-button {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
padding-inline: 8px;
|
||||
margin-block: 3em;
|
||||
pointer-events: auto;
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.carousel-button {
|
||||
@media (pointer: coarse) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
+ .carousel-button {
|
||||
left: auto;
|
||||
right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
.carousel-button {
|
||||
filter: opacity(0);
|
||||
}
|
||||
&:hover .carousel-button {
|
||||
filter: opacity(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.media-first-carousel {
|
||||
display: flex;
|
||||
max-height: 80vh;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
scroll-snap-type: x mandatory;
|
||||
scroll-behavior: smooth;
|
||||
user-select: none;
|
||||
scrollbar-width: none;
|
||||
/* border: var(--hairline-width) solid var(--outline-color);
|
||||
border-inline-width: 0;
|
||||
background-color: var(--bg-faded-color); */
|
||||
box-shadow: 0 0 0 var(--hairline-width) var(--outline-color);
|
||||
scroll-timeline: --media-carousel x;
|
||||
|
||||
@media (min-width: 40em) {
|
||||
/* margin-inline: 0; */
|
||||
/* border-radius: 4px; */
|
||||
/* border-inline-width: var(--hairline-width); */
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
> .media-first-item {
|
||||
scroll-snap-align: center;
|
||||
scroll-snap-stop: always;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:not(:only-child) {
|
||||
background-color: var(--bg-blur-color);
|
||||
/* box-shadow: inset 0 0 0 var(--hairline-width) var(--outline-color); */
|
||||
}
|
||||
|
||||
.media {
|
||||
/* background-color: var(--average-color, var(--bg-faded-color)); */
|
||||
width: var(--width, 100%);
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
min-height: var(--min-dimension);
|
||||
/* max-height: min(var(--height), 80vh); */
|
||||
|
||||
&:has(img:not([data-loaded='true'])) {
|
||||
min-height: 320px;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: none;
|
||||
filter: none;
|
||||
}
|
||||
|
||||
img,
|
||||
video {
|
||||
object-fit: scale-down;
|
||||
animation: none;
|
||||
|
||||
&:not([data-loaded='true']) {
|
||||
background-color: var(--bg-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
:is(:hover, :focus) > & .carousel-indexer {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.media-carousel-dots {
|
||||
pointer-events: none;
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
justify-content: center;
|
||||
margin-top: 8px;
|
||||
padding: 8px;
|
||||
|
||||
@supports (animation-timeline: scroll()) {
|
||||
animation: media-carousel-slide 1s linear both;
|
||||
animation-timeline: --media-carousel;
|
||||
}
|
||||
|
||||
.carousel-dot {
|
||||
display: inline-block;
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--text-color);
|
||||
transition: all 0.3s ease-in-out;
|
||||
opacity: 0.3;
|
||||
flex-shrink: 0;
|
||||
|
||||
&.active {
|
||||
opacity: 1;
|
||||
background-color: var(--text-color);
|
||||
transform: scale(1.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.media-first-content {
|
||||
margin-top: 8px;
|
||||
height: 1.75em;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 0.9em;
|
||||
mask-image: linear-gradient(to bottom, black 1.5em, transparent 1.75em);
|
||||
opacity: 0.5;
|
||||
transition: opacity 0.5s ease-in-out;
|
||||
|
||||
@media (min-width: 40em) {
|
||||
margin-inline: 16px;
|
||||
}
|
||||
|
||||
* {
|
||||
text-align: center;
|
||||
/* Brute force ellipsis */
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap !important;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
a {
|
||||
filter: grayscale(0.5);
|
||||
}
|
||||
}
|
||||
|
||||
:is(:hover, :focus) > & .media-first-content {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.status:not(.large) .hashtag-stuffing {
|
||||
opacity: 0.75;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
|
@ -2289,7 +2550,7 @@ a.card:is(:hover, :focus):visited {
|
|||
mask-image: linear-gradient(to bottom, #000 80px, transparent);
|
||||
}
|
||||
|
||||
&:after {
|
||||
&[data-read-more]:after {
|
||||
content: attr(data-read-more);
|
||||
line-height: 1;
|
||||
display: inline-block;
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
import { decodeBlurHash, getBlurHashAverageColor } from 'fast-blurhash';
|
||||
import { shallowEqual } from 'fast-equals';
|
||||
import prettify from 'html-prettify';
|
||||
import { Fragment } from 'preact';
|
||||
import { memo } from 'preact/compat';
|
||||
import {
|
||||
useCallback,
|
||||
|
@ -55,6 +56,8 @@ import { speak, supportsTTS } from '../utils/speech';
|
|||
import states, { getStatus, saveStatus, statusKey } from '../utils/states';
|
||||
import statusPeek from '../utils/status-peek';
|
||||
import store from '../utils/store';
|
||||
import { getCurrentAccountID } from '../utils/store-utils';
|
||||
import supports from '../utils/supports';
|
||||
import unfurlMastodonLink from '../utils/unfurl-link';
|
||||
import useTruncated from '../utils/useTruncated';
|
||||
import visibilityIconsMap from '../utils/visibility-icons-map';
|
||||
|
@ -148,6 +151,12 @@ const PostContent = memo(
|
|||
},
|
||||
);
|
||||
|
||||
const SIZE_CLASS = {
|
||||
s: 'small',
|
||||
m: 'medium',
|
||||
l: 'large',
|
||||
};
|
||||
|
||||
function Status({
|
||||
statusID,
|
||||
status,
|
||||
|
@ -169,15 +178,23 @@ function Status({
|
|||
allowContextMenu,
|
||||
showActionsBar,
|
||||
showReplyParent,
|
||||
mediaFirst,
|
||||
}) {
|
||||
if (skeleton) {
|
||||
return (
|
||||
<div class="status skeleton">
|
||||
<Avatar size="xxl" />
|
||||
<div
|
||||
class={`status skeleton ${
|
||||
mediaFirst ? 'status-media-first small' : ''
|
||||
}`}
|
||||
>
|
||||
{!mediaFirst && <Avatar size="xxl" />}
|
||||
<div class="container">
|
||||
<div class="meta">███ ████████</div>
|
||||
<div class="meta">
|
||||
{(size === 's' || mediaFirst) && <Avatar size="m" />} ███ ████████
|
||||
</div>
|
||||
<div class="content-container">
|
||||
<div class="content">
|
||||
{mediaFirst && <div class="media-first-container" />}
|
||||
<div class={`content ${mediaFirst ? 'media-first-content' : ''}`}>
|
||||
<p>████ ████████</p>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -247,8 +264,12 @@ function Status({
|
|||
emojiReactions,
|
||||
} = status;
|
||||
|
||||
// if (!mediaAttachments?.length) mediaFirst = false;
|
||||
const hasMediaAttachments = !!mediaAttachments?.length;
|
||||
if (mediaFirst && hasMediaAttachments) size = 's';
|
||||
|
||||
const currentAccount = useMemo(() => {
|
||||
return store.session.get('currentAccount');
|
||||
return getCurrentAccountID();
|
||||
}, []);
|
||||
const isSelf = useMemo(() => {
|
||||
return currentAccount && currentAccount === accountId;
|
||||
|
@ -354,6 +375,7 @@ function Status({
|
|||
size={size}
|
||||
contentTextWeight={contentTextWeight}
|
||||
readOnly={readOnly}
|
||||
mediaFirst={mediaFirst}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -378,14 +400,15 @@ function Status({
|
|||
contentTextWeight={contentTextWeight}
|
||||
readOnly={readOnly}
|
||||
enableCommentHint
|
||||
mediaFirst={mediaFirst}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Check followedTags
|
||||
if (showFollowedTags && !!snapStates.statusFollowedTags[sKey]?.length) {
|
||||
return (
|
||||
const FollowedTagsParent = useCallback(
|
||||
({ children }) => (
|
||||
<div
|
||||
data-state-post-id={sKey}
|
||||
class="status-followed-tags"
|
||||
|
@ -403,18 +426,15 @@ function Status({
|
|||
</Link>
|
||||
))}
|
||||
</div>
|
||||
<Status
|
||||
status={statusID ? null : status}
|
||||
statusID={statusID ? status.id : null}
|
||||
instance={instance}
|
||||
size={size}
|
||||
contentTextWeight={contentTextWeight}
|
||||
readOnly={readOnly}
|
||||
enableCommentHint
|
||||
/>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
),
|
||||
[sKey, instance, snapStates.statusFollowedTags[sKey]],
|
||||
);
|
||||
const StatusParent =
|
||||
showFollowedTags && !!snapStates.statusFollowedTags[sKey]?.length
|
||||
? FollowedTagsParent
|
||||
: Fragment;
|
||||
|
||||
const isSizeLarge = size === 'l';
|
||||
|
||||
|
@ -628,6 +648,7 @@ function Status({
|
|||
};
|
||||
|
||||
const bookmarkStatus = async () => {
|
||||
if (!supports('@mastodon/post-bookmark')) return;
|
||||
if (!sameInstance || !authenticated) {
|
||||
alert(unauthInteractionErrorMessage);
|
||||
return false;
|
||||
|
@ -815,13 +836,15 @@ function Status({
|
|||
: 'Like'}
|
||||
</span>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={bookmarkStatusNotify}
|
||||
className={`menu-bookmark ${bookmarked ? 'checked' : ''}`}
|
||||
>
|
||||
<Icon icon="bookmark" />
|
||||
<span>{bookmarked ? 'Unbookmark' : 'Bookmark'}</span>
|
||||
</MenuItem>
|
||||
{supports('@mastodon/post-bookmark') && (
|
||||
<MenuItem
|
||||
onClick={bookmarkStatusNotify}
|
||||
className={`menu-bookmark ${bookmarked ? 'checked' : ''}`}
|
||||
>
|
||||
<Icon icon="bookmark" />
|
||||
<span>{bookmarked ? 'Unbookmark' : 'Bookmark'}</span>
|
||||
</MenuItem>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
@ -848,56 +871,62 @@ function Status({
|
|||
</MenuItem>
|
||||
</>
|
||||
)}
|
||||
{(enableTranslate || !language || differentLanguage) && <MenuDivider />}
|
||||
{enableTranslate ? (
|
||||
<div class={supportsTTS ? 'menu-horizontal' : ''}>
|
||||
<MenuItem
|
||||
disabled={forceTranslate}
|
||||
onClick={() => {
|
||||
setForceTranslate(true);
|
||||
}}
|
||||
>
|
||||
<Icon icon="translate" />
|
||||
<span>Translate</span>
|
||||
</MenuItem>
|
||||
{supportsTTS && (
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
const postText = getPostText(status);
|
||||
if (postText) {
|
||||
speak(postText, language);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Icon icon="speak" />
|
||||
<span>Speak</span>
|
||||
</MenuItem>
|
||||
{!mediaFirst && (
|
||||
<>
|
||||
{(enableTranslate || !language || differentLanguage) && (
|
||||
<MenuDivider />
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
(!language || differentLanguage) && (
|
||||
<div class={supportsTTS ? 'menu-horizontal' : ''}>
|
||||
<MenuLink
|
||||
to={`${instance ? `/${instance}` : ''}/s/${id}?translate=1`}
|
||||
>
|
||||
<Icon icon="translate" />
|
||||
<span>Translate</span>
|
||||
</MenuLink>
|
||||
{supportsTTS && (
|
||||
{enableTranslate ? (
|
||||
<div class={supportsTTS ? 'menu-horizontal' : ''}>
|
||||
<MenuItem
|
||||
disabled={forceTranslate}
|
||||
onClick={() => {
|
||||
const postText = getPostText(status);
|
||||
if (postText) {
|
||||
speak(postText, language);
|
||||
}
|
||||
setForceTranslate(true);
|
||||
}}
|
||||
>
|
||||
<Icon icon="speak" />
|
||||
<span>Speak</span>
|
||||
<Icon icon="translate" />
|
||||
<span>Translate</span>
|
||||
</MenuItem>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
{supportsTTS && (
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
const postText = getPostText(status);
|
||||
if (postText) {
|
||||
speak(postText, language);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Icon icon="speak" />
|
||||
<span>Speak</span>
|
||||
</MenuItem>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
(!language || differentLanguage) && (
|
||||
<div class={supportsTTS ? 'menu-horizontal' : ''}>
|
||||
<MenuLink
|
||||
to={`${instance ? `/${instance}` : ''}/s/${id}?translate=1`}
|
||||
>
|
||||
<Icon icon="translate" />
|
||||
<span>Translate</span>
|
||||
</MenuLink>
|
||||
{supportsTTS && (
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
const postText = getPostText(status);
|
||||
if (postText) {
|
||||
speak(postText, language);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Icon icon="speak" />
|
||||
<span>Speak</span>
|
||||
</MenuItem>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{((!isSizeLarge && sameInstance) ||
|
||||
enableTranslate ||
|
||||
|
@ -1059,16 +1088,18 @@ function Status({
|
|||
)}
|
||||
{isSelf && (
|
||||
<div class="menu-horizontal">
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
states.showCompose = {
|
||||
editStatus: status,
|
||||
};
|
||||
}}
|
||||
>
|
||||
<Icon icon="pencil" />
|
||||
<span>Edit</span>
|
||||
</MenuItem>
|
||||
{supports('@mastodon/post-edit') && (
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
states.showCompose = {
|
||||
editStatus: status,
|
||||
};
|
||||
}}
|
||||
>
|
||||
<Icon icon="pencil" />
|
||||
<span>Edit</span>
|
||||
</MenuItem>
|
||||
)}
|
||||
{isSizeLarge && (
|
||||
<MenuConfirm
|
||||
subMenu
|
||||
|
@ -1349,7 +1380,7 @@ function Status({
|
|||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<StatusParent>
|
||||
{showReplyParent && !!(inReplyToId && inReplyToAccountId) && (
|
||||
<StatusCompact sKey={sKey} />
|
||||
)}
|
||||
|
@ -1377,14 +1408,10 @@ function Status({
|
|||
? 'status-reply-to'
|
||||
: ''
|
||||
} visibility-${visibility} ${_pinned ? 'status-pinned' : ''} ${
|
||||
{
|
||||
s: 'small',
|
||||
m: 'medium',
|
||||
l: 'large',
|
||||
}[size]
|
||||
SIZE_CLASS[size]
|
||||
} ${_deleted ? 'status-deleted' : ''} ${quoted ? 'status-card' : ''} ${
|
||||
isContextMenuOpen ? 'status-menu-open' : ''
|
||||
}`}
|
||||
} ${mediaFirst && hasMediaAttachments ? 'status-media-first' : ''}`}
|
||||
onMouseEnter={debugHover}
|
||||
onContextMenu={(e) => {
|
||||
if (!showContextMenu) return;
|
||||
|
@ -1712,188 +1739,253 @@ function Status({
|
|||
}
|
||||
}
|
||||
>
|
||||
{!!spoilerText && (
|
||||
{mediaFirst && hasMediaAttachments ? (
|
||||
<>
|
||||
<div
|
||||
class="content spoiler-content"
|
||||
lang={language}
|
||||
dir="auto"
|
||||
ref={spoilerContentRef}
|
||||
data-read-more={readMoreText}
|
||||
>
|
||||
<p>
|
||||
<EmojiText text={spoilerText} emojis={emojis} />
|
||||
</p>
|
||||
</div>
|
||||
{readingExpandSpoilers || previewMode ? (
|
||||
<div class="spoiler-divider">
|
||||
<Icon icon="eye-open" /> Content warning
|
||||
{(!!spoilerText || !!sensitive) && !readingExpandSpoilers && (
|
||||
<>
|
||||
{!!spoilerText && (
|
||||
<span
|
||||
class="spoiler-content media-first-spoiler-content"
|
||||
lang={language}
|
||||
dir="auto"
|
||||
ref={spoilerContentRef}
|
||||
data-read-more={readMoreText}
|
||||
>
|
||||
<EmojiText text={spoilerText} emojis={emojis} />{' '}
|
||||
</span>
|
||||
)}
|
||||
<button
|
||||
class={`light spoiler-button media-first-spoiler-button ${
|
||||
showSpoiler ? 'spoiling' : ''
|
||||
}`}
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (showSpoiler) {
|
||||
delete states.spoilers[id];
|
||||
if (!readingExpandSpoilers) {
|
||||
delete states.spoilersMedia[id];
|
||||
}
|
||||
} else {
|
||||
states.spoilers[id] = true;
|
||||
if (!readingExpandSpoilers) {
|
||||
states.spoilersMedia[id] = true;
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Icon icon={showSpoiler ? 'eye-open' : 'eye-close'} />{' '}
|
||||
{showSpoiler ? 'Show less' : 'Show content'}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
<MediaFirstContainer
|
||||
mediaAttachments={mediaAttachments}
|
||||
language={language}
|
||||
postID={id}
|
||||
instance={instance}
|
||||
/>
|
||||
{!!content && (
|
||||
<div class="media-first-content content" ref={contentRef}>
|
||||
<PostContent
|
||||
post={status}
|
||||
instance={instance}
|
||||
previewMode={previewMode}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
class={`light spoiler-button ${
|
||||
showSpoiler ? 'spoiling' : ''
|
||||
}`}
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (showSpoiler) {
|
||||
delete states.spoilers[id];
|
||||
if (!readingExpandSpoilers) {
|
||||
delete states.spoilersMedia[id];
|
||||
}
|
||||
} else {
|
||||
states.spoilers[id] = true;
|
||||
if (!readingExpandSpoilers) {
|
||||
states.spoilersMedia[id] = true;
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Icon icon={showSpoiler ? 'eye-open' : 'eye-close'} />{' '}
|
||||
{showSpoiler ? 'Show less' : 'Show content'}
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{!!content && (
|
||||
<div
|
||||
class="content"
|
||||
ref={contentRef}
|
||||
data-read-more={readMoreText}
|
||||
>
|
||||
<PostContent
|
||||
post={status}
|
||||
instance={instance}
|
||||
previewMode={previewMode}
|
||||
/>
|
||||
<QuoteStatuses id={id} instance={instance} level={quoted} />
|
||||
</div>
|
||||
)}
|
||||
{!!poll && (
|
||||
<Poll
|
||||
lang={language}
|
||||
poll={poll}
|
||||
readOnly={readOnly || !sameInstance || !authenticated}
|
||||
onUpdate={(newPoll) => {
|
||||
states.statuses[sKey].poll = newPoll;
|
||||
}}
|
||||
refresh={() => {
|
||||
return masto.v1.polls
|
||||
.$select(poll.id)
|
||||
.fetch()
|
||||
.then((pollResponse) => {
|
||||
states.statuses[sKey].poll = pollResponse;
|
||||
})
|
||||
.catch((e) => {}); // Silently fail
|
||||
}}
|
||||
votePoll={(choices) => {
|
||||
return masto.v1.polls
|
||||
.$select(poll.id)
|
||||
.votes.create({
|
||||
choices,
|
||||
})
|
||||
.then((pollResponse) => {
|
||||
states.statuses[sKey].poll = pollResponse;
|
||||
})
|
||||
.catch((e) => {}); // Silently fail
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{(((enableTranslate || inlineTranslate) &&
|
||||
!!content.trim() &&
|
||||
!!getHTMLText(emojifyText(content, emojis)) &&
|
||||
differentLanguage) ||
|
||||
forceTranslate) && (
|
||||
<TranslationBlock
|
||||
forceTranslate={forceTranslate || inlineTranslate}
|
||||
mini={!isSizeLarge && !withinContext}
|
||||
sourceLanguage={language}
|
||||
text={getPostText(status)}
|
||||
/>
|
||||
)}
|
||||
{!previewMode &&
|
||||
sensitive &&
|
||||
!!mediaAttachments.length &&
|
||||
readingExpandMedia !== 'show_all' && (
|
||||
<button
|
||||
class={`plain spoiler-media-button ${
|
||||
showSpoilerMedia ? 'spoiling' : ''
|
||||
}`}
|
||||
type="button"
|
||||
hidden={!readingExpandSpoilers && !!spoilerText}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (showSpoilerMedia) {
|
||||
delete states.spoilersMedia[id];
|
||||
} else {
|
||||
states.spoilersMedia[id] = true;
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Icon icon={showSpoilerMedia ? 'eye-open' : 'eye-close'} />{' '}
|
||||
{showSpoilerMedia ? 'Show less' : 'Show media'}
|
||||
</button>
|
||||
)}
|
||||
{!!mediaAttachments.length && (
|
||||
<MultipleMediaFigure
|
||||
lang={language}
|
||||
enabled={showMultipleMediaCaptions}
|
||||
captionChildren={captionChildren}
|
||||
>
|
||||
<div
|
||||
ref={mediaContainerRef}
|
||||
class={`media-container media-eq${mediaAttachments.length} ${
|
||||
mediaAttachments.length > 2 ? 'media-gt2' : ''
|
||||
} ${mediaAttachments.length > 4 ? 'media-gt4' : ''}`}
|
||||
>
|
||||
{displayedMediaAttachments.map((media, i) => (
|
||||
<Media
|
||||
key={media.id}
|
||||
media={media}
|
||||
autoAnimate={isSizeLarge}
|
||||
showCaption={mediaAttachments.length === 1}
|
||||
allowLongerCaption={
|
||||
!content && mediaAttachments.length === 1
|
||||
}
|
||||
) : (
|
||||
<>
|
||||
{!!spoilerText && (
|
||||
<>
|
||||
<div
|
||||
class="content spoiler-content"
|
||||
lang={language}
|
||||
altIndex={
|
||||
showMultipleMediaCaptions &&
|
||||
!!media.description &&
|
||||
i + 1
|
||||
}
|
||||
to={`/${instance}/s/${id}?${
|
||||
withinContext ? 'media' : 'media-only'
|
||||
}=${i + 1}`}
|
||||
onClick={
|
||||
onMediaClick
|
||||
? (e) => {
|
||||
onMediaClick(e, i, media, status);
|
||||
dir="auto"
|
||||
ref={spoilerContentRef}
|
||||
data-read-more={readMoreText}
|
||||
>
|
||||
<p>
|
||||
<EmojiText text={spoilerText} emojis={emojis} />
|
||||
</p>
|
||||
</div>
|
||||
{readingExpandSpoilers || previewMode ? (
|
||||
<div class="spoiler-divider">
|
||||
<Icon icon="eye-open" /> Content warning
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
class={`light spoiler-button ${
|
||||
showSpoiler ? 'spoiling' : ''
|
||||
}`}
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (showSpoiler) {
|
||||
delete states.spoilers[id];
|
||||
if (!readingExpandSpoilers) {
|
||||
delete states.spoilersMedia[id];
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
} else {
|
||||
states.spoilers[id] = true;
|
||||
if (!readingExpandSpoilers) {
|
||||
states.spoilersMedia[id] = true;
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Icon icon={showSpoiler ? 'eye-open' : 'eye-close'} />{' '}
|
||||
{showSpoiler ? 'Show less' : 'Show content'}
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{!!content && (
|
||||
<div
|
||||
class="content"
|
||||
ref={contentRef}
|
||||
data-read-more={readMoreText}
|
||||
>
|
||||
<PostContent
|
||||
post={status}
|
||||
instance={instance}
|
||||
previewMode={previewMode}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</MultipleMediaFigure>
|
||||
<QuoteStatuses id={id} instance={instance} level={quoted} />
|
||||
</div>
|
||||
)}
|
||||
{!!poll && (
|
||||
<Poll
|
||||
lang={language}
|
||||
poll={poll}
|
||||
readOnly={readOnly || !sameInstance || !authenticated}
|
||||
onUpdate={(newPoll) => {
|
||||
states.statuses[sKey].poll = newPoll;
|
||||
}}
|
||||
refresh={() => {
|
||||
return masto.v1.polls
|
||||
.$select(poll.id)
|
||||
.fetch()
|
||||
.then((pollResponse) => {
|
||||
states.statuses[sKey].poll = pollResponse;
|
||||
})
|
||||
.catch((e) => {}); // Silently fail
|
||||
}}
|
||||
votePoll={(choices) => {
|
||||
return masto.v1.polls
|
||||
.$select(poll.id)
|
||||
.votes.create({
|
||||
choices,
|
||||
})
|
||||
.then((pollResponse) => {
|
||||
states.statuses[sKey].poll = pollResponse;
|
||||
})
|
||||
.catch((e) => {}); // Silently fail
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{(((enableTranslate || inlineTranslate) &&
|
||||
!!content.trim() &&
|
||||
!!getHTMLText(emojifyText(content, emojis)) &&
|
||||
differentLanguage) ||
|
||||
forceTranslate) && (
|
||||
<TranslationBlock
|
||||
forceTranslate={forceTranslate || inlineTranslate}
|
||||
mini={!isSizeLarge && !withinContext}
|
||||
sourceLanguage={language}
|
||||
text={getPostText(status)}
|
||||
/>
|
||||
)}
|
||||
{!previewMode &&
|
||||
sensitive &&
|
||||
!!mediaAttachments.length &&
|
||||
readingExpandMedia !== 'show_all' && (
|
||||
<button
|
||||
class={`plain spoiler-media-button ${
|
||||
showSpoilerMedia ? 'spoiling' : ''
|
||||
}`}
|
||||
type="button"
|
||||
hidden={!readingExpandSpoilers && !!spoilerText}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (showSpoilerMedia) {
|
||||
delete states.spoilersMedia[id];
|
||||
} else {
|
||||
states.spoilersMedia[id] = true;
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
icon={showSpoilerMedia ? 'eye-open' : 'eye-close'}
|
||||
/>{' '}
|
||||
{showSpoilerMedia ? 'Show less' : 'Show media'}
|
||||
</button>
|
||||
)}
|
||||
{!!mediaAttachments.length && (
|
||||
<MultipleMediaFigure
|
||||
lang={language}
|
||||
enabled={showMultipleMediaCaptions}
|
||||
captionChildren={captionChildren}
|
||||
>
|
||||
<div
|
||||
ref={mediaContainerRef}
|
||||
class={`media-container media-eq${
|
||||
mediaAttachments.length
|
||||
} ${mediaAttachments.length > 2 ? 'media-gt2' : ''} ${
|
||||
mediaAttachments.length > 4 ? 'media-gt4' : ''
|
||||
}`}
|
||||
>
|
||||
{displayedMediaAttachments.map((media, i) => (
|
||||
<Media
|
||||
key={media.id}
|
||||
media={media}
|
||||
autoAnimate={isSizeLarge}
|
||||
showCaption={mediaAttachments.length === 1}
|
||||
allowLongerCaption={
|
||||
!content && mediaAttachments.length === 1
|
||||
}
|
||||
lang={language}
|
||||
altIndex={
|
||||
showMultipleMediaCaptions &&
|
||||
!!media.description &&
|
||||
i + 1
|
||||
}
|
||||
to={`/${instance}/s/${id}?${
|
||||
withinContext ? 'media' : 'media-only'
|
||||
}=${i + 1}`}
|
||||
onClick={
|
||||
onMediaClick
|
||||
? (e) => {
|
||||
onMediaClick(e, i, media, status);
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</MultipleMediaFigure>
|
||||
)}
|
||||
{!!card &&
|
||||
/^https/i.test(card?.url) &&
|
||||
!sensitive &&
|
||||
!spoilerText &&
|
||||
!poll &&
|
||||
!mediaAttachments.length &&
|
||||
!snapStates.statusQuotes[sKey] && (
|
||||
<Card
|
||||
card={card}
|
||||
selfReferential={
|
||||
card?.url === status.url || card?.url === status.uri
|
||||
}
|
||||
instance={currentInstance}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{!!card &&
|
||||
/^https/i.test(card?.url) &&
|
||||
!sensitive &&
|
||||
!spoilerText &&
|
||||
!poll &&
|
||||
!mediaAttachments.length &&
|
||||
!snapStates.statusQuotes[sKey] && (
|
||||
<Card
|
||||
card={card}
|
||||
selfReferential={
|
||||
card?.url === status.url || card?.url === status.uri
|
||||
}
|
||||
instance={currentInstance}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{!isSizeLarge && showCommentCount && (
|
||||
<div class="content-comment-hint insignificant">
|
||||
|
@ -2077,16 +2169,18 @@ function Status({
|
|||
onClick={favouriteStatus}
|
||||
/>
|
||||
</div>
|
||||
<div class="action">
|
||||
<StatusButton
|
||||
checked={bookmarked}
|
||||
title={['Bookmark', 'Unbookmark']}
|
||||
alt={['Bookmark', 'Bookmarked']}
|
||||
class="bookmark-button"
|
||||
icon="bookmark"
|
||||
onClick={bookmarkStatus}
|
||||
/>
|
||||
</div>
|
||||
{supports('@mastodon/post-bookmark') && (
|
||||
<div class="action">
|
||||
<StatusButton
|
||||
checked={bookmarked}
|
||||
title={['Bookmark', 'Unbookmark']}
|
||||
alt={['Bookmark', 'Bookmarked']}
|
||||
class="bookmark-button"
|
||||
icon="bookmark"
|
||||
onClick={bookmarkStatus}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<Menu2
|
||||
portal={{
|
||||
target:
|
||||
|
@ -2154,7 +2248,7 @@ function Status({
|
|||
</Modal>
|
||||
)}
|
||||
</article>
|
||||
</>
|
||||
</StatusParent>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -2171,6 +2265,108 @@ function MultipleMediaFigure(props) {
|
|||
);
|
||||
}
|
||||
|
||||
function MediaFirstContainer(props) {
|
||||
const { mediaAttachments, language, postID, instance } = props;
|
||||
const moreThanOne = mediaAttachments.length > 1;
|
||||
|
||||
const carouselRef = useRef();
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
let handleScroll = () => {
|
||||
const { clientWidth, scrollLeft } = carouselRef.current;
|
||||
const index = Math.round(scrollLeft / clientWidth);
|
||||
setCurrentIndex(index);
|
||||
};
|
||||
if (carouselRef.current) {
|
||||
carouselRef.current.addEventListener('scroll', handleScroll, {
|
||||
passive: true,
|
||||
});
|
||||
}
|
||||
return () => {
|
||||
if (carouselRef.current) {
|
||||
carouselRef.current.removeEventListener('scroll', handleScroll);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div class="media-first-container">
|
||||
<div class="media-first-carousel" ref={carouselRef}>
|
||||
{mediaAttachments.map((media, i) => (
|
||||
<div class="media-first-item" key={media.id}>
|
||||
<Media
|
||||
media={media}
|
||||
lang={language}
|
||||
to={`/${instance}/s/${postID}?media=${i + 1}`}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{moreThanOne && (
|
||||
<div class="media-carousel-controls">
|
||||
<div class="carousel-indexer">
|
||||
{currentIndex + 1}/{mediaAttachments.length}
|
||||
</div>
|
||||
<label class="media-carousel-button">
|
||||
<button
|
||||
type="button"
|
||||
class="carousel-button"
|
||||
hidden={currentIndex === 0}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
carouselRef.current.focus();
|
||||
carouselRef.current.scrollTo({
|
||||
left: carouselRef.current.clientWidth * (currentIndex - 1),
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Icon icon="arrow-left" />
|
||||
</button>
|
||||
</label>
|
||||
<label class="media-carousel-button">
|
||||
<button
|
||||
type="button"
|
||||
class="carousel-button"
|
||||
hidden={currentIndex === mediaAttachments.length - 1}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
carouselRef.current.focus();
|
||||
carouselRef.current.scrollTo({
|
||||
left: carouselRef.current.clientWidth * (currentIndex + 1),
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Icon icon="arrow-right" />
|
||||
</button>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{moreThanOne && (
|
||||
<div
|
||||
class="media-carousel-dots"
|
||||
style={{
|
||||
'--dots-count': mediaAttachments.length,
|
||||
}}
|
||||
>
|
||||
{mediaAttachments.map((media, i) => (
|
||||
<span
|
||||
key={media.id}
|
||||
class={`carousel-dot ${i === currentIndex ? 'active' : ''}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Card({ card, selfReferential, instance }) {
|
||||
const snapStates = useSnapshot(states);
|
||||
const {
|
||||
|
|
|
@ -1,5 +1,11 @@
|
|||
import { memo } from 'preact/compat';
|
||||
import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'preact/hooks';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { InView } from 'react-intersection-observer';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
|
@ -9,6 +15,7 @@ import FilterContext from '../utils/filter-context';
|
|||
import { filteredItems, isFiltered } from '../utils/filters';
|
||||
import states, { statusKey } from '../utils/states';
|
||||
import statusPeek from '../utils/status-peek';
|
||||
import { isMediaFirstInstance } from '../utils/store-utils';
|
||||
import { groupBoosts, groupContext } from '../utils/timeline-utils';
|
||||
import useInterval from '../utils/useInterval';
|
||||
import usePageVisibility from '../utils/usePageVisibility';
|
||||
|
@ -59,6 +66,8 @@ function Timeline({
|
|||
|
||||
console.debug('RENDER Timeline', id, refresh);
|
||||
|
||||
const mediaFirst = useMemo(() => isMediaFirstInstance(), []);
|
||||
|
||||
const allowGrouping = view !== 'media';
|
||||
const loadItems = useDebouncedCallback(
|
||||
(firstLoad) => {
|
||||
|
@ -355,7 +364,9 @@ function Timeline({
|
|||
<FilterContext.Provider value={filterContext}>
|
||||
<div
|
||||
id={`${id}-page`}
|
||||
class="deck-container"
|
||||
class={`deck-container ${
|
||||
mediaFirst ? 'deck-container-media-first' : ''
|
||||
}`}
|
||||
ref={(node) => {
|
||||
scrollableRef.current = node;
|
||||
jRef.current = node;
|
||||
|
@ -432,6 +443,7 @@ function Timeline({
|
|||
view={view}
|
||||
showFollowedTags={showFollowedTags}
|
||||
showReplyParent={showReplyParent}
|
||||
mediaFirst={mediaFirst}
|
||||
/>
|
||||
))}
|
||||
{showMore &&
|
||||
|
@ -443,14 +455,14 @@ function Timeline({
|
|||
height: '20vh',
|
||||
}}
|
||||
>
|
||||
<Status skeleton />
|
||||
<Status skeleton mediaFirst={mediaFirst} />
|
||||
</li>
|
||||
<li
|
||||
style={{
|
||||
height: '25vh',
|
||||
}}
|
||||
>
|
||||
<Status skeleton />
|
||||
<Status skeleton mediaFirst={mediaFirst} />
|
||||
</li>
|
||||
</>
|
||||
))}
|
||||
|
@ -490,7 +502,7 @@ function Timeline({
|
|||
/>
|
||||
) : (
|
||||
<li key={i}>
|
||||
<Status skeleton />
|
||||
<Status skeleton mediaFirst={mediaFirst} />
|
||||
</li>
|
||||
),
|
||||
)}
|
||||
|
@ -525,6 +537,7 @@ const TimelineItem = memo(
|
|||
view,
|
||||
showFollowedTags,
|
||||
showReplyParent,
|
||||
mediaFirst,
|
||||
}) => {
|
||||
console.debug('RENDER TimelineItem', status.id);
|
||||
const { id: statusID, reblog, items, type, _pinned } = status;
|
||||
|
@ -533,6 +546,7 @@ const TimelineItem = memo(
|
|||
const url = instance
|
||||
? `/${instance}/s/${actualStatusID}`
|
||||
: `/s/${actualStatusID}`;
|
||||
|
||||
if (items) {
|
||||
const fItems = filteredItems(items, filterContext);
|
||||
let title = '';
|
||||
|
@ -585,6 +599,7 @@ const TimelineItem = memo(
|
|||
contentTextWeight
|
||||
enableCommentHint
|
||||
// allowFilters={allowFilters}
|
||||
mediaFirst={mediaFirst}
|
||||
/>
|
||||
) : (
|
||||
<Status
|
||||
|
@ -594,6 +609,7 @@ const TimelineItem = memo(
|
|||
contentTextWeight
|
||||
enableCommentHint
|
||||
// allowFilters={allowFilters}
|
||||
mediaFirst={mediaFirst}
|
||||
/>
|
||||
)}
|
||||
</Link>
|
||||
|
@ -689,6 +705,7 @@ const TimelineItem = memo(
|
|||
showFollowedTags={showFollowedTags}
|
||||
showReplyParent={showReplyParent}
|
||||
// allowFilters={allowFilters}
|
||||
mediaFirst={mediaFirst}
|
||||
/>
|
||||
) : (
|
||||
<Status
|
||||
|
@ -698,6 +715,7 @@ const TimelineItem = memo(
|
|||
showFollowedTags={showFollowedTags}
|
||||
showReplyParent={showReplyParent}
|
||||
// allowFilters={allowFilters}
|
||||
mediaFirst={mediaFirst}
|
||||
/>
|
||||
)}
|
||||
</Link>
|
||||
|
|
|
@ -21,6 +21,7 @@ import pmem from '../utils/pmem';
|
|||
import showToast from '../utils/show-toast';
|
||||
import states from '../utils/states';
|
||||
import { saveStatus } from '../utils/states';
|
||||
import { isMediaFirstInstance } from '../utils/store-utils';
|
||||
import useTitle from '../utils/useTitle';
|
||||
|
||||
const LIMIT = 20;
|
||||
|
@ -68,6 +69,8 @@ function AccountStatuses() {
|
|||
searchOffsetRef.current = 0;
|
||||
}, allSearchParams);
|
||||
|
||||
const mediaFirst = useMemo(() => isMediaFirstInstance(), []);
|
||||
|
||||
const sameCurrentInstance = useMemo(
|
||||
() => instance === currentInstance,
|
||||
[instance, currentInstance],
|
||||
|
@ -151,7 +154,7 @@ function AccountStatuses() {
|
|||
}
|
||||
}
|
||||
|
||||
const results = [];
|
||||
let results = [];
|
||||
if (firstLoad) {
|
||||
const { value } = await masto.v1.accounts
|
||||
.$select(id)
|
||||
|
@ -186,12 +189,32 @@ function AccountStatuses() {
|
|||
limit: LIMIT,
|
||||
exclude_replies: excludeReplies,
|
||||
exclude_reblogs: excludeBoosts,
|
||||
only_media: media,
|
||||
only_media: media || undefined,
|
||||
tagged,
|
||||
});
|
||||
}
|
||||
const { value, done } = await accountStatusesIterator.current.next();
|
||||
if (value?.length) {
|
||||
// Check if value is same as pinned post (results)
|
||||
// If the index for every post is the same, means API might not support pinned posts
|
||||
if (results.length) {
|
||||
let pinnedStatusesIds = [];
|
||||
if (results[0]?.type === 'pinned') {
|
||||
pinnedStatusesIds = results[0].id;
|
||||
} else {
|
||||
pinnedStatusesIds = results
|
||||
.filter((status) => status._pinned)
|
||||
.map((status) => status.id);
|
||||
}
|
||||
const containsAllPinned = pinnedStatusesIds.every((postId) =>
|
||||
value.some((status) => status.id === postId),
|
||||
);
|
||||
if (containsAllPinned) {
|
||||
// Remove pinned posts
|
||||
results = [];
|
||||
}
|
||||
}
|
||||
|
||||
results.push(...value);
|
||||
|
||||
value.forEach((item) => {
|
||||
|
@ -250,17 +273,21 @@ function AccountStatuses() {
|
|||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
try {
|
||||
const featuredTags = await masto.v1.accounts
|
||||
.$select(id)
|
||||
.featuredTags.list();
|
||||
console.log({ featuredTags });
|
||||
setFeaturedTags(featuredTags);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
// No need, because the whole filter bar is hidden
|
||||
// TODO: Revisit this
|
||||
if (!mediaFirst) {
|
||||
try {
|
||||
const featuredTags = await masto.v1.accounts
|
||||
.$select(id)
|
||||
.featuredTags.list();
|
||||
console.log({ featuredTags });
|
||||
setFeaturedTags(featuredTags);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
})();
|
||||
}, [id]);
|
||||
}, [id, mediaFirst]);
|
||||
|
||||
const { displayName, acct, emojis } = account || {};
|
||||
|
||||
|
@ -279,95 +306,126 @@ function AccountStatuses() {
|
|||
authenticated={authenticated}
|
||||
standalone
|
||||
/>
|
||||
<div
|
||||
class="filter-bar"
|
||||
ref={filterBarRef}
|
||||
style={{
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
{filtered ? (
|
||||
{!mediaFirst && (
|
||||
<div
|
||||
class="filter-bar"
|
||||
ref={filterBarRef}
|
||||
style={{
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
{filtered ? (
|
||||
<Link
|
||||
to={`/${instance}/a/${id}`}
|
||||
class="insignificant filter-clear"
|
||||
title="Clear filters"
|
||||
key="clear-filters"
|
||||
>
|
||||
<Icon icon="x" size="l" />
|
||||
</Link>
|
||||
) : (
|
||||
<Icon icon="filter" class="insignificant" size="l" />
|
||||
)}
|
||||
<Link
|
||||
to={`/${instance}/a/${id}`}
|
||||
class="insignificant filter-clear"
|
||||
title="Clear filters"
|
||||
key="clear-filters"
|
||||
>
|
||||
<Icon icon="x" size="l" />
|
||||
</Link>
|
||||
) : (
|
||||
<Icon icon="filter" class="insignificant" size="l" />
|
||||
)}
|
||||
<Link
|
||||
to={`/${instance}/a/${id}${excludeReplies ? '?replies=1' : ''}`}
|
||||
onClick={() => {
|
||||
if (excludeReplies) {
|
||||
showToast('Showing post with replies');
|
||||
}
|
||||
}}
|
||||
class={excludeReplies ? '' : 'is-active'}
|
||||
>
|
||||
+ Replies
|
||||
</Link>
|
||||
<Link
|
||||
to={`/${instance}/a/${id}${excludeBoosts ? '' : '?boosts=0'}`}
|
||||
onClick={() => {
|
||||
if (!excludeBoosts) {
|
||||
showToast('Showing posts without boosts');
|
||||
}
|
||||
}}
|
||||
class={!excludeBoosts ? '' : 'is-active'}
|
||||
>
|
||||
- Boosts
|
||||
</Link>
|
||||
<Link
|
||||
to={`/${instance}/a/${id}${media ? '' : '?media=1'}`}
|
||||
onClick={() => {
|
||||
if (!media) {
|
||||
showToast('Showing posts with media');
|
||||
}
|
||||
}}
|
||||
class={media ? 'is-active' : ''}
|
||||
>
|
||||
Media
|
||||
</Link>
|
||||
{featuredTags.map((tag) => (
|
||||
<Link
|
||||
key={tag.id}
|
||||
to={`/${instance}/a/${id}${
|
||||
tagged === tag.name
|
||||
? ''
|
||||
: `?tagged=${encodeURIComponent(tag.name)}`
|
||||
}`}
|
||||
to={`/${instance}/a/${id}${excludeReplies ? '?replies=1' : ''}`}
|
||||
onClick={() => {
|
||||
if (tagged !== tag.name) {
|
||||
showToast(`Showing posts tagged with #${tag.name}`);
|
||||
if (excludeReplies) {
|
||||
showToast('Showing post with replies');
|
||||
}
|
||||
}}
|
||||
class={tagged === tag.name ? 'is-active' : ''}
|
||||
class={excludeReplies ? '' : 'is-active'}
|
||||
>
|
||||
<span>
|
||||
<span class="more-insignificant">#</span>
|
||||
{tag.name}
|
||||
</span>
|
||||
{
|
||||
// The count differs based on instance 😅
|
||||
}
|
||||
{/* <span class="filter-count">{tag.statusesCount}</span> */}
|
||||
+ Replies
|
||||
</Link>
|
||||
))}
|
||||
{searchEnabled &&
|
||||
(supportsInputMonth ? (
|
||||
<label class={`filter-field ${month ? 'is-active' : ''}`}>
|
||||
<Icon icon="month" size="l" />
|
||||
<input
|
||||
type="month"
|
||||
<Link
|
||||
to={`/${instance}/a/${id}${excludeBoosts ? '' : '?boosts=0'}`}
|
||||
onClick={() => {
|
||||
if (!excludeBoosts) {
|
||||
showToast('Showing posts without boosts');
|
||||
}
|
||||
}}
|
||||
class={!excludeBoosts ? '' : 'is-active'}
|
||||
>
|
||||
- Boosts
|
||||
</Link>
|
||||
<Link
|
||||
to={`/${instance}/a/${id}${media ? '' : '?media=1'}`}
|
||||
onClick={() => {
|
||||
if (!media) {
|
||||
showToast('Showing posts with media');
|
||||
}
|
||||
}}
|
||||
class={media ? 'is-active' : ''}
|
||||
>
|
||||
Media
|
||||
</Link>
|
||||
{featuredTags.map((tag) => (
|
||||
<Link
|
||||
key={tag.id}
|
||||
to={`/${instance}/a/${id}${
|
||||
tagged === tag.name
|
||||
? ''
|
||||
: `?tagged=${encodeURIComponent(tag.name)}`
|
||||
}`}
|
||||
onClick={() => {
|
||||
if (tagged !== tag.name) {
|
||||
showToast(`Showing posts tagged with #${tag.name}`);
|
||||
}
|
||||
}}
|
||||
class={tagged === tag.name ? 'is-active' : ''}
|
||||
>
|
||||
<span>
|
||||
<span class="more-insignificant">#</span>
|
||||
{tag.name}
|
||||
</span>
|
||||
{
|
||||
// The count differs based on instance 😅
|
||||
}
|
||||
{/* <span class="filter-count">{tag.statusesCount}</span> */}
|
||||
</Link>
|
||||
))}
|
||||
{searchEnabled &&
|
||||
(supportsInputMonth ? (
|
||||
<label class={`filter-field ${month ? 'is-active' : ''}`}>
|
||||
<Icon icon="month" size="l" />
|
||||
<input
|
||||
type="month"
|
||||
disabled={!account?.acct}
|
||||
value={month || ''}
|
||||
min={MIN_YEAR_MONTH}
|
||||
max={new Date().toISOString().slice(0, 7)}
|
||||
onInput={(e) => {
|
||||
const { value, validity } = e.currentTarget;
|
||||
if (!validity.valid) return;
|
||||
setSearchParams(
|
||||
value
|
||||
? {
|
||||
month: value,
|
||||
}
|
||||
: {},
|
||||
);
|
||||
const [year, month] = value.split('-');
|
||||
const monthIndex = parseInt(month, 10) - 1;
|
||||
const date = new Date(year, monthIndex);
|
||||
showToast(
|
||||
`Showing posts in ${date.toLocaleString('default', {
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
})}`,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
) : (
|
||||
// Fallback to <select> for month and <input type="number"> for year
|
||||
<MonthPicker
|
||||
class={`filter-field ${month ? 'is-active' : ''}`}
|
||||
disabled={!account?.acct}
|
||||
value={month || ''}
|
||||
min={MIN_YEAR_MONTH}
|
||||
max={new Date().toISOString().slice(0, 7)}
|
||||
onInput={(e) => {
|
||||
const { value, validity } = e.currentTarget;
|
||||
const { value, validity } = e;
|
||||
if (!validity.valid) return;
|
||||
setSearchParams(
|
||||
value
|
||||
|
@ -376,40 +434,11 @@ function AccountStatuses() {
|
|||
}
|
||||
: {},
|
||||
);
|
||||
const [year, month] = value.split('-');
|
||||
const monthIndex = parseInt(month, 10) - 1;
|
||||
const date = new Date(year, monthIndex);
|
||||
showToast(
|
||||
`Showing posts in ${date.toLocaleString('default', {
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
})}`,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
) : (
|
||||
// Fallback to <select> for month and <input type="number"> for year
|
||||
<MonthPicker
|
||||
class={`filter-field ${month ? 'is-active' : ''}`}
|
||||
disabled={!account?.acct}
|
||||
value={month || ''}
|
||||
min={MIN_YEAR_MONTH}
|
||||
max={new Date().toISOString().slice(0, 7)}
|
||||
onInput={(e) => {
|
||||
const { value, validity } = e;
|
||||
if (!validity.valid) return;
|
||||
setSearchParams(
|
||||
value
|
||||
? {
|
||||
month: value,
|
||||
}
|
||||
: {},
|
||||
);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}, [
|
||||
|
@ -472,7 +501,7 @@ function AccountStatuses() {
|
|||
errorText="Unable to load posts"
|
||||
fetchItems={fetchAccountStatuses}
|
||||
useItemID
|
||||
view={media ? 'media' : undefined}
|
||||
view={media || mediaFirst ? 'media' : undefined}
|
||||
boostsCarousel={snapStates.settings.boostsCarousel}
|
||||
timelineStart={TimelineStart}
|
||||
refresh={[
|
||||
|
|
|
@ -13,12 +13,13 @@ import NameText from '../components/name-text';
|
|||
import { api } from '../utils/api';
|
||||
import states from '../utils/states';
|
||||
import store from '../utils/store';
|
||||
import { getCurrentAccountID, setCurrentAccountID } from '../utils/store-utils';
|
||||
|
||||
function Accounts({ onClose }) {
|
||||
const { masto } = api();
|
||||
// Accounts
|
||||
const accounts = store.local.getJSON('accounts');
|
||||
const currentAccount = store.session.get('currentAccount');
|
||||
const currentAccount = getCurrentAccountID();
|
||||
const moreThanOneAccount = accounts.length > 1;
|
||||
|
||||
const [_, reload] = useReducer((x) => x + 1, 0);
|
||||
|
@ -81,7 +82,7 @@ function Accounts({ onClose }) {
|
|||
if (isCurrent) {
|
||||
states.showAccount = `${account.info.username}@${account.instanceURL}`;
|
||||
} else {
|
||||
store.session.set('currentAccount', account.info.id);
|
||||
setCurrentAccountID(account.info.id);
|
||||
location.reload();
|
||||
}
|
||||
}}
|
||||
|
|
|
@ -614,7 +614,7 @@
|
|||
}
|
||||
&.visibility-direct {
|
||||
--yellow-stripes: repeating-linear-gradient(
|
||||
-45deg,
|
||||
135deg,
|
||||
var(--reply-to-faded-color),
|
||||
var(--reply-to-faded-color) 10px,
|
||||
var(--reply-to-faded-color) 10px,
|
||||
|
|
|
@ -40,7 +40,7 @@ import showToast from '../utils/show-toast';
|
|||
import states, { statusKey } from '../utils/states';
|
||||
import statusPeek from '../utils/status-peek';
|
||||
import store from '../utils/store';
|
||||
import { getCurrentAccountNS } from '../utils/store-utils';
|
||||
import { getCurrentAccountID, getCurrentAccountNS } from '../utils/store-utils';
|
||||
import { assignFollowedTags } from '../utils/timeline-utils';
|
||||
import useTitle from '../utils/useTitle';
|
||||
|
||||
|
@ -112,7 +112,7 @@ function Catchup() {
|
|||
const [showTopLinks, setShowTopLinks] = useState(false);
|
||||
|
||||
const currentAccount = useMemo(() => {
|
||||
return store.session.get('currentAccount');
|
||||
return getCurrentAccountID();
|
||||
}, []);
|
||||
const isSelf = (accountID) => accountID === currentAccount;
|
||||
|
||||
|
|
|
@ -71,7 +71,8 @@ function Following({ title, path, id, ...props }) {
|
|||
.next();
|
||||
let { value } = results;
|
||||
console.log('checkForUpdates', latestItem.current, value);
|
||||
if (value?.length) {
|
||||
const valueContainsLatestItem = value[0]?.id === latestItem.current; // since_id might not be supported
|
||||
if (value?.length && !valueContainsLatestItem) {
|
||||
latestItem.current = value[0].id;
|
||||
value = dedupeBoosts(value, instance);
|
||||
value = filteredItems(value, 'home');
|
||||
|
|
|
@ -5,7 +5,7 @@ import {
|
|||
MenuHeader,
|
||||
MenuItem,
|
||||
} from '@szhsin/react-menu';
|
||||
import { useEffect, useRef, useState } from 'preact/hooks';
|
||||
import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
|
||||
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
||||
|
||||
import Icon from '../components/icon';
|
||||
|
@ -18,6 +18,7 @@ import { filteredItems } from '../utils/filters';
|
|||
import showToast from '../utils/show-toast';
|
||||
import states from '../utils/states';
|
||||
import { saveStatus } from '../utils/states';
|
||||
import { isMediaFirstInstance } from '../utils/store-utils';
|
||||
import useTitle from '../utils/useTitle';
|
||||
|
||||
const LIMIT = 20;
|
||||
|
@ -55,6 +56,8 @@ function Hashtags({ media: mediaView, columnMode, ...props }) {
|
|||
useTitle(title, `/:instance?/t/:hashtag`);
|
||||
const latestItem = useRef();
|
||||
|
||||
const mediaFirst = useMemo(() => isMediaFirstInstance(), []);
|
||||
|
||||
// const hashtagsIterator = useRef();
|
||||
const maxID = useRef(undefined);
|
||||
async function fetchHashtags(firstLoad) {
|
||||
|
@ -73,7 +76,7 @@ function Hashtags({ media: mediaView, columnMode, ...props }) {
|
|||
limit: LIMIT,
|
||||
any: hashtags.slice(1),
|
||||
maxId: firstLoad ? undefined : maxID.current,
|
||||
onlyMedia: media,
|
||||
onlyMedia: media ? true : undefined,
|
||||
})
|
||||
.next();
|
||||
let { value } = results;
|
||||
|
@ -85,7 +88,7 @@ function Hashtags({ media: mediaView, columnMode, ...props }) {
|
|||
// value = filteredItems(value, 'public');
|
||||
value.forEach((item) => {
|
||||
saveStatus(item, instance, {
|
||||
skipThreading: media, // If media view, no need to form threads
|
||||
skipThreading: media || mediaFirst, // If media view, no need to form threads
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -109,8 +112,9 @@ function Hashtags({ media: mediaView, columnMode, ...props }) {
|
|||
})
|
||||
.next();
|
||||
let { value } = results;
|
||||
value = filteredItems(value, 'public');
|
||||
if (value?.length) {
|
||||
const valueContainsLatestItem = value[0]?.id === latestItem.current; // since_id might not be supported
|
||||
if (value?.length && !valueContainsLatestItem) {
|
||||
value = filteredItems(value, 'public');
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
@ -155,7 +159,7 @@ function Hashtags({ media: mediaView, columnMode, ...props }) {
|
|||
fetchItems={fetchHashtags}
|
||||
checkForUpdates={checkForUpdates}
|
||||
useItemID
|
||||
view={media ? 'media' : undefined}
|
||||
view={media || mediaFirst ? 'media' : undefined}
|
||||
refresh={media}
|
||||
// allowFilters
|
||||
filterContext="public"
|
||||
|
@ -232,23 +236,27 @@ function Hashtags({ media: mediaView, 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 />
|
||||
{!mediaFirst && (
|
||||
<>
|
||||
<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
|
||||
|
|
|
@ -63,8 +63,9 @@ function List(props) {
|
|||
since_id: latestItem.current,
|
||||
});
|
||||
let { value } = results;
|
||||
value = filteredItems(value, 'home');
|
||||
if (value?.length) {
|
||||
const valueContainsLatestItem = value[0]?.id === latestItem.current; // since_id might not be supported
|
||||
if (value?.length && !valueContainsLatestItem) {
|
||||
value = filteredItems(value, 'home');
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
|
|
@ -95,7 +95,9 @@ function Mentions({ columnMode, ...props }) {
|
|||
latestConversationItem.current,
|
||||
value,
|
||||
);
|
||||
if (value?.length) {
|
||||
const valueContainsLatestItem =
|
||||
value[0]?.id === latestConversationItem.current; // since_id might not be supported
|
||||
if (value?.length && !valueContainsLatestItem) {
|
||||
latestConversationItem.current = value[0].lastStatus.id;
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -72,6 +72,13 @@ function Notifications({ columnMode }) {
|
|||
excludeTypes: ['follow_request'],
|
||||
});
|
||||
}
|
||||
if (/max_id=($|&)/i.test(notificationsIterator.current?.nextParams)) {
|
||||
// Pixelfed returns next paginationed link with empty max_id
|
||||
// I assume, it's done (end of list)
|
||||
return {
|
||||
done: true,
|
||||
};
|
||||
}
|
||||
const allNotifications = await notificationsIterator.current.next();
|
||||
const notifications = allNotifications.value;
|
||||
|
||||
|
@ -82,6 +89,21 @@ function Notifications({ columnMode }) {
|
|||
});
|
||||
});
|
||||
|
||||
// TEST: Slot in a fake notification to test 'severed_relationships'
|
||||
// notifications.unshift({
|
||||
// id: '123123',
|
||||
// type: 'severed_relationships',
|
||||
// createdAt: '2024-03-22T19:20:08.316Z',
|
||||
// event: {
|
||||
// type: 'account_suspension',
|
||||
// targetName: 'mastodon.dev',
|
||||
// followersCount: 0,
|
||||
// followingCount: 0,
|
||||
// },
|
||||
// });
|
||||
|
||||
// console.log({ notifications });
|
||||
|
||||
const groupedNotifications = groupNotifications(notifications);
|
||||
|
||||
if (firstLoad) {
|
||||
|
@ -247,7 +269,6 @@ function Notifications({ columnMode }) {
|
|||
|
||||
const lastHiddenTime = useRef();
|
||||
usePageVisibility((visible) => {
|
||||
let unsub;
|
||||
if (visible) {
|
||||
const timeDiff = Date.now() - lastHiddenTime.current;
|
||||
if (!lastHiddenTime.current || timeDiff > 1000 * 3) {
|
||||
|
@ -258,20 +279,16 @@ function Notifications({ columnMode }) {
|
|||
} else {
|
||||
lastHiddenTime.current = Date.now();
|
||||
}
|
||||
unsub = subscribeKey(states, 'notificationsShowNew', (v) => {
|
||||
if (uiState === 'loading') {
|
||||
return;
|
||||
}
|
||||
if (v) {
|
||||
loadUpdates();
|
||||
}
|
||||
setShowNew(v);
|
||||
});
|
||||
}
|
||||
return () => {
|
||||
unsub?.();
|
||||
};
|
||||
});
|
||||
useEffect(() => {
|
||||
let unsub = subscribeKey(states, 'notificationsShowNew', (v) => {
|
||||
if (uiState === 'loading') return;
|
||||
if (v) loadUpdates();
|
||||
setShowNew(v);
|
||||
});
|
||||
return () => unsub?.();
|
||||
}, []);
|
||||
|
||||
const todayDate = new Date();
|
||||
const yesterdayDate = new Date(todayDate - 24 * 60 * 60 * 1000);
|
||||
|
@ -418,7 +435,7 @@ function Notifications({ columnMode }) {
|
|||
{supportsFilteredNotifications && (
|
||||
<button
|
||||
type="button"
|
||||
class="button plain"
|
||||
class="button plain4"
|
||||
onClick={() => {
|
||||
setShowNotificationsSettings(true);
|
||||
}}
|
||||
|
@ -613,7 +630,7 @@ function Notifications({ columnMode }) {
|
|||
</label>
|
||||
</div>
|
||||
<h2 class="timeline-header">Today</h2>
|
||||
{showTodayEmpty && !!snapStates.notifications.length && (
|
||||
{showTodayEmpty && (
|
||||
<p class="ui-state insignificant">
|
||||
{uiState === 'default' ? "You're all caught up." : <>…</>}
|
||||
</p>
|
||||
|
|
|
@ -33,6 +33,7 @@ function Public({ local, columnMode, ...props }) {
|
|||
publicIterator.current = masto.v1.timelines.public.list({
|
||||
limit: LIMIT,
|
||||
local: isLocal,
|
||||
remote: !isLocal, // Pixelfed
|
||||
});
|
||||
}
|
||||
const results = await publicIterator.current.next();
|
||||
|
@ -63,8 +64,9 @@ function Public({ local, columnMode, ...props }) {
|
|||
})
|
||||
.next();
|
||||
let { value } = results;
|
||||
value = filteredItems(value, 'public');
|
||||
if (value?.length) {
|
||||
const valueContainsLatestItem = value[0]?.id === latestItem.current; // since_id might not be supported
|
||||
if (value?.length && !valueContainsLatestItem) {
|
||||
value = filteredItems(value, 'public');
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
|
|
@ -19,6 +19,7 @@ import pmem from '../utils/pmem';
|
|||
import shortenNumber from '../utils/shorten-number';
|
||||
import states from '../utils/states';
|
||||
import { saveStatus } from '../utils/states';
|
||||
import supports from '../utils/supports';
|
||||
import useTitle from '../utils/useTitle';
|
||||
|
||||
const LIMIT = 20;
|
||||
|
@ -33,6 +34,17 @@ const fetchLinks = pmem(
|
|||
},
|
||||
);
|
||||
|
||||
function fetchTrends(masto) {
|
||||
if (supports('@pixelfed/trending')) {
|
||||
return masto.pixelfed.v2.discover.posts.trending.list({
|
||||
range: 'daily',
|
||||
});
|
||||
}
|
||||
return masto.v1.trends.statuses.list({
|
||||
limit: LIMIT,
|
||||
});
|
||||
}
|
||||
|
||||
function Trending({ columnMode, ...props }) {
|
||||
const snapStates = useSnapshot(states);
|
||||
const params = columnMode ? {} : useParams();
|
||||
|
@ -48,36 +60,39 @@ function Trending({ columnMode, ...props }) {
|
|||
const [hashtags, setHashtags] = useState([]);
|
||||
const [links, setLinks] = useState([]);
|
||||
const trendIterator = useRef();
|
||||
|
||||
async function fetchTrend(firstLoad) {
|
||||
if (firstLoad || !trendIterator.current) {
|
||||
trendIterator.current = masto.v1.trends.statuses.list({
|
||||
limit: LIMIT,
|
||||
});
|
||||
trendIterator.current = fetchTrends(masto);
|
||||
|
||||
// Get hashtags
|
||||
try {
|
||||
const iterator = masto.v1.trends.tags.list();
|
||||
const { value: tags } = await iterator.next();
|
||||
console.log('tags', tags);
|
||||
if (tags?.length) {
|
||||
setHashtags(tags);
|
||||
if (supports('@mastodon/trending-hashtags')) {
|
||||
try {
|
||||
const iterator = masto.v1.trends.tags.list();
|
||||
const { value: tags } = await iterator.next();
|
||||
console.log('tags', tags);
|
||||
if (tags?.length) {
|
||||
setHashtags(tags);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
// Get links
|
||||
try {
|
||||
const { value } = await fetchLinks(masto, instance);
|
||||
// 4 types available: link, photo, video, rich
|
||||
// Only want links for now
|
||||
const links = value?.filter?.((link) => link.type === 'link');
|
||||
console.log('links', links);
|
||||
if (links?.length) {
|
||||
setLinks(links);
|
||||
if (supports('@mastodon/trending-links')) {
|
||||
try {
|
||||
const { value } = await fetchLinks(masto, instance);
|
||||
// 4 types available: link, photo, video, rich
|
||||
// Only want links for now
|
||||
const links = value?.filter?.((link) => link.type === 'link');
|
||||
console.log('links', links);
|
||||
if (links?.length) {
|
||||
setLinks(links);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
const results = await trendIterator.current.next();
|
||||
|
|
|
@ -7,6 +7,7 @@ import {
|
|||
getAccountByInstance,
|
||||
getCurrentAccount,
|
||||
saveAccount,
|
||||
setCurrentAccountID,
|
||||
} from './store-utils';
|
||||
|
||||
// Default *fallback* instance
|
||||
|
@ -118,7 +119,7 @@ export async function initAccount(client, instance, accessToken, vapidKey) {
|
|||
const mastoAccount = await masto.v1.accounts.verifyCredentials();
|
||||
|
||||
console.log('CURRENTACCOUNT SET', mastoAccount.id);
|
||||
store.session.set('currentAccount', mastoAccount.id);
|
||||
setCurrentAccountID(mastoAccount.id);
|
||||
|
||||
saveAccount({
|
||||
info: mastoAccount,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import mem from './mem';
|
||||
import store from './store';
|
||||
import { getCurrentAccountID } from './store-utils';
|
||||
|
||||
function _isFiltered(filtered, filterContext) {
|
||||
if (!filtered?.length) return false;
|
||||
|
@ -43,7 +43,7 @@ export function filteredItem(item, filterContext, currentAccountID) {
|
|||
export function filteredItems(items, filterContext) {
|
||||
if (!items?.length) return [];
|
||||
if (!filterContext) return items;
|
||||
const currentAccountID = store.session.get('currentAccount');
|
||||
const currentAccountID = getCurrentAccountID();
|
||||
return items.filter((item) =>
|
||||
filteredItem(item, filterContext, currentAccountID),
|
||||
);
|
||||
|
|
|
@ -6,6 +6,7 @@ const statusPostRegexes = [
|
|||
/\/notes\/([^\/]+)/i, // Misskey, Firefish
|
||||
/^\/(?:notice|objects)\/([a-z0-9-]+)/i, // Pleroma
|
||||
/\/@[^@\/]+@?[^\/]+?\/([^\/]+)/i, // Mastodon
|
||||
/^\/p\/[^\/]+\/([^\/]+)/i, // Pixelfed
|
||||
];
|
||||
|
||||
export function getInstanceStatusObject(url) {
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { api } from './api';
|
||||
import store from './store';
|
||||
import { getCurrentAccountID } from './store-utils';
|
||||
|
||||
export async function fetchRelationships(accounts, relationshipsMap = {}) {
|
||||
if (!accounts?.length) return;
|
||||
const { masto } = api();
|
||||
|
||||
const currentAccount = store.session.get('currentAccount');
|
||||
const currentAccount = getCurrentAccountID();
|
||||
const uniqueAccountIds = accounts.reduce((acc, a) => {
|
||||
// 1. Ignore duplicate accounts
|
||||
// 2. Ignore accounts that are already inside relationshipsMap
|
||||
|
|
|
@ -16,13 +16,40 @@ export function getAccountByInstance(instance) {
|
|||
return accounts.find((a) => a.instanceURL === instance);
|
||||
}
|
||||
|
||||
const standaloneMQ = window.matchMedia('(display-mode: standalone)');
|
||||
|
||||
export function getCurrentAccountID() {
|
||||
try {
|
||||
const id = store.session.get('currentAccount');
|
||||
if (id) return id;
|
||||
} catch (e) {}
|
||||
if (standaloneMQ.matches) {
|
||||
try {
|
||||
const id = store.local.get('currentAccount');
|
||||
if (id) return id;
|
||||
} catch (e) {}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function setCurrentAccountID(id) {
|
||||
try {
|
||||
store.session.set('currentAccount', id);
|
||||
} catch (e) {}
|
||||
if (standaloneMQ.matches) {
|
||||
try {
|
||||
store.local.set('currentAccount', id);
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
|
||||
export function getCurrentAccount() {
|
||||
if (!window.__IGNORE_GET_ACCOUNT_ERROR__) {
|
||||
// Track down getCurrentAccount() calls before account-based states are initialized
|
||||
console.error('getCurrentAccount() called before states are initialized');
|
||||
if (import.meta.env.DEV) console.trace();
|
||||
}
|
||||
const currentAccount = store.session.get('currentAccount');
|
||||
const currentAccount = getCurrentAccountID();
|
||||
const account = getAccount(currentAccount);
|
||||
return account;
|
||||
}
|
||||
|
@ -48,7 +75,7 @@ export function saveAccount(account) {
|
|||
accounts.push(account);
|
||||
}
|
||||
store.local.setJSON('accounts', accounts);
|
||||
store.session.set('currentAccount', account.info.id);
|
||||
setCurrentAccountID(account.info.id);
|
||||
}
|
||||
|
||||
export function updateAccount(accountInfo) {
|
||||
|
@ -126,3 +153,8 @@ export function getCurrentInstanceConfiguration() {
|
|||
const instance = getCurrentInstance();
|
||||
return getInstanceConfiguration(instance);
|
||||
}
|
||||
|
||||
export function isMediaFirstInstance() {
|
||||
const instance = getCurrentInstance();
|
||||
return /pixelfed/i.test(instance?.version);
|
||||
}
|
||||
|
|
|
@ -4,6 +4,21 @@ import features from '../data/features.json';
|
|||
|
||||
import { getCurrentInstance } from './store-utils';
|
||||
|
||||
// Non-semver(?) UA string detection
|
||||
const containPixelfed = /pixelfed/i;
|
||||
const notContainPixelfed = /^(?!.*pixelfed).*$/i;
|
||||
const platformFeatures = {
|
||||
'@mastodon/lists': notContainPixelfed,
|
||||
'@mastodon/filters': notContainPixelfed,
|
||||
'@mastodon/mentions': notContainPixelfed,
|
||||
'@mastodon/trending-hashtags': notContainPixelfed,
|
||||
'@mastodon/trending-links': notContainPixelfed,
|
||||
'@mastodon/post-bookmark': notContainPixelfed,
|
||||
'@mastodon/post-edit': notContainPixelfed,
|
||||
'@mastodon/profile-edit': notContainPixelfed,
|
||||
'@mastodon/profile-private-note': notContainPixelfed,
|
||||
'@pixelfed/trending': containPixelfed,
|
||||
};
|
||||
const supportsCache = {};
|
||||
|
||||
function supports(feature) {
|
||||
|
@ -11,6 +26,11 @@ function supports(feature) {
|
|||
const { version, domain } = getCurrentInstance();
|
||||
const key = `${domain}-${feature}`;
|
||||
if (supportsCache[key]) return supportsCache[key];
|
||||
|
||||
if (platformFeatures[feature]) {
|
||||
return (supportsCache[key] = platformFeatures[feature].test(version));
|
||||
}
|
||||
|
||||
const range = features[feature];
|
||||
if (!range) return false;
|
||||
return (supportsCache[key] = satisfies(version, range, {
|
||||
|
|
Ładowanie…
Reference in New Issue