Compare commits

...

360 Commits

Author SHA1 Message Date
diondiondion
15a7507a09 Use radio buttons for emoji style preference (#39126) 2026-05-21 16:55:28 +00:00
Claire
cdf721a273 Fix remote statuses with large media descriptions being rejected (#39135) 2026-05-21 15:46:10 +00:00
Echo
cafe7ea35c Use display name component for empty message (#39131) 2026-05-21 13:59:02 +00:00
David Roetzel
e18ca373eb Revert "Add partial accounts to collections endpoint (#38919)" (#39128) 2026-05-21 13:45:09 +00:00
diondiondion
e54f927149 Accessibility: Add skip link & landmark regions to settings (#39129) 2026-05-21 13:41:14 +00:00
Echo
6735902c1a Fixes collection notification urls (#39127) 2026-05-21 13:17:35 +00:00
renovate[bot]
dc3ffac4a2 Update dependency lint-staged to v17 (#38917)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-21 12:28:52 +00:00
David Roetzel
fe885d5788 Remove superfluous comment (#39123) 2026-05-21 12:10:31 +00:00
Echo
adfe7242f7 Updates vagrant to Node v24 (#39124) 2026-05-21 12:09:08 +00:00
David Roetzel
dfcfef38af Update to ruby 4.0.5 (#39099) 2026-05-21 11:45:36 +00:00
Matt Jankowski
fbc116ef90 Drop support for EOL node version 20 (#38926) 2026-05-21 07:41:52 +00:00
github-actions[bot]
6b5e18fb1d New Crowdin Translations (automated) (#39095)
Co-authored-by: GitHub Actions <noreply@github.com>
2026-05-21 07:37:39 +00:00
diondiondion
d39f7bc72f Accessibility: Add visible focus outlines to emoji/language search fields (#39120) 2026-05-21 07:24:55 +00:00
diondiondion
e68c1c824a Accessibility: Add visible focus outlines to main search & composer fields (#39111) 2026-05-21 06:41:34 +00:00
Eugen Rochko
076c8ec51e Refactor server reducer into TypeScript (#39089) 2026-05-20 14:06:38 +00:00
Claire
f5b57e8ba7 Bump version to v4.5.10 (#39104) 2026-05-20 13:20:08 +00:00
Claire
0786c1e57a Merge commit from fork 2026-05-20 14:38:24 +02:00
Claire
ec2a99341c Merge commit from fork
* Refactor `PrivateAddressCheck`

Also ensures IPv4-mapped IPv6 addresses get properly checked no matter the version of `ipaddr`.

* Add some missing IPv6 ranges from `PrivateAddressCheck`
2026-05-20 14:34:32 +02:00
diondiondion
6e7e8de343 Allow adding an account to a collection directly from the profile page (#39080) 2026-05-20 11:29:41 +00:00
diondiondion
a444a0b572 Accessibility: Add landmark elements to login/sign-up pages (#39098) 2026-05-20 11:28:37 +00:00
Claire
6f8558a6b9 Fix Request error when issuing a request which host is an IP address (#39030) 2026-05-20 09:04:50 +00:00
David Roetzel
22203f8aeb Improve collection item verification (#39096) 2026-05-20 07:55:17 +00:00
Claire
f28715d370 Fix custom emoji selection (#39088) 2026-05-20 07:23:29 +00:00
Claire
fcf012c602 Update browserslists target (#39076) 2026-05-19 16:21:18 +00:00
Echo
dee1dc41aa Include boosts to restore pinned ordering (#39084) 2026-05-19 15:15:36 +00:00
diondiondion
655de32990 Ensure quote posts have no collection previews (#39082) 2026-05-19 14:14:25 +00:00
Echo
99db6a1910 Trigger initial field flow recalculation (#39079) 2026-05-19 12:58:46 +00:00
Echo
d5a7b383fa Autosuggest emojis rendering fix (#39077) 2026-05-19 12:22:52 +00:00
Echo
34c91555ae Refactor emoji search (#39008) 2026-05-19 10:47:45 +00:00
renovate[bot]
bd77f2e86d Update dependency typescript to v6.0.3 (#39060)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-19 08:07:01 +00:00
renovate[bot]
de7282d1cd Update opentelemetry-ruby (non-major) (#39021)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-19 08:06:10 +00:00
renovate[bot]
7f5b16a6ad Update dependency @vitejs/plugin-react to v6.0.2 (#39041)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-19 07:52:11 +00:00
renovate[bot]
e3f81c7368 Update dependency @vitejs/plugin-legacy to v8.0.2 (#39039)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-19 07:52:07 +00:00
renovate[bot]
b36c121a53 Update github/codeql-action digest to 9e0d7b8 (#38981)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-19 07:18:08 +00:00
Tan, Kian-ting
b3992e62ed fix nan-tw not listed in SUPPORTED_LOCALES (#37721)
Co-authored-by: Claire <claire.github-309c@sitedethib.com>
2026-05-19 07:16:50 +00:00
Matt Jankowski
1232b55211 Use mime type symbol to set content_type for custom css response (#37845) 2026-05-19 07:16:24 +00:00
Matt Jankowski
5f33cf0b0a Extract api/v1/statuses#context to standalone controller (#38348) 2026-05-19 07:15:35 +00:00
Matt Jankowski
c3afdb760c Remove references to deleted lint config files (#39033) 2026-05-19 07:11:43 +00:00
renovate[bot]
40f5533990 Update peter-evans/create-pull-request digest to 5f6978f (#38982)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-19 07:07:26 +00:00
github-actions[bot]
eec97e387a New Crowdin Translations (automated) (#39075)
Co-authored-by: GitHub Actions <noreply@github.com>
2026-05-19 07:00:03 +00:00
renovate[bot]
eea90c205a Update DefinitelyTyped types (non-major) (#39059)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-19 06:53:52 +00:00
renovate[bot]
7592813e15 Update dependency postcss-preset-env to v11.3.0 (#39028)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-19 06:51:14 +00:00
renovate[bot]
f0204f3e62 Update dependency vite to v8.0.13 (#38985)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-19 06:50:28 +00:00
renovate[bot]
01434ad4b6 Update dependency ox to v2.14.26 (#38974)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-19 06:44:57 +00:00
diondiondion
c26003af21 Allow users to change how accounts are sorted when viewing a collection (#39073) 2026-05-18 16:48:40 +00:00
Pia B.
07a05e1edf Add batch remove for collections in reports (#39020) 2026-05-18 14:53:40 +00:00
Matt Jankowski
2402730083 Remove unused bin/retry script (#39071) 2026-05-18 14:43:55 +00:00
diondiondion
28ae61f34d Unify compact button size between variants (#39070) 2026-05-18 14:33:23 +00:00
diondiondion
dcb6dbbc86 Update content & placement of "sensitive content" warning on collection page (#39069) 2026-05-18 13:08:41 +00:00
Claire
99b72f60ad Nudge users to turn on discoverable when viewing the empty list of collections they are in (#39029)
Co-authored-by: diondiondion <mail@diondiondion.com>
2026-05-18 12:11:14 +00:00
renovate[bot]
19914e9ef6 Update dependency axios to v1.16.1 (#39031)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-18 12:10:48 +00:00
renovate[bot]
a05d2d7ee2 Update formatjs monorepo (#39013)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-18 11:32:04 +00:00
renovate[bot]
19b19ad8c2 Update dependency ws to v8.20.1 (#39018)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-18 10:31:45 +00:00
renovate[bot]
8f47470853 Update dependency aws-sdk-s3 to v1.222.0 (#39036)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-18 10:20:46 +00:00
Matt Jankowski
75024a1778 Use ruby version 4.0.4 (#39016) 2026-05-18 10:08:22 +00:00
David Roetzel
db304735bf Temporary tweak to account background refresh (#39062) 2026-05-18 10:04:28 +00:00
Shlee
bb94f91f86 Fix accounts header banner grayscale (#39042) 2026-05-18 08:46:49 +00:00
Echo
cdf48e806d Fixes bio spacing when there aren't paragraph tags (#39055) 2026-05-18 08:45:43 +00:00
renovate[bot]
b946b8679d Update dependency strong_migrations to v2.8.0 (#39040)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-18 07:26:23 +00:00
github-actions[bot]
3294b5777f New Crowdin Translations (automated) (#39037)
Co-authored-by: GitHub Actions <noreply@github.com>
2026-05-18 07:26:12 +00:00
renovate[bot]
f095346f8f Update dependency sidekiq to v8.1.5 (#39032)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-18 07:19:53 +00:00
renovate[bot]
d70c807a76 Update dependency aws-sdk-core to v3.247.0 (#39035)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-18 07:19:28 +00:00
Nicholas La Roux
630ad9fd49 Add libheif dependencies to setup-ruby action to unbreak media_attachment_spec.rb on latest pre-release yet rolled out runner image (#39052) 2026-05-18 06:23:20 +00:00
David Roetzel
8bbde181db Use the same condition for stale refresh (#39026) 2026-05-13 14:57:25 +00:00
Matt Jankowski
13fbf00a97 Update codecov-action to v6 (#39019) 2026-05-13 14:48:51 +00:00
Matt Jankowski
0ef5dca3c8 Remove flatware config block, re-extract simplecov config to standalone file (#39017) 2026-05-13 14:46:46 +00:00
github-actions[bot]
771fdcbb9f New Crowdin Translations (automated) (#39024)
Co-authored-by: GitHub Actions <noreply@github.com>
2026-05-13 09:08:11 +00:00
Echo
758db36ec7 Refactor account header banners (#38921) 2026-05-13 08:38:17 +00:00
Pia B.
bbb3392dbe add collections icon to admin report interface (#39009) 2026-05-12 17:46:24 +00:00
Michael Stanclift
cb5c5432b3 Narrow scope of Docker build cache in Github workflows (#39014) 2026-05-12 16:16:55 +00:00
Pia B.
7c05f56fe8 Add batch actions to collections and possibility to report multiple collections (#38991) 2026-05-12 16:13:01 +00:00
Michael Stanclift
d2f640272f Prepare Dockerfile for Node 26 (#38943) 2026-05-12 15:22:38 +00:00
renovate[bot]
2f1bbe051c Update dependency sidekiq to v8.1.4 (#38953)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-12 13:20:47 +00:00
Matt Jankowski
a547dfff37 Move flatware setup into rails_helper (#38944) 2026-05-12 13:12:49 +00:00
github-actions[bot]
735a00d741 New Crowdin Translations (automated) (#39000)
Co-authored-by: GitHub Actions <noreply@github.com>
2026-05-12 07:54:35 +00:00
Pia B.
a6a8a37ae1 add raketasks to generate collections for testing (#38986) 2026-05-11 15:17:00 +00:00
Echo
82ce9367c3 Fixes line spacing in bio (#38988) 2026-05-11 11:13:37 +00:00
github-actions[bot]
e081d5936c New Crowdin Translations (automated) (#38952)
Co-authored-by: GitHub Actions <noreply@github.com>
2026-05-11 07:02:39 +00:00
renovate[bot]
c3a1e04692 Update dorny/paths-filter digest to d1c1ffe (#38886)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-11 06:47:22 +00:00
renovate[bot]
817a0a6764 Update dependency aws-sdk-s3 to v1.221.0 (#38928)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-11 06:46:53 +00:00
renovate[bot]
c45287c72d Update dependency vite to v8.0.11 (#38931)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-11 06:46:36 +00:00
renovate[bot]
2a890822e3 Update dependency hiredis-client to v0.29.0 (#38958)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-11 06:22:19 +00:00
diondiondion
2e7df27180 Make search field and tabs sticky on search results page (#38968) 2026-05-11 06:21:51 +00:00
renovate[bot]
8e03c9c1fc Update dependency devise to v5.0.4 [SECURITY] (#38969)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-11 06:21:03 +00:00
David Roetzel
b1f7c9641f Fix updating collection items when position is unknown (#38962) 2026-05-11 06:08:44 +00:00
diondiondion
e7ed8bb682 Indent collection preview cards when displayed in notifications (#38957) 2026-05-10 08:31:12 +00:00
diondiondion
dcc26c1b24 Stylelint: Allow leading underscore in CSS variable names (#38966) 2026-05-08 14:24:02 +00:00
Echo
8d6406f561 Remove legacy emojify function (#38965) 2026-05-08 14:05:31 +00:00
diondiondion
2543425e04 New profile: Fix link colors in bio and display name overflow (#38964) 2026-05-08 13:30:28 +00:00
diondiondion
cf7a092053 Improve layout and spacing of number fields (#38963) 2026-05-08 13:30:20 +00:00
diondiondion
86e4ecfa20 Add language to collection payload (#38961) 2026-05-08 09:52:09 +00:00
diondiondion
658ad9f57e Fix crash when rendering remote post with collection card (#38959) 2026-05-08 09:22:28 +00:00
diondiondion
b71333921b Fix text overflow issues in list item component (#38954) 2026-05-08 08:34:55 +00:00
Shlee
9ff094b62e Fix #38946 (#38951) 2026-05-08 06:44:28 +00:00
Claire
b2aa476abb Redirect with interstitial when trying to view a remote collection while logged out (#38941) 2026-05-07 16:04:26 +00:00
Echo
496d41cdce Fix fields not having links (#38945) 2026-05-07 15:54:19 +00:00
diondiondion
674e2685be Fix "New collection" link appearing on other accounts' profiles (#38942) 2026-05-07 14:49:31 +00:00
Claire
fcd56d6732 Fix type of interactingObject, interactionTarget and add missing QuoteAuthorization (#38940) 2026-05-07 14:47:19 +00:00
diondiondion
53d0499254 Add label to "Why do you want to join" field during sign-up (#38936) 2026-05-07 11:34:54 +00:00
Trivikram Kamat
27e90864ac Globally install corepack (#34406) 2026-05-07 10:43:15 +00:00
diondiondion
9c8e1855a5 Fix collection sharing/link copying using the local/relative collection URL (#38935) 2026-05-07 10:27:24 +00:00
David Roetzel
11803e3d04 Relax uniqueness constraint to allow nil (#38934) 2026-05-07 10:08:50 +00:00
Echo
c47922602f React Strict Mode (#38895) 2026-05-07 10:03:08 +00:00
diondiondion
60a437e045 Show "Follow" button next to accounts in a collection when logged out (#38933) 2026-05-07 09:51:12 +00:00
Echo
f24f98ce40 Profile: Remove old classes (#38920) 2026-05-07 09:41:03 +00:00
diondiondion
2fed2edd5e Hide "Follows you" badge when viewing your own list of followers (#38932) 2026-05-07 09:34:20 +00:00
github-actions[bot]
92c9fda9e6 New Crowdin Translations (automated) (#38930)
Co-authored-by: GitHub Actions <noreply@github.com>
2026-05-07 09:26:43 +00:00
Echo
d0c8eb2f1b Swap from React Helmet to Unhead (#38896) 2026-05-07 09:09:27 +00:00
Claire
90c812ed16 Add explicit dependency to ipaddr (#38925) 2026-05-07 09:04:04 +00:00
Pia B.
1a2038775c Add ability to search email blocks by domain (#38923) 2026-05-06 15:54:12 +00:00
Matt Jankowski
65b7ddb3e8 Add failing service case to remote account refresh worker spec (#38922) 2026-05-06 15:10:12 +00:00
David Roetzel
f6f45c43a9 Add partial accounts to collections endpoint (#38919) 2026-05-06 15:08:26 +00:00
renovate[bot]
0f753038c4 Update github/codeql-action digest to e46ed2c (#38887)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-06 13:17:26 +00:00
renovate[bot]
fee4c262d2 Update formatjs monorepo (#38913)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-06 13:15:54 +00:00
renovate[bot]
a2d04ee7b4 Update dependency vite-plugin-pwa to v1.3.0 (#38910)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-06 13:15:36 +00:00
nicole mikołajczyk
194b889873 Expose mastodon-async-refresh response header through CORS (#38914) 2026-05-06 10:41:25 +00:00
renovate[bot]
1e3b089eb6 Update docker/build-push-action digest to 10e90e3 (#38815)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-06 09:53:28 +00:00
David Roetzel
07ce066d68 Move PartialAccountSerializer to the top-level (#38916) 2026-05-06 09:35:07 +00:00
renovate[bot]
b653660a5c Update dependency axios to v1.16.0 (#38880)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-06 09:14:07 +00:00
github-actions[bot]
b04f7e7411 New Crowdin Translations (automated) (#38915)
Co-authored-by: GitHub Actions <noreply@github.com>
2026-05-06 08:54:35 +00:00
renovate[bot]
9ef8df569e Update dependency rubyzip to v3.3.0 (#38881)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-06 08:49:03 +00:00
Matt Jankowski
d243ba36ce Add admin area spec for email subscriptions management (#38912) 2026-05-06 08:47:12 +00:00
Matt Jankowski
aee0025ca3 Add system spec for managing everyone user role 2FA (#38911) 2026-05-06 08:36:42 +00:00
Claire
cb2e770584 Remove duplicate index index_email_subscriptions_on_account_id (#38907) 2026-05-05 15:15:55 +00:00
Claire
5e3e11bbfa Fix role management interface not offering to require 2FA for all users (#38906) 2026-05-05 14:46:52 +00:00
Claire
3bc27b9b64 Resolve unknown tagged collections in remote posts (#38900) 2026-05-05 14:46:47 +00:00
github-actions[bot]
127de5bb3d New Crowdin Translations (automated) (#38905)
Co-authored-by: GitHub Actions <noreply@github.com>
2026-05-05 10:24:49 +00:00
renovate[bot]
0aae54dfd9 Update dependency linzer to v0.7.9 (#38874)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-05 08:17:59 +00:00
Kenta Ishizaki
066456ecdf Fix typo in typed_functions.ts comment (#38590) 2026-05-05 04:54:34 +00:00
renovate[bot]
e715531dd3 Update devDependencies (non-major) (#38901)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: ChaosExAnima <ChaosExAnima@users.noreply.github.com>
2026-05-04 21:21:52 +00:00
renovate[bot]
ba83509ff4 Update dependency FFmpeg/FFmpeg to v8.1.1 (#38888)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-04 16:24:22 +00:00
diondiondion
a23b3c7c25 Update block dialog copy to include quotes & collections (#38897) 2026-05-04 15:22:24 +00:00
Eugen Rochko
ee88da4511 Add admin UI for managing email subscriptions (#38741)
Co-authored-by: Claire <claire.github-309c@sitedethib.com>
2026-05-04 13:56:04 +00:00
Juan José Martos
46ccfa6e8d Updating rollup and flatted dependencies [SECURITY] (#38497) 2026-05-04 13:09:09 +00:00
renovate[bot]
5922d0181e Update formatjs monorepo (#38804)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-04 12:29:58 +00:00
Claire
030104a30c Change how invalid-but-not-expired invites are shown in moderation interface (#38736) 2026-05-04 11:38:11 +00:00
Jeong Arm
ff99131776 Fix unblocking domain from blocked domains column does not update the list (#38882) 2026-05-04 11:37:56 +00:00
Echo
a7001f52ab Wraps content in Callout component (#38893) 2026-05-04 10:18:43 +00:00
Echo
708fe31908 Keep trying to load emojis if data isn't available yet (#38892) 2026-05-04 09:50:24 +00:00
Gomasy
00c2089e81 Fix emoji picker not rendering when no custom emojis (#38885) 2026-05-04 09:09:17 +00:00
Matt Jankowski
696aaa616b Update rubocop-capybara to version 2.23.0 (#38868) 2026-05-04 07:52:29 +00:00
github-actions[bot]
e4b8bbe6e8 New Crowdin Translations (automated) (#38875)
Co-authored-by: GitHub Actions <noreply@github.com>
2026-05-04 07:49:54 +00:00
renovate[bot]
aa6baf15aa Update dependency jsdom to v29.1.1 (#38816)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-04 07:28:56 +00:00
renovate[bot]
ea52f76314 Update opentelemetry-ruby (non-major) (#38834)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-04 07:28:52 +00:00
renovate[bot]
b44aa94853 Update dependency playwright-ruby-client to v1.59.1 (#38848)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-04 07:28:46 +00:00
github-actions[bot]
c6facd27ed New Crowdin Translations (automated) (#38871)
Co-authored-by: GitHub Actions <noreply@github.com>
2026-04-30 16:44:20 +00:00
Echo
c79bd31234 Change handle explainer to refer to the Fediverse (#38872) 2026-04-30 16:32:48 +00:00
Claire
322ada898f Update dependency minimatch (#38869) 2026-04-30 16:21:38 +00:00
Echo
d63ca75422 Fixes minor profile visual glitches (#38870) 2026-04-30 16:17:14 +00:00
Matt Jankowski
59f3d8a993 Handle IPv6 scenario in custom Request::Socket (#38866) 2026-04-30 16:03:55 +00:00
Echo
c270634565 Profile editing: Control follower/following list visibility (#38845) 2026-04-30 14:16:30 +00:00
Matt Jankowski
b1703467f1 Use bundler version 4.0.11 (#38867) 2026-04-30 14:16:15 +00:00
diondiondion
b076808fd2 Add "Featuring you" tab to Collections page (#38865) 2026-04-30 14:10:25 +00:00
Echo
945ac23910 Remove and move profile code (#38863) 2026-04-30 13:58:22 +00:00
Matt Jankowski
3021cd8002 Update puma to version 8.0.1 (#38738) 2026-04-30 13:28:15 +00:00
renovate[bot]
a8c261ae7c Update dependency aws-sdk-s3 to v1.220.0 (#38788)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-30 13:22:03 +00:00
renovate[bot]
d4e7af910c Update dependency vite to v8.0.10 (#38748)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-30 12:59:56 +00:00
github-actions[bot]
20e3265f3b New Crowdin Translations (automated) (#38864)
Co-authored-by: GitHub Actions <noreply@github.com>
2026-04-30 12:32:25 +00:00
renovate[bot]
88b21e587c Update dependency irb to v1.18.0 (#38773)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-30 09:53:20 +00:00
renovate[bot]
c18db97254 Update dependency stoplight to v5.8.2 (#38752)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-30 09:52:13 +00:00
renovate[bot]
d4e60dae9a Update codecov/codecov-action digest to 75cd116 (#38747)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-30 09:25:22 +00:00
renovate[bot]
8456616793 Update actions/cache digest to 27d5ce7 (#38746)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-30 09:24:55 +00:00
renovate[bot]
9c5ef8f3f6 Update dependency aws-sdk-core to v3.246.0 (#38742)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-30 09:23:40 +00:00
renovate[bot]
5288abfb03 Update dependency axios to v1.15.2 (#38613)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-30 09:22:31 +00:00
renovate[bot]
6dbad32d65 Update actions/setup-node digest to 48b55a0 (#38483)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-30 09:21:23 +00:00
Claire
1df259f8c9 Fix translation string of some fallback notifications (#38860) 2026-04-30 09:18:32 +00:00
github-actions[bot]
deb72a4c91 New Crowdin Translations (automated) (#38859)
Co-authored-by: GitHub Actions <noreply@github.com>
2026-04-30 08:21:13 +00:00
Echo
a47ed31047 Fixes custom emoji not appearing in autocomplete (#38854)
Co-authored-by: Copilot <copilot@github.com>
2026-04-29 19:44:42 +00:00
Claire
5b395774c0 Add fallback attributes to notifications for new and infrequent notifications (#38832) 2026-04-29 15:53:29 +00:00
diondiondion
afeb63d287 Improve collection page loading states (#38847) 2026-04-29 15:49:52 +00:00
David Roetzel
725d8983fa Fix client-side collection routes (#38850) 2026-04-29 14:52:32 +00:00
diondiondion
b761310823 Fix stale collections list after deleting a collection (#38852) 2026-04-29 14:42:23 +00:00
github-actions[bot]
578836f9ae New Crowdin Translations (automated) (#38842)
Co-authored-by: GitHub Actions <noreply@github.com>
2026-04-29 13:47:29 +00:00
diondiondion
41a3679d83 Mark pending accounts in collection editor (#38843) 2026-04-29 10:35:09 +00:00
diondiondion
614eda43ff Add date & correct icon to "You are in this collection" callout (#38844) 2026-04-29 10:35:07 +00:00
diondiondion
b193913f46 Mark pending accounts on the collection detail page (#38830) 2026-04-28 16:31:18 +00:00
Nicholas La Roux
eb5bfa4541 Upgrade development Ruby from 4.0.2 to 4.0.3 (#38820) 2026-04-28 15:39:41 +00:00
diondiondion
31f89617d8 Fix error when collection is null in collection notification (#38831) 2026-04-28 15:25:17 +00:00
diondiondion
ffd7160980 Add "Follows you" badge to AccountListItem component (#38828) 2026-04-28 15:18:02 +00:00
David Roetzel
6c5bd4f9a8 Handle collections when blocking a user (#38827) 2026-04-28 14:49:19 +00:00
renovate[bot]
763e2ddc49 Update docker.io/ruby Docker tag to v4.0.3 (#38772)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-28 14:22:56 +00:00
Echo
5d9796afb2 Remove custom emojis from Redux (#38825) 2026-04-28 13:49:10 +00:00
renovate[bot]
bd17c48ef9 Update dependency ruby to v4.0.3 (#38765)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Claire <claire.github-309c@sitedethib.com>
2026-04-28 13:30:20 +00:00
Echo
03045425b7 Allow keyboard modal form submission (#38826) 2026-04-28 12:37:58 +00:00
github-actions[bot]
4f76bdfcb7 New Crowdin Translations (automated) (#38824)
Co-authored-by: GitHub Actions <noreply@github.com>
2026-04-28 11:39:12 +00:00
diondiondion
3d5cb624ba Update design of Collections list page (#38822) 2026-04-28 11:32:27 +00:00
David Roetzel
1f1653e039 Remove rejected and revoked collection items (#38792) 2026-04-28 11:10:25 +00:00
David Roetzel
d5f8b08d69 Add collections to Flag activities (#38817) 2026-04-28 11:09:43 +00:00
David Roetzel
2dd630bc58 Only update FASP availability if it actually changed (#38818) 2026-04-27 13:15:53 +00:00
diondiondion
2b93a2211f Increase clickable area around collection items, refactor ListItem component (#38776) 2026-04-27 09:11:07 +00:00
github-actions[bot]
c53bb2fcd6 New Crowdin Translations (automated) (#38805)
Co-authored-by: GitHub Actions <noreply@github.com>
2026-04-27 08:09:16 +00:00
renovate[bot]
b1cea4a3d3 Update dependency tzinfo-data to v1.2026.2 (#38807)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-27 07:56:23 +00:00
renovate[bot]
74d5f99ba3 Update dependency strong_migrations to v2.7.0 (#38808)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-27 07:55:00 +00:00
renovate[bot]
45221070cc Update crowdin/github-action digest to 8868a33 (#38814)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-27 07:53:04 +00:00
diondiondion
3473b8a652 Add server thumbnail alt text to frontend (#38801) 2026-04-24 10:28:04 +00:00
github-actions[bot]
a4e5c3244f New Crowdin Translations (automated) (#38800)
Co-authored-by: GitHub Actions <noreply@github.com>
2026-04-24 08:46:34 +00:00
renovate[bot]
ff57ef2c9b Update dependency ox to v2.14.25 (#38798)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-24 08:31:22 +00:00
diondiondion
a217b633b7 Allow defining alt text for server thumbnail (#38796) 2026-04-23 17:46:50 +00:00
Claire
be4ba1495c Remove unused devise strategies (#38795) 2026-04-23 14:26:19 +00:00
renovate[bot]
0142a4a9de Update dependency ox to v2.14.24 (#38760)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-23 13:55:35 +00:00
Shlee
7c1d6ab114 Role "collection limit" setting missing in params (#38794) 2026-04-23 13:32:12 +00:00
Echo
e2be688389 Profile redesign: Show full join date (#38687) 2026-04-23 12:47:01 +00:00
diondiondion
2f0db28aa4 Implement collection limit on frontend (#38786) 2026-04-23 12:44:30 +00:00
diondiondion
d7b60a2cb6 Fix preview for local collection links (#38793) 2026-04-23 11:47:31 +00:00
diondiondion
478dae0ab3 Show collection preview cards and open collections links locally (#38643) 2026-04-23 09:16:54 +00:00
github-actions[bot]
a8741495c4 New Crowdin Translations (automated) (#38790)
Co-authored-by: GitHub Actions <noreply@github.com>
2026-04-23 08:42:05 +00:00
diondiondion
540042dfe3 Fix minor collection bugs (#38785) 2026-04-23 08:37:22 +00:00
Echo
1d1deaab2a Profile editing: Fix overflow in advanced view (#38791) 2026-04-23 08:28:14 +00:00
Echo
5bc69ea668 Emoji loading performance (#38784) 2026-04-23 07:48:00 +00:00
David Roetzel
fdb2563abf Use /collections/:id as canonical URL for a collection (#38783) 2026-04-23 07:36:35 +00:00
Echo
c4eec632b9 Makes Vite use browserslist (#38777) 2026-04-22 16:29:29 +00:00
diondiondion
5b1891a1ae Fix confusing hover states in admin list items (#38782) 2026-04-22 14:12:27 +00:00
diondiondion
e3c0883d32 Fix ugly Combobox loading state (#38778) 2026-04-22 13:33:34 +00:00
David Roetzel
1cae543e8f Add per-user maximum number of collections (#38769) 2026-04-22 12:34:08 +00:00
Echo
bc09d3c5f2 Removes React Toggle library (#38771)
Co-authored-by: diondiondion <mail@diondiondion.com>
2026-04-22 11:31:14 +00:00
David Roetzel
58df263159 Make old migration more robust (#38775) 2026-04-22 10:44:36 +00:00
github-actions[bot]
a3127a146d New Crowdin Translations (automated) (#38774)
Co-authored-by: GitHub Actions <noreply@github.com>
2026-04-22 10:42:55 +00:00
diondiondion
a706fce678 Implement final design for collection editor account dropdown menu (#38767) 2026-04-21 14:12:44 +00:00
Echo
57c5d1c8dd Remove animation detection in favour of never cropping GIFs (#38766) 2026-04-21 11:40:09 +00:00
Matt Jankowski
c589530e22 Add constants to track media player height/width (#38755) 2026-04-21 11:10:45 +00:00
github-actions[bot]
9717dc64d0 New Crowdin Translations (automated) (#38762)
Co-authored-by: GitHub Actions <noreply@github.com>
2026-04-21 11:10:06 +00:00
renovate[bot]
5399d9761c Update dependency uuid to v14 (#38744)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-21 10:02:44 +00:00
renovate[bot]
27d92ede7e Update dependency pghero to v3.8.0 (#38706)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-21 10:02:40 +00:00
renovate[bot]
ec855cbf59 Update opentelemetry-ruby (non-major) (#38682)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-21 10:02:36 +00:00
renovate[bot]
6903d15559 Update formatjs monorepo (#38666)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-21 10:02:33 +00:00
Matt Jankowski
055b739b88 Use with_domain scope in ReportService (#38758) 2026-04-21 09:59:04 +00:00
diondiondion
d82bada742 Add "Must follow" section to account suggestion dropdown menu (#38750) 2026-04-20 17:50:37 +00:00
Matt Jankowski
28e5c3bb51 Add coverage for "no change" scenario in admin change emails (#38754) 2026-04-20 16:05:46 +00:00
Claire
ccf5c09ad3 Fix incorrect value for feature_approval.current_user for local users (#38751) 2026-04-20 16:00:15 +00:00
David Roetzel
c0b1fbe0a9 Fix item limit on collections (#38749) 2026-04-20 10:17:12 +00:00
github-actions[bot]
d9149bfed9 New Crowdin Translations (automated) (#38743)
Co-authored-by: GitHub Actions <noreply@github.com>
2026-04-20 07:47:02 +00:00
renovate[bot]
0d283cc48e Update dependency propshaft to v1.3.2 (#38740)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-20 07:12:43 +00:00
renovate[bot]
06417e2b92 Update dependency sidekiq-scheduler to v6.0.2 (#38737)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-20 07:12:21 +00:00
renovate[bot]
45fbb3b053 Update dependency faker to v3.8.0 (#38724)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-20 07:11:24 +00:00
renovate[bot]
225fe58d5c Update dependency sidekiq to v8.1.3 (#38723)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-20 07:11:15 +00:00
renovate[bot]
048700da2f Update Yarn to v4.14.1 (#38720)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-20 07:09:37 +00:00
Shlee
bdad4f78f3 Fallback to default theme when admin-selected theme does not exist (#38703) 2026-04-17 16:57:38 +00:00
Matt Jankowski
b15d234ccb Add domain_variants helper to DomainNormalizable concern (#38539) 2026-04-17 15:36:09 +00:00
diondiondion
05a1c170c2 Update design of account search dropdown in collection editor (#38739) 2026-04-17 15:13:18 +00:00
Matt Jankowski
ea33d7fba6 Add AccountMigration#remaining_cooldown_days method (#38561) 2026-04-17 14:01:07 +00:00
Matt Jankowski
1d3ca80bf7 Use model constants more consistently for view expiration collections (#38589) 2026-04-17 13:57:18 +00:00
Shlee
9afaa23e78 Fix incorrect only option in before_validation filters (#38704) 2026-04-17 13:36:38 +00:00
Matt Jankowski
475e6833ff Update to copy and order for media display options (#38731)
Co-authored-by: Claire <claire.github-309c@sitedethib.com>
2026-04-17 13:31:14 +00:00
David Roetzel
b846f88e16 Improve collection item behavior in REST API (#38732) 2026-04-17 13:28:39 +00:00
Matt Jankowski
5722b1bbc5 Remove invalid options from recovery codes controller (#38733) 2026-04-17 13:25:39 +00:00
diondiondion
570f2ef482 Allow grouping items in Combobox component (#38730) 2026-04-17 13:18:04 +00:00
diondiondion
e571994b5c Remove "View other collections from this user" from collection menu (#38728) 2026-04-17 08:50:35 +00:00
Matt Jankowski
3411d06f9e Pull user settings defaults from configuration (#38592) 2026-04-17 08:31:37 +00:00
Matt Jankowski
d5f0e37260 Include hosts resolver in request socket DNS lookup (#38699) 2026-04-17 08:09:37 +00:00
github-actions[bot]
3c88310f37 New Crowdin Translations (automated) (#38726)
Co-authored-by: GitHub Actions <noreply@github.com>
2026-04-17 08:08:49 +00:00
renovate[bot]
58f0a80ae9 Update Node.js to 24.15 (#38707)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-17 07:48:15 +00:00
diondiondion
a40b071640 Implement new Collection inclusion rules in Collection accounts editor (#38719) 2026-04-16 18:05:36 +00:00
Claire
0e6180a5af Fix Bundle being used with incorrect prop types by using type-dependent key (#38721) 2026-04-16 18:05:04 +00:00
Claire
fc1ba93cdc Refactor featured collections URL code (#38709) 2026-04-16 16:00:13 +00:00
Shlee
0e4ee62dfc Fix typo in block_spec.rb (#38714) 2026-04-16 15:59:48 +00:00
David Roetzel
e711f9d492 Federate featured item creation date (#38713) 2026-04-16 15:44:50 +00:00
diondiondion
5a38246ee8 Update design of collection accounts editor (#38712) 2026-04-16 13:59:38 +00:00
Matt Jankowski
0ef00be494 Use bundler version 4.0.10 (#38671) 2026-04-16 11:46:13 +00:00
David Roetzel
961acaf202 Include collection url in API responses (#38708) 2026-04-16 10:09:39 +00:00
renovate[bot]
e05ac2ec04 Update dependency dotenv to v17.4.2 (#38655)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-16 09:46:12 +00:00
renovate[bot]
b17c544f1d Update dependency postcss-preset-env to v11.2.1 (#38656)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-16 09:46:04 +00:00
renovate[bot]
89611bf32c Update dependency @rolldown/plugin-babel to v0.2.3 (#38661)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-16 09:45:58 +00:00
github-actions[bot]
18c79e4e45 New Crowdin Translations (automated) (#38705)
Co-authored-by: GitHub Actions <noreply@github.com>
2026-04-16 09:32:46 +00:00
renovate[bot]
21a6ecbfb4 Update dependency faker to v3.7.1 (#38681)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-16 09:17:37 +00:00
David Roetzel
fee38e57f0 Federate and store a collection url (#38697) 2026-04-16 07:26:34 +00:00
diondiondion
543db6d24c Add more actions to collections notifications & context menus (#38698) 2026-04-15 18:03:48 +00:00
Claire
6b1e1899fd Change discoverable accounts to only allow followers to feature them if they are locked (#38672) 2026-04-15 15:05:58 +00:00
Claire
3a84990780 Bump version to v4.5.9 (#38696) 2026-04-15 14:10:28 +00:00
Claire
d6f62f5fa4 Merge commit from fork
* Disallow some special characters in e-mail addresses

* Add size limit to email columns
2026-04-15 15:22:33 +02:00
Echo
fab1e799a6 Profile redesign: Make illustration use CSS vars (#38692) 2026-04-15 12:54:10 +00:00
diondiondion
4835c3b7b4 Allow viewing unlisted collections on your own Profile's Featured tab (#38690) 2026-04-15 12:25:43 +00:00
diondiondion
298fc7ce4c Prevent text wrapping in Badge component (#38691) 2026-04-15 12:06:26 +00:00
Echo
e71d6fa344 Makes RelativeTimestamp default to not showing the future (#38689) 2026-04-15 11:39:27 +00:00
Matt Jankowski
32edf53ea9 Fix hero image radius, bring into repo (#38679)
Co-authored-by: Kiru <mail@kiru.gay>
2026-04-15 11:32:51 +00:00
Echo
d9ea631d59 Featured tab: Check if collections are enabled for loading status (#38688) 2026-04-15 10:21:22 +00:00
Claire
e9af9c649f Fix definition for quote in JSON-LD context (#38686) 2026-04-15 10:17:47 +00:00
Claire
f6652caef4 Fix invalid arguments being passed to Redis in custom Chewy strategy (#38684) 2026-04-15 09:54:38 +00:00
Matt Jankowski
75bbf73737 Make fields build spec resilient to changed size constant (#38678) 2026-04-15 09:29:21 +00:00
Michael Stanclift
9439a2e944 Update FFmpeg renovate datasource to github-tags (#38676) 2026-04-15 09:26:50 +00:00
github-actions[bot]
05c9ebf2ce New Crowdin Translations (automated) (#38683)
Co-authored-by: GitHub Actions <noreply@github.com>
2026-04-15 09:25:46 +00:00
Claire
2b93d19d2c Update handle explainer copy (#38646)
Co-authored-by: nicolas <nclm@users.noreply.github.com>
2026-04-14 13:15:59 +00:00
Echo
d931e2f30d Prevents featured tags from flickering (#38667) 2026-04-14 11:58:50 +00:00
Eugen Rochko
ba0b9e8ea5 Add publiccode.yml (#38659) 2026-04-14 11:28:15 +00:00
github-actions[bot]
4fcab304e3 New Crowdin Translations (automated) (#38665)
Co-authored-by: GitHub Actions <noreply@github.com>
2026-04-14 10:12:20 +00:00
Echo
6142c7b003 Profile redesign: Allow animated and transparent avatars (#38663) 2026-04-14 09:21:06 +00:00
Claire
63a244fe1a Add /api/v1_alpha/accounts/:id/in_collections to list collections you are in (#38657) 2026-04-13 17:28:28 +00:00
diondiondion
02deb0b238 Allow revealing blocked/muted accounts in a collection (#38660) 2026-04-13 15:41:47 +00:00
renovate[bot]
96c8eeba49 Update actions/cache digest to 6682284 (#38482)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-13 15:14:36 +00:00
Claire
cea0cbdb73 Update dependency rack-session (#38601) 2026-04-13 14:52:15 +00:00
Claire
46af7467e0 Improve error handling when failing to refresh an actor's key (#38555) 2026-04-13 14:49:42 +00:00
Claire
06a8379dce Fix collections allowing multiple occurrences of the same user (#38636) 2026-04-13 10:28:54 +00:00
Elouan Martinet
7b343c9567 Fix streaming using deprecated url.parse instead of WHATWG URL API (#36973) 2026-04-13 09:21:11 +00:00
renovate[bot]
f98d8157d6 Update dependency axios to v1.15.0 [SECURITY] (#38654)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-13 09:11:37 +00:00
github-actions[bot]
f4f1a86da6 New Crowdin Translations (automated) (#38647)
Co-authored-by: GitHub Actions <noreply@github.com>
2026-04-13 08:57:02 +00:00
renovate[bot]
fa529c1883 Update dependency vite to v8.0.8 (#38603)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-13 08:14:21 +00:00
renovate[bot]
896e15bd4a Update Playwright (#38642)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-13 08:13:16 +00:00
Matt Jankowski
bb64905b21 Update redis gem to version 5.4.1 (#38110) 2026-04-13 07:44:06 +00:00
Claire
73fc8d34d9 Change collection update to also send notifications if the sensitive label or the topic are changed (#38644) 2026-04-11 11:01:55 +00:00
Claire
8124d44ee1 Fix local collection uri not being serialized in REST API responses (#38645) 2026-04-10 15:01:28 +00:00
Shlee
3b39562954 Minor: Moved the debug log to the correct location. (#38639) 2026-04-10 14:14:09 +00:00
diondiondion
a896081808 Fix Followers/Following list error when they contain accounts that have never posted (#38640) 2026-04-10 14:06:35 +00:00
diondiondion
df4b4f1620 Adds collection notification UI (#38638) 2026-04-10 12:11:11 +00:00
github-actions[bot]
9c164aa16c New Crowdin Translations (automated) (#38635)
Co-authored-by: GitHub Actions <noreply@github.com>
2026-04-10 11:18:59 +00:00
Shlee
8bc0eaa1bb Fix typo in create_collection_service.rb (#38629) 2026-04-10 08:55:23 +00:00
diondiondion
eed704d4bd Update wording for discoverable option (#38633) 2026-04-09 15:32:20 +00:00
diondiondion
d6c0b93c85 Change "My collections" path to /@username/collections (#38630) 2026-04-09 15:31:49 +00:00
diondiondion
ef4a583f54 More design tweaks for empty state in Featured tab > Collections (#38626) 2026-04-09 14:37:39 +00:00
Shlee
f429019f34 Missing .freeze on collection_update reference in notification.rb (#38628) 2026-04-09 14:36:54 +00:00
diondiondion
2ef9cceccd Fix broken line clamping for bios in AccountListItem (#38632) 2026-04-09 14:24:05 +00:00
Claire
d6f8ac97e8 Add trademark warning to mastodon:setup task (#38548) 2026-04-09 12:30:06 +00:00
diondiondion
19ef4e5c40 Allow hiding featured tab from empty state (#38625) 2026-04-09 10:59:11 +00:00
github-actions[bot]
245c03664a New Crowdin Translations (automated) (#38620)
Co-authored-by: GitHub Actions <noreply@github.com>
2026-04-09 10:50:03 +00:00
github-actions[bot]
875cd30150 New Crowdin Translations (automated) (#38620)
Co-authored-by: GitHub Actions <noreply@github.com>
2026-04-09 10:50:03 +00:00
Michael Stanclift
79505180a5 Optimize ffmpeg and libvips Dockerfile builds (#37401)
Signed-off-by: Michael Stanclift <mx@vmstan.com>
2026-04-09 09:46:57 +00:00
renovate[bot]
519b00f25b Update dependency aws-sdk-s3 to v1.219.0 (#38499)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-09 09:23:49 +00:00
renovate[bot]
cec3e82b21 Update dependency vite-plugin-svgr to v5.2.0 (#38547)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-09 09:23:10 +00:00
diondiondion
05bed6f3d8 Update Profile Featured tab to latest designs (#38616) 2026-04-09 09:15:05 +00:00
renovate[bot]
34514f00da Update opentelemetry-ruby (non-major) (#38599)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-09 09:08:27 +00:00
renovate[bot]
c2fafce995 Update dependency strong_migrations to v2.6.0 (#38598)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-09 09:08:01 +00:00
diondiondion
4e60a6f163 Hide bio & familiar followers from Followers/Following lists (#38622) 2026-04-09 09:00:09 +00:00
diondiondion
b6f09b9a2b Use more neutral background color on Share page (#38621) 2026-04-09 08:40:06 +00:00
Claire
66fdd3ae65 Fix serialization of added_to_collection notifications (#38612) 2026-04-08 16:40:51 +00:00
Claire
4ad54b279d Add ability to search for a collection by URL (#38588) 2026-04-08 16:03:35 +00:00
Claire
97ba08113d Fix being able to quote someone you blocked (#38608) 2026-04-08 16:03:24 +00:00
Eugen Rochko
ba9eabccbf Fix no notification being created when account is added to collection on creation (#38611) 2026-04-08 16:01:37 +00:00
Matt Jankowski
28b04ec24e Update sidekiq to version 8.1.2 (#38134) 2026-04-08 14:46:37 +00:00
diondiondion
7d9b1e6d1e Update collection account item design (#38586) 2026-04-08 14:26:39 +00:00
diondiondion
e65fedd672 Allow "Follows you" badge to wrap along with profile heading (#38607) 2026-04-08 13:16:45 +00:00
Claire
39c70649ca Add added_to_collection and collection_updated notification types (#38491) 2026-04-08 12:56:07 +00:00
diondiondion
df64716b34 Rename CSS classes for profile redesign (#38606) 2026-04-08 12:21:07 +00:00
Claire
99a219036f Fix new profile dropdown blocking without confirmation modal (#38605) 2026-04-08 11:24:44 +00:00
diondiondion
f091e7050c Fix gap in profile fields layout (#38604) 2026-04-08 10:24:42 +00:00
renovate[bot]
890452f54a fix(deps): update dependency @vitejs/plugin-react to v6 (#38180)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-08 09:32:45 +00:00
renovate[bot]
7b0da9bb4e chore(deps): update dependency addressable to v2.9.0 [security] (#38600)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-08 09:30:54 +00:00
renovate[bot]
b4d597af93 chore(deps): update dependency dotenv to v17.4.1 (#38524)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-08 09:23:09 +00:00
renovate[bot]
efea53e7a3 chore(deps): update dependency sass to v1.99.0 (#38554)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-08 09:21:08 +00:00
renovate[bot]
c155e0d58b chore(deps): update dependency jsdom to v29.0.2 (#38574)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-08 09:21:03 +00:00
Matt Jankowski
4299e33389 Update vite to version 8.0.5 (#38591)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-08 09:04:16 +00:00
renovate[bot]
f597589695 chore(deps): update dependency lodash to v4.18.1 [security] (#38526)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-08 08:53:02 +00:00
github-actions[bot]
de86ad56e5 New Crowdin Translations (automated) (#38597)
Co-authored-by: GitHub Actions <noreply@github.com>
2026-04-08 08:37:51 +00:00
Echo
e4e7e679b3 Profile editing: Removes old profile editing page (#38584) 2026-04-07 13:52:04 +00:00
Echo
db13dddcf9 Profile redesign: Changes verified field border colors (#38585) 2026-04-07 13:51:07 +00:00
Eugen Rochko
927c7d747f Change design of e-mail subscription form (#38582)
Co-authored-by: diondiondion <mail@diondiondion.com>
2026-04-07 13:31:59 +00:00
Echo
8e212fca59 Fix importing emoji loader statically inside worker (#38541) 2026-04-07 13:17:07 +00:00
Echo
31d2885d95 Profile editing: Adds bot toggle (#38581) 2026-04-07 12:14:50 +00:00
Echo
85fb9218a7 Profile editing: Fix regression with adding tags (#38580) 2026-04-07 12:05:06 +00:00
Echo
ed6ceda71d Profile redesign: Handle + tab changes (#38579) 2026-04-07 11:21:39 +00:00
Matt Jankowski
9fdc8246f2 Use consistent style in *Filter classes to skip pagination (#38559) 2026-04-07 09:59:36 +00:00
Echo
f2f07404b5 Profile redesign: Persist filter setting (#38575) 2026-04-07 09:58:57 +00:00
Matt Jankowski
71e6e50846 Simplify media attachment lookup in show/player actions (#38565) 2026-04-07 09:58:37 +00:00
renovate[bot]
4633b97c55 chore(deps): update dependency test-prof to v1.6.1 (#38545)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-07 09:35:42 +00:00
github-actions[bot]
0a64bcae63 New Crowdin Translations (automated) (#38564)
Co-authored-by: GitHub Actions <noreply@github.com>
2026-04-07 09:34:30 +00:00
Matt Jankowski
8fa91b4b81 Use partial to render settings/featured_tags (#36174) 2026-04-03 16:29:00 +00:00
Matt Jankowski
621628e2cf Clarify that language filter does not impact home/lists (#38490)
Co-authored-by: Brendan Jones <16049594+brendanjones@users.noreply.github.com>
2026-04-03 14:46:29 +00:00
Matt Jankowski
ee69290003 Use collection partial for "software updates" list in admin area (#38550) 2026-04-03 13:28:50 +00:00
Echo
759e97fd36 Profile redesign: Adds a "Follows you" badge (#38549) 2026-04-03 09:39:29 +00:00
github-actions[bot]
627023b452 New Crowdin Translations (automated) (#38553)
Co-authored-by: GitHub Actions <noreply@github.com>
2026-04-03 09:36:56 +00:00
Matt Jankowski
83ac8979c2 Use rubocop --format github instead of problem matcher (#38519) 2026-04-03 08:44:57 +00:00
Matt Jankowski
671568aec9 Use "batch" concern in admin routes (#38551) 2026-04-03 08:37:41 +00:00
Matt Jankowski
47f58212de Use approvable concern for repeated API approve/reject routes (#38542) 2026-04-02 16:11:37 +00:00
947 changed files with 22810 additions and 13311 deletions

View File

@@ -1,6 +1,6 @@
defaults
> 0.2%
firefox >= 78
> 0.2% and not ios < 15.6
firefox >= 91
ios >= 15.6
not dead
not OperaMini all

View File

@@ -59,7 +59,7 @@ body:
Any additional technical details you may have, like logs or error traces
value: |
If this is happening on your own Mastodon server, please fill out those:
- Ruby version: (from `ruby --version`, eg. v4.0.2)
- Ruby version: (from `ruby --version`, eg. v4.0.5)
- Node.js version: (from `node --version`, eg. v22.16.0)
validations:
required: false

View File

@@ -61,7 +61,7 @@ body:
value: |
Please at least include those informations:
- Operating system: (eg. Ubuntu 24.04.2)
- Ruby version: (from `ruby --version`, eg. v4.0.2)
- Ruby version: (from `ruby --version`, eg. v4.0.5)
- Node.js version: (from `node --version`, eg. v22.16.0)
validations:
required: false

View File

@@ -9,21 +9,21 @@ runs:
using: 'composite'
steps:
- name: Set up Node.js
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version-file: '.nvmrc'
# The following is needed because we can not use `cache: true` for `setup-node`, as it does not support Corepack yet and mess up with the cache location if ran after Node is installed
- name: Enable corepack
shell: bash
run: corepack enable
run: npm i -g corepack
- name: Get yarn cache directory path
id: yarn-cache-dir-path
shell: bash
run: echo "dir=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT
- uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5
- uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}

View File

@@ -14,10 +14,16 @@ runs:
shell: bash
run: |
sudo apt-get update
sudo apt-get install --no-install-recommends -y libicu-dev libidn11-dev libvips42 ${{ inputs.additional-system-dependencies }}
sudo apt-get install --no-install-recommends -y \
libicu-dev \
libidn11-dev \
libvips42 \
libheif-plugin-aomdec \
libheif-plugin-libde265 \
${{ inputs.additional-system-dependencies }}
- name: Set up Ruby
uses: ruby/setup-ruby@c984c1a20bb35a1cbda04477c816cea024418be9 # v1
uses: ruby/setup-ruby@afeafc3d1ab54a631816aba4c914a0081c12ff2f # v1
with:
ruby-version: ${{ inputs.ruby-version }}
bundler-cache: true

View File

@@ -76,7 +76,7 @@ jobs:
- name: Build and push by digest
id: build
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
with:
context: .
file: ${{ inputs.file_to_build }}
@@ -87,8 +87,8 @@ jobs:
platforms: ${{ matrix.platform }}
provenance: false
push: ${{ inputs.push_to_images != '' }}
cache-from: ${{ inputs.cache && 'type=gha' || '' }}
cache-to: ${{ inputs.cache && 'type=gha,mode=max' || '' }}
cache-from: ${{ inputs.cache && format('type=gha,scope=build-{0}-{1}', hashFiles(inputs.file_to_build), env.PLATFORM_PAIR) || '' }}
cache-to: ${{ inputs.cache && format('type=gha,mode=max,scope=build-{0}-{1}', hashFiles(inputs.file_to_build), env.PLATFORM_PAIR) || '' }}
outputs: type=image,"name=${{ env.IMAGE_NAMES }}",push-by-digest=true,name-canonical=true,push=${{ inputs.push_to_images != '' }}
- name: Export digest

View File

@@ -31,7 +31,7 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Set up Ruby
uses: ruby/setup-ruby@c984c1a20bb35a1cbda04477c816cea024418be9 # v1
uses: ruby/setup-ruby@afeafc3d1ab54a631816aba4c914a0081c12ff2f # v1
with:
bundler-cache: true

View File

@@ -20,7 +20,7 @@ jobs:
with:
fetch-depth: 0
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3
- uses: dorny/paths-filter@d1c1ffe0248fe513906c8e24db8ea791d46f8590 # v3
id: filter
with:
filters: |

View File

@@ -35,7 +35,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4
uses: github/codeql-action/init@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -48,7 +48,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4
uses: github/codeql-action/autobuild@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4
# Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
@@ -61,6 +61,6 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4
uses: github/codeql-action/analyze@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4
with:
category: '/language:${{matrix.language}}'

View File

@@ -24,7 +24,7 @@ jobs:
# Download the translation files from Crowdin
- name: crowdin action
uses: crowdin/github-action@b4b468cffefb50bdd99dd83e5d2eaeb63c880380 # v2
uses: crowdin/github-action@8868a33591d21088edfc398968173a3b98d51706 # v2
with:
upload_sources: false
upload_translations: false

View File

@@ -26,7 +26,7 @@ jobs:
# Download the translation files from Crowdin
- name: crowdin action
uses: crowdin/github-action@b4b468cffefb50bdd99dd83e5d2eaeb63c880380 # v2
uses: crowdin/github-action@8868a33591d21088edfc398968173a3b98d51706 # v2
with:
upload_sources: false
upload_translations: false
@@ -52,7 +52,7 @@ jobs:
# Create or update the pull request
- name: Create Pull Request
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8
with:
commit-message: 'New Crowdin translations'
title: 'New Crowdin Translations (automated)'

View File

@@ -26,7 +26,7 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: crowdin action
uses: crowdin/github-action@b4b468cffefb50bdd99dd83e5d2eaeb63c880380 # v2
uses: crowdin/github-action@8868a33591d21088edfc398968173a3b98d51706 # v2
with:
upload_sources: true
upload_translations: false

View File

@@ -13,7 +13,6 @@ on:
- '**/*.css'
- '**/*.scss'
- '.github/workflows/lint-css.yml'
- '.github/stylelint-matcher.json'
pull_request:
paths:
@@ -24,7 +23,6 @@ on:
- '**/*.css'
- '**/*.scss'
- '.github/workflows/lint-css.yml'
- '.github/stylelint-matcher.json'
jobs:
lint:

View File

@@ -6,7 +6,6 @@ on:
- 'main'
- 'stable-*'
paths:
- '.github/workflows/haml-lint-problem-matcher.json'
- '.github/workflows/lint-haml.yml'
- '.haml-lint*.yml'
- '.rubocop*.yml'
@@ -16,7 +15,6 @@ on:
pull_request:
paths:
- '.github/workflows/haml-lint-problem-matcher.json'
- '.github/workflows/lint-haml.yml'
- '.haml-lint*.yml'
- '.rubocop*.yml'
@@ -36,7 +34,7 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Set up Ruby
uses: ruby/setup-ruby@c984c1a20bb35a1cbda04477c816cea024418be9 # v1
uses: ruby/setup-ruby@afeafc3d1ab54a631816aba4c914a0081c12ff2f # v1
with:
bundler-cache: true

View File

@@ -10,7 +10,6 @@ on:
- '.rubocop*.yml'
- '.ruby-version'
- 'bin/rubocop'
- 'config/brakeman.ignore'
- '**/*.rb'
- '**/*.rake'
- '.github/workflows/lint-ruby.yml'
@@ -21,7 +20,6 @@ on:
- '.rubocop*.yml'
- '.ruby-version'
- 'bin/rubocop'
- 'config/brakeman.ignore'
- '**/*.rb'
- '**/*.rake'
- '.github/workflows/lint-ruby.yml'
@@ -38,15 +36,12 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Set up Ruby
uses: ruby/setup-ruby@c984c1a20bb35a1cbda04477c816cea024418be9 # v1
uses: ruby/setup-ruby@afeafc3d1ab54a631816aba4c914a0081c12ff2f # v1
with:
bundler-cache: true
- name: Set-up RuboCop Problem Matcher
uses: r7kamura/rubocop-problem-matchers-action@59f1a0759f50cc2649849fd850b8487594bb5a81 # v1.2.2
- name: Run rubocop
run: bin/rubocop
run: bin/rubocop --format github
- name: Run brakeman
if: always() # Run both checks, even if the first failed

View File

@@ -43,7 +43,7 @@ jobs:
onlyProduction: 'true'
- name: Cache assets from compilation
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with:
path: |
public/assets
@@ -151,7 +151,7 @@ jobs:
bin/flatware fan bin/rails db:test:prepare
- name: Cache RSpec persistence file
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with:
path: |
tmp/rspec/examples.txt
@@ -163,11 +163,11 @@ jobs:
rspec-persistence-main
rspec-persistence
- run: bin/flatware rspec -r ./spec/flatware_helper.rb
- run: bin/flatware rspec
- name: Upload coverage reports to Codecov
if: matrix.ruby-version == '.ruby-version'
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6
with:
files: coverage/lcov/*.lcov
env:
@@ -247,7 +247,7 @@ jobs:
- name: Cache Playwright Chromium browser
id: playwright-cache
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with:
path: ~/.cache/ms-playwright
key: playwright-browsers-${{ runner.os }}-${{ hashFiles('yarn.lock') }}

2
.nvmrc
View File

@@ -1 +1 @@
24.14
24.15

View File

@@ -1 +1 @@
4.0.2
4.0.5

26
.simplecov Normal file
View File

@@ -0,0 +1,26 @@
# frozen_string_literal: true
SimpleCov.start 'rails' do
# During parallel runs, ensure unique names for post-run merge
command_name "job-#{ENV['TEST_ENV_NUMBER']}" if ENV['TEST_ENV_NUMBER']
if ENV['CI']
require 'simplecov-lcov'
formatter SimpleCov::Formatter::LcovFormatter
formatter.config.report_with_single_file = true
else
formatter SimpleCov::Formatter::HTMLFormatter
end
enable_coverage :branch
add_filter 'lib/linter'
add_group 'Libraries', 'lib'
add_group 'Policies', 'app/policies'
add_group 'Presenters', 'app/presenters'
add_group 'Search', 'app/chewy'
add_group 'Serializers', 'app/serializers'
add_group 'Services', 'app/services'
add_group 'Validators', 'app/validators'
end

View File

@@ -2,6 +2,39 @@
All notable changes to this project will be documented in this file.
## [4.5.10] - 2026-05-20
### Security
- Fix SSRF protection bypass ([GHSA-crr4-7rm4-8gpw](https://github.com/mastodon/mastodon/security/advisories/GHSA-crr4-7rm4-8gpw), [GHSA-xx55-4rrg-8xg6](https://github.com/mastodon/mastodon/security/advisories/GHSA-xx55-4rrg-8xg6))
- Fix Linked-Data Signature bypass through JSON-LD graph restructuring features ([GHSA-53m7-2wrh-q839](https://github.com/mastodon/mastodon/security/advisories/GHSA-53m7-2wrh-q839), [GHSA-chgx-jx3p-rf73](https://github.com/mastodon/mastodon/security/advisories/GHSA-chgx-jx3p-rf73))
- Updated dependencies
### Fixed
- Fix type of `interactingObject`, `interactionTarget` and add missing `QuoteAuthorization` (#38940 by @ClearlyClaire)
### Removed
- Remove unused devise strategies (#38795 by @ClearlyClaire)
## [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

@@ -13,7 +13,7 @@ ARG BASE_REGISTRY="docker.io"
# Ruby image to use for base image, change with [--build-arg RUBY_VERSION="4.0.x"]
# renovate: datasource=docker depName=docker.io/ruby
ARG RUBY_VERSION="4.0.2"
ARG RUBY_VERSION="4.0.5"
# # Node.js version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="22"]
# renovate: datasource=node-version depName=node
ARG NODE_MAJOR_VERSION="24"
@@ -25,8 +25,8 @@ FROM ${BASE_REGISTRY}/node:${NODE_MAJOR_VERSION}-${DEBIAN_VERSION}-slim AS node
FROM ${BASE_REGISTRY}/ruby:${RUBY_VERSION}-slim-${DEBIAN_VERSION} AS ruby
# Resulting version string is vX.X.X-MASTODON_VERSION_PRERELEASE+MASTODON_VERSION_METADATA
# Example: v4.3.0-nightly.2023.11.09+pr-123456
# Overwrite existence of 'alpha.X' in version.rb [--build-arg MASTODON_VERSION_PRERELEASE="nightly.2023.11.09"]
# Example: v4.3.0-nightly.2023-11-09+pr-123456
# Overwrite existence of 'alpha.X' in version.rb [--build-arg MASTODON_VERSION_PRERELEASE="nightly.2023-11-09"]
ARG MASTODON_VERSION_PRERELEASE=""
# Append build metadata or fork information to version.rb [--build-arg MASTODON_VERSION_METADATA="pr-123456"]
ARG MASTODON_VERSION_METADATA=""
@@ -48,29 +48,27 @@ ARG GID="991"
# Apply Mastodon build options based on options above
ENV \
# Apply Mastodon version information
MASTODON_VERSION_PRERELEASE="${MASTODON_VERSION_PRERELEASE}" \
MASTODON_VERSION_METADATA="${MASTODON_VERSION_METADATA}" \
SOURCE_COMMIT="${SOURCE_COMMIT}" \
# Apply Mastodon static files and YJIT options
RAILS_SERVE_STATIC_FILES=${RAILS_SERVE_STATIC_FILES} \
RUBY_YJIT_ENABLE=${RUBY_YJIT_ENABLE} \
# Apply timezone
TZ=${TZ}
RAILS_SERVE_STATIC_FILES="${RAILS_SERVE_STATIC_FILES}" \
RUBY_YJIT_ENABLE="${RUBY_YJIT_ENABLE}" \
TZ="${TZ}"
# Configure runtime environment
# BIND: IP to bind Mastodon to when serving traffic
# NODE_ENV/RAILS_ENV: production settings for Node.js and Ruby on Rails
# DEBIAN_FRONTEND: suppress interactive prompts
# PATH: add Ruby and Mastodon installation directories
# MALLOC_CONF: optimize jemalloc 5.x performance
# MASTODON_SIDEKIQ_READY_FILENAME: Sidekiq readiness check filename for Kubernetes
ENV \
# Configure the IP to bind Mastodon to when serving traffic
BIND="0.0.0.0" \
# Use production settings for Yarn, Node.js and related tools
NODE_ENV="production" \
# Use production settings for Ruby on Rails
RAILS_ENV="production" \
# Add Ruby and Mastodon installation to the PATH
DEBIAN_FRONTEND="noninteractive" \
PATH="${PATH}:/opt/ruby/bin:/opt/mastodon/bin" \
# Optimize jemalloc 5.x performance
MALLOC_CONF="narenas:2,background_thread:true,thp:never,dirty_decay_ms:1000,muzzy_decay_ms:0" \
# Sidekiq will touch tmp/sidekiq_process_has_started_and_will_begin_processing_jobs to indicate it is ready. This can be used for a readiness check in Kubernetes
MASTODON_SIDEKIQ_READY_FILENAME=sidekiq_process_has_started_and_will_begin_processing_jobs
# Set default shell used for running commands
@@ -99,10 +97,10 @@ RUN \
# Mount Apt cache and lib directories from Docker buildx caches
--mount=type=cache,id=apt-cache-${TARGETPLATFORM},target=/var/cache/apt,sharing=locked \
--mount=type=cache,id=apt-lib-${TARGETPLATFORM},target=/var/lib/apt,sharing=locked \
# Apt update & upgrade to check for security updates to Debian image
# Update package list and upgrade system packages
apt-get update; \
apt-get dist-upgrade -yq; \
# Install jemalloc, curl and other necessary components
# Install jemalloc and other necessary components
apt-get install -y --no-install-recommends \
curl \
file \
@@ -112,6 +110,42 @@ RUN \
tini \
tzdata \
wget \
# Mastodon components
libexpat1 \
libglib2.0-0t64 \
libicu76 \
libidn12 \
libpq5 \
libreadline8t64 \
libssl3t64 \
libyaml-0-2 \
# libvips components
libcgif0 \
libexif12 \
libheif1 \
libhwy1t64 \
libimagequant0 \
libjpeg62-turbo \
liblcms2-2 \
libspng0 \
libtiff6 \
libwebp7 \
libwebpdemux2 \
libwebpmux3 \
# ffmpeg components
libdav1d7 \
libmp3lame0 \
libopencore-amrnb0 \
libopencore-amrwb0 \
libopus0 \
libsnappy1v5 \
libtheora0 \
libvorbis0a \
libvorbisenc2 \
libvorbisfile3 \
libvpx9 \
libx264-164 \
libx265-215 \
; \
# Patch Ruby to use jemalloc
patchelf --add-needed libjemalloc.so.2 /usr/local/bin/ruby; \
@@ -120,42 +154,37 @@ RUN \
patchelf \
;
# Create temporary build layer from base image
FROM ruby AS build
# Build stage for media libraries (libvips, ffmpeg)
FROM ${BASE_REGISTRY}/ruby:${RUBY_VERSION}-slim-${DEBIAN_VERSION} AS media-build
ARG TARGETPLATFORM
# Set default shell used for running commands
SHELL ["/bin/bash", "-o", "pipefail", "-o", "errexit", "-c"]
# hadolint ignore=DL3008
RUN \
# Mount Apt cache and lib directories from Docker buildx caches
--mount=type=cache,id=apt-cache-${TARGETPLATFORM},target=/var/cache/apt,sharing=locked \
--mount=type=cache,id=apt-lib-${TARGETPLATFORM},target=/var/lib/apt,sharing=locked \
# Install build tools and bundler dependencies from APT
--mount=type=cache,id=apt-native-cache-${TARGETPLATFORM},target=/var/cache/apt,sharing=locked \
--mount=type=cache,id=apt-native-lib-${TARGETPLATFORM},target=/var/lib/apt,sharing=locked \
# Remove automatic apt cache Docker cleanup scripts
rm -f /etc/apt/apt.conf.d/docker-clean; \
# Install build tools for native libraries
apt-get update; \
apt-get install -y --no-install-recommends \
autoconf \
automake \
build-essential \
cmake \
git \
libgdbm-dev \
libglib2.0-dev \
libgmp-dev \
libicu-dev \
libidn-dev \
libpq-dev \
libssl-dev \
libtool \
libyaml-dev \
meson \
nasm \
pkg-config \
shared-mime-info \
xz-utils \
# libvips components
libcgif-dev \
libexif-dev \
libexpat1-dev \
libgirepository1.0-dev \
libglib2.0-dev \
libheif-dev \
libhwy-dev \
libimagequant-dev \
@@ -176,8 +205,8 @@ RUN \
libx265-dev \
;
# Create temporary libvips specific build layer from build layer
FROM build AS libvips
# Create temporary libvips specific build layer
FROM media-build AS libvips
# libvips version to compile, change with [--build-arg VIPS_VERSION="8.15.2"]
# renovate: datasource=github-releases depName=libvips packageName=libvips/libvips
@@ -192,19 +221,20 @@ RUN tar xf vips-${VIPS_VERSION}.tar.xz;
WORKDIR /usr/local/libvips/src/vips-${VIPS_VERSION}
# Configure and compile libvips
RUN \
meson setup build --prefix /usr/local/libvips --libdir=lib -Ddeprecated=false -Dintrospection=disabled -Dmodules=disabled -Dexamples=false; \
cd build; \
ninja; \
ninja install;
# Configure libvips
RUN meson setup build --prefix /usr/local/libvips --libdir=lib -Ddeprecated=false -Dintrospection=disabled -Dmodules=disabled -Dexamples=false
# Create temporary ffmpeg specific build layer from build layer
FROM build AS ffmpeg
WORKDIR /usr/local/libvips/src/vips-${VIPS_VERSION}/build
# Compile and install libvips
RUN ninja && ninja install
# Create temporary ffmpeg specific build layer
FROM media-build AS ffmpeg
# ffmpeg version to compile, change with [--build-arg FFMPEG_VERSION="7.0.x"]
# renovate: datasource=repology depName=ffmpeg packageName=openpkg_current/ffmpeg
ARG FFMPEG_VERSION=8.1
# renovate: datasource=github-tags depName=FFmpeg/FFmpeg extractVersion=^n(?<version>\d+\.\d+(\.\d+)?)$
ARG FFMPEG_VERSION=8.1.1
# ffmpeg download URL, change with [--build-arg FFMPEG_URL="https://ffmpeg.org/releases"]
ARG FFMPEG_URL=https://github.com/FFmpeg/FFmpeg/archive/refs/tags
@@ -241,17 +271,48 @@ RUN \
--enable-shared \
--enable-version3 \
; \
make -j$(nproc); \
make -j"$(nproc)"; \
make install;
# Create temporary build layer from base image for Ruby dependencies
FROM ruby AS ruby-build
ARG TARGETPLATFORM
# hadolint ignore=DL3008
RUN \
# Mount Apt cache and lib directories from Docker buildx caches
--mount=type=cache,id=apt-cache-${TARGETPLATFORM},target=/var/cache/apt,sharing=locked \
--mount=type=cache,id=apt-lib-${TARGETPLATFORM},target=/var/lib/apt,sharing=locked \
# Install build tools and bundler dependencies from APT
apt-get install -y --no-install-recommends \
build-essential \
git \
libgdbm-dev \
libgmp-dev \
libicu-dev \
libidn-dev \
libpq-dev \
libssl-dev \
libyaml-dev \
shared-mime-info \
zlib1g-dev \
;
# Create temporary bundler specific build layer from build layer
FROM build AS bundler
FROM ruby-build AS bundler
ARG TARGETPLATFORM
# Copy Gemfile config into working directory
COPY Gemfile* /opt/mastodon/
# Copy libvips for gems that need it during install
COPY --from=libvips /usr/local/libvips/lib /usr/local/lib
COPY --from=libvips /usr/local/libvips/include /usr/local/include
RUN ldconfig
RUN \
# Mount Ruby Gem caches
--mount=type=cache,id=gem-cache-${TARGETPLATFORM},target=/usr/local/bundle/cache/,sharing=locked \
@@ -267,7 +328,7 @@ RUN \
bundle install -j"$(nproc)";
# Create temporary assets build layer from build layer
FROM build AS precompiler
FROM ruby-build AS precompiler
ARG TARGETPLATFORM
@@ -279,10 +340,13 @@ COPY --from=node /usr/local/bin /usr/local/bin
COPY --from=node /usr/local/lib /usr/local/lib
RUN \
# Configure Corepack
rm /usr/local/bin/yarn*; \
corepack enable; \
corepack prepare --activate;
# Mount local Corepack and Yarn caches from Docker buildx caches
--mount=type=cache,id=corepack-cache-${TARGETPLATFORM},target=/usr/local/share/.cache/corepack,sharing=locked \
--mount=type=cache,id=yarn-cache-${TARGETPLATFORM},target=/usr/local/share/.cache/yarn,sharing=locked \
# Remove pre-installed Yarn binaries (only present on Node <26)
rm -f /usr/local/bin/yarn*; \
# Install Corepack
npm i -g corepack;
# hadolint ignore=DL3008
RUN \
@@ -311,53 +375,6 @@ FROM ruby AS mastodon
ARG TARGETPLATFORM
# hadolint ignore=DL3008
RUN \
# Mount Apt cache and lib directories from Docker buildx caches
--mount=type=cache,id=apt-cache-${TARGETPLATFORM},target=/var/cache/apt,sharing=locked \
--mount=type=cache,id=apt-lib-${TARGETPLATFORM},target=/var/lib/apt,sharing=locked \
# Mount Corepack and Yarn caches from Docker buildx caches
--mount=type=cache,id=corepack-cache-${TARGETPLATFORM},target=/usr/local/share/.cache/corepack,sharing=locked \
--mount=type=cache,id=yarn-cache-${TARGETPLATFORM},target=/usr/local/share/.cache/yarn,sharing=locked \
# Apt update install non-dev versions of necessary components
apt-get install -y --no-install-recommends \
libexpat1 \
libglib2.0-0t64 \
libicu76 \
libidn12 \
libpq5 \
libreadline8t64 \
libssl3t64 \
libyaml-0-2 \
# libvips components
libcgif0 \
libexif12 \
libheif1 \
libhwy1t64 \
libimagequant0 \
libjpeg62-turbo \
liblcms2-2 \
libspng0 \
libtiff6 \
libwebp7 \
libwebpdemux2 \
libwebpmux3 \
# ffmpeg components
libdav1d7 \
libmp3lame0 \
libopencore-amrnb0 \
libopencore-amrwb0 \
libopus0 \
libsnappy1v5 \
libtheora0 \
libvorbis0a \
libvorbisenc2 \
libvorbisfile3 \
libvpx9 \
libx264-164 \
libx265-215 \
;
# Copy Mastodon sources into final layer
COPY . /opt/mastodon/

38
Gemfile
View File

@@ -4,7 +4,7 @@ source 'https://rubygems.org'
ruby '>= 3.3.0', '< 4.1.0'
gem 'propshaft'
gem 'puma', '~> 7.0'
gem 'puma'
gem 'rails', '~> 8.1.0'
gem 'thor', '~> 1.2'
@@ -50,7 +50,6 @@ gem 'doorkeeper', '~> 5.6'
gem 'faraday-httpclient'
gem 'fast_blank', '~> 1.0'
gem 'fastimage'
gem 'hiredis', '~> 0.6'
gem 'hiredis-client'
gem 'htmlentities', '~> 4.3'
gem 'http', '~> 5.3.0'
@@ -59,6 +58,7 @@ gem 'httplog', '~> 1.8.0', require: false
gem 'i18n'
gem 'idn-ruby', require: 'idn'
gem 'inline_svg'
gem 'ipaddr', '~> 1.2'
gem 'irb', '~> 1.8'
gem 'kaminari', '~> 1.2'
gem 'link_header', '~> 0.0'
@@ -76,7 +76,7 @@ gem 'rack-attack', '~> 6.6'
gem 'rack-cors', require: 'rack/cors'
gem 'rails-i18n', '~> 8.0'
gem 'redcarpet', '~> 3.6'
gem 'redis', '~> 4.5', require: ['redis', 'redis/connection/hiredis']
gem 'redis', '~> 5'
gem 'rqrcode', '~> 3.0'
gem 'ruby-progressbar', '~> 1.13'
gem 'sanitize', '~> 7.0'
@@ -102,23 +102,23 @@ gem 'rdf-normalize', '~> 0.5'
gem 'prometheus_exporter', '~> 2.2', require: false
gem 'opentelemetry-api', '~> 1.8.0'
gem 'opentelemetry-api', '~> 1.10.0'
group :opentelemetry do
gem 'opentelemetry-exporter-otlp', '~> 0.32.0', require: false
gem 'opentelemetry-instrumentation-active_job', '~> 0.10.0', require: false
gem 'opentelemetry-instrumentation-active_model_serializers', '~> 0.24.0', require: false
gem 'opentelemetry-instrumentation-concurrent_ruby', '~> 0.24.0', require: false
gem 'opentelemetry-instrumentation-excon', '~> 0.28.0', require: false
gem 'opentelemetry-instrumentation-faraday', '~> 0.32.0', require: false
gem 'opentelemetry-instrumentation-http', '~> 0.29.0', require: false
gem 'opentelemetry-instrumentation-http_client', '~> 0.28.0', require: false
gem 'opentelemetry-instrumentation-net_http', '~> 0.28.0', require: false
gem 'opentelemetry-instrumentation-pg', '~> 0.35.0', require: false
gem 'opentelemetry-instrumentation-rack', '~> 0.30.0', require: false
gem 'opentelemetry-instrumentation-rails', '~> 0.40.0', require: false
gem 'opentelemetry-instrumentation-redis', '~> 0.28.0', require: false
gem 'opentelemetry-instrumentation-sidekiq', '~> 0.28.0', require: false
gem 'opentelemetry-exporter-otlp', '~> 0.34.0', require: false
gem 'opentelemetry-instrumentation-active_job', '~> 0.12.0', require: false
gem 'opentelemetry-instrumentation-active_model_serializers', '~> 0.25.0', require: false
gem 'opentelemetry-instrumentation-concurrent_ruby', '~> 0.25.0', require: false
gem 'opentelemetry-instrumentation-excon', '~> 0.29.0', require: false
gem 'opentelemetry-instrumentation-faraday', '~> 0.33.0', require: false
gem 'opentelemetry-instrumentation-http', '~> 0.30.0', require: false
gem 'opentelemetry-instrumentation-http_client', '~> 0.29.0', require: false
gem 'opentelemetry-instrumentation-net_http', '~> 0.29.0', require: false
gem 'opentelemetry-instrumentation-pg', '~> 0.36.0', require: false
gem 'opentelemetry-instrumentation-rack', '~> 0.31.0', require: false
gem 'opentelemetry-instrumentation-rails', '~> 0.42.0', require: false
gem 'opentelemetry-instrumentation-redis', '~> 0.29.0', require: false
gem 'opentelemetry-instrumentation-sidekiq', '~> 0.29.0', require: false
gem 'opentelemetry-sdk', '~> 1.4', require: false
end
@@ -135,7 +135,7 @@ group :test do
# Browser integration testing
gem 'capybara', '~> 3.39'
gem 'capybara-playwright-driver'
gem 'playwright-ruby-client', '1.57.1', require: false # Pinning the exact version as it needs to be kept in sync with the installed npm package
gem 'playwright-ruby-client', '1.59.1', require: false # Pinning the exact version as it needs to be kept in sync with the installed npm package
# Used to reset the database between system tests
gem 'database_cleaner-active_record'

View File

@@ -89,7 +89,7 @@ GEM
securerandom (>= 0.3)
tzinfo (~> 2.0, >= 2.0.5)
uri (>= 0.13.1)
addressable (2.8.9)
addressable (2.9.0)
public_suffix (>= 2.0.2, < 8.0)
aes_key_wrap (1.1.0)
android_key_attestation (0.3.0)
@@ -99,8 +99,8 @@ GEM
ast (2.4.3)
attr_required (1.0.2)
aws-eventstream (1.4.0)
aws-partitions (1.1227.0)
aws-sdk-core (3.244.0)
aws-partitions (1.1249.0)
aws-sdk-core (3.247.0)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
@@ -108,11 +108,11 @@ GEM
bigdecimal
jmespath (~> 1, >= 1.6.1)
logger
aws-sdk-kms (1.123.0)
aws-sdk-core (~> 3, >= 3.244.0)
aws-sdk-kms (1.125.0)
aws-sdk-core (~> 3, >= 3.247.0)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.217.0)
aws-sdk-core (~> 3, >= 3.244.0)
aws-sdk-s3 (1.222.0)
aws-sdk-core (~> 3, >= 3.247.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
aws-sigv4 (1.12.1)
@@ -132,7 +132,7 @@ GEM
binding_of_caller (2.0.0)
debug_inspector (>= 1.2.0)
blurhash (0.1.8)
bootsnap (1.23.0)
bootsnap (1.24.4)
msgpack (~> 1.2)
brakeman (8.0.4)
racc
@@ -156,7 +156,7 @@ GEM
playwright-ruby-client (>= 1.16.0)
case_transform (0.2)
activesupport
cbor (0.5.10.1)
cbor (0.5.10.2)
cgi (0.5.1)
charlock_holmes (0.7.9)
chewy (8.0.1)
@@ -178,7 +178,7 @@ GEM
bigdecimal
rexml
crass (1.0.6)
css_parser (1.21.1)
css_parser (2.1.0)
addressable
csv (3.3.5)
database_cleaner-active_record (2.2.2)
@@ -190,7 +190,7 @@ GEM
irb (~> 1.10)
reline (>= 0.3.8)
debug_inspector (1.2.0)
devise (5.0.3)
devise (5.0.4)
bcrypt (~> 3.0)
orm_adapter (~> 0.1)
railties (>= 7.0)
@@ -214,7 +214,7 @@ GEM
dotenv (3.2.0)
drb (2.2.3)
dry-cli (1.4.1)
elastic-transport (8.4.1)
elastic-transport (8.5.1)
faraday (< 3)
multi_json
elasticsearch (8.19.3)
@@ -226,14 +226,14 @@ GEM
elasticsearch-dsl (0.1.10)
email_validator (2.2.4)
activemodel
erb (6.0.2)
erb (6.0.4)
erubi (1.13.1)
et-orbi (1.4.0)
tzinfo
excon (1.4.0)
excon (1.4.2)
logger
fabrication (3.0.0)
faker (3.6.1)
faker (3.8.0)
i18n (>= 1.8.11, < 2)
faraday (2.14.1)
faraday-net_http (>= 2.0, < 3.5)
@@ -247,7 +247,7 @@ GEM
net-http (~> 0.5)
fast_blank (1.0.1)
fastimage (2.4.1)
ffi (1.17.3)
ffi (1.17.4)
ffi-compiler (1.3.2)
ffi (>= 1.15.5)
rake
@@ -264,7 +264,7 @@ GEM
excon (~> 1.0)
formatador (>= 0.2, < 2.0)
mime-types
fog-json (1.2.0)
fog-json (1.3.0)
fog-core
multi_json (~> 1.10)
fog-openstack (1.1.5)
@@ -278,7 +278,7 @@ GEM
raabro (~> 1.4)
globalid (1.3.0)
activesupport (>= 6.1)
google-protobuf (4.34.0)
google-protobuf (4.34.1)
bigdecimal
rake (~> 13.3)
googleapis-common-protos-types (1.22.0)
@@ -292,9 +292,9 @@ GEM
activesupport (>= 5.1)
haml (>= 4.0.6)
railties (>= 5.1)
haml_lint (0.72.0)
haml_lint (0.73.0)
haml (>= 5.0)
parallel (~> 1.10)
parallel (>= 1.10)
rainbow
rubocop (>= 1.0)
sysexits (~> 1.1)
@@ -305,9 +305,8 @@ GEM
json
highline (3.1.2)
reline
hiredis (0.6.3)
hiredis-client (0.28.0)
redis-client (= 0.28.0)
hiredis-client (0.29.0)
redis-client (= 0.29.0)
hkdf (0.3.0)
htmlentities (4.4.2)
http (5.3.1)
@@ -315,7 +314,7 @@ GEM
http-cookie (~> 1.0)
http-form_data (~> 2.2)
llhttp-ffi (~> 0.5.0)
http-cookie (1.1.0)
http-cookie (1.1.6)
domain_name (~> 0.5)
http-form_data (2.3.0)
http_accept_language (2.1.1)
@@ -344,7 +343,8 @@ GEM
activesupport (>= 3.0)
nokogiri (>= 1.6)
io-console (0.8.2)
irb (1.17.0)
ipaddr (1.2.9)
irb (1.18.0)
pp (>= 0.6.0)
prism (>= 1.3.0)
rdoc (>= 4.0.0)
@@ -354,7 +354,7 @@ GEM
azure-blob (~> 0.5.2)
hashie (~> 5.0)
jmespath (1.6.2)
json (2.19.3)
json (2.19.5)
json-canonicalization (1.0.0)
json-jwt (1.17.0)
activesupport (>= 4.2)
@@ -412,15 +412,13 @@ GEM
rexml
link_header (0.0.8)
lint_roller (1.1.0)
linzer (0.7.8)
linzer (0.7.9)
cgi (>= 0.4.2, < 0.6.0)
forwardable (~> 1.3, >= 1.3.3)
logger (~> 1.7, >= 1.7.0)
net-http (>= 0.6, < 0.10)
openssl (>= 3, < 5)
rack (>= 2.2, < 4.0)
starry (~> 0.2)
stringio (~> 3.1, >= 3.1.2)
uri (~> 1.0, >= 1.0.2)
llhttp-ffi (0.5.1)
ffi-compiler (~> 1.0)
@@ -448,18 +446,18 @@ GEM
mime-types (3.7.0)
logger
mime-types-data (~> 3.2025, >= 3.2025.0507)
mime-types-data (3.2026.0317)
mime-types-data (3.2026.0414)
mini_mime (1.1.5)
mini_portile2 (2.8.9)
minitest (6.0.2)
minitest (6.0.6)
drb (~> 2.0)
prism (~> 1.5)
msgpack (1.8.0)
multi_json (1.19.1)
multi_json (1.20.1)
mutex_m (0.3.0)
net-http (0.6.0)
uri
net-imap (0.6.3)
net-imap (0.6.4)
date
net-protocol
net-ldap (0.20.0)
@@ -472,7 +470,7 @@ GEM
net-smtp (0.5.1)
net-protocol
nio4r (2.7.5)
nokogiri (1.19.2)
nokogiri (1.19.3)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
omniauth (2.1.4)
@@ -509,103 +507,104 @@ GEM
openssl (4.0.1)
openssl-signature_algorithm (1.3.0)
openssl (> 2.0)
opentelemetry-api (1.8.0)
opentelemetry-api (1.10.0)
logger
opentelemetry-common (0.23.0)
opentelemetry-common (0.25.0)
opentelemetry-api (~> 1.0)
opentelemetry-exporter-otlp (0.32.0)
opentelemetry-exporter-otlp (0.34.0)
google-protobuf (>= 3.18)
googleapis-common-protos-types (~> 1.3)
opentelemetry-api (~> 1.1)
opentelemetry-common (~> 0.20)
opentelemetry-sdk (~> 1.10)
opentelemetry-semantic_conventions
opentelemetry-helpers-sql (0.3.0)
opentelemetry-helpers-sql (0.4.0)
opentelemetry-api (~> 1.7)
opentelemetry-helpers-sql-processor (0.4.0)
opentelemetry-helpers-sql-processor (0.5.0)
opentelemetry-api (~> 1.0)
opentelemetry-common (~> 0.21)
opentelemetry-instrumentation-action_mailer (0.6.1)
opentelemetry-instrumentation-action_mailer (0.8.0)
opentelemetry-instrumentation-active_support (~> 0.10)
opentelemetry-instrumentation-action_pack (0.16.0)
opentelemetry-instrumentation-action_pack (0.18.0)
opentelemetry-instrumentation-rack (~> 0.29)
opentelemetry-instrumentation-action_view (0.11.2)
opentelemetry-instrumentation-action_view (0.13.0)
opentelemetry-instrumentation-active_support (~> 0.10)
opentelemetry-instrumentation-active_job (0.10.1)
opentelemetry-instrumentation-active_job (0.12.0)
opentelemetry-instrumentation-base (~> 0.25)
opentelemetry-instrumentation-active_model_serializers (0.24.0)
opentelemetry-instrumentation-active_model_serializers (0.25.0)
opentelemetry-instrumentation-active_support (>= 0.7.0)
opentelemetry-instrumentation-active_record (0.11.1)
opentelemetry-instrumentation-active_record (0.13.0)
opentelemetry-instrumentation-base (~> 0.25)
opentelemetry-instrumentation-active_storage (0.3.1)
opentelemetry-instrumentation-active_storage (0.5.0)
opentelemetry-instrumentation-active_support (~> 0.10)
opentelemetry-instrumentation-active_support (0.10.1)
opentelemetry-instrumentation-active_support (0.12.0)
opentelemetry-instrumentation-base (~> 0.25)
opentelemetry-instrumentation-base (0.25.0)
opentelemetry-instrumentation-base (0.26.0)
opentelemetry-api (~> 1.7)
opentelemetry-common (~> 0.21)
opentelemetry-registry (~> 0.1)
opentelemetry-instrumentation-concurrent_ruby (0.24.0)
opentelemetry-instrumentation-concurrent_ruby (0.25.0)
opentelemetry-instrumentation-base (~> 0.25)
opentelemetry-instrumentation-excon (0.28.0)
opentelemetry-instrumentation-excon (0.29.1)
opentelemetry-instrumentation-base (~> 0.25)
opentelemetry-instrumentation-faraday (0.32.0)
opentelemetry-instrumentation-faraday (0.33.0)
opentelemetry-instrumentation-base (~> 0.25)
opentelemetry-instrumentation-http (0.29.0)
opentelemetry-instrumentation-http (0.30.0)
opentelemetry-instrumentation-base (~> 0.25)
opentelemetry-instrumentation-http_client (0.28.0)
opentelemetry-instrumentation-http_client (0.29.0)
opentelemetry-instrumentation-base (~> 0.25)
opentelemetry-instrumentation-net_http (0.28.0)
opentelemetry-instrumentation-net_http (0.29.0)
opentelemetry-instrumentation-base (~> 0.25)
opentelemetry-instrumentation-pg (0.35.0)
opentelemetry-instrumentation-pg (0.36.0)
opentelemetry-helpers-sql
opentelemetry-helpers-sql-processor
opentelemetry-instrumentation-base (~> 0.25)
opentelemetry-instrumentation-rack (0.30.0)
opentelemetry-instrumentation-rack (0.31.0)
opentelemetry-instrumentation-base (~> 0.25)
opentelemetry-instrumentation-rails (0.40.0)
opentelemetry-instrumentation-action_mailer (~> 0.6)
opentelemetry-instrumentation-action_pack (~> 0.15)
opentelemetry-instrumentation-action_view (~> 0.11)
opentelemetry-instrumentation-active_job (~> 0.10)
opentelemetry-instrumentation-active_record (~> 0.11)
opentelemetry-instrumentation-active_storage (~> 0.3)
opentelemetry-instrumentation-active_support (~> 0.10)
opentelemetry-instrumentation-concurrent_ruby (~> 0.23)
opentelemetry-instrumentation-redis (0.28.0)
opentelemetry-instrumentation-rails (0.42.0)
opentelemetry-instrumentation-action_mailer (~> 0.7)
opentelemetry-instrumentation-action_pack (~> 0.17)
opentelemetry-instrumentation-action_view (~> 0.12)
opentelemetry-instrumentation-active_job (~> 0.11)
opentelemetry-instrumentation-active_record (~> 0.12)
opentelemetry-instrumentation-active_storage (~> 0.4)
opentelemetry-instrumentation-active_support (~> 0.11)
opentelemetry-instrumentation-concurrent_ruby (~> 0.25)
opentelemetry-instrumentation-redis (0.29.0)
opentelemetry-instrumentation-base (~> 0.25)
opentelemetry-instrumentation-sidekiq (0.28.1)
opentelemetry-instrumentation-sidekiq (0.29.0)
opentelemetry-instrumentation-base (~> 0.25)
opentelemetry-registry (0.4.0)
opentelemetry-registry (0.6.0)
opentelemetry-api (~> 1.1)
opentelemetry-sdk (1.10.0)
opentelemetry-sdk (1.12.0)
logger
opentelemetry-api (~> 1.1)
opentelemetry-common (~> 0.20)
opentelemetry-registry (~> 0.2)
opentelemetry-semantic_conventions
opentelemetry-semantic_conventions (1.36.0)
opentelemetry-semantic_conventions (1.37.1)
opentelemetry-api (~> 1.0)
orm_adapter (0.5.0)
ostruct (0.6.3)
ox (2.14.23)
ox (2.14.26)
bigdecimal (>= 3.0)
parallel (1.27.0)
parser (3.3.10.2)
parallel (1.28.0)
parser (3.3.11.1)
ast (~> 2.4.1)
racc
parslet (2.0.0)
pastel (0.8.0)
tty-color (~> 0.5)
pg (1.6.3)
pghero (3.7.0)
activerecord (>= 7.1)
playwright-ruby-client (1.57.1)
pghero (3.8.0)
activerecord (>= 7.2)
playwright-ruby-client (1.59.1)
base64
concurrent-ruby (>= 1.1.6)
mime-types (>= 3.0)
pp (0.6.3)
prettyprint
premailer (1.27.0)
premailer (1.29.0)
addressable
css_parser (>= 1.19.0)
htmlentities (>= 4.0.0)
@@ -617,7 +616,7 @@ GEM
prism (1.9.0)
prometheus_exporter (2.3.1)
webrick
propshaft (1.3.1)
propshaft (1.3.2)
actionpack (>= 7.0.0)
activesupport (>= 7.0.0)
rack
@@ -625,7 +624,7 @@ GEM
date
stringio
public_suffix (7.0.5)
puma (7.2.0)
puma (8.0.1)
nio4r (~> 2.0)
pundit (2.5.2)
activesupport (>= 3.0.0)
@@ -650,7 +649,7 @@ GEM
rack (>= 3.0.0, < 4)
rack-proxy (0.7.7)
rack
rack-session (2.1.1)
rack-session (2.1.2)
base64 (>= 0.1.0)
rack (>= 3.0.0)
rack-test (2.2.0)
@@ -691,7 +690,7 @@ GEM
tsort (>= 0.2)
zeitwerk (~> 2.6)
rainbow (3.1.1)
rake (13.3.1)
rake (13.4.2)
rdf (3.3.4)
bcp47_spec (~> 0.2)
bigdecimal (~> 3.1, >= 3.1.5)
@@ -708,10 +707,11 @@ GEM
readline (0.0.4)
reline
redcarpet (3.6.1)
redis (4.8.1)
redis-client (0.28.0)
redis (5.4.1)
redis-client (>= 0.22.0)
redis-client (0.29.0)
connection_pool
regexp_parser (2.11.3)
regexp_parser (2.12.0)
reline (0.6.3)
io-console (~> 0.5)
request_store (1.7.0)
@@ -769,10 +769,10 @@ GEM
rubocop-ast (1.49.1)
parser (>= 3.3.7.2)
prism (~> 1.7)
rubocop-capybara (2.22.1)
rubocop-capybara (2.23.0)
lint_roller (~> 1.1)
rubocop (~> 1.72, >= 1.72.1)
rubocop-i18n (3.2.3)
rubocop (~> 1.81)
rubocop-i18n (3.3.0)
lint_roller (~> 1.1)
rubocop (>= 1.72.1)
rubocop-performance (1.26.1)
@@ -802,7 +802,7 @@ GEM
ruby-vips (2.3.0)
ffi (~> 1.12)
logger
rubyzip (3.2.2)
rubyzip (3.3.0)
rufus-scheduler (3.9.2)
fugit (~> 1.1, >= 1.11.1)
safety_net_attestation (0.5.0)
@@ -816,15 +816,15 @@ GEM
securerandom (0.4.1)
shoulda-matchers (7.0.1)
activesupport (>= 7.1)
sidekiq (8.0.10)
connection_pool (>= 2.5.0)
json (>= 2.9.0)
logger (>= 1.6.2)
rack (>= 3.1.0)
redis-client (>= 0.23.2)
sidekiq (8.1.5)
connection_pool (>= 3.0.0)
json (>= 2.16.0)
logger (>= 1.7.0)
rack (>= 3.2.0)
redis-client (>= 0.29.0)
sidekiq-bulk (0.2.0)
sidekiq
sidekiq-scheduler (6.0.1)
sidekiq-scheduler (6.0.2)
rufus-scheduler (~> 3.2)
sidekiq (>= 7.3, < 9)
sidekiq-unique-jobs (8.0.13)
@@ -847,12 +847,12 @@ GEM
stackprof (0.2.28)
starry (0.2.0)
base64
stoplight (5.8.0)
stoplight (5.8.2)
concurrent-ruby
zeitwerk
stringio (3.2.0)
strong_migrations (2.5.2)
activerecord (>= 7.1)
strong_migrations (2.8.0)
activerecord (>= 7.2)
swd (2.0.3)
activesupport (>= 3)
attr_required (>= 0.0.5)
@@ -864,7 +864,7 @@ GEM
unicode-display_width (>= 1.1.1, < 4)
terrapin (1.1.1)
climate_control
test-prof (1.6.0)
test-prof (1.6.1)
thor (1.5.0)
tilt (2.7.0)
timeout (0.6.1)
@@ -888,7 +888,7 @@ GEM
unf (~> 0.1.0)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
tzinfo-data (1.2026.1)
tzinfo-data (1.2026.2)
tzinfo (>= 1.0.0)
unf (0.1.4)
unf_ext
@@ -904,7 +904,7 @@ GEM
vite_rails (3.0.20)
railties (>= 5.1, < 9)
vite_ruby (~> 3.0, >= 3.2.2)
vite_ruby (3.9.3)
vite_ruby (3.10.2)
dry-cli (>= 0.7, < 2)
logger (~> 1.6)
mutex_m
@@ -984,7 +984,6 @@ DEPENDENCIES
haml-rails (~> 3.0)
haml_lint
hcaptcha (~> 7.1)
hiredis (~> 0.6)
hiredis-client
htmlentities (~> 4.3)
http (~> 5.3.0)
@@ -994,6 +993,7 @@ DEPENDENCIES
i18n-tasks (~> 1.0)
idn-ruby
inline_svg
ipaddr (~> 1.2)
irb (~> 1.8)
jd-paperclip-azure (~> 3.0)
json
@@ -1020,32 +1020,32 @@ DEPENDENCIES
omniauth-rails_csrf_protection (~> 2.0)
omniauth-saml (~> 2.0)
omniauth_openid_connect (~> 0.8.0)
opentelemetry-api (~> 1.8.0)
opentelemetry-exporter-otlp (~> 0.32.0)
opentelemetry-instrumentation-active_job (~> 0.10.0)
opentelemetry-instrumentation-active_model_serializers (~> 0.24.0)
opentelemetry-instrumentation-concurrent_ruby (~> 0.24.0)
opentelemetry-instrumentation-excon (~> 0.28.0)
opentelemetry-instrumentation-faraday (~> 0.32.0)
opentelemetry-instrumentation-http (~> 0.29.0)
opentelemetry-instrumentation-http_client (~> 0.28.0)
opentelemetry-instrumentation-net_http (~> 0.28.0)
opentelemetry-instrumentation-pg (~> 0.35.0)
opentelemetry-instrumentation-rack (~> 0.30.0)
opentelemetry-instrumentation-rails (~> 0.40.0)
opentelemetry-instrumentation-redis (~> 0.28.0)
opentelemetry-instrumentation-sidekiq (~> 0.28.0)
opentelemetry-api (~> 1.10.0)
opentelemetry-exporter-otlp (~> 0.34.0)
opentelemetry-instrumentation-active_job (~> 0.12.0)
opentelemetry-instrumentation-active_model_serializers (~> 0.25.0)
opentelemetry-instrumentation-concurrent_ruby (~> 0.25.0)
opentelemetry-instrumentation-excon (~> 0.29.0)
opentelemetry-instrumentation-faraday (~> 0.33.0)
opentelemetry-instrumentation-http (~> 0.30.0)
opentelemetry-instrumentation-http_client (~> 0.29.0)
opentelemetry-instrumentation-net_http (~> 0.29.0)
opentelemetry-instrumentation-pg (~> 0.36.0)
opentelemetry-instrumentation-rack (~> 0.31.0)
opentelemetry-instrumentation-rails (~> 0.42.0)
opentelemetry-instrumentation-redis (~> 0.29.0)
opentelemetry-instrumentation-sidekiq (~> 0.29.0)
opentelemetry-sdk (~> 1.4)
ox (~> 2.14)
parslet
pg (~> 1.5)
pghero
playwright-ruby-client (= 1.57.1)
playwright-ruby-client (= 1.59.1)
premailer-rails
prometheus_exporter (~> 2.2)
propshaft
public_suffix (~> 7.0)
puma (~> 7.0)
puma
pundit (~> 2.3)
rack-attack (~> 6.6)
rack-cors
@@ -1054,7 +1054,7 @@ DEPENDENCIES
rails-i18n (~> 8.0)
rdf-normalize (~> 0.5)
redcarpet (~> 3.6)
redis (~> 4.5)
redis (~> 5)
rqrcode (~> 3.0)
rspec-github (~> 3.0)
rspec-rails (~> 8.0)
@@ -1097,7 +1097,7 @@ DEPENDENCIES
xorcist (~> 1.1)
RUBY VERSION
ruby 4.0.2
ruby 4.0.5
BUNDLED WITH
4.0.8
4.0.11

View File

@@ -4,7 +4,7 @@
<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>
@@ -59,7 +59,7 @@ Mastodon is a **free, open-source social network server** based on [ActivityPub]
- **Ruby** 3.3+
- **PostgreSQL** 14+
- **Redis** 7.0+
- **Node.js** 20+
- **Node.js** 22+
- **FFmpeg** 5.1+
This repository includes deployment configurations for **Docker and docker-compose**, as well as for other environments like Heroku and Scalingo. For Helm charts, reference the [mastodon/chart repository](https://github.com/mastodon/chart). A [**standalone** installation guide](https://docs.joinmastodon.org/admin/install/) is available in the main documentation.

4
Vagrantfile vendored
View File

@@ -12,7 +12,7 @@ sudo apt-add-repository 'deb https://dl.yarnpkg.com/debian/ stable main'
# Add repo for NodeJS
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
NODE_MAJOR=20
NODE_MAJOR=24
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | sudo tee /etc/apt/sources.list.d/nodesource.list
sudo apt-get update
@@ -115,7 +115,7 @@ gem install bundler foreman
bundle install
# Install node modules
sudo corepack enable
sudo npm i -g corepack
corepack prepare
yarn install

View File

@@ -4,13 +4,48 @@ module Admin
class CollectionsController < BaseController
before_action :set_account
before_action :set_collection, only: :show
before_action :set_collections, except: :show
PER_PAGE = 20
def index
authorize [:admin, :collection], :index?
@collection_batch_action = Admin::CollectionBatchAction.new
end
def show
authorize @collection, :show?
end
def batch
authorize [:admin, :collection], :index?
@collection_batch_action = Admin::CollectionBatchAction.new(admin_collection_batch_action_params.merge(current_account: current_account, report_id: params[:report_id], type: action_from_button))
@collection_batch_action.save!
rescue ActionController::ParameterMissing
flash[:alert] = I18n.t('admin.collections.no_collection_selected')
ensure
redirect_to after_create_redirect_path
end
private
def after_create_redirect_path
report_id = @collections_batch_action&.report_id || params[:report_id]
if report_id.present?
admin_report_path(report_id)
else
admin_account_collections_path(params[:account_id], params[:page])
end
end
def admin_collection_batch_action_params
params
.expect(admin_collection_batch_action: [collection_ids: []])
end
def set_account
@account = Account.find(params[:account_id])
end
@@ -18,5 +53,17 @@ module Admin
def set_collection
@collection = @account.collections.includes(accepted_collection_items: :account).find(params[:id])
end
def set_collections
@collections = @account.collections.includes(accepted_collection_items: :account).page(params[:page]).per(PER_PAGE)
end
def action_from_button
if params[:report]
'report'
elsif params[:remove_from_report]
'remove_from_report'
end
end
end
end

View File

@@ -5,7 +5,7 @@ module Admin
def index
authorize :email_domain_block, :index?
@email_domain_blocks = EmailDomainBlock.parents.includes(:children).order(id: :desc).page(params[:page])
@email_domain_blocks = filter_by_domain.page(params[:page])
@form = Form::EmailDomainBlockBatch.new
end
@@ -57,6 +57,12 @@ module Admin
private
def filter_by_domain
scope = EmailDomainBlock.parents.includes(:children).order(id: :desc)
scope.merge!(EmailDomainBlock.matches_domain(params[:domain])) if params[:domain].present?
scope
end
def set_resolved_records
@resolved_records = DomainResource.new(@email_domain_block.domain).mx
end

View File

@@ -0,0 +1,9 @@
# frozen_string_literal: true
class Admin::EmailSubscriptions::AdditionalFooterTextsController < Admin::SettingsController
private
def after_update_redirect_path
admin_email_subscriptions_path
end
end

View File

@@ -0,0 +1,34 @@
# frozen_string_literal: true
class Admin::EmailSubscriptions::SetupsController < Admin::BaseController
before_action :require_enabled!
def show
authorize :email_subscription, :enable?
@form = Form::EmailSubscriptionsConfirmation.new
end
def create
authorize :email_subscription, :enable?
@form = Form::EmailSubscriptionsConfirmation.new(resource_params)
if @form.valid?
Setting.email_subscriptions = true
redirect_to admin_email_subscriptions_path
else
render :show
end
end
private
def require_enabled!
raise ActionController::RoutingError, 'Feature disabled' unless Rails.application.config.x.email_subscriptions
end
def resource_params
params.expect(form_email_subscriptions_confirmation: [:agreement_email_volume, :agreement_privacy_and_terms])
end
end

View File

@@ -0,0 +1,23 @@
# frozen_string_literal: true
class Admin::EmailSubscriptionsController < Admin::BaseController
def index
authorize :email_subscription, :index?
@enabled = Setting.email_subscriptions
@roles = UserRole.where('permissions & ? != 0', UserRole::FLAGS[:manage_email_subscriptions] | UserRole::FLAGS[:administrator])
@accounts = Account.local.where.associated(:email_subscriptions).includes(:user)
end
def disable
authorize :email_subscription, :disable?
Setting.email_subscriptions = false
redirect_to admin_email_subscriptions_path, notice: I18n.t('admin.email_subscriptions.disabled_msg')
end
def purge
authorize :email_subscription, :purge?
Admin::EmailSubscriptionsPurgeWorker.perform_async
redirect_to admin_email_subscriptions_path, notice: I18n.t('admin.email_subscriptions.purged_msg')
end
end

View File

@@ -16,6 +16,8 @@ module Admin
@report_notes = @report.notes.chronological.includes(:account)
@action_logs = @report.history.includes(:target)
@form = Admin::StatusBatchAction.new
@collection_form = Admin::CollectionBatchAction.new
@collections = @report.collections
@statuses = @report.statuses.with_includes
end

View File

@@ -62,7 +62,7 @@ module Admin
def resource_params
params
.expect(user_role: [:name, :color, :highlighted, :position, :require_2fa, permissions_as_keys: []])
.expect(user_role: [:name, :color, :highlighted, :position, :require_2fa, :collection_limit, permissions_as_keys: []])
end
end
end

View File

@@ -19,7 +19,7 @@ class Api::V1::Accounts::EmailSubscriptionsController < Api::BaseController
end
def require_feature_enabled!
head 404 unless Mastodon::Feature.email_subscriptions_enabled?
head 404 unless Rails.application.config.x.email_subscriptions && Setting.email_subscriptions
end
def require_account_permissions!

View File

@@ -16,7 +16,7 @@ class Api::V1::NotificationsController < Api::BaseController
@relationships = StatusRelationshipsPresenter.new(target_statuses_from_notifications, current_user&.account_id)
end
render json: @notifications, each_serializer: REST::NotificationSerializer, relationships: @relationships
render json: @notifications, each_serializer: REST::NotificationSerializer, relationships: @relationships, supported_notification_types: params[:supported_types]
end
def unread_count
@@ -29,7 +29,7 @@ class Api::V1::NotificationsController < Api::BaseController
def show
@notification = current_account.notifications.without_suspended.find(params[:id])
render json: @notification, serializer: REST::NotificationSerializer
render json: @notification, serializer: REST::NotificationSerializer, supported_notification_types: params[:supported_types]
end
def clear
@@ -89,6 +89,8 @@ class Api::V1::NotificationsController < Api::BaseController
end
def pagination_params(core_params)
params.slice(:limit, :account_id, :types, :exclude_types, :include_filtered).permit(:limit, :account_id, :include_filtered, types: [], exclude_types: []).merge(core_params)
params.slice(:limit, :account_id, :types, :exclude_types, :include_filtered, :supported_types)
.permit(:limit, :account_id, :include_filtered, types: [], exclude_types: [], supported_types: [])
.merge(core_params)
end
end

View File

@@ -0,0 +1,67 @@
# frozen_string_literal: true
class Api::V1::Statuses::ContextsController < Api::BaseController
include Authorization
include AsyncRefreshesConcern
before_action -> { authorize_if_got_token! :read, :'read:statuses' }
before_action :set_status
# This API was originally unlimited, pagination cannot be introduced without
# breaking backwards-compatibility. Arbitrarily high number to cover most
# conversations as quasi-unlimited, it would be too much work to render more
# than this anyway
CONTEXT_LIMIT = 4_096
# This remains expensive and we don't want to show everything to logged-out users
ANCESTORS_LIMIT = 40
DESCENDANTS_LIMIT = 60
DESCENDANTS_DEPTH_LIMIT = 20
def show
cache_if_unauthenticated!
ancestors_limit = CONTEXT_LIMIT
descendants_limit = CONTEXT_LIMIT
descendants_depth_limit = nil
if current_account.nil?
ancestors_limit = ANCESTORS_LIMIT
descendants_limit = DESCENDANTS_LIMIT
descendants_depth_limit = DESCENDANTS_DEPTH_LIMIT
end
ancestors_results = @status.in_reply_to_id.nil? ? [] : @status.ancestors(ancestors_limit, current_account)
descendants_results = @status.descendants(descendants_limit, current_account, descendants_depth_limit)
loaded_ancestors = preload_collection(ancestors_results, Status)
loaded_descendants = preload_collection(descendants_results, Status)
@context = Context.new(ancestors: loaded_ancestors, descendants: loaded_descendants)
statuses = [@status] + @context.ancestors + @context.descendants
refresh_key = "context:#{@status.id}:refresh"
async_refresh = AsyncRefresh.new(refresh_key)
if async_refresh.running?
add_async_refresh_header(async_refresh)
elsif !current_account.nil? && @status.should_fetch_replies?
add_async_refresh_header(AsyncRefresh.create(refresh_key, count_results: true))
WorkerBatch.new.within do |batch|
batch.connect(refresh_key, threshold: 1.0)
ActivityPub::FetchAllRepliesWorker.perform_async(@status.id, { 'batch_id' => batch.id })
end
end
render json: @context, serializer: REST::ContextSerializer, relationships: StatusRelationshipsPresenter.new(statuses, current_user&.account_id)
end
private
def set_status
@status = Status.find(params[:status_id])
authorize @status, :show?
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
not_found
end
end

View File

@@ -2,14 +2,13 @@
class Api::V1::StatusesController < Api::BaseController
include Authorization
include AsyncRefreshesConcern
include Api::InteractionPoliciesConcern
before_action -> { authorize_if_got_token! :read, :'read:statuses' }, except: [:create, :update, :destroy]
before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only: [:create, :update, :destroy]
before_action :require_user!, except: [:index, :show, :context]
before_action :require_user!, except: [:index, :show]
before_action :set_statuses, only: [:index]
before_action :set_status, only: [:show, :context]
before_action :set_status, only: [:show]
before_action :set_thread, only: [:create]
before_action :set_quoted_status, only: [:create]
before_action :check_statuses_limit, only: [:index]
@@ -17,17 +16,6 @@ class Api::V1::StatusesController < Api::BaseController
override_rate_limit_headers :create, family: :statuses
override_rate_limit_headers :update, family: :statuses
# This API was originally unlimited, pagination cannot be introduced without
# breaking backwards-compatibility. Arbitrarily high number to cover most
# conversations as quasi-unlimited, it would be too much work to render more
# than this anyway
CONTEXT_LIMIT = 4_096
# This remains expensive and we don't want to show everything to logged-out users
ANCESTORS_LIMIT = 40
DESCENDANTS_LIMIT = 60
DESCENDANTS_DEPTH_LIMIT = 20
def index
@statuses = preload_collection(@statuses, Status)
render json: @statuses, each_serializer: REST::StatusSerializer
@@ -39,44 +27,6 @@ class Api::V1::StatusesController < Api::BaseController
render json: @status, serializer: REST::StatusSerializer
end
def context
cache_if_unauthenticated!
ancestors_limit = CONTEXT_LIMIT
descendants_limit = CONTEXT_LIMIT
descendants_depth_limit = nil
if current_account.nil?
ancestors_limit = ANCESTORS_LIMIT
descendants_limit = DESCENDANTS_LIMIT
descendants_depth_limit = DESCENDANTS_DEPTH_LIMIT
end
ancestors_results = @status.in_reply_to_id.nil? ? [] : @status.ancestors(ancestors_limit, current_account)
descendants_results = @status.descendants(descendants_limit, current_account, descendants_depth_limit)
loaded_ancestors = preload_collection(ancestors_results, Status)
loaded_descendants = preload_collection(descendants_results, Status)
@context = Context.new(ancestors: loaded_ancestors, descendants: loaded_descendants)
statuses = [@status] + @context.ancestors + @context.descendants
refresh_key = "context:#{@status.id}:refresh"
async_refresh = AsyncRefresh.new(refresh_key)
if async_refresh.running?
add_async_refresh_header(async_refresh)
elsif !current_account.nil? && @status.should_fetch_replies?
add_async_refresh_header(AsyncRefresh.create(refresh_key, count_results: true))
WorkerBatch.new.within do |batch|
batch.connect(refresh_key, threshold: 1.0)
ActivityPub::FetchAllRepliesWorker.perform_async(@status.id, { 'batch_id' => batch.id })
end
end
render json: @context, serializer: REST::ContextSerializer, relationships: StatusRelationshipsPresenter.new(statuses, current_user&.account_id)
end
def create
@status = PostStatusService.new.call(
current_user.account,

View File

@@ -0,0 +1,63 @@
# frozen_string_literal: true
class Api::V1Alpha::InCollectionsController < Api::BaseController
include Authorization
DEFAULT_COLLECTIONS_LIMIT = 40
before_action :check_feature_enabled
before_action -> { authorize_if_got_token! :read, :'read:collections' }, only: [:index]
before_action :require_user!
before_action :set_account, only: [:index]
before_action :set_collections, only: [:index]
after_action :insert_pagination_headers, only: [:index]
after_action :verify_authorized
def index
cache_if_unauthenticated!
authorize @account, :index_featured_in_collections?
render json: @collections, each_serializer: REST::CollectionSerializer, adapter: :json
end
private
def set_account
@account = Account.find(params[:account_id])
end
def set_collections
@collections = @account.featured_in_collections
.with_tag
.offset(offset_param)
.limit(limit_param(DEFAULT_COLLECTIONS_LIMIT))
end
def check_feature_enabled
raise ActionController::RoutingError unless Mastodon::Feature.collections_enabled?
end
def next_path
return unless records_continue?
api_v1_alpha_account_in_collections_url(@account, pagination_params(offset: offset_param + limit_param(DEFAULT_COLLECTIONS_LIMIT)))
end
def prev_path
return if offset_param.zero?
api_v1_alpha_account_in_collections_url(@account, pagination_params(offset: offset_param - limit_param(DEFAULT_COLLECTIONS_LIMIT)))
end
def records_continue?
((offset_param * limit_param(DEFAULT_COLLECTIONS_LIMIT)) + @collections.size) < @account.featured_in_collections.size
end
def offset_param
params[:offset].to_i
end
end

View File

@@ -33,7 +33,7 @@ class Api::V2::NotificationsController < Api::BaseController
'app.notification_grouping.expand_accounts_param' => expand_accounts_param
)
render json: @presenter, serializer: REST::DedupNotificationGroupSerializer, relationships: @relationships, expand_accounts: expand_accounts_param
render json: @presenter, serializer: REST::DedupNotificationGroupSerializer, relationships: @relationships, expand_accounts: expand_accounts_param, supported_notification_types: params[:supported_types]
end
end
@@ -48,7 +48,7 @@ class Api::V2::NotificationsController < Api::BaseController
def show
@notification = current_account.notifications.without_suspended.by_group_key(params[:group_key]).take!
presenter = GroupedNotificationsPresenter.new(NotificationGroup.from_notifications([@notification]))
render json: presenter, serializer: REST::DedupNotificationGroupSerializer
render json: presenter, serializer: REST::DedupNotificationGroupSerializer, supported_notification_types: params[:supported_types]
end
def clear
@@ -138,7 +138,9 @@ class Api::V2::NotificationsController < Api::BaseController
end
def pagination_params(core_params)
params.slice(:limit, :include_filtered, :types, :exclude_types, :grouped_types).permit(:limit, :include_filtered, types: [], exclude_types: [], grouped_types: []).merge(core_params)
params.slice(:limit, :include_filtered, :types, :exclude_types, :grouped_types, :supported_types)
.permit(:limit, :include_filtered, types: [], exclude_types: [], grouped_types: [], supported_types: [])
.merge(core_params)
end
def expand_accounts_param

View File

@@ -17,7 +17,10 @@ class CollectionsController < ApplicationController
def show
respond_to do |format|
# TODO: format.html
format.html do
expires_in expiration_duration, public: true unless user_signed_in?
render template: 'home/index'
end
format.json do
expires_in expiration_duration, public: true if public_fetch_mode?
@@ -28,8 +31,17 @@ class CollectionsController < ApplicationController
private
def set_account
if account_id_param.present?
@account = Account.local.find(account_id_param)
else
@collection = Collection.find(params[:id])
@account = @collection.account
end
end
def set_collection
@collection = @account.collections.find(params[:id])
@collection ||= @account.collections.find(params[:id])
authorize @collection, :show?
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
not_found

View File

@@ -142,6 +142,7 @@ module SignatureVerification
# Only refreshing keys, skipping potentially more expensive requests
ActivityPub::FetchRemoteActorService.new.call(keypair.actor.uri, only_key: true, suppress_errors: false)
end
return if actor.nil?
keypair_uri = keypair.uri

View File

@@ -3,7 +3,7 @@
class CustomCssController < ActionController::Base # rubocop:disable Rails/ApplicationController
def show
expires_in 1.month, public: true
render content_type: 'text/css'
render content_type: :css
end
private

View File

@@ -24,12 +24,7 @@ class MediaController < ApplicationController
private
def set_media_attachment
id = params[:id] || params[:medium_id]
return if id.nil?
scope = MediaAttachment.local.attached
# If id is 19 characters long, it's a shortcode, otherwise it's an identifier
@media_attachment = id.size == 19 ? scope.find_by!(shortcode: id) : scope.find(id)
@media_attachment = MediaAttachment.local.attached.identified(params[:id])
end
def verify_permitted_status!

View File

@@ -0,0 +1,10 @@
# frozen_string_literal: true
class Redirect::CollectionsController < Redirect::BaseController
private
def set_resource
@resource = Collection.find(params[:id])
not_found if @resource.local? || @resource&.account&.suspended?
end
end

View File

@@ -7,7 +7,7 @@ module Settings
skip_before_action :require_functional!
before_action :require_challenge!, on: :create
before_action :require_challenge!
def create
@recovery_codes = current_user.generate_otp_backup_codes!

View File

@@ -127,6 +127,10 @@ module ApplicationHelper
)
end
def emptyphaunt
inline_svg_tag 'elephant_ui.svg'
end
def check_icon
inline_svg_tag 'check.svg'
end

View File

@@ -34,7 +34,7 @@ module ContextHelper
},
quote_requests: { 'QuoteRequest' => 'https://w3id.org/fep/044f#QuoteRequest' },
quotes: {
'quote' => 'https://w3id.org/fep/044f#quote',
'quote' => { '@id' => 'https://w3id.org/fep/044f#quote', '@type' => '@id' },
'quoteUri' => 'http://fedibird.com/ns#quoteUri',
'_misskey_quote' => 'https://misskey-hub.net/ns#_misskey_quote',
'quoteAuthorization' => { '@id' => 'https://w3id.org/fep/044f#quoteAuthorization', '@type' => '@id' },
@@ -48,9 +48,9 @@ module ContextHelper
},
quote_authorizations: {
'gts' => 'https://gotosocial.org/ns#',
'quoteAuthorization' => { '@id' => 'https://w3id.org/fep/044f#quoteAuthorization', '@type' => '@id' },
'interactingObject' => { '@id' => 'gts:interactingObject' },
'interactionTarget' => { '@id' => 'gts:interactionTarget' },
'QuoteAuthorization' => 'https://w3id.org/fep/044f#QuoteAuthorization',
'interactingObject' => { '@id' => 'gts:interactingObject', '@type' => '@id' },
'interactionTarget' => { '@id' => 'gts:interactionTarget', '@type' => '@id' },
},
}.freeze

View File

@@ -1,11 +0,0 @@
# frozen_string_literal: true
module InvitesHelper
def invites_max_uses_options
[1, 5, 10, 25, 50, 100]
end
def invites_expires_options
[30.minutes, 1.hour, 6.hours, 12.hours, 1.day, 1.week]
end
end

View File

@@ -3,6 +3,8 @@
module JsonLdHelper
include ContextHelper
UNSUPPORTED_JSONLD_KEYWORDS = %w(@graph @included @reverse).freeze
def equals_or_includes?(haystack, needle)
haystack.is_a?(Array) ? haystack.include?(needle) : haystack == needle
end
@@ -110,6 +112,16 @@ module JsonLdHelper
compacted
end
def unsupported_jsonld_features?(json)
if json.is_a?(Hash)
json.any? { |key, value| UNSUPPORTED_JSONLD_KEYWORDS.include?(key) || unsupported_jsonld_features?(value) }
elsif json.is_a?(Array)
json.any? { |value| unsupported_jsonld_features?(value) }
else
false
end
end
# Patches a JSON-LD document to avoid compatibility issues on redistribution
#
# Since compacting a JSON-LD document against Mastodon's built-in vocabulary

View File

@@ -223,7 +223,14 @@ module LanguagesHelper
'zh-YUE': ['Cantonese', '廣東話'].freeze,
}.freeze
SUPPORTED_LOCALES = {}.merge(ISO_639_1).merge(ISO_639_1_REGIONAL).merge(ISO_639_3).freeze
# Since nan is not translated but nan-TW is translated,
# to enable the ISO-639-3 language-code with the regional variant but no
# official name, we use a specific hash for nan-TW
ISO_639_3_REGIONAL = {
'nan-TW': ['Hokkien (Taiwan)', '臺語 (Hô-ló話)'].freeze,
}.freeze
SUPPORTED_LOCALES = {}.merge(ISO_639_1).merge(ISO_639_1_REGIONAL).merge(ISO_639_3).merge(ISO_639_3_REGIONAL).freeze
# For ISO-639-1 and ISO-639-3 language codes, we have their official
# names, but for some translations, we need the names of the
@@ -233,7 +240,6 @@ module LanguagesHelper
'es-AR': 'Español (Argentina)',
'es-MX': 'Español (México)',
'fr-CA': 'Français (Canadien)',
'nan-TW': '臺語 (Hô-ló話)',
'pt-BR': 'Português (Brasil)',
'pt-PT': 'Português (Portugal)',
'sr-Latn': 'Srpski (latinica)',

View File

@@ -0,0 +1,6 @@
# frozen_string_literal: true
module MediaPlayerHelper
PLAYER_HEIGHT = 380
PLAYER_WIDTH = 670
end

View File

@@ -23,6 +23,10 @@ module SettingsHelper
)
end
def user_settings_collection(value)
UserSettings.definition_for(value)&.in || []
end
def author_attribution_name(account)
return if account.nil?

View File

@@ -47,9 +47,15 @@ module ThemeHelper
end
def current_theme
return Setting.theme unless Themes.instance.names.include? current_user&.setting_theme
available_themes = Themes.instance.names
current_user.setting_theme
user_theme = current_user&.setting_theme
return user_theme if user_theme && available_themes.include?(user_theme)
site_theme = Setting.theme
return site_theme if available_themes.include?(site_theme)
'default' # Fallback
end
def color_scheme

View File

@@ -69,8 +69,9 @@ on('change', '#batch_checkbox_all', ({ target }) => {
'.batch-table__select-all',
);
document
.querySelectorAll<HTMLInputElement>(batchCheckboxClassName)
target
.closest('.batch-table')
?.querySelectorAll<HTMLInputElement>(batchCheckboxClassName)
.forEach((content) => {
content.checked = target.checked;
});
@@ -112,17 +113,20 @@ on('click', '.batch-table__select-all button', () => {
}
});
on('change', batchCheckboxClassName, () => {
const checkAllElement = document.querySelector<HTMLInputElement>(
on('change', batchCheckboxClassName, (event) => {
const targetTable = (event.target as HTMLElement).closest('.batch-table');
if (!targetTable) return;
const checkAllElement = targetTable.querySelector<HTMLInputElement>(
'input#batch_checkbox_all',
);
const selectAllMatchingElement = document.querySelector(
const selectAllMatchingElement = targetTable.querySelector(
'.batch-table__select-all',
);
if (checkAllElement) {
const allCheckboxes = Array.from(
document.querySelectorAll<HTMLInputElement>(batchCheckboxClassName),
targetTable.querySelectorAll<HTMLInputElement>(batchCheckboxClassName),
);
checkAllElement.checked = allCheckboxes.every((content) => content.checked);
checkAllElement.indeterminate =

View File

@@ -13,14 +13,17 @@ import axios from 'axios';
import { on } from 'delegated-events';
import { throttle } from 'lodash';
import { determineEmojiMode } from '@/mastodon/features/emoji/mode';
import { updateHtmlWithEmoji } from '@/mastodon/features/emoji/render';
import loadKeyboardExtensions from '@/mastodon/load_keyboard_extensions';
import { loadLocale, getLocale } from '@/mastodon/locales';
import { loadPolyfills } from '@/mastodon/polyfills';
import ready from '@/mastodon/ready';
import { assetHost } from '@/mastodon/utils/config';
import { getNestedProperty } from '@/mastodon/utils/objects';
import { isDarkMode } from '@/mastodon/utils/theme';
import { formatTime } from '@/mastodon/utils/time';
import emojify from '../mastodon/features/emoji/emoji';
import loadKeyboardExtensions from '../mastodon/load_keyboard_extensions';
import { loadLocale, getLocale } from '../mastodon/locales';
import { loadPolyfills } from '../mastodon/polyfills';
import ready from '../mastodon/ready';
import 'cocoon-js-vanilla';
const messages = defineMessages({
@@ -38,7 +41,7 @@ const messages = defineMessages({
},
});
function loaded() {
async function loaded() {
const { messages: localeData } = getLocale();
const locale = document.documentElement.lang;
@@ -75,9 +78,30 @@ function loaded() {
return messageFormat.format(values) as string;
};
document.querySelectorAll('.emojify').forEach((content) => {
content.innerHTML = emojify(content.innerHTML);
});
let emojiStyle = 'auto';
const initialStateText =
document.getElementById('initial-state')?.textContent;
if (initialStateText) {
const stateEmojiStyle = getNestedProperty(
JSON.parse(initialStateText) as unknown,
'meta',
'emoji_style',
);
if (typeof stateEmojiStyle === 'string') {
emojiStyle = stateEmojiStyle;
}
}
const emojiMode = determineEmojiMode(emojiStyle);
const darkTheme = isDarkMode();
for (const element of document.querySelectorAll('.emojify')) {
await updateHtmlWithEmoji({
assetHost,
element,
locale,
mode: emojiMode,
darkTheme,
});
}
document
.querySelectorAll<HTMLTimeElement>('time.formatted')

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.5 KiB

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

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="black">
<path d="M13.2502 8.00029C13.2502 5.10089 10.8996 2.75044 8.00024 2.75029C5.10075 2.75029 2.75024 5.10079 2.75024 8.00029C2.7504 10.8997 5.10084 13.2503 8.00024 13.2503C10.8995 13.2501 13.2501 10.8996 13.2502 8.00029ZM7.41663 4.50029C7.41663 4.17812 7.67808 3.91667 8.00024 3.91667C8.32228 3.91683 8.58301 4.17822 8.58301 4.50029V7.63884L10.5945 8.64458C10.8825 8.78865 10.999 9.13922 10.8551 9.42729C10.711 9.71541 10.3605 9.83276 10.0724 9.68877L7.73877 8.52153C7.54143 8.4227 7.41673 8.221 7.41663 8.00029V4.50029ZM14.4166 8.00029C14.4165 11.5439 11.5438 14.4165 8.00024 14.4167C4.45651 14.4167 1.58316 11.544 1.58301 8.00029C1.58301 4.45646 4.45642 1.58305 8.00024 1.58305C11.5439 1.58321 14.4166 4.45656 14.4166 8.00029Z" />
</svg>

After

Width:  |  Height:  |  Size: 799 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 16 16"><path fill="currentColor" d="M7.467 7.266 11.533 3.2a.586.586 0 0 1 .85-.001.585.585 0 0 1 0 .851L8.317 8.116zm1.416 1.417 3.5-3.5a.586.586 0 0 1 .85 0 .586.586 0 0 1 0 .85l-3.5 3.5zM3.35 12.267Q2 10.917 1.983 9.017q-.015-1.9 1.334-3.25L5.2 3.882l.883.9q.135.133.25.283.117.15.15.35l2.5-2.5a.586.586 0 0 1 .85 0 .586.586 0 0 1 0 .85L6.8 6.8l-.75.766.15.15q.65.65.642 1.55a2.15 2.15 0 0 1-.659 1.55l-.416.417-.85-.85.416-.417a.98.98 0 0 0 .292-.7.94.94 0 0 0-.276-.705l-.582-.578a.586.586 0 0 1 0-.85l.433-.417a.8.8 0 0 0 .233-.57.77.77 0 0 0-.233-.563L4.15 6.616a3.2 3.2 0 0 0-.975 2.409A3.37 3.37 0 0 0 4.2 11.433q1 1 2.387 1t2.38-1L12.68 7.72a.583.583 0 0 1 .853-.002.586.586 0 0 1 0 .85l-3.716 3.7q-1.342 1.35-3.23 1.35t-3.237-1.35M11.2 15.2V14a2.7 2.7 0 0 0 1.983-.817A2.7 2.7 0 0 0 14 11.2h1.2q0 1.66-1.17 2.83T11.2 15.2M.8 4.8q0-1.66 1.17-2.83T4.8.8V2a2.7 2.7 0 0 0-1.983.816A2.7 2.7 0 0 0 2 4.8z"/></svg>

After

Width:  |  Height:  |  Size: 984 B

View File

@@ -6,8 +6,8 @@ import { throttle } from 'lodash';
import api from 'mastodon/api';
import { browserHistory } from 'mastodon/components/router';
import { countableText } from 'mastodon/features/compose/util/counter';
import { search as emojiSearch } from 'mastodon/features/emoji/emoji_mart_search_light';
import { tagHistory } from 'mastodon/settings';
import { emojiMartSearch } from '@/mastodon/features/emoji/picker';
import { showAlert, showAlertForError } from './alerts';
import { useEmoji } from './emojis';
@@ -99,7 +99,7 @@ export const ensureComposeIsVisible = (getState) => {
export function setComposeToStatus(status, text, spoiler_text) {
return (dispatch, getState) => {
const maxOptions = getState().server.getIn(['server', 'configuration', 'polls', 'max_options']);
const maxOptions = getState().server.server.item?.configuration.polls.max_options;
dispatch({
type: COMPOSE_SET_STATUS,
@@ -153,10 +153,11 @@ export function resetCompose() {
};
}
export const focusCompose = (defaultText = '') => (dispatch, getState) => {
export const focusCompose = (defaultText = '', caretStart = false) => (dispatch, getState) => {
dispatch({
type: COMPOSE_FOCUS,
defaultText,
caretStart,
});
ensureComposeIsVisible(getState);
@@ -562,7 +563,7 @@ export function clearComposeSuggestions() {
};
}
const fetchComposeSuggestionsAccounts = throttle((dispatch, getState, token) => {
const fetchComposeSuggestionsAccounts = throttle((dispatch, token) => {
if (fetchComposeSuggestionsAccountsController) {
fetchComposeSuggestionsAccountsController.abort();
}
@@ -589,12 +590,14 @@ const fetchComposeSuggestionsAccounts = throttle((dispatch, getState, token) =>
});
}, 200, { leading: true, trailing: true });
const fetchComposeSuggestionsEmojis = (dispatch, getState, token) => {
const results = emojiSearch(token.replace(':', ''), { maxResults: 5 });
const fetchComposeSuggestionsEmojis = async (dispatch, token) => {
// Right now we are hard-coding the locale to English since the picker search only supports English.
// Once we replace the legacy picker we can remove this and use the actual locale of the user.
const results = await emojiMartSearch(token, 'en', 5);
dispatch(readyComposeSuggestionsEmojis(token, results));
};
const fetchComposeSuggestionsTags = throttle((dispatch, getState, token) => {
const fetchComposeSuggestionsTags = throttle((dispatch, token) => {
if (fetchComposeSuggestionsTagsController) {
fetchComposeSuggestionsTagsController.abort();
}
@@ -628,14 +631,14 @@ export function fetchComposeSuggestions(token) {
return (dispatch, getState) => {
switch (token[0]) {
case ':':
fetchComposeSuggestionsEmojis(dispatch, getState, token);
void fetchComposeSuggestionsEmojis(dispatch, token);
break;
case '#':
case '':
fetchComposeSuggestionsTags(dispatch, getState, token);
fetchComposeSuggestionsTags(dispatch, token);
break;
default:
fetchComposeSuggestionsAccounts(dispatch, getState, token);
fetchComposeSuggestionsAccounts(dispatch, token);
break;
}
};
@@ -668,7 +671,7 @@ export function selectComposeSuggestion(position, token, suggestion, path) {
let completion, startPosition;
if (suggestion.type === 'emoji') {
completion = suggestion.native || suggestion.colons;
completion = suggestion.native || `:${suggestion.id}:`;
startPosition = position - 1;
dispatch(useEmoji(suggestion));

View File

@@ -1,40 +0,0 @@
import api from '../api';
export const CUSTOM_EMOJIS_FETCH_REQUEST = 'CUSTOM_EMOJIS_FETCH_REQUEST';
export const CUSTOM_EMOJIS_FETCH_SUCCESS = 'CUSTOM_EMOJIS_FETCH_SUCCESS';
export const CUSTOM_EMOJIS_FETCH_FAIL = 'CUSTOM_EMOJIS_FETCH_FAIL';
export function fetchCustomEmojis() {
return (dispatch) => {
dispatch(fetchCustomEmojisRequest());
api().get('/api/v1/custom_emojis').then(response => {
dispatch(fetchCustomEmojisSuccess(response.data));
}).catch(error => {
dispatch(fetchCustomEmojisFail(error));
});
};
}
export function fetchCustomEmojisRequest() {
return {
type: CUSTOM_EMOJIS_FETCH_REQUEST,
skipLoading: true,
};
}
export function fetchCustomEmojisSuccess(custom_emojis) {
return {
type: CUSTOM_EMOJIS_FETCH_SUCCESS,
custom_emojis,
skipLoading: true,
};
}
export function fetchCustomEmojisFail(error) {
return {
type: CUSTOM_EMOJIS_FETCH_FAIL,
error,
skipLoading: true,
};
}

View File

@@ -1,139 +0,0 @@
import api from '../api';
import { importFetchedAccount } from './importer';
export const SERVER_FETCH_REQUEST = 'Server_FETCH_REQUEST';
export const SERVER_FETCH_SUCCESS = 'Server_FETCH_SUCCESS';
export const SERVER_FETCH_FAIL = 'Server_FETCH_FAIL';
export const SERVER_TRANSLATION_LANGUAGES_FETCH_REQUEST = 'SERVER_TRANSLATION_LANGUAGES_FETCH_REQUEST';
export const SERVER_TRANSLATION_LANGUAGES_FETCH_SUCCESS = 'SERVER_TRANSLATION_LANGUAGES_FETCH_SUCCESS';
export const SERVER_TRANSLATION_LANGUAGES_FETCH_FAIL = 'SERVER_TRANSLATION_LANGUAGES_FETCH_FAIL';
export const EXTENDED_DESCRIPTION_REQUEST = 'EXTENDED_DESCRIPTION_REQUEST';
export const EXTENDED_DESCRIPTION_SUCCESS = 'EXTENDED_DESCRIPTION_SUCCESS';
export const EXTENDED_DESCRIPTION_FAIL = 'EXTENDED_DESCRIPTION_FAIL';
export const SERVER_DOMAIN_BLOCKS_FETCH_REQUEST = 'SERVER_DOMAIN_BLOCKS_FETCH_REQUEST';
export const SERVER_DOMAIN_BLOCKS_FETCH_SUCCESS = 'SERVER_DOMAIN_BLOCKS_FETCH_SUCCESS';
export const SERVER_DOMAIN_BLOCKS_FETCH_FAIL = 'SERVER_DOMAIN_BLOCKS_FETCH_FAIL';
export const fetchServer = () => (dispatch, getState) => {
if (getState().getIn(['server', 'server', 'isLoading'])) {
return;
}
dispatch(fetchServerRequest());
api()
.get('/api/v2/instance').then(({ data }) => {
// Only import the account if it doesn't already exist,
// because the API is cached even for logged in users.
const account = data.contact.account;
if (account) {
const existingAccount = getState().getIn(['accounts', account.id]);
if (!existingAccount) {
dispatch(importFetchedAccount(account));
}
}
dispatch(fetchServerSuccess(data));
}).catch(err => dispatch(fetchServerFail(err)));
};
const fetchServerRequest = () => ({
type: SERVER_FETCH_REQUEST,
});
const fetchServerSuccess = server => ({
type: SERVER_FETCH_SUCCESS,
server,
});
const fetchServerFail = error => ({
type: SERVER_FETCH_FAIL,
error,
});
export const fetchServerTranslationLanguages = () => (dispatch) => {
dispatch(fetchServerTranslationLanguagesRequest());
api()
.get('/api/v1/instance/translation_languages').then(({ data }) => {
dispatch(fetchServerTranslationLanguagesSuccess(data));
}).catch(err => dispatch(fetchServerTranslationLanguagesFail(err)));
};
const fetchServerTranslationLanguagesRequest = () => ({
type: SERVER_TRANSLATION_LANGUAGES_FETCH_REQUEST,
});
const fetchServerTranslationLanguagesSuccess = translationLanguages => ({
type: SERVER_TRANSLATION_LANGUAGES_FETCH_SUCCESS,
translationLanguages,
});
const fetchServerTranslationLanguagesFail = error => ({
type: SERVER_TRANSLATION_LANGUAGES_FETCH_FAIL,
error,
});
export const fetchExtendedDescription = () => (dispatch, getState) => {
if (getState().getIn(['server', 'extendedDescription', 'isLoading'])) {
return;
}
dispatch(fetchExtendedDescriptionRequest());
api()
.get('/api/v1/instance/extended_description')
.then(({ data }) => dispatch(fetchExtendedDescriptionSuccess(data)))
.catch(err => dispatch(fetchExtendedDescriptionFail(err)));
};
const fetchExtendedDescriptionRequest = () => ({
type: EXTENDED_DESCRIPTION_REQUEST,
});
const fetchExtendedDescriptionSuccess = description => ({
type: EXTENDED_DESCRIPTION_SUCCESS,
description,
});
const fetchExtendedDescriptionFail = error => ({
type: EXTENDED_DESCRIPTION_FAIL,
error,
});
export const fetchDomainBlocks = () => (dispatch, getState) => {
if (getState().getIn(['server', 'domainBlocks', 'isLoading'])) {
return;
}
dispatch(fetchDomainBlocksRequest());
api()
.get('/api/v1/instance/domain_blocks')
.then(({ data }) => dispatch(fetchDomainBlocksSuccess(true, data)))
.catch(err => {
if (err.response.status === 404) {
dispatch(fetchDomainBlocksSuccess(false, []));
} else {
dispatch(fetchDomainBlocksFail(err));
}
});
};
const fetchDomainBlocksRequest = () => ({
type: SERVER_DOMAIN_BLOCKS_FETCH_REQUEST,
});
const fetchDomainBlocksSuccess = (isAvailable, blocks) => ({
type: SERVER_DOMAIN_BLOCKS_FETCH_SUCCESS,
isAvailable,
blocks,
});
const fetchDomainBlocksFail = error => ({
type: SERVER_DOMAIN_BLOCKS_FETCH_FAIL,
error,
});

View File

@@ -0,0 +1,34 @@
import {
apiGetInstance,
apiGetExtendedDescription,
apiGetDomainBlocks,
apiGetTranslationLanguages,
} from 'mastodon/api/instance';
import { createDataLoadingThunk } from 'mastodon/store/typed_functions';
import { importFetchedAccount } from './importer';
export const fetchServer = createDataLoadingThunk(
'server/fetch',
() => apiGetInstance(),
(instance, { dispatch }) => {
if (instance.contact.account) {
dispatch(importFetchedAccount(instance.contact.account));
}
},
);
export const fetchExtendedDescription = createDataLoadingThunk(
'server/extended_description',
() => apiGetExtendedDescription(),
);
export const fetchServerTranslationLanguages = createDataLoadingThunk(
'server/translation_languages',
() => apiGetTranslationLanguages(),
);
export const fetchDomainBlocks = createDataLoadingThunk(
'server/domain_blocks',
() => apiGetDomainBlocks(),
);

View File

@@ -111,7 +111,7 @@ export function fetchStatusFail(id, error, skipLoading, parentQuotePostId) {
export function redraft(status, raw_text, quoted_status_id = null) {
return (dispatch, getState) => {
const maxOptions = getState().server.getIn(['server', 'configuration', 'polls', 'max_options']);
const maxOptions = getState().server.server.item?.configuration.polls.max_options;
dispatch({
type: REDRAFT,

View File

@@ -34,11 +34,16 @@ export const apiGetCollection = (collectionId: string) =>
`v1_alpha/collections/${collectionId}`,
);
export const apiGetAccountCollections = (accountId: string) =>
export const apiGetCollectionsCreatedByAccount = (accountId: string) =>
apiRequestGet<ApiCollectionsJSON>(
`v1_alpha/accounts/${accountId}/collections`,
);
export const apiGetCollectionsFeaturingAccount = (accountId: string) =>
apiRequestGet<ApiCollectionsJSON>(
`v1_alpha/accounts/${accountId}/in_collections`,
);
export const apiAddCollectionItem = (collectionId: string, accountId: string) =>
apiRequestPost<WrappedCollectionAccountItem>(
`v1_alpha/collections/${collectionId}/items`,

View File

@@ -2,6 +2,10 @@ import { apiRequestGet } from 'mastodon/api';
import type {
ApiTermsOfServiceJSON,
ApiPrivacyPolicyJSON,
ApiInstanceJSON,
ApiExtendedDescriptionJSON,
ApiTranslationLanguagesJSON,
ApiDomainBlockJSON,
} from 'mastodon/api_types/instance';
export const apiGetTermsOfService = (version?: string) =>
@@ -13,3 +17,17 @@ export const apiGetTermsOfService = (version?: string) =>
export const apiGetPrivacyPolicy = () =>
apiRequestGet<ApiPrivacyPolicyJSON>('v1/instance/privacy_policy');
export const apiGetInstance = () =>
apiRequestGet<ApiInstanceJSON>('v2/instance');
export const apiGetExtendedDescription = () =>
apiRequestGet<ApiExtendedDescriptionJSON>('v1/instance/extended_description');
export const apiGetTranslationLanguages = () =>
apiRequestGet<ApiTranslationLanguagesJSON>(
'v1/instance/translation_languages',
);
export const apiGetDomainBlocks = () =>
apiRequestGet<ApiDomainBlockJSON[]>('v1/instance/domain_blocks');

View File

@@ -53,7 +53,7 @@ export interface BaseApiAccountJSON {
header_static: string;
header_description: string;
id: string;
last_status_at: string;
last_status_at: string | null;
locked: boolean;
show_media: boolean;
show_media_replies: boolean;

View File

@@ -11,7 +11,8 @@ export interface ApiCollectionJSON {
account_id: string;
id: string;
uri: string | null;
uri: string;
url: string;
local: boolean;
item_count: number;
@@ -56,7 +57,7 @@ export interface CollectionAccountItem {
id: string;
account_id?: string; // Only present when state is 'accepted' (or the collection is your own)
state: 'pending' | 'accepted' | 'rejected' | 'revoked';
position: number;
created_at: string;
}
export interface WrappedCollectionAccountItem {

View File

@@ -1,3 +1,5 @@
import type { ApiAccountJSON } from './accounts';
export interface ApiTermsOfServiceJSON {
effective_date: string;
effective: boolean;
@@ -9,3 +11,136 @@ export interface ApiPrivacyPolicyJSON {
updated_at: string;
content: string;
}
interface ApiBaseRuleJSON {
text: string;
hint: string;
}
export interface ApiRuleJSON {
id: string;
text: string;
hint: string;
translations?: Record<string, ApiBaseRuleJSON>;
}
export interface ApiExtendedDescriptionJSON {
updated_at: string;
content: string;
}
export interface ApiDomainBlockJSON {
domain: string;
digest: string;
severity: string;
comment: string;
}
export type ApiTranslationLanguagesJSON = Record<string, string[]>;
export interface ApiInstanceJSON {
domain: string;
title: string;
version: string;
source_url: string;
description: string;
languages: string[];
usage: {
users: {
active_month: number;
};
};
thumbnail: {
url: string;
blurhash?: string;
description: string;
versions?: Record<string, string>;
};
contact: {
email: string | null;
account: ApiAccountJSON | null;
};
api_versions: {
mastodon: number;
};
registrations: {
enabled: boolean;
approval_required: boolean;
reason_required: boolean | null;
message: string | null;
min_age: string | null;
url: string | null;
};
rules: ApiRuleJSON[];
configuration: {
urls: {
streaming: string;
status: string | null;
about: string;
privacy_policy: string | null;
terms_of_service: string | null;
};
vapid: {
public_key: string;
};
accounts: {
max_display_name_length: number;
max_note_length: number;
max_avatar_description_length: number;
max_header_description_length: number;
max_featured_tags: number;
max_pinned_statuses: number;
max_profile_fields: number;
profile_field_name_limit: number;
profile_field_value_limit: number;
};
statuses: {
max_characters: number;
max_media_attachments: number;
characters_reserved_per_url: number;
};
media_attachments: {
description_limit: number;
image_matrix_limit: number;
image_size_limit: number;
supported_mime_types: string[];
video_frame_rate_limit: number;
video_matrix_limit: number;
video_size_limit: number;
};
polls: {
max_options: number;
max_characters_per_option: number;
min_expiration: number;
max_expiration: number;
};
translation: {
enabled: boolean;
};
timeline_access: {
live_feeds: {
local: string;
remote: string;
};
hashtag_feeds: {
local: string;
remote: string;
};
trending_link_feeds: {
local: string;
remote: string;
};
};
limited_federation: boolean;
};
}

View File

@@ -3,6 +3,7 @@
import type { AccountWarningAction } from 'mastodon/models/notification_group';
import type { ApiAccountJSON } from './accounts';
import type { ApiCollectionJSON } from './collections';
import type { ApiReportJSON } from './reports';
import type { ApiStatusJSON } from './statuses';
@@ -22,6 +23,8 @@ export const allNotificationTypes: NotificationType[] = [
'moderation_warning',
'severed_relationships',
'annual_report',
'added_to_collection',
'collection_update',
];
export type NotificationWithStatusType =
@@ -42,7 +45,9 @@ export type NotificationType =
| 'severed_relationships'
| 'admin.sign_up'
| 'admin.report'
| 'annual_report';
| 'annual_report'
| 'added_to_collection'
| 'collection_update';
export interface BaseNotificationJSON {
id: string;
@@ -83,6 +88,26 @@ interface ReportNotificationJSON extends BaseNotificationJSON {
report: ApiReportJSON;
}
interface AddedToCollectionNotificationGroupJSON extends BaseNotificationGroupJSON {
type: 'added_to_collection';
collection: ApiCollectionJSON;
}
interface AddedToCollectionNotificationJSON extends BaseNotificationJSON {
type: 'added_to_collection';
collection: ApiCollectionJSON;
}
interface CollectionUpdateNotificationGroupJSON extends BaseNotificationGroupJSON {
type: 'collection_update';
collection: ApiCollectionJSON;
}
interface CollectionUpdateNotificationJSON extends BaseNotificationJSON {
type: 'collection_update';
collection: ApiCollectionJSON;
}
type SimpleNotificationTypes = 'follow' | 'follow_request' | 'admin.sign_up';
interface SimpleNotificationGroupJSON extends BaseNotificationGroupJSON {
type: SimpleNotificationTypes;
@@ -146,7 +171,9 @@ export type ApiNotificationJSON =
| ReportNotificationJSON
| AccountRelationshipSeveranceNotificationJSON
| NotificationWithStatusJSON
| ModerationWarningNotificationJSON;
| ModerationWarningNotificationJSON
| AddedToCollectionNotificationJSON
| CollectionUpdateNotificationJSON;
export type ApiNotificationGroupJSON =
| SimpleNotificationGroupJSON
@@ -154,7 +181,9 @@ export type ApiNotificationGroupJSON =
| AccountRelationshipSeveranceNotificationGroupJSON
| NotificationGroupWithStatusJSON
| ModerationWarningNotificationGroupJSON
| AnnualReportNotificationGroupJSON;
| AnnualReportNotificationGroupJSON
| AddedToCollectionNotificationGroupJSON
| CollectionUpdateNotificationGroupJSON;
export interface ApiNotificationGroupsResultJSON {
accounts: ApiAccountJSON[];

View File

@@ -1,35 +0,0 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`<AutosuggestEmoji /> > renders emoji with custom url 1`] = `
<div
className="autosuggest-emoji"
>
<img
alt="foobar"
className="emojione"
src="http://example.com/emoji.png"
/>
<div
className="autosuggest-emoji__name"
>
:foobar:
</div>
</div>
`;
exports[`<AutosuggestEmoji /> > renders native emoji 1`] = `
<div
className="autosuggest-emoji"
>
<img
alt="💙"
className="emojione"
src="/emoji/1f499.svg"
/>
<div
className="autosuggest-emoji__name"
>
:foobar:
</div>
</div>
`;

View File

@@ -1,29 +0,0 @@
import renderer from 'react-test-renderer';
import AutosuggestEmoji from '../autosuggest_emoji';
describe('<AutosuggestEmoji />', () => {
it('renders native emoji', () => {
const emoji = {
native: '💙',
colons: ':foobar:',
};
const component = renderer.create(<AutosuggestEmoji emoji={emoji} />);
const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
it('renders emoji with custom url', () => {
const emoji = {
custom: true,
imageUrl: 'http://example.com/emoji.png',
native: 'foobar',
colons: ':foobar:',
};
const component = renderer.create(<AutosuggestEmoji emoji={emoji} />);
const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
});

View File

@@ -21,6 +21,7 @@ import { openModal } from 'mastodon/actions/modal';
import { initMuteModal } from 'mastodon/actions/mutes';
import { apiFollowAccount } from 'mastodon/api/accounts';
import { Avatar } from 'mastodon/components/avatar';
import { VerifiedBadge } from 'mastodon/components/badge';
import { Button } from 'mastodon/components/button';
import { FollowersCounter } from 'mastodon/components/counters';
import { DisplayName } from 'mastodon/components/display_name';
@@ -29,7 +30,6 @@ import { FollowButton } from 'mastodon/components/follow_button';
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
import { ShortNumber } from 'mastodon/components/short_number';
import { Skeleton } from 'mastodon/components/skeleton';
import { VerifiedBadge } from 'mastodon/components/verified_badge';
import { useIdentity } from 'mastodon/identity_context';
import { me } from 'mastodon/initial_state';
import type { MenuItem } from 'mastodon/models/dropdown_menu';
@@ -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

@@ -3,10 +3,11 @@ import type { FC } from 'react';
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import IconPinned from '@/images/icons/icon_pinned.svg?react';
import { fetchRelationships } from '@/mastodon/actions/accounts';
import { useAccount } from '@/mastodon/hooks/useAccount';
import type { AccountRole } from '@/mastodon/models/account';
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
import {
AdminBadge,
AutomatedBadge,
@@ -14,13 +15,9 @@ import {
BlockedBadge,
GroupBadge,
MutedBadge,
} from '@/mastodon/components/badge';
import { Icon } from '@/mastodon/components/icon';
import { useAccount } from '@/mastodon/hooks/useAccount';
import type { AccountRole } from '@/mastodon/models/account';
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
} from '../badge';
import classes from './redesign.module.scss';
import classes from './styles.module.scss';
export const AccountBadges: FC<{ accountId: string }> = ({ accountId }) => {
const account = useAccount(accountId);
@@ -53,7 +50,6 @@ export const AccountBadges: FC<{ accountId: string }> = ({ accountId }) => {
<AdminBadge
key={role.id}
label={role.name}
className={classes.badge}
domain={`(${domain})`}
roleId={role.id}
/>,
@@ -63,7 +59,6 @@ export const AccountBadges: FC<{ accountId: string }> = ({ accountId }) => {
<Badge
key={role.id}
label={role.name}
className={classes.badge}
domain={`(${domain})`}
roleId={role.id}
/>,
@@ -72,25 +67,19 @@ export const AccountBadges: FC<{ accountId: string }> = ({ accountId }) => {
});
if (account.bot) {
badges.push(<AutomatedBadge key='bot-badge' className={classes.badge} />);
badges.push(<AutomatedBadge key='bot-badge' />);
}
if (account.group) {
badges.push(<GroupBadge key='group-badge' className={classes.badge} />);
badges.push(<GroupBadge key='group-badge' />);
}
if (relationship) {
if (relationship.blocking) {
badges.push(
<BlockedBadge
key='blocking'
className={classNames(classes.badge, classes.badgeBlocked)}
/>,
);
badges.push(<BlockedBadge key='blocking' />);
}
if (relationship.domain_blocking) {
badges.push(
<BlockedBadge
key='domain-blocking'
className={classNames(classes.badge, classes.badgeBlocked)}
domain={domain}
label={
<FormattedMessage
@@ -105,7 +94,6 @@ export const AccountBadges: FC<{ accountId: string }> = ({ accountId }) => {
badges.push(
<MutedBadge
key='muted-badge'
className={classNames(classes.badge, classes.badgeMuted)}
expiresAt={relationship.muting_expires_at}
/>,
);
@@ -116,19 +104,9 @@ export const AccountBadges: FC<{ accountId: string }> = ({ accountId }) => {
return null;
}
return <div className={'account__header__badges'}>{badges}</div>;
return <div className={classes.badges}>{badges}</div>;
};
export const PinnedBadge: FC = () => (
<Badge
className={classes.badge}
icon={<Icon id='pinned' icon={IconPinned} />}
label={
<FormattedMessage id='account.timeline.pinned' defaultMessage='Pinned' />
}
/>
);
function isAdminBadge(role: AccountRole) {
const name = role.name.toLowerCase();
return name === 'admin' || name === 'owner';

View File

@@ -0,0 +1,140 @@
import { useCallback } from 'react';
import type { FC, ReactElement, ReactNode } from 'react';
import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router-dom';
import {
authorizeFollowRequest,
rejectFollowRequest,
} from '@/mastodon/actions/accounts';
import { useAccountVisibility } from '@/mastodon/hooks/useAccountVisibility';
import { useRelationship } from '@/mastodon/hooks/useRelationship';
import type { Account } from '@/mastodon/models/account';
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
import CheckIcon from '@/material-icons/400-24px/check.svg?react';
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
import { AvatarOverlay } from '../avatar_overlay';
import { Button } from '../button';
import { DisplayName } from '../display_name';
import { Icon } from '../icon';
import classes from './styles.module.scss';
export const AccountBanners: FC<{ account: Account }> = ({ account }) => {
const { suspended, hidden } = useAccountVisibility(account.id);
const relationship = useRelationship(account.id);
if (hidden) {
return null;
}
let banner: ReactNode = null;
if (account.memorial) {
banner = (
<MessageText>
<FormattedMessage
id='account.in_memoriam'
defaultMessage='In Memoriam.'
/>
</MessageText>
);
}
if (account.moved) {
banner = <MovedNote account={account} targetAccountId={account.moved} />;
}
if (!suspended && relationship?.requested_by) {
banner = <FollowRequestNote account={account} />;
}
if (!banner) {
return null;
}
return <div className={classes.bannerWrapper}>{banner}</div>;
};
const FollowRequestNote: FC<{ account: Account }> = ({ account }) => {
const accountId = account.id;
const dispatch = useAppDispatch();
const handleAuthorize = useCallback(() => {
dispatch(authorizeFollowRequest(accountId));
}, [accountId, dispatch]);
const handleReject = useCallback(() => {
dispatch(rejectFollowRequest(accountId));
}, [accountId, dispatch]);
return (
<>
<MessageText>
<FormattedMessage
id='account.requested_follow'
defaultMessage='{name} has requested to follow you'
values={{ name: <DisplayName account={account} variant='simple' /> }}
/>
</MessageText>
<div className={classes.bannerActions}>
<Button secondary onClick={handleAuthorize}>
<Icon id='check' icon={CheckIcon} />
<FormattedMessage
id='follow_request.authorize'
defaultMessage='Authorize'
/>
</Button>
<Button secondary onClick={handleReject}>
<Icon id='times' icon={CloseIcon} />
<FormattedMessage
id='follow_request.reject'
defaultMessage='Reject'
/>
</Button>
</div>
</>
);
};
const MovedNote: React.FC<{
account: Account;
targetAccountId: string;
}> = ({ account: from, targetAccountId }) => {
const to = useAppSelector((state) => state.accounts.get(targetAccountId));
return (
<>
<MessageText>
<FormattedMessage
id='account.moved_to'
defaultMessage='{name} has indicated that their new account is now:'
values={{
name: <DisplayName account={from} variant='simple' />,
}}
/>
</MessageText>
<div className={classes.bannerActions}>
<Link to={`/@${to?.acct}`} className={classes.bannerActionsDisplayName}>
<AvatarOverlay account={to} friend={from} />
<DisplayName account={to} />
</Link>
<Link to={`/@${to?.acct}`} className='button'>
<FormattedMessage
id='account.go_to_profile'
defaultMessage='Go to profile'
/>
</Link>
</div>
</>
);
};
const MessageText: React.FC<{ children: ReactElement }> = ({ children }) => (
<div className={classes.bannerText}>{children}</div>
);

View File

@@ -3,12 +3,7 @@ import type { FC } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import classNames from 'classnames';
import { followAccount } from '@/mastodon/actions/accounts';
import { CopyIconButton } from '@/mastodon/components/copy_icon_button';
import { FollowButton } from '@/mastodon/components/follow_button';
import { IconButton } from '@/mastodon/components/icon_button';
import { useAccount } from '@/mastodon/hooks/useAccount';
import { getAccountHidden } from '@/mastodon/selectors/accounts';
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
@@ -16,7 +11,12 @@ import NotificationsIcon from '@/material-icons/400-24px/notifications.svg?react
import NotificationsActiveIcon from '@/material-icons/400-24px/notifications_active-fill.svg?react';
import ShareIcon from '@/material-icons/400-24px/share.svg?react';
import { CopyIconButton } from '../copy_icon_button';
import { FollowButton } from '../follow_button';
import { IconButton } from '../icon_button';
import { AccountMenu } from './menu';
import classes from './styles.module.scss';
const messages = defineMessages({
enableNotifications: {
@@ -48,7 +48,7 @@ export const AccountButtons: FC<AccountButtonsProps> = ({
const me = useAppSelector((state) => state.meta.get('me') as string);
return (
<div className={classNames('account__header__buttons', className)}>
<div className={className}>
{!hidden && (
<AccountButtonsOther accountId={accountId} noShare={noShare} />
)}
@@ -93,7 +93,7 @@ const AccountButtonsOther: FC<
{!isMovedAndUnfollowedAccount && (
<FollowButton
accountId={accountId}
className='account__header__follow-button'
className={classes.followButton}
labelLength='long'
/>
)}

View File

@@ -7,23 +7,23 @@ import classNames from 'classnames';
import IconVerified from '@/images/icons/icon_verified.svg?react';
import { openModal } from '@/mastodon/actions/modal';
import { CustomEmojiProvider } from '@/mastodon/components/emoji/context';
import type { EmojiHTMLProps } from '@/mastodon/components/emoji/html';
import { EmojiHTML } from '@/mastodon/components/emoji/html';
import { Icon } from '@/mastodon/components/icon';
import { IconButton } from '@/mastodon/components/icon_button';
import { MiniCard } from '@/mastodon/components/mini_card';
import { useElementHandledLink } from '@/mastodon/components/status/handled_link';
import { useFieldHtml } from '@/mastodon/features/account_timeline/hooks/useFieldHtml';
import { cleanExtraEmojis } from '@/mastodon/features/emoji/normalize';
import { useAccount } from '@/mastodon/hooks/useAccount';
import { useResizeObserver } from '@/mastodon/hooks/useObserver';
import type { AccountFieldShape } from '@/mastodon/models/account';
import { useAppDispatch } from '@/mastodon/store';
import MoreIcon from '@/material-icons/400-24px/more_horiz.svg?react';
import { cleanExtraEmojis } from '../../emoji/normalize';
import type { AccountField } from '../common';
import { useFieldHtml } from '../hooks/useFieldHtml';
import { CustomEmojiProvider } from '../emoji/context';
import type { EmojiHTMLProps } from '../emoji/html';
import { EmojiHTML } from '../emoji/html';
import { Icon } from '../icon';
import { IconButton } from '../icon_button';
import { MiniCard } from '../mini_card';
import { useElementHandledLink } from '../status/handled_link';
import classes from './redesign.module.scss';
import classes from './styles.module.scss';
const verifyMessage = defineMessage({
id: 'account.link_verified_on',
@@ -37,6 +37,12 @@ const dateFormatOptions: Intl.DateTimeFormatOptions = {
minute: '2-digit',
};
export interface AccountField extends AccountFieldShape {
nameHasEmojis: boolean;
value_plain: string;
valueHasEmojis: boolean;
}
export const AccountHeaderFields: FC<{ accountId: string }> = ({
accountId,
}) => {
@@ -102,11 +108,9 @@ const FieldCard: FC<{
}> = ({ htmlHandlers, field }) => {
const intl = useIntl();
const {
name,
name_emojified,
nameHasEmojis,
value_emojified,
value_plain,
valueHasEmojis,
verified_at,
} = field;
@@ -132,8 +136,7 @@ const FieldCard: FC<{
)}
label={
<FieldHTML
text={name}
textEmojified={name_emojified}
text={name_emojified}
textHasCustomEmoji={nameHasEmojis}
className='translate'
isOverflowing={isLabelOverflowing}
@@ -143,8 +146,7 @@ const FieldCard: FC<{
}
value={
<FieldHTML
text={value_plain}
textEmojified={value_emojified}
text={value_emojified}
textHasCustomEmoji={valueHasEmojis}
isOverflowing={isValueOverflowing}
onOverflowClick={handleOverflowClick}
@@ -169,7 +171,6 @@ const FieldCard: FC<{
type FieldHTMLProps = {
text: string;
textEmojified: string;
textHasCustomEmoji: boolean;
isOverflowing?: boolean;
onOverflowClick?: () => void;
@@ -177,9 +178,7 @@ type FieldHTMLProps = {
const FieldHTML: FC<FieldHTMLProps> = ({
className,
extraEmojis,
text,
textEmojified,
textHasCustomEmoji,
isOverflowing,
onOverflowClick,
@@ -192,7 +191,7 @@ const FieldHTML: FC<FieldHTMLProps> = ({
const html = (
<EmojiHTML
as='span'
htmlString={textEmojified}
htmlString={text}
className={className}
onElement={handleElement}
data-contents
@@ -285,11 +284,14 @@ function useColumnWrap() {
if (!item) {
break;
}
const { ele, span } = item;
if (i < row.length - 1) {
ele.dataset.cols = span.toString();
remainingRowSpan -= span;
} else if (
row.length > 1 &&
row.length === halfColSpan &&
span === 1 &&
remainingRowSpan > 1
@@ -315,9 +317,10 @@ function useColumnWrap() {
if (element) {
listRef.current = element;
observer.observe(element);
handleRecalculate();
}
},
[observer],
[handleRecalculate, observer],
);
return { wrapperRef: wrapperRefCallback };

View File

@@ -1,13 +1,10 @@
import { useCallback } from 'react';
import classNames from 'classnames';
import { Helmet } from 'react-helmet';
import { Helmet } from '@unhead/react/helmet';
import { openModal } from '@/mastodon/actions/modal';
import { AccountBio } from '@/mastodon/components/account_bio';
import { Avatar } from '@/mastodon/components/avatar';
import { AnimateEmojiProvider } from '@/mastodon/components/emoji/context';
import FollowRequestNoteContainer from '@/mastodon/features/account/containers/follow_request_note_container';
import { useLayout } from '@/mastodon/hooks/useLayout';
import { useVisibility } from '@/mastodon/hooks/useVisibility';
import {
@@ -19,17 +16,19 @@ import type { Account } from '@/mastodon/models/account';
import { getAccountHidden } from '@/mastodon/selectors/accounts';
import { useAppSelector, useAppDispatch } from '@/mastodon/store';
import { AccountName } from './account_name';
import { AccountSubscriptionForm } from './account_subscription_form';
import { AccountBadges } from './badges';
import { AccountBio } from '../account_bio';
import { Avatar } from '../avatar';
import { AnimateEmojiProvider } from '../emoji/context';
import { FamiliarFollowers } from '../familiar_followers';
import { AccountBanners } from './banners';
import { AccountButtons } from './buttons';
import { FamiliarFollowers } from './familiar_followers';
import { AccountHeaderFields } from './fields';
import { MemorialNote } from './memorial_note';
import { MovedNote } from './moved_note';
import { AccountNote as AccountNoteRedesign } from './note';
import { AccountName } from './name';
import { AccountNote } from './note';
import { AccountNumberFields } from './number_fields';
import redesignClasses from './redesign.module.scss';
import classes from './styles.module.scss';
import { AccountSubscriptionForm } from './subscription_form';
import { AccountTabs } from './tabs';
const titleFromAccount = (account: Account) => {
@@ -50,9 +49,6 @@ export const AccountHeader: React.FC<{
}> = ({ accountId, hideTabs }) => {
const dispatch = useAppDispatch();
const account = useAppSelector((state) => state.accounts.get(accountId));
const relationship = useAppSelector((state) =>
state.relationships.get(accountId),
);
const hidden = useAppSelector((state) => getAccountHidden(state, accountId));
const handleOpenAvatar = useCallback(
@@ -96,27 +92,13 @@ export const AccountHeader: React.FC<{
const isMe = me && account.id === me;
return (
<div className='account-timeline__header'>
{!hidden && account.memorial && <MemorialNote />}
{!hidden && account.moved && (
<MovedNote accountId={account.id} targetAccountId={account.moved} />
)}
<div>
<AccountBanners account={account} />
<AnimateEmojiProvider
className={classNames('account__header', {
inactive: !!account.moved,
})}
className={classNames(!!account.moved && classes.moved)}
>
{!suspendedOrHidden && !account.moved && relationship?.requested_by && (
<FollowRequestNoteContainer account={account} />
)}
<div
className={classNames(
'account__header__image',
redesignClasses.header,
)}
>
<div className={classes.header}>
{!suspendedOrHidden && (
<img
src={autoPlayGif ? account.header : account.header_static}
@@ -126,26 +108,16 @@ export const AccountHeader: React.FC<{
)}
</div>
<div
className={classNames(
'account__header__bar',
redesignClasses.barWrapper,
)}
>
<div
className={classNames(
'account__header__tabs',
redesignClasses.avatarWrapper,
)}
>
<div className={classes.barWrapper}>
<div className={classes.avatarWrapper}>
<a
className='avatar'
href={account.avatar}
rel='noopener'
target='_blank'
onClick={handleOpenAvatar}
>
<Avatar
className={classes.avatar}
account={suspendedOrHidden ? undefined : account}
alt={account.avatar_description}
size={80}
@@ -153,47 +125,36 @@ export const AccountHeader: React.FC<{
</a>
</div>
<div
className={classNames(
'account__header__tabs__name',
redesignClasses.nameWrapper,
)}
>
<div className={classes.displayNameWrapper}>
<AccountName accountId={accountId} />
<AccountButtons
accountId={accountId}
className={redesignClasses.buttonsDesktop}
className={classes.buttonsDesktop}
noShare={!isMe || 'share' in navigator}
forceMenu={'share' in navigator}
/>
</div>
<AccountBadges accountId={accountId} />
<AccountNumberFields accountId={accountId} />
{!isMe && !suspendedOrHidden && (
<FamiliarFollowers accountId={accountId} />
<FamiliarFollowers
accountId={accountId}
className={classes.familiarFollowers}
/>
)}
{!suspendedOrHidden && (
<div className='account__header__extra'>
<div className='account__header__bio'>
{me && account.id !== me && (
<AccountNoteRedesign accountId={accountId} />
)}
<div className={classes.bioButtonsWrapper}>
{me && account.id !== me && <AccountNote accountId={accountId} />}
<AccountBio
showDropdown
accountId={accountId}
className={classNames(
'account__header__content',
redesignClasses.bio,
)}
/>
<AccountBio
showDropdown
accountId={accountId}
className={classes.bio}
/>
<AccountHeaderFields accountId={accountId} />
</div>
<AccountHeaderFields accountId={accountId} />
{!me && account.email_subscriptions && (
<AccountSubscriptionForm accountId={accountId} />
@@ -203,8 +164,8 @@ export const AccountHeader: React.FC<{
<AccountButtons
className={classNames(
redesignClasses.buttonsMobile,
!isIntersecting && redesignClasses.buttonsMobileIsStuck,
classes.buttonsMobile,
!isIntersecting && classes.buttonsMobileIsStuck,
)}
accountId={accountId}
noShare

View File

@@ -4,7 +4,6 @@ import type { FC } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import {
blockAccount,
followAccount,
pinAccount,
unblockAccount,
@@ -13,6 +12,7 @@ import {
} from '@/mastodon/actions/accounts';
import { removeAccountFromFollowers } from '@/mastodon/actions/accounts_typed';
import { showAlert } from '@/mastodon/actions/alerts';
import { initBlockModal } from '@/mastodon/actions/blocks';
import { directCompose, mentionCompose } from '@/mastodon/actions/compose';
import {
initDomainBlockModal,
@@ -21,7 +21,10 @@ import {
import { openModal } from '@/mastodon/actions/modal';
import { initMuteModal } from '@/mastodon/actions/mutes';
import { initReport } from '@/mastodon/actions/reports';
import { Dropdown } from '@/mastodon/components/dropdown_menu';
import {
canAccountBeAdded,
canAccountBeAddedByFollowers,
} from '@/mastodon/features/collections/utils';
import { useAccount } from '@/mastodon/hooks/useAccount';
import { useIdentity } from '@/mastodon/identity_context';
import type { Account } from '@/mastodon/models/account';
@@ -40,7 +43,9 @@ import PersonRemoveIcon from '@/material-icons/400-24px/person_remove.svg?react'
import ReportIcon from '@/material-icons/400-24px/report.svg?react';
import ShareIcon from '@/material-icons/400-24px/share.svg?react';
import classes from './redesign.module.scss';
import { Dropdown } from '../dropdown_menu';
import classes from './styles.module.scss';
export const AccountMenu: FC<{ accountId: string }> = ({ accountId }) => {
const intl = useIntl();
@@ -61,7 +66,7 @@ export const AccountMenu: FC<{ accountId: string }> = ({ accountId }) => {
return [];
}
return redesignMenuItems({
return getMenuItems({
account,
signedIn: !isMe && signedIn,
permissions,
@@ -213,6 +218,10 @@ const redesignMessages = defineMessages({
id: 'account.menu.add_to_list',
defaultMessage: 'Add to list…',
},
addToCollection: {
id: 'account.menu.add_to_collection',
defaultMessage: 'Add to collection…',
},
openOriginalPage: {
id: 'account.menu.open_original_page',
defaultMessage: 'View on {domain}',
@@ -223,7 +232,7 @@ const redesignMessages = defineMessages({
},
});
function redesignMenuItems({
function getMenuItems({
account,
signedIn,
permissions,
@@ -293,35 +302,57 @@ function redesignMenuItems({
return items;
}
// List and featuring options
// Add to list
if (relationship?.following) {
items.push(
{
text: intl.formatMessage(redesignMessages.addToList),
action: () => {
dispatch(
openModal({
modalType: 'LIST_ADDER',
modalProps: {
accountId: account.id,
},
}),
);
},
items.push({
text: intl.formatMessage(redesignMessages.addToList),
action: () => {
dispatch(
openModal({
modalType: 'LIST_ADDER',
modalProps: {
accountId: account.id,
},
}),
);
},
{
text: intl.formatMessage(
relationship.endorsed ? messages.unendorse : messages.endorse,
),
action: () => {
if (relationship.endorsed) {
dispatch(unpinAccount(account.id));
} else {
dispatch(pinAccount(account.id));
}
},
});
}
// Add to collection
if (
canAccountBeAdded(account) ||
(canAccountBeAddedByFollowers(account) && relationship?.following)
) {
items.push({
text: intl.formatMessage(redesignMessages.addToCollection),
action: () => {
dispatch(
openModal({
modalType: 'COLLECTION_ADDER',
modalProps: {
accountId: account.id,
},
}),
);
},
);
});
}
// Feature on profile
if (relationship?.following) {
items.push({
text: intl.formatMessage(
relationship.endorsed ? messages.unendorse : messages.endorse,
),
action: () => {
if (relationship.endorsed) {
dispatch(unpinAccount(account.id));
} else {
dispatch(pinAccount(account.id));
}
},
});
}
items.push(
@@ -435,7 +466,7 @@ function redesignMenuItems({
if (relationship?.blocking) {
dispatch(unblockAccount(account.id));
} else {
dispatch(blockAccount(account.id));
dispatch(initBlockModal(account));
}
},
dangerous: true,

View File

@@ -7,15 +7,22 @@ import classNames from 'classnames';
import Overlay from 'react-overlays/esm/Overlay';
import { DisplayName } from '@/mastodon/components/display_name';
import { Icon } from '@/mastodon/components/icon';
import { showAlert } from '@/mastodon/actions/alerts';
import { useAccount } from '@/mastodon/hooks/useAccount';
import { useAppSelector } from '@/mastodon/store';
import { useRelationship } from '@/mastodon/hooks/useRelationship';
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
import AtIcon from '@/material-icons/400-24px/alternate_email.svg?react';
import ContentCopyIcon from '@/material-icons/400-24px/content_copy.svg?react';
import HelpIcon from '@/material-icons/400-24px/help.svg?react';
import DomainIcon from '@/material-icons/400-24px/language.svg?react';
import classes from './redesign.module.scss';
import { FollowsYouBadge } from '../badge';
import { Button } from '../button';
import { DisplayName } from '../display_name';
import { Icon } from '../icon';
import { AccountBadges } from './badges';
import classes from './styles.module.scss';
const messages = defineMessages({
lockedInfo: {
@@ -27,6 +34,10 @@ const messages = defineMessages({
id: 'account.name_info',
defaultMessage: 'What does this mean?',
},
copied: {
id: 'copy_icon_button.copied',
defaultMessage: 'Copied to clipboard',
},
});
export const AccountName: FC<{ accountId: string }> = ({ accountId }) => {
@@ -35,6 +46,7 @@ export const AccountName: FC<{ accountId: string }> = ({ accountId }) => {
const localDomain = useAppSelector(
(state) => state.meta.get('domain') as string,
);
const relationship = useRelationship(accountId);
if (!account) {
return null;
@@ -43,18 +55,21 @@ export const AccountName: FC<{ accountId: string }> = ({ accountId }) => {
const [username = '', domain = localDomain] = account.acct.split('@');
return (
<div className={classes.name}>
<h1>
<DisplayName account={account} variant='simple' />
</h1>
<p className={classes.username}>
@{username}@{domain}
<AccountNameHelp
username={username}
domain={domain}
isSelf={account.id === me}
/>
</p>
<div className={classes.nameWrapper}>
<div className={classes.name}>
<h1>
<DisplayName account={account} variant='simple' />
</h1>
{relationship?.followed_by && <FollowsYouBadge />}
</div>
<AccountNameHelp
username={username}
domain={domain}
isSelf={account.id === me}
/>
<AccountBadges accountId={accountId} />
</div>
);
};
@@ -73,6 +88,19 @@ const AccountNameHelp: FC<{
setOpen((prev) => !prev);
}, []);
const handle = `@${username}@${domain}`;
const dispatch = useAppDispatch();
const [copied, setCopied] = useState(false);
const handleCopy = useCallback(() => {
void navigator.clipboard.writeText(handle);
setCopied(true);
dispatch(showAlert({ message: messages.copied }));
setTimeout(() => {
setCopied(false);
}, 700);
}, [handle, dispatch]);
return (
<>
<button
@@ -83,6 +111,7 @@ const AccountNameHelp: FC<{
aria-expanded={open}
aria-controls={accessibilityId}
>
{handle}
<Icon
id='help'
icon={HelpIcon}
@@ -149,9 +178,25 @@ const AccountNameHelp: FC<{
</ol>
<FormattedMessage
id='account.name.help.footer'
defaultMessage='Just like you can send emails to people using different email clients, you can interact with people on other Mastodon servers and with anyone on other social apps powered by the same set of rules as Mastodon uses (the ActivityPub protocol).'
defaultMessage='Just like you can send emails to people using different email providers, you can interact with people on other Mastodon servers, and with anyone on other Fediverse apps.'
tagName='p'
/>
<Button onClick={handleCopy} className={classes.handleCopy}>
<Icon id='copy' icon={ContentCopyIcon} />
{!copied && (
<FormattedMessage
id='account.name.copy'
defaultMessage='Copy handle'
/>
)}
{copied && (
<FormattedMessage
id='copypaste.copied'
defaultMessage='Copied'
/>
)}
</Button>
</div>
)}
</Overlay>

View File

@@ -5,12 +5,13 @@ import { defineMessages, useIntl } from 'react-intl';
import { fetchRelationships } from '@/mastodon/actions/accounts';
import { openModal } from '@/mastodon/actions/modal';
import { Callout } from '@/mastodon/components/callout';
import { IconButton } from '@/mastodon/components/icon_button';
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
import EditIcon from '@/material-icons/400-24px/edit_square.svg?react';
import classes from './redesign.module.scss';
import { Callout } from '../callout';
import { IconButton } from '../icon_button';
import classes from './styles.module.scss';
const messages = defineMessages({
title: {

View File

@@ -1,15 +1,17 @@
import { useMemo } from 'react';
import { useCallback, useMemo } from 'react';
import type { FC } from 'react';
import { FormattedMessage, useIntl } from 'react-intl';
import { FormattedDateWrapper } from '@/mastodon/components/formatted_date';
import {
NumberFields,
NumberFieldsItem,
} from '@/mastodon/components/number_fields';
import { ShortNumber } from '@/mastodon/components/short_number';
import { openModal } from '@/mastodon/actions/modal';
import { useAccount } from '@/mastodon/hooks/useAccount';
import { useAppDispatch } from '@/mastodon/store';
import { FormattedDateWrapper } from '../formatted_date';
import { NumberFields, NumberFieldsItem } from '../number_fields';
import { ShortNumber } from '../short_number';
import classes from './styles.module.scss';
export const AccountNumberFields: FC<{ accountId: string }> = ({
accountId,
@@ -21,12 +23,19 @@ export const AccountNumberFields: FC<{ accountId: string }> = ({
[account?.created_at],
);
const dispatch = useAppDispatch();
const showJoinModal = useCallback(() => {
dispatch(
openModal({ modalType: 'ACCOUNT_JOIN_DATE', modalProps: { accountId } }),
);
}, [accountId, dispatch]);
if (!account) {
return null;
}
return (
<NumberFields>
<NumberFields className={classes.numberFields}>
<NumberFieldsItem
label={
<FormattedMessage id='account.followers' defaultMessage='Followers' />
@@ -60,15 +69,17 @@ export const AccountNumberFields: FC<{ accountId: string }> = ({
}
hint={intl.formatDate(account.created_at)}
>
{createdThisYear ? (
<FormattedDateWrapper
value={account.created_at}
month='short'
day='2-digit'
/>
) : (
<FormattedDateWrapper value={account.created_at} year='numeric' />
)}
<button type='button' onClick={showJoinModal}>
{createdThisYear ? (
<FormattedDateWrapper
value={account.created_at}
month='short'
day='2-digit'
/>
) : (
<FormattedDateWrapper value={account.created_at} year='numeric' />
)}
</button>
</NumberFieldsItem>
</NumberFields>
);

View File

@@ -1,62 +1,118 @@
.moved {
opacity: 0.5;
}
// Account header
.header {
height: 120px;
overflow: hidden;
background: var(--color-bg-secondary);
border-bottom: 1px solid var(--color-border-primary);
img {
object-fit: cover;
display: block;
width: 100%;
height: 100%;
margin: 0;
}
@container (width >= 500px) {
height: 160px;
}
}
.barWrapper {
border-bottom: none;
padding-inline: 16px;
}
.avatarWrapper {
margin-top: -64px;
padding-top: 0;
}
.nameWrapper {
display: flex;
align-items: start;
gap: 16px;
}
.name {
flex-grow: 1;
> h1 {
font-size: 22px;
line-height: normal;
white-space: initial;
.moved & {
filter: grayscale(100%);
}
}
.username {
// Wraps everything except the header image.
.barWrapper {
padding-inline: 16px;
}
// Avatar
.avatarWrapper {
margin-top: -64px;
padding-top: 0;
display: flex;
font-size: 13px;
color: var(--color-text-secondary);
align-items: center;
user-select: all;
margin-top: 4px;
align-items: flex-start;
justify-content: space-between;
gap: 8px;
overflow: hidden;
margin-inline-start: -2px; // aligns the pfp with content below
}
.avatar {
background: var(--color-bg-primary);
border: 1px solid var(--color-border-primary);
border-radius: var(--avatar-border-radius);
.moved & {
filter: grayscale(100%);
}
}
.displayNameWrapper {
display: flex;
align-items: start;
gap: 16px;
margin-top: 16px;
margin-bottom: 16px;
h1 {
font-size: 17px;
line-height: 22px;
color: var(--color-text-primary);
font-weight: 600;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
:global(.emojione) {
width: 22px;
height: 22px;
}
}
.nameWrapper {
flex-grow: 1;
min-width: 0;
overflow-wrap: break-word;
}
.name {
> h1 {
display: inline;
font-size: 22px;
line-height: normal;
white-space: initial;
margin-inline-end: 4px;
}
}
.badges {
margin-top: 8px;
}
.handleHelpButton {
appearance: none;
border: none;
background: none;
display: flex;
gap: 2px;
padding: 0;
color: inherit;
font-size: 1em;
margin-left: 2px;
width: 16px;
height: 16px;
margin-top: 4px;
appearance: none;
background: none;
border: none;
color: var(--color-text-secondary);
font-size: 13px;
transition: color 0.2s ease-in-out;
word-break: break-all;
text-align: left;
> svg {
width: 100%;
height: 100%;
width: 16px;
height: 16px;
}
&:hover,
@@ -74,6 +130,10 @@
max-width: 400px;
box-sizing: border-box;
[data-color-scheme='dark'] & {
border: 1px solid var(--color-border-primary);
}
> h3 {
font-size: 17px;
font-weight: 600;
@@ -91,15 +151,15 @@
&:first-child {
margin-bottom: 12px;
}
}
svg {
background: var(--color-bg-brand-softest);
width: 28px;
height: 28px;
padding: 5px;
border-radius: 9999px;
box-sizing: border-box;
> svg {
background: var(--color-bg-brand-softest);
width: 28px;
height: 28px;
padding: 5px;
border-radius: 9999px;
box-sizing: border-box;
}
}
strong {
@@ -107,9 +167,53 @@
}
}
.handleCopy {
border: 1px solid var(--color-border-primary);
border-radius: 8px;
box-sizing: border-box;
padding: 4px 8px;
background-color: var(--color-bg-primary);
color: var(--color-text-primary);
font-size: 13px;
transition:
color 0.2s ease-in-out,
background-color 0.2s ease-in-out;
margin-top: 12px;
&:active,
&:focus,
&:hover {
background-color: var(--color-bg-brand-softest);
}
}
$button-breakpoint: 420px;
$button-fallback-breakpoint: $button-breakpoint + 55px;
.buttonsDesktop,
.buttonsMobile {
display: flex;
align-items: center;
gap: 8px;
:global(.button) {
flex-shrink: 1;
white-space: nowrap;
min-width: 80px;
}
:global(.icon-button) {
border: 1px solid var(--color-border-primary);
border-radius: 4px;
box-sizing: content-box;
padding: 5px;
&:global(.copied) {
border-color: var(--color-text-success);
}
}
}
.buttonsDesktop {
@container (width < #{$button-breakpoint}) {
display: none;
@@ -161,40 +265,36 @@ $button-fallback-breakpoint: $button-breakpoint + 55px;
}
}
.numberFields {
@container (width >= #{$button-breakpoint}) {
--number-fields-gap: 40px;
}
}
.bio {
font-size: 15px;
}
color: var(--color-text-primary);
unicode-bidi: plaintext;
.badge {
background-color: var(--color-bg-secondary);
border: none;
color: var(--color-text-secondary);
font-weight: 500;
padding: 4px;
font-size: 13px;
p {
margin-bottom: 20px;
:global(.account__header__badges) > & {
line-height: 1;
&:last-child {
margin-bottom: 0;
}
}
> span {
font-weight: unset;
opacity: 1;
:any-link {
color: var(--color-text-status-links);
&:hover {
text-decoration: none;
}
}
}
.badgeMuted {
background-color: var(--color-bg-inverted);
color: var(--color-text-inverted);
}
.badgeBlocked {
background-color: var(--color-bg-error-base);
color: var(--color-text-on-error-base);
}
svg.badgeIcon {
opacity: 1;
.familiarFollowers {
margin-top: 16px;
}
.note {
@@ -273,8 +373,9 @@ svg.badgeIcon {
.fieldVerified {
background-color: var(--color-bg-success-softest);
dt {
padding-right: 24px;
&.fieldItem {
border-color: var(--color-border-success-soft);
padding-right: 32px; // 8px padding + 16px for the icon + 8px gap
}
}
@@ -351,63 +452,52 @@ svg.badgeIcon {
}
}
.tabs,
.noTabs {
border-bottom: 1px solid var(--color-border-primary);
}
.tabs {
display: flex;
gap: 12px;
padding: 0 16px;
@container (width < 500px) {
a {
flex: 1 1 0px;
text-align: center;
}
}
a {
display: block;
font-size: 15px;
font-weight: 500;
padding: 18px 4px;
text-decoration: none;
color: var(--color-text-primary);
border-radius: 0;
transition: color 0.2s ease-in-out;
&:not([aria-current='page']):is(:hover, :focus) {
color: var(--color-text-brand-soft);
}
}
:global(.active) {
color: var(--color-text-brand);
border-bottom: 4px solid var(--color-border-brand);
padding-bottom: 14px;
}
}
.noTabs {
width: 100%;
border-width: 0 0 1px;
border-bottom: 1px solid var(--color-border-primary);
}
// Banners
.bannerWrapper,
.bannerBase {
box-sizing: border-box;
display: flex;
flex-direction: column;
}
.bannerWrapper {
background: var(--color-bg-tertiary);
padding: 16px;
align-items: center;
}
.bannerBase {
box-sizing: border-box;
padding: 16px;
border-radius: 12px;
background: var(--color-bg-secondary);
display: flex;
flex-direction: column;
gap: 12px;
justify-content: center;
align-items: flex-start;
margin: 16px 0;
}
.bannerBaseCentered {
min-height: 146px;
align-items: center;
.bannerTextAndActions {
text-align: center;
}
}
.bannerText {
color: var(--color-text-secondary);
font-size: 14px;
font-weight: 500;
text-align: center;
}
.bannerTextAndActions {
display: flex;
flex-direction: column;
@@ -422,22 +512,22 @@ svg.badgeIcon {
}
}
.bannerDisclaimer {
color: var(--color-text-secondary);
font-size: 11px;
.bannerActions {
display: flex;
justify-content: space-between;
align-items: center;
gap: 15px;
width: 100%;
margin-top: 16px;
a {
color: inherit;
button {
width: 100%;
}
}
.bannerBaseCentered {
composes: bannerBase;
min-height: 146px;
align-items: center;
.bannerTextAndActions {
text-align: center;
.bannerDisclaimer {
a {
color: inherit;
}
}
@@ -451,8 +541,52 @@ svg.badgeIcon {
flex-grow: 1;
}
label {
font-weight: 400;
}
:global(.button) {
margin-top: 24px; // To align with input under label
}
input[type='email'] {
padding: 7px 8px; // To align size with button
background: var(--color-bg-primary);
}
}
.bannerActionsDisplayName {
color: var(--color-text-secondary);
display: flex;
align-items: center;
gap: 10px;
font-size: 15px;
line-height: 22px;
overflow: hidden;
text-decoration: none;
&:hover strong {
text-decoration: underline;
}
strong,
span {
display: block;
text-overflow: ellipsis;
overflow: hidden;
}
strong {
color: var(--color-text-primary);
}
}
// Buttons
.followButton {
flex-grow: 1;
}
.bioButtonsWrapper {
margin-top: 16px;
}

View File

@@ -3,25 +3,24 @@ import { useState, useCallback, useId } from 'react';
import { FormattedMessage, useIntl, defineMessages } from 'react-intl';
import type { IntlShape } from 'react-intl';
import classNames from 'classnames';
import { Link } from 'react-router-dom';
import { AxiosError } from 'axios';
import { apiSubscribeByEmail } from 'mastodon/api/accounts';
import { apiSubscribeByEmail } from '@/mastodon/api/accounts';
import type {
ValidationErrorResponse,
ValidationError,
} from 'mastodon/api_types/errors';
import { A11yLiveRegion } from 'mastodon/components/a11y_live_region';
import { Button } from 'mastodon/components/button';
import { CalloutInline } from 'mastodon/components/callout_inline';
import { DisplayName } from 'mastodon/components/display_name';
import type { FieldStatus } from 'mastodon/components/form_fields';
import formFieldClasses from 'mastodon/components/form_fields/form_field_wrapper.module.scss';
import { TextInput } from 'mastodon/components/form_fields/text_input_field';
import { useAppSelector } from 'mastodon/store';
} from '@/mastodon/api_types/errors';
import { useAppSelector } from '@/mastodon/store';
import classes from './redesign.module.scss';
import { Button } from '../button';
import { DisplayName } from '../display_name';
import type { FieldStatus } from '../form_fields';
import { TextInputField } from '../form_fields/text_input_field';
import classes from './styles.module.scss';
const messages = defineMessages({
emailInvalid: {
@@ -34,7 +33,7 @@ const messages = defineMessages({
},
email: {
id: 'email_subscriptions.email',
defaultMessage: 'Email address',
defaultMessage: 'Email',
},
});
@@ -105,8 +104,6 @@ export const AccountSubscriptionForm: React.FC<{ accountId: string }> = ({
.then(() => {
setSubmitting(false);
setSubmitted(true);
return '';
})
.catch((err: unknown) => {
setSubmitting(false);
@@ -130,14 +127,15 @@ export const AccountSubscriptionForm: React.FC<{ accountId: string }> = ({
if (submitted) {
return (
<div className={classes.bannerBaseCentered}>
<div
className={classNames(classes.bannerBase, classes.bannerBaseCentered)}
>
<div className={classes.bannerTextAndActions}>
<h2>
<FormattedMessage
id='email_subscriptions.submitted.title'
defaultMessage='One more step'
/>
</h2>
<FormattedMessage
id='email_subscriptions.submitted.title'
defaultMessage='One more step'
tagName='h2'
/>
<FormattedMessage
id='email_subscriptions.submitted.lead'
defaultMessage='Check your inbox for an email to finish signing up for email updates.'
@@ -150,42 +148,27 @@ export const AccountSubscriptionForm: React.FC<{ accountId: string }> = ({
return (
<form onSubmit={handleSubmit} className={classes.bannerBase} noValidate>
<div className={classes.bannerTextAndActions}>
<h2>
<FormattedMessage
id='email_subscriptions.form.title'
defaultMessage='Sign up for email updates from {name}'
values={{
name: <DisplayName account={account} variant='simple' />,
}}
/>
</h2>
<FormattedMessage
id='email_subscriptions.form.lead'
defaultMessage='Get posts in your inbox without creating a Mastodon account.'
id='email_subscriptions.form.title'
defaultMessage='Sign up for email updates from {name}'
tagName='h2'
values={{
name: <DisplayName account={account} variant='simple' />,
}}
/>
</div>
<div className={classes.bannerInputButton}>
<div className={formFieldClasses.wrapper}>
<TextInput
id={`${accessibilityId}-input`}
type='email'
value={email}
onChange={handleChange}
placeholder='name@email.com'
aria-label={intl.formatMessage(messages.email)}
aria-describedby={errors.email ? `${accessibilityId}-status` : ''}
/>
<A11yLiveRegion
className={formFieldClasses.status}
id={`${accessibilityId}-status`}
>
{errors.email && (
<CalloutInline {...fieldStatusFromErrors(intl, errors.email)} />
)}
</A11yLiveRegion>
</div>
<TextInputField
id={`${accessibilityId}-input`}
type='email'
value={email}
onChange={handleChange}
label={intl.formatMessage(messages.email)}
status={
errors.email ? fieldStatusFromErrors(intl, errors.email) : undefined
}
/>
<Button type='submit' loading={submitting}>
<FormattedMessage
@@ -197,8 +180,8 @@ export const AccountSubscriptionForm: React.FC<{ accountId: string }> = ({
<div className={classes.bannerDisclaimer}>
<FormattedMessage
id='email_subscriptions.form.disclaimer'
defaultMessage='You can unsubscribe at any time. For more information, refer to the <a>Privacy Policy</a>.'
id='email_subscriptions.form.bottom'
defaultMessage='Get posts in your inbox without creating a Mastodon account. Unsubscribe at any time. For more information, refer to the <a>Privacy Policy</a>.'
values={{ a: (str) => <Link to='/privacy-policy'>{str}</Link> }}
/>
</div>

View File

@@ -3,14 +3,13 @@ import type { FC } from 'react';
import { FormattedMessage } from 'react-intl';
import type { NavLinkProps } from 'react-router-dom';
import { NavLink } from 'react-router-dom';
import { useAccount } from '@/mastodon/hooks/useAccount';
import { useAccountId } from '@/mastodon/hooks/useAccountId';
import { areCollectionsEnabled } from '../../collections/utils';
import { TabLink, TabList } from '../tab_list';
import classes from './redesign.module.scss';
import classes from './styles.module.scss';
const isActive: Required<NavLinkProps>['isActive'] = (match, location) =>
match?.url === location.pathname ||
@@ -30,27 +29,20 @@ export const AccountTabs: FC = () => {
}
return (
<div className={classes.tabs}>
<NavLink isActive={isActive} to={`/@${acct}`}>
<TabList>
<TabLink isActive={isActive} to={`/@${acct}`}>
<FormattedMessage id='account.activity' defaultMessage='Activity' />
</NavLink>
</TabLink>
{show_media && (
<NavLink exact to={`/@${acct}/media`}>
<TabLink exact to={`/@${acct}/media`}>
<FormattedMessage id='account.media' defaultMessage='Media' />
</NavLink>
</TabLink>
)}
{show_featured && (
<NavLink exact to={`/@${acct}/featured`}>
{areCollectionsEnabled() ? (
<FormattedMessage
id='account.featured.collections'
defaultMessage='Collections'
/>
) : (
<FormattedMessage id='account.featured' defaultMessage='Featured' />
)}
</NavLink>
<TabLink exact to={`/@${acct}/featured`}>
<FormattedMessage id='account.featured' defaultMessage='Featured' />
</TabLink>
)}
</div>
</TabList>
);
};

View File

@@ -0,0 +1,65 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { accountFactoryState, relationshipsFactory } from '@/testing/factories';
import { PendingBadge } from '../badge';
import { AccountListItem } from './index';
const meta = {
title: 'Components/AccountListItem',
component: AccountListItem,
args: {
accountId: '1',
withBorder: false,
},
parameters: {
state: {
accounts: {
'1': accountFactoryState(),
},
},
},
} satisfies Meta<typeof AccountListItem>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {};
export const FollowsYou: Story = {
parameters: {
state: {
relationships: {
'1': relationshipsFactory({
followed_by: true,
}),
},
},
},
};
export const WithCustomStats: Story = {
args: {
stats: ['posts', 'last-active'],
},
};
export const WithCustomBadge: Story = {
args: {
badge: <PendingBadge />,
},
};
export const WithBorder: Story = {
args: {
withBorder: true,
},
};
export const WithoutButton: Story = {
args: {
renderButton: () => null,
},
};

View File

@@ -0,0 +1,204 @@
import type { ReactNode } from 'react';
import { useMemo } from 'react';
import { FormattedMessage, useIntl } from 'react-intl';
import classNames from 'classnames';
import { FollowsYouBadge, VerifiedBadge } from 'mastodon/components/badge';
import { useAccount } from 'mastodon/hooks/useAccount';
import { useRelationship } from 'mastodon/hooks/useRelationship';
import { domain } from 'mastodon/initial_state';
import type { Relationship } from 'mastodon/models/relationship';
import { Avatar } from '../avatar';
import { useAccountHandle } from '../display_name/default';
import { DisplayNameSimple } from '../display_name/simple';
import { EmojiHTML } from '../emoji/html';
import { FollowButton } from '../follow_button';
import { FormattedDateWrapper } from '../formatted_date';
import { ListItemLink, ListItemWrapper } from '../list_item';
import { NumberFields, NumberFieldsItem } from '../number_fields';
import { RelativeTimestamp } from '../relative_timestamp';
import { ShortNumber } from '../short_number';
import classes from './styles.module.scss';
export interface RenderButtonOptions {
accountId: string | undefined;
relationship: Relationship | null | undefined;
}
type Stat = 'followers' | 'following' | 'posts' | 'joined' | 'last-active';
interface Props {
accountId: string | undefined;
stats?: Stat[];
withBio?: boolean;
withBorder?: boolean;
badge?: ReactNode;
renderButton?: (options: RenderButtonOptions) => React.ReactNode;
}
const DEFAULT_STATS: Stat[] = ['followers', 'posts', 'last-active'];
/**
* Extended account list item with bio, verified link badge,
* and familiar follower widget.
*
* The displayed account stats can be customised using the `stats` prop,
* and button rendering can be customised via the `renderButton` prop.
*/
export const AccountListItem: React.FC<Props> = ({
accountId,
stats = DEFAULT_STATS,
withBio = true,
withBorder = true,
badge: badgeProp,
renderButton = defaultRenderButton,
}) => {
const intl = useIntl();
const account = useAccount(accountId);
const handle = useAccountHandle(account, domain);
const relationship = useRelationship(accountId);
const createdThisYear = useMemo(
() => account?.created_at.includes(new Date().getFullYear().toString()),
[account?.created_at],
);
if (!accountId || !account) {
return null;
}
const badge =
badgeProp ?? (relationship?.followed_by ? <FollowsYouBadge /> : null);
const firstVerifiedField = account.fields.find((item) => !!item.verified_at);
return (
<div className={classes.wrapper} data-with-border={withBorder}>
<ListItemWrapper
className={classes.main}
icon={<Avatar account={account} size={40} />}
sideContent={
<span className={classes.button}>
{renderButton({ accountId, relationship })}
</span>
}
>
<ListItemLink
to={`/@${account.acct}`}
data-hover-card-account={accountId}
subtitle={<span className={classes.handle}>{handle}</span>}
>
<DisplayNameSimple
account={account}
className={classes.displayName}
/>
{badge && <span className={classes.badge}>{badge}</span>}
</ListItemLink>
</ListItemWrapper>
<NumberFields>
{stats.includes('followers') && (
<NumberFieldsItem
label={
<FormattedMessage
id='account.followers'
defaultMessage='Followers'
/>
}
hint={intl.formatNumber(account.followers_count)}
>
<ShortNumber value={account.followers_count} />
</NumberFieldsItem>
)}
{stats.includes('following') && (
<NumberFieldsItem
label={
<FormattedMessage
id='account.following'
defaultMessage='Following'
/>
}
hint={intl.formatNumber(account.following_count)}
link={`/@${account.acct}/following`}
>
<ShortNumber value={account.following_count} />
</NumberFieldsItem>
)}
{stats.includes('posts') && (
<NumberFieldsItem
label={
<FormattedMessage id='account.posts' defaultMessage='Posts' />
}
hint={intl.formatNumber(account.statuses_count)}
>
<ShortNumber value={account.statuses_count} />
</NumberFieldsItem>
)}
{stats.includes('joined') && (
<NumberFieldsItem
label={
<FormattedMessage
id='account.joined_short'
defaultMessage='Joined'
/>
}
hint={intl.formatDate(account.created_at)}
>
{createdThisYear ? (
<FormattedDateWrapper
value={account.created_at}
month='short'
day='2-digit'
/>
) : (
<FormattedDateWrapper value={account.created_at} year='numeric' />
)}
</NumberFieldsItem>
)}
{stats.includes('last-active') && (
<NumberFieldsItem
label={
<FormattedMessage
id='account.last_active'
defaultMessage='Last active'
/>
}
>
{account.last_status_at ? (
<RelativeTimestamp long timestamp={account.last_status_at} />
) : (
'-'
)}
</NumberFieldsItem>
)}
{firstVerifiedField && (
<VerifiedBadge
link={firstVerifiedField.value}
className={classes.verifiedBadge}
/>
)}
</NumberFields>
{withBio && account.note.length > 0 && (
<EmojiHTML
className={classNames(classes.bio, 'translate')}
htmlString={account.note_emojified}
extraEmojis={account.emojis}
/>
)}
</div>
);
};
const defaultRenderButton = ({ accountId }: RenderButtonOptions) => (
<AccountListItemFollowButton accountId={accountId} />
);
export const AccountListItemFollowButton: React.FC<{
accountId: string | undefined;
}> = ({ accountId }) => (
<FollowButton compact labelLength='short' accountId={accountId} />
);

View File

@@ -0,0 +1,55 @@
.wrapper {
display: flex;
flex-direction: column;
gap: 12px;
padding: 16px;
&[data-with-border='true'] {
border-bottom: 1px solid var(--color-border-primary);
}
}
.main {
--list-item-padding: 0;
}
.displayName {
// Spacing for badge
margin-inline-end: 6px;
}
.badge {
// Sort out vertical alignment next to name
vertical-align: -4px;
}
.handle {
display: block;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.button {
align-self: start;
}
.verifiedBadge {
align-self: end;
}
.bio {
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
line-clamp: 3;
:any-link {
color: var(--color-text-status-links);
&:hover {
text-decoration: none;
}
}
}

View File

@@ -2,6 +2,8 @@ import { useState, useCallback, useRef, useId } from 'react';
import { FormattedMessage, useIntl } from 'react-intl';
import classNames from 'classnames';
import type {
OffsetValue,
UsePopperOptions,
@@ -18,9 +20,10 @@ import classes from './styles.module.scss';
const offset = [0, 4] as OffsetValue;
const popperConfig = { strategy: 'fixed' } as UsePopperOptions;
export const AltTextBadge: React.FC<{ description: string }> = ({
description,
}) => {
export const AltTextBadge: React.FC<{
description: string;
className?: string;
}> = ({ description, className }) => {
const intl = useIntl();
const uniqueId = useId();
const popoverId = `${uniqueId}-popover`;
@@ -48,7 +51,7 @@ export const AltTextBadge: React.FC<{ description: string }> = ({
<button
type='button'
ref={buttonRef}
className='media-gallery__alt__label'
className={classNames('media-gallery__alt__label', className)}
onClick={handleClick}
aria-expanded={open}
aria-controls={popoverId}

View File

@@ -1,43 +0,0 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import { assetHost } from 'mastodon/utils/config';
import { unicodeMapping } from '../features/emoji/emoji_unicode_mapping_light';
export default class AutosuggestEmoji extends PureComponent {
static propTypes = {
emoji: PropTypes.object.isRequired,
};
render () {
const { emoji } = this.props;
let url;
if (emoji.custom) {
url = emoji.imageUrl;
} else {
const mapping = unicodeMapping[emoji.native] || unicodeMapping[emoji.native.replace(/\uFE0F$/, '')];
if (!mapping) {
return null;
}
url = `${assetHost}/emoji/${mapping.filename}.svg`;
}
return (
<div className='autosuggest-emoji'>
<img
className='emojione'
src={url}
alt={emoji.native || emoji.colons}
/>
<div className='autosuggest-emoji__name'>{emoji.colons}</div>
</div>
);
}
}

View File

@@ -0,0 +1,20 @@
import type { FC } from 'react';
import { Emoji } from './emoji';
interface LegacyEmoji {
id: string;
custom?: boolean;
native?: string;
imageUrl?: string;
}
export const AutosuggestEmoji: FC<{ emoji: LegacyEmoji }> = ({ emoji }) => {
const colons = `:${emoji.id}:`;
return (
<div className='autosuggest-emoji'>
<Emoji code={emoji.native ?? colons} />
<div className='autosuggest-emoji__name'>{colons}</div>
</div>
);
};

View File

@@ -9,8 +9,9 @@ import Overlay from 'react-overlays/Overlay';
import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container';
import AutosuggestEmoji from './autosuggest_emoji';
import { AutosuggestEmoji } from './autosuggest_emoji';
import { AutosuggestHashtag } from './autosuggest_hashtag';
import { LocalCustomEmojiProvider } from './emoji/context';
const textAtCursorMatchesToken = (str, caretPosition, searchTokens) => {
let word;
@@ -219,15 +220,17 @@ export default class AutosuggestInput extends ImmutablePureComponent {
spellCheck={spellCheck}
/>
<Overlay show={!(suggestionsHidden || suggestions.isEmpty())} offset={[0, 0]} placement='bottom' target={this.input} popperConfig={{ strategy: 'fixed' }}>
{({ props }) => (
<div {...props}>
<div className='autosuggest-textarea__suggestions' style={{ width: this.input?.clientWidth }}>
{suggestions.map(this.renderSuggestion)}
<LocalCustomEmojiProvider>
<Overlay show={!(suggestionsHidden || suggestions.isEmpty())} offset={[0, 0]} placement='bottom' target={this.input} popperConfig={{ strategy: 'fixed' }}>
{({ props }) => (
<div {...props}>
<div className='autosuggest-textarea__suggestions' style={{ width: this.input?.clientWidth }}>
{suggestions.map(this.renderSuggestion)}
</div>
</div>
</div>
)}
</Overlay>
)}
</Overlay>
</LocalCustomEmojiProvider>
</div>
);
}

View File

@@ -10,8 +10,9 @@ import Textarea from 'react-textarea-autosize';
import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container';
import AutosuggestEmoji from './autosuggest_emoji';
import { AutosuggestEmoji } from './autosuggest_emoji';
import { AutosuggestHashtag } from './autosuggest_hashtag';
import { LocalCustomEmojiProvider } from './emoji/context';
const textAtCursorMatchesToken = (str, caretPosition) => {
let word;
@@ -218,15 +219,17 @@ const AutosuggestTextarea = forwardRef(({
lang={lang}
/>
<Overlay show={!(suggestionsHidden || suggestions.isEmpty())} offset={[0, 0]} placement='bottom' target={textareaRef} popperConfig={{ strategy: 'fixed' }}>
{({ props }) => (
<div {...props}>
<div className='autosuggest-textarea__suggestions' style={{ width: textareaRef.current?.clientWidth }}>
{suggestions.map(renderSuggestion)}
<LocalCustomEmojiProvider>
<Overlay show={!(suggestionsHidden || suggestions.isEmpty())} offset={[0, 0]} placement='bottom' target={textareaRef} popperConfig={{ strategy: 'fixed' }}>
{({ props }) => (
<div {...props}>
<div className='autosuggest-textarea__suggestions' style={{ width: textareaRef.current?.clientWidth }}>
{suggestions.map(renderSuggestion)}
</div>
</div>
</div>
)}
</Overlay>
)}
</Overlay>
</LocalCustomEmojiProvider>
</div>
);
});

View File

@@ -2,14 +2,20 @@ import type { Meta, StoryObj } from '@storybook/react-vite';
import CelebrationIcon from '@/material-icons/400-24px/celebration-fill.svg?react';
import * as badges from './badge';
import * as badges from '.';
const meta = {
component: badges.Badge,
title: 'Components/Badge',
args: {
domain: '',
label: undefined,
},
argTypes: {
domain: {
control: 'text',
},
},
} satisfies Meta<typeof badges.Badge>;
export default meta;
@@ -29,6 +35,12 @@ export const Domain: Story = {
},
};
export const Verified: Story = {
render() {
return <badges.VerifiedBadge link='example.com' />;
},
};
export const CustomIcon: Story = {
args: {
...Default.args,

View File

@@ -5,34 +5,64 @@ import { FormattedMessage, useIntl } from 'react-intl';
import classNames from 'classnames';
import AdminIcon from '@/images/icons/icon_admin.svg?react';
import ClockIcon from '@/images/icons/icon_clock.svg?react';
import FollowerIcon from '@/images/icons/icon_follower.svg?react';
import IconVerified from '@/images/icons/icon_verified.svg?react';
import type { OnAttributeHandler } from '@/mastodon/utils/html';
import BlockIcon from '@/material-icons/400-24px/block.svg?react';
import GroupsIcon from '@/material-icons/400-24px/group.svg?react';
import PersonIcon from '@/material-icons/400-24px/person.svg?react';
import SmartToyIcon from '@/material-icons/400-24px/smart_toy.svg?react';
import VolumeOffIcon from '@/material-icons/400-24px/volume_off.svg?react';
interface BadgeProps {
import { EmojiHTML } from '../emoji/html';
import { Icon } from '../icon';
import classes from './styles.module.scss';
interface BadgeProps extends React.ComponentPropsWithoutRef<'div'> {
label: ReactNode;
icon?: ReactNode;
className?: string;
domain?: ReactNode;
roleId?: string;
variant?:
| 'default'
| 'subtle'
| 'inverted'
| 'success'
| 'warning'
| 'danger';
}
type PresetBadgeProps = Omit<
BadgeProps,
'label' | 'icon' | 'domain' | 'roleId'
>;
export const Badge: FC<BadgeProps> = ({
icon = <PersonIcon />,
variant = 'default',
label,
className,
domain,
roleId,
...otherProps
}) => (
<div
className={classNames('account-role', className)}
{...otherProps}
className={classNames(
classes.badge,
!icon && classes.badgeWithoutIcon,
classes[variant],
className,
)}
data-account-role-id={roleId}
>
{icon}
<span>{label}</span>
{domain && <span className='account-role__domain'>{domain}</span>}
<span className={classes.content}>
{label}
{domain && <span className={classes.domain}> {domain}</span>}
</span>
</div>
);
@@ -60,13 +90,32 @@ export const GroupBadge: FC<Partial<BadgeProps>> = ({ label, ...props }) => (
/>
);
export const AutomatedBadge: FC<{ className?: string }> = ({ className }) => (
export const AutomatedBadge: FC<PresetBadgeProps> = (props) => (
<Badge
icon={<SmartToyIcon />}
label={
<FormattedMessage id='account.badges.bot' defaultMessage='Automated' />
}
className={className}
{...props}
/>
);
export const FollowsYouBadge: FC<PresetBadgeProps> = (props) => (
<Badge
icon={<FollowerIcon />}
label={
<FormattedMessage id='account.follows_you' defaultMessage='Follows you' />
}
{...props}
/>
);
export const PendingBadge: FC<PresetBadgeProps> = (props) => (
<Badge
variant='warning'
icon={<ClockIcon />}
label={<FormattedMessage id='account.pending' defaultMessage='Pending' />}
{...props}
/>
);
@@ -89,6 +138,7 @@ export const MutedBadge: FC<
return (
<Badge
icon={<VolumeOffIcon />}
variant='inverted'
label={
label ??
(formattedDate ? (
@@ -111,6 +161,7 @@ export const MutedBadge: FC<
export const BlockedBadge: FC<Partial<BadgeProps>> = ({ label, ...props }) => (
<Badge
icon={<BlockIcon />}
variant='danger'
label={
label ?? (
<FormattedMessage
@@ -122,3 +173,31 @@ export const BlockedBadge: FC<Partial<BadgeProps>> = ({ label, ...props }) => (
{...props}
/>
);
const onAttribute: OnAttributeHandler = (name, value, tagName) => {
if (name === 'rel' && tagName === 'a') {
if (value === 'me') {
return null;
}
return [
name,
value
.split(' ')
.filter((x) => x !== 'me')
.join(' '),
];
}
return undefined;
};
export const VerifiedBadge: React.FC<{ link: string; className?: string }> = ({
link,
className,
}) => (
<Badge
variant='success'
icon={<Icon id='verified' icon={IconVerified} noFill />}
label={<EmojiHTML as='span' htmlString={link} onAttribute={onAttribute} />}
className={className}
/>
);

View File

@@ -0,0 +1,70 @@
.badge {
color: var(--color-text-primary);
font-size: 13px;
font-weight: 400;
display: inline-flex;
max-width: 100%;
padding: 4px;
gap: 4px;
border-radius: 8px;
align-items: center;
> svg {
flex-shrink: 0;
width: auto;
height: 17px;
fill: currentColor;
opacity: 0.85;
}
a {
color: inherit;
text-decoration: none;
}
&:not(.badgeWithoutIcon) {
padding-inline-end: 8px;
}
}
.content {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.domain {
opacity: 0.75;
letter-spacing: 0;
}
.default {
background-color: var(--color-bg-secondary);
}
.subtle {
background-color: var(--color-bg-brand-softest);
}
.feature {
background-color: var(--color-bg-brand-base);
color: var(--color-text-on-brand-base);
}
.inverted {
background-color: var(--color-bg-inverted);
color: var(--color-text-inverted);
}
.success {
background-color: var(--color-bg-success-softest);
}
.warning {
background-color: var(--color-bg-warning-softest);
}
.danger {
background-color: var(--color-bg-error-base);
color: var(--color-text-on-error-base);
}

View File

@@ -74,6 +74,24 @@ export const Compact: Story = {
play: buttonTest,
};
export const CompactSecondary: Story = {
args: {
compact: true,
secondary: true,
children: 'Compact secondary button',
},
play: buttonTest,
};
export const CompactPlain: Story = {
args: {
compact: true,
plain: true,
children: 'Compact plain button',
},
play: buttonTest,
};
export const Dangerous: Story = {
args: {
dangerous: true,

View File

@@ -17,6 +17,11 @@
margin-top: -2px;
}
.content,
.body {
min-width: 0;
}
.content {
display: flex;
gap: 8px;
@@ -32,6 +37,8 @@
.body {
flex-grow: 1;
overflow-wrap: break-word;
hyphens: auto;
a {
color: inherit;
@@ -87,7 +94,7 @@
}
.variantSubtle {
border: 1px solid var(--color-bg-brand-softest);
border: 1px solid var(--color-border-brand-soft);
background-color: var(--color-bg-primary);
.icon {

View File

@@ -73,7 +73,7 @@ const BackButton: React.FC<{
};
export interface Props {
title?: string;
title?: React.ReactNode;
icon?: string;
iconComponent?: IconProp;
active?: boolean;
@@ -276,9 +276,11 @@ export const ColumnHeader: React.FC<Props> = ({
</>
);
const HeadingElement = hasTitle ? 'h1' : 'div';
const component = (
<div className={wrapperClassName}>
<h1 className={buttonClassName}>
<HeadingElement className={buttonClassName}>
{hasTitle && (
<>
{backButton}
@@ -311,7 +313,7 @@ export const ColumnHeader: React.FC<Props> = ({
{extraButton}
{collapseButton}
</div>
</h1>
</HeadingElement>
<div
className={collapsibleClassName}

Some files were not shown because too many files have changed in this diff Show More