Merge commit '3a84990780fb1b48999f661f0cea5cb29cca4d33' into glitch-soc/merge-upstream

This commit is contained in:
Claire 2026-04-15 16:27:32 +02:00
commit 3072de056a
27 changed files with 368 additions and 147 deletions

View File

@ -2,6 +2,23 @@
All notable changes to this project will be documented in this file.
## [4.5.9] - 2026-04-15
### Security
- Insufficient verification of email addresses ([GHSA-5r37-qpwq-2jhh](https://github.com/mastodon/mastodon/security/advisories/GHSA-5r37-qpwq-2jhh))
- Updated dependencies
### Added
- Add trademark warning to `mastodon:setup` task (#38548 by @ClearlyClaire)
### Fixed
- Fix definition for `quote` in JSON-LD context (#38686 by @ClearlyClaire)
- Fix being unable to disable sound for quote update notification (#38537 by @ClearlyClaire)
- Fix being able to quote someone you blocked (#38608 by @ClearlyClaire)
## [4.5.8] - 2026-03-24
### Security

View File

@ -20,7 +20,7 @@ Mastodon Glitch Edition is a fork of [Mastodon](https://github.com/mastodon/mast
<p align="center">
<a style="text-decoration:none" href="https://www.youtube.com/watch?v=IPSbNdBmWKE">
<img alt="Mastodon hero image" src="https://github.com/user-attachments/assets/ef53f5e9-c0d8-484d-9f53-00efdebb92c3" />
<img alt="Mastodon hero image" src="./docs/hero-nodes.gif" />
</a>
</p>

View File

@ -0,0 +1,95 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 334 353">
<ellipse cx="162.778" cy="327.222" fill="var(--color-shadow, #f2f2f7)" rx="113.889" ry="25" />
<g clip-path="url(#a)">
<path fill="var(--color-skin-1, #d7d6e1)"
d="M181.219 292.602s.546 21.402 1.091 27.288 1.636 8.561 10.909 9.096 22.365-1.071 27.274-2.141 8.727-3.21 7.636-15.516-2.182-27.288-2.182-27.288" />
<path stroke="var(--color-outline, #b2b1c8)" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10"
stroke-width="1.826"
d="M181.219 292.602s.546 21.402 1.091 27.288 1.636 8.561 10.909 9.096 22.365-1.071 27.274-2.141 8.727-3.21 7.636-15.516-2.182-27.288-2.182-27.288" />
<path fill="var(--color-skin-2, #f6f6f9)" stroke="var(--color-outline, #b2b1c8)" stroke-linecap="round" stroke-linejoin="round"
stroke-miterlimit="10" stroke-width="1.826"
d="m227.584 320.851-.054-.321c-.818-3.05-2.455-3.746-4.364-3.371-2.618.535-3.327 4.12-3.764 8.935 0 .214-.054.428-.054.696l.382.16c.218-.053.49-.107.709-.16 3.218-.642 5.945-1.819 7.145-5.939" />
<path fill="var(--color-skin-3, #eeedf3)"
d="M231.184 121.917c25.092-2.676 43.092-14.982 45.82-17.122 0 0-7.637-6.42-1.637-10.701 6-4.28 9.819 1.07 9.819 1.07s12.818-15.999 23.728-7.438c10.909 8.561 15 17.604 11.182 26.165-3.819 8.561-28.365 38.524-80.184 45.48" />
<path stroke="var(--color-outline, #b2b1c8)" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10"
stroke-width="1.826"
d="M231.184 121.917c25.092-2.676 43.092-14.982 45.82-17.122 0 0-7.637-6.42-1.637-10.701 6-4.28 9.819 1.07 9.819 1.07s12.818-15.999 23.728-7.438c10.909 8.561 15 17.604 11.182 26.165-3.819 8.561-28.365 38.524-80.184 45.48" />
<path fill="var(--color-skin-1, #d7d6e1)"
d="M83.363 281.364c-8.728 8.026-22.364 12.307-29.455 2.676-7.092-9.632-2.182-13.912-3.819-16.052-1.636-2.14-11.454 4.28-7.09 16.587 4.363 12.306 29.455 25.682 52.91 7.49" />
<path fill="var(--color-skin-3, #eeedf3)"
d="M225.403 101.638s18.873 37.935 23.51 93.849c3.927 47.62 8.182 74.373-25.092 89.89s-77.456 12.841-98.184 13.376-42.546-2.14-48.001-25.148.273-40.932.273-40.932.763-38.096-1.146-44.249c-1.91-6.153-5.018-15.356-5.018-15.356s-3.11 21.456-19.364 28.679c-16.255 7.277-30.601.428-29.401-19.53 0 0-13.91-22.793-20.51-42.751s10.8-39.54 42.71-31.461c1.091-37.99 34.692-69.932 81.33-73.678 46.583-3.799 57.983 20.707 82.856 21.777 24.001 1.07 40.91-6.42 53.456-18.727 12.546-12.306 38.183-28.358 58.365-15.517 0 0 9.818-3.21 10.909 5.351s-3.818 8.56-6.545 10.166c0 0 3.818 11.771-3.819 13.377-7.636 1.605-6.545-9.631-6.545-9.631s-12.546-1.606-22.91 14.446-25.746 37.561-66.874 46.069c-8.782.749-16.091-2.087-16.091-2.087z" />
<path stroke="var(--color-outline, #b2b1c8)" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10"
stroke-width="1.826"
d="M225.403 101.638s18.873 37.935 23.51 93.849c3.927 47.62 8.182 74.373-25.092 89.89s-77.456 12.841-98.184 13.376-42.546-2.14-48.001-25.148.273-40.932.273-40.932.763-38.096-1.146-44.249c-1.91-6.153-5.018-15.356-5.018-15.356s-3.11 21.456-19.364 28.679c-16.255 7.277-30.601.428-29.401-19.53 0 0-13.91-22.793-20.51-42.751s10.8-39.54 42.71-31.461c1.091-37.99 34.692-69.932 81.33-73.678 46.583-3.799 57.983 20.707 82.856 21.777 24.001 1.07 40.91-6.42 53.456-18.727 12.546-12.306 38.183-28.358 58.365-15.517 0 0 9.818-3.21 10.909 5.351s-3.818 8.56-6.545 10.166c0 0 3.818 11.771-3.819 13.377-7.636 1.605-6.545-9.631-6.545-9.631s-12.546-1.606-22.91 14.446-25.746 37.561-66.874 46.069m0 0c-8.782.749-16.091-2.087-16.091-2.087" />
<path fill="var(--color-skin-1, #d7d6e1)"
d="M94.272 285.11c0 12.306-2.182 28.358-2.727 34.244-.546 5.885 1.636 11.985 11.454 12.306 15.273.535 44.729 2.675 47.456-6.421 3.436-11.557 2.727-31.033 0-37.454" />
<path stroke="var(--color-outline, #b2b1c8)" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10"
stroke-width="1.826"
d="M94.272 285.11c0 12.306-2.182 28.358-2.727 34.244-.546 5.885 1.636 11.985 11.454 12.306 15.273.535 44.729 2.675 47.456-6.421 3.436-11.557 2.727-31.033 0-37.454" />
<path fill="var(--color-skin-2, #f6f6f9)" d="M113.471 332.035c.437-5.19.982-8.401 3.71-8.936s4.8 1.659 4.854 9.15" />
<path stroke="var(--color-outline, #b2b1c8)" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10"
stroke-width="1.826" d="M113.471 332.035c.437-5.19.982-8.401 3.71-8.936s4.8 1.659 4.854 9.15" />
<path fill="var(--color-skin-2, #f6f6f9)" d="M122.199 332.035c.437-5.19.982-8.401 3.709-8.936s4.801 1.659 4.855 9.15" />
<path stroke="var(--color-outline, #b2b1c8)" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10"
stroke-width="1.826"
d="M122.199 332.035c.437-5.19.982-8.401 3.709-8.936s4.801 1.659 4.855 9.15" />
<path fill="var(--color-skin-2, #f6f6f9)"
d="M130.818 332.195c.437-5.19 1.091-9.096 3.818-9.631s4.855 1.124 4.91 8.561" />
<path stroke="var(--color-outline, #b2b1c8)" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10"
stroke-width="1.826"
d="M130.818 332.195c.437-5.19 1.091-9.096 3.818-9.631s4.855 1.124 4.91 8.561" />
<path fill="var(--color-skin-1, #d7d6e1)"
d="M21.835 140.216c3.982 4.869 11.237 20.439 10.8 32.639-.49 12.252 12.982 17.549 20.073 6.955s9.055-31.247 4.146-41.306c-4.91-10.113-7.582-13.912-7.582-13.912s-2.673 4.816-9.546 5.779-7.09-1.659-9.436-5.244c0 0 .327 3.96-5.782 6.153-6.11 2.248-8.073-2.675-8.073-2.675s1.473 6.795 5.4 11.611m99.438 157.521c-11.619 0-20.074-1.444-26.674-4.494-8.4-3.906-13.636-10.38-15.873-19.904-5.345-22.473.218-40.183.273-40.344l.055-.16v-.161s.054-1.979.054-5.19c7.582.268 18.164 1.498 27.437 5.779 9.164 4.227 14.946 10.487 17.237 18.513 5.073 17.871 18.273 26.539 40.419 26.539 6.873 0 14.728-.803 23.946-2.461 4.691-.857 9.327-1.499 13.8-2.141 21.11-2.996 39.71-5.618 47.947-25.629-1.8 15.463-8.073 27.662-26.51 36.277-27.982 13.055-63.383 13.109-86.784 13.162-4.2 0-7.8 0-10.964.107-1.582.054-3 .107-4.363.107" />
<path fill="var(--color-outline, #b2b1c8)" stroke="var(--color-outline, #b2b1c8)" stroke-linecap="round" stroke-linejoin="round"
stroke-miterlimit="10" stroke-width="1.826"
d="M180.456 116.78c.709-4.548 1.091-7.865-4.255-6.741-5.345 1.123-38.073 9.631-43.91 10.861-5.891 1.231-4.745 6.475.982 9.631s21.655 12.414 28.201 11.558 12.382-2.034 15.054-10.273c2.673-8.187 3.928-15.036 3.928-15.036" />
<path fill="var(--color-skin-1, #d7d6e1)"
d="M160.6 141.072c-6.218 0-19.8-7.597-25.582-10.808l-1.2-.695c-2.782-1.552-4.582-3.799-4.309-5.458.163-1.07 1.254-1.819 3-2.194 3-.642 13.091-3.157 22.8-5.618l2.073-.535c8.673 2.247 15.491 7.973 18.219 15.356l-.11.375c-2.454 7.491-7.418 8.668-14.127 9.524-.273.053-.491.053-.764.053" />
<path fill="var(--color-highlight, #fff)"
d="M130.817 128.925c-1.963-1.391-3.054-3.371-2.727-5.083.273-1.498 1.473-2.515 3.436-2.889 3.164-.643 13.692-3.318 23.837-5.886 4.855-1.231 9.546-2.408 13.255-3.317l1.855 8.025z" />
<path fill="var(--color-outline, #b2b1c8)"
d="m167.855 113.087 1.364 5.939-38.183 8.775c-1.363-1.123-2.127-2.514-1.909-3.692.273-1.391 1.746-1.872 2.618-2.086 3.164-.642 13.692-3.318 23.892-5.886 4.473-1.124 8.727-2.194 12.218-3.05m1.637-2.622c-11.128 2.729-33.383 8.454-38.183 9.471-5.618 1.177-5.782 6.956-.709 10.166l41.237-9.524z" />
<path fill="var(--color-skin-3, #eeedf3)"
d="M70.163 79.22c-11.292 1.819-20.128-.16-23.837-14.66-3.71-14.501 9.273-31.516 27.928-38.15s37.091-1.231 37.091-1.231 5.182-12.039 6.982-18.995 7.255-7.544 12.873 2.783c0 0 15.71-2.194 17.619 14.34 1.964 16.532-11.291 24.826-11.291 24.826" />
<path stroke="var(--color-outline, #b2b1c8)" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10"
stroke-width="1.826"
d="M70.163 79.22c-11.292 1.819-20.128-.16-23.837-14.66-3.71-14.501 9.273-31.516 27.928-38.15s37.091-1.231 37.091-1.231 5.182-12.039 6.982-18.995 7.255-7.544 12.873 2.783c0 0 15.71-2.194 17.619 14.34 1.964 16.532-11.291 24.826-11.291 24.826" />
<path fill="var(--color-skin-2, #f6f6f9)" d="M131.255 8.912c2.345 5.511 1.582 12.253 1.473 14.447z" />
<path stroke="var(--color-outline, #b2b1c8)" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10"
stroke-width="1.826" d="M131.255 8.912c2.345 5.511 1.582 12.253 1.473 14.447" />
<path fill="var(--color-outline, #b2b1c8)"
d="M106.055 118.172c1.2 3.049 3.382 5.297 4.854 4.441 1.473-.857 1.582-3.05.219-6.635-1.309-3.532-7.419-15.945-9.055-18.353-1.637-2.407-3.491-3.103-4.964-2.247s-1.036 2.943-.054 4.923c1.036 2.033 9 17.871 9 17.871" />
<path fill="var(--color-highlight, #fff)"
d="M121.709 168.307c-2.345 0-4.8-.267-7.2-.856-11.564-2.782-17.946-11.504-19.91-18.192-1.254-4.28-.872-7.972 1.091-9.898 1.746-1.766 3.655-2.676 5.564-2.676 4.091 0 7.855 3.853 9.709 6.153 4.146 5.083 8.073 7.652 11.673 7.652 2.891 0 5.4-1.605 7.473-4.816 5.455-8.24 6.928-18.245 4.473-29.642-.818-3.799.164-5.993 1.146-7.17a5.1 5.1 0 0 1 3.981-1.926q1.31 0 2.291.642c9.928 6.314 11.019 27.823 7.419 39.808-3.055 10.06-13.037 20.921-27.71 20.921" />
<path fill="var(--color-outline, #b2b1c8)"
d="M139.654 108.005c.655 0 1.2.16 1.691.481 4.146 2.622 7.091 8.561 8.292 16.694 1.09 7.277.545 15.677-1.364 21.938-1.582 5.297-4.964 10.273-9.437 13.911-4.963 4.066-10.909 6.207-17.182 6.207a28.3 28.3 0 0 1-6.927-.856c-11.128-2.676-17.237-11.076-19.092-17.443-1.145-3.853-.818-7.224.819-8.829 1.581-1.551 3.163-2.354 4.8-2.354 3.763 0 7.472 4.013 8.891 5.725 4.363 5.351 8.564 8.026 12.491 8.026 3.273 0 6.109-1.766 8.4-5.297 5.618-8.508 7.146-18.727 4.582-30.445-.6-2.622-.273-4.869.927-6.26.818-.963 1.964-1.498 3.109-1.498m0-2.141c-3.654 0-7.691 3.425-6.163 10.381 1.963 8.882 1.963 19.262-4.31 28.786-2.072 3.157-4.363 4.334-6.6 4.334-4.472 0-8.782-4.709-10.8-7.224-2.073-2.568-6.055-6.527-10.582-6.527-2.073 0-4.2.802-6.382 2.996-6 6.046 1.091 25.469 19.364 29.856 2.564.642 5.073.91 7.473.91 14.564 0 25.419-10.594 28.746-21.67 3.873-12.841 2.509-34.458-7.855-41.039-.818-.535-1.854-.803-2.891-.803" />
<path stroke="var(--color-outline, #b2b1c8)" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10"
stroke-width="1.826"
d="M110.8 152.737c3.545.909 10.582 1.07 16.255-3.532m-79.965-22.42c-2.018 2.675-5.51 4.227-8.891 3.853-3.382-.321-6.491-2.515-7.91-5.565-.6 2.889-2.89 5.458-5.781 6.153-2.891.696-6.219-.374-8.073-2.675.873 6.956 6.545 12.146 9 18.727" />
<path fill="var(--color-skin-1, #d7d6e1)"
d="m96.181 132.778-9.327 3.478c-2.728 1.017-5.782-.321-6.764-2.996a5.076 5.076 0 0 1 3.054-6.635l9.328-3.478c2.727-1.017 5.782.321 6.764 2.996 1.036 2.622-.328 5.618-3.055 6.635" />
<path stroke="var(--color-skin-2, #f6f6f9)" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10"
stroke-width="1.826"
d="M293.641 24.964c3.982-1.498 7.691-2.3 10.473-2.033M258.73 51.235c6.382-6.1 11.945-11.076 17.236-15.838 2.128-1.873 4.746-3.799 7.582-5.511M209.584 64.88c8.564.748 18.328-.054 28.746-3.532M149.8 42.246c10.855 1.873 19.855 6.528 30.601 12.627" />
<path fill="var(--color-skin-3, #eeedf3)"
d="M80.09 177.027c-26.183 11.772-37.092 19.798-31.092 41.735s32.182 21.937 32.182 21.937-4.363 13.377 9.819 14.982 30.001-2.675 30.546-21.937c.546-19.262-10.909-32.639-34.364-34.779" />
<path stroke="var(--color-outline, #b2b1c8)" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10"
stroke-width="1.826"
d="M80.09 177.027c-26.183 11.772-37.092 19.798-31.092 41.735s32.182 21.937 32.182 21.937-4.363 13.377 9.819 14.982 30.001-2.675 30.546-21.937c.546-19.262-10.909-32.639-34.364-34.779" />
<path fill="var(--color-skin-2, #f6f6f9)" d="M81.18 240.699c5.454 1.07 13.636 1.07 13.636 1.07z" />
<path stroke="var(--color-outline, #b2b1c8)" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10"
stroke-width="1.826" d="M81.18 240.699c5.454 1.07 13.636 1.07 13.636 1.07" />
<path fill="var(--color-skin-2, #f6f6f9)"
d="m103.654 210.041 3.545 3.639c2.673 2.729 2.564 7.116-.218 9.738l-1.036.963c-2.782 2.622-7.255 2.515-9.928-.214l-3.545-3.638c-2.673-2.729-2.564-7.117.218-9.738l1.036-.964c2.782-2.621 7.255-2.514 9.928.214" />
<path fill="var(--color-skin-3, #eeedf3)"
d="m169.001 136.15-3.546 1.391c-2.672 1.016-5.727-.268-6.763-2.89l-.382-.963c-1.037-2.621.273-5.618 2.945-6.634l3.546-1.392c2.673-1.016 5.727.268 6.764 2.89l.382.963c1.09 2.622-.273 5.618-2.946 6.635" />
<path fill="var(--color-skin-2, #f6f6f9)" d="M71.744 173.015c.982-7.491.327-11.825.327-11.825z" />
<path stroke="var(--color-outline, #b2b1c8)" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10"
stroke-width="1.826" d="M71.744 173.015c.982-7.491.327-11.825.327-11.825" />
</g>
<defs>
<clipPath id="a">
<path fill="var(--color-highlight, #fff)" d="M0 0h333.333v333.333H0z" />
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 13 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 11 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 11 KiB

View File

@ -277,7 +277,7 @@ export const Account: React.FC<AccountProps> = ({
if (account?.mute_expires_at) {
muteTimeRemaining = (
<>
· <RelativeTimestamp timestamp={account.mute_expires_at} />
· <RelativeTimestamp hasFuture timestamp={account.mute_expires_at} />
</>
);
}

View File

@ -149,11 +149,7 @@ export const AccountListItem: React.FC<Props> = ({
}
>
{account.last_status_at ? (
<RelativeTimestamp
long
timestamp={account.last_status_at}
noFuture
/>
<RelativeTimestamp long timestamp={account.last_status_at} />
) : (
'-'
)}

View File

@ -52,8 +52,10 @@ export const Badge: FC<BadgeProps> = ({
data-account-role-id={roleId}
>
{icon}
<span>{label}</span>
{domain && <span className={classes.domain}>{domain}</span>}
<span className={classes.content}>
{label}
{domain && <span className={classes.domain}> {domain}</span>}
</span>
</div>
);

View File

@ -7,9 +7,9 @@
gap: 4px;
border-radius: 8px;
align-items: center;
overflow-wrap: anywhere;
> svg {
flex-shrink: 0;
width: auto;
height: 17px;
fill: currentColor;
@ -26,6 +26,12 @@
}
}
.content {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.domain {
opacity: 0.75;
letter-spacing: 0;

View File

@ -70,7 +70,7 @@ export const Poll: React.FC<PollProps> = ({ pollId, disabled, status }) => {
if (expired) {
return intl.formatMessage(messages.closed);
}
return <RelativeTimestamp timestamp={poll.expires_at} />;
return <RelativeTimestamp hasFuture timestamp={poll.expires_at} />;
}, [expired, intl, poll]);
const votesCount = useMemo(() => {
if (!poll) {

View File

@ -23,16 +23,16 @@ export const RelativeTimestamp: FC<{
timestamp: string;
long?: boolean;
noTime?: boolean;
noFuture?: boolean;
}> = ({ timestamp, long = false, noTime = false, noFuture = false }) => {
hasFuture?: boolean;
}> = ({ timestamp, long = false, noTime = false, hasFuture = false }) => {
const intl = useIntl();
const [now, setNow] = useState(() => Date.now());
const date = useMemo(() => {
const date = new Date(timestamp);
return noFuture ? new Date(Math.min(date.getTime(), now)) : date;
}, [noFuture, now, timestamp]);
return !hasFuture ? new Date(Math.min(date.getTime(), now)) : date;
}, [hasFuture, now, timestamp]);
const ts = date.getTime();
useEffect(() => {

View File

@ -11,7 +11,7 @@ const meta = {
timestamp: new Date(Date.now() - DAY * 3).toISOString(),
long: false,
noTime: false,
noFuture: false,
hasFuture: false,
},
argTypes: {
timestamp: {
@ -44,10 +44,10 @@ export const DateOnly: Story = {
},
};
export const NoFuture: Story = {
export const HasFuture: Story = {
args: {
timestamp: new Date(Date.now() + DAY * 3).toISOString(),
noFuture: true,
hasFuture: true,
},
};

View File

@ -56,10 +56,7 @@ export const StatusHeader: FC<StatusHeaderProps> = ({
className='status__relative-time'
>
<StatusVisibility visibility={status.get('visibility')} />
<RelativeTimestamp
timestamp={status.get('created_at') as string}
noFuture
/>
<RelativeTimestamp timestamp={status.get('created_at') as string} />
{editedAt && <StatusEditedAt editedAt={editedAt} />}
</Link>

View File

@ -0,0 +1,107 @@
import { useCallback, useState } from 'react';
import { Article } from '@/mastodon/components/scrollable_list/components';
import KeyboardArrowDownIcon from '@/material-icons/400-24px/keyboard_arrow_down.svg?react';
import KeyboardArrowUpIcon from '@/material-icons/400-24px/keyboard_arrow_up.svg?react';
import { Icon } from '../icon';
import type { IconProp } from '../icon';
import { ListItemButton, ListItemWrapper } from '../list_item';
export interface TruncatedListItemInfo<TListItem> {
item: TListItem;
index: number;
totalListLength: number;
isLastElement: boolean;
}
interface ToggleButtonOptions {
title: NonNullable<React.ReactNode>;
subtitle?: React.ReactNode;
icon?: IconProp;
}
interface TruncatedListProps<TListItem> {
visibleItems: TListItem[];
truncatedItems: TListItem[];
renderListItem: (
itemInfo: TruncatedListItemInfo<TListItem>,
) => React.ReactElement;
toggleButton: ToggleButtonOptions;
}
/**
* Truncate the children of an `ItemList` component with this helper
* component.
* It handles rendering the children with correct indexes for accessibility,
* and has a configurable toggle button.
*/
export const TruncatedListItems = <TListItem,>({
visibleItems,
truncatedItems,
toggleButton,
renderListItem,
}: TruncatedListProps<TListItem>) => {
const [showTruncatedItems, setShowTruncatedItems] = useState(false);
const toggleTruncatedItems = useCallback(() => {
setShowTruncatedItems((prev) => !prev);
}, []);
const hasHiddenAccounts = truncatedItems.length > 0;
// Add the toggle button's item to the list size when needed
const initialListSize = visibleItems.length + (hasHiddenAccounts ? 1 : 0);
const totalListLength =
initialListSize + (showTruncatedItems ? truncatedItems.length : 0);
return (
<>
{visibleItems.map((item, index) => {
return renderListItem({
item,
index,
totalListLength,
isLastElement:
index === visibleItems.length - 1 && !hasHiddenAccounts,
});
})}
{hasHiddenAccounts && (
<Article aria-posinset={initialListSize} aria-setsize={totalListLength}>
<ListItemWrapper
icon={
toggleButton.icon && (
<Icon id='toggle-icon' icon={toggleButton.icon} />
)
}
iconEnd={
<Icon
id='open-status'
icon={
showTruncatedItems
? KeyboardArrowUpIcon
: KeyboardArrowDownIcon
}
/>
}
>
<ListItemButton
aria-expanded={showTruncatedItems}
onClick={toggleTruncatedItems}
subtitle={toggleButton.subtitle}
>
{toggleButton.title}
</ListItemButton>
</ListItemWrapper>
</Article>
)}
{showTruncatedItems &&
truncatedItems.map((item, index) => {
return renderListItem({
item,
index: initialListSize + index + 1,
totalListLength,
isLastElement: index === truncatedItems.length - 1,
});
})}
</>
);
};

View File

@ -0,0 +1,8 @@
[data-color-scheme='dark'] .image {
--color-skin-1: #3a3a50;
--color-skin-2: #67678e;
--color-skin-3: #44445f;
--color-outline: #21212c;
--color-shadow: #181820;
--color-highlight: #b2b1c8;
}

View File

@ -5,17 +5,17 @@ import { FormattedMessage } from 'react-intl';
import { useParams } from 'react-router';
import { Link } from 'react-router-dom';
import ElephantDarkImage from '@/images/elephant_ui_dark.svg?react';
import ElephantLightImage from '@/images/elephant_ui_light.svg?react';
import ElephantImage from '@/images/elephant_ui.svg?react';
import { openModal } from '@/mastodon/actions/modal';
import { Button } from '@/mastodon/components/button';
import { EmptyState } from '@/mastodon/components/empty_state';
import { LimitedAccountHint } from '@/mastodon/features/account_timeline/components/limited_account_hint';
import { areCollectionsEnabled } from '@/mastodon/features/collections/utils';
import { useCurrentAccountId } from '@/mastodon/hooks/useAccountId';
import { useTheme } from '@/mastodon/hooks/useTheme';
import { useAppDispatch } from '@/mastodon/store';
import classes from './empty_message.module.scss';
interface EmptyMessageProps {
suspended: boolean;
hidden: boolean;
@ -33,9 +33,6 @@ export const EmptyMessage: React.FC<EmptyMessageProps> = ({
}) => {
const { acct } = useParams<{ acct?: string }>();
const me = useCurrentAccountId();
const theme = useTheme();
const ElephantImage =
theme === 'dark' ? ElephantDarkImage : ElephantLightImage;
const dispatch = useAppDispatch();
@ -57,7 +54,7 @@ export const EmptyMessage: React.FC<EmptyMessageProps> = ({
const hasCollections = areCollectionsEnabled();
const image = <ElephantImage />;
const image = <ElephantImage className={classes.image} />;
if (me === accountId) {
if (hasCollections) {

View File

@ -1,4 +1,4 @@
import { useEffect } from 'react';
import { useCallback, useEffect } from 'react';
import { FormattedMessage } from 'react-intl';
@ -6,10 +6,9 @@ import { useHistory } from 'react-router';
import { List as ImmutableList } from 'immutable';
import { AccountListItem } from '@/mastodon/components/account_list_item';
import { useAccount } from '@/mastodon/hooks/useAccount';
import AddIcon from '@/material-icons/400-24px/add.svg?react';
import { fetchEndorsedAccounts } from 'mastodon/actions/accounts';
import { AccountListItem } from 'mastodon/components/account_list_item';
import { ColumnBackButton } from 'mastodon/components/column_back_button';
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
import { RemoteHint } from 'mastodon/components/remote_hint';
@ -18,9 +17,12 @@ import {
ItemList,
Scrollable,
} from 'mastodon/components/scrollable_list/components';
import type { TruncatedListItemInfo } from 'mastodon/components/truncated_list';
import { TruncatedListItems } from 'mastodon/components/truncated_list';
import { AccountHeader } from 'mastodon/features/account_timeline/components/account_header';
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
import Column from 'mastodon/features/ui/components/column';
import { useAccount } from 'mastodon/hooks/useAccount';
import { useAccountId } from 'mastodon/hooks/useAccountId';
import { useAccountVisibility } from 'mastodon/hooks/useAccountVisibility';
import {
@ -74,11 +76,32 @@ const AccountFeatured: React.FC<{ multiColumn: boolean }> = ({
const { collections, status: collectionsLoadStatus } = useAppSelector(
(state) => selectAccountCollections(state, accountId ?? null),
);
const listedCollections = collections.filter(
// Hide unlisted and empty collections to avoid confusion
// (Unlisted collections will only be part of the payload
// when viewing your own profile.)
(item) => item.discoverable && !!item.item_count,
const { listedCollections = [], unlistedCollections = [] } = Object.groupBy(
collections,
(item) =>
item.discoverable && !!item.item_count
? 'listedCollections'
: 'unlistedCollections',
);
const renderListItem = useCallback(
({
item,
index,
totalListLength,
isLastElement,
}: TruncatedListItemInfo<(typeof listedCollections)[number]>) => (
<CollectionListItem
key={item.id}
collection={item}
withoutBorder={isLastElement}
withAuthorHandle={false}
positionInList={index}
listSize={totalListLength}
/>
),
[],
);
const hasCollections =
@ -167,16 +190,26 @@ const AccountFeatured: React.FC<{ multiColumn: boolean }> = ({
</Subheading>
{hasCollections ? (
<ItemList>
{listedCollections.map((item, index) => (
<CollectionListItem
key={item.id}
collection={item}
withoutBorder={index === listedCollections.length - 1}
withAuthorHandle={false}
positionInList={index + 1}
listSize={listedCollections.length}
/>
))}
<TruncatedListItems
visibleItems={listedCollections}
truncatedItems={unlistedCollections}
toggleButton={{
title: (
<FormattedMessage
id='collections.unlisted_collections_with_count'
defaultMessage='Unlisted collections ({count})'
values={{ count: unlistedCollections.length }}
/>
),
subtitle: (
<FormattedMessage
id='collections.unlisted_collections_description'
defaultMessage='These dont appear on your profile to others. Anyone with the link can discover them.'
/>
),
}}
renderListItem={renderListItem}
/>
</ItemList>
) : (
<EmptyMessage

View File

@ -36,10 +36,7 @@ export const AccountStatusHeader: FC<StatusHeaderProps> = ({
className='status__relative-time'
>
<StatusVisibility visibility={status.get('visibility')} />
<RelativeTimestamp
timestamp={status.get('created_at') as string}
noFuture
/>
<RelativeTimestamp timestamp={status.get('created_at') as string} />
{editedAt && <StatusEditedAt editedAt={editedAt} />}
</Link>

View File

@ -2,13 +2,6 @@ import { useCallback, useMemo, useRef, useState } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import {
ListItemButton,
ListItemWrapper,
} from '@/mastodon/components/list_item';
import { createAppSelector, useAppSelector } from '@/mastodon/store';
import KeyboardArrowDownIcon from '@/material-icons/400-24px/keyboard_arrow_down.svg?react';
import KeyboardArrowUpIcon from '@/material-icons/400-24px/keyboard_arrow_up.svg?react';
import VisibilityOffIcon from '@/material-icons/400-24px/visibility_off.svg?react';
import type { ApiCollectionJSON } from 'mastodon/api_types/collections';
import type { RenderButtonOptions } from 'mastodon/components/account_list_item';
@ -18,13 +11,15 @@ import {
} from 'mastodon/components/account_list_item';
import { Button } from 'mastodon/components/button';
import { Callout } from 'mastodon/components/callout';
import { Icon } from 'mastodon/components/icon';
import {
Article,
ItemList,
} from 'mastodon/components/scrollable_list/components';
import type { TruncatedListItemInfo } from 'mastodon/components/truncated_list';
import { TruncatedListItems } from 'mastodon/components/truncated_list';
import { me } from 'mastodon/initial_state';
import type { Account } from 'mastodon/models/account';
import { createAppSelector, useAppSelector } from 'mastodon/store';
import { useConfirmRevoke } from './revoke_collection_inclusion_modal';
import classes from './styles.module.scss';
@ -99,13 +94,9 @@ export const CollectionAccountsList: React.FC<{
const intl = useIntl();
const confirmRevoke = useConfirmRevoke(collection);
const listHeadingRef = useRef<HTMLHeadingElement>(null);
const [canShowHiddenAccounts, setCanShowHiddenAccounts] = useState(false);
const toggleHiddenAccounts = useCallback(() => {
setCanShowHiddenAccounts((prev) => !prev);
}, []);
const isOwnCollection = collection?.account_id === me;
const { items = [], account_id: collectionOwnerId, id } = collection ?? {};
const { account_id: collectionOwnerId, id } = collection ?? {};
const relationships = useAppSelector((state) => state.relationships);
const collectionAccounts = useAppSelector((state) =>
@ -135,9 +126,6 @@ export const CollectionAccountsList: React.FC<{
return { visibleAccounts, hiddenAccounts };
}, [collectionAccounts, relationships]);
const hasHiddenAccounts = hiddenAccounts.length > 0;
const initialListSize = visibleAccounts.length + (hasHiddenAccounts ? 1 : 0);
const renderAccountItemButton = useCallback(
({ relationship, accountId }: RenderButtonOptions) => {
// When viewing your own collection, only show the Follow button
@ -165,6 +153,28 @@ export const CollectionAccountsList: React.FC<{
[collectionOwnerId, confirmRevoke],
);
const renderListItem = useCallback(
({
item,
index,
totalListLength,
isLastElement,
}: TruncatedListItemInfo<Account>) => (
<Article
key={item.id}
aria-posinset={index + 1}
aria-setsize={totalListLength}
>
<AccountListItem
accountId={item.id}
withBorder={!isLastElement}
renderButton={renderAccountItemButton}
/>
</Article>
),
[renderAccountItemButton],
);
return (
<>
<h3
@ -194,71 +204,28 @@ export const CollectionAccountsList: React.FC<{
isLoading={isLoading}
emptyMessage={intl.formatMessage(messages.empty)}
>
{visibleAccounts.map(({ id }, index) => (
<Article
key={id}
aria-posinset={index + 1}
aria-setsize={initialListSize}
>
<AccountListItem
accountId={id}
withBorder={index !== items.length - 1 || hasHiddenAccounts}
renderButton={renderAccountItemButton}
/>
</Article>
))}
{hasHiddenAccounts && (
<Article
aria-posinset={initialListSize}
aria-setsize={initialListSize}
>
<ListItemWrapper
icon={<Icon id='visibility-off' icon={VisibilityOffIcon} />}
iconEnd={
<Icon
id='open-status'
icon={
canShowHiddenAccounts
? KeyboardArrowUpIcon
: KeyboardArrowDownIcon
}
/>
}
>
<ListItemButton
aria-expanded={canShowHiddenAccounts}
onClick={toggleHiddenAccounts}
subtitle={
<FormattedMessage
id='collections.hidden_accounts_description'
defaultMessage='Youve blocked or muted {count, plural, one {this user} other {these users}}'
values={{ count: hiddenAccounts.length }}
/>
}
>
<FormattedMessage
id='collections.hidden_accounts_link'
defaultMessage='{count, plural, one {# hidden account} other {# hidden accounts}}'
values={{ count: hiddenAccounts.length }}
/>
</ListItemButton>
</ListItemWrapper>
</Article>
)}
{canShowHiddenAccounts &&
hiddenAccounts.map(({ id }, index) => (
<Article
key={id}
aria-posinset={initialListSize + index + 1}
aria-setsize={initialListSize + hiddenAccounts.length}
>
<AccountListItem
accountId={id}
withBorder={index !== hiddenAccounts.length - 1}
renderButton={renderAccountItemButton}
<TruncatedListItems
visibleItems={visibleAccounts}
truncatedItems={hiddenAccounts}
toggleButton={{
icon: VisibilityOffIcon,
title: (
<FormattedMessage
id='collections.hidden_accounts_link'
defaultMessage='{count, plural, one {# hidden account} other {# hidden accounts}}'
values={{ count: hiddenAccounts.length }}
/>
</Article>
))}
),
subtitle: (
<FormattedMessage
id='collections.hidden_accounts_description'
defaultMessage='Youve blocked or muted {count, plural, one {this user} other {these users}}'
values={{ count: hiddenAccounts.length }}
/>
),
}}
renderListItem={renderListItem}
/>
</ItemList>
</SensitiveScreen>
)}

View File

@ -409,6 +409,8 @@
"collections.sensitive": "Sensitive",
"collections.topic_hint": "Add a hashtag that helps others understand the main topic of this collection.",
"collections.topic_special_chars_hint": "Special characters will be removed when saving",
"collections.unlisted_collections_description": "These dont appear on your profile to others. Anyone with the link can discover them.",
"collections.unlisted_collections_with_count": "Unlisted collections ({count})",
"collections.view_collection": "View collection",
"collections.view_other_collections_by_user": "View other collections by this user",
"collections.visibility_public": "Public",

View File

@ -10657,14 +10657,7 @@ noscript {
}
&__source {
overflow: hidden;
white-space: nowrap;
cursor: help;
> span {
overflow: hidden;
text-overflow: ellipsis;
}
}
}

View File

@ -19,7 +19,7 @@ class EmailSubscription < ApplicationRecord
normalizes :email, with: ->(str) { str.squish.downcase }
validates :email, presence: true, email_address: true, uniqueness: { scope: :account_id }
validates :email, presence: true, email_address: true, length: { maximum: 320 }, uniqueness: { scope: :account_id }
validates :email, email_mx: true, if: -> { email_changed? && !Rails.env.local? }
scope :confirmed, -> { where.not(confirmed_at: nil) }

View File

@ -92,7 +92,7 @@ class User < ApplicationRecord
accepts_nested_attributes_for :invite_request, reject_if: ->(attributes) { attributes['text'].blank? && !Setting.require_invite_text }
validates :invite_request, presence: true, on: :create, if: :invite_text_required?
validates :email, presence: true, email_address: true
validates :email, presence: true, email_address: true, length: { maximum: 320 }
validates :email, email_mx: { attempt_ip: :sign_up_ip }, if: :validate_email_dns?
validates_with UserEmailValidator, if: -> { ENV['EMAIL_DOMAIN_LISTS_APPLY_AFTER_CONFIRMATION'] == 'true' || !confirmed? }

View File

@ -11,8 +11,14 @@ class EmailAddressValidator < ActiveModel::EachValidator
value = value.strip
address = Mail::Address.new(value)
record.errors.add(attribute, :invalid) if address.address != value
record.errors.add(attribute, :invalid) if address.address != value || contains_disallowed_characters?(value)
rescue Mail::Field::FieldError
record.errors.add(attribute, :invalid)
end
private
def contains_disallowed_characters?(value)
value.include?('%') || value.include?(',') || value.include?('"')
end
end

View File

@ -59,7 +59,7 @@ services:
web:
# You can uncomment the following line if you want to not use the prebuilt image, for example if you have local code changes
# build: .
image: ghcr.io/glitch-soc/mastodon:v4.5.8
image: ghcr.io/glitch-soc/mastodon:v4.5.9
restart: always
env_file: .env.production
command: bundle exec puma -C config/puma.rb
@ -83,7 +83,7 @@ services:
# build:
# dockerfile: ./streaming/Dockerfile
# context: .
image: ghcr.io/glitch-soc/mastodon-streaming:v4.5.8
image: ghcr.io/glitch-soc/mastodon-streaming:v4.5.9
restart: always
env_file: .env.production
command: node ./streaming/index.js
@ -102,7 +102,7 @@ services:
sidekiq:
# You can uncomment the following line if you want to not use the prebuilt image, for example if you have local code changes
# build: .
image: ghcr.io/glitch-soc/mastodon:v4.5.8
image: ghcr.io/glitch-soc/mastodon:v4.5.9
restart: always
env_file: .env.production
command: bundle exec sidekiq

BIN
docs/hero-nodes.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 MiB

View File

@ -17,7 +17,7 @@ module Mastodon
end
def default_prerelease
'alpha.6'
'alpha.7'
end
def prerelease