Compare commits
82 commits
c9ffcdf535
...
9d813802a9
Author | SHA1 | Date | |
---|---|---|---|
Natsu Kagami | 9d813802a9 | ||
173cad2275 | |||
0403fc35f4 | |||
077b655c44 | |||
eeb89212d2 | |||
cb04659ab1 | |||
d478dbddba | |||
cb36308790 | |||
d4dca0e81f | |||
f8fc24aca4 | |||
7ba5ee5fe2 | |||
4c3666df6a | |||
e3b0c31798 | |||
da03de4115 | |||
34fcf5e8bd | |||
d6499cf7fd | |||
1e9f0bdf39 | |||
cd3ab50a18 | |||
f6ab5e9afa | |||
b1dec8810b | |||
a10e2804ba | |||
bd7e099f6e | |||
3d06662559 | |||
1f584f945a | |||
a816b69ee9 | |||
85a4b382da | |||
7ec1cd1e3d | |||
5661729748 | |||
551de5a37c | |||
38bd5c0b5d | |||
9387e37baa | |||
baca2b5851 | |||
7e01b4a33a | |||
674c99a05d | |||
b3501d158f | |||
c955427d8f | |||
56e846bec6 | |||
4acfb2a1cf | |||
f9b2ab3b94 | |||
42f9483491 | |||
fe80215325 | |||
f7ffce1b46 | |||
64db69af63 | |||
59dae782b2 | |||
dafff4b635 | |||
887503e40b | |||
1a714d214b | |||
941d2efeb1 | |||
908efb17ff | |||
7d28744234 | |||
679fba4f66 | |||
ad831fae35 | |||
e102a9f925 | |||
9571271d83 | |||
b116cbfe8c | |||
b1030cb38a | |||
72438bbf06 | |||
f3b81bc540 | |||
020d8e3631 | |||
dac07a35d8 | |||
6db40d7d3e | |||
0b5693ae27 | |||
7a30cc4b12 | |||
d18db56032 | |||
27274eeab1 | |||
fce5e45bc9 | |||
fa145d3ed0 | |||
244f3325ae | |||
ec57c75fa0 | |||
5ac255f808 | |||
62201b0250 | |||
f02cd50d7b | |||
61e1a5042f | |||
2145f761b5 | |||
979c3b1498 | |||
c4961b26bb | |||
aa3033b4ff | |||
641d274d7b | |||
3fc3641437 | |||
b57d8adf18 | |||
dd2ca7bf35 | |||
f5184bd608 |
|
@ -6,6 +6,10 @@ Phanpy does not collect or process any personal information from its users. The
|
||||||
|
|
||||||
Phanpy is hosted on [Cloudflare Pages](https://pages.cloudflare.com/) as a static website. Read more about [Cloudflare's privacy policy](https://www.cloudflare.com/privacypolicy/).
|
Phanpy is hosted on [Cloudflare Pages](https://pages.cloudflare.com/) as a static website. Read more about [Cloudflare's privacy policy](https://www.cloudflare.com/privacypolicy/).
|
||||||
|
|
||||||
|
## Post translations
|
||||||
|
|
||||||
|
Phanpy uses [Lingva Translate](https://github.com/thedaviddelta/lingva-translate) to translate posts.
|
||||||
|
|
||||||
## Error logging
|
## Error logging
|
||||||
|
|
||||||
Phanpy dev site (*dev.phanpy.social*) uses [Rollbar](https://rollbar.com/) to log errors for debugging purposes. Read more about [Rollbar's privacy policy](https://rollbar.com/privacy/). The production site (*phanpy.social*) does not use error logging.
|
Phanpy dev site (*dev.phanpy.social*) uses [Rollbar](https://rollbar.com/) to log errors for debugging purposes. Read more about [Rollbar's privacy policy](https://rollbar.com/privacy/). The production site (*phanpy.social*) does not use error logging.
|
||||||
|
|
10
README.md
10
README.md
|
@ -93,7 +93,7 @@ Everything is designed and engineered following my taste and vision. This is a p
|
||||||
- Content can be partially revealed by hovering over the post, with tooltip showing the post text.
|
- Content can be partially revealed by hovering over the post, with tooltip showing the post text.
|
||||||
- Clicking it will open the Post page.
|
- Clicking it will open the Post page.
|
||||||
- Long-pressing or right-clicking it will "peek" the post with a bottom sheet UI.
|
- Long-pressing or right-clicking it will "peek" the post with a bottom sheet UI.
|
||||||
- On boosts carousel, they are not partially hidden, but sorted to the end of the carousel.
|
- On boosts carousel, they are sorted to the end of the carousel.
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
|
@ -155,14 +155,14 @@ And here I am. Building a Mastodon web client.
|
||||||
|
|
||||||
## Alternative web clients
|
## Alternative web clients
|
||||||
|
|
||||||
- [Pinafore](https://pinafore.social/) ([retired](https://nolanlawson.com/2023/01/09/retiring-pinafore/))
|
- [Pinafore](https://pinafore.social/) ([retired](https://nolanlawson.com/2023/01/09/retiring-pinafore/)) - forks ↓
|
||||||
- [Semaphore](https://semaphore.social/)
|
- [Semaphore](https://semaphore.social/)
|
||||||
- [Enafore](https://pinafore.easrng.net/)
|
- [Enafore](https://enafore.social/)
|
||||||
- [Cuckoo+](https://www.cuckoo.social/)
|
- [Cuckoo+](https://www.cuckoo.social/)
|
||||||
- [Sengi](https://nicolasconstant.github.io/sengi/)
|
- [Sengi](https://nicolasconstant.github.io/sengi/)
|
||||||
- [Soapbox](https://fe.soapbox.pub/)
|
- [Soapbox](https://fe.soapbox.pub/)
|
||||||
- [Elk](https://elk.zone/)
|
- [Elk](https://elk.zone/) - forks ↓
|
||||||
- Fork https://elk.fedified.com/
|
- [elk.fedified.com](https://elk.fedified.com/)
|
||||||
- [Mastodeck](https://mastodeck.com/)
|
- [Mastodeck](https://mastodeck.com/)
|
||||||
- [Trunks](https://trunks.social/)
|
- [Trunks](https://trunks.social/)
|
||||||
- [Tooty](https://github.com/n1k0/tooty)
|
- [Tooty](https://github.com/n1k0/tooty)
|
||||||
|
|
Binary file not shown.
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
src = lib.cleanSource ./.;
|
src = lib.cleanSource ./.;
|
||||||
|
|
||||||
npmDepsHash = "sha256-nd4RHxJIHJIHkoQUELURBeTc0qrThbbUwFnQhdyKKRI=";
|
npmDepsHash = "sha256-tqR3YQ++nJmwDNKIm7uFLhJ5HlAqfeEmJVyynHx3Hzw=";
|
||||||
# npmDepsHash = lib.fakeHash;
|
# npmDepsHash = lib.fakeHash;
|
||||||
|
|
||||||
# DTTH-specific env variables
|
# DTTH-specific env variables
|
||||||
|
|
48
package-lock.json
generated
48
package-lock.json
generated
|
@ -8,7 +8,7 @@
|
||||||
"name": "phanpy",
|
"name": "phanpy",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@formatjs/intl-localematcher": "~0.4.1",
|
"@formatjs/intl-localematcher": "~0.4.2",
|
||||||
"@github/text-expander-element": "~2.5.0",
|
"@github/text-expander-element": "~2.5.0",
|
||||||
"@iconify-icons/mingcute": "~1.2.7",
|
"@iconify-icons/mingcute": "~1.2.7",
|
||||||
"@justinribeiro/lite-youtube": "~1.5.0",
|
"@justinribeiro/lite-youtube": "~1.5.0",
|
||||||
|
@ -20,10 +20,10 @@
|
||||||
"fast-deep-equal": "~3.1.3",
|
"fast-deep-equal": "~3.1.3",
|
||||||
"idb-keyval": "~6.2.1",
|
"idb-keyval": "~6.2.1",
|
||||||
"just-debounce-it": "~3.2.0",
|
"just-debounce-it": "~3.2.0",
|
||||||
"lz-string": "^1.5.0",
|
"lz-string": "~1.5.0",
|
||||||
"masto": "~5.11.4",
|
"masto": "~5.11.4",
|
||||||
"mem": "~9.0.2",
|
"mem": "~9.0.2",
|
||||||
"p-retry": "~5.1.2",
|
"p-retry": "~6.0.0",
|
||||||
"p-throttle": "~5.1.0",
|
"p-throttle": "~5.1.0",
|
||||||
"preact": "~10.17.1",
|
"preact": "~10.17.1",
|
||||||
"react-hotkeys-hook": "~4.4.1",
|
"react-hotkeys-hook": "~4.4.1",
|
||||||
|
@ -2989,9 +2989,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@formatjs/intl-localematcher": {
|
"node_modules/@formatjs/intl-localematcher": {
|
||||||
"version": "0.4.1",
|
"version": "0.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.4.2.tgz",
|
||||||
"integrity": "sha512-Fs4MhhHlLC0RrspX2u2KP7zlwL9eHrBZsOBxaPOeqrCZYLaOUK4cYXQ1ErpAB0HnGV/GUXNa5smzV/7jCuRzxg==",
|
"integrity": "sha512-BGdtJFmaNJy5An/Zan4OId/yR9Ih1OojFjcduX/xOvq798OgWSyDtd6Qd5jqJXwJs1ipe4Fxu9+cshic5Ox2tA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": "^2.4.0"
|
"tslib": "^2.4.0"
|
||||||
}
|
}
|
||||||
|
@ -3385,9 +3385,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/retry": {
|
"node_modules/@types/retry": {
|
||||||
"version": "0.12.1",
|
"version": "0.12.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz",
|
||||||
"integrity": "sha512-xoDlM2S4ortawSWORYqsdU+2rxdh4LRW9ytc3zmT37RIKQh6IHyKwwtKhKis9ah8ol07DCkZxPt8BBvPjC6v4g=="
|
"integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow=="
|
||||||
},
|
},
|
||||||
"node_modules/@types/trusted-types": {
|
"node_modules/@types/trusted-types": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
|
@ -5573,15 +5573,15 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/p-retry": {
|
"node_modules/p-retry": {
|
||||||
"version": "5.1.2",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/p-retry/-/p-retry-5.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.0.0.tgz",
|
||||||
"integrity": "sha512-couX95waDu98NfNZV+i/iLt+fdVxmI7CbrrdC2uDWfPdUAApyxT4wmDlyOtR5KtTDmkDO0zDScDjDou9YHhd9g==",
|
"integrity": "sha512-6NuuXu8Upembd4sNdo4PRbs+M6aHgBTrFE6lkH0YKjVzne3cDW4gkncB98ty/bkMxLxLVNeD5bX9FyWjM7WZ+A==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/retry": "0.12.1",
|
"@types/retry": "0.12.2",
|
||||||
"retry": "^0.13.1"
|
"retry": "^0.13.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
"node": ">=16.17"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
@ -9661,9 +9661,9 @@
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@formatjs/intl-localematcher": {
|
"@formatjs/intl-localematcher": {
|
||||||
"version": "0.4.1",
|
"version": "0.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.4.2.tgz",
|
||||||
"integrity": "sha512-Fs4MhhHlLC0RrspX2u2KP7zlwL9eHrBZsOBxaPOeqrCZYLaOUK4cYXQ1ErpAB0HnGV/GUXNa5smzV/7jCuRzxg==",
|
"integrity": "sha512-BGdtJFmaNJy5An/Zan4OId/yR9Ih1OojFjcduX/xOvq798OgWSyDtd6Qd5jqJXwJs1ipe4Fxu9+cshic5Ox2tA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"tslib": "^2.4.0"
|
"tslib": "^2.4.0"
|
||||||
}
|
}
|
||||||
|
@ -9995,9 +9995,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@types/retry": {
|
"@types/retry": {
|
||||||
"version": "0.12.1",
|
"version": "0.12.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz",
|
||||||
"integrity": "sha512-xoDlM2S4ortawSWORYqsdU+2rxdh4LRW9ytc3zmT37RIKQh6IHyKwwtKhKis9ah8ol07DCkZxPt8BBvPjC6v4g=="
|
"integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow=="
|
||||||
},
|
},
|
||||||
"@types/trusted-types": {
|
"@types/trusted-types": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
|
@ -11585,11 +11585,11 @@
|
||||||
"integrity": "sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw=="
|
"integrity": "sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw=="
|
||||||
},
|
},
|
||||||
"p-retry": {
|
"p-retry": {
|
||||||
"version": "5.1.2",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/p-retry/-/p-retry-5.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.0.0.tgz",
|
||||||
"integrity": "sha512-couX95waDu98NfNZV+i/iLt+fdVxmI7CbrrdC2uDWfPdUAApyxT4wmDlyOtR5KtTDmkDO0zDScDjDou9YHhd9g==",
|
"integrity": "sha512-6NuuXu8Upembd4sNdo4PRbs+M6aHgBTrFE6lkH0YKjVzne3cDW4gkncB98ty/bkMxLxLVNeD5bX9FyWjM7WZ+A==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@types/retry": "0.12.1",
|
"@types/retry": "0.12.2",
|
||||||
"retry": "^0.13.1"
|
"retry": "^0.13.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
"sourcemap": "npx source-map-explorer dist/assets/*.js"
|
"sourcemap": "npx source-map-explorer dist/assets/*.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@formatjs/intl-localematcher": "~0.4.1",
|
"@formatjs/intl-localematcher": "~0.4.2",
|
||||||
"@github/text-expander-element": "~2.5.0",
|
"@github/text-expander-element": "~2.5.0",
|
||||||
"@iconify-icons/mingcute": "~1.2.7",
|
"@iconify-icons/mingcute": "~1.2.7",
|
||||||
"@justinribeiro/lite-youtube": "~1.5.0",
|
"@justinribeiro/lite-youtube": "~1.5.0",
|
||||||
|
@ -22,10 +22,10 @@
|
||||||
"fast-deep-equal": "~3.1.3",
|
"fast-deep-equal": "~3.1.3",
|
||||||
"idb-keyval": "~6.2.1",
|
"idb-keyval": "~6.2.1",
|
||||||
"just-debounce-it": "~3.2.0",
|
"just-debounce-it": "~3.2.0",
|
||||||
"lz-string": "^1.5.0",
|
"lz-string": "~1.5.0",
|
||||||
"masto": "~5.11.4",
|
"masto": "~5.11.4",
|
||||||
"mem": "~9.0.2",
|
"mem": "~9.0.2",
|
||||||
"p-retry": "~5.1.2",
|
"p-retry": "~6.0.0",
|
||||||
"p-throttle": "~5.1.0",
|
"p-throttle": "~5.1.0",
|
||||||
"preact": "~10.17.1",
|
"preact": "~10.17.1",
|
||||||
"react-hotkeys-hook": "~4.4.1",
|
"react-hotkeys-hook": "~4.4.1",
|
||||||
|
|
BIN
public/logo-badge-72.png
Normal file
BIN
public/logo-badge-72.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
21
public/sw.js
21
public/sw.js
|
@ -9,6 +9,25 @@ import {
|
||||||
|
|
||||||
self.__WB_DISABLE_DEV_LOGS = true;
|
self.__WB_DISABLE_DEV_LOGS = true;
|
||||||
|
|
||||||
|
const assetsRoute = new Route(
|
||||||
|
({ request, sameOrigin }) => {
|
||||||
|
const isAsset =
|
||||||
|
request.destination === 'style' || request.destination === 'script';
|
||||||
|
const hasHash = /-[0-9a-f]{4,}\./i.test(request.url);
|
||||||
|
return sameOrigin && isAsset && hasHash;
|
||||||
|
},
|
||||||
|
new NetworkFirst({
|
||||||
|
cacheName: 'assets',
|
||||||
|
networkTimeoutSeconds: 5,
|
||||||
|
plugins: [
|
||||||
|
new CacheableResponsePlugin({
|
||||||
|
statuses: [0, 200],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
registerRoute(assetsRoute);
|
||||||
|
|
||||||
const imageRoute = new Route(
|
const imageRoute = new Route(
|
||||||
({ request, sameOrigin }) => {
|
({ request, sameOrigin }) => {
|
||||||
const isRemote = !sameOrigin;
|
const isRemote = !sameOrigin;
|
||||||
|
@ -124,7 +143,7 @@ self.addEventListener('push', (event) => {
|
||||||
body,
|
body,
|
||||||
icon,
|
icon,
|
||||||
dir: 'auto',
|
dir: 'auto',
|
||||||
badge: '/logo-192.png',
|
badge: '/logo-badge-72.png',
|
||||||
lang: preferred_locale,
|
lang: preferred_locale,
|
||||||
tag: notification_id,
|
tag: notification_id,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
|
|
42
src/app.css
42
src/app.css
|
@ -788,6 +788,7 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
counter-reset: index;
|
counter-reset: index;
|
||||||
|
min-height: 160px;
|
||||||
}
|
}
|
||||||
.status-carousel > ul > li {
|
.status-carousel > ul > li {
|
||||||
scroll-snap-align: center;
|
scroll-snap-align: center;
|
||||||
|
@ -1737,25 +1738,22 @@ meter.donut[hidden] {
|
||||||
|
|
||||||
:is(.shiny-pill, :root .toastify.shiny-pill) {
|
:is(.shiny-pill, :root .toastify.shiny-pill) {
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
color: var(--button-text-color);
|
color: var(--link-text-color);
|
||||||
text-shadow: 0 calc(var(--hairline-width) * -1) var(--drop-shadow-color);
|
font-weight: 500;
|
||||||
background-color: var(--button-bg-color);
|
text-shadow: 0 1px var(--bg-color);
|
||||||
background-image: linear-gradient(
|
background-color: var(--bg-color);
|
||||||
160deg,
|
border: 1px solid var(--outline-color);
|
||||||
rgba(255, 255, 255, 0.5),
|
box-shadow: 0 3px 16px var(--drop-shadow-color),
|
||||||
rgba(0, 0, 0, 0.1)
|
0 6px 16px -3px var(--drop-shadow-color);
|
||||||
);
|
|
||||||
box-shadow: 0 3px 8px -1px var(--drop-shadow-color),
|
|
||||||
0 10px 36px -4px var(--button-bg-blur-color),
|
|
||||||
inset var(--hairline-width) var(--hairline-width) rgba(255, 255, 255, 0.5);
|
|
||||||
transition: filter 0.3s;
|
|
||||||
}
|
}
|
||||||
:is(.shiny-pill, :root .toastify.shiny-pill):hover {
|
:is(.shiny-pill, :root .toastify.shiny-pill):hover:not(:active) {
|
||||||
filter: brightness(1.2);
|
color: var(--text-color);
|
||||||
}
|
border-color: var(--link-color);
|
||||||
:is(.shiny-pill, :root .toastify.shiny-pill):active {
|
filter: none !important;
|
||||||
transition: none;
|
box-shadow: 0 0 0 1px var(--link-text-color),
|
||||||
filter: brightness(0.9);
|
0 3px 16px var(--drop-shadow-color),
|
||||||
|
0 6px 16px -3px var(--drop-shadow-color),
|
||||||
|
0 6px 16px var(--drop-shadow-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* TOAST */
|
/* TOAST */
|
||||||
|
@ -1767,9 +1765,10 @@ meter.donut[hidden] {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
color: var(--button-text-color);
|
color: var(--button-text-color);
|
||||||
text-shadow: 0 calc(var(--hairline-width) * -1) var(--drop-shadow-color);
|
text-shadow: 0 calc(var(--hairline-width) * -1) var(--drop-shadow-color);
|
||||||
background-color: var(--button-bg-blur-color);
|
background-color: var(--button-bg-color);
|
||||||
background-image: none;
|
background-image: none;
|
||||||
backdrop-filter: blur(16px);
|
box-shadow: 0 3px 8px -1px var(--drop-shadow-color),
|
||||||
|
0 10px 36px -4px var(--button-bg-blur-color);
|
||||||
}
|
}
|
||||||
.toastify-bottom {
|
.toastify-bottom {
|
||||||
margin-bottom: env(safe-area-inset-bottom);
|
margin-bottom: env(safe-area-inset-bottom);
|
||||||
|
@ -2193,6 +2192,9 @@ ul.link-list li a .icon {
|
||||||
.timeline-deck > header[hidden] {
|
.timeline-deck > header[hidden] {
|
||||||
transform: translate3d(0, calc((100% + var(--margin-top)) * -1), 0);
|
transform: translate3d(0, calc((100% + var(--margin-top)) * -1), 0);
|
||||||
}
|
}
|
||||||
|
.deck > header {
|
||||||
|
text-shadow: 0 1px var(--bg-color);
|
||||||
|
}
|
||||||
.deck > header h1 {
|
.deck > header h1 {
|
||||||
font-size: 1.5em;
|
font-size: 1.5em;
|
||||||
}
|
}
|
||||||
|
|
202
src/app.jsx
202
src/app.jsx
|
@ -7,33 +7,21 @@ import {
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from 'preact/hooks';
|
} from 'preact/hooks';
|
||||||
import {
|
import { matchPath, Route, Routes, useLocation } from 'react-router-dom';
|
||||||
matchPath,
|
|
||||||
Route,
|
|
||||||
Routes,
|
|
||||||
useLocation,
|
|
||||||
useNavigate,
|
|
||||||
} from 'react-router-dom';
|
|
||||||
import 'swiped-events';
|
import 'swiped-events';
|
||||||
import { useSnapshot } from 'valtio';
|
import { useSnapshot } from 'valtio';
|
||||||
|
|
||||||
import AccountSheet from './components/account-sheet';
|
|
||||||
import BackgroundService from './components/background-service';
|
import BackgroundService from './components/background-service';
|
||||||
import Compose from './components/compose';
|
|
||||||
import ComposeButton from './components/compose-button';
|
import ComposeButton from './components/compose-button';
|
||||||
import Drafts from './components/drafts';
|
|
||||||
import { ICONS } from './components/icon';
|
import { ICONS } from './components/icon';
|
||||||
import KeyboardShortcutsHelp from './components/keyboard-shortcuts-help';
|
import KeyboardShortcutsHelp from './components/keyboard-shortcuts-help';
|
||||||
import Loader from './components/loader';
|
import Loader from './components/loader';
|
||||||
import MediaModal from './components/media-modal';
|
import Modals from './components/modals';
|
||||||
import Modal from './components/modal';
|
|
||||||
import NotificationService from './components/notification-service';
|
import NotificationService from './components/notification-service';
|
||||||
import SearchCommand from './components/search-command';
|
import SearchCommand from './components/search-command';
|
||||||
import Shortcuts from './components/shortcuts';
|
import Shortcuts from './components/shortcuts';
|
||||||
import ShortcutsSettings from './components/shortcuts-settings';
|
|
||||||
import NotFound from './pages/404';
|
import NotFound from './pages/404';
|
||||||
import AccountStatuses from './pages/account-statuses';
|
import AccountStatuses from './pages/account-statuses';
|
||||||
import Accounts from './pages/accounts';
|
|
||||||
import Bookmarks from './pages/bookmarks';
|
import Bookmarks from './pages/bookmarks';
|
||||||
import Favourites from './pages/favourites';
|
import Favourites from './pages/favourites';
|
||||||
import FollowedHashtags from './pages/followed-hashtags';
|
import FollowedHashtags from './pages/followed-hashtags';
|
||||||
|
@ -48,7 +36,6 @@ import Mentions from './pages/mentions';
|
||||||
import Notifications from './pages/notifications';
|
import Notifications from './pages/notifications';
|
||||||
import Public from './pages/public';
|
import Public from './pages/public';
|
||||||
import Search from './pages/search';
|
import Search from './pages/search';
|
||||||
import Settings from './pages/settings';
|
|
||||||
import StatusRoute from './pages/status-route';
|
import StatusRoute from './pages/status-route';
|
||||||
import Trending from './pages/trending';
|
import Trending from './pages/trending';
|
||||||
import Welcome from './pages/welcome';
|
import Welcome from './pages/welcome';
|
||||||
|
@ -60,7 +47,7 @@ import {
|
||||||
initPreferences,
|
initPreferences,
|
||||||
} from './utils/api';
|
} from './utils/api';
|
||||||
import { getAccessToken } from './utils/auth';
|
import { getAccessToken } from './utils/auth';
|
||||||
import showToast from './utils/show-toast';
|
import focusDeck from './utils/focus-deck';
|
||||||
import states, { initStates } from './utils/states';
|
import states, { initStates } from './utils/states';
|
||||||
import store from './utils/store';
|
import store from './utils/store';
|
||||||
import { getCurrentAccount } from './utils/store-utils';
|
import { getCurrentAccount } from './utils/store-utils';
|
||||||
|
@ -85,7 +72,6 @@ function App() {
|
||||||
const snapStates = useSnapshot(states);
|
const snapStates = useSnapshot(states);
|
||||||
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
||||||
const [uiState, setUIState] = useState('loading');
|
const [uiState, setUIState] = useState('loading');
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
const theme = store.local.get('theme');
|
const theme = store.local.get('theme');
|
||||||
|
@ -165,41 +151,9 @@ function App() {
|
||||||
let location = useLocation();
|
let location = useLocation();
|
||||||
states.currentLocation = location.pathname;
|
states.currentLocation = location.pathname;
|
||||||
|
|
||||||
const focusDeck = () => {
|
|
||||||
let timer = setTimeout(() => {
|
|
||||||
const columns = document.getElementById('columns');
|
|
||||||
if (columns) {
|
|
||||||
// Focus first column
|
|
||||||
// columns.querySelector('.deck-container')?.focus?.();
|
|
||||||
} else {
|
|
||||||
const backDrop = document.querySelector('.deck-backdrop');
|
|
||||||
if (backDrop) return;
|
|
||||||
// Focus last deck
|
|
||||||
const pages = document.querySelectorAll('.deck-container');
|
|
||||||
const page = pages[pages.length - 1]; // last one
|
|
||||||
if (page && page.tabIndex === -1) {
|
|
||||||
console.log('FOCUS', page);
|
|
||||||
page.focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
return () => clearTimeout(timer);
|
|
||||||
};
|
|
||||||
useEffect(focusDeck, [location, isLoggedIn]);
|
useEffect(focusDeck, [location, isLoggedIn]);
|
||||||
const showModal =
|
|
||||||
snapStates.showCompose ||
|
|
||||||
snapStates.showSettings ||
|
|
||||||
snapStates.showAccounts ||
|
|
||||||
snapStates.showAccount ||
|
|
||||||
snapStates.showDrafts ||
|
|
||||||
snapStates.showMediaModal ||
|
|
||||||
snapStates.showShortcutsSettings ||
|
|
||||||
snapStates.showKeyboardShortcutsHelp;
|
|
||||||
useEffect(() => {
|
|
||||||
if (!showModal) focusDeck();
|
|
||||||
}, [showModal]);
|
|
||||||
|
|
||||||
const { prevLocation } = snapStates;
|
const prevLocation = snapStates.prevLocation;
|
||||||
const backgroundLocation = useRef(prevLocation || null);
|
const backgroundLocation = useRef(prevLocation || null);
|
||||||
const isModalPage = useMemo(() => {
|
const isModalPage = useMemo(() => {
|
||||||
return (
|
return (
|
||||||
|
@ -230,9 +184,11 @@ function App() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const $app = document.getElementById('app');
|
const $app = document.getElementById('app');
|
||||||
if ($app) {
|
if ($app) {
|
||||||
$app.dataset.shortcutsViewMode = snapStates.settings.shortcutsViewMode;
|
$app.dataset.shortcutsViewMode = snapStates.shortcuts?.length
|
||||||
|
? snapStates.settings.shortcutsViewMode
|
||||||
|
: '';
|
||||||
}
|
}
|
||||||
}, [snapStates.settings.shortcutsViewMode]);
|
}, [snapStates.shortcuts, snapStates.settings.shortcutsViewMode]);
|
||||||
|
|
||||||
// Add/Remove cloak class to body
|
// Add/Remove cloak class to body
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -294,147 +250,7 @@ function App() {
|
||||||
snapStates.settings.shortcutsViewMode !== 'multi-column' && (
|
snapStates.settings.shortcutsViewMode !== 'multi-column' && (
|
||||||
<Shortcuts />
|
<Shortcuts />
|
||||||
)}
|
)}
|
||||||
{!!snapStates.showCompose && (
|
<Modals />
|
||||||
<Modal>
|
|
||||||
<Compose
|
|
||||||
replyToStatus={
|
|
||||||
typeof snapStates.showCompose !== 'boolean'
|
|
||||||
? snapStates.showCompose.replyToStatus
|
|
||||||
: window.__COMPOSE__?.replyToStatus || null
|
|
||||||
}
|
|
||||||
editStatus={
|
|
||||||
states.showCompose?.editStatus ||
|
|
||||||
window.__COMPOSE__?.editStatus ||
|
|
||||||
null
|
|
||||||
}
|
|
||||||
draftStatus={
|
|
||||||
states.showCompose?.draftStatus ||
|
|
||||||
window.__COMPOSE__?.draftStatus ||
|
|
||||||
null
|
|
||||||
}
|
|
||||||
onClose={(results) => {
|
|
||||||
const { newStatus, instance } = results || {};
|
|
||||||
states.showCompose = false;
|
|
||||||
window.__COMPOSE__ = null;
|
|
||||||
if (newStatus) {
|
|
||||||
states.reloadStatusPage++;
|
|
||||||
showToast({
|
|
||||||
text: 'Post published. Check it out.',
|
|
||||||
delay: 1000,
|
|
||||||
duration: 10_000, // 10 seconds
|
|
||||||
onClick: (toast) => {
|
|
||||||
toast.hideToast();
|
|
||||||
states.prevLocation = location;
|
|
||||||
navigate(
|
|
||||||
instance
|
|
||||||
? `/${instance}/s/${newStatus.id}`
|
|
||||||
: `/s/${newStatus.id}`,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
)}
|
|
||||||
{!!snapStates.showSettings && (
|
|
||||||
<Modal
|
|
||||||
onClick={(e) => {
|
|
||||||
if (e.target === e.currentTarget) {
|
|
||||||
states.showSettings = false;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Settings
|
|
||||||
onClose={() => {
|
|
||||||
states.showSettings = false;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
)}
|
|
||||||
{!!snapStates.showAccounts && (
|
|
||||||
<Modal
|
|
||||||
onClick={(e) => {
|
|
||||||
if (e.target === e.currentTarget) {
|
|
||||||
states.showAccounts = false;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Accounts
|
|
||||||
onClose={() => {
|
|
||||||
states.showAccounts = false;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
)}
|
|
||||||
{!!snapStates.showAccount && (
|
|
||||||
<Modal
|
|
||||||
class="light"
|
|
||||||
onClick={(e) => {
|
|
||||||
if (e.target === e.currentTarget) {
|
|
||||||
states.showAccount = false;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<AccountSheet
|
|
||||||
account={snapStates.showAccount?.account || snapStates.showAccount}
|
|
||||||
instance={snapStates.showAccount?.instance}
|
|
||||||
onClose={({ destination } = {}) => {
|
|
||||||
states.showAccount = false;
|
|
||||||
if (destination) {
|
|
||||||
states.showAccounts = false;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
)}
|
|
||||||
{!!snapStates.showDrafts && (
|
|
||||||
<Modal
|
|
||||||
onClick={(e) => {
|
|
||||||
if (e.target === e.currentTarget) {
|
|
||||||
states.showDrafts = false;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Drafts onClose={() => (states.showDrafts = false)} />
|
|
||||||
</Modal>
|
|
||||||
)}
|
|
||||||
{!!snapStates.showMediaModal && (
|
|
||||||
<Modal
|
|
||||||
onClick={(e) => {
|
|
||||||
if (
|
|
||||||
e.target === e.currentTarget ||
|
|
||||||
e.target.classList.contains('media')
|
|
||||||
) {
|
|
||||||
states.showMediaModal = false;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<MediaModal
|
|
||||||
mediaAttachments={snapStates.showMediaModal.mediaAttachments}
|
|
||||||
instance={snapStates.showMediaModal.instance}
|
|
||||||
index={snapStates.showMediaModal.index}
|
|
||||||
statusID={snapStates.showMediaModal.statusID}
|
|
||||||
onClose={() => {
|
|
||||||
states.showMediaModal = false;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
)}
|
|
||||||
{!!snapStates.showShortcutsSettings && (
|
|
||||||
<Modal
|
|
||||||
class="light"
|
|
||||||
onClick={(e) => {
|
|
||||||
if (e.target === e.currentTarget) {
|
|
||||||
states.showShortcutsSettings = false;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ShortcutsSettings
|
|
||||||
onClose={() => (states.showShortcutsSettings = false)}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
)}
|
|
||||||
<NotificationService />
|
<NotificationService />
|
||||||
<BackgroundService isLoggedIn={isLoggedIn} />
|
<BackgroundService isLoggedIn={isLoggedIn} />
|
||||||
<SearchCommand onClose={focusDeck} />
|
<SearchCommand onClose={focusDeck} />
|
||||||
|
|
|
@ -9,6 +9,46 @@
|
||||||
color: var(--outline-color);
|
color: var(--outline-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.account-container .account-moved {
|
||||||
|
animation: fade-in 0.3s both ease-in-out 0.3s;
|
||||||
|
padding: 16px;
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
inset-inline: 8px;
|
||||||
|
z-index: 2;
|
||||||
|
border: 1px solid var(--outline-color);
|
||||||
|
box-shadow: 0 8px 16px var(--drop-shadow-color);
|
||||||
|
border-radius: calc(16px - 8px);
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0 0 8px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-block {
|
||||||
|
background-color: var(--bg-faded-color);
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--link-faded-color);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--link-bg-hover-color);
|
||||||
|
border-color: var(--link-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
b {
|
||||||
|
color: var(--link-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
~ * {
|
||||||
|
/* pointer-events: none; */
|
||||||
|
filter: grayscale(0.75) brightness(0.75);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.account-container .header-banner {
|
.account-container .header-banner {
|
||||||
/* pointer-events: none; */
|
/* pointer-events: none; */
|
||||||
aspect-ratio: 6 / 1;
|
aspect-ratio: 6 / 1;
|
||||||
|
@ -139,15 +179,29 @@
|
||||||
/* flex-wrap: wrap; */
|
/* flex-wrap: wrap; */
|
||||||
column-gap: 24px;
|
column-gap: 24px;
|
||||||
row-gap: 8px;
|
row-gap: 8px;
|
||||||
opacity: 0.75;
|
/* opacity: 0.75; */
|
||||||
font-size: 90%;
|
font-size: 90%;
|
||||||
background-color: var(--bg-faded-color);
|
background-color: var(--bg-faded-color);
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
border-radius: 16px;
|
/* border-radius: 16px; */
|
||||||
line-height: 1.25;
|
line-height: 1.25;
|
||||||
overflow-x: auto;
|
overflow-x: auto !important;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
[tabindex='0']:is(:hover, :focus) {
|
||||||
|
color: var(--text-color);
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration-color: var(--text-insignificant-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-avatars-bunch {
|
||||||
|
animation: appear 1s both ease-in-out;
|
||||||
|
|
||||||
|
> *:not(:first-child) {
|
||||||
|
margin: 0 0 0 -4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.timeline-start .account-container .stats {
|
.timeline-start .account-container .stats {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
@ -158,6 +212,9 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.5em;
|
gap: 0.5em;
|
||||||
}
|
}
|
||||||
|
.account-container .stats a:not(.insignificant) {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
.account-container .stats a:hover {
|
.account-container .stats a:hover {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
@ -176,11 +233,35 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.account-container .account-metadata-box {
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 16px;
|
||||||
|
display: block;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
& > * {
|
||||||
|
margin-bottom: 2px;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:has(+ .account-metadata-box) {
|
||||||
|
border-bottom-left-radius: 4px;
|
||||||
|
border-bottom-right-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
+ .account-metadata-box {
|
||||||
|
border-top-left-radius: 4px;
|
||||||
|
border-top-right-radius: 4px;
|
||||||
|
border-bottom-left-radius: 16px;
|
||||||
|
border-bottom-right-radius: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.account-container .profile-metadata {
|
.account-container .profile-metadata {
|
||||||
display: flex;
|
display: flex;
|
||||||
/* flex-wrap: wrap; */
|
/* flex-wrap: wrap; */
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
border-radius: 16px;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
@ -226,12 +307,11 @@
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.account-container .common-followers p {
|
.account-container .common-followers {
|
||||||
font-size: 90%;
|
font-size: 90%;
|
||||||
color: var(--text-insignificant-color);
|
color: var(--text-insignificant-color);
|
||||||
border-top: 1px solid var(--outline-color);
|
background-color: var(--bg-faded-color);
|
||||||
border-bottom: 1px solid var(--outline-color);
|
padding: 8px 12px;
|
||||||
padding: 8px 0;
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -252,6 +332,84 @@
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes swoosh-bg-image {
|
||||||
|
0% {
|
||||||
|
background-position: -320px 0;
|
||||||
|
opacity: 0.25;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: 0 0;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.account-container .posting-stats {
|
||||||
|
font-size: 90%;
|
||||||
|
color: var(--text-insignificant-color);
|
||||||
|
background-color: var(--bg-faded-color);
|
||||||
|
padding: 8px 12px;
|
||||||
|
--size: 8px;
|
||||||
|
--original-color: var(--link-color);
|
||||||
|
|
||||||
|
&:is(:hover, :focus-within) {
|
||||||
|
background-color: var(--link-bg-hover-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.posting-stats-bar {
|
||||||
|
--gap: 0.5px;
|
||||||
|
--gap-color: var(--outline-color);
|
||||||
|
height: var(--size);
|
||||||
|
border-radius: var(--size);
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 8px 0;
|
||||||
|
box-shadow: inset 0 0 0 1px var(--outline-color),
|
||||||
|
inset 0 0 0 1.5px var(--bg-blur-color);
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
animation: swoosh-bg-image 0.3s ease-in-out 0.3s both;
|
||||||
|
background-image: linear-gradient(
|
||||||
|
to right,
|
||||||
|
var(--original-color) 0%,
|
||||||
|
var(--original-color) calc(var(--originals-percentage) - var(--gap)),
|
||||||
|
var(--gap-color) calc(var(--originals-percentage) - var(--gap)),
|
||||||
|
var(--gap-color) calc(var(--originals-percentage) + var(--gap)),
|
||||||
|
var(--reply-to-color) calc(var(--originals-percentage) + var(--gap)),
|
||||||
|
var(--reply-to-color) calc(var(--replies-percentage) - var(--gap)),
|
||||||
|
var(--gap-color) calc(var(--replies-percentage) - var(--gap)),
|
||||||
|
var(--gap-color) calc(var(--replies-percentage) + var(--gap)),
|
||||||
|
var(--reblog-color) calc(var(--replies-percentage) + var(--gap)),
|
||||||
|
var(--reblog-color) 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.posting-stats-legends {
|
||||||
|
font-size: 12px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.posting-stats-legend-item {
|
||||||
|
display: inline-block;
|
||||||
|
width: var(--size);
|
||||||
|
height: var(--size);
|
||||||
|
border-radius: var(--size);
|
||||||
|
background-color: var(--text-insignificant-color);
|
||||||
|
vertical-align: middle;
|
||||||
|
margin: 0 4px 2px;
|
||||||
|
/* border: 1px solid var(--outline-color); */
|
||||||
|
box-shadow: inset 0 0 0 1px var(--outline-color),
|
||||||
|
inset 0 0 0 1.5px var(--bg-blur-color);
|
||||||
|
|
||||||
|
&.posting-stats-legend-item-originals {
|
||||||
|
background-color: var(--original-color);
|
||||||
|
}
|
||||||
|
&.posting-stats-legend-item-replies {
|
||||||
|
background-color: var(--reply-to-color);
|
||||||
|
}
|
||||||
|
&.posting-stats-legend-item-boosts {
|
||||||
|
background-color: var(--reblog-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes shine {
|
@keyframes shine {
|
||||||
0% {
|
0% {
|
||||||
left: -100%;
|
left: -100%;
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import './account-info.css';
|
import './account-info.css';
|
||||||
|
|
||||||
import { Menu, MenuDivider, MenuItem, SubMenu } from '@szhsin/react-menu';
|
import { Menu, MenuDivider, MenuItem, SubMenu } from '@szhsin/react-menu';
|
||||||
import { useEffect, useReducer, useRef, useState } from 'preact/hooks';
|
import { useEffect, useMemo, useReducer, useRef, useState } from 'preact/hooks';
|
||||||
|
import { proxy, useSnapshot } from 'valtio';
|
||||||
|
|
||||||
import { api } from '../utils/api';
|
import { api } from '../utils/api';
|
||||||
import enhanceContent from '../utils/enhance-content';
|
import enhanceContent from '../utils/enhance-content';
|
||||||
|
@ -46,6 +47,12 @@ const MUTE_DURATIONS_LABELS = {
|
||||||
604_800_000: '1 week',
|
604_800_000: '1 week',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const LIMIT = 80;
|
||||||
|
|
||||||
|
const accountInfoStates = proxy({
|
||||||
|
familiarFollowers: [],
|
||||||
|
});
|
||||||
|
|
||||||
function AccountInfo({
|
function AccountInfo({
|
||||||
account,
|
account,
|
||||||
fetchAccount = () => {},
|
fetchAccount = () => {},
|
||||||
|
@ -53,9 +60,23 @@ function AccountInfo({
|
||||||
instance,
|
instance,
|
||||||
authenticated,
|
authenticated,
|
||||||
}) {
|
}) {
|
||||||
|
const { masto } = api({
|
||||||
|
instance,
|
||||||
|
});
|
||||||
const [uiState, setUIState] = useState('default');
|
const [uiState, setUIState] = useState('default');
|
||||||
const isString = typeof account === 'string';
|
const isString = typeof account === 'string';
|
||||||
const [info, setInfo] = useState(isString ? null : account);
|
const [info, setInfo] = useState(isString ? null : account);
|
||||||
|
const snapAccountInfoStates = useSnapshot(accountInfoStates);
|
||||||
|
|
||||||
|
const isSelf = useMemo(
|
||||||
|
() => account.id === store.session.get('currentAccount'),
|
||||||
|
[account?.id],
|
||||||
|
);
|
||||||
|
|
||||||
|
const sameCurrentInstance = useMemo(
|
||||||
|
() => instance === api().instance,
|
||||||
|
[instance],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isString) {
|
if (!isString) {
|
||||||
|
@ -99,6 +120,7 @@ function AccountInfo({
|
||||||
url,
|
url,
|
||||||
username,
|
username,
|
||||||
memorial,
|
memorial,
|
||||||
|
moved,
|
||||||
} = info || {};
|
} = info || {};
|
||||||
let headerIsAvatar = false;
|
let headerIsAvatar = false;
|
||||||
let { header, headerStatic } = info || {};
|
let { header, headerStatic } = info || {};
|
||||||
|
@ -114,6 +136,65 @@ function AccountInfo({
|
||||||
|
|
||||||
const [headerCornerColors, setHeaderCornerColors] = useState([]);
|
const [headerCornerColors, setHeaderCornerColors] = useState([]);
|
||||||
|
|
||||||
|
const followersIterator = useRef();
|
||||||
|
const familiarFollowersCache = useRef([]);
|
||||||
|
async function fetchFollowers(firstLoad) {
|
||||||
|
if (firstLoad || !followersIterator.current) {
|
||||||
|
followersIterator.current = masto.v1.accounts.listFollowers(id, {
|
||||||
|
limit: LIMIT,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const results = await followersIterator.current.next();
|
||||||
|
if (isSelf) return results;
|
||||||
|
if (!sameCurrentInstance) return results;
|
||||||
|
|
||||||
|
const { value } = results;
|
||||||
|
let newValue = [];
|
||||||
|
// 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.fetchFamiliarFollowers(
|
||||||
|
id,
|
||||||
|
);
|
||||||
|
familiarFollowersCache.current = familiarFollowers[0].accounts;
|
||||||
|
newValue = [
|
||||||
|
...familiarFollowersCache.current,
|
||||||
|
...value.filter(
|
||||||
|
(account) =>
|
||||||
|
!familiarFollowersCache.current.some(
|
||||||
|
(familiar) => familiar.id === account.id,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
} else if (value?.length) {
|
||||||
|
newValue = value.filter(
|
||||||
|
(account) =>
|
||||||
|
!familiarFollowersCache.current.some(
|
||||||
|
(familiar) => familiar.id === account.id,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...results,
|
||||||
|
value: newValue,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const followingIterator = useRef();
|
||||||
|
async function fetchFollowing(firstLoad) {
|
||||||
|
if (firstLoad || !followingIterator.current) {
|
||||||
|
followingIterator.current = masto.v1.accounts.listFollowing(id, {
|
||||||
|
limit: LIMIT,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const results = await followingIterator.current.next();
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LinkOrDiv = standalone ? 'div' : Link;
|
||||||
|
const accountLink = instance ? `/${instance}/a/${id}` : `/a/${id}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
class={`account-container ${uiState === 'loading' ? 'skeleton' : ''}`}
|
class={`account-container ${uiState === 'loading' ? 'skeleton' : ''}`}
|
||||||
|
@ -128,7 +209,11 @@ function AccountInfo({
|
||||||
<div class="ui-state">
|
<div class="ui-state">
|
||||||
<p>Unable to load account.</p>
|
<p>Unable to load account.</p>
|
||||||
<p>
|
<p>
|
||||||
<a href={account} target="_blank">
|
<a
|
||||||
|
href={isString ? account : url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
Go to account page <Icon icon="external" />
|
Go to account page <Icon icon="external" />
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
@ -161,6 +246,22 @@ function AccountInfo({
|
||||||
) : (
|
) : (
|
||||||
info && (
|
info && (
|
||||||
<>
|
<>
|
||||||
|
{!!moved && (
|
||||||
|
<div class="account-moved">
|
||||||
|
<p>
|
||||||
|
<b>{displayName}</b> has indicated that their new account is
|
||||||
|
now:
|
||||||
|
</p>
|
||||||
|
<AccountBlock
|
||||||
|
account={moved}
|
||||||
|
instance={instance}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
states.showAccount = moved;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{header && !/missing\.png$/.test(header) && (
|
{header && !/missing\.png$/.test(header) && (
|
||||||
<img
|
<img
|
||||||
src={header}
|
src={header}
|
||||||
|
@ -289,6 +390,7 @@ function AccountInfo({
|
||||||
__html: enhanceContent(note, { emojis }),
|
__html: enhanceContent(note, { emojis }),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<div class="account-metadata-box">
|
||||||
{fields?.length > 0 && (
|
{fields?.length > 0 && (
|
||||||
<div class="profile-metadata">
|
<div class="profile-metadata">
|
||||||
{fields.map(({ name, value, verifiedAt }, i) => (
|
{fields.map(({ name, value, verifiedAt }, i) => (
|
||||||
|
@ -300,7 +402,9 @@ function AccountInfo({
|
||||||
>
|
>
|
||||||
<b>
|
<b>
|
||||||
<EmojiText text={name} emojis={emojis} />{' '}
|
<EmojiText text={name} emojis={emojis} />{' '}
|
||||||
{!!verifiedAt && <Icon icon="check-circle" size="s" />}
|
{!!verifiedAt && (
|
||||||
|
<Icon icon="check-circle" size="s" />
|
||||||
|
)}
|
||||||
</b>
|
</b>
|
||||||
<p
|
<p
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
|
@ -311,41 +415,73 @@ function AccountInfo({
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<p class="stats">
|
<div class="stats">
|
||||||
<div>
|
<LinkOrDiv
|
||||||
|
tabIndex={0}
|
||||||
|
to={accountLink}
|
||||||
|
onClick={() => {
|
||||||
|
states.showAccount = false;
|
||||||
|
states.showGenericAccounts = {
|
||||||
|
heading: 'Followers',
|
||||||
|
fetchAccounts: fetchFollowers,
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{!!snapAccountInfoStates.familiarFollowers.length && (
|
||||||
|
<span class="shazam-container-horizontal">
|
||||||
|
<span class="shazam-container-inner stats-avatars-bunch">
|
||||||
|
{(snapAccountInfoStates.familiarFollowers || []).map(
|
||||||
|
(follower) => (
|
||||||
|
<Avatar
|
||||||
|
url={follower.avatarStatic}
|
||||||
|
size="s"
|
||||||
|
alt={`${follower.displayName} @${follower.acct}`}
|
||||||
|
squircle={follower?.bot}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
<span title={followersCount}>
|
<span title={followersCount}>
|
||||||
{shortenNumber(followersCount)}
|
{shortenNumber(followersCount)}
|
||||||
</span>{' '}
|
</span>{' '}
|
||||||
Followers
|
Followers
|
||||||
</div>
|
</LinkOrDiv>
|
||||||
<div class="insignificant">
|
<LinkOrDiv
|
||||||
|
class="insignificant"
|
||||||
|
tabIndex={0}
|
||||||
|
to={accountLink}
|
||||||
|
onClick={() => {
|
||||||
|
states.showAccount = false;
|
||||||
|
states.showGenericAccounts = {
|
||||||
|
heading: 'Following',
|
||||||
|
fetchAccounts: fetchFollowing,
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
>
|
||||||
<span title={followingCount}>
|
<span title={followingCount}>
|
||||||
{shortenNumber(followingCount)}
|
{shortenNumber(followingCount)}
|
||||||
</span>{' '}
|
</span>{' '}
|
||||||
Following
|
Following
|
||||||
<br />
|
<br />
|
||||||
</div>
|
</LinkOrDiv>
|
||||||
{standalone ? (
|
<LinkOrDiv
|
||||||
<div class="insignificant">
|
|
||||||
<span title={statusesCount}>
|
|
||||||
{shortenNumber(statusesCount)}
|
|
||||||
</span>{' '}
|
|
||||||
Posts
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<Link
|
|
||||||
class="insignificant"
|
class="insignificant"
|
||||||
to={instance ? `/${instance}/a/${id}` : `/a/${id}`}
|
to={accountLink}
|
||||||
onClick={() => {
|
onClick={
|
||||||
|
standalone
|
||||||
|
? undefined
|
||||||
|
: () => {
|
||||||
hideAllModals();
|
hideAllModals();
|
||||||
}}
|
}
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<span title={statusesCount}>
|
<span title={statusesCount}>
|
||||||
{shortenNumber(statusesCount)}
|
{shortenNumber(statusesCount)}
|
||||||
</span>{' '}
|
</span>{' '}
|
||||||
Posts
|
Posts
|
||||||
</Link>
|
</LinkOrDiv>
|
||||||
)}
|
|
||||||
{!!createdAt && (
|
{!!createdAt && (
|
||||||
<div class="insignificant">
|
<div class="insignificant">
|
||||||
Joined{' '}
|
Joined{' '}
|
||||||
|
@ -356,11 +492,13 @@ function AccountInfo({
|
||||||
</time>
|
</time>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</p>
|
</div>
|
||||||
|
</div>
|
||||||
<RelatedActions
|
<RelatedActions
|
||||||
info={info}
|
info={info}
|
||||||
instance={instance}
|
instance={instance}
|
||||||
authenticated={authenticated}
|
authenticated={authenticated}
|
||||||
|
standalone={standalone}
|
||||||
/>
|
/>
|
||||||
</main>
|
</main>
|
||||||
</>
|
</>
|
||||||
|
@ -370,7 +508,9 @@ function AccountInfo({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function RelatedActions({ info, instance, authenticated }) {
|
const FAMILIAR_FOLLOWERS_LIMIT = 3;
|
||||||
|
|
||||||
|
function RelatedActions({ info, instance, authenticated, standalone }) {
|
||||||
if (!info) return null;
|
if (!info) return null;
|
||||||
const {
|
const {
|
||||||
masto: currentMasto,
|
masto: currentMasto,
|
||||||
|
@ -381,9 +521,10 @@ function RelatedActions({ info, instance, authenticated }) {
|
||||||
|
|
||||||
const [relationshipUIState, setRelationshipUIState] = useState('default');
|
const [relationshipUIState, setRelationshipUIState] = useState('default');
|
||||||
const [relationship, setRelationship] = useState(null);
|
const [relationship, setRelationship] = useState(null);
|
||||||
const [familiarFollowers, setFamiliarFollowers] = useState([]);
|
const [postingStats, setPostingStats] = useState();
|
||||||
|
|
||||||
const { id, acct, url, username, locked, lastStatusAt, note, fields } = info;
|
const { id, acct, url, username, locked, lastStatusAt, note, fields, moved } =
|
||||||
|
info;
|
||||||
const accountID = useRef(id);
|
const accountID = useRef(id);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
@ -440,33 +581,82 @@ function RelatedActions({ info, instance, authenticated }) {
|
||||||
|
|
||||||
accountID.current = currentID;
|
accountID.current = currentID;
|
||||||
|
|
||||||
|
if (moved) return;
|
||||||
|
|
||||||
setRelationshipUIState('loading');
|
setRelationshipUIState('loading');
|
||||||
setFamiliarFollowers([]);
|
accountInfoStates.familiarFollowers = [];
|
||||||
|
setPostingStats(null);
|
||||||
|
|
||||||
const fetchRelationships = currentMasto.v1.accounts.fetchRelationships([
|
const fetchRelationships = currentMasto.v1.accounts.fetchRelationships([
|
||||||
currentID,
|
currentID,
|
||||||
]);
|
]);
|
||||||
const fetchFamiliarFollowers =
|
|
||||||
currentMasto.v1.accounts.fetchFamiliarFollowers(currentID);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const relationships = await fetchRelationships;
|
const relationships = await fetchRelationships;
|
||||||
console.log('fetched relationship', relationships);
|
console.log('fetched relationship', relationships);
|
||||||
|
setRelationshipUIState('default');
|
||||||
|
|
||||||
if (relationships.length) {
|
if (relationships.length) {
|
||||||
const relationship = relationships[0];
|
const relationship = relationships[0];
|
||||||
setRelationship(relationship);
|
setRelationship(relationship);
|
||||||
|
|
||||||
if (!relationship.following) {
|
if (!relationship.following) {
|
||||||
try {
|
try {
|
||||||
|
const fetchFamiliarFollowers =
|
||||||
|
currentMasto.v1.accounts.fetchFamiliarFollowers(currentID);
|
||||||
|
const fetchStatuses = currentMasto.v1.accounts
|
||||||
|
.listStatuses(currentID, {
|
||||||
|
limit: 20,
|
||||||
|
})
|
||||||
|
.next();
|
||||||
|
|
||||||
const followers = await fetchFamiliarFollowers;
|
const followers = await fetchFamiliarFollowers;
|
||||||
console.log('fetched familiar followers', followers);
|
console.log('fetched familiar followers', followers);
|
||||||
setFamiliarFollowers(followers[0].accounts.slice(0, 10));
|
accountInfoStates.familiarFollowers =
|
||||||
|
followers[0].accounts.slice(0, FAMILIAR_FOLLOWERS_LIMIT);
|
||||||
|
|
||||||
|
if (!standalone) {
|
||||||
|
const { value: statuses } = await fetchStatuses;
|
||||||
|
console.log('fetched statuses', statuses);
|
||||||
|
const stats = {
|
||||||
|
total: statuses.length,
|
||||||
|
originals: 0,
|
||||||
|
replies: 0,
|
||||||
|
boosts: 0,
|
||||||
|
};
|
||||||
|
// Categories statuses by type
|
||||||
|
// - Original posts (not replies to others)
|
||||||
|
// - Threads (self-replies + 1st original post)
|
||||||
|
// - Boosts (reblogs)
|
||||||
|
// - Replies (not-self replies)
|
||||||
|
statuses.forEach((status) => {
|
||||||
|
if (status.reblog) {
|
||||||
|
stats.boosts++;
|
||||||
|
} else if (
|
||||||
|
status.inReplyToAccountId !== currentID &&
|
||||||
|
!!status.inReplyToId
|
||||||
|
) {
|
||||||
|
stats.replies++;
|
||||||
|
} else {
|
||||||
|
stats.originals++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Count days since last post
|
||||||
|
stats.daysSinceLastPost = Math.ceil(
|
||||||
|
(Date.now() -
|
||||||
|
new Date(statuses[statuses.length - 1].createdAt)) /
|
||||||
|
86400000,
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('posting stats', stats);
|
||||||
|
setPostingStats(stats);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setRelationshipUIState('default');
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
setRelationshipUIState('error');
|
setRelationshipUIState('error');
|
||||||
|
@ -487,40 +677,74 @@ function RelatedActions({ info, instance, authenticated }) {
|
||||||
const [showTranslatedBio, setShowTranslatedBio] = useState(false);
|
const [showTranslatedBio, setShowTranslatedBio] = useState(false);
|
||||||
const [showAddRemoveLists, setShowAddRemoveLists] = useState(false);
|
const [showAddRemoveLists, setShowAddRemoveLists] = useState(false);
|
||||||
|
|
||||||
|
const hasPostingStats = postingStats?.total >= 3;
|
||||||
|
const accountLink = instance ? `/${instance}/a/${id}` : `/a/${id}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
{hasPostingStats && (
|
||||||
class="common-followers shazam-container no-animation"
|
<Link
|
||||||
hidden={!familiarFollowers?.length}
|
to={accountLink}
|
||||||
>
|
class="account-metadata-box"
|
||||||
<div class="shazam-container-inner">
|
onClick={() => {
|
||||||
<p>
|
states.showAccount = false;
|
||||||
Followed by{' '}
|
|
||||||
<span class="ib">
|
|
||||||
{familiarFollowers.map((follower) => (
|
|
||||||
<a
|
|
||||||
href={follower.url}
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
states.showAccount = {
|
|
||||||
account: follower,
|
|
||||||
instance,
|
|
||||||
};
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Avatar
|
<div class="shazam-container">
|
||||||
url={follower.avatarStatic}
|
<div class="shazam-container-inner">
|
||||||
size="l"
|
<div
|
||||||
alt={`${follower.displayName} @${follower.acct}`}
|
class="posting-stats"
|
||||||
squircle={follower?.bot}
|
title={`${Math.round(
|
||||||
|
(postingStats.originals / postingStats.total) * 100,
|
||||||
|
)}% original posts, ${Math.round(
|
||||||
|
(postingStats.replies / postingStats.total) * 100,
|
||||||
|
)}% replies, ${Math.round(
|
||||||
|
(postingStats.boosts / postingStats.total) * 100,
|
||||||
|
)}% boosts`}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
{postingStats.daysSinceLastPost < 365
|
||||||
|
? `Last ${postingStats.total} posts in the past
|
||||||
|
${postingStats.daysSinceLastPost} day${
|
||||||
|
postingStats.daysSinceLastPost > 1 ? 's' : ''
|
||||||
|
}`
|
||||||
|
: `
|
||||||
|
Last ${postingStats.total} posts in the past year(s)
|
||||||
|
`}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="posting-stats-bar"
|
||||||
|
style={{
|
||||||
|
// [originals | replies | boosts]
|
||||||
|
'--originals-percentage': `${
|
||||||
|
(postingStats.originals / postingStats.total) * 100
|
||||||
|
}%`,
|
||||||
|
'--replies-percentage': `${
|
||||||
|
((postingStats.originals + postingStats.replies) /
|
||||||
|
postingStats.total) *
|
||||||
|
100
|
||||||
|
}%`,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</a>
|
<div class="posting-stats-legends">
|
||||||
))}
|
<span class="ib">
|
||||||
|
<span class="posting-stats-legend-item posting-stats-legend-item-originals" />{' '}
|
||||||
|
Original
|
||||||
|
</span>{' '}
|
||||||
|
<span class="ib">
|
||||||
|
<span class="posting-stats-legend-item posting-stats-legend-item-replies" />{' '}
|
||||||
|
Replies
|
||||||
|
</span>{' '}
|
||||||
|
<span class="ib">
|
||||||
|
<span class="posting-stats-legend-item posting-stats-legend-item-boosts" />{' '}
|
||||||
|
Boosts
|
||||||
</span>
|
</span>
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
<p class="actions">
|
<p class="actions">
|
||||||
<span>
|
<span>
|
||||||
{followedBy ? (
|
{followedBy ? (
|
||||||
|
@ -876,10 +1100,8 @@ function RelatedActions({ info, instance, authenticated }) {
|
||||||
{!!showTranslatedBio && (
|
{!!showTranslatedBio && (
|
||||||
<Modal
|
<Modal
|
||||||
class="light"
|
class="light"
|
||||||
onClick={(e) => {
|
onClose={() => {
|
||||||
if (e.target === e.currentTarget) {
|
|
||||||
setShowTranslatedBio(false);
|
setShowTranslatedBio(false);
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TranslatedBioSheet
|
<TranslatedBioSheet
|
||||||
|
@ -892,10 +1114,8 @@ function RelatedActions({ info, instance, authenticated }) {
|
||||||
{!!showAddRemoveLists && (
|
{!!showAddRemoveLists && (
|
||||||
<Modal
|
<Modal
|
||||||
class="light"
|
class="light"
|
||||||
onClick={(e) => {
|
onClose={() => {
|
||||||
if (e.target === e.currentTarget) {
|
|
||||||
setShowAddRemoveLists(false);
|
setShowAddRemoveLists(false);
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<AddRemoveListsSheet
|
<AddRemoveListsSheet
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { useEffect } from 'preact/hooks';
|
import { useEffect } from 'preact/hooks';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
|
||||||
|
|
||||||
import { api } from '../utils/api';
|
import { api } from '../utils/api';
|
||||||
import states from '../utils/states';
|
import states from '../utils/states';
|
||||||
|
@ -11,8 +10,6 @@ function AccountSheet({ account, instance: propInstance, onClose }) {
|
||||||
const { masto, instance, authenticated } = api({ instance: propInstance });
|
const { masto, instance, authenticated } = api({ instance: propInstance });
|
||||||
const isString = typeof account === 'string';
|
const isString = typeof account === 'string';
|
||||||
|
|
||||||
const escRef = useHotkeys('esc', onClose, [onClose]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isString) {
|
if (!isString) {
|
||||||
states.accounts[`${account.id}@${instance}`] = account;
|
states.accounts[`${account.id}@${instance}`] = account;
|
||||||
|
@ -21,7 +18,6 @@ function AccountSheet({ account, instance: propInstance, onClose }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={escRef}
|
|
||||||
class="sheet"
|
class="sheet"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
const accountBlock = e.target.closest('.account-block');
|
const accountBlock = e.target.closest('.account-block');
|
||||||
|
|
|
@ -931,6 +931,14 @@ function Compose({
|
||||||
}}
|
}}
|
||||||
maxCharacters={maxCharacters}
|
maxCharacters={maxCharacters}
|
||||||
performSearch={(params) => {
|
performSearch={(params) => {
|
||||||
|
const { type, q, limit } = params;
|
||||||
|
if (type === 'accounts') {
|
||||||
|
return masto.v1.accounts.search({
|
||||||
|
q,
|
||||||
|
limit,
|
||||||
|
resolve: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
return masto.v2.search(params);
|
return masto.v2.search(params);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -1175,6 +1183,17 @@ function Compose({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function autoResizeTextarea(textarea) {
|
||||||
|
if (!textarea) return;
|
||||||
|
const { value, offsetHeight, scrollHeight, clientHeight } = textarea;
|
||||||
|
if (offsetHeight < window.innerHeight) {
|
||||||
|
// NOTE: This check is needed because the offsetHeight return 50000 (really large number) on first render
|
||||||
|
// No idea why it does that, will re-investigate in far future
|
||||||
|
const offset = offsetHeight - clientHeight;
|
||||||
|
textarea.style.height = value ? scrollHeight + offset + 'px' : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const Textarea = forwardRef((props, ref) => {
|
const Textarea = forwardRef((props, ref) => {
|
||||||
const { masto } = api();
|
const { masto } = api();
|
||||||
const [text, setText] = useState(ref.current?.value || '');
|
const [text, setText] = useState(ref.current?.value || '');
|
||||||
|
@ -1258,7 +1277,7 @@ const Textarea = forwardRef((props, ref) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log({ value, type, v: value[type] });
|
console.log({ value, type, v: value[type] });
|
||||||
const results = value[type];
|
const results = value[type] || value;
|
||||||
console.log('RESULTS', value, results);
|
console.log('RESULTS', value, results);
|
||||||
let html = '';
|
let html = '';
|
||||||
results.forEach((result) => {
|
results.forEach((result) => {
|
||||||
|
@ -1367,15 +1386,46 @@ const Textarea = forwardRef((props, ref) => {
|
||||||
ref={ref}
|
ref={ref}
|
||||||
name="status"
|
name="status"
|
||||||
value={text}
|
value={text}
|
||||||
onInput={(e) => {
|
onKeyDown={(e) => {
|
||||||
const { scrollHeight, offsetHeight, clientHeight, value } = e.target;
|
// Get line before cursor position after pressing 'Enter'
|
||||||
setText(value);
|
const { key, target } = e;
|
||||||
if (offsetHeight < window.innerHeight) {
|
if (key === 'Enter') {
|
||||||
// NOTE: This check is needed because the offsetHeight return 50000 (really large number) on first render
|
try {
|
||||||
// No idea why it does that, will re-investigate in far future
|
const { value, selectionStart } = target;
|
||||||
const offset = offsetHeight - clientHeight;
|
const textBeforeCursor = value.slice(0, selectionStart);
|
||||||
e.target.style.height = value ? scrollHeight + offset + 'px' : null;
|
const lastLine = textBeforeCursor.split('\n').slice(-1)[0];
|
||||||
|
if (lastLine) {
|
||||||
|
// If line starts with "- " or "12. "
|
||||||
|
if (/^\s*(-|\d+\.)\s/.test(lastLine)) {
|
||||||
|
// insert "- " at cursor position
|
||||||
|
const [_, preSpaces, bullet, postSpaces, anything] =
|
||||||
|
lastLine.match(/^(\s*)(-|\d+\.)(\s+)(.+)?/) || [];
|
||||||
|
if (anything) {
|
||||||
|
e.preventDefault();
|
||||||
|
const [number] = bullet.match(/\d+/) || [];
|
||||||
|
const newBullet = number ? `${+number + 1}.` : '-';
|
||||||
|
const text = `\n${preSpaces}${newBullet}${postSpaces}`;
|
||||||
|
target.setRangeText(text, selectionStart, selectionStart);
|
||||||
|
const pos = selectionStart + text.length;
|
||||||
|
target.setSelectionRange(pos, pos);
|
||||||
|
} else {
|
||||||
|
// trim the line before the cursor, then insert new line
|
||||||
|
const pos = selectionStart - lastLine.length;
|
||||||
|
target.setRangeText('', pos, selectionStart);
|
||||||
}
|
}
|
||||||
|
autoResizeTextarea(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// silent fail
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onInput={(e) => {
|
||||||
|
const { target } = e;
|
||||||
|
setText(target.value);
|
||||||
|
autoResizeTextarea(target);
|
||||||
props.onInput?.(e);
|
props.onInput?.(e);
|
||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
|
@ -1851,7 +1901,15 @@ function CustomEmojisModal({
|
||||||
}}
|
}}
|
||||||
title={`:${emoji.shortcode}:`}
|
title={`:${emoji.shortcode}:`}
|
||||||
>
|
>
|
||||||
|
<picture>
|
||||||
|
{!!emoji.staticUrl && (
|
||||||
|
<source
|
||||||
|
srcset={emoji.staticUrl}
|
||||||
|
media="(prefers-reduced-motion: reduce)"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<img
|
<img
|
||||||
|
class="shortcode-emoji"
|
||||||
src={emoji.url || emoji.staticUrl}
|
src={emoji.url || emoji.staticUrl}
|
||||||
alt={emoji.shortcode}
|
alt={emoji.shortcode}
|
||||||
width="16"
|
width="16"
|
||||||
|
@ -1859,6 +1917,7 @@ function CustomEmojisModal({
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
decoding="async"
|
decoding="async"
|
||||||
/>
|
/>
|
||||||
|
</picture>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</section>
|
</section>
|
||||||
|
|
|
@ -18,8 +18,8 @@ function EmojiText({ text, emojis }) {
|
||||||
src={url}
|
src={url}
|
||||||
alt={word}
|
alt={word}
|
||||||
class="shortcode-emoji emoji"
|
class="shortcode-emoji emoji"
|
||||||
width="12"
|
width="16"
|
||||||
height="12"
|
height="16"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
decoding="async"
|
decoding="async"
|
||||||
/>
|
/>
|
||||||
|
|
42
src/components/generic-accounts.css
Normal file
42
src/components/generic-accounts.css
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
#generic-accounts-container {
|
||||||
|
.accounts-list {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 8px 0;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
flex-direction: row;
|
||||||
|
column-gap: 1.5em;
|
||||||
|
row-gap: 16px;
|
||||||
|
|
||||||
|
li {
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-basis: 16em;
|
||||||
|
align-items: center;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-block-acct {
|
||||||
|
font-size: 80%;
|
||||||
|
color: var(--text-insignificant-color);
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.reactions-block {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-self: center;
|
||||||
|
|
||||||
|
.favourite-icon {
|
||||||
|
color: var(--favourite-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reblog-icon {
|
||||||
|
color: var(--reblog-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
136
src/components/generic-accounts.jsx
Normal file
136
src/components/generic-accounts.jsx
Normal file
|
@ -0,0 +1,136 @@
|
||||||
|
import './generic-accounts.css';
|
||||||
|
|
||||||
|
import { useEffect, useState } from 'preact/hooks';
|
||||||
|
import { InView } from 'react-intersection-observer';
|
||||||
|
import { useSnapshot } from 'valtio';
|
||||||
|
|
||||||
|
import states from '../utils/states';
|
||||||
|
|
||||||
|
import AccountBlock from './account-block';
|
||||||
|
import Icon from './icon';
|
||||||
|
import Loader from './loader';
|
||||||
|
|
||||||
|
export default function GenericAccounts({ onClose = () => {} }) {
|
||||||
|
const snapStates = useSnapshot(states);
|
||||||
|
const [uiState, setUIState] = useState('default');
|
||||||
|
const [accounts, setAccounts] = useState([]);
|
||||||
|
const [showMore, setShowMore] = useState(false);
|
||||||
|
|
||||||
|
if (!snapStates.showGenericAccounts) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
heading,
|
||||||
|
fetchAccounts,
|
||||||
|
accounts: staticAccounts,
|
||||||
|
showReactions,
|
||||||
|
} = snapStates.showGenericAccounts;
|
||||||
|
|
||||||
|
const loadAccounts = (firstLoad) => {
|
||||||
|
if (!fetchAccounts) return;
|
||||||
|
if (firstLoad) setAccounts([]);
|
||||||
|
setUIState('loading');
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const { done, value } = await fetchAccounts(firstLoad);
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
if (firstLoad) {
|
||||||
|
setAccounts(value);
|
||||||
|
} else {
|
||||||
|
setAccounts((prev) => [...prev, ...value]);
|
||||||
|
}
|
||||||
|
setShowMore(!done);
|
||||||
|
} else {
|
||||||
|
setShowMore(false);
|
||||||
|
}
|
||||||
|
setUIState('default');
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
setUIState('error');
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (staticAccounts?.length > 0) {
|
||||||
|
setAccounts(staticAccounts);
|
||||||
|
} else {
|
||||||
|
loadAccounts(true);
|
||||||
|
}
|
||||||
|
}, [staticAccounts, fetchAccounts]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div id="generic-accounts-container" class="sheet" tabindex="-1">
|
||||||
|
<button type="button" class="sheet-close" onClick={onClose}>
|
||||||
|
<Icon icon="x" />
|
||||||
|
</button>
|
||||||
|
<header>
|
||||||
|
<h2>{heading || 'Accounts'}</h2>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
{accounts.length > 0 ? (
|
||||||
|
<>
|
||||||
|
<ul class="accounts-list">
|
||||||
|
{accounts.map((account) => (
|
||||||
|
<li key={account.id + (account._types || '')}>
|
||||||
|
{showReactions && account._types?.length > 0 && (
|
||||||
|
<div class="reactions-block">
|
||||||
|
{account._types.map((type) => (
|
||||||
|
<Icon
|
||||||
|
icon={
|
||||||
|
{
|
||||||
|
reblog: 'rocket',
|
||||||
|
favourite: 'heart',
|
||||||
|
}[type]
|
||||||
|
}
|
||||||
|
class={`${type}-icon`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<AccountBlock account={account} />
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
{uiState === 'default' ? (
|
||||||
|
showMore ? (
|
||||||
|
<InView
|
||||||
|
onChange={(inView) => {
|
||||||
|
if (inView) {
|
||||||
|
loadAccounts();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="plain block"
|
||||||
|
onClick={() => loadAccounts()}
|
||||||
|
>
|
||||||
|
Show more…
|
||||||
|
</button>
|
||||||
|
</InView>
|
||||||
|
) : (
|
||||||
|
<p class="ui-state insignificant">The end.</p>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
uiState === 'loading' && (
|
||||||
|
<p class="ui-state">
|
||||||
|
<Loader abrupt />
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : uiState === 'loading' ? (
|
||||||
|
<p class="ui-state">
|
||||||
|
<Loader abrupt />
|
||||||
|
</p>
|
||||||
|
) : uiState === 'error' ? (
|
||||||
|
<p class="ui-state">Error loading accounts</p>
|
||||||
|
) : (
|
||||||
|
<p class="ui-state insignificant">Nothing to show</p>
|
||||||
|
)}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -45,6 +45,7 @@ export const ICONS = {
|
||||||
plus: () => import('@iconify-icons/mingcute/add-circle-line'),
|
plus: () => import('@iconify-icons/mingcute/add-circle-line'),
|
||||||
'chevron-left': () => import('@iconify-icons/mingcute/left-line'),
|
'chevron-left': () => import('@iconify-icons/mingcute/left-line'),
|
||||||
'chevron-right': () => import('@iconify-icons/mingcute/right-line'),
|
'chevron-right': () => import('@iconify-icons/mingcute/right-line'),
|
||||||
|
'chevron-down': () => import('@iconify-icons/mingcute/down-line'),
|
||||||
reply: [
|
reply: [
|
||||||
() => import('@iconify-icons/mingcute/share-forward-line'),
|
() => import('@iconify-icons/mingcute/share-forward-line'),
|
||||||
'180deg',
|
'180deg',
|
||||||
|
|
|
@ -39,6 +39,10 @@ const Link = forwardRef((props, ref) => {
|
||||||
{...restProps}
|
{...restProps}
|
||||||
class={`${props.class || ''} ${isActive ? 'is-active' : ''}`}
|
class={`${props.class || ''} ${isActive ? 'is-active' : ''}`}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
|
if (e.currentTarget?.parentNode?.closest('a')) {
|
||||||
|
// If this <a> is nested inside another <a>
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
if (routerLocation) states.prevLocation = routerLocation;
|
if (routerLocation) states.prevLocation = routerLocation;
|
||||||
props.onClick?.(e);
|
props.onClick?.(e);
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { getBlurHashAverageColor } from 'fast-blurhash';
|
import { getBlurHashAverageColor } from 'fast-blurhash';
|
||||||
|
import { Fragment } from 'preact';
|
||||||
import {
|
import {
|
||||||
useCallback,
|
useCallback,
|
||||||
useLayoutEffect,
|
useLayoutEffect,
|
||||||
|
@ -103,7 +104,11 @@ function Media({ media, to, showOriginal, autoAnimate, onClick = () => {} }) {
|
||||||
[to],
|
[to],
|
||||||
);
|
);
|
||||||
|
|
||||||
const isImage = type === 'image' || (type === 'unknown' && previewUrl);
|
const isVideoMaybe =
|
||||||
|
type === 'unknown' &&
|
||||||
|
/\.(mp4|m4a|m4p|m4b|m4r|m4v|mov|webm)$/i.test(remoteMediaURL);
|
||||||
|
const isImage =
|
||||||
|
type === 'image' || (type === 'unknown' && previewUrl && !isVideoMaybe);
|
||||||
|
|
||||||
const parentRef = useRef();
|
const parentRef = useRef();
|
||||||
const [imageSmallerThanParent, setImageSmallerThanParent] = useState(false);
|
const [imageSmallerThanParent, setImageSmallerThanParent] = useState(false);
|
||||||
|
@ -221,7 +226,7 @@ function Media({ media, to, showOriginal, autoAnimate, onClick = () => {} }) {
|
||||||
)}
|
)}
|
||||||
</Parent>
|
</Parent>
|
||||||
);
|
);
|
||||||
} else if (type === 'gifv' || type === 'video') {
|
} else if (type === 'gifv' || type === 'video' || isVideoMaybe) {
|
||||||
const shortDuration = original.duration < 31;
|
const shortDuration = original.duration < 31;
|
||||||
const isGIF = type === 'gifv' && shortDuration;
|
const isGIF = type === 'gifv' && shortDuration;
|
||||||
// If GIF is too long, treat it as a video
|
// If GIF is too long, treat it as a video
|
||||||
|
@ -247,7 +252,11 @@ function Media({ media, to, showOriginal, autoAnimate, onClick = () => {} }) {
|
||||||
></video>
|
></video>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const showInlineDesc = !showOriginal && !isGIF && !!description;
|
||||||
|
const Container = showInlineDesc ? 'figure' : Fragment;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Container>
|
||||||
<Parent
|
<Parent
|
||||||
class={`media media-${isGIF ? 'gif' : 'video'} ${
|
class={`media media-${isGIF ? 'gif' : 'video'} ${
|
||||||
autoGIFAnimate ? 'media-contain' : ''
|
autoGIFAnimate ? 'media-contain' : ''
|
||||||
|
@ -319,7 +328,7 @@ function Media({ media, to, showOriginal, autoAnimate, onClick = () => {} }) {
|
||||||
<>
|
<>
|
||||||
<img
|
<img
|
||||||
src={previewUrl}
|
src={previewUrl}
|
||||||
alt={description}
|
alt={showInlineDesc ? '' : description}
|
||||||
width={width}
|
width={width}
|
||||||
height={height}
|
height={height}
|
||||||
data-orientation={orientation}
|
data-orientation={orientation}
|
||||||
|
@ -331,6 +340,16 @@ function Media({ media, to, showOriginal, autoAnimate, onClick = () => {} }) {
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Parent>
|
</Parent>
|
||||||
|
{showInlineDesc && (
|
||||||
|
<figcaption
|
||||||
|
onClick={() => {
|
||||||
|
location.hash = to;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{description}
|
||||||
|
</figcaption>
|
||||||
|
)}
|
||||||
|
</Container>
|
||||||
);
|
);
|
||||||
} else if (type === 'audio') {
|
} else if (type === 'audio') {
|
||||||
const formattedDuration = formatDuration(original.duration);
|
const formattedDuration = formatDuration(original.duration);
|
||||||
|
|
|
@ -2,10 +2,11 @@ import './modal.css';
|
||||||
|
|
||||||
import { createPortal } from 'preact/compat';
|
import { createPortal } from 'preact/compat';
|
||||||
import { useEffect, useRef } from 'preact/hooks';
|
import { useEffect, useRef } from 'preact/hooks';
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
|
||||||
const $modalContainer = document.getElementById('modal-container');
|
const $modalContainer = document.getElementById('modal-container');
|
||||||
|
|
||||||
function Modal({ children, onClick, class: className }) {
|
function Modal({ children, onClose, onClick, class: className }) {
|
||||||
if (!children) return null;
|
if (!children) return null;
|
||||||
|
|
||||||
const modalRef = useRef();
|
const modalRef = useRef();
|
||||||
|
@ -19,8 +20,30 @@ function Modal({ children, onClick, class: className }) {
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const escRef = useHotkeys('esc', onClose, [onClose], {
|
||||||
|
enabled: !!onClose,
|
||||||
|
});
|
||||||
|
|
||||||
const Modal = (
|
const Modal = (
|
||||||
<div ref={modalRef} className={className} onClick={onClick}>
|
<div
|
||||||
|
ref={(node) => {
|
||||||
|
modalRef.current = node;
|
||||||
|
escRef.current = node?.querySelector?.('[tabindex="-1"]') || node;
|
||||||
|
}}
|
||||||
|
className={className}
|
||||||
|
onClick={(e) => {
|
||||||
|
onClick?.(e);
|
||||||
|
if (e.target === e.currentTarget) {
|
||||||
|
onClose?.(e);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
tabIndex="-1"
|
||||||
|
onFocus={(e) => {
|
||||||
|
if (e.target === e.currentTarget) {
|
||||||
|
modalRef.current?.querySelector?.('[tabindex="-1"]')?.focus?.();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
179
src/components/modals.jsx
Normal file
179
src/components/modals.jsx
Normal file
|
@ -0,0 +1,179 @@
|
||||||
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
|
import { subscribe, useSnapshot } from 'valtio';
|
||||||
|
|
||||||
|
import Accounts from '../pages/accounts';
|
||||||
|
import Settings from '../pages/settings';
|
||||||
|
import focusDeck from '../utils/focus-deck';
|
||||||
|
import showToast from '../utils/show-toast';
|
||||||
|
import states from '../utils/states';
|
||||||
|
|
||||||
|
import AccountSheet from './account-sheet';
|
||||||
|
import Compose from './compose';
|
||||||
|
import Drafts from './drafts';
|
||||||
|
import GenericAccounts from './generic-accounts';
|
||||||
|
import MediaModal from './media-modal';
|
||||||
|
import Modal from './modal';
|
||||||
|
import ShortcutsSettings from './shortcuts-settings';
|
||||||
|
|
||||||
|
subscribe(states, (changes) => {
|
||||||
|
for (const [action, path, value, prevValue] of changes) {
|
||||||
|
// When closing modal, focus on deck
|
||||||
|
if (/^show/i.test(path) && !value) {
|
||||||
|
focusDeck();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function Modals() {
|
||||||
|
const snapStates = useSnapshot(states);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{!!snapStates.showCompose && (
|
||||||
|
<Modal>
|
||||||
|
<Compose
|
||||||
|
replyToStatus={
|
||||||
|
typeof snapStates.showCompose !== 'boolean'
|
||||||
|
? snapStates.showCompose.replyToStatus
|
||||||
|
: window.__COMPOSE__?.replyToStatus || null
|
||||||
|
}
|
||||||
|
editStatus={
|
||||||
|
states.showCompose?.editStatus ||
|
||||||
|
window.__COMPOSE__?.editStatus ||
|
||||||
|
null
|
||||||
|
}
|
||||||
|
draftStatus={
|
||||||
|
states.showCompose?.draftStatus ||
|
||||||
|
window.__COMPOSE__?.draftStatus ||
|
||||||
|
null
|
||||||
|
}
|
||||||
|
onClose={(results) => {
|
||||||
|
const { newStatus, instance } = results || {};
|
||||||
|
states.showCompose = false;
|
||||||
|
window.__COMPOSE__ = null;
|
||||||
|
if (newStatus) {
|
||||||
|
states.reloadStatusPage++;
|
||||||
|
showToast({
|
||||||
|
text: 'Post published. Check it out.',
|
||||||
|
delay: 1000,
|
||||||
|
duration: 10_000, // 10 seconds
|
||||||
|
onClick: (toast) => {
|
||||||
|
toast.hideToast();
|
||||||
|
states.prevLocation = location;
|
||||||
|
navigate(
|
||||||
|
instance
|
||||||
|
? `/${instance}/s/${newStatus.id}`
|
||||||
|
: `/s/${newStatus.id}`,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
{!!snapStates.showSettings && (
|
||||||
|
<Modal
|
||||||
|
onClose={() => {
|
||||||
|
states.showSettings = false;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Settings
|
||||||
|
onClose={() => {
|
||||||
|
states.showSettings = false;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
{!!snapStates.showAccounts && (
|
||||||
|
<Modal
|
||||||
|
onClose={() => {
|
||||||
|
states.showAccounts = false;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Accounts
|
||||||
|
onClose={() => {
|
||||||
|
states.showAccounts = false;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
{!!snapStates.showAccount && (
|
||||||
|
<Modal
|
||||||
|
class="light"
|
||||||
|
onClose={() => {
|
||||||
|
states.showAccount = false;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AccountSheet
|
||||||
|
account={snapStates.showAccount?.account || snapStates.showAccount}
|
||||||
|
instance={snapStates.showAccount?.instance}
|
||||||
|
onClose={({ destination } = {}) => {
|
||||||
|
states.showAccount = false;
|
||||||
|
if (destination) {
|
||||||
|
states.showAccounts = false;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
{!!snapStates.showDrafts && (
|
||||||
|
<Modal
|
||||||
|
onClose={() => {
|
||||||
|
states.showDrafts = false;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Drafts onClose={() => (states.showDrafts = false)} />
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
{!!snapStates.showMediaModal && (
|
||||||
|
<Modal
|
||||||
|
onClick={(e) => {
|
||||||
|
if (
|
||||||
|
e.target === e.currentTarget ||
|
||||||
|
e.target.classList.contains('media')
|
||||||
|
) {
|
||||||
|
states.showMediaModal = false;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MediaModal
|
||||||
|
mediaAttachments={snapStates.showMediaModal.mediaAttachments}
|
||||||
|
instance={snapStates.showMediaModal.instance}
|
||||||
|
index={snapStates.showMediaModal.index}
|
||||||
|
statusID={snapStates.showMediaModal.statusID}
|
||||||
|
onClose={() => {
|
||||||
|
states.showMediaModal = false;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
{!!snapStates.showShortcutsSettings && (
|
||||||
|
<Modal
|
||||||
|
class="light"
|
||||||
|
onClose={() => {
|
||||||
|
states.showShortcutsSettings = false;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ShortcutsSettings
|
||||||
|
onClose={() => (states.showShortcutsSettings = false)}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
{!!snapStates.showGenericAccounts && (
|
||||||
|
<Modal
|
||||||
|
class="light"
|
||||||
|
onClose={() => {
|
||||||
|
states.showGenericAccounts = false;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<GenericAccounts
|
||||||
|
onClose={() => (states.showGenericAccounts = false)}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -72,9 +72,9 @@ function NameText({
|
||||||
<i class="instance">{acct2}</i>
|
<i class="instance">{acct2}</i>
|
||||||
</>
|
</>
|
||||||
) : short ? (
|
) : short ? (
|
||||||
<i>@{username}</i>
|
<i>{username}</i>
|
||||||
) : (
|
) : (
|
||||||
<b>@{username}</b>
|
<b>{username}</b>
|
||||||
)}
|
)}
|
||||||
{showAcct && (
|
{showAcct && (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -239,6 +239,13 @@ function NavMenu(props) {
|
||||||
<MenuLink to="/login">
|
<MenuLink to="/login">
|
||||||
<Icon icon="user" size="l" /> <span>Log in</span>
|
<Icon icon="user" size="l" /> <span>Log in</span>
|
||||||
</MenuLink>
|
</MenuLink>
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
states.showSettings = true;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon="gear" size="l" /> <span>Settings…</span>
|
||||||
|
</MenuItem>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|
|
@ -106,6 +106,11 @@ export default memo(function NotificationService() {
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (navigator?.clearAppBadge) {
|
||||||
|
navigator.clearAppBadge();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
usePageVisibility((visible) => {
|
usePageVisibility((visible) => {
|
||||||
if (visible && navigator?.clearAppBadge) {
|
if (visible && navigator?.clearAppBadge) {
|
||||||
console.log('🔰 Clear app badge');
|
console.log('🔰 Clear app badge');
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
|
import shortenNumber from '../utils/shorten-number';
|
||||||
import states from '../utils/states';
|
import states from '../utils/states';
|
||||||
import store from '../utils/store';
|
import store from '../utils/store';
|
||||||
|
import useTruncated from '../utils/useTruncated';
|
||||||
|
|
||||||
import Avatar from './avatar';
|
import Avatar from './avatar';
|
||||||
import FollowRequestButtons from './follow-request-buttons';
|
import FollowRequestButtons from './follow-request-buttons';
|
||||||
|
@ -60,6 +62,8 @@ const contentText = {
|
||||||
'admin.report': 'reported a post.',
|
'admin.report': 'reported a post.',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const AVATARS_LIMIT = 50;
|
||||||
|
|
||||||
function Notification({ notification, instance, reload, isStatic }) {
|
function Notification({ notification, instance, reload, isStatic }) {
|
||||||
const { id, status, account, _accounts, _statuses } = notification;
|
const { id, status, account, _accounts, _statuses } = notification;
|
||||||
let { type } = notification;
|
let { type } = notification;
|
||||||
|
@ -126,6 +130,21 @@ function Notification({ notification, instance, reload, isStatic }) {
|
||||||
const formattedCreatedAt =
|
const formattedCreatedAt =
|
||||||
notification.createdAt && new Date(notification.createdAt).toLocaleString();
|
notification.createdAt && new Date(notification.createdAt).toLocaleString();
|
||||||
|
|
||||||
|
const genericAccountsHeading =
|
||||||
|
{
|
||||||
|
'favourite+reblog': 'Boosted/Favourited by…',
|
||||||
|
favourite: 'Favourited by…',
|
||||||
|
reblog: 'Boosted by…',
|
||||||
|
follow: 'Followed by…',
|
||||||
|
}[type] || 'Accounts';
|
||||||
|
const handleOpenGenericAccounts = () => {
|
||||||
|
states.showGenericAccounts = {
|
||||||
|
heading: genericAccountsHeading,
|
||||||
|
accounts: _accounts,
|
||||||
|
showReactions: type === 'favourite+reblog',
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={`notification notification-${type}`} tabIndex="0">
|
<div class={`notification notification-${type}`} tabIndex="0">
|
||||||
<div
|
<div
|
||||||
|
@ -153,7 +172,12 @@ function Notification({ notification, instance, reload, isStatic }) {
|
||||||
<>
|
<>
|
||||||
{_accounts?.length > 1 ? (
|
{_accounts?.length > 1 ? (
|
||||||
<>
|
<>
|
||||||
<b>{_accounts.length} people</b>{' '}
|
<b tabIndex="0" onClick={handleOpenGenericAccounts}>
|
||||||
|
<span title={_accounts.length}>
|
||||||
|
{shortenNumber(_accounts.length)}
|
||||||
|
</span>{' '}
|
||||||
|
people
|
||||||
|
</b>{' '}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
@ -186,7 +210,7 @@ function Notification({ notification, instance, reload, isStatic }) {
|
||||||
)}
|
)}
|
||||||
{_accounts?.length > 1 && (
|
{_accounts?.length > 1 && (
|
||||||
<p class="avatars-stack">
|
<p class="avatars-stack">
|
||||||
{_accounts.map((account, i) => (
|
{_accounts.slice(0, AVATARS_LIMIT).map((account, i) => (
|
||||||
<>
|
<>
|
||||||
<a
|
<a
|
||||||
href={account.url}
|
href={account.url}
|
||||||
|
@ -202,11 +226,11 @@ function Notification({ notification, instance, reload, isStatic }) {
|
||||||
size={
|
size={
|
||||||
_accounts.length <= 10
|
_accounts.length <= 10
|
||||||
? 'xxl'
|
? 'xxl'
|
||||||
: _accounts.length < 100
|
: _accounts.length < 20
|
||||||
? 'xl'
|
? 'xl'
|
||||||
: _accounts.length < 1000
|
: _accounts.length < 30
|
||||||
? 'l'
|
? 'l'
|
||||||
: _accounts.length < 2000
|
: _accounts.length < 40
|
||||||
? 'm'
|
? 'm'
|
||||||
: 's' // My god, this person is popular!
|
: 's' // My god, this person is popular!
|
||||||
}
|
}
|
||||||
|
@ -228,43 +252,71 @@ function Notification({ notification, instance, reload, isStatic }) {
|
||||||
</a>{' '}
|
</a>{' '}
|
||||||
</>
|
</>
|
||||||
))}
|
))}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="small plain"
|
||||||
|
onClick={handleOpenGenericAccounts}
|
||||||
|
>
|
||||||
|
{_accounts.length > AVATARS_LIMIT &&
|
||||||
|
`+${_accounts.length - AVATARS_LIMIT}`}
|
||||||
|
<Icon icon="chevron-down" />
|
||||||
|
</button>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{_statuses?.length > 1 && (
|
{_statuses?.length > 1 && (
|
||||||
<ul class="notification-group-statuses">
|
<ul class="notification-group-statuses">
|
||||||
{_statuses.map((status) => (
|
{_statuses.map((status) => (
|
||||||
<li key={status.id}>
|
<li key={status.id}>
|
||||||
<Link
|
<TruncatedLink
|
||||||
class={`status-link status-type-${type}`}
|
class={`status-link status-type-${type}`}
|
||||||
to={
|
to={
|
||||||
instance ? `/${instance}/s/${status.id}` : `/s/${status.id}`
|
instance ? `/${instance}/s/${status.id}` : `/s/${status.id}`
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Status status={status} size="s" />
|
<Status status={status} size="s" />
|
||||||
</Link>
|
</TruncatedLink>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
)}
|
)}
|
||||||
{status && (!_statuses?.length || _statuses?.length <= 1) && (
|
{status && (!_statuses?.length || _statuses?.length <= 1) && (
|
||||||
<Link
|
<TruncatedLink
|
||||||
class={`status-link status-type-${type}`}
|
class={`status-link status-type-${type}`}
|
||||||
to={
|
to={
|
||||||
instance
|
instance
|
||||||
? `/${instance}/s/${actualStatusID}`
|
? `/${instance}/s/${actualStatusID}`
|
||||||
: `/s/${actualStatusID}`
|
: `/s/${actualStatusID}`
|
||||||
}
|
}
|
||||||
|
onContextMenu={(e) => {
|
||||||
|
const post = e.target.querySelector('.status');
|
||||||
|
if (post) {
|
||||||
|
// Fire a custom event to open the context menu
|
||||||
|
if (e.metaKey) return;
|
||||||
|
e.preventDefault();
|
||||||
|
post.dispatchEvent(
|
||||||
|
new MouseEvent('contextmenu', {
|
||||||
|
clientX: e.clientX,
|
||||||
|
clientY: e.clientY,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{isStatic ? (
|
{isStatic ? (
|
||||||
<Status status={actualStatus} size="s" />
|
<Status status={actualStatus} size="s" />
|
||||||
) : (
|
) : (
|
||||||
<Status statusID={actualStatusID} size="s" />
|
<Status statusID={actualStatusID} size="s" />
|
||||||
)}
|
)}
|
||||||
</Link>
|
</TruncatedLink>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function TruncatedLink(props) {
|
||||||
|
const ref = useTruncated();
|
||||||
|
return <Link {...props} data-read-more="Read more →" ref={ref} />;
|
||||||
|
}
|
||||||
|
|
||||||
export default Notification;
|
export default Notification;
|
||||||
|
|
|
@ -16,6 +16,7 @@ export default memo(function SearchCommand({ onClose = () => {} }) {
|
||||||
setShowSearch(true);
|
setShowSearch(true);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
searchFormRef.current?.focus?.();
|
searchFormRef.current?.focus?.();
|
||||||
|
searchFormRef.current?.select?.();
|
||||||
}, 0);
|
}, 0);
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -23,6 +23,9 @@ const SearchForm = forwardRef((props, ref) => {
|
||||||
focus: () => {
|
focus: () => {
|
||||||
searchFieldRef.current.focus();
|
searchFieldRef.current.focus();
|
||||||
},
|
},
|
||||||
|
select: () => {
|
||||||
|
searchFieldRef.current.select();
|
||||||
|
},
|
||||||
blur: () => {
|
blur: () => {
|
||||||
searchFieldRef.current.blur();
|
searchFieldRef.current.blur();
|
||||||
},
|
},
|
||||||
|
@ -67,6 +70,9 @@ const SearchForm = forwardRef((props, ref) => {
|
||||||
// autofocus
|
// autofocus
|
||||||
placeholder="Search"
|
placeholder="Search"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
|
autocomplete="off"
|
||||||
|
autocorrect="off"
|
||||||
|
autocapitalize="off"
|
||||||
onSearch={(e) => {
|
onSearch={(e) => {
|
||||||
if (!e.target.value) {
|
if (!e.target.value) {
|
||||||
setSearchParams({});
|
setSearchParams({});
|
||||||
|
|
|
@ -93,6 +93,38 @@
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
}
|
}
|
||||||
|
.status-card-link:not(
|
||||||
|
.truncated .status-card-link, /* parent status already truncated */
|
||||||
|
.status-card-link .status-card-link /* nested status cards */
|
||||||
|
):has(.truncated) {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
content: attr(data-read-more);
|
||||||
|
line-height: 1;
|
||||||
|
display: inline-block;
|
||||||
|
position: absolute;
|
||||||
|
--inset-offset: 16px;
|
||||||
|
inset-block-end: var(--inset-offset);
|
||||||
|
inset-inline-end: var(--inset-offset);
|
||||||
|
color: var(--text-color);
|
||||||
|
background-color: var(--bg-faded-color);
|
||||||
|
border: 1px dashed var(--link-color);
|
||||||
|
box-shadow: 0 0 0 1px var(--bg-color), 0 -5px 10px var(--bg-color),
|
||||||
|
0 -5px 15px var(--bg-color), 0 -5px 20px var(--bg-color);
|
||||||
|
padding: 0.5em 0.75em;
|
||||||
|
border-radius: 10em;
|
||||||
|
font-size: 90%;
|
||||||
|
white-space: nowrap;
|
||||||
|
transition: transform 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:is(:hover, :focus):after {
|
||||||
|
color: var(--link-color);
|
||||||
|
transform: translate(2px, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
.status-card-link:is(:hover, :focus) .status-card {
|
.status-card-link:is(:hover, :focus) .status-card {
|
||||||
border-color: var(--outline-hover-color);
|
border-color: var(--outline-hover-color);
|
||||||
box-shadow: inset 0 0 0 4px var(--bg-faded-blur-color);
|
box-shadow: inset 0 0 0 4px var(--bg-faded-blur-color);
|
||||||
|
@ -131,11 +163,16 @@
|
||||||
:is(.content, .poll, .media-container) {
|
:is(.content, .poll, .media-container) {
|
||||||
max-height: 80px !important;
|
max-height: 80px !important;
|
||||||
}
|
}
|
||||||
.status-card :is(.content, .poll) {
|
.status.large .status-card :is(.content, .poll, .media-container) {
|
||||||
|
max-height: 80vh !important;
|
||||||
|
}
|
||||||
|
.status-card :is(.content.truncated, .poll, .media-container.truncated) {
|
||||||
font-size: inherit !important;
|
font-size: inherit !important;
|
||||||
mask-image: linear-gradient(to bottom, #000 80px, transparent);
|
mask-image: linear-gradient(to bottom, #000 80px, transparent);
|
||||||
}
|
}
|
||||||
.status.small .status-card :is(.content, .poll) {
|
.status.small
|
||||||
|
.status-card
|
||||||
|
:is(.content.truncated, .poll, .media-container.truncated) {
|
||||||
mask-image: linear-gradient(to bottom, #000 40px, transparent);
|
mask-image: linear-gradient(to bottom, #000 40px, transparent);
|
||||||
}
|
}
|
||||||
.status-card .card {
|
.status-card .card {
|
||||||
|
@ -174,6 +211,10 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
|
.status-carousel & {
|
||||||
|
padding: 16px 16px 16px 24px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.status.filtered .status-filtered-info {
|
.status.filtered .status-filtered-info {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
@ -246,8 +287,8 @@
|
||||||
|
|
||||||
.status > .container > .meta {
|
.status > .container > .meta {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 4px;
|
||||||
justify-content: space-between;
|
/* justify-content: space-between; */
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.status.small > .container > .meta {
|
.status.small > .container > .meta {
|
||||||
|
@ -256,7 +297,11 @@
|
||||||
.status > .container > .meta > * {
|
.status > .container > .meta > * {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
/* text-overflow: ellipsis; */
|
||||||
|
}
|
||||||
|
.status > .container > .meta .name-text {
|
||||||
|
mask-image: linear-gradient(to left, transparent, black 16px);
|
||||||
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
.status.large > .container > .meta {
|
.status.large > .container > .meta {
|
||||||
min-height: 50px;
|
min-height: 50px;
|
||||||
|
@ -417,7 +462,12 @@
|
||||||
.content-container.has-spoiler:not(.show-spoiler)
|
.content-container.has-spoiler:not(.show-spoiler)
|
||||||
.spoiler
|
.spoiler
|
||||||
~ .card
|
~ .card
|
||||||
.meta-container {
|
.meta-container,
|
||||||
|
.status
|
||||||
|
.content-container.has-spoiler:not(.show-spoiler)
|
||||||
|
.spoiler
|
||||||
|
~ .media-container
|
||||||
|
figcaption {
|
||||||
filter: blur(5px) invert(0.5);
|
filter: blur(5px) invert(0.5);
|
||||||
image-rendering: crisp-edges;
|
image-rendering: crisp-edges;
|
||||||
image-rendering: pixelated;
|
image-rendering: pixelated;
|
||||||
|
@ -560,8 +610,20 @@
|
||||||
.status .content blockquote {
|
.status .content blockquote {
|
||||||
margin-block: min(0.75em, 12px);
|
margin-block: min(0.75em, 12px);
|
||||||
margin-inline: 0;
|
margin-inline: 0;
|
||||||
padding: 0 0 0 8px;
|
padding-block: 0;
|
||||||
border-left: 4px solid var(--link-faded-color);
|
padding-inline: 12px 0;
|
||||||
|
/* border-left: 4px solid var(--link-faded-color); */
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
position: absolute;
|
||||||
|
content: '';
|
||||||
|
width: 3px;
|
||||||
|
background-color: var(--link-faded-color);
|
||||||
|
inset-block: 0;
|
||||||
|
inset-inline-start: 0;
|
||||||
|
border-radius: 9999px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.status .content > :is(ul, ol),
|
.status .content > :is(ul, ol),
|
||||||
.status .content > div > :is(ul, ol) {
|
.status .content > div > :is(ul, ol) {
|
||||||
|
@ -642,6 +704,31 @@
|
||||||
width: auto !important;
|
width: auto !important;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
|
figure {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
figcaption {
|
||||||
|
margin: -2px 0 0;
|
||||||
|
padding: 0 4px;
|
||||||
|
font-size: 90%;
|
||||||
|
color: var(--text-insignificant-color);
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: normal;
|
||||||
|
display: -webkit-box;
|
||||||
|
display: box;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
line-clamp: 2;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover figure figcaption {
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.status .media-container.media-eq1 .media {
|
.status .media-container.media-eq1 .media {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
@ -779,7 +866,7 @@ body:has(#modal-container .carousel) .status .media img:hover {
|
||||||
position: relative;
|
position: relative;
|
||||||
background-clip: padding-box;
|
background-clip: padding-box;
|
||||||
}
|
}
|
||||||
.status :is(.media-video, .media-audio)[data-formatted-duration] .media-play {
|
.status :is(.media-video, .media-audio) .media-play {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
width: 44px;
|
width: 44px;
|
||||||
height: 44px;
|
height: 44px;
|
||||||
|
@ -787,23 +874,17 @@ body:has(#modal-container .carousel) .status .media img:hover {
|
||||||
left: 50%;
|
left: 50%;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
color: var(--text-color);
|
color: var(--video-fg-color);
|
||||||
background-color: var(--bg-blur-color);
|
background-color: var(--video-bg-color);
|
||||||
/* backdrop-filter: blur(6px) saturate(3) invert(0.2); */
|
box-shadow: inset 0 0 0 2px var(--video-outline-color);
|
||||||
box-shadow: 0 0 16px var(--drop-shadow-color);
|
|
||||||
display: flex;
|
display: flex;
|
||||||
place-content: center;
|
place-content: center;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
border-radius: 70px;
|
border-radius: 70px;
|
||||||
transition: all 0.2s ease-in-out;
|
transition: transform 0.2s ease-in-out;
|
||||||
}
|
}
|
||||||
.status
|
.status :is(.media-video, .media-audio):hover:not(:active) .media-play {
|
||||||
:is(.media-video, .media-audio)[data-formatted-duration]:hover:not(:active)
|
|
||||||
.media-play {
|
|
||||||
transform: translate(-50%, -50%) scale(1.1);
|
transform: translate(-50%, -50%) scale(1.1);
|
||||||
background-color: var(--bg-color);
|
|
||||||
box-shadow: 0 0 16px var(--drop-shadow-color),
|
|
||||||
0 0 8px var(--drop-shadow-color);
|
|
||||||
}
|
}
|
||||||
.status :is(.media-video, .media-audio)[data-formatted-duration]:after {
|
.status :is(.media-video, .media-audio)[data-formatted-duration]:after {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
@ -812,9 +893,9 @@ body:has(#modal-container .carousel) .status .media img:hover {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 8px;
|
bottom: 8px;
|
||||||
right: 8px;
|
right: 8px;
|
||||||
color: var(--bg-color);
|
color: var(--video-fg-color);
|
||||||
background-color: var(--text-color);
|
background-color: var(--video-bg-color);
|
||||||
backdrop-filter: blur(6px) saturate(3) invert(0.2);
|
border: var(--hairline-width) solid var(--video-outline-color);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 0 4px;
|
padding: 0 4px;
|
||||||
}
|
}
|
||||||
|
@ -881,6 +962,15 @@ body:has(#modal-container .carousel) .status .media img:hover {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
|
||||||
|
/* Convert breaks to spaces */
|
||||||
|
br {
|
||||||
|
display: none;
|
||||||
|
|
||||||
|
+ * {
|
||||||
|
margin-left: 1ex;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.status:not(.large) .hashtag-stuffing:first-child {
|
.status:not(.large) .hashtag-stuffing:first-child {
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
|
@ -1057,6 +1147,9 @@ a:focus-visible .card img {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
.card:visited .meta.domain {
|
||||||
|
color: var(--link-visited-color);
|
||||||
|
}
|
||||||
.card .meta.domain * {
|
.card .meta.domain * {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
@ -1068,6 +1161,9 @@ a.card:is(:hover, :focus) {
|
||||||
border: 1px solid var(--link-color);
|
border: 1px solid var(--link-color);
|
||||||
box-shadow: 0 0 0 2px var(--link-faded-color);
|
box-shadow: 0 0 0 2px var(--link-faded-color);
|
||||||
}
|
}
|
||||||
|
a.card:is(:hover, :focus):visited {
|
||||||
|
border-color: var(--link-visited-color);
|
||||||
|
}
|
||||||
.card.video {
|
.card.video {
|
||||||
max-width: 320px;
|
max-width: 320px;
|
||||||
max-height: 320px;
|
max-height: 320px;
|
||||||
|
@ -1394,10 +1490,13 @@ a.card:is(:hover, :focus) {
|
||||||
}
|
}
|
||||||
|
|
||||||
.shortcode-emoji {
|
.shortcode-emoji {
|
||||||
width: 1.2em;
|
width: auto;
|
||||||
|
min-width: 1.2em;
|
||||||
|
max-width: 100%;
|
||||||
height: 1.2em;
|
height: 1.2em;
|
||||||
vertical-align: text-bottom;
|
vertical-align: text-bottom;
|
||||||
object-fit: scale-down;
|
object-fit: cover;
|
||||||
|
object-position: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* EDIT HISTORY */
|
/* EDIT HISTORY */
|
||||||
|
@ -1454,26 +1553,52 @@ a.card:is(:hover, :focus) {
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
#filtered-status-peek .status-link {
|
#filtered-status-peek {
|
||||||
|
.status-link {
|
||||||
|
margin: 8px 0 0;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
border: var(--hairline-width) dashed var(--text-insignificant-color);
|
border: var(--hairline-width) solid var(--divider-color);
|
||||||
|
position: relative;
|
||||||
max-height: 33vh;
|
max-height: 33vh;
|
||||||
max-height: 33dvh;
|
max-height: 33dvh;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
|
||||||
#filtered-status-peek .status-link .status {
|
&.truncated {
|
||||||
|
.status {
|
||||||
|
mask-image: linear-gradient(to bottom, #000 80px, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
content: attr(data-read-more);
|
||||||
|
line-height: 1;
|
||||||
|
display: inline-block;
|
||||||
|
position: absolute;
|
||||||
|
--inset-offset: 16px;
|
||||||
|
inset-block-end: var(--inset-offset);
|
||||||
|
inset-inline-end: var(--inset-offset);
|
||||||
|
color: var(--text-color);
|
||||||
|
background-color: var(--bg-faded-color);
|
||||||
|
border: 1px dashed var(--link-color);
|
||||||
|
box-shadow: 0 0 0 1px var(--bg-color), 0 -5px 10px var(--bg-color),
|
||||||
|
0 -5px 15px var(--bg-color), 0 -5px 20px var(--bg-color);
|
||||||
|
padding: 0.5em 0.75em;
|
||||||
|
border-radius: 10em;
|
||||||
|
font-size: 90%;
|
||||||
|
white-space: nowrap;
|
||||||
|
transition: transform 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:is(:hover, :focus):after {
|
||||||
|
color: var(--link-color);
|
||||||
|
transform: translate(2px, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
font-size: 90%;
|
font-size: 90%;
|
||||||
max-height: 33vh;
|
}
|
||||||
max-height: 33dvh;
|
}
|
||||||
overflow: hidden;
|
|
||||||
mask-image: linear-gradient(black 80%, transparent 95%);
|
|
||||||
}
|
|
||||||
#filtered-status-peek .status-post-link {
|
|
||||||
float: right;
|
|
||||||
position: sticky;
|
|
||||||
bottom: 8px;
|
|
||||||
right: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* REACTIONS */
|
/* REACTIONS */
|
||||||
|
|
|
@ -34,6 +34,7 @@ import Modal from '../components/modal';
|
||||||
import NameText from '../components/name-text';
|
import NameText from '../components/name-text';
|
||||||
import Poll from '../components/poll';
|
import Poll from '../components/poll';
|
||||||
import { api } from '../utils/api';
|
import { api } from '../utils/api';
|
||||||
|
import emojifyText from '../utils/emojify-text';
|
||||||
import enhanceContent from '../utils/enhance-content';
|
import enhanceContent from '../utils/enhance-content';
|
||||||
import getTranslateTargetLanguage from '../utils/get-translate-target-language';
|
import getTranslateTargetLanguage from '../utils/get-translate-target-language';
|
||||||
import getHTMLText from '../utils/getHTMLText';
|
import getHTMLText from '../utils/getHTMLText';
|
||||||
|
@ -48,6 +49,7 @@ import showToast from '../utils/show-toast';
|
||||||
import states, { getStatus, saveStatus, statusKey } from '../utils/states';
|
import states, { getStatus, saveStatus, statusKey } from '../utils/states';
|
||||||
import statusPeek from '../utils/status-peek';
|
import statusPeek from '../utils/status-peek';
|
||||||
import store from '../utils/store';
|
import store from '../utils/store';
|
||||||
|
import useTruncated from '../utils/useTruncated';
|
||||||
import visibilityIconsMap from '../utils/visibility-icons-map';
|
import visibilityIconsMap from '../utils/visibility-icons-map';
|
||||||
|
|
||||||
import Avatar from './avatar';
|
import Avatar from './avatar';
|
||||||
|
@ -228,7 +230,12 @@ function Status({
|
||||||
inReplyToAccountId === currentAccount ||
|
inReplyToAccountId === currentAccount ||
|
||||||
mentions?.find((mention) => mention.id === currentAccount);
|
mentions?.find((mention) => mention.id === currentAccount);
|
||||||
|
|
||||||
const showSpoiler = previewMode || !!snapStates.spoilers[id] || false;
|
const readingExpandSpoilers = useMemo(() => {
|
||||||
|
const prefs = store.account.get('preferences') || {};
|
||||||
|
return !!prefs['reading:expand:spoilers'];
|
||||||
|
}, []);
|
||||||
|
const showSpoiler =
|
||||||
|
previewMode || readingExpandSpoilers || !!snapStates.spoilers[id] || false;
|
||||||
|
|
||||||
if (reblog) {
|
if (reblog) {
|
||||||
// If has statusID, means useItemID (cached in states)
|
// If has statusID, means useItemID (cached in states)
|
||||||
|
@ -313,40 +320,9 @@ function Status({
|
||||||
const [showEdited, setShowEdited] = useState(false);
|
const [showEdited, setShowEdited] = useState(false);
|
||||||
const [showReactions, setShowReactions] = useState(false);
|
const [showReactions, setShowReactions] = useState(false);
|
||||||
|
|
||||||
const spoilerContentRef = useRef(null);
|
const spoilerContentRef = useTruncated();
|
||||||
useResizeObserver({
|
const contentRef = useTruncated();
|
||||||
ref: spoilerContentRef,
|
const mediaContainerRef = useTruncated();
|
||||||
onResize: () => {
|
|
||||||
if (spoilerContentRef.current) {
|
|
||||||
const { scrollHeight, clientHeight } = spoilerContentRef.current;
|
|
||||||
if (scrollHeight < window.innerHeight * 0.4) {
|
|
||||||
spoilerContentRef.current.classList.remove('truncated');
|
|
||||||
} else {
|
|
||||||
spoilerContentRef.current.classList.toggle(
|
|
||||||
'truncated',
|
|
||||||
scrollHeight > clientHeight,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const contentRef = useRef(null);
|
|
||||||
useResizeObserver({
|
|
||||||
ref: contentRef,
|
|
||||||
onResize: () => {
|
|
||||||
if (contentRef.current) {
|
|
||||||
const { scrollHeight, clientHeight } = contentRef.current;
|
|
||||||
if (scrollHeight < window.innerHeight * 0.4) {
|
|
||||||
contentRef.current.classList.remove('truncated');
|
|
||||||
} else {
|
|
||||||
contentRef.current.classList.toggle(
|
|
||||||
'truncated',
|
|
||||||
scrollHeight > clientHeight,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const readMoreText = 'Read more →';
|
const readMoreText = 'Read more →';
|
||||||
|
|
||||||
const statusRef = useRef(null);
|
const statusRef = useRef(null);
|
||||||
|
@ -1128,6 +1104,7 @@ function Status({
|
||||||
<button
|
<button
|
||||||
class={`light spoiler ${showSpoiler ? 'spoiling' : ''}`}
|
class={`light spoiler ${showSpoiler ? 'spoiling' : ''}`}
|
||||||
type="button"
|
type="button"
|
||||||
|
disabled={readingExpandSpoilers}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
@ -1139,7 +1116,11 @@ function Status({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon={showSpoiler ? 'eye-open' : 'eye-close'} />{' '}
|
<Icon icon={showSpoiler ? 'eye-open' : 'eye-close'} />{' '}
|
||||||
{showSpoiler ? 'Show less' : 'Show more'}
|
{readingExpandSpoilers
|
||||||
|
? 'Content warning'
|
||||||
|
: showSpoiler
|
||||||
|
? 'Show less'
|
||||||
|
: 'Show more'}
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
@ -1148,7 +1129,12 @@ function Status({
|
||||||
lang={language}
|
lang={language}
|
||||||
dir="auto"
|
dir="auto"
|
||||||
class="inner-content"
|
class="inner-content"
|
||||||
onClick={handleContentLinks({ mentions, instance, previewMode })}
|
onClick={handleContentLinks({
|
||||||
|
mentions,
|
||||||
|
instance,
|
||||||
|
previewMode,
|
||||||
|
statusURL: url,
|
||||||
|
})}
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: enhanceContent(content, {
|
__html: enhanceContent(content, {
|
||||||
emojis,
|
emojis,
|
||||||
|
@ -1225,6 +1211,7 @@ function Status({
|
||||||
)}
|
)}
|
||||||
{(((enableTranslate || inlineTranslate) &&
|
{(((enableTranslate || inlineTranslate) &&
|
||||||
!!content.trim() &&
|
!!content.trim() &&
|
||||||
|
!!getHTMLText(emojifyText(content, emojis)) &&
|
||||||
differentLanguage) ||
|
differentLanguage) ||
|
||||||
forceTranslate) && (
|
forceTranslate) && (
|
||||||
<TranslationBlock
|
<TranslationBlock
|
||||||
|
@ -1269,6 +1256,7 @@ function Status({
|
||||||
)}
|
)}
|
||||||
{!!mediaAttachments.length && (
|
{!!mediaAttachments.length && (
|
||||||
<div
|
<div
|
||||||
|
ref={mediaContainerRef}
|
||||||
class={`media-container media-eq${mediaAttachments.length} ${
|
class={`media-container media-eq${mediaAttachments.length} ${
|
||||||
mediaAttachments.length > 2 ? 'media-gt2' : ''
|
mediaAttachments.length > 2 ? 'media-gt2' : ''
|
||||||
} ${mediaAttachments.length > 4 ? 'media-gt4' : ''}`}
|
} ${mediaAttachments.length > 4 ? 'media-gt4' : ''}`}
|
||||||
|
@ -1317,7 +1305,7 @@ function Status({
|
||||||
icon={visibilityIconsMap[visibility]}
|
icon={visibilityIconsMap[visibility]}
|
||||||
alt={visibilityText[visibility]}
|
alt={visibilityText[visibility]}
|
||||||
/>{' '}
|
/>{' '}
|
||||||
<a href={url} target="_blank">
|
<a href={url} target="_blank" rel="noopener noreferrer">
|
||||||
<time
|
<time
|
||||||
class="created"
|
class="created"
|
||||||
datetime={createdAtDate.toISOString()}
|
datetime={createdAtDate.toISOString()}
|
||||||
|
@ -1569,6 +1557,7 @@ function Card({ card, instance }) {
|
||||||
rel="nofollow noopener noreferrer"
|
rel="nofollow noopener noreferrer"
|
||||||
class={`card link ${blurhashImage ? '' : size}`}
|
class={`card link ${blurhashImage ? '' : size}`}
|
||||||
lang={language}
|
lang={language}
|
||||||
|
dir="auto"
|
||||||
>
|
>
|
||||||
<div class="card-image">
|
<div class="card-image">
|
||||||
<img
|
<img
|
||||||
|
@ -1585,9 +1574,15 @@ function Card({ card, instance }) {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="meta-container">
|
<div class="meta-container">
|
||||||
<p class="meta domain">{domain}</p>
|
<p class="meta domain" dir="auto">
|
||||||
<p class="title">{title}</p>
|
{domain}
|
||||||
<p class="meta">{description || providerName || authorName}</p>
|
</p>
|
||||||
|
<p class="title" dir="auto">
|
||||||
|
{title}
|
||||||
|
</p>
|
||||||
|
<p class="meta" dir="auto">
|
||||||
|
{description || providerName || authorName}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
|
@ -2096,6 +2091,8 @@ function FilteredStatus({ status, filterInfo, instance, containerProps = {} }) {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const statusPeekRef = useTruncated();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
class={isReblog ? (group ? 'status-group' : 'status-reblog') : ''}
|
class={isReblog ? (group ? 'status-group' : 'status-reblog') : ''}
|
||||||
|
@ -2169,16 +2166,15 @@ function FilteredStatus({ status, filterInfo, instance, containerProps = {} }) {
|
||||||
</header>
|
</header>
|
||||||
<main tabIndex="-1">
|
<main tabIndex="-1">
|
||||||
<Link
|
<Link
|
||||||
|
ref={statusPeekRef}
|
||||||
class="status-link"
|
class="status-link"
|
||||||
to={`/${instance}/s/${status.id}`}
|
to={`/${instance}/s/${status.id}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowPeek(false);
|
setShowPeek(false);
|
||||||
}}
|
}}
|
||||||
|
data-read-more="Read more →"
|
||||||
>
|
>
|
||||||
<Status status={status} instance={instance} size="s" readOnly />
|
<Status status={status} instance={instance} size="s" readOnly />
|
||||||
<button type="button" class="status-post-link plain3">
|
|
||||||
See post »
|
|
||||||
</button>
|
|
||||||
</Link>
|
</Link>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
@ -2206,6 +2202,7 @@ const QuoteStatuses = memo(({ id, instance, level = 0 }) => {
|
||||||
key={q.instance + q.id}
|
key={q.instance + q.id}
|
||||||
to={`${q.instance ? `/${q.instance}` : ''}/s/${q.id}`}
|
to={`${q.instance ? `/${q.instance}` : ''}/s/${q.id}`}
|
||||||
class="status-card-link"
|
class="status-card-link"
|
||||||
|
data-read-more="Read more →"
|
||||||
>
|
>
|
||||||
<Status
|
<Status
|
||||||
statusID={q.id}
|
statusID={q.id}
|
||||||
|
|
|
@ -206,7 +206,6 @@ function Timeline({
|
||||||
}
|
}
|
||||||
}, [nearReachEnd, showMore]);
|
}, [nearReachEnd, showMore]);
|
||||||
|
|
||||||
const isHovering = useRef(false);
|
|
||||||
const idle = useIdle(5000);
|
const idle = useIdle(5000);
|
||||||
console.debug('🧘♀️ IDLE', idle);
|
console.debug('🧘♀️ IDLE', idle);
|
||||||
const loadOrCheckUpdates = useCallback(
|
const loadOrCheckUpdates = useCallback(
|
||||||
|
@ -275,12 +274,6 @@ function Timeline({
|
||||||
oRef.current = node;
|
oRef.current = node;
|
||||||
}}
|
}}
|
||||||
tabIndex="-1"
|
tabIndex="-1"
|
||||||
onPointerEnter={(e) => {
|
|
||||||
isHovering.current = true;
|
|
||||||
}}
|
|
||||||
onPointerLeave={() => {
|
|
||||||
isHovering.current = false;
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div class="timeline-deck deck">
|
<div class="timeline-deck deck">
|
||||||
<header
|
<header
|
||||||
|
@ -392,6 +385,7 @@ function Timeline({
|
||||||
instance={instance}
|
instance={instance}
|
||||||
size="s"
|
size="s"
|
||||||
contentTextWeight
|
contentTextWeight
|
||||||
|
allowFilters={allowFilters}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Status
|
<Status
|
||||||
|
@ -399,6 +393,7 @@ function Timeline({
|
||||||
instance={instance}
|
instance={instance}
|
||||||
size="s"
|
size="s"
|
||||||
contentTextWeight
|
contentTextWeight
|
||||||
|
allowFilters={allowFilters}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -419,7 +414,13 @@ function Timeline({
|
||||||
const isSpoiler = item.sensitive && !!item.spoilerText;
|
const isSpoiler = item.sensitive && !!item.spoilerText;
|
||||||
const showCompact =
|
const showCompact =
|
||||||
(isSpoiler && i > 0) ||
|
(isSpoiler && i > 0) ||
|
||||||
(manyItems && isMiddle && type === 'thread');
|
(manyItems &&
|
||||||
|
isMiddle &&
|
||||||
|
(type === 'thread' ||
|
||||||
|
(type === 'conversation' &&
|
||||||
|
!_differentAuthor &&
|
||||||
|
!items[i - 1]._differentAuthor &&
|
||||||
|
!items[i + 1]._differentAuthor)));
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
key={`timeline-${statusID}`}
|
key={`timeline-${statusID}`}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import './translation-block.css';
|
import './translation-block.css';
|
||||||
|
|
||||||
|
import pRetry from 'p-retry';
|
||||||
import pThrottle from 'p-throttle';
|
import pThrottle from 'p-throttle';
|
||||||
import { useEffect, useRef, useState } from 'preact/hooks';
|
import { useEffect, useRef, useState } from 'preact/hooks';
|
||||||
|
|
||||||
|
@ -15,12 +16,21 @@ const throttle = pThrottle({
|
||||||
interval: 2000,
|
interval: 2000,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Using other API instances instead of lingva.ml because of this bug (slashes don't work):
|
||||||
|
// https://github.com/thedaviddelta/lingva-translate/issues/68
|
||||||
|
const LINGVA_INSTANCES = [
|
||||||
|
'lingva.garudalinux.org',
|
||||||
|
'lingva.lunar.icu',
|
||||||
|
'translate.plausibility.cloud',
|
||||||
|
];
|
||||||
|
let currentLingvaInstance = 0;
|
||||||
|
|
||||||
function lingvaTranslate(text, source, target) {
|
function lingvaTranslate(text, source, target) {
|
||||||
console.log('TRANSLATE', text, source, target);
|
console.log('TRANSLATE', text, source, target);
|
||||||
// Using another API instance instead of lingva.ml because of this bug (slashes don't work):
|
const fetchCall = () => {
|
||||||
// https://github.com/thedaviddelta/lingva-translate/issues/68
|
let instance = LINGVA_INSTANCES[currentLingvaInstance];
|
||||||
return fetch(
|
return fetch(
|
||||||
`https://lingva.garudalinux.org/api/v1/${source}/${target}/${encodeURIComponent(
|
`https://${instance}/api/v1/${source}/${target}/${encodeURIComponent(
|
||||||
text,
|
text,
|
||||||
)}`,
|
)}`,
|
||||||
)
|
)
|
||||||
|
@ -33,6 +43,18 @@ function lingvaTranslate(text, source, target) {
|
||||||
info: res.info,
|
info: res.info,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
return pRetry(fetchCall, {
|
||||||
|
retries: 3,
|
||||||
|
onFailedAttempt: (e) => {
|
||||||
|
currentLingvaInstance =
|
||||||
|
(currentLingvaInstance + 1) % LINGVA_INSTANCES.length;
|
||||||
|
console.log(
|
||||||
|
'Retrying translation with another instance',
|
||||||
|
currentLingvaInstance,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
// return masto.v1.statuses.translate(id, {
|
// return masto.v1.statuses.translate(id, {
|
||||||
// lang: DEFAULT_LANG,
|
// lang: DEFAULT_LANG,
|
||||||
// });
|
// });
|
||||||
|
@ -66,7 +88,7 @@ function TranslationBlock({
|
||||||
const translate = async () => {
|
const translate = async () => {
|
||||||
setUIState('loading');
|
setUIState('loading');
|
||||||
try {
|
try {
|
||||||
const { content, detectedSourceLanguage, provider, ...props } =
|
const { content, detectedSourceLanguage, provider, error, ...props } =
|
||||||
await onTranslate(text, apiSourceLang.current, targetLang);
|
await onTranslate(text, apiSourceLang.current, targetLang);
|
||||||
if (content) {
|
if (content) {
|
||||||
if (detectedSourceLanguage) {
|
if (detectedSourceLanguage) {
|
||||||
|
@ -89,7 +111,7 @@ function TranslationBlock({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.error(result);
|
if (error) console.error(error);
|
||||||
setUIState('error');
|
setUIState('error');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
@ -63,6 +63,11 @@
|
||||||
--close-button-color: rgba(0, 0, 0, 0.5);
|
--close-button-color: rgba(0, 0, 0, 0.5);
|
||||||
--close-button-hover-color: rgba(0, 0, 0, 1);
|
--close-button-hover-color: rgba(0, 0, 0, 1);
|
||||||
|
|
||||||
|
/* Video colors won't change based on color scheme */
|
||||||
|
--video-fg-color: #f0f2f5;
|
||||||
|
--video-bg-color: #242526;
|
||||||
|
--video-outline-color: color-mix(in lch, var(--video-fg-color), transparent);
|
||||||
|
|
||||||
--timing-function: cubic-bezier(0.3, 0.5, 0, 1);
|
--timing-function: cubic-bezier(0.3, 0.5, 0, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -438,3 +443,24 @@ kbd {
|
||||||
.shazam-container-inner {
|
.shazam-container-inner {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes shazam-horizontal {
|
||||||
|
0% {
|
||||||
|
grid-template-columns: 0fr;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.shazam-container-horizontal {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
transition: grid-template-columns 0.5s ease-in-out;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.shazam-container-horizontal:not(.no-animation) {
|
||||||
|
animation: shazam-horizontal 0.5s ease-in-out both !important;
|
||||||
|
}
|
||||||
|
.shazam-container-horizontal[hidden] {
|
||||||
|
grid-template-columns: 0fr;
|
||||||
|
}
|
||||||
|
|
|
@ -30,7 +30,7 @@ export default function HttpRoute() {
|
||||||
<>
|
<>
|
||||||
<h2>Unable to process URL</h2>
|
<h2>Unable to process URL</h2>
|
||||||
<p>
|
<p>
|
||||||
<a href={url} target="_blank">
|
<a href={url} target="_blank" rel="noopener noreferrer">
|
||||||
{url}
|
{url}
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
|
@ -4,6 +4,11 @@
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
animation: appear 0.2s ease-out;
|
animation: appear 0.2s ease-out;
|
||||||
clear: both;
|
clear: both;
|
||||||
|
|
||||||
|
b[tabindex='0']:is(:hover, :focus) {
|
||||||
|
text-decoration: underline;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.notification.notification-mention {
|
.notification.notification-mention {
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
|
@ -93,8 +98,8 @@
|
||||||
}
|
}
|
||||||
.notification .status-link:not(.status-type-mention) > .status {
|
.notification .status-link:not(.status-type-mention) > .status {
|
||||||
font-size: calc(var(--text-size) * 0.9);
|
font-size: calc(var(--text-size) * 0.9);
|
||||||
max-height: 160px;
|
}
|
||||||
overflow: hidden;
|
.notification .status-link.truncated:not(.status-type-mention) > .status {
|
||||||
/* fade out mask gradient bottom */
|
/* fade out mask gradient bottom */
|
||||||
mask-image: linear-gradient(
|
mask-image: linear-gradient(
|
||||||
rgba(0, 0, 0, 1) 130px,
|
rgba(0, 0, 0, 1) 130px,
|
||||||
|
@ -102,6 +107,33 @@
|
||||||
transparent 159px
|
transparent 159px
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
.notification .status-link.truncated {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.notification .status-link.truncated:after {
|
||||||
|
content: attr(data-read-more);
|
||||||
|
line-height: 1;
|
||||||
|
display: inline-block;
|
||||||
|
position: absolute;
|
||||||
|
--inset-offset: 16px;
|
||||||
|
inset-block-end: var(--inset-offset);
|
||||||
|
inset-inline-end: var(--inset-offset);
|
||||||
|
color: var(--text-color);
|
||||||
|
background-color: var(--bg-faded-color);
|
||||||
|
border: 1px dashed var(--link-color);
|
||||||
|
box-shadow: 0 0 0 1px var(--bg-color), 0 -5px 10px var(--bg-color),
|
||||||
|
0 -5px 15px var(--bg-color), 0 -5px 20px var(--bg-color);
|
||||||
|
padding: 0.5em 0.75em;
|
||||||
|
border-radius: 10em;
|
||||||
|
font-size: 90%;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-shadow: 0 -1px var(--bg-color);
|
||||||
|
transition: transform 0.2s ease-out;
|
||||||
|
}
|
||||||
|
.notification .status-link:is(:hover, :focus).truncated:after {
|
||||||
|
color: var(--link-color);
|
||||||
|
transform: translate(2px, 0);
|
||||||
|
}
|
||||||
.notification .status-link.status-type-mention {
|
.notification .status-link.status-type-mention {
|
||||||
max-height: 320px;
|
max-height: 320px;
|
||||||
filter: none;
|
filter: none;
|
||||||
|
|
|
@ -162,14 +162,12 @@ function Notifications({ columnMode }) {
|
||||||
}
|
}
|
||||||
}, [nearReachEnd, showMore]);
|
}, [nearReachEnd, showMore]);
|
||||||
|
|
||||||
const isHovering = useRef(false);
|
|
||||||
const idle = useIdle(5000);
|
const idle = useIdle(5000);
|
||||||
console.debug('🧘♀️ IDLE', idle);
|
console.debug('🧘♀️ IDLE', idle);
|
||||||
const loadUpdates = useCallback(() => {
|
const loadUpdates = useCallback(() => {
|
||||||
console.log('✨ Load updates', {
|
console.log('✨ Load updates', {
|
||||||
autoRefresh: snapStates.settings.autoRefresh,
|
autoRefresh: snapStates.settings.autoRefresh,
|
||||||
scrollTop: scrollableRef.current?.scrollTop === 0,
|
scrollTop: scrollableRef.current?.scrollTop === 0,
|
||||||
isHovering: isHovering.current,
|
|
||||||
inBackground: inBackground(),
|
inBackground: inBackground(),
|
||||||
notificationsShowNew: snapStates.notificationsShowNew,
|
notificationsShowNew: snapStates.notificationsShowNew,
|
||||||
uiState,
|
uiState,
|
||||||
|
@ -177,7 +175,7 @@ function Notifications({ columnMode }) {
|
||||||
if (
|
if (
|
||||||
snapStates.settings.autoRefresh &&
|
snapStates.settings.autoRefresh &&
|
||||||
scrollableRef.current?.scrollTop === 0 &&
|
scrollableRef.current?.scrollTop === 0 &&
|
||||||
(!isHovering.current || idle) &&
|
idle &&
|
||||||
!inBackground() &&
|
!inBackground() &&
|
||||||
snapStates.notificationsShowNew &&
|
snapStates.notificationsShowNew &&
|
||||||
uiState !== 'loading'
|
uiState !== 'loading'
|
||||||
|
@ -236,14 +234,6 @@ function Notifications({ columnMode }) {
|
||||||
class="deck-container"
|
class="deck-container"
|
||||||
ref={scrollableRef}
|
ref={scrollableRef}
|
||||||
tabIndex="-1"
|
tabIndex="-1"
|
||||||
onPointerEnter={() => {
|
|
||||||
console.log('👆 Pointer enter');
|
|
||||||
isHovering.current = true;
|
|
||||||
}}
|
|
||||||
onPointerLeave={() => {
|
|
||||||
console.log('👇 Pointer leave');
|
|
||||||
isHovering.current = false;
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div class={`timeline-deck deck ${onlyMentions ? 'only-mentions' : ''}`}>
|
<div class={`timeline-deck deck ${onlyMentions ? 'only-mentions' : ''}`}>
|
||||||
<header
|
<header
|
||||||
|
|
|
@ -87,7 +87,7 @@ function Search(props) {
|
||||||
if (type) {
|
if (type) {
|
||||||
params.limit = LIMIT;
|
params.limit = LIMIT;
|
||||||
params.type = type;
|
params.type = type;
|
||||||
params.offset = offsetRef.current;
|
if (authenticated) params.offset = offsetRef.current;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const results = await masto.v2.search(params);
|
const results = await masto.v2.search(params);
|
||||||
|
|
|
@ -34,6 +34,7 @@ function Settings({ onClose }) {
|
||||||
const currentTextSize = store.local.get('textSize') || DEFAULT_TEXT_SIZE;
|
const currentTextSize = store.local.get('textSize') || DEFAULT_TEXT_SIZE;
|
||||||
|
|
||||||
const [prefs, setPrefs] = useState(store.account.get('preferences') || {});
|
const [prefs, setPrefs] = useState(store.account.get('preferences') || {});
|
||||||
|
const { masto, authenticated } = api();
|
||||||
// Get preferences every time Settings is opened
|
// Get preferences every time Settings is opened
|
||||||
// NOTE: Disabled for now because I don't expect this to change often. Also for some reason, the /api/v1/preferences endpoint is cached for a while and return old prefs if refresh immediately after changing them.
|
// NOTE: Disabled for now because I don't expect this to change often. Also for some reason, the /api/v1/preferences endpoint is cached for a while and return old prefs if refresh immediately after changing them.
|
||||||
// useEffect(() => {
|
// useEffect(() => {
|
||||||
|
@ -169,12 +170,16 @@ function Settings({ onClose }) {
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
{authenticated && (
|
||||||
|
<>
|
||||||
<h3>Posting</h3>
|
<h3>Posting</h3>
|
||||||
<section>
|
<section>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<div>
|
<div>
|
||||||
<label for="posting-privacy-field">Default visibility</label>
|
<label for="posting-privacy-field">
|
||||||
|
Default visibility
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<select
|
<select
|
||||||
|
@ -182,7 +187,6 @@ function Settings({ onClose }) {
|
||||||
value={prefs['posting:default:visibility'] || 'public'}
|
value={prefs['posting:default:visibility'] || 'public'}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const { value } = e.target;
|
const { value } = e.target;
|
||||||
const { masto } = api();
|
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
await masto.v1.accounts.updateCredentials({
|
await masto.v1.accounts.updateCredentials({
|
||||||
|
@ -213,6 +217,8 @@ function Settings({ onClose }) {
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<h3>Experiments</h3>
|
<h3>Experiments</h3>
|
||||||
<section>
|
<section>
|
||||||
<ul>
|
<ul>
|
||||||
|
@ -384,6 +390,7 @@ function Settings({ onClose }) {
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
{authenticated && (
|
||||||
<li>
|
<li>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -396,9 +403,10 @@ function Settings({ onClose }) {
|
||||||
Unsent drafts
|
Unsent drafts
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
<PushNotificationsSection onClose={onClose} />
|
{authenticated && <PushNotificationsSection onClose={onClose} />}
|
||||||
<h3>About</h3>
|
<h3>About</h3>
|
||||||
<section>
|
<section>
|
||||||
<div
|
<div
|
||||||
|
@ -439,7 +447,11 @@ function Settings({ onClose }) {
|
||||||
</a>
|
</a>
|
||||||
)
|
)
|
||||||
<br />
|
<br />
|
||||||
<a href="https://github.com/cheeaun/phanpy" target="_blank">
|
<a
|
||||||
|
href="https://github.com/cheeaun/phanpy"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
Original
|
Original
|
||||||
</a>{' '}
|
</a>{' '}
|
||||||
by{' '}
|
by{' '}
|
||||||
|
|
|
@ -31,6 +31,9 @@
|
||||||
.ancestors-indicator {
|
.ancestors-indicator {
|
||||||
font-size: 70% !important;
|
font-size: 70% !important;
|
||||||
}
|
}
|
||||||
|
.ancestors-indicator:not([hidden]) {
|
||||||
|
animation: slide-up 0.3s both ease-out 0.3s;
|
||||||
|
}
|
||||||
.ancestors-indicator[hidden] {
|
.ancestors-indicator[hidden] {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|
|
@ -8,7 +8,7 @@ function emojifyText(text, emojis = []) {
|
||||||
const { shortcode, staticUrl, url } = emoji;
|
const { shortcode, staticUrl, url } = emoji;
|
||||||
text = text.replace(
|
text = text.replace(
|
||||||
new RegExp(`:${shortcode}:`, 'g'),
|
new RegExp(`:${shortcode}:`, 'g'),
|
||||||
`<picture><source srcset="${staticUrl}" media="(prefers-reduced-motion: reduce)"></source><img class="shortcode-emoji emoji" src="${url}" alt=":${shortcode}:" width="12" height="12" loading="lazy" decoding="async" /></picture>`,
|
`<picture><source srcset="${staticUrl}" media="(prefers-reduced-motion: reduce)"></source><img class="shortcode-emoji emoji" src="${url}" alt=":${shortcode}:" width="16" height="16" loading="lazy" decoding="async" /></picture>`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
// console.log(text, emojis);
|
// console.log(text, emojis);
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import emojifyText from './emojify-text';
|
import emojifyText from './emojify-text';
|
||||||
|
|
||||||
const fauxDiv = document.createElement('div');
|
const fauxDiv = document.createElement('div');
|
||||||
|
const whitelistLinkClasses = ['u-url', 'mention', 'hashtag'];
|
||||||
|
|
||||||
function enhanceContent(content, opts = {}) {
|
function enhanceContent(content, opts = {}) {
|
||||||
const { emojis, postEnhanceDOM = () => {} } = opts;
|
const { emojis, postEnhanceDOM = () => {} } = opts;
|
||||||
|
@ -10,15 +11,25 @@ function enhanceContent(content, opts = {}) {
|
||||||
const hasLink = /<a/i.test(enhancedContent);
|
const hasLink = /<a/i.test(enhancedContent);
|
||||||
const hasCodeBlock = enhancedContent.indexOf('```') !== -1;
|
const hasCodeBlock = enhancedContent.indexOf('```') !== -1;
|
||||||
|
|
||||||
|
if (hasLink) {
|
||||||
// Add target="_blank" to all links with no target="_blank"
|
// Add target="_blank" to all links with no target="_blank"
|
||||||
// E.g. `note` in `account`
|
// E.g. `note` in `account`
|
||||||
if (hasLink) {
|
|
||||||
const noTargetBlankLinks = Array.from(
|
const noTargetBlankLinks = Array.from(
|
||||||
dom.querySelectorAll('a:not([target="_blank"])'),
|
dom.querySelectorAll('a:not([target="_blank"])'),
|
||||||
);
|
);
|
||||||
noTargetBlankLinks.forEach((link) => {
|
noTargetBlankLinks.forEach((link) => {
|
||||||
link.setAttribute('target', '_blank');
|
link.setAttribute('target', '_blank');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Remove all classes except `u-url`, `mention`, `hashtag`
|
||||||
|
const links = Array.from(dom.querySelectorAll('a[class]'));
|
||||||
|
links.forEach((link) => {
|
||||||
|
Array.from(link.classList).forEach((c) => {
|
||||||
|
if (!whitelistLinkClasses.includes(c)) {
|
||||||
|
link.classList.remove(c);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add 'has-url-text' to all links that contains a url
|
// Add 'has-url-text' to all links that contains a url
|
||||||
|
|
33
src/utils/focus-deck.jsx
Normal file
33
src/utils/focus-deck.jsx
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
const focusDeck = () => {
|
||||||
|
let timer = setTimeout(() => {
|
||||||
|
const columns = document.getElementById('columns');
|
||||||
|
if (columns) {
|
||||||
|
// Focus first column
|
||||||
|
// columns.querySelector('.deck-container')?.focus?.();
|
||||||
|
} else {
|
||||||
|
const modals = document.querySelectorAll('#modal-container > *');
|
||||||
|
if (modals?.length) {
|
||||||
|
// Focus last modal
|
||||||
|
const modal = modals[modals.length - 1]; // last one
|
||||||
|
const modalFocusElement =
|
||||||
|
modal.querySelector('[tabindex="-1"]') || modal;
|
||||||
|
if (modalFocusElement) {
|
||||||
|
modalFocusElement.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const backDrop = document.querySelector('.deck-backdrop');
|
||||||
|
if (backDrop) return;
|
||||||
|
// Focus last deck
|
||||||
|
const pages = document.querySelectorAll('.deck-container');
|
||||||
|
const page = pages[pages.length - 1]; // last one
|
||||||
|
if (page && page.tabIndex === -1) {
|
||||||
|
console.log('FOCUS', page);
|
||||||
|
page.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default focusDeck;
|
|
@ -1,11 +1,17 @@
|
||||||
import states from './states';
|
import states from './states';
|
||||||
|
|
||||||
function handleContentLinks(opts) {
|
function handleContentLinks(opts) {
|
||||||
const { mentions = [], instance, previewMode } = opts || {};
|
const { mentions = [], instance, previewMode, statusURL } = opts || {};
|
||||||
return (e) => {
|
return (e) => {
|
||||||
let { target } = e;
|
let { target } = e;
|
||||||
target = target.closest('a');
|
target = target.closest('a');
|
||||||
if (!target) return;
|
if (!target) return;
|
||||||
|
|
||||||
|
// If cmd/ctrl/shift/alt key is pressed or middle-click, let the browser handle it
|
||||||
|
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.which === 2) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const prevText = target.previousSibling?.textContent;
|
const prevText = target.previousSibling?.textContent;
|
||||||
const textBeforeLinkIsAt = prevText?.endsWith('@');
|
const textBeforeLinkIsAt = prevText?.endsWith('@');
|
||||||
const textStartsWithAt = target.innerText.startsWith('@');
|
const textStartsWithAt = target.innerText.startsWith('@');
|
||||||
|
@ -50,7 +56,11 @@ function handleContentLinks(opts) {
|
||||||
const hashURL = instance ? `#/${instance}/t/${tag}` : `#/t/${tag}`;
|
const hashURL = instance ? `#/${instance}/t/${tag}` : `#/t/${tag}`;
|
||||||
console.log({ hashURL });
|
console.log({ hashURL });
|
||||||
location.hash = hashURL;
|
location.hash = hashURL;
|
||||||
} else if (states.unfurledLinks[target.href]?.url) {
|
} else if (
|
||||||
|
states.unfurledLinks[target.href]?.url &&
|
||||||
|
statusURL !== target.href
|
||||||
|
) {
|
||||||
|
// If unfurled AND not self-referential
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
states.prevLocation = {
|
states.prevLocation = {
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import Toastify from 'toastify-js';
|
import Toastify from 'toastify-js';
|
||||||
|
|
||||||
|
window._showToast = showToast;
|
||||||
|
|
||||||
function showToast(props) {
|
function showToast(props) {
|
||||||
if (typeof props === 'string') {
|
if (typeof props === 'string') {
|
||||||
props = { text: props };
|
props = { text: props };
|
||||||
|
|
|
@ -139,6 +139,7 @@ export function hideAllModals() {
|
||||||
states.showMediaModal = false;
|
states.showMediaModal = false;
|
||||||
states.showShortcutsSettings = false;
|
states.showShortcutsSettings = false;
|
||||||
states.showKeyboardShortcutsHelp = false;
|
states.showKeyboardShortcutsHelp = false;
|
||||||
|
states.showGenericAccounts = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function statusKey(id, instance) {
|
export function statusKey(id, instance) {
|
||||||
|
|
17
src/utils/useTruncated.js
Normal file
17
src/utils/useTruncated.js
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import { useRef } from 'preact/hooks';
|
||||||
|
import useResizeObserver from 'use-resize-observer';
|
||||||
|
|
||||||
|
export default function useTruncated({ className = 'truncated' } = {}) {
|
||||||
|
const ref = useRef();
|
||||||
|
useResizeObserver({
|
||||||
|
ref,
|
||||||
|
box: 'border-box',
|
||||||
|
onResize: ({ height }) => {
|
||||||
|
if (ref.current) {
|
||||||
|
const { scrollHeight } = ref.current;
|
||||||
|
ref.current.classList.toggle(className, scrollHeight > height);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return ref;
|
||||||
|
}
|
Loading…
Reference in a new issue