kopia lustrzana https://github.com/cheeaun/phanpy
Porównaj commity
97 Commity
2024.04.04
...
main
Autor | SHA1 | Data |
---|---|---|
Lim Chee Aun | bd8817e61b | |
Lim Chee Aun | ef712c62a9 | |
Lim Chee Aun | 9aa2bac685 | |
Lim Chee Aun | 34077e8467 | |
Lim Chee Aun | b473061845 | |
Lim Chee Aun | 64c7b5b4f0 | |
Lim Chee Aun | c11bbbb2b3 | |
Lim Chee Aun | 2c1a6c8cb5 | |
Lim Chee Aun | 67a85e1eef | |
Lim Chee Aun | 2e0ef6494b | |
Lim Chee Aun | 012b86d7ce | |
Lim Chee Aun | 0c45f515f0 | |
Lim Chee Aun | 9cc590be1b | |
Lim Chee Aun | 7589ec8803 | |
Lim Chee Aun | cd17ca0b42 | |
Lim Chee Aun | 8aab997900 | |
Lim Chee Aun | 96c44ed485 | |
Lim Chee Aun | 7053fcc96a | |
Lim Chee Aun | ad7cb46547 | |
Lim Chee Aun | 1b1af67064 | |
Lim Chee Aun | bdd238de0e | |
Lim Chee Aun | ced4dc86aa | |
Lim Chee Aun | 7be1e589ab | |
Lim Chee Aun | 7da1745cca | |
Lim Chee Aun | 025a5429cc | |
Lim Chee Aun | 62f843b4dc | |
Lim Chee Aun | b0a53b7fa1 | |
Lim Chee Aun | 9934daeb4d | |
Lim Chee Aun | d4a0a080b5 | |
Lim Chee Aun | bc4e3b0f72 | |
Lim Chee Aun | ac760265da | |
Lim Chee Aun | 98b0ccf032 | |
Lim Chee Aun | 90f06c511a | |
Lim Chee Aun | e7aad03279 | |
Lim Chee Aun | 1c6b0aa0d7 | |
Lim Chee Aun | 3e1b9ff53d | |
Lim Chee Aun | 5c9a47c31e | |
Lim Chee Aun | 65a4c3441c | |
Lim Chee Aun | 77bc06545c | |
Lim Chee Aun | 11e64a2cc4 | |
Lim Chee Aun | 5433e4e119 | |
Lim Chee Aun | c8dc32b884 | |
Lim Chee Aun | 1f29aee26e | |
Lim Chee Aun | daae055f4d | |
Chee Aun | 044f754d7e | |
Mick O'Brien | 5ae2058c07 | |
Lim Chee Aun | 7376cb1e99 | |
Lim Chee Aun | ffbae70178 | |
Lim Chee Aun | 9235d2c800 | |
Lim Chee Aun | 6ccefaebe1 | |
Lim Chee Aun | 5a448c8049 | |
Lim Chee Aun | 9bf77fa97a | |
Lim Chee Aun | b9058c6e3d | |
Lim Chee Aun | 55ad6500bc | |
Lim Chee Aun | f4b95d254c | |
Lim Chee Aun | effbe189e1 | |
Lim Chee Aun | 44e910b8c9 | |
Lim Chee Aun | a68dccd7cf | |
Lim Chee Aun | 9a6364a674 | |
Lim Chee Aun | e2f39596f0 | |
Lim Chee Aun | 701b9e99b3 | |
Lim Chee Aun | 294ab2bf00 | |
Lim Chee Aun | 304ce5a3e8 | |
Lim Chee Aun | 57390a291b | |
Lim Chee Aun | cd5920114f | |
Lim Chee Aun | 06c6360cae | |
Lim Chee Aun | afdfdb86da | |
Lim Chee Aun | 6f8f3e4fd0 | |
Lim Chee Aun | 342ff20986 | |
Lim Chee Aun | 94996d098e | |
Lim Chee Aun | c286562ee8 | |
Lim Chee Aun | 5babdc9d63 | |
Lim Chee Aun | 260bb8746d | |
Lim Chee Aun | 7be620808f | |
Lim Chee Aun | df3aca70fa | |
Lim Chee Aun | ec65163c89 | |
Lim Chee Aun | 6f22ec3842 | |
Lim Chee Aun | 2faf9b4c20 | |
Lim Chee Aun | 501e43207b | |
Lim Chee Aun | e782cc0dde | |
Lim Chee Aun | aefda31c2a | |
Lim Chee Aun | 9285a0ba9a | |
Chee Aun | 7fb56d9f6c | |
steve mookie kong | f7c69e56e9 | |
Lim Chee Aun | c3bcf3d595 | |
Lim Chee Aun | 0efa39b825 | |
Lim Chee Aun | a0d2037007 | |
Lim Chee Aun | 6e73728e2b | |
Lim Chee Aun | 60920966d6 | |
Lim Chee Aun | 5083463942 | |
Lim Chee Aun | 8b5fee3dfd | |
Lim Chee Aun | c9124bf150 | |
Lim Chee Aun | b85174155c | |
Lim Chee Aun | 5c9f6bae3c | |
Lim Chee Aun | 4e5940900e | |
Lim Chee Aun | 7fa0b4f076 | |
Lim Chee Aun | ecfcc68f15 |
|
@ -179,6 +179,9 @@ Available variables:
|
||||||
- May specify a self-hosted Lingva instance, powered by either [lingva-translate](https://github.com/thedaviddelta/lingva-translate) or [lingva-api](https://github.com/cheeaun/lingva-api)
|
- May specify a self-hosted Lingva instance, powered by either [lingva-translate](https://github.com/thedaviddelta/lingva-translate) or [lingva-api](https://github.com/cheeaun/lingva-api)
|
||||||
- List of fallback instances hard-coded in `/.env`
|
- List of fallback instances hard-coded in `/.env`
|
||||||
- [↗️ List of lingva-translate instances](https://github.com/thedaviddelta/lingva-translate?tab=readme-ov-file#instances)
|
- [↗️ List of lingva-translate instances](https://github.com/thedaviddelta/lingva-translate?tab=readme-ov-file#instances)
|
||||||
|
- `PHANPY_IMG_ALT_API_URL` (optional, no defaults):
|
||||||
|
- API endpoint for self-hosted instance of [img-alt-api](https://github.com/cheeaun/img-alt-api).
|
||||||
|
- If provided, a setting will appear for users to enable the image description generator in the composer. Disabled by default.
|
||||||
- `PHANPY_GIPHY_API_KEY` (optional, no defaults):
|
- `PHANPY_GIPHY_API_KEY` (optional, no defaults):
|
||||||
- API key for [GIPHY](https://developers.giphy.com/). See [API docs](https://developers.giphy.com/docs/api/).
|
- API key for [GIPHY](https://developers.giphy.com/). See [API docs](https://developers.giphy.com/docs/api/).
|
||||||
- If provided, a setting will appear for users to enable the GIF picker in the composer. Disabled by default.
|
- If provided, a setting will appear for users to enable the GIF picker in the composer. Disabled by default.
|
||||||
|
@ -205,6 +208,7 @@ These are self-hosted by other wonderful folks.
|
||||||
- [phanpy.hear-me.social](https://phanpy.hear-me.social) by [@admin@hear-me.social](https://hear-me.social/@admin)
|
- [phanpy.hear-me.social](https://phanpy.hear-me.social) by [@admin@hear-me.social](https://hear-me.social/@admin)
|
||||||
- [phanpy.fulda.social](https://phanpy.fulda.social) by [@Ganneff@fulda.social](https://fulda.social/@Ganneff)
|
- [phanpy.fulda.social](https://phanpy.fulda.social) by [@Ganneff@fulda.social](https://fulda.social/@Ganneff)
|
||||||
- [phanpy.crmbl.uk](https://phanpy.crmbl.uk) by [@snail@crmbl.uk](https://mstdn.crmbl.uk/@snail)
|
- [phanpy.crmbl.uk](https://phanpy.crmbl.uk) by [@snail@crmbl.uk](https://mstdn.crmbl.uk/@snail)
|
||||||
|
- [halo.mookiesplace.com](https://halo.mookiesplace.com) by [@mookie@mookiesplace.com](https://mookiesplace.com/@mookie)
|
||||||
|
|
||||||
> Note: Add yours by creating a pull request.
|
> Note: Add yours by creating a pull request.
|
||||||
|
|
||||||
|
@ -240,6 +244,8 @@ And here I am. Building a Mastodon web client.
|
||||||
|
|
||||||
## Alternative web clients
|
## Alternative web clients
|
||||||
|
|
||||||
|
- Phanpy forks ↓
|
||||||
|
- [Agora](https://agorasocial.app/)
|
||||||
- [Pinafore](https://pinafore.social/) ([retired](https://nolanlawson.com/2023/01/09/retiring-pinafore/)) - forks ↓
|
- [Pinafore](https://pinafore.social/) ([retired](https://nolanlawson.com/2023/01/09/retiring-pinafore/)) - forks ↓
|
||||||
- [Semaphore](https://semaphore.social/)
|
- [Semaphore](https://semaphore.social/)
|
||||||
- [Enafore](https://enafore.social/)
|
- [Enafore](https://enafore.social/)
|
||||||
|
@ -256,6 +262,7 @@ And here I am. Building a Mastodon web client.
|
||||||
- [Tusked](https://tusked.app/)
|
- [Tusked](https://tusked.app/)
|
||||||
- [Mastodon Glitch Edition (standalone frontend)](https://iceshrimp.dev/iceshrimp/masto-fe-standalone)
|
- [Mastodon Glitch Edition (standalone frontend)](https://iceshrimp.dev/iceshrimp/masto-fe-standalone)
|
||||||
- [Mangane](https://github.com/BDX-town/Mangane)
|
- [Mangane](https://github.com/BDX-town/Mangane)
|
||||||
|
- [TheDesk](https://github.com/cutls/TheDesk)
|
||||||
- [More...](https://github.com/hueyy/awesome-mastodon/#clients)
|
- [More...](https://github.com/hueyy/awesome-mastodon/#clients)
|
||||||
|
|
||||||
## 💁♂️ Notice to all other social media client developers
|
## 💁♂️ Notice to all other social media client developers
|
||||||
|
|
Plik diff jest za duży
Load Diff
33
package.json
33
package.json
|
@ -11,34 +11,35 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@formatjs/intl-localematcher": "~0.5.4",
|
"@formatjs/intl-localematcher": "~0.5.4",
|
||||||
"@formatjs/intl-segmenter": "~11.5.5",
|
"@formatjs/intl-segmenter": "~11.5.7",
|
||||||
"@formkit/auto-animate": "~0.8.1",
|
"@formkit/auto-animate": "~0.8.2",
|
||||||
"@github/text-expander-element": "~2.6.1",
|
"@github/text-expander-element": "2.6.1",
|
||||||
"@iconify-icons/mingcute": "~1.2.9",
|
"@iconify-icons/mingcute": "~1.2.9",
|
||||||
"@justinribeiro/lite-youtube": "~1.5.0",
|
"@justinribeiro/lite-youtube": "~1.5.0",
|
||||||
"@szhsin/react-menu": "~4.1.0",
|
"@szhsin/react-menu": "~4.1.0",
|
||||||
"@uidotdev/usehooks": "~2.4.1",
|
"@uidotdev/usehooks": "~2.4.1",
|
||||||
"compare-versions": "~6.1.0",
|
"compare-versions": "~6.1.0",
|
||||||
"dayjs": "~1.11.10",
|
"dayjs": "~1.11.11",
|
||||||
"dayjs-twitter": "~0.5.0",
|
"dayjs-twitter": "~0.5.0",
|
||||||
"fast-blurhash": "~1.1.2",
|
"fast-blurhash": "~1.1.2",
|
||||||
"fast-equals": "~5.0.1",
|
"fast-equals": "~5.0.1",
|
||||||
|
"fuse.js": "~7.0.0",
|
||||||
"html-prettify": "^1.0.7",
|
"html-prettify": "^1.0.7",
|
||||||
"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": "~6.7.0",
|
"masto": "~6.7.7",
|
||||||
"moize": "~6.1.6",
|
"moize": "~6.1.6",
|
||||||
"p-retry": "~6.2.0",
|
"p-retry": "~6.2.0",
|
||||||
"p-throttle": "~6.1.0",
|
"p-throttle": "~6.1.0",
|
||||||
"preact": "~10.20.1",
|
"preact": "~10.22.0",
|
||||||
"punycode": "~2.3.1",
|
"punycode": "~2.3.1",
|
||||||
"react-hotkeys-hook": "~4.5.0",
|
"react-hotkeys-hook": "~4.5.0",
|
||||||
"react-intersection-observer": "~9.8.1",
|
"react-intersection-observer": "~9.10.2",
|
||||||
"react-quick-pinch-zoom": "~5.1.0",
|
"react-quick-pinch-zoom": "~5.1.0",
|
||||||
"react-router-dom": "6.6.2",
|
"react-router-dom": "6.6.2",
|
||||||
"string-length": "6.0.0",
|
"string-length": "6.0.0",
|
||||||
"swiped-events": "~1.1.9",
|
"swiped-events": "~1.2.0",
|
||||||
"toastify-js": "~1.12.0",
|
"toastify-js": "~1.12.0",
|
||||||
"uid": "~2.0.2",
|
"uid": "~2.0.2",
|
||||||
"use-debounce": "~10.0.0",
|
"use-debounce": "~10.0.0",
|
||||||
|
@ -50,18 +51,18 @@
|
||||||
"@preact/preset-vite": "~2.8.2",
|
"@preact/preset-vite": "~2.8.2",
|
||||||
"@trivago/prettier-plugin-sort-imports": "~4.3.0",
|
"@trivago/prettier-plugin-sort-imports": "~4.3.0",
|
||||||
"postcss": "~8.4.38",
|
"postcss": "~8.4.38",
|
||||||
"postcss-dark-theme-class": "~1.2.1",
|
"postcss-dark-theme-class": "~1.3.0",
|
||||||
"postcss-preset-env": "~9.5.2",
|
"postcss-preset-env": "~9.5.13",
|
||||||
"twitter-text": "~3.1.0",
|
"twitter-text": "~3.1.0",
|
||||||
"vite": "~5.2.6",
|
"vite": "~5.2.11",
|
||||||
"vite-plugin-generate-file": "~0.1.1",
|
"vite-plugin-generate-file": "~0.1.1",
|
||||||
"vite-plugin-html-config": "~1.0.11",
|
"vite-plugin-html-config": "~1.0.11",
|
||||||
"vite-plugin-pwa": "~0.19.7",
|
"vite-plugin-pwa": "~0.20.0",
|
||||||
"vite-plugin-remove-console": "~2.2.0",
|
"vite-plugin-remove-console": "~2.2.0",
|
||||||
"workbox-cacheable-response": "~7.0.0",
|
"workbox-cacheable-response": "~7.1.0",
|
||||||
"workbox-expiration": "~7.0.0",
|
"workbox-expiration": "~7.1.0",
|
||||||
"workbox-routing": "~7.0.0",
|
"workbox-routing": "~7.1.0",
|
||||||
"workbox-strategies": "~7.0.0"
|
"workbox-strategies": "~7.1.0"
|
||||||
},
|
},
|
||||||
"postcss": {
|
"postcss": {
|
||||||
"plugins": {
|
"plugins": {
|
||||||
|
|
|
@ -62,7 +62,7 @@ const iconsRoute = new Route(
|
||||||
cacheName: 'icons',
|
cacheName: 'icons',
|
||||||
plugins: [
|
plugins: [
|
||||||
new ExpirationPlugin({
|
new ExpirationPlugin({
|
||||||
maxEntries: 50,
|
maxEntries: 300,
|
||||||
maxAgeSeconds: 3 * 24 * 60 * 60, // 3 days
|
maxAgeSeconds: 3 * 24 * 60 * 60, // 3 days
|
||||||
purgeOnQuotaError: true,
|
purgeOnQuotaError: true,
|
||||||
}),
|
}),
|
||||||
|
|
90
src/app.css
90
src/app.css
|
@ -301,6 +301,41 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.deck-container-media-first {
|
||||||
|
.timeline {
|
||||||
|
> li:not(.timeline-item-carousel, .timeline-item-container) {
|
||||||
|
&:has(.status-media-first) {
|
||||||
|
@media (min-width: 40em) {
|
||||||
|
width: fit-content;
|
||||||
|
max-width: min(480px, 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
background-color: transparent !important;
|
||||||
|
border: 0 !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
margin-inline: auto !important;
|
||||||
|
|
||||||
|
&:not(:first-child) {
|
||||||
|
margin-block: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:has(.skeleton) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:has(.media[data-orientation='landscape']) {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-link:has(.status-media-first):hover {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.timeline.grow {
|
.timeline.grow {
|
||||||
/* min-height: 100vh;
|
/* min-height: 100vh;
|
||||||
min-height: 100dvh; */
|
min-height: 100dvh; */
|
||||||
|
@ -1544,8 +1579,8 @@ body:has(.media-modal-container + .status-deck) .media-post-link {
|
||||||
0 10px 36px -4px var(--button-bg-blur-color);
|
0 10px 36px -4px var(--button-bg-blur-color);
|
||||||
transition: all 0.3s ease-in-out;
|
transition: all 0.3s ease-in-out;
|
||||||
}
|
}
|
||||||
.deck-container:has(header[hidden]) ~ #compose-button,
|
.deck-container:has(header[hidden]) ~ #compose-button:not(.loading),
|
||||||
#compose-button[hidden] {
|
#compose-button[hidden]:not(.loading) {
|
||||||
transform: translateY(200%);
|
transform: translateY(200%);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
@ -1574,6 +1609,48 @@ body:has(.media-modal-container + .status-deck) .media-post-link {
|
||||||
bottom: calc(16px + env(safe-area-inset-bottom) + 52px);
|
bottom: calc(16px + env(safe-area-inset-bottom) + 52px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#compose-button {
|
||||||
|
&.min {
|
||||||
|
outline: 2px solid var(--button-text-color);
|
||||||
|
z-index: 1001; /* Higher than modal */
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: var(--button-bg-color);
|
||||||
|
border: 2px solid var(--button-text-color);
|
||||||
|
box-shadow: 0 2px 8px var(--drop-shadow-color);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s ease-out 0.5s;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.loading {
|
||||||
|
outline-color: var(--button-bg-blur-color);
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
content: '';
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 5s linear infinite;
|
||||||
|
border: 2px dashed var(--button-text-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.error {
|
||||||
|
&:after {
|
||||||
|
background-color: var(--red-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* SHEET */
|
/* SHEET */
|
||||||
|
|
||||||
|
@ -1882,7 +1959,8 @@ body > .szh-menu-container {
|
||||||
/* two columns only */
|
/* two columns only */
|
||||||
grid-template-columns: repeat(2, 1fr);
|
grid-template-columns: repeat(2, 1fr);
|
||||||
}
|
}
|
||||||
.szh-menu .menu-horizontal:has(> .szh-menu__item:only-child) {
|
.szh-menu .menu-horizontal:has(> .szh-menu__item:only-child),
|
||||||
|
.szh-menu .menu-horizontal:has(> .szh-menu__submenu:only-child) {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
.szh-menu .menu-horizontal > .szh-menu__item:not(:only-child):first-child,
|
.szh-menu .menu-horizontal > .szh-menu__item:not(:only-child):first-child,
|
||||||
|
@ -1930,6 +2008,10 @@ body > .szh-menu-container {
|
||||||
.szh-menu
|
.szh-menu
|
||||||
.szh-menu__item.danger:not(.szh-menu__item--disabled).szh-menu__item--hover {
|
.szh-menu__item.danger:not(.szh-menu__item--disabled).szh-menu__item--hover {
|
||||||
background-color: var(--red-text-color);
|
background-color: var(--red-text-color);
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
background-color: var(--red-color);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.szh-menu
|
.szh-menu
|
||||||
.szh-menu__item:not(.szh-menu__item--disabled):not(
|
.szh-menu__item:not(.szh-menu__item--disabled):not(
|
||||||
|
@ -2143,6 +2225,8 @@ body > .szh-menu-container {
|
||||||
box-shadow: 0 3px 8px -1px var(--drop-shadow-color),
|
box-shadow: 0 3px 8px -1px var(--drop-shadow-color),
|
||||||
0 10px 36px -4px var(--button-bg-blur-color);
|
0 10px 36px -4px var(--button-bg-blur-color);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
width: fit-content;
|
||||||
|
max-width: calc(100vw - 32px);
|
||||||
}
|
}
|
||||||
.toastify-bottom {
|
.toastify-bottom {
|
||||||
margin-bottom: env(safe-area-inset-bottom);
|
margin-bottom: env(safe-area-inset-bottom);
|
||||||
|
|
|
@ -53,7 +53,7 @@ import { getAccessToken } from './utils/auth';
|
||||||
import focusDeck from './utils/focus-deck';
|
import focusDeck from './utils/focus-deck';
|
||||||
import states, { initStates, statusKey } from './utils/states';
|
import states, { initStates, statusKey } from './utils/states';
|
||||||
import store from './utils/store';
|
import store from './utils/store';
|
||||||
import { getCurrentAccount } from './utils/store-utils';
|
import { getCurrentAccount, setCurrentAccountID } from './utils/store-utils';
|
||||||
import './utils/toast-alert';
|
import './utils/toast-alert';
|
||||||
|
|
||||||
window.__STATES__ = states;
|
window.__STATES__ = states;
|
||||||
|
@ -338,7 +338,7 @@ function App() {
|
||||||
window.__IGNORE_GET_ACCOUNT_ERROR__ = true;
|
window.__IGNORE_GET_ACCOUNT_ERROR__ = true;
|
||||||
const account = getCurrentAccount();
|
const account = getCurrentAccount();
|
||||||
if (account) {
|
if (account) {
|
||||||
store.session.set('currentAccount', account.info.id);
|
setCurrentAccountID(account.info.id);
|
||||||
const { client } = api({ account });
|
const { client } = api({ account });
|
||||||
const { instance } = client;
|
const { instance } = client;
|
||||||
// console.log('masto', masto);
|
// console.log('masto', masto);
|
||||||
|
|
|
@ -108,4 +108,5 @@ export const ICONS = {
|
||||||
settings: () => import('@iconify-icons/mingcute/settings-6-line'),
|
settings: () => import('@iconify-icons/mingcute/settings-6-line'),
|
||||||
'heart-break': () => import('@iconify-icons/mingcute/heart-crack-line'),
|
'heart-break': () => import('@iconify-icons/mingcute/heart-crack-line'),
|
||||||
'user-x': () => import('@iconify-icons/mingcute/user-x-line'),
|
'user-x': () => import('@iconify-icons/mingcute/user-x-line'),
|
||||||
|
minimize: () => import('@iconify-icons/mingcute/arrows-down-line'),
|
||||||
};
|
};
|
||||||
|
|
|
@ -133,21 +133,18 @@ function AccountBlock({
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
{showActivity && (
|
{showActivity && (
|
||||||
<>
|
<div class="account-block-stats">
|
||||||
<br />
|
Posts: {shortenNumber(statusesCount)}
|
||||||
<small class="last-status-at insignificant">
|
{!!lastStatusAt && (
|
||||||
Posts: {statusesCount}
|
<>
|
||||||
{!!lastStatusAt && (
|
{' '}
|
||||||
<>
|
· Last posted:{' '}
|
||||||
{' '}
|
{niceDateTime(lastStatusAt, {
|
||||||
· Last posted:{' '}
|
hideTime: true,
|
||||||
{niceDateTime(lastStatusAt, {
|
})}
|
||||||
hideTime: true,
|
</>
|
||||||
})}
|
)}
|
||||||
</>
|
</div>
|
||||||
)}
|
|
||||||
</small>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
{showStats && (
|
{showStats && (
|
||||||
<div class="account-block-stats">
|
<div class="account-block-stats">
|
||||||
|
|
|
@ -19,10 +19,12 @@ import { getLists } from '../utils/lists';
|
||||||
import niceDateTime from '../utils/nice-date-time';
|
import niceDateTime from '../utils/nice-date-time';
|
||||||
import pmem from '../utils/pmem';
|
import pmem from '../utils/pmem';
|
||||||
import shortenNumber from '../utils/shorten-number';
|
import shortenNumber from '../utils/shorten-number';
|
||||||
|
import showCompose from '../utils/show-compose';
|
||||||
import showToast from '../utils/show-toast';
|
import showToast from '../utils/show-toast';
|
||||||
import states, { hideAllModals } from '../utils/states';
|
import states, { hideAllModals } from '../utils/states';
|
||||||
import store from '../utils/store';
|
import store from '../utils/store';
|
||||||
import { updateAccount } from '../utils/store-utils';
|
import { getCurrentAccountID, updateAccount } from '../utils/store-utils';
|
||||||
|
import supports from '../utils/supports';
|
||||||
|
|
||||||
import AccountBlock from './account-block';
|
import AccountBlock from './account-block';
|
||||||
import Avatar from './avatar';
|
import Avatar from './avatar';
|
||||||
|
@ -185,6 +187,7 @@ function AccountInfo({
|
||||||
memorial,
|
memorial,
|
||||||
moved,
|
moved,
|
||||||
roles,
|
roles,
|
||||||
|
hideCollections,
|
||||||
} = info || {};
|
} = info || {};
|
||||||
let headerIsAvatar = false;
|
let headerIsAvatar = false;
|
||||||
let { header, headerStatic } = info || {};
|
let { header, headerStatic } = info || {};
|
||||||
|
@ -198,10 +201,7 @@ function AccountInfo({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const isSelf = useMemo(
|
const isSelf = useMemo(() => id === getCurrentAccountID(), [id]);
|
||||||
() => id === store.session.get('currentAccount'),
|
|
||||||
[id],
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const infoHasEssentials = !!(
|
const infoHasEssentials = !!(
|
||||||
|
@ -254,12 +254,13 @@ function AccountInfo({
|
||||||
// On first load, fetch familiar followers, merge to top of results' `value`
|
// On first load, fetch familiar followers, merge to top of results' `value`
|
||||||
// Remove dups on every fetch
|
// Remove dups on every fetch
|
||||||
if (firstLoad) {
|
if (firstLoad) {
|
||||||
const familiarFollowers = await masto.v1.accounts.familiarFollowers.fetch(
|
let familiarFollowers = [];
|
||||||
{
|
try {
|
||||||
|
familiarFollowers = await masto.v1.accounts.familiarFollowers.fetch({
|
||||||
id: [id],
|
id: [id],
|
||||||
},
|
});
|
||||||
);
|
} catch (e) {}
|
||||||
familiarFollowersCache.current = familiarFollowers[0].accounts;
|
familiarFollowersCache.current = familiarFollowers?.[0]?.accounts || [];
|
||||||
newValue = [
|
newValue = [
|
||||||
...familiarFollowersCache.current,
|
...familiarFollowersCache.current,
|
||||||
...value.filter(
|
...value.filter(
|
||||||
|
@ -678,6 +679,9 @@ function AccountInfo({
|
||||||
excludeRelationshipAttrs: isSelf
|
excludeRelationshipAttrs: isSelf
|
||||||
? ['followedBy']
|
? ['followedBy']
|
||||||
: [],
|
: [],
|
||||||
|
blankCopy: hideCollections
|
||||||
|
? 'This user has chosen to not make this information available.'
|
||||||
|
: undefined,
|
||||||
};
|
};
|
||||||
}, 0);
|
}, 0);
|
||||||
}}
|
}}
|
||||||
|
@ -713,6 +717,9 @@ function AccountInfo({
|
||||||
fetchAccounts: fetchFollowing,
|
fetchAccounts: fetchFollowing,
|
||||||
instance,
|
instance,
|
||||||
excludeRelationshipAttrs: isSelf ? ['following'] : [],
|
excludeRelationshipAttrs: isSelf ? ['following'] : [],
|
||||||
|
blankCopy: hideCollections
|
||||||
|
? 'This user has chosen to not make this information available.'
|
||||||
|
: undefined,
|
||||||
};
|
};
|
||||||
}, 0);
|
}, 0);
|
||||||
}}
|
}}
|
||||||
|
@ -919,7 +926,7 @@ function RelatedActions({
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (info) {
|
if (info) {
|
||||||
const currentAccount = store.session.get('currentAccount');
|
const currentAccount = getCurrentAccountID();
|
||||||
let currentID;
|
let currentID;
|
||||||
(async () => {
|
(async () => {
|
||||||
if (sameInstance && authenticated) {
|
if (sameInstance && authenticated) {
|
||||||
|
@ -1075,11 +1082,11 @@ function RelatedActions({
|
||||||
<>
|
<>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
states.showCompose = {
|
showCompose({
|
||||||
draftStatus: {
|
draftStatus: {
|
||||||
status: `@${currentInfo?.acct || acct} `,
|
status: `@${currentInfo?.acct || acct} `,
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="at" />
|
<Icon icon="at" />
|
||||||
|
@ -1093,16 +1100,18 @@ function RelatedActions({
|
||||||
<Icon icon="translate" />
|
<Icon icon="translate" />
|
||||||
<span>Translate bio</span>
|
<span>Translate bio</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
{supports('@mastodon/profile-private-note') && (
|
||||||
onClick={() => {
|
<MenuItem
|
||||||
setShowPrivateNoteModal(true);
|
onClick={() => {
|
||||||
}}
|
setShowPrivateNoteModal(true);
|
||||||
>
|
}}
|
||||||
<Icon icon="pencil" />
|
>
|
||||||
<span>
|
<Icon icon="pencil" />
|
||||||
{privateNote ? 'Edit private note' : 'Add private note'}
|
<span>
|
||||||
</span>
|
{privateNote ? 'Edit private note' : 'Add private note'}
|
||||||
</MenuItem>
|
</span>
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
{following && !!relationship && (
|
{following && !!relationship && (
|
||||||
<>
|
<>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
|
@ -1451,19 +1460,22 @@ function RelatedActions({
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{currentAuthenticated && isSelf && standalone && (
|
{currentAuthenticated &&
|
||||||
<>
|
isSelf &&
|
||||||
<MenuDivider />
|
standalone &&
|
||||||
<MenuItem
|
supports('@mastodon/profile-edit') && (
|
||||||
onClick={() => {
|
<>
|
||||||
setShowEditProfile(true);
|
<MenuDivider />
|
||||||
}}
|
<MenuItem
|
||||||
>
|
onClick={() => {
|
||||||
<Icon icon="pencil" />
|
setShowEditProfile(true);
|
||||||
<span>Edit profile</span>
|
}}
|
||||||
</MenuItem>
|
>
|
||||||
</>
|
<Icon icon="pencil" />
|
||||||
)}
|
<span>Edit profile</span>
|
||||||
|
</MenuItem>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
{import.meta.env.DEV && currentAuthenticated && isSelf && (
|
{import.meta.env.DEV && currentAuthenticated && isSelf && (
|
||||||
<>
|
<>
|
||||||
<MenuDivider />
|
<MenuDivider />
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
import { useSnapshot } from 'valtio';
|
||||||
|
|
||||||
import openCompose from '../utils/open-compose';
|
import openCompose from '../utils/open-compose';
|
||||||
import openOSK from '../utils/open-osk';
|
import openOSK from '../utils/open-osk';
|
||||||
|
@ -7,7 +8,15 @@ import states from '../utils/states';
|
||||||
import Icon from './icon';
|
import Icon from './icon';
|
||||||
|
|
||||||
export default function ComposeButton() {
|
export default function ComposeButton() {
|
||||||
|
const snapStates = useSnapshot(states);
|
||||||
|
|
||||||
function handleButton(e) {
|
function handleButton(e) {
|
||||||
|
if (snapStates.composerState.minimized) {
|
||||||
|
states.composerState.minimized = false;
|
||||||
|
openOSK();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (e.shiftKey) {
|
if (e.shiftKey) {
|
||||||
const newWin = openCompose();
|
const newWin = openCompose();
|
||||||
|
|
||||||
|
@ -28,7 +37,14 @@ export default function ComposeButton() {
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button type="button" id="compose-button" onClick={handleButton}>
|
<button
|
||||||
|
type="button"
|
||||||
|
id="compose-button"
|
||||||
|
onClick={handleButton}
|
||||||
|
class={`${snapStates.composerState.minimized ? 'min' : ''} ${
|
||||||
|
snapStates.composerState.publishing ? 'loading' : ''
|
||||||
|
} ${snapStates.composerState.publishingError ? 'error' : ''}`}
|
||||||
|
>
|
||||||
<Icon icon="quill" size="xl" alt="Compose" />
|
<Icon icon="quill" size="xl" alt="Compose" />
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
import { shouldPolyfill } from '@formatjs/intl-segmenter/should-polyfill';
|
||||||
|
import { useEffect, useState } from 'preact/hooks';
|
||||||
|
|
||||||
|
import Loader from './loader';
|
||||||
|
|
||||||
|
const supportsIntlSegmenter = !shouldPolyfill();
|
||||||
|
|
||||||
|
function importIntlSegmenter() {
|
||||||
|
if (!supportsIntlSegmenter) {
|
||||||
|
return import('@formatjs/intl-segmenter/polyfill-force').catch(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function importCompose() {
|
||||||
|
return import('./compose');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function preload() {
|
||||||
|
try {
|
||||||
|
await importIntlSegmenter();
|
||||||
|
importCompose();
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ComposeSuspense(props) {
|
||||||
|
const [Compose, setCompose] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
if (supportsIntlSegmenter) {
|
||||||
|
const component = await importCompose();
|
||||||
|
setCompose(component);
|
||||||
|
} else {
|
||||||
|
await importIntlSegmenter();
|
||||||
|
const component = await importCompose();
|
||||||
|
setCompose(component);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return Compose?.default ? <Compose.default {...props} /> : <Loader />;
|
||||||
|
}
|
|
@ -298,19 +298,25 @@
|
||||||
height: 2.2em;
|
height: 2.2em;
|
||||||
}
|
}
|
||||||
#compose-container .text-expander-menu li:is(:hover, :focus, [aria-selected]) {
|
#compose-container .text-expander-menu li:is(:hover, :focus, [aria-selected]) {
|
||||||
color: var(--bg-color);
|
background-color: var(--link-bg-color);
|
||||||
background-color: var(--link-color);
|
|
||||||
}
|
|
||||||
#compose-container
|
|
||||||
.text-expander-menu:hover
|
|
||||||
li[aria-selected]:not(:hover, :focus) {
|
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
background-color: var(--bg-color);
|
}
|
||||||
|
#compose-container .text-expander-menu li[aria-selected] {
|
||||||
|
box-shadow: inset 4px 0 0 0 var(--button-bg-color);
|
||||||
|
}
|
||||||
|
#compose-container .text-expander-menu li[data-more] {
|
||||||
|
&:not(:hover, :focus, [aria-selected]) {
|
||||||
|
color: var(--text-insignificant-color);
|
||||||
|
background-color: var(--bg-faded-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
font-size: 0.8em;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
#compose-container .form-visibility-direct {
|
#compose-container .form-visibility-direct {
|
||||||
--yellow-stripes: repeating-linear-gradient(
|
--yellow-stripes: repeating-linear-gradient(
|
||||||
-45deg,
|
135deg,
|
||||||
var(--reply-to-faded-color),
|
var(--reply-to-faded-color),
|
||||||
var(--reply-to-faded-color) 10px,
|
var(--reply-to-faded-color) 10px,
|
||||||
var(--reply-to-faded-color) 10px,
|
var(--reply-to-faded-color) 10px,
|
||||||
|
@ -334,6 +340,21 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
|
|
||||||
|
.media-error {
|
||||||
|
padding: 2px;
|
||||||
|
color: var(--orange-fg-color);
|
||||||
|
background-color: transparent;
|
||||||
|
border: 1.5px dashed transparent;
|
||||||
|
line-height: 1;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
&:is(:hover, :focus) {
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
border-color: var(--orange-fg-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
#compose-container .media-preview {
|
#compose-container .media-preview {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
@ -594,44 +615,194 @@
|
||||||
} */
|
} */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#mention-sheet {
|
||||||
|
height: 50vh;
|
||||||
|
|
||||||
|
.accounts-list {
|
||||||
|
--list-gap: 1px;
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 8px 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
row-gap: var(--list-gap);
|
||||||
|
|
||||||
|
&.loading {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
/* align-items: center; */
|
||||||
|
margin: 0 -8px;
|
||||||
|
padding: 8px;
|
||||||
|
gap: 8px;
|
||||||
|
position: relative;
|
||||||
|
justify-content: space-between;
|
||||||
|
border-radius: 8px;
|
||||||
|
/* align-items: center; */
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-image: linear-gradient(
|
||||||
|
to right,
|
||||||
|
transparent 75%,
|
||||||
|
var(--link-bg-color)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
background-image: linear-gradient(
|
||||||
|
to right,
|
||||||
|
var(--bg-faded-color) 75%,
|
||||||
|
var(--link-bg-color)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
border-top: var(--hairline-width) solid var(--divider-color);
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 58px;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:has(+ li:is(.selected, :hover)):before,
|
||||||
|
&:is(.selected, :hover):before {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
> button {
|
||||||
|
border-radius: 4px;
|
||||||
|
&:hover {
|
||||||
|
outline: 2px solid var(--button-bg-blur-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#custom-emojis-sheet {
|
#custom-emojis-sheet {
|
||||||
max-height: 50vh;
|
max-height: 50vh;
|
||||||
max-height: 50dvh;
|
max-height: 50dvh;
|
||||||
}
|
|
||||||
#custom-emojis-sheet main {
|
header {
|
||||||
mask-image: none;
|
.loader-container {
|
||||||
}
|
margin: 0;
|
||||||
#custom-emojis-sheet .custom-emojis-list .section-header {
|
}
|
||||||
font-size: 80%;
|
|
||||||
text-transform: uppercase;
|
form {
|
||||||
color: var(--text-insignificant-color);
|
margin: 8px 0 0;
|
||||||
padding: 8px 0 4px;
|
|
||||||
position: sticky;
|
input {
|
||||||
top: 0;
|
width: 100%;
|
||||||
background-color: var(--bg-blur-color);
|
min-width: 0;
|
||||||
backdrop-filter: blur(1px);
|
}
|
||||||
}
|
}
|
||||||
#custom-emojis-sheet .custom-emojis-list section {
|
}
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
main {
|
||||||
}
|
mask-image: none;
|
||||||
#custom-emojis-sheet .custom-emojis-list button {
|
min-height: 40vh;
|
||||||
border-radius: 8px;
|
padding-bottom: 88px;
|
||||||
background-image: radial-gradient(
|
}
|
||||||
closest-side,
|
|
||||||
var(--img-bg-color),
|
.custom-emojis-matches {
|
||||||
transparent
|
margin: 0;
|
||||||
);
|
padding: 0;
|
||||||
}
|
list-style: none;
|
||||||
#custom-emojis-sheet .custom-emojis-list button:is(:hover, :focus) {
|
display: flex;
|
||||||
filter: none;
|
flex-wrap: wrap;
|
||||||
background-color: var(--bg-faded-color);
|
}
|
||||||
}
|
|
||||||
#custom-emojis-sheet .custom-emojis-list button img {
|
.custom-emojis-list {
|
||||||
transition: transform 0.1s ease-out;
|
.section-header {
|
||||||
}
|
font-size: 80%;
|
||||||
#custom-emojis-sheet .custom-emojis-list button:is(:hover, :focus) img {
|
text-transform: uppercase;
|
||||||
transform: scale(1.5);
|
color: var(--text-insignificant-color);
|
||||||
|
padding: 8px 0 4px;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
section {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
color: var(--text-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
background-image: radial-gradient(
|
||||||
|
closest-side,
|
||||||
|
var(--img-bg-color),
|
||||||
|
transparent
|
||||||
|
);
|
||||||
|
text-shadow: 0 1px 0 var(--bg-color);
|
||||||
|
position: relative;
|
||||||
|
min-width: 44px;
|
||||||
|
min-height: 44px;
|
||||||
|
font-variant-numeric: slashed-zero;
|
||||||
|
font-feature-settings: 'ss01';
|
||||||
|
|
||||||
|
&[data-title]:after {
|
||||||
|
max-width: 50vw;
|
||||||
|
pointer-events: none;
|
||||||
|
position: absolute;
|
||||||
|
content: attr(data-title);
|
||||||
|
left: 50%;
|
||||||
|
top: 0;
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
padding: 2px 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
border: 1px solid var(--text-color);
|
||||||
|
transform: translate(-50%, -110%);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.1s ease-out 0.1s;
|
||||||
|
font-family: var(--monospace-font);
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
&.edge-left[data-title]:after {
|
||||||
|
left: 0;
|
||||||
|
transform: translate(0, -110%);
|
||||||
|
}
|
||||||
|
&.edge-right[data-title]:after {
|
||||||
|
left: 100%;
|
||||||
|
transform: translate(-100%, -110%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:is(:hover, :focus) {
|
||||||
|
z-index: 1;
|
||||||
|
filter: none;
|
||||||
|
background-color: var(--bg-faded-color);
|
||||||
|
|
||||||
|
&[data-title]:after {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
transition: transform 0.1s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:is(:hover, :focus) img {
|
||||||
|
transform: scale(2);
|
||||||
|
}
|
||||||
|
&.edge-left img {
|
||||||
|
transform-origin: left center;
|
||||||
|
}
|
||||||
|
&.edge-right img {
|
||||||
|
transform-origin: right center;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-size: 0.8em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.compose-field-container {
|
.compose-field-container {
|
||||||
|
|
Plik diff jest za duży
Load Diff
|
@ -17,6 +17,21 @@
|
||||||
);
|
);
|
||||||
filter: saturate(0.5);
|
filter: saturate(0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:is(a) {
|
||||||
|
pointer-events: auto;
|
||||||
|
display: block;
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--outline-hover-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
> * {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.accounts-list {
|
.accounts-list {
|
||||||
|
|
|
@ -11,6 +11,7 @@ import useLocationChange from '../utils/useLocationChange';
|
||||||
|
|
||||||
import AccountBlock from './account-block';
|
import AccountBlock from './account-block';
|
||||||
import Icon from './icon';
|
import Icon from './icon';
|
||||||
|
import Link from './link';
|
||||||
import Loader from './loader';
|
import Loader from './loader';
|
||||||
import Status from './status';
|
import Status from './status';
|
||||||
|
|
||||||
|
@ -19,6 +20,7 @@ export default function GenericAccounts({
|
||||||
excludeRelationshipAttrs = [],
|
excludeRelationshipAttrs = [],
|
||||||
postID,
|
postID,
|
||||||
onClose = () => {},
|
onClose = () => {},
|
||||||
|
blankCopy = 'Nothing to show',
|
||||||
}) {
|
}) {
|
||||||
const { masto, instance: currentInstance } = api();
|
const { masto, instance: currentInstance } = api();
|
||||||
const isCurrentInstance = instance ? instance === currentInstance : true;
|
const isCurrentInstance = instance ? instance === currentInstance : true;
|
||||||
|
@ -143,9 +145,12 @@ export default function GenericAccounts({
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
{post && (
|
{post && (
|
||||||
<div class="post-preview">
|
<Link
|
||||||
|
to={`/${instance || currentInstance}/s/${post.id}`}
|
||||||
|
class="post-preview"
|
||||||
|
>
|
||||||
<Status status={post} size="s" readOnly />
|
<Status status={post} size="s" readOnly />
|
||||||
</div>
|
</Link>
|
||||||
)}
|
)}
|
||||||
{accounts.length > 0 ? (
|
{accounts.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
|
@ -217,7 +222,7 @@ export default function GenericAccounts({
|
||||||
) : uiState === 'error' ? (
|
) : uiState === 'error' ? (
|
||||||
<p class="ui-state">Error loading accounts</p>
|
<p class="ui-state">Error loading accounts</p>
|
||||||
) : (
|
) : (
|
||||||
<p class="ui-state insignificant">Nothing to show</p>
|
<p class="ui-state insignificant">{blankCopy}</p>
|
||||||
)}
|
)}
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -7,10 +7,13 @@ import { useInView } from 'react-intersection-observer';
|
||||||
// The sticky header, usually at the top
|
// The sticky header, usually at the top
|
||||||
const TOP = 48;
|
const TOP = 48;
|
||||||
|
|
||||||
export default function LazyShazam({ children }) {
|
const shazamIDs = {};
|
||||||
|
|
||||||
|
export default function LazyShazam({ id, children }) {
|
||||||
const containerRef = useRef();
|
const containerRef = useRef();
|
||||||
|
const hasID = !!shazamIDs[id];
|
||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
const [visibleStart, setVisibleStart] = useState(false);
|
const [visibleStart, setVisibleStart] = useState(hasID || false);
|
||||||
|
|
||||||
const { ref } = useInView({
|
const { ref } = useInView({
|
||||||
root: null,
|
root: null,
|
||||||
|
@ -20,6 +23,7 @@ export default function LazyShazam({ children }) {
|
||||||
onChange: (inView) => {
|
onChange: (inView) => {
|
||||||
if (inView) {
|
if (inView) {
|
||||||
setVisible(true);
|
setVisible(true);
|
||||||
|
if (id) shazamIDs[id] = true;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
triggerOnce: true,
|
triggerOnce: true,
|
||||||
|
@ -35,6 +39,7 @@ export default function LazyShazam({ children }) {
|
||||||
} else {
|
} else {
|
||||||
setVisibleStart(true);
|
setVisibleStart(true);
|
||||||
}
|
}
|
||||||
|
if (id) shazamIDs[id] = true;
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ import FilterContext from '../utils/filter-context';
|
||||||
import { isFiltered } from '../utils/filters';
|
import { isFiltered } from '../utils/filters';
|
||||||
import states, { statusKey } from '../utils/states';
|
import states, { statusKey } from '../utils/states';
|
||||||
import store from '../utils/store';
|
import store from '../utils/store';
|
||||||
|
import { getCurrentAccountID } from '../utils/store-utils';
|
||||||
|
|
||||||
import Media from './media';
|
import Media from './media';
|
||||||
|
|
||||||
|
@ -88,7 +89,7 @@ function MediaPost({
|
||||||
};
|
};
|
||||||
|
|
||||||
const currentAccount = useMemo(() => {
|
const currentAccount = useMemo(() => {
|
||||||
return store.session.get('currentAccount');
|
return getCurrentAccountID();
|
||||||
}, []);
|
}, []);
|
||||||
const isSelf = useMemo(() => {
|
const isSelf = useMemo(() => {
|
||||||
return currentAccount && currentAccount === accountId;
|
return currentAccount && currentAccount === accountId;
|
||||||
|
|
|
@ -74,7 +74,7 @@ function Media({
|
||||||
altIndex,
|
altIndex,
|
||||||
onClick = () => {},
|
onClick = () => {},
|
||||||
}) {
|
}) {
|
||||||
const {
|
let {
|
||||||
blurhash,
|
blurhash,
|
||||||
description,
|
description,
|
||||||
meta,
|
meta,
|
||||||
|
@ -84,15 +84,27 @@ function Media({
|
||||||
url,
|
url,
|
||||||
type,
|
type,
|
||||||
} = media;
|
} = media;
|
||||||
|
if (/no\-preview\./i.test(previewUrl)) {
|
||||||
|
previewUrl = null;
|
||||||
|
}
|
||||||
const { original = {}, small, focus } = meta || {};
|
const { original = {}, small, focus } = meta || {};
|
||||||
|
|
||||||
const width = showOriginal ? original?.width : small?.width;
|
const width = showOriginal
|
||||||
const height = showOriginal ? original?.height : small?.height;
|
? original?.width
|
||||||
|
: small?.width || original?.width;
|
||||||
|
const height = showOriginal
|
||||||
|
? original?.height
|
||||||
|
: small?.height || original?.height;
|
||||||
const mediaURL = showOriginal ? url : previewUrl || url;
|
const mediaURL = showOriginal ? url : previewUrl || url;
|
||||||
const remoteMediaURL = showOriginal
|
const remoteMediaURL = showOriginal
|
||||||
? remoteUrl
|
? remoteUrl
|
||||||
: previewRemoteUrl || remoteUrl;
|
: previewRemoteUrl || remoteUrl;
|
||||||
const orientation = width >= height ? 'landscape' : 'portrait';
|
const hasDimensions = width && height;
|
||||||
|
const orientation = hasDimensions
|
||||||
|
? width > height
|
||||||
|
? 'landscape'
|
||||||
|
: 'portrait'
|
||||||
|
: null;
|
||||||
|
|
||||||
const rgbAverageColor = blurhash ? getBlurHashAverageColor(blurhash) : null;
|
const rgbAverageColor = blurhash ? getBlurHashAverageColor(blurhash) : null;
|
||||||
|
|
||||||
|
@ -133,7 +145,8 @@ function Media({
|
||||||
enabled: pinchZoomEnabled,
|
enabled: pinchZoomEnabled,
|
||||||
draggableUnZoomed: false,
|
draggableUnZoomed: false,
|
||||||
inertiaFriction: 0.9,
|
inertiaFriction: 0.9,
|
||||||
doubleTapZoomOutOnMaxScale: true,
|
tapZoomFactor: 2,
|
||||||
|
doubleTapToggleZoom: true,
|
||||||
containerProps: {
|
containerProps: {
|
||||||
className: 'media-zoom',
|
className: 'media-zoom',
|
||||||
style: {
|
style: {
|
||||||
|
@ -153,7 +166,7 @@ function Media({
|
||||||
[to],
|
[to],
|
||||||
);
|
);
|
||||||
|
|
||||||
const remoteMediaURLObj = remoteMediaURL ? new URL(remoteMediaURL) : null;
|
const remoteMediaURLObj = remoteMediaURL ? getURLObj(remoteMediaURL) : null;
|
||||||
const isVideoMaybe =
|
const isVideoMaybe =
|
||||||
type === 'unknown' &&
|
type === 'unknown' &&
|
||||||
remoteMediaURLObj &&
|
remoteMediaURLObj &&
|
||||||
|
@ -290,7 +303,11 @@ function Media({
|
||||||
}}
|
}}
|
||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
const { src } = e.target;
|
const { src } = e.target;
|
||||||
if (src === mediaURL && mediaURL !== remoteMediaURL) {
|
if (
|
||||||
|
src === mediaURL &&
|
||||||
|
remoteMediaURL &&
|
||||||
|
mediaURL !== remoteMediaURL
|
||||||
|
) {
|
||||||
e.target.src = remoteMediaURL;
|
e.target.src = remoteMediaURL;
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
@ -321,6 +338,20 @@ function Media({
|
||||||
onLoad={(e) => {
|
onLoad={(e) => {
|
||||||
// e.target.closest('.media-image').style.backgroundImage = '';
|
// e.target.closest('.media-image').style.backgroundImage = '';
|
||||||
e.target.dataset.loaded = true;
|
e.target.dataset.loaded = true;
|
||||||
|
if (!hasDimensions) {
|
||||||
|
const $media = e.target.closest('.media');
|
||||||
|
if ($media) {
|
||||||
|
const { naturalWidth, naturalHeight } = e.target;
|
||||||
|
$media.dataset.orientation =
|
||||||
|
naturalWidth > naturalHeight ? 'landscape' : 'portrait';
|
||||||
|
$media.style.setProperty('--width', `${naturalWidth}px`);
|
||||||
|
$media.style.setProperty(
|
||||||
|
'--height',
|
||||||
|
`${naturalHeight}px`,
|
||||||
|
);
|
||||||
|
$media.style.aspectRatio = `${naturalWidth}/${naturalHeight}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
const { src } = e.target;
|
const { src } = e.target;
|
||||||
|
@ -338,6 +369,7 @@ function Media({
|
||||||
</Figure>
|
</Figure>
|
||||||
);
|
);
|
||||||
} else if (type === 'gifv' || type === 'video' || isVideoMaybe) {
|
} else if (type === 'gifv' || type === 'video' || isVideoMaybe) {
|
||||||
|
const hasDuration = original.duration > 0;
|
||||||
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
|
||||||
|
@ -356,7 +388,7 @@ function Media({
|
||||||
data-orientation="${orientation}"
|
data-orientation="${orientation}"
|
||||||
preload="auto"
|
preload="auto"
|
||||||
autoplay
|
autoplay
|
||||||
muted="${isGIF}"
|
${isGIF ? 'muted' : ''}
|
||||||
${isGIF ? '' : 'controls'}
|
${isGIF ? '' : 'controls'}
|
||||||
playsinline
|
playsinline
|
||||||
loop="${loopable}"
|
loop="${loopable}"
|
||||||
|
@ -473,14 +505,61 @@ function Media({
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<img
|
{previewUrl ? (
|
||||||
src={previewUrl}
|
<img
|
||||||
alt={showInlineDesc ? '' : description}
|
src={previewUrl}
|
||||||
width={width}
|
alt={showInlineDesc ? '' : description}
|
||||||
height={height}
|
width={width}
|
||||||
data-orientation={orientation}
|
height={height}
|
||||||
loading="lazy"
|
data-orientation={orientation}
|
||||||
/>
|
loading="lazy"
|
||||||
|
decoding="async"
|
||||||
|
onLoad={(e) => {
|
||||||
|
if (!hasDimensions) {
|
||||||
|
const $media = e.target.closest('.media');
|
||||||
|
if ($media) {
|
||||||
|
const { naturalHeight, naturalWidth } = e.target;
|
||||||
|
$media.dataset.orientation =
|
||||||
|
naturalWidth > naturalHeight
|
||||||
|
? 'landscape'
|
||||||
|
: 'portrait';
|
||||||
|
$media.style.setProperty(
|
||||||
|
'--width',
|
||||||
|
`${naturalWidth}px`,
|
||||||
|
);
|
||||||
|
$media.style.setProperty(
|
||||||
|
'--height',
|
||||||
|
`${naturalHeight}px`,
|
||||||
|
);
|
||||||
|
$media.style.aspectRatio = `${naturalWidth}/${naturalHeight}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<video
|
||||||
|
src={url + '#t=0.1'} // Make Safari show 1st-frame preview
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
data-orientation={orientation}
|
||||||
|
preload="metadata"
|
||||||
|
muted
|
||||||
|
disablePictureInPicture
|
||||||
|
onLoadedMetadata={(e) => {
|
||||||
|
if (!hasDuration) {
|
||||||
|
const { duration } = e.target;
|
||||||
|
if (duration) {
|
||||||
|
const formattedDuration = formatDuration(duration);
|
||||||
|
const container = e.target.closest('.media-video');
|
||||||
|
if (container) {
|
||||||
|
container.dataset.formattedDuration =
|
||||||
|
formattedDuration;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<div class="media-play">
|
<div class="media-play">
|
||||||
<Icon icon="play" size="xl" />
|
<Icon icon="play" size="xl" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -539,4 +618,13 @@ function Media({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getURLObj(url) {
|
||||||
|
try {
|
||||||
|
// Fake base URL if url doesn't have https:// prefix
|
||||||
|
return new URL(url, location.origin);
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default Media;
|
export default Media;
|
||||||
|
|
|
@ -10,17 +10,56 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: var(--backdrop-color);
|
background-color: var(--backdrop-color);
|
||||||
animation: appear 0.5s var(--timing-function) both;
|
animation: appear 0.5s var(--timing-function) both;
|
||||||
|
transition: all 0.5s var(--timing-function);
|
||||||
|
|
||||||
&.solid {
|
&.solid {
|
||||||
background-color: var(--backdrop-solid-color);
|
background-color: var(--backdrop-solid-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
--compose-button-dimension: 56px;
|
||||||
|
--compose-button-dimension-half: calc(var(--compose-button-dimension) / 2);
|
||||||
|
--compose-button-dimension-margin: 16px;
|
||||||
|
|
||||||
|
&.min {
|
||||||
|
/* Minimized */
|
||||||
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
|
overflow: hidden;
|
||||||
|
transform: scale(0);
|
||||||
|
--right: max(
|
||||||
|
var(--compose-button-dimension-margin),
|
||||||
|
env(safe-area-inset-right)
|
||||||
|
);
|
||||||
|
--bottom: max(
|
||||||
|
var(--compose-button-dimension-margin),
|
||||||
|
env(safe-area-inset-bottom)
|
||||||
|
);
|
||||||
|
--origin-right: calc(
|
||||||
|
100% - var(--compose-button-dimension-half) - var(--right)
|
||||||
|
);
|
||||||
|
--origin-bottom: calc(
|
||||||
|
100% - var(--compose-button-dimension-half) - var(--bottom)
|
||||||
|
);
|
||||||
|
transform-origin: var(--origin-right) var(--origin-bottom);
|
||||||
|
}
|
||||||
|
|
||||||
.sheet {
|
.sheet {
|
||||||
transition: transform 0.3s var(--timing-function);
|
transition: transform 0.3s var(--timing-function);
|
||||||
transform-origin: center bottom;
|
transform-origin: 80% 80%;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:has(~ div) .sheet {
|
&:has(~ div) .sheet {
|
||||||
transform: scale(0.975);
|
transform: scale(0.975);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: calc(40em - 1px)) {
|
||||||
|
#app[data-shortcuts-view-mode='tab-menu-bar'] ~ #modal-container > div.min {
|
||||||
|
border: 2px solid red;
|
||||||
|
|
||||||
|
--bottom: calc(
|
||||||
|
var(--compose-button-dimension-margin) + env(safe-area-inset-bottom) +
|
||||||
|
52px
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ import useCloseWatcher from '../utils/useCloseWatcher';
|
||||||
|
|
||||||
const $modalContainer = document.getElementById('modal-container');
|
const $modalContainer = document.getElementById('modal-container');
|
||||||
|
|
||||||
function Modal({ children, onClose, onClick, class: className }) {
|
function Modal({ children, onClose, onClick, class: className, minimized }) {
|
||||||
if (!children) return null;
|
if (!children) return null;
|
||||||
|
|
||||||
const modalRef = useRef();
|
const modalRef = useRef();
|
||||||
|
@ -41,6 +41,33 @@ function Modal({ children, onClose, onClick, class: className }) {
|
||||||
);
|
);
|
||||||
useCloseWatcher(onClose, [onClose]);
|
useCloseWatcher(onClose, [onClose]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const $deckContainers = document.querySelectorAll('.deck-container');
|
||||||
|
if (minimized) {
|
||||||
|
// Similar to focusDeck in focus-deck.jsx
|
||||||
|
// Focus last deck
|
||||||
|
const page = $deckContainers[$deckContainers.length - 1]; // last one
|
||||||
|
if (page && page.tabIndex === -1) {
|
||||||
|
page.focus();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (children) {
|
||||||
|
$deckContainers.forEach(($deckContainer) => {
|
||||||
|
$deckContainer.setAttribute('inert', '');
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
$deckContainers.forEach(($deckContainer) => {
|
||||||
|
$deckContainer.removeAttribute('inert');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
$deckContainers.forEach(($deckContainer) => {
|
||||||
|
$deckContainer.removeAttribute('inert');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}, [children, minimized]);
|
||||||
|
|
||||||
const Modal = (
|
const Modal = (
|
||||||
<div
|
<div
|
||||||
ref={(node) => {
|
ref={(node) => {
|
||||||
|
@ -54,7 +81,8 @@ function Modal({ children, onClose, onClick, class: className }) {
|
||||||
onClose?.(e);
|
onClose?.(e);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
tabIndex="-1"
|
tabIndex={minimized ? 0 : '-1'}
|
||||||
|
inert={minimized}
|
||||||
onFocus={(e) => {
|
onFocus={(e) => {
|
||||||
try {
|
try {
|
||||||
if (e.target === e.currentTarget) {
|
if (e.target === e.currentTarget) {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { lazy } from 'preact/compat';
|
import { useEffect } from 'preact/hooks';
|
||||||
import { useLocation, useNavigate } from 'react-router-dom';
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { subscribe, useSnapshot } from 'valtio';
|
import { subscribe, useSnapshot } from 'valtio';
|
||||||
|
|
||||||
|
@ -9,19 +9,16 @@ import showToast from '../utils/show-toast';
|
||||||
import states from '../utils/states';
|
import states from '../utils/states';
|
||||||
|
|
||||||
import AccountSheet from './account-sheet';
|
import AccountSheet from './account-sheet';
|
||||||
// import Compose from './compose';
|
import ComposeSuspense, { preload } from './compose-suspense';
|
||||||
import Drafts from './drafts';
|
import Drafts from './drafts';
|
||||||
import EmbedModal from './embed-modal';
|
import EmbedModal from './embed-modal';
|
||||||
import GenericAccounts from './generic-accounts';
|
import GenericAccounts from './generic-accounts';
|
||||||
import IntlSegmenterSuspense from './intl-segmenter-suspense';
|
|
||||||
import MediaAltModal from './media-alt-modal';
|
import MediaAltModal from './media-alt-modal';
|
||||||
import MediaModal from './media-modal';
|
import MediaModal from './media-modal';
|
||||||
import Modal from './modal';
|
import Modal from './modal';
|
||||||
import ReportModal from './report-modal';
|
import ReportModal from './report-modal';
|
||||||
import ShortcutsSettings from './shortcuts-settings';
|
import ShortcutsSettings from './shortcuts-settings';
|
||||||
|
|
||||||
const Compose = lazy(() => import('./compose'));
|
|
||||||
|
|
||||||
subscribe(states, (changes) => {
|
subscribe(states, (changes) => {
|
||||||
for (const [action, path, value, prevValue] of changes) {
|
for (const [action, path, value, prevValue] of changes) {
|
||||||
// When closing modal, focus on deck
|
// When closing modal, focus on deck
|
||||||
|
@ -36,55 +33,60 @@ export default function Modals() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
queueMicrotask(preload);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{!!snapStates.showCompose && (
|
{!!snapStates.showCompose && (
|
||||||
<Modal class="solid">
|
<Modal
|
||||||
<IntlSegmenterSuspense>
|
class={`solid ${snapStates.composerState.minimized ? 'min' : ''}`}
|
||||||
<Compose
|
minimized={!!snapStates.composerState.minimized}
|
||||||
replyToStatus={
|
>
|
||||||
typeof snapStates.showCompose !== 'boolean'
|
<ComposeSuspense
|
||||||
? snapStates.showCompose.replyToStatus
|
replyToStatus={
|
||||||
: window.__COMPOSE__?.replyToStatus || null
|
typeof snapStates.showCompose !== 'boolean'
|
||||||
|
? snapStates.showCompose.replyToStatus
|
||||||
|
: window.__COMPOSE__?.replyToStatus || null
|
||||||
|
}
|
||||||
|
editStatus={
|
||||||
|
states.showCompose?.editStatus ||
|
||||||
|
window.__COMPOSE__?.editStatus ||
|
||||||
|
null
|
||||||
|
}
|
||||||
|
draftStatus={
|
||||||
|
states.showCompose?.draftStatus ||
|
||||||
|
window.__COMPOSE__?.draftStatus ||
|
||||||
|
null
|
||||||
|
}
|
||||||
|
onClose={(results) => {
|
||||||
|
const { newStatus, instance, type } = results || {};
|
||||||
|
states.showCompose = false;
|
||||||
|
window.__COMPOSE__ = null;
|
||||||
|
if (newStatus) {
|
||||||
|
states.reloadStatusPage++;
|
||||||
|
showToast({
|
||||||
|
text: {
|
||||||
|
post: 'Post published. Check it out.',
|
||||||
|
reply: 'Reply posted. Check it out.',
|
||||||
|
edit: 'Post updated. Check it out.',
|
||||||
|
}[type || 'post'],
|
||||||
|
delay: 1000,
|
||||||
|
duration: 10_000, // 10 seconds
|
||||||
|
onClick: (toast) => {
|
||||||
|
toast.hideToast();
|
||||||
|
states.prevLocation = location;
|
||||||
|
navigate(
|
||||||
|
instance
|
||||||
|
? `/${instance}/s/${newStatus.id}`
|
||||||
|
: `/s/${newStatus.id}`,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
editStatus={
|
}}
|
||||||
states.showCompose?.editStatus ||
|
/>
|
||||||
window.__COMPOSE__?.editStatus ||
|
|
||||||
null
|
|
||||||
}
|
|
||||||
draftStatus={
|
|
||||||
states.showCompose?.draftStatus ||
|
|
||||||
window.__COMPOSE__?.draftStatus ||
|
|
||||||
null
|
|
||||||
}
|
|
||||||
onClose={(results) => {
|
|
||||||
const { newStatus, instance, type } = results || {};
|
|
||||||
states.showCompose = false;
|
|
||||||
window.__COMPOSE__ = null;
|
|
||||||
if (newStatus) {
|
|
||||||
states.reloadStatusPage++;
|
|
||||||
showToast({
|
|
||||||
text: {
|
|
||||||
post: 'Post published. Check it out.',
|
|
||||||
reply: 'Reply posted. Check it out.',
|
|
||||||
edit: 'Post updated. Check it out.',
|
|
||||||
}[type || 'post'],
|
|
||||||
delay: 1000,
|
|
||||||
duration: 10_000, // 10 seconds
|
|
||||||
onClick: (toast) => {
|
|
||||||
toast.hideToast();
|
|
||||||
states.prevLocation = location;
|
|
||||||
navigate(
|
|
||||||
instance
|
|
||||||
? `/${instance}/s/${newStatus.id}`
|
|
||||||
: `/s/${newStatus.id}`,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</IntlSegmenterSuspense>
|
|
||||||
</Modal>
|
</Modal>
|
||||||
)}
|
)}
|
||||||
{!!snapStates.showSettings && (
|
{!!snapStates.showSettings && (
|
||||||
|
@ -187,6 +189,7 @@ export default function Modals() {
|
||||||
}
|
}
|
||||||
postID={snapStates.showGenericAccounts.postID}
|
postID={snapStates.showGenericAccounts.postID}
|
||||||
onClose={() => (states.showGenericAccounts = false)}
|
onClose={() => (states.showGenericAccounts = false)}
|
||||||
|
blankCopy={snapStates.showGenericAccounts.blankCopy}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -31,16 +31,17 @@ function NameText({
|
||||||
.replace(/(\:(\w|\+|\-)+\:)(?=|[\!\.\?]|$)/g, '') // Remove shortcodes, regex from https://regex101.com/r/iE9uV0/1
|
.replace(/(\:(\w|\+|\-)+\:)(?=|[\!\.\?]|$)/g, '') // Remove shortcodes, regex from https://regex101.com/r/iE9uV0/1
|
||||||
.replace(/\s+/g, ''); // E.g. "My name" === "myname"
|
.replace(/\s+/g, ''); // E.g. "My name" === "myname"
|
||||||
const shortenedAlphaNumericDisplayName = shortenedDisplayName.replace(
|
const shortenedAlphaNumericDisplayName = shortenedDisplayName.replace(
|
||||||
/[^a-z0-9]/gi,
|
/[^a-z0-9@\.]/gi,
|
||||||
'',
|
'',
|
||||||
); // Remove non-alphanumeric characters
|
); // Remove non-alphanumeric characters
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!short &&
|
(!short &&
|
||||||
(trimmedUsername === trimmedDisplayName ||
|
(trimmedUsername === trimmedDisplayName ||
|
||||||
trimmedUsername === shortenedDisplayName ||
|
trimmedUsername === shortenedDisplayName ||
|
||||||
trimmedUsername === shortenedAlphaNumericDisplayName ||
|
trimmedUsername === shortenedAlphaNumericDisplayName ||
|
||||||
nameCollator.compare(trimmedUsername, shortenedDisplayName) === 0)
|
nameCollator.compare(trimmedUsername, shortenedDisplayName) === 0)) ||
|
||||||
|
shortenedAlphaNumericDisplayName === acct.toLowerCase()
|
||||||
) {
|
) {
|
||||||
username = null;
|
username = null;
|
||||||
}
|
}
|
||||||
|
@ -57,9 +58,15 @@ function NameText({
|
||||||
}
|
}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
if (external) return;
|
if (external) return;
|
||||||
|
if (e.shiftKey) return; // Save link? 🤷♂️
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (onClick) return onClick(e);
|
if (onClick) return onClick(e);
|
||||||
|
if (e.metaKey || e.ctrlKey || e.shiftKey || e.which === 2) {
|
||||||
|
const internalURL = `#/${instance}/a/${id}`;
|
||||||
|
window.open(internalURL, '_blank');
|
||||||
|
return;
|
||||||
|
}
|
||||||
states.showAccount = {
|
states.showAccount = {
|
||||||
account,
|
account,
|
||||||
instance,
|
instance,
|
||||||
|
|
|
@ -11,6 +11,8 @@ import { getLists } from '../utils/lists';
|
||||||
import safeBoundingBoxPadding from '../utils/safe-bounding-box-padding';
|
import safeBoundingBoxPadding from '../utils/safe-bounding-box-padding';
|
||||||
import states from '../utils/states';
|
import states from '../utils/states';
|
||||||
import store from '../utils/store';
|
import store from '../utils/store';
|
||||||
|
import { getCurrentAccountID } from '../utils/store-utils';
|
||||||
|
import supports from '../utils/supports';
|
||||||
|
|
||||||
import Avatar from './avatar';
|
import Avatar from './avatar';
|
||||||
import Icon from './icon';
|
import Icon from './icon';
|
||||||
|
@ -24,9 +26,8 @@ function NavMenu(props) {
|
||||||
const [currentAccount, moreThanOneAccount] = useMemo(() => {
|
const [currentAccount, moreThanOneAccount] = useMemo(() => {
|
||||||
const accounts = store.local.getJSON('accounts') || [];
|
const accounts = store.local.getJSON('accounts') || [];
|
||||||
const acc =
|
const acc =
|
||||||
accounts.find(
|
accounts.find((account) => account.info.id === getCurrentAccountID()) ||
|
||||||
(account) => account.info.id === store.session.get('currentAccount'),
|
accounts[0];
|
||||||
) || accounts[0];
|
|
||||||
return [acc, accounts.length > 1];
|
return [acc, accounts.length > 1];
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
@ -83,8 +84,10 @@ function NavMenu(props) {
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const supportsLists = supports('@mastodon/lists');
|
||||||
const [lists, setLists] = useState([]);
|
const [lists, setLists] = useState([]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!supportsLists) return;
|
||||||
if (menuState === 'open') {
|
if (menuState === 'open') {
|
||||||
getLists().then(setLists);
|
getLists().then(setLists);
|
||||||
}
|
}
|
||||||
|
@ -186,9 +189,11 @@ function NavMenu(props) {
|
||||||
<Icon icon="history2" size="l" />
|
<Icon icon="history2" size="l" />
|
||||||
<span>Catch-up</span>
|
<span>Catch-up</span>
|
||||||
</MenuLink>
|
</MenuLink>
|
||||||
<MenuLink to="/mentions">
|
{supports('@mastodon/mentions') && (
|
||||||
<Icon icon="at" size="l" /> <span>Mentions</span>
|
<MenuLink to="/mentions">
|
||||||
</MenuLink>
|
<Icon icon="at" size="l" /> <span>Mentions</span>
|
||||||
|
</MenuLink>
|
||||||
|
)}
|
||||||
<MenuLink to="/notifications">
|
<MenuLink to="/notifications">
|
||||||
<Icon icon="notification" size="l" /> <span>Notifications</span>
|
<Icon icon="notification" size="l" /> <span>Notifications</span>
|
||||||
{snapStates.notificationsShowNew && (
|
{snapStates.notificationsShowNew && (
|
||||||
|
@ -232,10 +237,12 @@ function NavMenu(props) {
|
||||||
)}
|
)}
|
||||||
</SubMenu2>
|
</SubMenu2>
|
||||||
) : (
|
) : (
|
||||||
<MenuLink to="/l">
|
supportsLists && (
|
||||||
<Icon icon="list" size="l" />
|
<MenuLink to="/l">
|
||||||
<span>Lists</span>
|
<Icon icon="list" size="l" />
|
||||||
</MenuLink>
|
<span>Lists</span>
|
||||||
|
</MenuLink>
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
<MenuLink to="/b">
|
<MenuLink to="/b">
|
||||||
<Icon icon="bookmark" size="l" /> <span>Bookmarks</span>
|
<Icon icon="bookmark" size="l" /> <span>Bookmarks</span>
|
||||||
|
@ -260,10 +267,12 @@ function NavMenu(props) {
|
||||||
<span>Followed Hashtags</span>
|
<span>Followed Hashtags</span>
|
||||||
</MenuLink>
|
</MenuLink>
|
||||||
<MenuDivider />
|
<MenuDivider />
|
||||||
<MenuLink to="/ft">
|
{supports('@mastodon/filters') && (
|
||||||
<Icon icon="filters" size="l" />
|
<MenuLink to="/ft">
|
||||||
Filters
|
<Icon icon="filters" size="l" />
|
||||||
</MenuLink>
|
Filters
|
||||||
|
</MenuLink>
|
||||||
|
)}
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
states.showGenericAccounts = {
|
states.showGenericAccounts = {
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { memo } from 'preact/compat';
|
||||||
import shortenNumber from '../utils/shorten-number';
|
import shortenNumber from '../utils/shorten-number';
|
||||||
import states, { statusKey } from '../utils/states';
|
import states, { statusKey } from '../utils/states';
|
||||||
import store from '../utils/store';
|
import store from '../utils/store';
|
||||||
|
import { getCurrentAccountID } from '../utils/store-utils';
|
||||||
import useTruncated from '../utils/useTruncated';
|
import useTruncated from '../utils/useTruncated';
|
||||||
|
|
||||||
import Avatar from './avatar';
|
import Avatar from './avatar';
|
||||||
|
@ -27,6 +28,7 @@ const NOTIFICATION_ICONS = {
|
||||||
'admin.signup': 'account-edit',
|
'admin.signup': 'account-edit',
|
||||||
'admin.report': 'account-warning',
|
'admin.report': 'account-warning',
|
||||||
severed_relationships: 'heart-break',
|
severed_relationships: 'heart-break',
|
||||||
|
moderation_warning: 'alert',
|
||||||
emoji_reaction: 'emoji2',
|
emoji_reaction: 'emoji2',
|
||||||
'pleroma:emoji_reaction': 'emoji2',
|
'pleroma:emoji_reaction': 'emoji2',
|
||||||
};
|
};
|
||||||
|
@ -44,6 +46,8 @@ poll = A poll you have voted in or created has ended
|
||||||
update = A status you interacted with has been edited
|
update = A status you interacted with has been edited
|
||||||
admin.sign_up = Someone signed up (optionally sent to admins)
|
admin.sign_up = Someone signed up (optionally sent to admins)
|
||||||
admin.report = A new report has been filed
|
admin.report = A new report has been filed
|
||||||
|
severed_relationships = Severed relationships
|
||||||
|
moderation_warning = Moderation warning
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function emojiText(emoji, emoji_url) {
|
function emojiText(emoji, emoji_url) {
|
||||||
|
@ -90,6 +94,7 @@ const contentText = {
|
||||||
Lost connections with <i>{name}</i>.
|
Lost connections with <i>{name}</i>.
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
|
moderation_warning: <b>Moderation warning</b>,
|
||||||
emoji_reaction: emojiText,
|
emoji_reaction: emojiText,
|
||||||
'pleroma:emoji_reaction': emojiText,
|
'pleroma:emoji_reaction': emojiText,
|
||||||
};
|
};
|
||||||
|
@ -116,6 +121,17 @@ const SEVERED_RELATIONSHIPS_TEXT = {
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const MODERATION_WARNING_TEXT = {
|
||||||
|
none: 'Your account has received a moderation warning.',
|
||||||
|
disable: 'Your account has been disabled.',
|
||||||
|
mark_statuses_as_sensitive:
|
||||||
|
'Some of your posts have been marked as sensitive.',
|
||||||
|
delete_statuses: 'Some of your posts have been deleted.',
|
||||||
|
sensitive: 'Your posts will be marked as sensitive from now on.',
|
||||||
|
silence: 'Your account has been limited.',
|
||||||
|
suspend: 'Your account has been suspended.',
|
||||||
|
};
|
||||||
|
|
||||||
const AVATARS_LIMIT = 50;
|
const AVATARS_LIMIT = 50;
|
||||||
|
|
||||||
function Notification({
|
function Notification({
|
||||||
|
@ -124,15 +140,23 @@ function Notification({
|
||||||
isStatic,
|
isStatic,
|
||||||
disableContextMenu,
|
disableContextMenu,
|
||||||
}) {
|
}) {
|
||||||
const { id, status, account, report, event, _accounts, _statuses } =
|
const {
|
||||||
notification;
|
id,
|
||||||
|
status,
|
||||||
|
account,
|
||||||
|
report,
|
||||||
|
event,
|
||||||
|
moderation_warning,
|
||||||
|
_accounts,
|
||||||
|
_statuses,
|
||||||
|
} = notification;
|
||||||
let { type } = notification;
|
let { type } = notification;
|
||||||
|
|
||||||
// status = Attached when type of the notification is favourite, reblog, status, mention, poll, or update
|
// status = Attached when type of the notification is favourite, reblog, status, mention, poll, or update
|
||||||
const actualStatus = status?.reblog || status;
|
const actualStatus = status?.reblog || status;
|
||||||
const actualStatusID = actualStatus?.id;
|
const actualStatusID = actualStatus?.id;
|
||||||
|
|
||||||
const currentAccount = store.session.get('currentAccount');
|
const currentAccount = getCurrentAccountID();
|
||||||
const isSelf = currentAccount === account?.id;
|
const isSelf = currentAccount === account?.id;
|
||||||
const isVoted = status?.poll?.voted;
|
const isVoted = status?.poll?.voted;
|
||||||
const isReplyToOthers =
|
const isReplyToOthers =
|
||||||
|
@ -313,6 +337,20 @@ function Notification({
|
||||||
.
|
.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{type === 'moderation_warning' && !!moderation_warning && (
|
||||||
|
<div>
|
||||||
|
{MODERATION_WARNING_TEXT[moderation_warning.action]}
|
||||||
|
<br />
|
||||||
|
<a
|
||||||
|
href={`/disputes/strikes/${moderation_warning.id}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
Learn more <Icon icon="external" size="s" />
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{_accounts?.length > 1 && (
|
{_accounts?.length > 1 && (
|
||||||
|
|
|
@ -21,6 +21,7 @@ export default function RelativeTime({ datetime, format }) {
|
||||||
const [renderCount, rerender] = useReducer((x) => x + 1, 0);
|
const [renderCount, rerender] = useReducer((x) => x + 1, 0);
|
||||||
const date = useMemo(() => dayjs(datetime), [datetime]);
|
const date = useMemo(() => dayjs(datetime), [datetime]);
|
||||||
const [dateStr, dt, title] = useMemo(() => {
|
const [dateStr, dt, title] = useMemo(() => {
|
||||||
|
if (!date.isValid()) return ['' + datetime, '', ''];
|
||||||
let str;
|
let str;
|
||||||
if (format === 'micro') {
|
if (format === 'micro') {
|
||||||
// If date <= 1 day ago or day is within this year
|
// If date <= 1 day ago or day is within this year
|
||||||
|
@ -37,6 +38,7 @@ export default function RelativeTime({ datetime, format }) {
|
||||||
}, [date, format, renderCount]);
|
}, [date, format, renderCount]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!date.isValid()) return;
|
||||||
let timeout;
|
let timeout;
|
||||||
let raf;
|
let raf;
|
||||||
function rafRerender() {
|
function rafRerender() {
|
||||||
|
|
|
@ -19,6 +19,7 @@ import pmem from '../utils/pmem';
|
||||||
import showToast from '../utils/show-toast';
|
import showToast from '../utils/show-toast';
|
||||||
import states from '../utils/states';
|
import states from '../utils/states';
|
||||||
import store from '../utils/store';
|
import store from '../utils/store';
|
||||||
|
import { getCurrentAccountID } from '../utils/store-utils';
|
||||||
|
|
||||||
import AsyncText from './AsyncText';
|
import AsyncText from './AsyncText';
|
||||||
import Icon from './icon';
|
import Icon from './icon';
|
||||||
|
@ -787,7 +788,7 @@ function ImportExport({ shortcuts, onClose }) {
|
||||||
disabled={importUIState === 'cloud-downloading'}
|
disabled={importUIState === 'cloud-downloading'}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
setImportUIState('cloud-downloading');
|
setImportUIState('cloud-downloading');
|
||||||
const currentAccount = store.session.get('currentAccount');
|
const currentAccount = getCurrentAccountID();
|
||||||
showToast(
|
showToast(
|
||||||
'Downloading saved shortcuts from instance server…',
|
'Downloading saved shortcuts from instance server…',
|
||||||
);
|
);
|
||||||
|
@ -1043,7 +1044,7 @@ function ImportExport({ shortcuts, onClose }) {
|
||||||
disabled={importUIState === 'cloud-uploading'}
|
disabled={importUIState === 'cloud-uploading'}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
setImportUIState('cloud-uploading');
|
setImportUIState('cloud-uploading');
|
||||||
const currentAccount = store.session.get('currentAccount');
|
const currentAccount = getCurrentAccountID();
|
||||||
try {
|
try {
|
||||||
const relationships =
|
const relationships =
|
||||||
await masto.v1.accounts.relationships.fetch({
|
await masto.v1.accounts.relationships.fetch({
|
||||||
|
|
|
@ -47,7 +47,7 @@
|
||||||
}
|
}
|
||||||
.visibility-direct {
|
.visibility-direct {
|
||||||
--yellow-stripes: repeating-linear-gradient(
|
--yellow-stripes: repeating-linear-gradient(
|
||||||
-45deg,
|
135deg,
|
||||||
var(--reply-to-faded-color),
|
var(--reply-to-faded-color),
|
||||||
var(--reply-to-faded-color) 10px,
|
var(--reply-to-faded-color) 10px,
|
||||||
var(--reply-to-faded-color) 10px,
|
var(--reply-to-faded-color) 10px,
|
||||||
|
@ -160,7 +160,7 @@
|
||||||
display: block;
|
display: block;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
&:after {
|
&[data-read-more]:after {
|
||||||
content: attr(data-read-more);
|
content: attr(data-read-more);
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
@ -365,6 +365,10 @@
|
||||||
background-image: var(--yellow-stripes);
|
background-image: var(--yellow-stripes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.status-pre-meta + & {
|
||||||
|
background-image: none;
|
||||||
|
}
|
||||||
|
|
||||||
> * {
|
> * {
|
||||||
opacity: 0.65;
|
opacity: 0.65;
|
||||||
transition: opacity 1s ease-out;
|
transition: opacity 1s ease-out;
|
||||||
|
@ -565,8 +569,15 @@
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|
||||||
|
&.horizontal {
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.status-filtered-badge.badge-meta {
|
.status-filtered-badge:not(.horizontal).badge-meta {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -580,10 +591,10 @@
|
||||||
border-color: var(--text-color);
|
border-color: var(--text-color);
|
||||||
background: var(--bg-color);
|
background: var(--bg-color);
|
||||||
}
|
}
|
||||||
.status-filtered-badge.badge-meta > span:first-child {
|
.status-filtered-badge:not(.horizontal).badge-meta > span:first-child {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.status-filtered-badge.badge-meta > span + span {
|
.status-filtered-badge:not(.horizontal).badge-meta > span + span {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 9px;
|
font-size: 9px;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
|
@ -597,6 +608,10 @@
|
||||||
left: 0;
|
left: 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
.status-filtered-badge.horizontal.badge-meta > span + span {
|
||||||
|
font-weight: normal;
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
.status.large > .container > .content-container {
|
.status.large > .container > .content-container {
|
||||||
margin-left: calc(-50px - 16px);
|
margin-left: calc(-50px - 16px);
|
||||||
|
@ -618,6 +633,7 @@
|
||||||
~ *:not(
|
~ *:not(
|
||||||
.content.truncated,
|
.content.truncated,
|
||||||
.media-container,
|
.media-container,
|
||||||
|
.media-first-container,
|
||||||
.card,
|
.card,
|
||||||
.media-figure-multiple,
|
.media-figure-multiple,
|
||||||
.spoiler-media-button
|
.spoiler-media-button
|
||||||
|
@ -638,6 +654,7 @@
|
||||||
|
|
||||||
~ *:not(
|
~ *:not(
|
||||||
.media-container,
|
.media-container,
|
||||||
|
.media-first-container,
|
||||||
.card,
|
.card,
|
||||||
.media-figure-multiple,
|
.media-figure-multiple,
|
||||||
.spoiler-media-button
|
.spoiler-media-button
|
||||||
|
@ -708,11 +725,12 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
~ :is(.media-container, .media-figure-multiple) .media {
|
~ :is(.media-container, .media-first-container, .media-figure-multiple)
|
||||||
|
.media {
|
||||||
background-image: radial-gradient(
|
background-image: radial-gradient(
|
||||||
circle at 50% 50%,
|
circle at 50% 50%,
|
||||||
var(--average-color, var(--bg-faded-color)),
|
var(--average-color, var(--bg-faded-color)),
|
||||||
var(--bg-color) 20em
|
var(--bg-color) 25em
|
||||||
);
|
);
|
||||||
|
|
||||||
> *:not(.media-play, .alt-badge) {
|
> *:not(.media-play, .alt-badge) {
|
||||||
|
@ -790,7 +808,9 @@
|
||||||
black 1.5em
|
black 1.5em
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
.timeline-deck .status:not(.truncated .status) .content.truncated:after {
|
.timeline-deck
|
||||||
|
.status:not(.truncated .status)
|
||||||
|
.content.truncated[data-read-more]:after {
|
||||||
content: attr(data-read-more);
|
content: attr(data-read-more);
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
@ -816,6 +836,12 @@
|
||||||
.timeline-deck .status .content.truncated ~ .card {
|
.timeline-deck .status .content.truncated ~ .card {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
.status .content .inner-content {
|
||||||
|
> img[height] {
|
||||||
|
height: auto;
|
||||||
|
aspect-ratio: var(--original-aspect-ratio);
|
||||||
|
}
|
||||||
|
}
|
||||||
.status .content .inner-content a:not(.mention, .has-url-text) {
|
.status .content .inner-content a:not(.mention, .has-url-text) {
|
||||||
color: var(--link-text-color);
|
color: var(--link-text-color);
|
||||||
}
|
}
|
||||||
|
@ -1314,6 +1340,258 @@ body:has(#modal-container .carousel) .status .media img:hover {
|
||||||
background-blend-mode: multiply;
|
background-blend-mode: multiply;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.status.skeleton .media-first-container {
|
||||||
|
min-height: 320px;
|
||||||
|
background-color: var(--outline-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes media-carousel-slide {
|
||||||
|
0% {
|
||||||
|
transform: translateX(calc(var(--dots-count, 1) * 2.5px));
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateX(calc(var(--dots-count, 1) * -2.5px));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-media-first {
|
||||||
|
timeline-scope: --media-carousel;
|
||||||
|
|
||||||
|
.meta-name {
|
||||||
|
opacity: 0.65;
|
||||||
|
transition: opacity 0.5s ease-in-out;
|
||||||
|
|
||||||
|
b + i {
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.5s ease-in-out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
:is(:hover, :focus) > & .meta-name {
|
||||||
|
opacity: 1;
|
||||||
|
b + i {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-first-spoiler-content {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 100%;
|
||||||
|
transition: opacity 0.5s ease-in-out;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
&:hover .media-first-spoiler-content {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-first-spoiler-button {
|
||||||
|
display: inline-flex !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-first-container {
|
||||||
|
position: relative;
|
||||||
|
margin-top: 8px;
|
||||||
|
margin-inline: -16px;
|
||||||
|
|
||||||
|
@media (min-width: 40em) {
|
||||||
|
margin-inline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-carousel-controls {
|
||||||
|
flex-shrink: 0;
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carousel-indexer {
|
||||||
|
z-index: 1;
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
right: 8px;
|
||||||
|
color: var(--media-fg-color);
|
||||||
|
background-color: var(--media-bg-color);
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 16px;
|
||||||
|
font-size: 0.8em;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
opacity: 0.6;
|
||||||
|
transition: opacity 1s ease-in-out 0.3s;
|
||||||
|
border: var(--hairline-width) solid var(--media-outline-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-carousel-button {
|
||||||
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding-inline: 8px;
|
||||||
|
margin-block: 3em;
|
||||||
|
pointer-events: auto;
|
||||||
|
cursor: pointer;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.carousel-button {
|
||||||
|
@media (pointer: coarse) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
+ .carousel-button {
|
||||||
|
left: auto;
|
||||||
|
right: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (hover: hover) and (pointer: fine) {
|
||||||
|
.carousel-button {
|
||||||
|
filter: opacity(0);
|
||||||
|
}
|
||||||
|
&:hover .carousel-button {
|
||||||
|
filter: opacity(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-first-carousel {
|
||||||
|
display: flex;
|
||||||
|
max-height: 80vh;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
|
scroll-snap-type: x mandatory;
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
user-select: none;
|
||||||
|
scrollbar-width: none;
|
||||||
|
/* border: var(--hairline-width) solid var(--outline-color);
|
||||||
|
border-inline-width: 0;
|
||||||
|
background-color: var(--bg-faded-color); */
|
||||||
|
box-shadow: 0 0 0 var(--hairline-width) var(--outline-color);
|
||||||
|
scroll-timeline: --media-carousel x;
|
||||||
|
|
||||||
|
@media (min-width: 40em) {
|
||||||
|
/* margin-inline: 0; */
|
||||||
|
/* border-radius: 4px; */
|
||||||
|
/* border-inline-width: var(--hairline-width); */
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .media-first-item {
|
||||||
|
scroll-snap-align: center;
|
||||||
|
scroll-snap-stop: always;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
&:not(:only-child) {
|
||||||
|
background-color: var(--bg-blur-color);
|
||||||
|
/* box-shadow: inset 0 0 0 var(--hairline-width) var(--outline-color); */
|
||||||
|
}
|
||||||
|
|
||||||
|
.media {
|
||||||
|
/* background-color: var(--average-color, var(--bg-faded-color)); */
|
||||||
|
width: var(--width, 100%);
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
min-height: var(--min-dimension);
|
||||||
|
/* max-height: min(var(--height), 80vh); */
|
||||||
|
|
||||||
|
&:has(img:not([data-loaded='true'])) {
|
||||||
|
min-height: 320px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: none;
|
||||||
|
filter: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
img,
|
||||||
|
video {
|
||||||
|
object-fit: scale-down;
|
||||||
|
animation: none;
|
||||||
|
|
||||||
|
&:not([data-loaded='true']) {
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
:is(:hover, :focus) > & .carousel-indexer {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-carousel-dots {
|
||||||
|
pointer-events: none;
|
||||||
|
display: flex;
|
||||||
|
gap: 5px;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
|
||||||
|
@supports (animation-timeline: scroll()) {
|
||||||
|
animation: media-carousel-slide 1s linear both;
|
||||||
|
animation-timeline: --media-carousel;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carousel-dot {
|
||||||
|
display: inline-block;
|
||||||
|
width: 5px;
|
||||||
|
height: 5px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: var(--text-color);
|
||||||
|
transition: all 0.3s ease-in-out;
|
||||||
|
opacity: 0.3;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
opacity: 1;
|
||||||
|
background-color: var(--text-color);
|
||||||
|
transform: scale(1.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-first-content {
|
||||||
|
margin-top: 8px;
|
||||||
|
height: 1.75em;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
font-size: 0.9em;
|
||||||
|
mask-image: linear-gradient(to bottom, black 1.5em, transparent 1.75em);
|
||||||
|
opacity: 0.5;
|
||||||
|
transition: opacity 0.5s ease-in-out;
|
||||||
|
|
||||||
|
@media (min-width: 40em) {
|
||||||
|
margin-inline: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
text-align: center;
|
||||||
|
/* Brute force ellipsis */
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap !important;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
filter: grayscale(0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:is(:hover, :focus) > & .media-first-content {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.status:not(.large) .hashtag-stuffing {
|
.status:not(.large) .hashtag-stuffing {
|
||||||
opacity: 0.75;
|
opacity: 0.75;
|
||||||
transition: opacity 0.2s ease-in-out;
|
transition: opacity 0.2s ease-in-out;
|
||||||
|
@ -2119,8 +2397,8 @@ a.card:is(:hover, :focus):visited {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
height: 1.2em;
|
height: 1.2em;
|
||||||
vertical-align: text-bottom;
|
vertical-align: text-bottom;
|
||||||
object-fit: cover;
|
object-fit: contain;
|
||||||
object-position: left;
|
/* object-position: left; */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* EDIT HISTORY */
|
/* EDIT HISTORY */
|
||||||
|
@ -2289,7 +2567,7 @@ a.card:is(:hover, :focus):visited {
|
||||||
mask-image: linear-gradient(to bottom, #000 80px, transparent);
|
mask-image: linear-gradient(to bottom, #000 80px, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:after {
|
&[data-read-more]:after {
|
||||||
content: attr(data-read-more);
|
content: attr(data-read-more);
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|
|
@ -11,6 +11,7 @@ import {
|
||||||
import { decodeBlurHash, getBlurHashAverageColor } from 'fast-blurhash';
|
import { decodeBlurHash, getBlurHashAverageColor } from 'fast-blurhash';
|
||||||
import { shallowEqual } from 'fast-equals';
|
import { shallowEqual } from 'fast-equals';
|
||||||
import prettify from 'html-prettify';
|
import prettify from 'html-prettify';
|
||||||
|
import { Fragment } from 'preact';
|
||||||
import { memo } from 'preact/compat';
|
import { memo } from 'preact/compat';
|
||||||
import {
|
import {
|
||||||
useCallback,
|
useCallback,
|
||||||
|
@ -50,11 +51,14 @@ import openCompose from '../utils/open-compose';
|
||||||
import pmem from '../utils/pmem';
|
import pmem from '../utils/pmem';
|
||||||
import safeBoundingBoxPadding from '../utils/safe-bounding-box-padding';
|
import safeBoundingBoxPadding from '../utils/safe-bounding-box-padding';
|
||||||
import shortenNumber from '../utils/shorten-number';
|
import shortenNumber from '../utils/shorten-number';
|
||||||
|
import showCompose from '../utils/show-compose';
|
||||||
import showToast from '../utils/show-toast';
|
import showToast from '../utils/show-toast';
|
||||||
import { speak, supportsTTS } from '../utils/speech';
|
import { speak, supportsTTS } from '../utils/speech';
|
||||||
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 { getCurrentAccountID } from '../utils/store-utils';
|
||||||
|
import supports from '../utils/supports';
|
||||||
import unfurlMastodonLink from '../utils/unfurl-link';
|
import unfurlMastodonLink from '../utils/unfurl-link';
|
||||||
import useTruncated from '../utils/useTruncated';
|
import useTruncated from '../utils/useTruncated';
|
||||||
import visibilityIconsMap from '../utils/visibility-icons-map';
|
import visibilityIconsMap from '../utils/visibility-icons-map';
|
||||||
|
@ -148,6 +152,12 @@ const PostContent = memo(
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const SIZE_CLASS = {
|
||||||
|
s: 'small',
|
||||||
|
m: 'medium',
|
||||||
|
l: 'large',
|
||||||
|
};
|
||||||
|
|
||||||
function Status({
|
function Status({
|
||||||
statusID,
|
statusID,
|
||||||
status,
|
status,
|
||||||
|
@ -169,15 +179,23 @@ function Status({
|
||||||
allowContextMenu,
|
allowContextMenu,
|
||||||
showActionsBar,
|
showActionsBar,
|
||||||
showReplyParent,
|
showReplyParent,
|
||||||
|
mediaFirst,
|
||||||
}) {
|
}) {
|
||||||
if (skeleton) {
|
if (skeleton) {
|
||||||
return (
|
return (
|
||||||
<div class="status skeleton">
|
<div
|
||||||
<Avatar size="xxl" />
|
class={`status skeleton ${
|
||||||
|
mediaFirst ? 'status-media-first small' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{!mediaFirst && <Avatar size="xxl" />}
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="meta">███ ████████</div>
|
<div class="meta">
|
||||||
|
{(size === 's' || mediaFirst) && <Avatar size="m" />} ███ ████████
|
||||||
|
</div>
|
||||||
<div class="content-container">
|
<div class="content-container">
|
||||||
<div class="content">
|
{mediaFirst && <div class="media-first-container" />}
|
||||||
|
<div class={`content ${mediaFirst ? 'media-first-content' : ''}`}>
|
||||||
<p>████ ████████</p>
|
<p>████ ████████</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -247,8 +265,12 @@ function Status({
|
||||||
emojiReactions,
|
emojiReactions,
|
||||||
} = status;
|
} = status;
|
||||||
|
|
||||||
|
// if (!mediaAttachments?.length) mediaFirst = false;
|
||||||
|
const hasMediaAttachments = !!mediaAttachments?.length;
|
||||||
|
if (mediaFirst && hasMediaAttachments) size = 's';
|
||||||
|
|
||||||
const currentAccount = useMemo(() => {
|
const currentAccount = useMemo(() => {
|
||||||
return store.session.get('currentAccount');
|
return getCurrentAccountID();
|
||||||
}, []);
|
}, []);
|
||||||
const isSelf = useMemo(() => {
|
const isSelf = useMemo(() => {
|
||||||
return currentAccount && currentAccount === accountId;
|
return currentAccount && currentAccount === accountId;
|
||||||
|
@ -282,6 +304,7 @@ function Status({
|
||||||
onMouseEnter: debugHover,
|
onMouseEnter: debugHover,
|
||||||
}}
|
}}
|
||||||
showFollowedTags
|
showFollowedTags
|
||||||
|
quoted={quoted}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -354,6 +377,7 @@ function Status({
|
||||||
size={size}
|
size={size}
|
||||||
contentTextWeight={contentTextWeight}
|
contentTextWeight={contentTextWeight}
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
|
mediaFirst={mediaFirst}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -378,14 +402,15 @@ function Status({
|
||||||
contentTextWeight={contentTextWeight}
|
contentTextWeight={contentTextWeight}
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
enableCommentHint
|
enableCommentHint
|
||||||
|
mediaFirst={mediaFirst}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check followedTags
|
// Check followedTags
|
||||||
if (showFollowedTags && !!snapStates.statusFollowedTags[sKey]?.length) {
|
const FollowedTagsParent = useCallback(
|
||||||
return (
|
({ children }) => (
|
||||||
<div
|
<div
|
||||||
data-state-post-id={sKey}
|
data-state-post-id={sKey}
|
||||||
class="status-followed-tags"
|
class="status-followed-tags"
|
||||||
|
@ -403,18 +428,15 @@ function Status({
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<Status
|
{children}
|
||||||
status={statusID ? null : status}
|
|
||||||
statusID={statusID ? status.id : null}
|
|
||||||
instance={instance}
|
|
||||||
size={size}
|
|
||||||
contentTextWeight={contentTextWeight}
|
|
||||||
readOnly={readOnly}
|
|
||||||
enableCommentHint
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
),
|
||||||
}
|
[sKey, instance, snapStates.statusFollowedTags[sKey]],
|
||||||
|
);
|
||||||
|
const StatusParent =
|
||||||
|
showFollowedTags && !!snapStates.statusFollowedTags[sKey]?.length
|
||||||
|
? FollowedTagsParent
|
||||||
|
: Fragment;
|
||||||
|
|
||||||
const isSizeLarge = size === 'l';
|
const isSizeLarge = size === 'l';
|
||||||
|
|
||||||
|
@ -503,9 +525,9 @@ function Status({
|
||||||
});
|
});
|
||||||
if (newWin) return;
|
if (newWin) return;
|
||||||
}
|
}
|
||||||
states.showCompose = {
|
showCompose({
|
||||||
replyToStatus: status,
|
replyToStatus: status,
|
||||||
};
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Check if media has no descriptions
|
// Check if media has no descriptions
|
||||||
|
@ -628,6 +650,7 @@ function Status({
|
||||||
};
|
};
|
||||||
|
|
||||||
const bookmarkStatus = async () => {
|
const bookmarkStatus = async () => {
|
||||||
|
if (!supports('@mastodon/post-bookmark')) return;
|
||||||
if (!sameInstance || !authenticated) {
|
if (!sameInstance || !authenticated) {
|
||||||
alert(unauthInteractionErrorMessage);
|
alert(unauthInteractionErrorMessage);
|
||||||
return false;
|
return false;
|
||||||
|
@ -749,11 +772,11 @@ function Status({
|
||||||
menuExtras={
|
menuExtras={
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
states.showCompose = {
|
showCompose({
|
||||||
draftStatus: {
|
draftStatus: {
|
||||||
status: `\n${url}`,
|
status: `\n${url}`,
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="quote" />
|
<Icon icon="quote" />
|
||||||
|
@ -815,13 +838,15 @@ function Status({
|
||||||
: 'Like'}
|
: 'Like'}
|
||||||
</span>
|
</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
{supports('@mastodon/post-bookmark') && (
|
||||||
onClick={bookmarkStatusNotify}
|
<MenuItem
|
||||||
className={`menu-bookmark ${bookmarked ? 'checked' : ''}`}
|
onClick={bookmarkStatusNotify}
|
||||||
>
|
className={`menu-bookmark ${bookmarked ? 'checked' : ''}`}
|
||||||
<Icon icon="bookmark" />
|
>
|
||||||
<span>{bookmarked ? 'Unbookmark' : 'Bookmark'}</span>
|
<Icon icon="bookmark" />
|
||||||
</MenuItem>
|
<span>{bookmarked ? 'Unbookmark' : 'Bookmark'}</span>
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
@ -848,56 +873,62 @@ function Status({
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{(enableTranslate || !language || differentLanguage) && <MenuDivider />}
|
{!mediaFirst && (
|
||||||
{enableTranslate ? (
|
<>
|
||||||
<div class={supportsTTS ? 'menu-horizontal' : ''}>
|
{(enableTranslate || !language || differentLanguage) && (
|
||||||
<MenuItem
|
<MenuDivider />
|
||||||
disabled={forceTranslate}
|
|
||||||
onClick={() => {
|
|
||||||
setForceTranslate(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon icon="translate" />
|
|
||||||
<span>Translate</span>
|
|
||||||
</MenuItem>
|
|
||||||
{supportsTTS && (
|
|
||||||
<MenuItem
|
|
||||||
onClick={() => {
|
|
||||||
const postText = getPostText(status);
|
|
||||||
if (postText) {
|
|
||||||
speak(postText, language);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon icon="speak" />
|
|
||||||
<span>Speak</span>
|
|
||||||
</MenuItem>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
{enableTranslate ? (
|
||||||
) : (
|
<div class={supportsTTS ? 'menu-horizontal' : ''}>
|
||||||
(!language || differentLanguage) && (
|
|
||||||
<div class={supportsTTS ? 'menu-horizontal' : ''}>
|
|
||||||
<MenuLink
|
|
||||||
to={`${instance ? `/${instance}` : ''}/s/${id}?translate=1`}
|
|
||||||
>
|
|
||||||
<Icon icon="translate" />
|
|
||||||
<span>Translate</span>
|
|
||||||
</MenuLink>
|
|
||||||
{supportsTTS && (
|
|
||||||
<MenuItem
|
<MenuItem
|
||||||
|
disabled={forceTranslate}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const postText = getPostText(status);
|
setForceTranslate(true);
|
||||||
if (postText) {
|
|
||||||
speak(postText, language);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="speak" />
|
<Icon icon="translate" />
|
||||||
<span>Speak</span>
|
<span>Translate</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
{supportsTTS && (
|
||||||
</div>
|
<MenuItem
|
||||||
)
|
onClick={() => {
|
||||||
|
const postText = getPostText(status);
|
||||||
|
if (postText) {
|
||||||
|
speak(postText, language);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon="speak" />
|
||||||
|
<span>Speak</span>
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
(!language || differentLanguage) && (
|
||||||
|
<div class={supportsTTS ? 'menu-horizontal' : ''}>
|
||||||
|
<MenuLink
|
||||||
|
to={`${instance ? `/${instance}` : ''}/s/${id}?translate=1`}
|
||||||
|
>
|
||||||
|
<Icon icon="translate" />
|
||||||
|
<span>Translate</span>
|
||||||
|
</MenuLink>
|
||||||
|
{supportsTTS && (
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
const postText = getPostText(status);
|
||||||
|
if (postText) {
|
||||||
|
speak(postText, language);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon="speak" />
|
||||||
|
<span>Speak</span>
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
{((!isSizeLarge && sameInstance) ||
|
{((!isSizeLarge && sameInstance) ||
|
||||||
enableTranslate ||
|
enableTranslate ||
|
||||||
|
@ -1059,16 +1090,18 @@ function Status({
|
||||||
)}
|
)}
|
||||||
{isSelf && (
|
{isSelf && (
|
||||||
<div class="menu-horizontal">
|
<div class="menu-horizontal">
|
||||||
<MenuItem
|
{supports('@mastodon/post-edit') && (
|
||||||
onClick={() => {
|
<MenuItem
|
||||||
states.showCompose = {
|
onClick={() => {
|
||||||
editStatus: status,
|
showCompose({
|
||||||
};
|
editStatus: status,
|
||||||
}}
|
});
|
||||||
>
|
}}
|
||||||
<Icon icon="pencil" />
|
>
|
||||||
<span>Edit</span>
|
<Icon icon="pencil" />
|
||||||
</MenuItem>
|
<span>Edit</span>
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
{isSizeLarge && (
|
{isSizeLarge && (
|
||||||
<MenuConfirm
|
<MenuConfirm
|
||||||
subMenu
|
subMenu
|
||||||
|
@ -1349,7 +1382,7 @@ function Status({
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<StatusParent>
|
||||||
{showReplyParent && !!(inReplyToId && inReplyToAccountId) && (
|
{showReplyParent && !!(inReplyToId && inReplyToAccountId) && (
|
||||||
<StatusCompact sKey={sKey} />
|
<StatusCompact sKey={sKey} />
|
||||||
)}
|
)}
|
||||||
|
@ -1377,14 +1410,10 @@ function Status({
|
||||||
? 'status-reply-to'
|
? 'status-reply-to'
|
||||||
: ''
|
: ''
|
||||||
} visibility-${visibility} ${_pinned ? 'status-pinned' : ''} ${
|
} visibility-${visibility} ${_pinned ? 'status-pinned' : ''} ${
|
||||||
{
|
SIZE_CLASS[size]
|
||||||
s: 'small',
|
|
||||||
m: 'medium',
|
|
||||||
l: 'large',
|
|
||||||
}[size]
|
|
||||||
} ${_deleted ? 'status-deleted' : ''} ${quoted ? 'status-card' : ''} ${
|
} ${_deleted ? 'status-deleted' : ''} ${quoted ? 'status-card' : ''} ${
|
||||||
isContextMenuOpen ? 'status-menu-open' : ''
|
isContextMenuOpen ? 'status-menu-open' : ''
|
||||||
}`}
|
} ${mediaFirst && hasMediaAttachments ? 'status-media-first' : ''}`}
|
||||||
onMouseEnter={debugHover}
|
onMouseEnter={debugHover}
|
||||||
onContextMenu={(e) => {
|
onContextMenu={(e) => {
|
||||||
if (!showContextMenu) return;
|
if (!showContextMenu) return;
|
||||||
|
@ -1712,188 +1741,253 @@ function Status({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{!!spoilerText && (
|
{mediaFirst && hasMediaAttachments ? (
|
||||||
<>
|
<>
|
||||||
<div
|
{(!!spoilerText || !!sensitive) && !readingExpandSpoilers && (
|
||||||
class="content spoiler-content"
|
<>
|
||||||
lang={language}
|
{!!spoilerText && (
|
||||||
dir="auto"
|
<span
|
||||||
ref={spoilerContentRef}
|
class="spoiler-content media-first-spoiler-content"
|
||||||
data-read-more={readMoreText}
|
lang={language}
|
||||||
>
|
dir="auto"
|
||||||
<p>
|
ref={spoilerContentRef}
|
||||||
<EmojiText text={spoilerText} emojis={emojis} />
|
data-read-more={readMoreText}
|
||||||
</p>
|
>
|
||||||
</div>
|
<EmojiText text={spoilerText} emojis={emojis} />{' '}
|
||||||
{readingExpandSpoilers || previewMode ? (
|
</span>
|
||||||
<div class="spoiler-divider">
|
)}
|
||||||
<Icon icon="eye-open" /> Content warning
|
<button
|
||||||
|
class={`light spoiler-button media-first-spoiler-button ${
|
||||||
|
showSpoiler ? 'spoiling' : ''
|
||||||
|
}`}
|
||||||
|
type="button"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
if (showSpoiler) {
|
||||||
|
delete states.spoilers[id];
|
||||||
|
if (!readingExpandSpoilers) {
|
||||||
|
delete states.spoilersMedia[id];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
states.spoilers[id] = true;
|
||||||
|
if (!readingExpandSpoilers) {
|
||||||
|
states.spoilersMedia[id] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon={showSpoiler ? 'eye-open' : 'eye-close'} />{' '}
|
||||||
|
{showSpoiler ? 'Show less' : 'Show content'}
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<MediaFirstContainer
|
||||||
|
mediaAttachments={mediaAttachments}
|
||||||
|
language={language}
|
||||||
|
postID={id}
|
||||||
|
instance={instance}
|
||||||
|
/>
|
||||||
|
{!!content && (
|
||||||
|
<div class="media-first-content content" ref={contentRef}>
|
||||||
|
<PostContent
|
||||||
|
post={status}
|
||||||
|
instance={instance}
|
||||||
|
previewMode={previewMode}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
class={`light spoiler-button ${
|
|
||||||
showSpoiler ? 'spoiling' : ''
|
|
||||||
}`}
|
|
||||||
type="button"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
if (showSpoiler) {
|
|
||||||
delete states.spoilers[id];
|
|
||||||
if (!readingExpandSpoilers) {
|
|
||||||
delete states.spoilersMedia[id];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
states.spoilers[id] = true;
|
|
||||||
if (!readingExpandSpoilers) {
|
|
||||||
states.spoilersMedia[id] = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon icon={showSpoiler ? 'eye-open' : 'eye-close'} />{' '}
|
|
||||||
{showSpoiler ? 'Show less' : 'Show content'}
|
|
||||||
</button>
|
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
) : (
|
||||||
{!!content && (
|
<>
|
||||||
<div
|
{!!spoilerText && (
|
||||||
class="content"
|
<>
|
||||||
ref={contentRef}
|
<div
|
||||||
data-read-more={readMoreText}
|
class="content spoiler-content"
|
||||||
>
|
|
||||||
<PostContent
|
|
||||||
post={status}
|
|
||||||
instance={instance}
|
|
||||||
previewMode={previewMode}
|
|
||||||
/>
|
|
||||||
<QuoteStatuses id={id} instance={instance} level={quoted} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{!!poll && (
|
|
||||||
<Poll
|
|
||||||
lang={language}
|
|
||||||
poll={poll}
|
|
||||||
readOnly={readOnly || !sameInstance || !authenticated}
|
|
||||||
onUpdate={(newPoll) => {
|
|
||||||
states.statuses[sKey].poll = newPoll;
|
|
||||||
}}
|
|
||||||
refresh={() => {
|
|
||||||
return masto.v1.polls
|
|
||||||
.$select(poll.id)
|
|
||||||
.fetch()
|
|
||||||
.then((pollResponse) => {
|
|
||||||
states.statuses[sKey].poll = pollResponse;
|
|
||||||
})
|
|
||||||
.catch((e) => {}); // Silently fail
|
|
||||||
}}
|
|
||||||
votePoll={(choices) => {
|
|
||||||
return masto.v1.polls
|
|
||||||
.$select(poll.id)
|
|
||||||
.votes.create({
|
|
||||||
choices,
|
|
||||||
})
|
|
||||||
.then((pollResponse) => {
|
|
||||||
states.statuses[sKey].poll = pollResponse;
|
|
||||||
})
|
|
||||||
.catch((e) => {}); // Silently fail
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{(((enableTranslate || inlineTranslate) &&
|
|
||||||
!!content.trim() &&
|
|
||||||
!!getHTMLText(emojifyText(content, emojis)) &&
|
|
||||||
differentLanguage) ||
|
|
||||||
forceTranslate) && (
|
|
||||||
<TranslationBlock
|
|
||||||
forceTranslate={forceTranslate || inlineTranslate}
|
|
||||||
mini={!isSizeLarge && !withinContext}
|
|
||||||
sourceLanguage={language}
|
|
||||||
text={getPostText(status)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{!previewMode &&
|
|
||||||
sensitive &&
|
|
||||||
!!mediaAttachments.length &&
|
|
||||||
readingExpandMedia !== 'show_all' && (
|
|
||||||
<button
|
|
||||||
class={`plain spoiler-media-button ${
|
|
||||||
showSpoilerMedia ? 'spoiling' : ''
|
|
||||||
}`}
|
|
||||||
type="button"
|
|
||||||
hidden={!readingExpandSpoilers && !!spoilerText}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
if (showSpoilerMedia) {
|
|
||||||
delete states.spoilersMedia[id];
|
|
||||||
} else {
|
|
||||||
states.spoilersMedia[id] = true;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon icon={showSpoilerMedia ? 'eye-open' : 'eye-close'} />{' '}
|
|
||||||
{showSpoilerMedia ? 'Show less' : 'Show media'}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{!!mediaAttachments.length && (
|
|
||||||
<MultipleMediaFigure
|
|
||||||
lang={language}
|
|
||||||
enabled={showMultipleMediaCaptions}
|
|
||||||
captionChildren={captionChildren}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
ref={mediaContainerRef}
|
|
||||||
class={`media-container media-eq${mediaAttachments.length} ${
|
|
||||||
mediaAttachments.length > 2 ? 'media-gt2' : ''
|
|
||||||
} ${mediaAttachments.length > 4 ? 'media-gt4' : ''}`}
|
|
||||||
>
|
|
||||||
{displayedMediaAttachments.map((media, i) => (
|
|
||||||
<Media
|
|
||||||
key={media.id}
|
|
||||||
media={media}
|
|
||||||
autoAnimate={isSizeLarge}
|
|
||||||
showCaption={mediaAttachments.length === 1}
|
|
||||||
allowLongerCaption={
|
|
||||||
!content && mediaAttachments.length === 1
|
|
||||||
}
|
|
||||||
lang={language}
|
lang={language}
|
||||||
altIndex={
|
dir="auto"
|
||||||
showMultipleMediaCaptions &&
|
ref={spoilerContentRef}
|
||||||
!!media.description &&
|
data-read-more={readMoreText}
|
||||||
i + 1
|
>
|
||||||
}
|
<p>
|
||||||
to={`/${instance}/s/${id}?${
|
<EmojiText text={spoilerText} emojis={emojis} />
|
||||||
withinContext ? 'media' : 'media-only'
|
</p>
|
||||||
}=${i + 1}`}
|
</div>
|
||||||
onClick={
|
{readingExpandSpoilers || previewMode ? (
|
||||||
onMediaClick
|
<div class="spoiler-divider">
|
||||||
? (e) => {
|
<Icon icon="eye-open" /> Content warning
|
||||||
onMediaClick(e, i, media, status);
|
</div>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
class={`light spoiler-button ${
|
||||||
|
showSpoiler ? 'spoiling' : ''
|
||||||
|
}`}
|
||||||
|
type="button"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
if (showSpoiler) {
|
||||||
|
delete states.spoilers[id];
|
||||||
|
if (!readingExpandSpoilers) {
|
||||||
|
delete states.spoilersMedia[id];
|
||||||
}
|
}
|
||||||
: undefined
|
} else {
|
||||||
}
|
states.spoilers[id] = true;
|
||||||
|
if (!readingExpandSpoilers) {
|
||||||
|
states.spoilersMedia[id] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon={showSpoiler ? 'eye-open' : 'eye-close'} />{' '}
|
||||||
|
{showSpoiler ? 'Show less' : 'Show content'}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{!!content && (
|
||||||
|
<div
|
||||||
|
class="content"
|
||||||
|
ref={contentRef}
|
||||||
|
data-read-more={readMoreText}
|
||||||
|
>
|
||||||
|
<PostContent
|
||||||
|
post={status}
|
||||||
|
instance={instance}
|
||||||
|
previewMode={previewMode}
|
||||||
/>
|
/>
|
||||||
))}
|
<QuoteStatuses id={id} instance={instance} level={quoted} />
|
||||||
</div>
|
</div>
|
||||||
</MultipleMediaFigure>
|
)}
|
||||||
|
{!!poll && (
|
||||||
|
<Poll
|
||||||
|
lang={language}
|
||||||
|
poll={poll}
|
||||||
|
readOnly={readOnly || !sameInstance || !authenticated}
|
||||||
|
onUpdate={(newPoll) => {
|
||||||
|
states.statuses[sKey].poll = newPoll;
|
||||||
|
}}
|
||||||
|
refresh={() => {
|
||||||
|
return masto.v1.polls
|
||||||
|
.$select(poll.id)
|
||||||
|
.fetch()
|
||||||
|
.then((pollResponse) => {
|
||||||
|
states.statuses[sKey].poll = pollResponse;
|
||||||
|
})
|
||||||
|
.catch((e) => {}); // Silently fail
|
||||||
|
}}
|
||||||
|
votePoll={(choices) => {
|
||||||
|
return masto.v1.polls
|
||||||
|
.$select(poll.id)
|
||||||
|
.votes.create({
|
||||||
|
choices,
|
||||||
|
})
|
||||||
|
.then((pollResponse) => {
|
||||||
|
states.statuses[sKey].poll = pollResponse;
|
||||||
|
})
|
||||||
|
.catch((e) => {}); // Silently fail
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{(((enableTranslate || inlineTranslate) &&
|
||||||
|
!!content.trim() &&
|
||||||
|
!!getHTMLText(emojifyText(content, emojis)) &&
|
||||||
|
differentLanguage) ||
|
||||||
|
forceTranslate) && (
|
||||||
|
<TranslationBlock
|
||||||
|
forceTranslate={forceTranslate || inlineTranslate}
|
||||||
|
mini={!isSizeLarge && !withinContext}
|
||||||
|
sourceLanguage={language}
|
||||||
|
text={getPostText(status)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{!previewMode &&
|
||||||
|
sensitive &&
|
||||||
|
!!mediaAttachments.length &&
|
||||||
|
readingExpandMedia !== 'show_all' && (
|
||||||
|
<button
|
||||||
|
class={`plain spoiler-media-button ${
|
||||||
|
showSpoilerMedia ? 'spoiling' : ''
|
||||||
|
}`}
|
||||||
|
type="button"
|
||||||
|
hidden={!readingExpandSpoilers && !!spoilerText}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
if (showSpoilerMedia) {
|
||||||
|
delete states.spoilersMedia[id];
|
||||||
|
} else {
|
||||||
|
states.spoilersMedia[id] = true;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon={showSpoilerMedia ? 'eye-open' : 'eye-close'}
|
||||||
|
/>{' '}
|
||||||
|
{showSpoilerMedia ? 'Show less' : 'Show media'}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{!!mediaAttachments.length && (
|
||||||
|
<MultipleMediaFigure
|
||||||
|
lang={language}
|
||||||
|
enabled={showMultipleMediaCaptions}
|
||||||
|
captionChildren={captionChildren}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref={mediaContainerRef}
|
||||||
|
class={`media-container media-eq${
|
||||||
|
mediaAttachments.length
|
||||||
|
} ${mediaAttachments.length > 2 ? 'media-gt2' : ''} ${
|
||||||
|
mediaAttachments.length > 4 ? 'media-gt4' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{displayedMediaAttachments.map((media, i) => (
|
||||||
|
<Media
|
||||||
|
key={media.id}
|
||||||
|
media={media}
|
||||||
|
autoAnimate={isSizeLarge}
|
||||||
|
showCaption={mediaAttachments.length === 1}
|
||||||
|
allowLongerCaption={
|
||||||
|
!content && mediaAttachments.length === 1
|
||||||
|
}
|
||||||
|
lang={language}
|
||||||
|
altIndex={
|
||||||
|
showMultipleMediaCaptions &&
|
||||||
|
!!media.description &&
|
||||||
|
i + 1
|
||||||
|
}
|
||||||
|
to={`/${instance}/s/${id}?${
|
||||||
|
withinContext ? 'media' : 'media-only'
|
||||||
|
}=${i + 1}`}
|
||||||
|
onClick={
|
||||||
|
onMediaClick
|
||||||
|
? (e) => {
|
||||||
|
onMediaClick(e, i, media, status);
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</MultipleMediaFigure>
|
||||||
|
)}
|
||||||
|
{!!card &&
|
||||||
|
/^https/i.test(card?.url) &&
|
||||||
|
!sensitive &&
|
||||||
|
!spoilerText &&
|
||||||
|
!poll &&
|
||||||
|
!mediaAttachments.length &&
|
||||||
|
!snapStates.statusQuotes[sKey] && (
|
||||||
|
<Card
|
||||||
|
card={card}
|
||||||
|
selfReferential={
|
||||||
|
card?.url === status.url || card?.url === status.uri
|
||||||
|
}
|
||||||
|
instance={currentInstance}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
{!!card &&
|
|
||||||
/^https/i.test(card?.url) &&
|
|
||||||
!sensitive &&
|
|
||||||
!spoilerText &&
|
|
||||||
!poll &&
|
|
||||||
!mediaAttachments.length &&
|
|
||||||
!snapStates.statusQuotes[sKey] && (
|
|
||||||
<Card
|
|
||||||
card={card}
|
|
||||||
selfReferential={
|
|
||||||
card?.url === status.url || card?.url === status.uri
|
|
||||||
}
|
|
||||||
instance={currentInstance}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
{!isSizeLarge && showCommentCount && (
|
{!isSizeLarge && showCommentCount && (
|
||||||
<div class="content-comment-hint insignificant">
|
<div class="content-comment-hint insignificant">
|
||||||
|
@ -2032,11 +2126,11 @@ function Status({
|
||||||
menuExtras={
|
menuExtras={
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
states.showCompose = {
|
showCompose({
|
||||||
draftStatus: {
|
draftStatus: {
|
||||||
status: `\n${url}`,
|
status: `\n${url}`,
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="quote" />
|
<Icon icon="quote" />
|
||||||
|
@ -2077,16 +2171,18 @@ function Status({
|
||||||
onClick={favouriteStatus}
|
onClick={favouriteStatus}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="action">
|
{supports('@mastodon/post-bookmark') && (
|
||||||
<StatusButton
|
<div class="action">
|
||||||
checked={bookmarked}
|
<StatusButton
|
||||||
title={['Bookmark', 'Unbookmark']}
|
checked={bookmarked}
|
||||||
alt={['Bookmark', 'Bookmarked']}
|
title={['Bookmark', 'Unbookmark']}
|
||||||
class="bookmark-button"
|
alt={['Bookmark', 'Bookmarked']}
|
||||||
icon="bookmark"
|
class="bookmark-button"
|
||||||
onClick={bookmarkStatus}
|
icon="bookmark"
|
||||||
/>
|
onClick={bookmarkStatus}
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<Menu2
|
<Menu2
|
||||||
portal={{
|
portal={{
|
||||||
target:
|
target:
|
||||||
|
@ -2154,7 +2250,7 @@ function Status({
|
||||||
</Modal>
|
</Modal>
|
||||||
)}
|
)}
|
||||||
</article>
|
</article>
|
||||||
</>
|
</StatusParent>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2171,6 +2267,108 @@ function MultipleMediaFigure(props) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function MediaFirstContainer(props) {
|
||||||
|
const { mediaAttachments, language, postID, instance } = props;
|
||||||
|
const moreThanOne = mediaAttachments.length > 1;
|
||||||
|
|
||||||
|
const carouselRef = useRef();
|
||||||
|
const [currentIndex, setCurrentIndex] = useState(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let handleScroll = () => {
|
||||||
|
const { clientWidth, scrollLeft } = carouselRef.current;
|
||||||
|
const index = Math.round(scrollLeft / clientWidth);
|
||||||
|
setCurrentIndex(index);
|
||||||
|
};
|
||||||
|
if (carouselRef.current) {
|
||||||
|
carouselRef.current.addEventListener('scroll', handleScroll, {
|
||||||
|
passive: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
if (carouselRef.current) {
|
||||||
|
carouselRef.current.removeEventListener('scroll', handleScroll);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div class="media-first-container">
|
||||||
|
<div class="media-first-carousel" ref={carouselRef}>
|
||||||
|
{mediaAttachments.map((media, i) => (
|
||||||
|
<div class="media-first-item" key={media.id}>
|
||||||
|
<Media
|
||||||
|
media={media}
|
||||||
|
lang={language}
|
||||||
|
to={`/${instance}/s/${postID}?media=${i + 1}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{moreThanOne && (
|
||||||
|
<div class="media-carousel-controls">
|
||||||
|
<div class="carousel-indexer">
|
||||||
|
{currentIndex + 1}/{mediaAttachments.length}
|
||||||
|
</div>
|
||||||
|
<label class="media-carousel-button">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="carousel-button"
|
||||||
|
hidden={currentIndex === 0}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
carouselRef.current.focus();
|
||||||
|
carouselRef.current.scrollTo({
|
||||||
|
left: carouselRef.current.clientWidth * (currentIndex - 1),
|
||||||
|
behavior: 'smooth',
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon="arrow-left" />
|
||||||
|
</button>
|
||||||
|
</label>
|
||||||
|
<label class="media-carousel-button">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="carousel-button"
|
||||||
|
hidden={currentIndex === mediaAttachments.length - 1}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
carouselRef.current.focus();
|
||||||
|
carouselRef.current.scrollTo({
|
||||||
|
left: carouselRef.current.clientWidth * (currentIndex + 1),
|
||||||
|
behavior: 'smooth',
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon="arrow-right" />
|
||||||
|
</button>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{moreThanOne && (
|
||||||
|
<div
|
||||||
|
class="media-carousel-dots"
|
||||||
|
style={{
|
||||||
|
'--dots-count': mediaAttachments.length,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{mediaAttachments.map((media, i) => (
|
||||||
|
<span
|
||||||
|
key={media.id}
|
||||||
|
class={`carousel-dot ${i === currentIndex ? 'active' : ''}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function Card({ card, selfReferential, instance }) {
|
function Card({ card, selfReferential, instance }) {
|
||||||
const snapStates = useSnapshot(states);
|
const snapStates = useSnapshot(states);
|
||||||
const {
|
const {
|
||||||
|
@ -2980,6 +3178,7 @@ function FilteredStatus({
|
||||||
instance,
|
instance,
|
||||||
containerProps = {},
|
containerProps = {},
|
||||||
showFollowedTags,
|
showFollowedTags,
|
||||||
|
quoted,
|
||||||
}) {
|
}) {
|
||||||
const snapStates = useSnapshot(states);
|
const snapStates = useSnapshot(states);
|
||||||
const {
|
const {
|
||||||
|
@ -3024,7 +3223,9 @@ function FilteredStatus({
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
class={
|
class={
|
||||||
isReblog
|
quoted
|
||||||
|
? ''
|
||||||
|
: isReblog
|
||||||
? group
|
? group
|
||||||
? 'status-group'
|
? 'status-group'
|
||||||
: 'status-reblog'
|
: 'status-reblog'
|
||||||
|
@ -3040,7 +3241,11 @@ function FilteredStatus({
|
||||||
}}
|
}}
|
||||||
{...bindLongPressPeek()}
|
{...bindLongPressPeek()}
|
||||||
>
|
>
|
||||||
<article data-state-post-id={ssKey} class="status filtered" tabindex="-1">
|
<article
|
||||||
|
data-state-post-id={ssKey}
|
||||||
|
class={`status filtered ${quoted ? 'status-card' : ''}`}
|
||||||
|
tabindex="-1"
|
||||||
|
>
|
||||||
<b
|
<b
|
||||||
class="status-filtered-badge clickable badge-meta"
|
class="status-filtered-badge clickable badge-meta"
|
||||||
title={filterTitleStr}
|
title={filterTitleStr}
|
||||||
|
@ -3141,7 +3346,7 @@ const QuoteStatuses = memo(({ id, instance, level = 0 }) => {
|
||||||
|
|
||||||
return uniqueQuotes.map((q) => {
|
return uniqueQuotes.map((q) => {
|
||||||
return (
|
return (
|
||||||
<LazyShazam>
|
<LazyShazam id={q.instance + q.id}>
|
||||||
<Link
|
<Link
|
||||||
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}`}
|
||||||
|
|
|
@ -1,5 +1,11 @@
|
||||||
import { memo } from 'preact/compat';
|
import { memo } from 'preact/compat';
|
||||||
import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
|
import {
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'preact/hooks';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { InView } from 'react-intersection-observer';
|
import { InView } from 'react-intersection-observer';
|
||||||
import { useDebouncedCallback } from 'use-debounce';
|
import { useDebouncedCallback } from 'use-debounce';
|
||||||
|
@ -9,6 +15,7 @@ import FilterContext from '../utils/filter-context';
|
||||||
import { filteredItems, isFiltered } from '../utils/filters';
|
import { filteredItems, isFiltered } from '../utils/filters';
|
||||||
import states, { statusKey } from '../utils/states';
|
import states, { statusKey } from '../utils/states';
|
||||||
import statusPeek from '../utils/status-peek';
|
import statusPeek from '../utils/status-peek';
|
||||||
|
import { isMediaFirstInstance } from '../utils/store-utils';
|
||||||
import { groupBoosts, groupContext } from '../utils/timeline-utils';
|
import { groupBoosts, groupContext } from '../utils/timeline-utils';
|
||||||
import useInterval from '../utils/useInterval';
|
import useInterval from '../utils/useInterval';
|
||||||
import usePageVisibility from '../utils/usePageVisibility';
|
import usePageVisibility from '../utils/usePageVisibility';
|
||||||
|
@ -59,6 +66,8 @@ function Timeline({
|
||||||
|
|
||||||
console.debug('RENDER Timeline', id, refresh);
|
console.debug('RENDER Timeline', id, refresh);
|
||||||
|
|
||||||
|
const mediaFirst = useMemo(() => isMediaFirstInstance(), []);
|
||||||
|
|
||||||
const allowGrouping = view !== 'media';
|
const allowGrouping = view !== 'media';
|
||||||
const loadItems = useDebouncedCallback(
|
const loadItems = useDebouncedCallback(
|
||||||
(firstLoad) => {
|
(firstLoad) => {
|
||||||
|
@ -200,8 +209,8 @@ function Timeline({
|
||||||
|
|
||||||
const oRef = useHotkeys(['enter', 'o'], () => {
|
const oRef = useHotkeys(['enter', 'o'], () => {
|
||||||
// open active status
|
// open active status
|
||||||
const activeItem = document.activeElement.closest(itemsSelector);
|
const activeItem = document.activeElement;
|
||||||
if (activeItem) {
|
if (activeItem?.matches(itemsSelector)) {
|
||||||
activeItem.click();
|
activeItem.click();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -355,7 +364,9 @@ function Timeline({
|
||||||
<FilterContext.Provider value={filterContext}>
|
<FilterContext.Provider value={filterContext}>
|
||||||
<div
|
<div
|
||||||
id={`${id}-page`}
|
id={`${id}-page`}
|
||||||
class="deck-container"
|
class={`deck-container ${
|
||||||
|
mediaFirst ? 'deck-container-media-first' : ''
|
||||||
|
}`}
|
||||||
ref={(node) => {
|
ref={(node) => {
|
||||||
scrollableRef.current = node;
|
scrollableRef.current = node;
|
||||||
jRef.current = node;
|
jRef.current = node;
|
||||||
|
@ -432,6 +443,7 @@ function Timeline({
|
||||||
view={view}
|
view={view}
|
||||||
showFollowedTags={showFollowedTags}
|
showFollowedTags={showFollowedTags}
|
||||||
showReplyParent={showReplyParent}
|
showReplyParent={showReplyParent}
|
||||||
|
mediaFirst={mediaFirst}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{showMore &&
|
{showMore &&
|
||||||
|
@ -443,14 +455,14 @@ function Timeline({
|
||||||
height: '20vh',
|
height: '20vh',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Status skeleton />
|
<Status skeleton mediaFirst={mediaFirst} />
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
style={{
|
style={{
|
||||||
height: '25vh',
|
height: '25vh',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Status skeleton />
|
<Status skeleton mediaFirst={mediaFirst} />
|
||||||
</li>
|
</li>
|
||||||
</>
|
</>
|
||||||
))}
|
))}
|
||||||
|
@ -490,7 +502,7 @@ function Timeline({
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<li key={i}>
|
<li key={i}>
|
||||||
<Status skeleton />
|
<Status skeleton mediaFirst={mediaFirst} />
|
||||||
</li>
|
</li>
|
||||||
),
|
),
|
||||||
)}
|
)}
|
||||||
|
@ -525,6 +537,7 @@ const TimelineItem = memo(
|
||||||
view,
|
view,
|
||||||
showFollowedTags,
|
showFollowedTags,
|
||||||
showReplyParent,
|
showReplyParent,
|
||||||
|
mediaFirst,
|
||||||
}) => {
|
}) => {
|
||||||
console.debug('RENDER TimelineItem', status.id);
|
console.debug('RENDER TimelineItem', status.id);
|
||||||
const { id: statusID, reblog, items, type, _pinned } = status;
|
const { id: statusID, reblog, items, type, _pinned } = status;
|
||||||
|
@ -533,6 +546,7 @@ const TimelineItem = memo(
|
||||||
const url = instance
|
const url = instance
|
||||||
? `/${instance}/s/${actualStatusID}`
|
? `/${instance}/s/${actualStatusID}`
|
||||||
: `/s/${actualStatusID}`;
|
: `/s/${actualStatusID}`;
|
||||||
|
|
||||||
if (items) {
|
if (items) {
|
||||||
const fItems = filteredItems(items, filterContext);
|
const fItems = filteredItems(items, filterContext);
|
||||||
let title = '';
|
let title = '';
|
||||||
|
@ -585,6 +599,7 @@ const TimelineItem = memo(
|
||||||
contentTextWeight
|
contentTextWeight
|
||||||
enableCommentHint
|
enableCommentHint
|
||||||
// allowFilters={allowFilters}
|
// allowFilters={allowFilters}
|
||||||
|
mediaFirst={mediaFirst}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Status
|
<Status
|
||||||
|
@ -594,6 +609,7 @@ const TimelineItem = memo(
|
||||||
contentTextWeight
|
contentTextWeight
|
||||||
enableCommentHint
|
enableCommentHint
|
||||||
// allowFilters={allowFilters}
|
// allowFilters={allowFilters}
|
||||||
|
mediaFirst={mediaFirst}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -630,7 +646,11 @@ const TimelineItem = memo(
|
||||||
>
|
>
|
||||||
<Link class="status-link timeline-item" to={url}>
|
<Link class="status-link timeline-item" to={url}>
|
||||||
{showCompact ? (
|
{showCompact ? (
|
||||||
<TimelineStatusCompact status={item} instance={instance} />
|
<TimelineStatusCompact
|
||||||
|
status={item}
|
||||||
|
instance={instance}
|
||||||
|
filterContext={filterContext}
|
||||||
|
/>
|
||||||
) : useItemID ? (
|
) : useItemID ? (
|
||||||
<Status
|
<Status
|
||||||
statusID={statusID}
|
statusID={statusID}
|
||||||
|
@ -689,6 +709,7 @@ const TimelineItem = memo(
|
||||||
showFollowedTags={showFollowedTags}
|
showFollowedTags={showFollowedTags}
|
||||||
showReplyParent={showReplyParent}
|
showReplyParent={showReplyParent}
|
||||||
// allowFilters={allowFilters}
|
// allowFilters={allowFilters}
|
||||||
|
mediaFirst={mediaFirst}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Status
|
<Status
|
||||||
|
@ -698,6 +719,7 @@ const TimelineItem = memo(
|
||||||
showFollowedTags={showFollowedTags}
|
showFollowedTags={showFollowedTags}
|
||||||
showReplyParent={showReplyParent}
|
showReplyParent={showReplyParent}
|
||||||
// allowFilters={allowFilters}
|
// allowFilters={allowFilters}
|
||||||
|
mediaFirst={mediaFirst}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -802,11 +824,12 @@ function StatusCarousel({ title, class: className, children }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function TimelineStatusCompact({ status, instance }) {
|
function TimelineStatusCompact({ status, instance, filterContext }) {
|
||||||
const snapStates = useSnapshot(states);
|
const snapStates = useSnapshot(states);
|
||||||
const { id, visibility, language } = status;
|
const { id, visibility, language } = status;
|
||||||
const statusPeekText = statusPeek(status);
|
const statusPeekText = statusPeek(status);
|
||||||
const sKey = statusKey(id, instance);
|
const sKey = statusKey(id, instance);
|
||||||
|
const filterInfo = isFiltered(status.filtered, filterContext);
|
||||||
return (
|
return (
|
||||||
<article
|
<article
|
||||||
class={`status compact-thread ${
|
class={`status compact-thread ${
|
||||||
|
@ -832,13 +855,24 @@ function TimelineStatusCompact({ status, instance }) {
|
||||||
lang={language}
|
lang={language}
|
||||||
dir="auto"
|
dir="auto"
|
||||||
>
|
>
|
||||||
{statusPeekText}
|
{!!filterInfo ? (
|
||||||
{status.sensitive && status.spoilerText && (
|
<b
|
||||||
|
class="status-filtered-badge badge-meta horizontal"
|
||||||
|
title={filterInfo?.titlesStr || ''}
|
||||||
|
>
|
||||||
|
<span>Filtered</span>: <span>{filterInfo?.titlesStr || ''}</span>
|
||||||
|
</b>
|
||||||
|
) : (
|
||||||
<>
|
<>
|
||||||
{' '}
|
{statusPeekText}
|
||||||
<span class="spoiler-badge">
|
{status.sensitive && status.spoilerText && (
|
||||||
<Icon icon="eye-close" size="s" />
|
<>
|
||||||
</span>
|
{' '}
|
||||||
|
<span class="spoiler-badge">
|
||||||
|
<Icon icon="eye-close" size="s" />
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -3,15 +3,12 @@ import './index.css';
|
||||||
import './app.css';
|
import './app.css';
|
||||||
|
|
||||||
import { render } from 'preact';
|
import { render } from 'preact';
|
||||||
import { lazy } from 'preact/compat';
|
|
||||||
import { useEffect, useState } from 'preact/hooks';
|
import { useEffect, useState } from 'preact/hooks';
|
||||||
|
|
||||||
import IntlSegmenterSuspense from './components/intl-segmenter-suspense';
|
import ComposeSuspense from './components/compose-suspense';
|
||||||
// import Compose from './components/compose';
|
import { initStates } from './utils/states';
|
||||||
import useTitle from './utils/useTitle';
|
import useTitle from './utils/useTitle';
|
||||||
|
|
||||||
const Compose = lazy(() => import('./components/compose'));
|
|
||||||
|
|
||||||
if (window.opener) {
|
if (window.opener) {
|
||||||
console = window.opener.console;
|
console = window.opener.console;
|
||||||
}
|
}
|
||||||
|
@ -31,6 +28,10 @@ function App() {
|
||||||
: 'Compose',
|
: 'Compose',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
initStates();
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (uiState === 'closed') {
|
if (uiState === 'closed') {
|
||||||
try {
|
try {
|
||||||
|
@ -61,25 +62,23 @@ function App() {
|
||||||
console.debug('OPEN COMPOSE');
|
console.debug('OPEN COMPOSE');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IntlSegmenterSuspense>
|
<ComposeSuspense
|
||||||
<Compose
|
editStatus={editStatus}
|
||||||
editStatus={editStatus}
|
replyToStatus={replyToStatus}
|
||||||
replyToStatus={replyToStatus}
|
draftStatus={draftStatus}
|
||||||
draftStatus={draftStatus}
|
standalone
|
||||||
standalone
|
hasOpener={window.opener}
|
||||||
hasOpener={window.opener}
|
onClose={(results) => {
|
||||||
onClose={(results) => {
|
const { newStatus, fn = () => {} } = results || {};
|
||||||
const { newStatus, fn = () => {} } = results || {};
|
try {
|
||||||
try {
|
if (newStatus) {
|
||||||
if (newStatus) {
|
window.opener.__STATES__.reloadStatusPage++;
|
||||||
window.opener.__STATES__.reloadStatusPage++;
|
}
|
||||||
}
|
fn();
|
||||||
fn();
|
setUIState('closed');
|
||||||
setUIState('closed');
|
} catch (e) {}
|
||||||
} catch (e) {}
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
|
||||||
</IntlSegmenterSuspense>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -547,3 +547,9 @@ kbd {
|
||||||
.shazam-container-horizontal[hidden] {
|
.shazam-container-horizontal[hidden] {
|
||||||
grid-template-columns: 0fr;
|
grid-template-columns: 0fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ import pmem from '../utils/pmem';
|
||||||
import showToast from '../utils/show-toast';
|
import showToast from '../utils/show-toast';
|
||||||
import states from '../utils/states';
|
import states from '../utils/states';
|
||||||
import { saveStatus } from '../utils/states';
|
import { saveStatus } from '../utils/states';
|
||||||
|
import { isMediaFirstInstance } from '../utils/store-utils';
|
||||||
import useTitle from '../utils/useTitle';
|
import useTitle from '../utils/useTitle';
|
||||||
|
|
||||||
const LIMIT = 20;
|
const LIMIT = 20;
|
||||||
|
@ -68,6 +69,8 @@ function AccountStatuses() {
|
||||||
searchOffsetRef.current = 0;
|
searchOffsetRef.current = 0;
|
||||||
}, allSearchParams);
|
}, allSearchParams);
|
||||||
|
|
||||||
|
const mediaFirst = useMemo(() => isMediaFirstInstance(), []);
|
||||||
|
|
||||||
const sameCurrentInstance = useMemo(
|
const sameCurrentInstance = useMemo(
|
||||||
() => instance === currentInstance,
|
() => instance === currentInstance,
|
||||||
[instance, currentInstance],
|
[instance, currentInstance],
|
||||||
|
@ -151,7 +154,7 @@ function AccountStatuses() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const results = [];
|
let results = [];
|
||||||
if (firstLoad) {
|
if (firstLoad) {
|
||||||
const { value } = await masto.v1.accounts
|
const { value } = await masto.v1.accounts
|
||||||
.$select(id)
|
.$select(id)
|
||||||
|
@ -186,12 +189,32 @@ function AccountStatuses() {
|
||||||
limit: LIMIT,
|
limit: LIMIT,
|
||||||
exclude_replies: excludeReplies,
|
exclude_replies: excludeReplies,
|
||||||
exclude_reblogs: excludeBoosts,
|
exclude_reblogs: excludeBoosts,
|
||||||
only_media: media,
|
only_media: media || undefined,
|
||||||
tagged,
|
tagged,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const { value, done } = await accountStatusesIterator.current.next();
|
const { value, done } = await accountStatusesIterator.current.next();
|
||||||
if (value?.length) {
|
if (value?.length) {
|
||||||
|
// Check if value is same as pinned post (results)
|
||||||
|
// If the index for every post is the same, means API might not support pinned posts
|
||||||
|
if (results.length) {
|
||||||
|
let pinnedStatusesIds = [];
|
||||||
|
if (results[0]?.type === 'pinned') {
|
||||||
|
pinnedStatusesIds = results[0].id;
|
||||||
|
} else {
|
||||||
|
pinnedStatusesIds = results
|
||||||
|
.filter((status) => status._pinned)
|
||||||
|
.map((status) => status.id);
|
||||||
|
}
|
||||||
|
const containsAllPinned = pinnedStatusesIds.every((postId) =>
|
||||||
|
value.some((status) => status.id === postId),
|
||||||
|
);
|
||||||
|
if (containsAllPinned) {
|
||||||
|
// Remove pinned posts
|
||||||
|
results = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
results.push(...value);
|
results.push(...value);
|
||||||
|
|
||||||
value.forEach((item) => {
|
value.forEach((item) => {
|
||||||
|
@ -250,17 +273,21 @@ function AccountStatuses() {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
try {
|
// No need, because the whole filter bar is hidden
|
||||||
const featuredTags = await masto.v1.accounts
|
// TODO: Revisit this
|
||||||
.$select(id)
|
if (!mediaFirst) {
|
||||||
.featuredTags.list();
|
try {
|
||||||
console.log({ featuredTags });
|
const featuredTags = await masto.v1.accounts
|
||||||
setFeaturedTags(featuredTags);
|
.$select(id)
|
||||||
} catch (e) {
|
.featuredTags.list();
|
||||||
console.error(e);
|
console.log({ featuredTags });
|
||||||
|
setFeaturedTags(featuredTags);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}, [id]);
|
}, [id, mediaFirst]);
|
||||||
|
|
||||||
const { displayName, acct, emojis } = account || {};
|
const { displayName, acct, emojis } = account || {};
|
||||||
|
|
||||||
|
@ -279,95 +306,126 @@ function AccountStatuses() {
|
||||||
authenticated={authenticated}
|
authenticated={authenticated}
|
||||||
standalone
|
standalone
|
||||||
/>
|
/>
|
||||||
<div
|
{!mediaFirst && (
|
||||||
class="filter-bar"
|
<div
|
||||||
ref={filterBarRef}
|
class="filter-bar"
|
||||||
style={{
|
ref={filterBarRef}
|
||||||
position: 'relative',
|
style={{
|
||||||
}}
|
position: 'relative',
|
||||||
>
|
}}
|
||||||
{filtered ? (
|
>
|
||||||
|
{filtered ? (
|
||||||
|
<Link
|
||||||
|
to={`/${instance}/a/${id}`}
|
||||||
|
class="insignificant filter-clear"
|
||||||
|
title="Clear filters"
|
||||||
|
key="clear-filters"
|
||||||
|
>
|
||||||
|
<Icon icon="x" size="l" />
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<Icon icon="filter" class="insignificant" size="l" />
|
||||||
|
)}
|
||||||
<Link
|
<Link
|
||||||
to={`/${instance}/a/${id}`}
|
to={`/${instance}/a/${id}${excludeReplies ? '?replies=1' : ''}`}
|
||||||
class="insignificant filter-clear"
|
|
||||||
title="Clear filters"
|
|
||||||
key="clear-filters"
|
|
||||||
>
|
|
||||||
<Icon icon="x" size="l" />
|
|
||||||
</Link>
|
|
||||||
) : (
|
|
||||||
<Icon icon="filter" class="insignificant" size="l" />
|
|
||||||
)}
|
|
||||||
<Link
|
|
||||||
to={`/${instance}/a/${id}${excludeReplies ? '?replies=1' : ''}`}
|
|
||||||
onClick={() => {
|
|
||||||
if (excludeReplies) {
|
|
||||||
showToast('Showing post with replies');
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
class={excludeReplies ? '' : 'is-active'}
|
|
||||||
>
|
|
||||||
+ Replies
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
to={`/${instance}/a/${id}${excludeBoosts ? '' : '?boosts=0'}`}
|
|
||||||
onClick={() => {
|
|
||||||
if (!excludeBoosts) {
|
|
||||||
showToast('Showing posts without boosts');
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
class={!excludeBoosts ? '' : 'is-active'}
|
|
||||||
>
|
|
||||||
- Boosts
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
to={`/${instance}/a/${id}${media ? '' : '?media=1'}`}
|
|
||||||
onClick={() => {
|
|
||||||
if (!media) {
|
|
||||||
showToast('Showing posts with media');
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
class={media ? 'is-active' : ''}
|
|
||||||
>
|
|
||||||
Media
|
|
||||||
</Link>
|
|
||||||
{featuredTags.map((tag) => (
|
|
||||||
<Link
|
|
||||||
key={tag.id}
|
|
||||||
to={`/${instance}/a/${id}${
|
|
||||||
tagged === tag.name
|
|
||||||
? ''
|
|
||||||
: `?tagged=${encodeURIComponent(tag.name)}`
|
|
||||||
}`}
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (tagged !== tag.name) {
|
if (excludeReplies) {
|
||||||
showToast(`Showing posts tagged with #${tag.name}`);
|
showToast('Showing post with replies');
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
class={tagged === tag.name ? 'is-active' : ''}
|
class={excludeReplies ? '' : 'is-active'}
|
||||||
>
|
>
|
||||||
<span>
|
+ Replies
|
||||||
<span class="more-insignificant">#</span>
|
|
||||||
{tag.name}
|
|
||||||
</span>
|
|
||||||
{
|
|
||||||
// The count differs based on instance 😅
|
|
||||||
}
|
|
||||||
{/* <span class="filter-count">{tag.statusesCount}</span> */}
|
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
<Link
|
||||||
{searchEnabled &&
|
to={`/${instance}/a/${id}${excludeBoosts ? '' : '?boosts=0'}`}
|
||||||
(supportsInputMonth ? (
|
onClick={() => {
|
||||||
<label class={`filter-field ${month ? 'is-active' : ''}`}>
|
if (!excludeBoosts) {
|
||||||
<Icon icon="month" size="l" />
|
showToast('Showing posts without boosts');
|
||||||
<input
|
}
|
||||||
type="month"
|
}}
|
||||||
|
class={!excludeBoosts ? '' : 'is-active'}
|
||||||
|
>
|
||||||
|
- Boosts
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
to={`/${instance}/a/${id}${media ? '' : '?media=1'}`}
|
||||||
|
onClick={() => {
|
||||||
|
if (!media) {
|
||||||
|
showToast('Showing posts with media');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
class={media ? 'is-active' : ''}
|
||||||
|
>
|
||||||
|
Media
|
||||||
|
</Link>
|
||||||
|
{featuredTags.map((tag) => (
|
||||||
|
<Link
|
||||||
|
key={tag.id}
|
||||||
|
to={`/${instance}/a/${id}${
|
||||||
|
tagged === tag.name
|
||||||
|
? ''
|
||||||
|
: `?tagged=${encodeURIComponent(tag.name)}`
|
||||||
|
}`}
|
||||||
|
onClick={() => {
|
||||||
|
if (tagged !== tag.name) {
|
||||||
|
showToast(`Showing posts tagged with #${tag.name}`);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
class={tagged === tag.name ? 'is-active' : ''}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<span class="more-insignificant">#</span>
|
||||||
|
{tag.name}
|
||||||
|
</span>
|
||||||
|
{
|
||||||
|
// The count differs based on instance 😅
|
||||||
|
}
|
||||||
|
{/* <span class="filter-count">{tag.statusesCount}</span> */}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
{searchEnabled &&
|
||||||
|
(supportsInputMonth ? (
|
||||||
|
<label class={`filter-field ${month ? 'is-active' : ''}`}>
|
||||||
|
<Icon icon="month" size="l" />
|
||||||
|
<input
|
||||||
|
type="month"
|
||||||
|
disabled={!account?.acct}
|
||||||
|
value={month || ''}
|
||||||
|
min={MIN_YEAR_MONTH}
|
||||||
|
max={new Date().toISOString().slice(0, 7)}
|
||||||
|
onInput={(e) => {
|
||||||
|
const { value, validity } = e.currentTarget;
|
||||||
|
if (!validity.valid) return;
|
||||||
|
setSearchParams(
|
||||||
|
value
|
||||||
|
? {
|
||||||
|
month: value,
|
||||||
|
}
|
||||||
|
: {},
|
||||||
|
);
|
||||||
|
const [year, month] = value.split('-');
|
||||||
|
const monthIndex = parseInt(month, 10) - 1;
|
||||||
|
const date = new Date(year, monthIndex);
|
||||||
|
showToast(
|
||||||
|
`Showing posts in ${date.toLocaleString('default', {
|
||||||
|
month: 'long',
|
||||||
|
year: 'numeric',
|
||||||
|
})}`,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
) : (
|
||||||
|
// Fallback to <select> for month and <input type="number"> for year
|
||||||
|
<MonthPicker
|
||||||
|
class={`filter-field ${month ? 'is-active' : ''}`}
|
||||||
disabled={!account?.acct}
|
disabled={!account?.acct}
|
||||||
value={month || ''}
|
value={month || ''}
|
||||||
min={MIN_YEAR_MONTH}
|
min={MIN_YEAR_MONTH}
|
||||||
max={new Date().toISOString().slice(0, 7)}
|
max={new Date().toISOString().slice(0, 7)}
|
||||||
onInput={(e) => {
|
onInput={(e) => {
|
||||||
const { value, validity } = e.currentTarget;
|
const { value, validity } = e;
|
||||||
if (!validity.valid) return;
|
if (!validity.valid) return;
|
||||||
setSearchParams(
|
setSearchParams(
|
||||||
value
|
value
|
||||||
|
@ -376,40 +434,11 @@ function AccountStatuses() {
|
||||||
}
|
}
|
||||||
: {},
|
: {},
|
||||||
);
|
);
|
||||||
const [year, month] = value.split('-');
|
|
||||||
const monthIndex = parseInt(month, 10) - 1;
|
|
||||||
const date = new Date(year, monthIndex);
|
|
||||||
showToast(
|
|
||||||
`Showing posts in ${date.toLocaleString('default', {
|
|
||||||
month: 'long',
|
|
||||||
year: 'numeric',
|
|
||||||
})}`,
|
|
||||||
);
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</label>
|
))}
|
||||||
) : (
|
</div>
|
||||||
// Fallback to <select> for month and <input type="number"> for year
|
)}
|
||||||
<MonthPicker
|
|
||||||
class={`filter-field ${month ? 'is-active' : ''}`}
|
|
||||||
disabled={!account?.acct}
|
|
||||||
value={month || ''}
|
|
||||||
min={MIN_YEAR_MONTH}
|
|
||||||
max={new Date().toISOString().slice(0, 7)}
|
|
||||||
onInput={(e) => {
|
|
||||||
const { value, validity } = e;
|
|
||||||
if (!validity.valid) return;
|
|
||||||
setSearchParams(
|
|
||||||
value
|
|
||||||
? {
|
|
||||||
month: value,
|
|
||||||
}
|
|
||||||
: {},
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}, [
|
}, [
|
||||||
|
@ -472,7 +501,7 @@ function AccountStatuses() {
|
||||||
errorText="Unable to load posts"
|
errorText="Unable to load posts"
|
||||||
fetchItems={fetchAccountStatuses}
|
fetchItems={fetchAccountStatuses}
|
||||||
useItemID
|
useItemID
|
||||||
view={media ? 'media' : undefined}
|
view={media || mediaFirst ? 'media' : undefined}
|
||||||
boostsCarousel={snapStates.settings.boostsCarousel}
|
boostsCarousel={snapStates.settings.boostsCarousel}
|
||||||
timelineStart={TimelineStart}
|
timelineStart={TimelineStart}
|
||||||
refresh={[
|
refresh={[
|
||||||
|
|
|
@ -13,12 +13,13 @@ import NameText from '../components/name-text';
|
||||||
import { api } from '../utils/api';
|
import { api } from '../utils/api';
|
||||||
import states from '../utils/states';
|
import states from '../utils/states';
|
||||||
import store from '../utils/store';
|
import store from '../utils/store';
|
||||||
|
import { getCurrentAccountID, setCurrentAccountID } from '../utils/store-utils';
|
||||||
|
|
||||||
function Accounts({ onClose }) {
|
function Accounts({ onClose }) {
|
||||||
const { masto } = api();
|
const { masto } = api();
|
||||||
// Accounts
|
// Accounts
|
||||||
const accounts = store.local.getJSON('accounts');
|
const accounts = store.local.getJSON('accounts');
|
||||||
const currentAccount = store.session.get('currentAccount');
|
const currentAccount = getCurrentAccountID();
|
||||||
const moreThanOneAccount = accounts.length > 1;
|
const moreThanOneAccount = accounts.length > 1;
|
||||||
|
|
||||||
const [_, reload] = useReducer((x) => x + 1, 0);
|
const [_, reload] = useReducer((x) => x + 1, 0);
|
||||||
|
@ -81,7 +82,7 @@ function Accounts({ onClose }) {
|
||||||
if (isCurrent) {
|
if (isCurrent) {
|
||||||
states.showAccount = `${account.info.username}@${account.instanceURL}`;
|
states.showAccount = `${account.info.username}@${account.instanceURL}`;
|
||||||
} else {
|
} else {
|
||||||
store.session.set('currentAccount', account.info.id);
|
setCurrentAccountID(account.info.id);
|
||||||
location.reload();
|
location.reload();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -614,7 +614,7 @@
|
||||||
}
|
}
|
||||||
&.visibility-direct {
|
&.visibility-direct {
|
||||||
--yellow-stripes: repeating-linear-gradient(
|
--yellow-stripes: repeating-linear-gradient(
|
||||||
-45deg,
|
135deg,
|
||||||
var(--reply-to-faded-color),
|
var(--reply-to-faded-color),
|
||||||
var(--reply-to-faded-color) 10px,
|
var(--reply-to-faded-color) 10px,
|
||||||
var(--reply-to-faded-color) 10px,
|
var(--reply-to-faded-color) 10px,
|
||||||
|
|
|
@ -40,7 +40,7 @@ import showToast from '../utils/show-toast';
|
||||||
import states, { statusKey } from '../utils/states';
|
import states, { 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 { getCurrentAccountNS } from '../utils/store-utils';
|
import { getCurrentAccountID, getCurrentAccountNS } from '../utils/store-utils';
|
||||||
import { assignFollowedTags } from '../utils/timeline-utils';
|
import { assignFollowedTags } from '../utils/timeline-utils';
|
||||||
import useTitle from '../utils/useTitle';
|
import useTitle from '../utils/useTitle';
|
||||||
|
|
||||||
|
@ -112,7 +112,7 @@ function Catchup() {
|
||||||
const [showTopLinks, setShowTopLinks] = useState(false);
|
const [showTopLinks, setShowTopLinks] = useState(false);
|
||||||
|
|
||||||
const currentAccount = useMemo(() => {
|
const currentAccount = useMemo(() => {
|
||||||
return store.session.get('currentAccount');
|
return getCurrentAccountID();
|
||||||
}, []);
|
}, []);
|
||||||
const isSelf = (accountID) => accountID === currentAccount;
|
const isSelf = (accountID) => accountID === currentAccount;
|
||||||
|
|
||||||
|
@ -1677,63 +1677,70 @@ function PostPeek({ post, filterInfo }) {
|
||||||
} = post;
|
} = post;
|
||||||
const isThread =
|
const isThread =
|
||||||
(inReplyToId && inReplyToAccountId === account.id) || !!_thread;
|
(inReplyToId && inReplyToAccountId === account.id) || !!_thread;
|
||||||
const showMedia = !spoilerText && !sensitive;
|
|
||||||
|
const readingExpandSpoilers = useMemo(() => {
|
||||||
|
const prefs = store.account.get('preferences') || {};
|
||||||
|
return !!prefs['reading:expand:spoilers'];
|
||||||
|
}, []);
|
||||||
|
// const readingExpandSpoilers = true;
|
||||||
|
const showMedia = readingExpandSpoilers || (!spoilerText && !sensitive);
|
||||||
const postText = content ? statusPeek(post) : '';
|
const postText = content ? statusPeek(post) : '';
|
||||||
|
|
||||||
|
const showPostContent = !spoilerText || readingExpandSpoilers;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="post-peek" title={!spoilerText ? postText : ''}>
|
<div class="post-peek" title={!spoilerText ? postText : ''}>
|
||||||
<span class="post-peek-content">
|
<span class="post-peek-content">
|
||||||
|
{isThread && !showPostContent && (
|
||||||
|
<>
|
||||||
|
<span class="post-peek-tag post-peek-thread">Thread</span>{' '}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
{!!filterInfo ? (
|
{!!filterInfo ? (
|
||||||
<>
|
<span class="post-peek-filtered">
|
||||||
{isThread && (
|
Filtered{filterInfo?.titlesStr ? `: ${filterInfo.titlesStr}` : ''}
|
||||||
<>
|
</span>
|
||||||
<span class="post-peek-tag post-peek-thread">Thread</span>{' '}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<span class="post-peek-filtered">
|
|
||||||
Filtered{filterInfo?.titlesStr ? `: ${filterInfo.titlesStr}` : ''}
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
) : !!spoilerText ? (
|
|
||||||
<>
|
|
||||||
{isThread && (
|
|
||||||
<>
|
|
||||||
<span class="post-peek-tag post-peek-thread">Thread</span>{' '}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<span class="post-peek-spoiler">
|
|
||||||
<Icon icon="eye-close" /> {spoilerText}
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
) : (
|
) : (
|
||||||
<div class="post-peek-html">
|
<>
|
||||||
{isThread && (
|
{!!spoilerText && (
|
||||||
<>
|
<span class="post-peek-spoiler">
|
||||||
<span class="post-peek-tag post-peek-thread">Thread</span>{' '}
|
<Icon
|
||||||
</>
|
icon={`${readingExpandSpoilers ? 'eye-open' : 'eye-close'}`}
|
||||||
|
/>{' '}
|
||||||
|
{spoilerText}
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
{!!content && (
|
{showPostContent && (
|
||||||
<div
|
<div class="post-peek-html">
|
||||||
dangerouslySetInnerHTML={{
|
{isThread && (
|
||||||
__html: emojifyText(content, emojis),
|
<>
|
||||||
}}
|
<span class="post-peek-tag post-peek-thread">Thread</span>{' '}
|
||||||
/>
|
</>
|
||||||
|
)}
|
||||||
|
{!!content && (
|
||||||
|
<div
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: emojifyText(content, emojis),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{!!poll?.options?.length &&
|
||||||
|
poll.options.map((o) => (
|
||||||
|
<div>
|
||||||
|
{poll.multiple ? '▪️' : '•'} {o.title}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{!content &&
|
||||||
|
mediaAttachments?.length === 1 &&
|
||||||
|
mediaAttachments[0].description && (
|
||||||
|
<>
|
||||||
|
<span class="post-peek-tag post-peek-alt">ALT</span>{' '}
|
||||||
|
<div>{mediaAttachments[0].description}</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
{!!poll?.options?.length &&
|
</>
|
||||||
poll.options.map((o) => (
|
|
||||||
<div>
|
|
||||||
{poll.multiple ? '▪️' : '•'} {o.title}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
{!content &&
|
|
||||||
mediaAttachments?.length === 1 &&
|
|
||||||
mediaAttachments[0].description && (
|
|
||||||
<>
|
|
||||||
<span class="post-peek-tag post-peek-alt">ALT</span>{' '}
|
|
||||||
<div>{mediaAttachments[0].description}</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
{!filterInfo && (
|
{!filterInfo && (
|
||||||
|
|
|
@ -286,7 +286,13 @@ function FiltersAddEdit({ filter, onClose }) {
|
||||||
// Preserve existing expiry if not specified
|
// Preserve existing expiry if not specified
|
||||||
// Seconds from now to expiresAtDate
|
// Seconds from now to expiresAtDate
|
||||||
// Other clients don't do this
|
// Other clients don't do this
|
||||||
expiresIn = Math.floor((expiresAtDate - new Date()) / 1000);
|
if (hasExpiry) {
|
||||||
|
expiresIn = Math.floor(
|
||||||
|
(expiresAtDate - new Date()) / 1000,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
expiresIn = null;
|
||||||
|
}
|
||||||
} else if (expiresIn === '0' || expiresIn === 0) {
|
} else if (expiresIn === '0' || expiresIn === 0) {
|
||||||
// 0 = Never
|
// 0 = Never
|
||||||
expiresIn = null;
|
expiresIn = null;
|
||||||
|
|
|
@ -71,7 +71,8 @@ function Following({ title, path, id, ...props }) {
|
||||||
.next();
|
.next();
|
||||||
let { value } = results;
|
let { value } = results;
|
||||||
console.log('checkForUpdates', latestItem.current, value);
|
console.log('checkForUpdates', latestItem.current, value);
|
||||||
if (value?.length) {
|
const valueContainsLatestItem = value[0]?.id === latestItem.current; // since_id might not be supported
|
||||||
|
if (value?.length && !valueContainsLatestItem) {
|
||||||
latestItem.current = value[0].id;
|
latestItem.current = value[0].id;
|
||||||
value = dedupeBoosts(value, instance);
|
value = dedupeBoosts(value, instance);
|
||||||
value = filteredItems(value, 'home');
|
value = filteredItems(value, 'home');
|
||||||
|
|
|
@ -5,7 +5,7 @@ import {
|
||||||
MenuHeader,
|
MenuHeader,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
} from '@szhsin/react-menu';
|
} from '@szhsin/react-menu';
|
||||||
import { useEffect, useRef, useState } from 'preact/hooks';
|
import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
|
||||||
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
import Icon from '../components/icon';
|
import Icon from '../components/icon';
|
||||||
|
@ -18,6 +18,7 @@ import { filteredItems } from '../utils/filters';
|
||||||
import showToast from '../utils/show-toast';
|
import showToast from '../utils/show-toast';
|
||||||
import states from '../utils/states';
|
import states from '../utils/states';
|
||||||
import { saveStatus } from '../utils/states';
|
import { saveStatus } from '../utils/states';
|
||||||
|
import { isMediaFirstInstance } from '../utils/store-utils';
|
||||||
import useTitle from '../utils/useTitle';
|
import useTitle from '../utils/useTitle';
|
||||||
|
|
||||||
const LIMIT = 20;
|
const LIMIT = 20;
|
||||||
|
@ -55,6 +56,8 @@ function Hashtags({ media: mediaView, columnMode, ...props }) {
|
||||||
useTitle(title, `/:instance?/t/:hashtag`);
|
useTitle(title, `/:instance?/t/:hashtag`);
|
||||||
const latestItem = useRef();
|
const latestItem = useRef();
|
||||||
|
|
||||||
|
const mediaFirst = useMemo(() => isMediaFirstInstance(), []);
|
||||||
|
|
||||||
// const hashtagsIterator = useRef();
|
// const hashtagsIterator = useRef();
|
||||||
const maxID = useRef(undefined);
|
const maxID = useRef(undefined);
|
||||||
async function fetchHashtags(firstLoad) {
|
async function fetchHashtags(firstLoad) {
|
||||||
|
@ -73,7 +76,7 @@ function Hashtags({ media: mediaView, columnMode, ...props }) {
|
||||||
limit: LIMIT,
|
limit: LIMIT,
|
||||||
any: hashtags.slice(1),
|
any: hashtags.slice(1),
|
||||||
maxId: firstLoad ? undefined : maxID.current,
|
maxId: firstLoad ? undefined : maxID.current,
|
||||||
onlyMedia: media,
|
onlyMedia: media ? true : undefined,
|
||||||
})
|
})
|
||||||
.next();
|
.next();
|
||||||
let { value } = results;
|
let { value } = results;
|
||||||
|
@ -85,7 +88,7 @@ function Hashtags({ media: mediaView, columnMode, ...props }) {
|
||||||
// value = filteredItems(value, 'public');
|
// value = filteredItems(value, 'public');
|
||||||
value.forEach((item) => {
|
value.forEach((item) => {
|
||||||
saveStatus(item, instance, {
|
saveStatus(item, instance, {
|
||||||
skipThreading: media, // If media view, no need to form threads
|
skipThreading: media || mediaFirst, // If media view, no need to form threads
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -109,8 +112,9 @@ function Hashtags({ media: mediaView, columnMode, ...props }) {
|
||||||
})
|
})
|
||||||
.next();
|
.next();
|
||||||
let { value } = results;
|
let { value } = results;
|
||||||
value = filteredItems(value, 'public');
|
const valueContainsLatestItem = value[0]?.id === latestItem.current; // since_id might not be supported
|
||||||
if (value?.length) {
|
if (value?.length && !valueContainsLatestItem) {
|
||||||
|
value = filteredItems(value, 'public');
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
@ -155,7 +159,7 @@ function Hashtags({ media: mediaView, columnMode, ...props }) {
|
||||||
fetchItems={fetchHashtags}
|
fetchItems={fetchHashtags}
|
||||||
checkForUpdates={checkForUpdates}
|
checkForUpdates={checkForUpdates}
|
||||||
useItemID
|
useItemID
|
||||||
view={media ? 'media' : undefined}
|
view={media || mediaFirst ? 'media' : undefined}
|
||||||
refresh={media}
|
refresh={media}
|
||||||
// allowFilters
|
// allowFilters
|
||||||
filterContext="public"
|
filterContext="public"
|
||||||
|
@ -232,23 +236,27 @@ function Hashtags({ media: mediaView, columnMode, ...props }) {
|
||||||
<MenuDivider />
|
<MenuDivider />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<MenuHeader className="plain">Filters</MenuHeader>
|
{!mediaFirst && (
|
||||||
<MenuItem
|
<>
|
||||||
type="checkbox"
|
<MenuHeader className="plain">Filters</MenuHeader>
|
||||||
checked={!!media}
|
<MenuItem
|
||||||
onClick={() => {
|
type="checkbox"
|
||||||
if (media) {
|
checked={!!media}
|
||||||
searchParams.delete('media');
|
onClick={() => {
|
||||||
} else {
|
if (media) {
|
||||||
searchParams.set('media', '1');
|
searchParams.delete('media');
|
||||||
}
|
} else {
|
||||||
setSearchParams(searchParams);
|
searchParams.set('media', '1');
|
||||||
}}
|
}
|
||||||
>
|
setSearchParams(searchParams);
|
||||||
<Icon icon="check-circle" />{' '}
|
}}
|
||||||
<span class="menu-grow">Media only</span>
|
>
|
||||||
</MenuItem>
|
<Icon icon="check-circle" />{' '}
|
||||||
<MenuDivider />
|
<span class="menu-grow">Media only</span>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuDivider />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<FocusableItem className="menu-field" disabled={reachLimit}>
|
<FocusableItem className="menu-field" disabled={reachLimit}>
|
||||||
{({ ref }) => (
|
{({ ref }) => (
|
||||||
<form
|
<form
|
||||||
|
|
|
@ -84,7 +84,7 @@ function NotificationsLink() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const NOTIFICATIONS_LIMIT = 30;
|
const NOTIFICATIONS_LIMIT = 80;
|
||||||
const NOTIFICATIONS_DISPLAY_LIMIT = 5;
|
const NOTIFICATIONS_DISPLAY_LIMIT = 5;
|
||||||
function NotificationsMenu({ anchorRef, state, onClose }) {
|
function NotificationsMenu({ anchorRef, state, onClose }) {
|
||||||
const { masto, instance } = api();
|
const { masto, instance } = api();
|
||||||
|
|
|
@ -63,8 +63,9 @@ function List(props) {
|
||||||
since_id: latestItem.current,
|
since_id: latestItem.current,
|
||||||
});
|
});
|
||||||
let { value } = results;
|
let { value } = results;
|
||||||
value = filteredItems(value, 'home');
|
const valueContainsLatestItem = value[0]?.id === latestItem.current; // since_id might not be supported
|
||||||
if (value?.length) {
|
if (value?.length && !valueContainsLatestItem) {
|
||||||
|
value = filteredItems(value, 'home');
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import './login.css';
|
import './login.css';
|
||||||
|
|
||||||
|
import Fuse from 'fuse.js';
|
||||||
import { useEffect, useRef, useState } from 'preact/hooks';
|
import { useEffect, useRef, useState } from 'preact/hooks';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
@ -27,12 +28,14 @@ function Login() {
|
||||||
);
|
);
|
||||||
|
|
||||||
const [instancesList, setInstancesList] = useState([]);
|
const [instancesList, setInstancesList] = useState([]);
|
||||||
|
const searcher = useRef();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(instancesListURL);
|
const res = await fetch(instancesListURL);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
setInstancesList(data);
|
setInstancesList(data);
|
||||||
|
searcher.current = new Fuse(data);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Silently fail
|
// Silently fail
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
@ -90,21 +93,11 @@ function Login() {
|
||||||
!/[\s\/\\@]/.test(cleanInstanceText);
|
!/[\s\/\\@]/.test(cleanInstanceText);
|
||||||
|
|
||||||
const instancesSuggestions = cleanInstanceText
|
const instancesSuggestions = cleanInstanceText
|
||||||
? instancesList
|
? searcher.current
|
||||||
.filter((instance) => instance.includes(instanceText))
|
?.search(cleanInstanceText, {
|
||||||
.sort((a, b) => {
|
limit: 10,
|
||||||
// Move text that starts with instanceText to the start
|
|
||||||
const aStartsWith = a
|
|
||||||
.toLowerCase()
|
|
||||||
.startsWith(instanceText.toLowerCase());
|
|
||||||
const bStartsWith = b
|
|
||||||
.toLowerCase()
|
|
||||||
.startsWith(instanceText.toLowerCase());
|
|
||||||
if (aStartsWith && !bStartsWith) return -1;
|
|
||||||
if (!aStartsWith && bStartsWith) return 1;
|
|
||||||
return 0;
|
|
||||||
})
|
})
|
||||||
.slice(0, 10)
|
?.map((match) => match.item)
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
const selectedInstanceText = instanceTextLooksLikeDomain
|
const selectedInstanceText = instanceTextLooksLikeDomain
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { useSearchParams } from 'react-router-dom';
|
||||||
import Link from '../components/link';
|
import Link from '../components/link';
|
||||||
import Timeline from '../components/timeline';
|
import Timeline from '../components/timeline';
|
||||||
import { api } from '../utils/api';
|
import { api } from '../utils/api';
|
||||||
|
import { fixNotifications } from '../utils/group-notifications';
|
||||||
import { saveStatus } from '../utils/states';
|
import { saveStatus } from '../utils/states';
|
||||||
import useTitle from '../utils/useTitle';
|
import useTitle from '../utils/useTitle';
|
||||||
|
|
||||||
|
@ -30,6 +31,8 @@ function Mentions({ columnMode, ...props }) {
|
||||||
const results = await mentionsIterator.current.next();
|
const results = await mentionsIterator.current.next();
|
||||||
let { value } = results;
|
let { value } = results;
|
||||||
if (value?.length) {
|
if (value?.length) {
|
||||||
|
value = fixNotifications(value);
|
||||||
|
|
||||||
if (firstLoad) {
|
if (firstLoad) {
|
||||||
latestItem.current = value[0].id;
|
latestItem.current = value[0].id;
|
||||||
console.log('First load', latestItem.current);
|
console.log('First load', latestItem.current);
|
||||||
|
@ -95,7 +98,9 @@ function Mentions({ columnMode, ...props }) {
|
||||||
latestConversationItem.current,
|
latestConversationItem.current,
|
||||||
value,
|
value,
|
||||||
);
|
);
|
||||||
if (value?.length) {
|
const valueContainsLatestItem =
|
||||||
|
value[0]?.id === latestConversationItem.current; // since_id might not be supported
|
||||||
|
if (value?.length && !valueContainsLatestItem) {
|
||||||
latestConversationItem.current = value[0].lastStatus.id;
|
latestConversationItem.current = value[0].lastStatus.id;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,7 +33,7 @@ import usePageVisibility from '../utils/usePageVisibility';
|
||||||
import useScroll from '../utils/useScroll';
|
import useScroll from '../utils/useScroll';
|
||||||
import useTitle from '../utils/useTitle';
|
import useTitle from '../utils/useTitle';
|
||||||
|
|
||||||
const LIMIT = 30; // 30 is the maximum limit :(
|
const LIMIT = 80;
|
||||||
const emptySearchParams = new URLSearchParams();
|
const emptySearchParams = new URLSearchParams();
|
||||||
|
|
||||||
const scrollIntoViewOptions = {
|
const scrollIntoViewOptions = {
|
||||||
|
@ -72,6 +72,13 @@ function Notifications({ columnMode }) {
|
||||||
excludeTypes: ['follow_request'],
|
excludeTypes: ['follow_request'],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (/max_id=($|&)/i.test(notificationsIterator.current?.nextParams)) {
|
||||||
|
// Pixelfed returns next paginationed link with empty max_id
|
||||||
|
// I assume, it's done (end of list)
|
||||||
|
return {
|
||||||
|
done: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
const allNotifications = await notificationsIterator.current.next();
|
const allNotifications = await notificationsIterator.current.next();
|
||||||
const notifications = allNotifications.value;
|
const notifications = allNotifications.value;
|
||||||
|
|
||||||
|
@ -82,6 +89,32 @@ function Notifications({ columnMode }) {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// TEST: Slot in a fake notification to test 'severed_relationships'
|
||||||
|
// notifications.unshift({
|
||||||
|
// id: '123123',
|
||||||
|
// type: 'severed_relationships',
|
||||||
|
// createdAt: '2024-03-22T19:20:08.316Z',
|
||||||
|
// event: {
|
||||||
|
// type: 'account_suspension',
|
||||||
|
// targetName: 'mastodon.dev',
|
||||||
|
// followersCount: 0,
|
||||||
|
// followingCount: 0,
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
|
||||||
|
// TEST: Slot in a fake notification to test 'moderation_warning'
|
||||||
|
// notifications.unshift({
|
||||||
|
// id: '123123',
|
||||||
|
// type: 'moderation_warning',
|
||||||
|
// createdAt: new Date().toISOString(),
|
||||||
|
// moderation_warning: {
|
||||||
|
// id: '1231234',
|
||||||
|
// action: 'mark_statuses_as_sensitive',
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
|
||||||
|
// console.log({ notifications });
|
||||||
|
|
||||||
const groupedNotifications = groupNotifications(notifications);
|
const groupedNotifications = groupNotifications(notifications);
|
||||||
|
|
||||||
if (firstLoad) {
|
if (firstLoad) {
|
||||||
|
@ -247,7 +280,6 @@ function Notifications({ columnMode }) {
|
||||||
|
|
||||||
const lastHiddenTime = useRef();
|
const lastHiddenTime = useRef();
|
||||||
usePageVisibility((visible) => {
|
usePageVisibility((visible) => {
|
||||||
let unsub;
|
|
||||||
if (visible) {
|
if (visible) {
|
||||||
const timeDiff = Date.now() - lastHiddenTime.current;
|
const timeDiff = Date.now() - lastHiddenTime.current;
|
||||||
if (!lastHiddenTime.current || timeDiff > 1000 * 3) {
|
if (!lastHiddenTime.current || timeDiff > 1000 * 3) {
|
||||||
|
@ -258,20 +290,16 @@ function Notifications({ columnMode }) {
|
||||||
} else {
|
} else {
|
||||||
lastHiddenTime.current = Date.now();
|
lastHiddenTime.current = Date.now();
|
||||||
}
|
}
|
||||||
unsub = subscribeKey(states, 'notificationsShowNew', (v) => {
|
|
||||||
if (uiState === 'loading') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (v) {
|
|
||||||
loadUpdates();
|
|
||||||
}
|
|
||||||
setShowNew(v);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
return () => {
|
|
||||||
unsub?.();
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
useEffect(() => {
|
||||||
|
let unsub = subscribeKey(states, 'notificationsShowNew', (v) => {
|
||||||
|
if (uiState === 'loading') return;
|
||||||
|
if (v) loadUpdates();
|
||||||
|
setShowNew(v);
|
||||||
|
});
|
||||||
|
return () => unsub?.();
|
||||||
|
}, []);
|
||||||
|
|
||||||
const todayDate = new Date();
|
const todayDate = new Date();
|
||||||
const yesterdayDate = new Date(todayDate - 24 * 60 * 60 * 1000);
|
const yesterdayDate = new Date(todayDate - 24 * 60 * 60 * 1000);
|
||||||
|
@ -418,7 +446,7 @@ function Notifications({ columnMode }) {
|
||||||
{supportsFilteredNotifications && (
|
{supportsFilteredNotifications && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="button plain"
|
class="button plain4"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowNotificationsSettings(true);
|
setShowNotificationsSettings(true);
|
||||||
}}
|
}}
|
||||||
|
@ -613,7 +641,7 @@ function Notifications({ columnMode }) {
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<h2 class="timeline-header">Today</h2>
|
<h2 class="timeline-header">Today</h2>
|
||||||
{showTodayEmpty && !!snapStates.notifications.length && (
|
{showTodayEmpty && (
|
||||||
<p class="ui-state insignificant">
|
<p class="ui-state insignificant">
|
||||||
{uiState === 'default' ? "You're all caught up." : <>…</>}
|
{uiState === 'default' ? "You're all caught up." : <>…</>}
|
||||||
</p>
|
</p>
|
||||||
|
|
|
@ -33,6 +33,7 @@ function Public({ local, columnMode, ...props }) {
|
||||||
publicIterator.current = masto.v1.timelines.public.list({
|
publicIterator.current = masto.v1.timelines.public.list({
|
||||||
limit: LIMIT,
|
limit: LIMIT,
|
||||||
local: isLocal,
|
local: isLocal,
|
||||||
|
remote: !isLocal, // Pixelfed
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const results = await publicIterator.current.next();
|
const results = await publicIterator.current.next();
|
||||||
|
@ -63,8 +64,9 @@ function Public({ local, columnMode, ...props }) {
|
||||||
})
|
})
|
||||||
.next();
|
.next();
|
||||||
let { value } = results;
|
let { value } = results;
|
||||||
value = filteredItems(value, 'public');
|
const valueContainsLatestItem = value[0]?.id === latestItem.current; // since_id might not be supported
|
||||||
if (value?.length) {
|
if (value?.length && !valueContainsLatestItem) {
|
||||||
|
value = filteredItems(value, 'public');
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -23,12 +23,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-heading {
|
.hero-heading {
|
||||||
font-size: var(--text-size);
|
font-size: var(--text-size);
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|
|
@ -153,6 +153,18 @@ function StatusPage(params) {
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, [showMediaOnly]);
|
}, [showMediaOnly]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const $deckContainers = document.querySelectorAll('.deck-container');
|
||||||
|
$deckContainers.forEach(($deckContainer) => {
|
||||||
|
$deckContainer.setAttribute('inert', '');
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
$deckContainers.forEach(($deckContainer) => {
|
||||||
|
$deckContainer.removeAttribute('inert');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="deck-backdrop">
|
<div class="deck-backdrop">
|
||||||
{showMedia ? (
|
{showMedia ? (
|
||||||
|
@ -972,6 +984,18 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
|
||||||
[statuses, limit, renderStatus],
|
[statuses, limit, renderStatus],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// If there's spoiler in hero status, auto-expand it
|
||||||
|
useEffect(() => {
|
||||||
|
let timer = setTimeout(() => {
|
||||||
|
if (!heroStatusRef.current) return;
|
||||||
|
const spoilerButton = heroStatusRef.current.querySelector(
|
||||||
|
'.spoiler-button:not(.spoiling), .spoiler-media-button:not(.spoiling)',
|
||||||
|
);
|
||||||
|
if (spoilerButton) spoilerButton.click();
|
||||||
|
}, 1000);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
tabIndex="-1"
|
tabIndex="-1"
|
||||||
|
|
|
@ -19,6 +19,7 @@ import pmem from '../utils/pmem';
|
||||||
import shortenNumber from '../utils/shorten-number';
|
import shortenNumber from '../utils/shorten-number';
|
||||||
import states from '../utils/states';
|
import states from '../utils/states';
|
||||||
import { saveStatus } from '../utils/states';
|
import { saveStatus } from '../utils/states';
|
||||||
|
import supports from '../utils/supports';
|
||||||
import useTitle from '../utils/useTitle';
|
import useTitle from '../utils/useTitle';
|
||||||
|
|
||||||
const LIMIT = 20;
|
const LIMIT = 20;
|
||||||
|
@ -33,6 +34,17 @@ const fetchLinks = pmem(
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
function fetchTrends(masto) {
|
||||||
|
if (supports('@pixelfed/trending')) {
|
||||||
|
return masto.pixelfed.v2.discover.posts.trending.list({
|
||||||
|
range: 'daily',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return masto.v1.trends.statuses.list({
|
||||||
|
limit: LIMIT,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function Trending({ columnMode, ...props }) {
|
function Trending({ columnMode, ...props }) {
|
||||||
const snapStates = useSnapshot(states);
|
const snapStates = useSnapshot(states);
|
||||||
const params = columnMode ? {} : useParams();
|
const params = columnMode ? {} : useParams();
|
||||||
|
@ -48,36 +60,39 @@ function Trending({ columnMode, ...props }) {
|
||||||
const [hashtags, setHashtags] = useState([]);
|
const [hashtags, setHashtags] = useState([]);
|
||||||
const [links, setLinks] = useState([]);
|
const [links, setLinks] = useState([]);
|
||||||
const trendIterator = useRef();
|
const trendIterator = useRef();
|
||||||
|
|
||||||
async function fetchTrend(firstLoad) {
|
async function fetchTrend(firstLoad) {
|
||||||
if (firstLoad || !trendIterator.current) {
|
if (firstLoad || !trendIterator.current) {
|
||||||
trendIterator.current = masto.v1.trends.statuses.list({
|
trendIterator.current = fetchTrends(masto);
|
||||||
limit: LIMIT,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get hashtags
|
// Get hashtags
|
||||||
try {
|
if (supports('@mastodon/trending-hashtags')) {
|
||||||
const iterator = masto.v1.trends.tags.list();
|
try {
|
||||||
const { value: tags } = await iterator.next();
|
const iterator = masto.v1.trends.tags.list();
|
||||||
console.log('tags', tags);
|
const { value: tags } = await iterator.next();
|
||||||
if (tags?.length) {
|
console.log('tags', tags);
|
||||||
setHashtags(tags);
|
if (tags?.length) {
|
||||||
|
setHashtags(tags);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get links
|
// Get links
|
||||||
try {
|
if (supports('@mastodon/trending-links')) {
|
||||||
const { value } = await fetchLinks(masto, instance);
|
try {
|
||||||
// 4 types available: link, photo, video, rich
|
const { value } = await fetchLinks(masto, instance);
|
||||||
// Only want links for now
|
// 4 types available: link, photo, video, rich
|
||||||
const links = value?.filter?.((link) => link.type === 'link');
|
// Only want links for now
|
||||||
console.log('links', links);
|
const links = value?.filter?.((link) => link.type === 'link');
|
||||||
if (links?.length) {
|
console.log('links', links);
|
||||||
setLinks(links);
|
if (links?.length) {
|
||||||
|
setLinks(links);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const results = await trendIterator.current.next();
|
const results = await trendIterator.current.next();
|
||||||
|
|
|
@ -7,6 +7,7 @@ import {
|
||||||
getAccountByInstance,
|
getAccountByInstance,
|
||||||
getCurrentAccount,
|
getCurrentAccount,
|
||||||
saveAccount,
|
saveAccount,
|
||||||
|
setCurrentAccountID,
|
||||||
} from './store-utils';
|
} from './store-utils';
|
||||||
|
|
||||||
// Default *fallback* instance
|
// Default *fallback* instance
|
||||||
|
@ -118,7 +119,7 @@ export async function initAccount(client, instance, accessToken, vapidKey) {
|
||||||
const mastoAccount = await masto.v1.accounts.verifyCredentials();
|
const mastoAccount = await masto.v1.accounts.verifyCredentials();
|
||||||
|
|
||||||
console.log('CURRENTACCOUNT SET', mastoAccount.id);
|
console.log('CURRENTACCOUNT SET', mastoAccount.id);
|
||||||
store.session.set('currentAccount', mastoAccount.id);
|
setCurrentAccountID(mastoAccount.id);
|
||||||
|
|
||||||
saveAccount({
|
saveAccount({
|
||||||
info: mastoAccount,
|
info: mastoAccount,
|
||||||
|
|
|
@ -242,6 +242,17 @@ function _enhanceContent(content, opts = {}) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ADD ASPECT RATIO TO ALL IMAGES
|
||||||
|
if (enhancedContent.includes('<img')) {
|
||||||
|
dom.querySelectorAll('img').forEach((img) => {
|
||||||
|
const width = img.getAttribute('width') || img.naturalWidth;
|
||||||
|
const height = img.getAttribute('height') || img.naturalHeight;
|
||||||
|
if (width && height) {
|
||||||
|
img.style.setProperty('--original-aspect-ratio', `${width}/${height}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (postEnhanceDOM) {
|
if (postEnhanceDOM) {
|
||||||
queueMicrotask(() => postEnhanceDOM(dom));
|
queueMicrotask(() => postEnhanceDOM(dom));
|
||||||
// postEnhanceDOM(dom); // mutate dom
|
// postEnhanceDOM(dom); // mutate dom
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import mem from './mem';
|
import mem from './mem';
|
||||||
import store from './store';
|
import { getCurrentAccountID } from './store-utils';
|
||||||
|
|
||||||
function _isFiltered(filtered, filterContext) {
|
function _isFiltered(filtered, filterContext) {
|
||||||
if (!filtered?.length) return false;
|
if (!filtered?.length) return false;
|
||||||
|
@ -43,7 +43,7 @@ export function filteredItem(item, filterContext, currentAccountID) {
|
||||||
export function filteredItems(items, filterContext) {
|
export function filteredItems(items, filterContext) {
|
||||||
if (!items?.length) return [];
|
if (!items?.length) return [];
|
||||||
if (!filterContext) return items;
|
if (!filterContext) return items;
|
||||||
const currentAccountID = store.session.get('currentAccount');
|
const currentAccountID = getCurrentAccountID();
|
||||||
return items.filter((item) =>
|
return items.filter((item) =>
|
||||||
filteredItem(item, filterContext, currentAccountID),
|
filteredItem(item, filterContext, currentAccountID),
|
||||||
);
|
);
|
||||||
|
|
|
@ -6,6 +6,7 @@ const statusPostRegexes = [
|
||||||
/\/notes\/([^\/]+)/i, // Misskey, Firefish
|
/\/notes\/([^\/]+)/i, // Misskey, Firefish
|
||||||
/^\/(?:notice|objects)\/([a-z0-9-]+)/i, // Pleroma
|
/^\/(?:notice|objects)\/([a-z0-9-]+)/i, // Pleroma
|
||||||
/\/@[^@\/]+@?[^\/]+?\/([^\/]+)/i, // Mastodon
|
/\/@[^@\/]+@?[^\/]+?\/([^\/]+)/i, // Mastodon
|
||||||
|
/^\/p\/[^\/]+\/([^\/]+)/i, // Pixelfed
|
||||||
];
|
];
|
||||||
|
|
||||||
export function getInstanceStatusObject(url) {
|
export function getInstanceStatusObject(url) {
|
||||||
|
|
|
@ -9,7 +9,7 @@ const notificationTypeKeys = {
|
||||||
poll: ['status'],
|
poll: ['status'],
|
||||||
update: ['status'],
|
update: ['status'],
|
||||||
};
|
};
|
||||||
function fixNotifications(notifications) {
|
export function fixNotifications(notifications) {
|
||||||
return notifications.filter((notification) => {
|
return notifications.filter((notification) => {
|
||||||
const { type, id, createdAt } = notification;
|
const { type, id, createdAt } = notification;
|
||||||
if (!type) {
|
if (!type) {
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { api } from './api';
|
import { api } from './api';
|
||||||
import store from './store';
|
import { getCurrentAccountID } from './store-utils';
|
||||||
|
|
||||||
export async function fetchRelationships(accounts, relationshipsMap = {}) {
|
export async function fetchRelationships(accounts, relationshipsMap = {}) {
|
||||||
if (!accounts?.length) return;
|
if (!accounts?.length) return;
|
||||||
const { masto } = api();
|
const { masto } = api();
|
||||||
|
|
||||||
const currentAccount = store.session.get('currentAccount');
|
const currentAccount = getCurrentAccountID();
|
||||||
const uniqueAccountIds = accounts.reduce((acc, a) => {
|
const uniqueAccountIds = accounts.reduce((acc, a) => {
|
||||||
// 1. Ignore duplicate accounts
|
// 1. Ignore duplicate accounts
|
||||||
// 2. Ignore accounts that are already inside relationshipsMap
|
// 2. Ignore accounts that are already inside relationshipsMap
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
const { locale } = Intl.NumberFormat().resolvedOptions();
|
const { locale } = Intl.NumberFormat().resolvedOptions();
|
||||||
const shortenNumber = Intl.NumberFormat(locale, {
|
const shortenNumber = Intl.NumberFormat(locale, {
|
||||||
notation: 'compact',
|
notation: 'compact',
|
||||||
|
roundingMode: 'floor',
|
||||||
}).format;
|
}).format;
|
||||||
export default shortenNumber;
|
export default shortenNumber;
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
import openOSK from './open-osk';
|
||||||
|
import showToast from './show-toast';
|
||||||
|
import states from './states';
|
||||||
|
|
||||||
|
const TOAST_DURATION = 5_000; // 5 seconds
|
||||||
|
|
||||||
|
export default function showCompose(opts) {
|
||||||
|
if (!opts) opts = true;
|
||||||
|
|
||||||
|
if (states.showCompose) {
|
||||||
|
if (states.composerState.minimized) {
|
||||||
|
showToast({
|
||||||
|
duration: TOAST_DURATION,
|
||||||
|
text: `A draft post is currently minimized. Post or discard it before creating a new one.`,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
showToast({
|
||||||
|
duration: TOAST_DURATION,
|
||||||
|
text: `A post is currently open. Post or discard it before creating a new one.`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
openOSK();
|
||||||
|
states.showCompose = opts;
|
||||||
|
}
|
|
@ -40,6 +40,7 @@ const states = proxy({
|
||||||
statusReply: {},
|
statusReply: {},
|
||||||
accounts: {},
|
accounts: {},
|
||||||
routeNotification: null,
|
routeNotification: null,
|
||||||
|
composerState: {},
|
||||||
// Modals
|
// Modals
|
||||||
showCompose: false,
|
showCompose: false,
|
||||||
showSettings: false,
|
showSettings: false,
|
||||||
|
|
|
@ -16,13 +16,40 @@ export function getAccountByInstance(instance) {
|
||||||
return accounts.find((a) => a.instanceURL === instance);
|
return accounts.find((a) => a.instanceURL === instance);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const standaloneMQ = window.matchMedia('(display-mode: standalone)');
|
||||||
|
|
||||||
|
export function getCurrentAccountID() {
|
||||||
|
try {
|
||||||
|
const id = store.session.get('currentAccount');
|
||||||
|
if (id) return id;
|
||||||
|
} catch (e) {}
|
||||||
|
if (standaloneMQ.matches) {
|
||||||
|
try {
|
||||||
|
const id = store.local.get('currentAccount');
|
||||||
|
if (id) return id;
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setCurrentAccountID(id) {
|
||||||
|
try {
|
||||||
|
store.session.set('currentAccount', id);
|
||||||
|
} catch (e) {}
|
||||||
|
if (standaloneMQ.matches) {
|
||||||
|
try {
|
||||||
|
store.local.set('currentAccount', id);
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function getCurrentAccount() {
|
export function getCurrentAccount() {
|
||||||
if (!window.__IGNORE_GET_ACCOUNT_ERROR__) {
|
if (!window.__IGNORE_GET_ACCOUNT_ERROR__) {
|
||||||
// Track down getCurrentAccount() calls before account-based states are initialized
|
// Track down getCurrentAccount() calls before account-based states are initialized
|
||||||
console.error('getCurrentAccount() called before states are initialized');
|
console.error('getCurrentAccount() called before states are initialized');
|
||||||
if (import.meta.env.DEV) console.trace();
|
if (import.meta.env.DEV) console.trace();
|
||||||
}
|
}
|
||||||
const currentAccount = store.session.get('currentAccount');
|
const currentAccount = getCurrentAccountID();
|
||||||
const account = getAccount(currentAccount);
|
const account = getAccount(currentAccount);
|
||||||
return account;
|
return account;
|
||||||
}
|
}
|
||||||
|
@ -48,7 +75,7 @@ export function saveAccount(account) {
|
||||||
accounts.push(account);
|
accounts.push(account);
|
||||||
}
|
}
|
||||||
store.local.setJSON('accounts', accounts);
|
store.local.setJSON('accounts', accounts);
|
||||||
store.session.set('currentAccount', account.info.id);
|
setCurrentAccountID(account.info.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateAccount(accountInfo) {
|
export function updateAccount(accountInfo) {
|
||||||
|
@ -80,10 +107,10 @@ export function getCurrentInstance() {
|
||||||
return (currentInstance = instances[instance]);
|
return (currentInstance = instances[instance]);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
alert(`Failed to load instance configuration. Please try again.\n\n${e}`);
|
// alert(`Failed to load instance configuration. Please try again.\n\n${e}`);
|
||||||
// Temporary fix for corrupted data
|
// Temporary fix for corrupted data
|
||||||
store.local.del('instances');
|
// store.local.del('instances');
|
||||||
location.reload();
|
// location.reload();
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -126,3 +153,8 @@ export function getCurrentInstanceConfiguration() {
|
||||||
const instance = getCurrentInstance();
|
const instance = getCurrentInstance();
|
||||||
return getInstanceConfiguration(instance);
|
return getInstanceConfiguration(instance);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isMediaFirstInstance() {
|
||||||
|
const instance = getCurrentInstance();
|
||||||
|
return /pixelfed/i.test(instance?.version);
|
||||||
|
}
|
||||||
|
|
|
@ -4,6 +4,21 @@ import features from '../data/features.json';
|
||||||
|
|
||||||
import { getCurrentInstance } from './store-utils';
|
import { getCurrentInstance } from './store-utils';
|
||||||
|
|
||||||
|
// Non-semver(?) UA string detection
|
||||||
|
const containPixelfed = /pixelfed/i;
|
||||||
|
const notContainPixelfed = /^(?!.*pixelfed).*$/i;
|
||||||
|
const platformFeatures = {
|
||||||
|
'@mastodon/lists': notContainPixelfed,
|
||||||
|
'@mastodon/filters': notContainPixelfed,
|
||||||
|
'@mastodon/mentions': notContainPixelfed,
|
||||||
|
'@mastodon/trending-hashtags': notContainPixelfed,
|
||||||
|
'@mastodon/trending-links': notContainPixelfed,
|
||||||
|
'@mastodon/post-bookmark': notContainPixelfed,
|
||||||
|
'@mastodon/post-edit': notContainPixelfed,
|
||||||
|
'@mastodon/profile-edit': notContainPixelfed,
|
||||||
|
'@mastodon/profile-private-note': notContainPixelfed,
|
||||||
|
'@pixelfed/trending': containPixelfed,
|
||||||
|
};
|
||||||
const supportsCache = {};
|
const supportsCache = {};
|
||||||
|
|
||||||
function supports(feature) {
|
function supports(feature) {
|
||||||
|
@ -11,6 +26,11 @@ function supports(feature) {
|
||||||
const { version, domain } = getCurrentInstance();
|
const { version, domain } = getCurrentInstance();
|
||||||
const key = `${domain}-${feature}`;
|
const key = `${domain}-${feature}`;
|
||||||
if (supportsCache[key]) return supportsCache[key];
|
if (supportsCache[key]) return supportsCache[key];
|
||||||
|
|
||||||
|
if (platformFeatures[feature]) {
|
||||||
|
return (supportsCache[key] = platformFeatures[feature].test(version));
|
||||||
|
}
|
||||||
|
|
||||||
const range = features[feature];
|
const range = features[feature];
|
||||||
if (!range) return false;
|
if (!range) return false;
|
||||||
return (supportsCache[key] = satisfies(version, range, {
|
return (supportsCache[key] = satisfies(version, range, {
|
||||||
|
|
|
@ -9,6 +9,20 @@ export const throttle = pThrottle({
|
||||||
interval: 1000,
|
interval: 1000,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const STATUS_ID_REGEXES = [
|
||||||
|
/\/@[^@\/]+@?[^\/]+?\/(\d+)$/i, // Mastodon
|
||||||
|
/\/notice\/(\w+)$/i, // Pleroma
|
||||||
|
];
|
||||||
|
function getStatusID(path) {
|
||||||
|
for (let i = 0; i < STATUS_ID_REGEXES.length; i++) {
|
||||||
|
const statusMatchID = path.match(STATUS_ID_REGEXES[i])?.[1];
|
||||||
|
if (statusMatchID) {
|
||||||
|
return statusMatchID;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const denylistDomains = /(twitter|github)\.com/i;
|
const denylistDomains = /(twitter|github)\.com/i;
|
||||||
const failedUnfurls = {};
|
const failedUnfurls = {};
|
||||||
function _unfurlMastodonLink(instance, url) {
|
function _unfurlMastodonLink(instance, url) {
|
||||||
|
@ -53,11 +67,11 @@ function _unfurlMastodonLink(instance, url) {
|
||||||
}
|
}
|
||||||
const domain = urlObj.hostname;
|
const domain = urlObj.hostname;
|
||||||
const path = urlObj.pathname;
|
const path = urlObj.pathname;
|
||||||
// Regex /:username/:id, where username = @username or @username@domain, id = number
|
// Regex /:username/:id, where username = @username or @username@domain, id = post ID
|
||||||
const statusRegex = /\/@([^@\/]+)@?([^\/]+)?\/(\d+)$/i;
|
let statusMatchID = getStatusID(path);
|
||||||
const statusMatch = statusRegex.exec(path);
|
|
||||||
if (statusMatch) {
|
if (statusMatchID) {
|
||||||
const id = statusMatch[3];
|
const id = statusMatchID;
|
||||||
const { masto } = api({ instance: domain });
|
const { masto } = api({ instance: domain });
|
||||||
remoteInstanceFetch = masto.v1.statuses
|
remoteInstanceFetch = masto.v1.statuses
|
||||||
.$select(id)
|
.$select(id)
|
||||||
|
|
Ładowanie…
Reference in New Issue