Compare commits

...

344 commits

Author SHA1 Message Date
06b7ebe45c Merge remote-tracking branch 'upstream/main'
All checks were successful
continuous-integration/drone Build is passing
2025-01-10 10:09:45 +01:00
Matt Jankowski
54e2030146
Add AccountWarning#appeal_eligible? method (#33526) 2025-01-09 15:08:39 +00:00
Matt Jankowski
3a4aed9890
Rename app/helpers/jsonld_helper.rb to app/helpers/json_ld_helper.rb (#33489) 2025-01-09 14:39:49 +00:00
Matt Jankowski
9b8d1fb6d1
Add Account#actor_type_application? query method (#33525) 2025-01-09 14:32:48 +00:00
Claire
a8b2b474d7
Add timestamp to all announcements in Web UI (#18329) 2025-01-09 14:22:33 +00:00
Claire
c6c8e7e6ab
Fix last paginated notification group only including data on a single notification (#33271) 2025-01-09 13:47:12 +00:00
Claire
91c75a6361
Re-introduce application_id in ScheduledStatusSerializer (#33505) 2025-01-09 13:38:23 +00:00
renovate[bot]
51a92427ce
Update dependency react-intl to v7 (#32954)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-09 10:04:14 +00:00
renovate[bot]
b0fbb71759
Update dependency jsdom to v26 (#33521)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-09 09:55:34 +00:00
Christian Schmidt
8e2c642d44
Do now swallow response body on persistent connection (#32729) 2025-01-09 08:35:35 +00:00
github-actions[bot]
4e2c15b45d
New Crowdin Translations (automated) (#33522)
Co-authored-by: GitHub Actions <noreply@github.com>
2025-01-09 08:21:47 +00:00
Matt Jankowski
cbae00ad23
Remove render_template from accounts request spec (#33519) 2025-01-09 08:17:11 +00:00
Matt Jankowski
4148b68430
Remove render_template from remote interaction helper request spec (#33518) 2025-01-09 08:17:06 +00:00
Matt Jankowski
f4b463ecb1
Use response.parsed_body for error view application controller spec (#33515) 2025-01-09 08:17:00 +00:00
Matt Jankowski
ce1501c3a7
Add "Account::Search" shared example for use in Account spec (#33507) 2025-01-09 08:12:48 +00:00
Christian Schmidt
1ce2dc3d13
Target same browsers in development and production (#33513) 2025-01-09 08:09:13 +00:00
renovate[bot]
3c7f3b190c
Update formatjs monorepo (#32774)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-08 16:53:12 +00:00
Claire
6b1ea8dd2c
Require specific subtype of formatMessage in timeAgoString (#33511) 2025-01-08 16:26:54 +00:00
Claire
1bf6195763
Fix use of deprecated Iterable.isIndexed from immutable (#33510) 2025-01-08 16:25:41 +00:00
Matt Jankowski
78e0d6a11a
Extract development section from top-level readme into standalone doc (#33328) 2025-01-08 14:29:32 +00:00
Matt Jankowski
f22a2aab40
Add Account#remote? query method (#33508) 2025-01-08 14:28:08 +00:00
Emelia Smith
15669fcf75
Include time portion in formatted datetimes when provided (#33191) 2025-01-08 14:26:08 +00:00
Lamparter
2c36283a89
Refresh README.md (#32143) 2025-01-08 14:03:21 +00:00
renovate[bot]
d34e9eaf17
Update dependency @reduxjs/toolkit to v2.5.0 (#33102)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-08 14:02:07 +00:00
renovate[bot]
a16d83ffef
Update Node.js to 22.13 (#33502)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-08 12:22:44 +00:00
Nick Schonning
67a8d4638c
Unpin peter-evans/create-pull-request to v7 (#30817) 2025-01-08 11:01:51 +00:00
renovate[bot]
6a351e2247
Update dependency core-js to v3.40.0 (#33504)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-08 10:57:40 +00:00
renovate[bot]
5a142060e1
Update opentelemetry-ruby (non-major) (#33506)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-08 10:47:39 +00:00
renovate[bot]
242221c11f
Update dependency connection_pool to v2.5.0 (#33503)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-08 10:21:58 +00:00
renovate[bot]
ee1cbda226
Update opentelemetry-ruby (non-major) (#33501)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-08 09:06:37 +00:00
github-actions[bot]
35e57138f1
New Crowdin Translations (automated) (#33500)
Co-authored-by: GitHub Actions <noreply@github.com>
2025-01-08 09:01:28 +00:00
Matt Jankowski
0c690511c2
Convert auth/challenges spec controller->request (#33495) 2025-01-08 08:54:08 +00:00
Matt Jankowski
b6c2923cf7
Convert settings/migration spec controller->system (#33496) 2025-01-08 08:53:26 +00:00
renovate[bot]
c3fc12c2b4
Update dependency rubyzip to v2.4.1 (#33494)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-08 08:48:51 +00:00
Matt Jankowski
c0264c8013
Extend custom CSS cache time with digest paths (#33207) 2025-01-08 08:48:45 +00:00
Matt Jankowski
b3243ef41c
Refer to constant values from api/v1/apps request spec (#33488) 2025-01-07 16:28:54 +00:00
Matt Jankowski
7ad44e22ed
Remove role color highlighting from custom css (#33493) 2025-01-07 16:28:35 +00:00
renovate[bot]
bbe9dcfade
Update dependency aws-sdk-s3 to v1.177.0 (#33490)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-07 16:06:11 +00:00
renovate[bot]
5bbcb1bb2e
Update dependency fastimage to v2.4.0 (#33491)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-07 16:06:05 +00:00
renovate[bot]
e0863fd766
Update peter-evans/create-pull-request action to v7.0.6 (#33492)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-07 16:06:00 +00:00
Noel De Martin
5c4e224b66
Comment sidekiq build in docker compose (#33483) 2025-01-07 15:40:24 +00:00
renovate[bot]
7d6fd68efd
Update dependency test-prof to v1.4.4 (#33487)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-07 14:18:09 +00:00
renovate[bot]
fd7bcfa749
Update dependency rubocop-performance to v1.23.1 (#33486)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-07 14:18:05 +00:00
Matt Jankowski
fdfbf6e04e
Set statement timeout to zero in maintenance CLI (#33484) 2025-01-07 14:11:12 +00:00
Matt Jankowski
927c7bb6bb
Use ruby version 3.4.1 (#33304) 2025-01-07 13:46:56 +00:00
Claire
82e046ea06
Fix preview card sizing in “Author attribution” in profile settings (#33482) 2025-01-07 11:29:38 +00:00
Nick Schonning
c12b85e7a9
Fix Style/MultipleComparison (#33313)
Co-authored-by: Matt Jankowski <matt@jankowski.online>
2025-01-07 11:24:52 +00:00
renovate[bot]
8233293429
Update DefinitelyTyped types (non-major) (#33220)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Renaud Chaput <renchap@gmail.com>
2025-01-07 09:28:06 +00:00
github-actions[bot]
9715bd796f
New Crowdin Translations (automated) (#33480)
Co-authored-by: GitHub Actions <noreply@github.com>
2025-01-07 07:50:27 +00:00
Matt Jankowski
edf62d4fe3
Add self_editing? method to user role policy (#33476) 2025-01-07 07:50:19 +00:00
Matt Jankowski
dd937e115a
Use distributable? method in admin/status_policy method (#33477) 2025-01-07 07:47:58 +00:00
Matt Jankowski
efcd4ea5de
Reference value constants from specs (#33479) 2025-01-06 23:25:13 +00:00
Matt Jankowski
b0634b2943
Add helpers for ios/android app store links in welcome mailer (#33475) 2025-01-06 19:59:11 +00:00
Matt Jankowski
82d2ce293d
Convert admin/warning_presets spec controller->system (#33474) 2025-01-06 18:24:54 +00:00
Matt Jankowski
fbd283329e
Avoid "double icon" near each other in nav sidebar (#33449) 2025-01-06 18:24:39 +00:00
Matt Jankowski
24554bb6b9
Use ubuntu-latest for all GH actions runners (#33473) 2025-01-06 18:24:04 +00:00
Matt Jankowski
ef39398b82
Add urgent scope and by_version method to SoftwareUpdate (#33470) 2025-01-06 18:23:57 +00:00
Matt Jankowski
4cf031ee13
Extricate Invite constants for code generation (#33472) 2025-01-06 18:23:05 +00:00
Matt Jankowski
1eb752fb38
Extract constants for AnnualReport::* minimum thresholds (#33469) 2025-01-06 18:22:32 +00:00
Claire
e8672e27e8
Further remove old notifications code (#33465) 2025-01-06 18:22:07 +00:00
Claire
c692f69dba
Fix color contrast in report modal (#33468) 2025-01-06 16:24:32 +00:00
Claire
ea9b10d112
Fix error 500 when passing an invalid lang parameter (#33467) 2025-01-06 10:16:42 +00:00
Claire
dbbf450ef5
Fix fediverse:creator metadata not showing up in REST API (#33466) 2025-01-06 10:04:25 +00:00
Matt Jankowski
08dd11f8d4
Use in_order_of with filter: false in AccountSummary.localized (#33446) 2025-01-06 08:18:05 +00:00
Jeremy Kescher
612d6182e4
Fix /share not using server-set characters limit (#33459) 2025-01-06 08:14:11 +00:00
github-actions[bot]
c93c1f38b7
New Crowdin Translations (automated) (#33451)
Co-authored-by: GitHub Actions <noreply@github.com>
2025-01-06 08:13:39 +00:00
renovate[bot]
c0f5705c34
Update dependency uuid to v11.0.4 (#33464)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-06 08:01:22 +00:00
renovate[bot]
081244f692
Update dependency sass to v1.83.1 (#33453)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-06 07:55:37 +00:00
zunda
f16fbc069a
Use configured limit for number of profile fields (#33463) 2025-01-06 04:47:10 +00:00
Tim Rogers
3bdfa3eb4c
Added validator for extra profile field values with empty name (#33421) 2025-01-04 19:42:27 +00:00
Matt Jankowski
9b82bedc6f
Only register stats dirs when command available (#33454) 2025-01-04 17:44:49 +00:00
Claire
9712518b2f
Refactor status handleClick and handleHotkeyOpen handlers (#33435) 2025-01-03 22:00:52 +00:00
Matt Jankowski
af7d6e59af
Replace deprecated STATS_DIRECTORIES with Rails::CodeStatistics.register_directory approach (#33445) 2025-01-03 21:52:16 +00:00
Claire
02fe176fd4
Fix audio player modal having white-on-white buttons in light theme (#33444) 2025-01-03 21:51:51 +00:00
Claire
7d6da219c0
Remove old notifications actions and reducers (#31843) 2025-01-03 16:12:08 +00:00
Matt Jankowski
535866218e
Update rails to version 8.0.1 (#32357) 2025-01-03 16:08:21 +00:00
Christian Schmidt
f9582bcfc3
Localize language names (#33402) 2025-01-03 15:29:25 +00:00
Claire
839a4e27a9
Fix incorrect relationship_severance_event attribute name in changelog (#33443) 2025-01-03 13:23:01 +00:00
renovate[bot]
6ffa251d6a
Update dependency net-http to '~> 0.6.0' (#33130)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-03 11:20:13 +00:00
renovate[bot]
098dd91523
Update dependency typescript to v5.7.2 (#33059)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-03 11:16:44 +00:00
Claire
ed04547de2
Fix Renovate by temporarily constraining it to Ruby 3.3 (#33440) 2025-01-03 09:24:55 +00:00
Claire
79a9069deb
Add t hotkey for post translations (#33441) 2025-01-03 09:24:50 +00:00
Matt Jankowski
7cbdcd7b3f
Add role-specific user fabricators (#33437) 2025-01-03 08:28:14 +00:00
github-actions[bot]
a8a16695f1
New Crowdin Translations (automated) (#33439)
Co-authored-by: GitHub Actions <noreply@github.com>
2025-01-03 07:53:00 +00:00
Matt Jankowski
590b7b2bb5
Use bundler version 2.6.2 (#33436) 2025-01-03 07:52:58 +00:00
Matt Jankowski
8e3fd1b823
Convert disputes/appeals spec controller->system (#33434) 2025-01-02 16:08:25 +00:00
Matt Jankowski
1fbd106af9
Convert admin/report_notes spec controller->system (#33433) 2025-01-02 15:54:21 +00:00
Jeong Arm
a557f9bbaa
Fix keyboard shortcut for open status (#33419) 2025-01-02 15:48:59 +00:00
Matt Jankowski
00a8a5467c
Convert admin/relays spec controller->system (#33430) 2025-01-02 15:01:44 +00:00
gunchleoc
3201485df2
Fix favorite & bookmark text toggle in timeline, status and image view (#27209) 2025-01-02 13:52:07 +00:00
Claire
655c4a5ebb
Fix processing of incoming notifications for unfilterable types (#33429) 2025-01-02 13:22:25 +00:00
Matt Jankowski
2e936785b7
Convert admin/rules spec controller->system (#33377) 2025-01-02 12:57:55 +00:00
Claire
68bfa7ae3f
Fix WebUI erroneously stopping to offer expanding search results after second page (#33428) 2025-01-02 10:58:00 +00:00
Claire
f9fef9864a
Fix intermittent failure on ap/activity/update spec timestamp check (#33425) 2025-01-02 09:17:49 +00:00
renovate[bot]
ca06156087
Update Yarn to v4.6.0 (#33417)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-02 09:07:45 +00:00
renovate[bot]
a9f2e261d3
Update dependency ox to v2.14.19 (#33398)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-02 09:05:15 +00:00
renovate[bot]
7b5adb0d64
Update dependency pino to v9.6.0 (#33386)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-02 09:03:56 +00:00
github-actions[bot]
ba18289477
New Crowdin Translations (automated) (#33378)
Co-authored-by: GitHub Actions <noreply@github.com>
2025-01-02 09:02:47 +00:00
renovate[bot]
28466865c1
Update dependency csv to v3.3.2 (#33381)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-02 08:38:56 +00:00
Matt Jankowski
441dcd7396
Fix intermittent failure on ap/activity/create spec timestamp check (#33406) 2025-01-02 08:38:22 +00:00
renovate[bot]
7c50e53baa
Update dependency bufferutil to v4.0.9 (#33414)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-02 08:37:51 +00:00
renovate[bot]
79cfffda3b
Update dependency postcss-preset-env to v10.1.3 (#33415)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-02 08:36:40 +00:00
renovate[bot]
d94b2b1c39
Update dependency rubocop-rails to v2.28.0 (#33397)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-02 08:36:24 +00:00
Matt Jankowski
8770905186
Convert admin/ip_blocks spec controller->system (#33376) 2024-12-20 15:36:19 +00:00
renovate[bot]
b01e8f4a9b
Update dependency ioredis to v5.4.2 (#33375)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-20 15:35:36 +00:00
Matt Jankowski
f35de1ed0d
Remove un-needed Poll.attached scope (#33368) 2024-12-20 15:15:11 +00:00
Matt Jankowski
b648c64e2e
Add NOT NULL requirement to columns on polls (#33374) 2024-12-20 14:33:48 +00:00
Matt Jankowski
d2fbf42b0e
Avoid validate: false save in expired poll create activity spec (#33367) 2024-12-20 13:34:05 +00:00
Claire
344e2903b3
Fix featured tags for remote accounts not being kept up to date (#33372) 2024-12-20 11:50:31 +00:00
Matt Jankowski
d31d988e24
Add coverage for UserMailer not delivering to memorialized users (#33366)
Co-authored-by: Claire <claire.github-309c@sitedethib.com>
2024-12-20 10:27:40 +00:00
renovate[bot]
aa455aaa49
Update opentelemetry-ruby (non-major) (#33371)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-20 08:31:27 +00:00
Matt Jankowski
28d309fd86
Add shared example for Expireable concern (#33369) 2024-12-20 08:21:34 +00:00
github-actions[bot]
dd56292d1f
New Crowdin Translations (automated) (#33370)
Co-authored-by: GitHub Actions <noreply@github.com>
2024-12-20 08:06:18 +00:00
Matt Jankowski
87c892191a
Convert admin/domain_allows spec controller->system (#33365) 2024-12-20 08:04:22 +00:00
Matt Jankowski
1acc54cfc7
Convert admin/webhooks spec controller->system (#33364) 2024-12-20 07:52:16 +00:00
Matt Jankowski
03bf4cde55
Convert statuses_cleanup controller specs to system specs (#33363) 2024-12-19 13:34:54 +00:00
renovate[bot]
d826919aeb
Update dependency debug to v1.10.0 (#33361)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-19 13:27:37 +00:00
Matt Jankowski
0ac43700d8
Add coverage for REST::AnnualReportEventSerializer and two ActivityPub::* serializers (#33355)
Co-authored-by: Claire <claire.github-309c@sitedethib.com>
2024-12-19 12:30:09 +00:00
Renaud Chaput
22813dbc6e
Fix /health endpoint when OTEL is configured (#33362) 2024-12-19 10:44:44 +00:00
renovate[bot]
32184e3f04
Update dependency test-prof to v1.4.3 (#33360)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-19 09:08:56 +00:00
github-actions[bot]
7a08fe91b5
New Crowdin Translations (automated) (#33358)
Co-authored-by: GitHub Actions <noreply@github.com>
2024-12-19 08:47:20 +00:00
Matt Jankowski
22caddaa24
Update user role position range limit (#33349) 2024-12-19 08:10:39 +00:00
Matt Jankowski
aee64a465c
Remove unused Poll.unattached scope (#33350) 2024-12-19 08:10:06 +00:00
Matt Jankowski
d014024dc5
Use bundler version 2.6.1 (#33351) 2024-12-19 08:08:24 +00:00
Matt Jankowski
545a6b9eed
Convert admin/account_moderation_notes spec controller->system (#33354) 2024-12-19 08:02:08 +00:00
Claire
85792cc375
Fix incorrect notification settings migration for non-followers (#33348) 2024-12-18 12:25:40 +00:00
Renaud Chaput
3f8f06eb46
Add support for Active Record query log tags (#33342) 2024-12-18 10:05:00 +00:00
Jesse Karmani
db749c7d2b
Fix down clause for notification policy v2 migrations (#33340) 2024-12-18 08:52:47 +00:00
Matt Jankowski
668ce14ff2
Convert admin/custom_emojis spec controller->system (#33327) 2024-12-18 08:34:15 +00:00
Matt Jankowski
ca2a7d66b8
Fix missing value limits for UserRole position (#33172) 2024-12-18 08:29:27 +00:00
github-actions[bot]
1992c2a4fa
New Crowdin Translations (automated) (#33345)
Co-authored-by: GitHub Actions <noreply@github.com>
2024-12-18 07:51:31 +00:00
renovate[bot]
c4c9f2bf47
Update dependency opentelemetry-instrumentation-faraday to v0.24.8 (#33338)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-18 07:34:34 +00:00
Renaud Chaput
3febe7d020
Add OTEL trace & span IDs to logs (#33339) 2024-12-18 07:34:20 +00:00
Matt Jankowski
3f9ac22af3
Remove empty/unused config/storage.yml file (#33341) 2024-12-18 07:25:14 +00:00
renovate[bot]
efada38271
Update dependency dotenv to v3.1.7 (#33344)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-18 07:24:51 +00:00
Matt Jankowski
14234d2bda
Use bundler version 2.6.0 (#33337) 2024-12-18 07:24:02 +00:00
Matt Jankowski
d65e246192
Convert statuses#activity action controller to request spec (#33336) 2024-12-17 16:37:30 +00:00
Matt Jankowski
0085e8c427
Wrap assertions in change within AccountStatusesCleanupPolicy spec (#33335) 2024-12-17 16:25:02 +00:00
Matt Jankowski
ce5c33c65d
Add API datetime/format matcher for serialization specs, reduce factories (#33325) 2024-12-17 13:38:57 +00:00
Matt Jankowski
978142ac9e
Add missing NOT NULL on more columns from "large but valid" tables (#33330) 2024-12-17 13:38:18 +00:00
Eugen Rochko
f19fd0b889
Change design of interaction modal in web UI (#33278)
Co-authored-by: Claire <claire.github-309c@sitedethib.com>
2024-12-17 12:36:32 +00:00
Claire
1712543c68
Revert "Support animated PNGs in media attachments (#28516)" (#33334) 2024-12-17 10:52:59 +00:00
github-actions[bot]
02748f172d
New Crowdin Translations (automated) (#33332)
Co-authored-by: GitHub Actions <noreply@github.com>
2024-12-17 09:33:42 +00:00
Matt Jankowski
60bb51eef8
Convert filters spec controller->system (#33326) 2024-12-17 09:20:01 +00:00
Matt Jankowski
a7673d361d
Fix application_id erroneous inclusion in ScheduledStatusSerializer (#33159) 2024-12-17 08:54:07 +00:00
Matt Jankowski
3ac478472e
Remove reference to removed Capfile in lint-staged config (#33329) 2024-12-17 08:51:45 +00:00
Claire
a837f7d693
Fix clicking on a profile mention while logged out potentially leading to incorrect account (#33324) 2024-12-16 19:29:51 +00:00
Matt Jankowski
efe4e72f93
Convert settings/preferences/appearance controller spec to system (#33323) 2024-12-16 17:08:19 +00:00
Matt Jankowski
9067a7f696
Convert settings/preferences/notifications controller spec to system (#33322) 2024-12-16 16:24:05 +00:00
Matt Jankowski
bfef66276a
Convert settings/preferences/other controller spec to system (#33321) 2024-12-16 16:23:26 +00:00
Matt Jankowski
c68eb82ffa
Use explicit config loading from bin/rubocop (#33256) 2024-12-16 13:50:58 +00:00
renovate[bot]
ecdf4ea062
Update codecov/codecov-action action to v5 (#33317)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-16 12:36:36 +00:00
Claire
6305b6e747
Fix error decrementing status count when FeaturedTags#last_status_at is nil (#33320) 2024-12-16 12:33:55 +00:00
Matt Jankowski
cf4595967b
Add NOT NULL requirement to columns on account_conversations (#33308) 2024-12-16 08:20:08 +00:00
Matt Jankowski
a596f3479b
Convert settings/login_activities controller spec to system spec (#33306) 2024-12-16 08:12:36 +00:00
Matt Jankowski
3d00b95359
Convert settings/profiles controller spec to system spec (#33305) 2024-12-16 08:11:48 +00:00
github-actions[bot]
504ea31bd3
New Crowdin Translations (automated) (#33310)
Co-authored-by: GitHub Actions <noreply@github.com>
2024-12-16 07:54:12 +00:00
renovate[bot]
d19eaebd5e
Update dependency dotenv to v3.1.6 (#33302)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-16 07:47:51 +00:00
renovate[bot]
fd0793dd0e
Update dependency postcss-preset-env to v10.1.2 (#33303)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-16 07:47:32 +00:00
Eugen Rochko
3cf6681736
Change animation on feed generation screen in web UI (#33311) 2024-12-16 07:38:56 +00:00
renovate[bot]
74478888ff
Update dependency oj to v3.16.8 (#33314)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-16 07:37:56 +00:00
renovate[bot]
519630bc03
Update dependency csv to v3.3.1 (#33315)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-16 07:37:39 +00:00
renovate[bot]
02e78a7dac
Update devDependencies (non-major) (#33316)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-16 07:37:09 +00:00
Nik Clayton
5cf37248cc
Ensure datetime/date are serialized to correct format (#33086) 2024-12-14 15:50:41 +00:00
Matt Jankowski
a8edc82471
Replace deprecated "github" format for stylelint on CI runs (#33292) 2024-12-14 09:50:08 +00:00
Matt Jankowski
efc85e39a0
Add missing NOT NULL requirement to small, indexed, valid data tables (#33284) 2024-12-14 00:16:03 +00:00
Matt Jankowski
4130bda12e
Convert settings/verifications controller spec to system spec (#33285) 2024-12-13 10:39:07 +00:00
github-actions[bot]
7200f9d25c
New Crowdin Translations (automated) (#33300)
Co-authored-by: GitHub Actions <noreply@github.com>
2024-12-13 10:38:09 +00:00
renovate[bot]
92a4423ab7
Update dependency irb to v1.14.2 (#33282)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-13 09:22:48 +00:00
renovate[bot]
03aa09561d
Update RuboCop (non-major) to v1.69.2 (#33283)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-13 09:22:27 +00:00
Matt Jankowski
1bd9306ced
Add coverage for Tag model validations on name/display_name (#33291) 2024-12-13 09:21:55 +00:00
renovate[bot]
4f2d060f94
Update dependency aws-sdk-s3 to v1.176.1 (#33293)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-13 09:20:45 +00:00
renovate[bot]
148b1ff230
Update dependency nokogiri to v1.17.2 (#33294)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-13 09:19:56 +00:00
renovate[bot]
e2c7462852
Update dependency dotenv to v3.1.5 (#33298)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-13 09:17:48 +00:00
renovate[bot]
ed33064e7b
Update dependency sass to v1.83.0 (#33299)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-13 09:17:26 +00:00
Matt Jankowski
3c71ee9b90
Use touch_all for time column updates (#33295) 2024-12-13 08:41:42 +00:00
Matt Jankowski
234af14966
Fix long account username leaving container (#33286) 2024-12-13 08:41:38 +00:00
Robb Kidd
d580607510
don't start OTel traces with Redis spans (#33090) 2024-12-13 08:21:38 +00:00
Matt Jankowski
42f6a36f21
Run optimize PNG on emoji sheet asset (#33281) 2024-12-12 20:18:56 +00:00
Matt Jankowski
bc5b9d98a5
Remove unused admin/terms_of_service routes (#33288) 2024-12-12 20:17:15 +00:00
Eugen Rochko
0636bcdbe1
Change search to use query params in web UI (#32949) 2024-12-12 17:12:33 +00:00
Matt Jankowski
708919ee93
Update deprecated enum style in older migrations (#32925) 2024-12-12 10:44:58 +00:00
Matt Jankowski
4bf6af3b61
Remove unused model_have_error_on_field matcher (#33273) 2024-12-12 10:28:05 +00:00
github-actions[bot]
e0a49af47d
New Crowdin Translations (automated) (#33280)
Co-authored-by: GitHub Actions <noreply@github.com>
2024-12-12 09:10:00 +00:00
Matt Jankowski
b51e0bc3c7
Simplify model validation specs for Report (#33267) 2024-12-12 09:09:35 +00:00
Matt Jankowski
d56aa9031a
Disable Naming/VariableNumber cop in old migrations (#33274) 2024-12-12 08:15:38 +00:00
renovate[bot]
6445facb8c
Update dependency react-select to v5.9.0 (#33279)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-12 08:08:59 +00:00
Matt Jankowski
8fa89c2db1
Remove app/views rubocop rule exception for haml-lint (#33270) 2024-12-11 15:50:56 +00:00
Matt Jankowski
43702b95a2
Enable Rails/ReversibleMigration cop (#33264) 2024-12-11 15:50:50 +00:00
Jonathan de Jong
315c170910
Fix bug where remote suspension causes local instance to remove remote follows (#27588) 2024-12-11 15:08:07 +00:00
Matt Jankowski
d6241f6751
Fix Rails/ThreeStateBooleanColumn cop (#33269) 2024-12-11 14:25:48 +00:00
Claire
bcb3b627b9
Fix processing of mentions for post edits with an existing corresponding silent mention (#33227) 2024-12-11 12:59:29 +00:00
Renaud Chaput
da279df8ae
Fix voting on polls (#33266) 2024-12-11 10:32:03 +00:00
Claire
a4560d9278
Change label of grouped favourite notification on private mentions (#31659) 2024-12-11 10:25:51 +00:00
Filippo Giunchedi
3614e9a30b
Fix Vagrant failure to sync dangling symlinks (#28101)
Co-authored-by: Filippo Giunchedi <filippo@debian.org>
2024-12-11 10:18:30 +00:00
Matt Jankowski
ad68686bec
Simplify model validation specs for CustomFilter (#33248) 2024-12-11 09:55:35 +00:00
Matt Jankowski
f721e76d13
Avoid double request in admin/export_domain_allows controller spec (#33255) 2024-12-11 08:58:59 +00:00
Matt Jankowski
f6daf73286
Add non-matching cases to Report#history spec (#33253) 2024-12-11 08:24:50 +00:00
github-actions[bot]
f12b3a9cef
New Crowdin Translations (automated) (#33262)
Co-authored-by: GitHub Actions <noreply@github.com>
2024-12-11 08:20:44 +00:00
renovate[bot]
32e23ef630
Update dependency react-redux to v9.2.0 (#33260)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-11 08:17:47 +00:00
renovate[bot]
5e4ca0881e
Update dependency @rails/ujs to v7.1.501 (#33258)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-11 07:55:14 +00:00
renovate[bot]
bf4ad7e200
Update dependency rails to v7.2.2.1 (#33259)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-11 07:54:50 +00:00
Jeong Arm
9ba4c8ef66
Handle rotation is not present in the video metadata (#33261) 2024-12-11 04:47:42 +00:00
Christian Schmidt
a70ae28644
Support animated PNGs in media attachments (#28516) 2024-12-11 02:28:21 +00:00
Matt Jankowski
39364346bb
Add coverage for admin/terms/drafts#update and admin/terms/generates#create actions (#33251) 2024-12-10 23:09:36 +00:00
Matt Jankowski
094e2172ec
Add system spec for Admin::TermsOfService index view (#33250) 2024-12-10 23:08:14 +00:00
Renaud Chaput
ded799f913
Convert polls to Typescript / Immutable Records (#29789) 2024-12-10 22:54:07 +00:00
Matt Jankowski
e4e35ab134
Add NOT NULL requirement to account columns on AccountPin (#33244) 2024-12-10 15:50:49 +00:00
Matt Jankowski
e76aff7de5
Standardize uniqueness validation declaration on Mention (#33247) 2024-12-10 15:45:13 +00:00
renovate[bot]
3222c19d45
Update dependency nokogiri to v1.17.1 (#33246)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-10 15:10:09 +00:00
Matt Jankowski
12eb6b0e6d
Add coverage for TermsOfServicePolicy class (#33241) 2024-12-10 15:09:44 +00:00
Matt Jankowski
66266ec6fc
Add coverage for UserMailer#terms_of_service_changed mailer (#33242) 2024-12-10 14:45:40 +00:00
Matt Jankowski
19c6cb82f6
Add coverage for Admin::DistributeTermsOfServiceNotificationWorker class (#33245) 2024-12-10 14:45:14 +00:00
Matt Jankowski
c72083ad68
Add system spec for TOS page (#33243) 2024-12-10 14:14:28 +00:00
Renaud Chaput
2a369a8977
Use a system setting for the Referer policy (#33239) 2024-12-10 13:16:52 +00:00
Matt Jankowski
7d52b24569
Add coverage for AccountPin model (#33231) 2024-12-10 13:02:52 +00:00
Claire
58c5068bda
Fix Chromium showing scrollbar on embedded posts (#33237) 2024-12-10 13:02:17 +00:00
github-actions[bot]
801cd731be
New Crowdin Translations (automated) (#33235)
Co-authored-by: GitHub Actions <noreply@github.com>
2024-12-10 09:10:28 +00:00
FND
c8c916e657
Change URL truncation to account for ellipses (#33229) 2024-12-10 08:41:06 +00:00
Matt Jankowski
2131e61548
Fix hard wrap of markdown list in TOS template (#33230) 2024-12-10 01:43:03 +00:00
Matt Jankowski
12d9ef6003
Character conversion on TOS template (#33233) 2024-12-10 00:04:59 +00:00
Eugen Rochko
425311e1d9
Change referrer policy to be controlled by header in web UI (#33214) 2024-12-09 22:42:45 +00:00
renovate[bot]
2e35b15b4d
Update dependency react-textarea-autosize to v8.5.6 (#33228)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-09 15:12:31 +00:00
renovate[bot]
cfd2879ba4
Update dependency nokogiri to v1.17.0 (#33218)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-09 15:08:16 +00:00
Matt Jankowski
342055cb15
Update rubocop to version 1.69.1 (#32967) 2024-12-09 14:52:49 +00:00
Emelia Smith
6615f17b48
Fix missing top border on Admin Hashtags UI (#31443) 2024-12-09 12:55:28 +00:00
renovate[bot]
7b7a29867b
Update devDependencies (non-major) (#33221)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-09 10:24:10 +00:00
Eugen Rochko
30aa0df88c
Add terms of service (#33055) 2024-12-09 10:04:46 +00:00
github-actions[bot]
7a2a345c08
New Crowdin Translations (automated) (#33210)
Co-authored-by: GitHub Actions <noreply@github.com>
2024-12-09 09:20:28 +00:00
renovate[bot]
f646fb0259
Update dependency doorkeeper to v5.8.1 (#33225)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-09 09:19:58 +00:00
Eugen Rochko
bf79174829
Fix design of search bar on explore screen in light theme in web UI (#33224) 2024-12-09 09:19:00 +00:00
Claire
ab133c5e30
Bump version to v4.3.2 (#33226) 2024-12-09 09:09:00 +00:00
Eugen Rochko
796dd57e1b
Change ability to navigate of unconfirmed users (#33209) 2024-12-09 08:31:56 +00:00
Eugen Rochko
ca45f896dd
Fix various visual sign-up flow issues (#33206) 2024-12-09 08:23:19 +00:00
Matt Jankowski
58c40caeb4
Extract SPA-handled routes to stand-alone file (#33190) 2024-12-07 07:46:34 +00:00
Matt Jankowski
72a4da83fd
Merge duplicate custom css request specs (#33204) 2024-12-06 18:55:47 +00:00
github-actions[bot]
8dc7f8533d
New Crowdin Translations (automated) (#33193)
Co-authored-by: GitHub Actions <noreply@github.com>
2024-12-06 15:44:21 +00:00
renovate[bot]
96498a09c2
Update dependency @babel/preset-react to v7.26.3 (#33174)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-06 15:21:12 +00:00
renovate[bot]
3ad45ced5e
Update dependency @dnd-kit/core to v6.3.1 (#33194)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-06 15:20:36 +00:00
renovate[bot]
e35655cf55
Update dependency express to v4.21.2 (#33195)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-06 15:20:19 +00:00
Claire
fbcd14ff7e
Always remove setting keys in legacy user settings migrations (#33196) 2024-12-06 10:58:18 +00:00
Eugen Rochko
25387dc423
Refactor <HashtagHeader> to TypeScript (#33096) 2024-12-06 08:42:24 +00:00
Matt Jankowski
a1143c522b
Add missing on_delete: :cascade foreign keys option (#33175) 2024-12-06 08:29:54 +00:00
renovate[bot]
635da00cfa
Update Node.js to 22.12 (#33169)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-06 08:28:21 +00:00
Eugen Rochko
d29eddc401
Fix tag trend migration failing due to duplicate rows (#33189) 2024-12-05 16:12:26 +00:00
Matt Jankowski
17c02c9210
Remove thing_type and thing_id columns from settings table (#31971) 2024-12-05 14:34:14 +00:00
Mohammad Kazemi
b82c791770
Set unicode-bidi of account header content to plaintext in styles (#33088) 2024-12-05 11:40:50 +00:00
Claire
1ee149d015
Add explicit migration breakpoints (#33089) 2024-12-05 11:40:30 +00:00
Matt Jankowski
86b45a4a48
Add rel alternate rss/json links to tags show html page (#33179) 2024-12-05 11:34:49 +00:00
Claire
21f6780498
Fix deletion of unconfirmed users with Webauthn set (#33186) 2024-12-05 09:38:48 +00:00
github-actions[bot]
8b223f3b15
New Crowdin Translations (automated) (#33185)
Co-authored-by: GitHub Actions <noreply@github.com>
Co-authored-by: Claire <claire.github-309c@sitedethib.com>
2024-12-05 09:37:49 +00:00
renovate[bot]
f9dc7842de
Update dependency browser to v6.2.0 (#33182)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-05 09:36:49 +00:00
renovate[bot]
f57c82ed35
Update opentelemetry-ruby (non-major) (#33180)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-05 09:36:25 +00:00
renovate[bot]
d58173e45b
Update dependency @dnd-kit/sortable to v10 (#33183)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-05 09:12:21 +00:00
renovate[bot]
3708039856
Update dependency @dnd-kit/core to v6.3.0 (#33181)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-05 09:12:08 +00:00
Matt Jankowski
b85d7eb7a3
Update rails-html-sanitizer to version 1.6.1 (#33177) 2024-12-05 08:48:15 +00:00
707fb0254d Change bio limit from 500 > 1000
All checks were successful
continuous-integration/drone/push Build is passing
2024-12-05 09:40:13 +01:00
Matt Jankowski
4934242aca
Update schema/annotations after tag trend db/redis migrate (#33173) 2024-12-04 17:55:56 +00:00
Matt Jankowski
0fcf227a92
Add media attachment description limit to instance API response (#33153) 2024-12-04 15:10:20 +00:00
Eugen Rochko
48ea7552dd
Change hashtag trends to be stored in the database instead of redis (#32837)
Co-authored-by: David Roetzel <david@roetzel.de>
2024-12-04 11:59:17 +00:00
renovate[bot]
b9b26490e7
Update dependency sass to v1.82.0 (#32411)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-04 10:40:47 +00:00
renovate[bot]
23a2366090
Update dependency dotenv to v16.4.7 (#33147)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-04 10:20:50 +00:00
github-actions[bot]
0262310c88
New Crowdin Translations (automated) (#33166)
Co-authored-by: GitHub Actions <noreply@github.com>
2024-12-04 10:04:02 +00:00
renovate[bot]
ebbccb196a
Update dependency aws-sdk-s3 to v1.176.0 (#33170)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-04 09:48:14 +00:00
Claire
c521a62b1e
Add tests for broken mention processing (#33171) 2024-12-04 09:45:35 +00:00
renovate[bot]
1cb303ca75
Update dependency axios to v1.7.9 (#33168)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-04 09:12:26 +00:00
Matt Jankowski
e5bea68b20
Fix empty authors preview card serialization (#33151) 2024-12-04 09:05:58 +00:00
Matt Jankowski
6fddf1610c
Fix awkwardly-worded nature of scheduled too soon error (#33156) 2024-12-04 07:43:29 +00:00
Matt Jankowski
7420f59f76
Add maxlength to registration reason input (#33162) 2024-12-04 07:40:56 +00:00
renovate[bot]
30c2a6c01c
Update dependency @rails/ujs to v7.1.500 (#32648)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-03 15:53:36 +00:00
792fbd44bd Merge remote-tracking branch 'upstream/main'
All checks were successful
continuous-integration/drone/push Build is passing
2024-12-03 15:22:44 +01:00
f9841db0be Merge remote-tracking branch 'upstream/main'
All checks were successful
continuous-integration/drone/push Build is passing
2024-11-23 16:44:47 +01:00
324e356372 Merge remote-tracking branch 'upstream/main'
All checks were successful
continuous-integration/drone/push Build is passing
2024-11-11 09:00:48 +01:00
2f5d989bab Merge remote-tracking branch 'upstream/main'
All checks were successful
continuous-integration/drone/push Build is passing
2024-10-27 13:11:29 +01:00
9df8594415 Merge remote-tracking branch 'upstream/main'
All checks were successful
continuous-integration/drone/push Build is passing
2024-10-24 11:35:29 +02:00
8ff3985e38 Merge remote-tracking branch 'upstream/main'
All checks were successful
continuous-integration/drone/push Build is passing
2024-10-08 16:53:41 +02:00
db24d060b1 Merge remote-tracking branch 'upstream/main'
All checks were successful
continuous-integration/drone/push Build is passing
2024-10-04 21:36:13 +02:00
0105d08422 Merge remote-tracking branch 'upstream/main'
All checks were successful
continuous-integration/drone/push Build is passing
2024-10-01 11:25:05 +02:00
192bc20432 Merge remote-tracking branch 'upstream/main'
All checks were successful
continuous-integration/drone/push Build is passing
2024-09-27 09:59:28 +02:00
59ef432b97 furry tab in nav panel
All checks were successful
continuous-integration/drone/push Build is passing
2024-09-26 11:39:09 +02:00
0e80d060dc Merge remote-tracking branch 'upstream/main'
Some checks failed
continuous-integration/drone/push Build is failing
2024-09-26 11:25:14 +02:00
f532af92f1 Merge remote-tracking branch 'upstream/main'
All checks were successful
continuous-integration/drone/push Build is passing
2024-09-13 13:39:47 +02:00
5c07d5c138 Merge remote-tracking branch 'upstream/main'
All checks were successful
continuous-integration/drone/push Build is passing
2024-09-13 11:20:05 +02:00
058ddd61e2 Merge remote-tracking branch 'upstream/main'
All checks were successful
continuous-integration/drone/push Build is passing
2024-09-12 22:56:19 +02:00
b8e117e713 Merge remote-tracking branch 'upstream/main'
All checks were successful
continuous-integration/drone/push Build is passing
2024-09-04 13:15:20 +02:00
59fd54020d Merge remote-tracking branch 'upstream/main'
All checks were successful
continuous-integration/drone Build is passing
2024-08-30 08:55:20 +02:00
b782e3b21c Merge remote-tracking branch 'upstream/main'
All checks were successful
continuous-integration/drone/push Build is passing
2024-07-22 10:30:39 +02:00
ae4aa86a12 Merge remote-tracking branch 'upstream/main'
All checks were successful
continuous-integration/drone/push Build is passing
2024-07-17 14:50:57 +02:00
33ec036619 Merge remote-tracking branch 'upstream/main'
All checks were successful
continuous-integration/drone/push Build is passing
2024-07-17 09:27:49 +02:00
46de49cc45 Merge remote-tracking branch 'upstream/main'
All checks were successful
continuous-integration/drone/push Build is passing
2024-07-07 11:07:03 +02:00
631378ee1c Merge remote-tracking branch 'upstream/main'
All checks were successful
continuous-integration/drone/push Build is passing
2024-06-24 09:05:39 +02:00
c1c0670a04 Merge remote-tracking branch 'upstream/main'
All checks were successful
continuous-integration/drone/push Build is passing
2024-06-19 10:02:05 +02:00
6f73d7eedd Merge remote-tracking branch 'upstream/main'
All checks were successful
continuous-integration/drone/push Build is passing
2024-06-15 18:21:04 +02:00
0552eda5d9 Merge remote-tracking branch 'upstream/main'
All checks were successful
continuous-integration/drone/push Build is passing
2024-06-03 09:52:08 +02:00
e19315b0cd Merge remote-tracking branch 'upstream/main'
All checks were successful
continuous-integration/drone/push Build is passing
2024-05-30 13:10:44 +02:00
eae50b2cb3 Merge remote-tracking branch 'upstream/main'
All checks were successful
continuous-integration/drone/push Build is passing
2024-05-21 11:19:43 +02:00
5beb356718 Merge remote-tracking branch 'upstream/main'
All checks were successful
continuous-integration/drone/push Build is passing
2024-05-01 13:57:16 +02:00
14db7f1a05 Merge remote-tracking branch 'upstream/main'
All checks were successful
continuous-integration/drone/push Build is passing
2024-04-29 09:50:14 +02:00
61c71f9c5b Merge remote-tracking branch 'upstream/main'
All checks were successful
continuous-integration/drone/push Build is passing
2024-04-26 13:26:00 +02:00
12ef898447 Merge remote-tracking branch 'upstream/main'
All checks were successful
continuous-integration/drone/push Build is passing
2024-04-23 13:55:48 +02:00
3b11f173b3 Merge remote-tracking branch 'upstream/main'
All checks were successful
continuous-integration/drone/push Build is passing
2024-04-22 09:59:07 +02:00
61cd0f81b5 max fields
All checks were successful
continuous-integration/drone/push Build is passing
2024-04-17 16:55:18 +02:00
32414a51e9 Merge remote-tracking branch 'upstream/main'
All checks were successful
continuous-integration/drone/push Build is passing
2024-03-25 11:42:07 +01:00
95a990fb6d Merge remote-tracking branch 'upstream/main'
All checks were successful
continuous-integration/drone/push Build is passing
2024-03-19 13:41:44 +01:00
ffae506e12 Merge remote-tracking branch 'upstream/main'
Some checks reported errors
continuous-integration/drone/push Build was killed
continuous-integration/drone Build is passing
2024-03-15 11:06:28 +01:00
2ed4a45528 Merge remote-tracking branch 'upstream/main'
All checks were successful
continuous-integration/drone/push Build is passing
2024-03-12 13:34:44 +01:00
2cc89b04b4 Merge remote-tracking branch 'upstream/main'
All checks were successful
continuous-integration/drone/push Build is passing
2024-03-04 13:53:47 +01:00
6cb18320c0 Merge remote-tracking branch 'upstream/main'
All checks were successful
continuous-integration/drone/push Build is passing
2024-02-17 13:01:40 +01:00
ff5beb6a60 Merge remote-tracking branch 'upstream/main'
All checks were successful
continuous-integration/drone/push Build is passing
2024-02-12 08:46:58 +01:00
815809a6a2 Merge remote-tracking branch 'upstream/main'
All checks were successful
continuous-integration/drone/push Build is passing
2024-02-06 14:30:04 +01:00
4d23759cf7 Merge remote-tracking branch 'upstream/main'
Some checks reported errors
continuous-integration/drone/push Build encountered an error
continuous-integration/drone Build is passing
2024-02-01 19:35:09 +01:00
ed4a0bba04 Merge remote-tracking branch 'upstream/main'
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-31 10:02:26 +01:00
d5769ec3e9 Merge remote-tracking branch 'upstream/main'
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-24 10:52:32 +01:00
19c61afd99 Merge remote-tracking branch 'upstream/main'
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-18 10:22:32 +01:00
ee9d63a7a1 Merge remote-tracking branch 'upstream/main'
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-17 11:25:25 +01:00
29523f518b Merge remote-tracking branch 'upstream/main'
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-16 13:14:39 +01:00
9b8fc472b5 add pets svg
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-16 10:07:00 +01:00
12bfa9781b Merge remote-tracking branch 'upstream/main'
Some checks failed
continuous-integration/drone/push Build is failing
2024-01-16 09:47:16 +01:00
373f1871b9 Merge remote-tracking branch 'upstream/main'
All checks were successful
continuous-integration/drone/push Build is passing
2023-12-28 11:26:44 +01:00
f3fefa3dea Merge remote-tracking branch 'upstream/main'
All checks were successful
continuous-integration/drone/push Build is passing
2023-12-20 10:29:58 +01:00
9c31391cb9 Merge remote-tracking branch 'upstream/main'
All checks were successful
continuous-integration/drone/push Build is passing
2023-12-19 13:10:05 +01:00
6b71734b73 Merge remote-tracking branch 'upstream/main'
All checks were successful
continuous-integration/drone/push Build is passing
2023-12-18 14:47:49 +01:00
63a70ea1f1 Merge remote-tracking branch 'upstream/main'
All checks were successful
continuous-integration/drone Build is passing
continuous-integration/drone/push Build is passing
2023-12-17 14:40:41 +01:00
7e5ed46553 Merge remote-tracking branch 'upstream/main'
All checks were successful
continuous-integration/drone Build is passing
2023-12-11 17:06:55 +01:00
5082246ee5 Merge remote-tracking branch 'upstream/main'
All checks were successful
continuous-integration/drone Build is passing
2023-12-03 11:19:50 +01:00
3dd4a94c57 Merge remote-tracking branch 'upstream/main'
All checks were successful
continuous-integration/drone Build is passing
2023-12-01 10:57:58 +01:00
2656e00d74 update ci
All checks were successful
continuous-integration/drone Build is passing
2023-11-30 12:14:24 +01:00
91a78e0652 Merge remote-tracking branch 'upstream/main' 2023-11-30 12:12:59 +01:00
03fdbfe11a Merge remote-tracking branch 'upstream/main'
All checks were successful
continuous-integration/drone Build is passing
2023-11-24 13:23:08 +01:00
b3377b7883 trending score half-life to 2 hours 2023-11-19 02:53:34 +01:00
2c4f8b71cb Merge remote-tracking branch 'upstream/main'
All checks were successful
continuous-integration/drone Build is passing
2023-11-19 02:41:47 +01:00
7586c4c6f9 Merge remote-tracking branch 'upstream/main'
All checks were successful
continuous-integration/drone Build is passing
2023-11-10 22:09:54 +01:00
d2dd0d5e15 version
All checks were successful
continuous-integration/drone Build is passing
2023-11-10 13:30:48 +01:00
c07ddbe7b1 Merge remote-tracking branch 'upstream/main'
All checks were successful
continuous-integration/drone Build is passing
2023-11-10 00:29:32 +01:00
05855361cb revert ba7703207c
All checks were successful
continuous-integration/drone Build is passing
revert new dockerfile
2023-11-09 23:17:05 +01:00
0b20085f0e Merge pull request 'new dockerfile' (#1) from dockerfile-rewrite into main
All checks were successful
continuous-integration/drone Build is passing
Reviewed-on: #1
2023-11-09 22:29:42 +01:00
ba7703207c new dockerfile 2023-11-09 22:28:33 +01:00
4c6fcd3452 version
All checks were successful
continuous-integration/drone Build is passing
2023-11-09 21:58:00 +01:00
f39c791bbd s3 retries 2023-11-09 21:55:34 +01:00
bb10a911b5 update check cron 2023-11-09 21:54:16 +01:00
d126d231c7 footer 2023-11-09 21:53:50 +01:00
fb94cd866a drone
All checks were successful
continuous-integration/drone Build is passing
2023-11-09 21:47:38 +01:00
bcbdfc19fd max post chars and profile fields 2023-11-09 21:45:08 +01:00
f3309786f8 furry tab 2023-11-09 21:41:45 +01:00
966 changed files with 17533 additions and 9444 deletions

View file

@ -1,10 +1,6 @@
[production]
defaults
> 0.2%
firefox >= 78
ios >= 15.6
not dead
not OperaMini all
[development]
supports es6-module

33
.drone.yml Normal file
View file

@ -0,0 +1,33 @@
kind: pipeline
name: mastodon
type: kubernetes
steps:
- name: build-mastodon
image: plugins/docker
environment:
DOCKER_BUILDKIT: 1
settings:
registry: git.greyfox.tech
username:
from_secret: registry_user
password:
from_secret: registry_pass
repo: git.greyfox.tech/bark/mastodon
dockerfile: Dockerfile
tags:
- latest
- name: build-streaming
image: plugins/docker
environment:
DOCKER_BUILDKIT: 1
settings:
registry: git.greyfox.tech
username:
from_secret: registry_user
password:
from_secret: registry_pass
repo: git.greyfox.tech/bark/mastodon-streaming
dockerfile: streaming/Dockerfile
tags:
- latest

View file

@ -109,7 +109,7 @@ module.exports = defineConfig({
'react/jsx-equals-spacing': 'error',
'react/jsx-no-bind': 'error',
'react/jsx-no-useless-fragment': 'error',
'react/jsx-no-target-blank': 'off',
'react/jsx-no-target-blank': ['error', { allowReferrer: true }],
'react/jsx-tag-spacing': 'error',
'react/jsx-uses-react': 'off', // not needed with new JSX transform
'react/jsx-wrap-multilines': 'error',

View file

@ -60,7 +60,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. v3.3.5)
- Ruby version: (from `ruby --version`, eg. v3.4.1)
- Node.js version: (from `node --version`, eg. v20.18.0)
validations:
required: false

View file

@ -61,7 +61,7 @@ body:
value: |
Please at least include those informations:
- Operating system: (eg. Ubuntu 22.04)
- Ruby version: (from `ruby --version`, eg. v3.3.5)
- Ruby version: (from `ruby --version`, eg. v3.4.1)
- Node.js version: (from `node --version`, eg. v20.18.0)
validations:
required: false

View file

@ -18,7 +18,7 @@ permissions:
jobs:
check-i18n:
runs-on: ubuntu-24.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

View file

@ -50,7 +50,7 @@ jobs:
# Create or update the pull request
- name: Create Pull Request
uses: peter-evans/create-pull-request@v7.0.5
uses: peter-evans/create-pull-request@v7.0.6
with:
commit-message: 'New Crowdin translations'
title: 'New Crowdin Translations for ${{ github.base_ref || github.ref_name }} (automated)'

View file

@ -52,7 +52,7 @@ jobs:
# Create or update the pull request
- name: Create Pull Request
uses: peter-evans/create-pull-request@v7.0.5
uses: peter-evans/create-pull-request@v7
with:
commit-message: 'New Crowdin translations'
title: 'New Crowdin Translations (automated)'

View file

@ -40,4 +40,4 @@ jobs:
uses: ./.github/actions/setup-javascript
- name: Stylelint
run: yarn lint:css -f github
run: yarn lint:css --custom-formatter @csstools/stylelint-formatter-github

View file

@ -9,6 +9,7 @@ on:
- 'Gemfile*'
- '.rubocop*.yml'
- '.ruby-version'
- 'bin/rubocop'
- 'config/brakeman.ignore'
- '**/*.rb'
- '**/*.rake'
@ -19,6 +20,7 @@ on:
- 'Gemfile*'
- '.rubocop*.yml'
- '.ruby-version'
- 'bin/rubocop'
- 'config/brakeman.ignore'
- '**/*.rb'
- '**/*.rake'

View file

@ -12,6 +12,7 @@ on:
- '**/*.rb'
- '.github/workflows/test-migrations.yml'
- 'lib/tasks/tests.rake'
- 'lib/tasks/db.rake'
pull_request:
paths:
@ -90,6 +91,11 @@ jobs:
bin/rails db:drop
bin/rails db:create
SKIP_POST_DEPLOYMENT_MIGRATIONS=true bin/rails tests:migrations:prepare_database
# Migrate up to v4.2.0 breakpoint
bin/rails db:migrate VERSION=20230907150100
# Migrate the rest
SKIP_POST_DEPLOYMENT_MIGRATIONS=true bin/rails db:migrate
bin/rails db:migrate
bin/rails tests:migrations:check_database

View file

@ -125,6 +125,7 @@ jobs:
matrix:
ruby-version:
- '3.2'
- '3.3'
- '.ruby-version'
steps:
- uses: actions/checkout@v4
@ -166,7 +167,7 @@ jobs:
- name: Upload coverage reports to Codecov
if: matrix.ruby-version == '.ruby-version'
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@v5
with:
files: coverage/lcov/*.lcov
env:
@ -174,7 +175,7 @@ jobs:
test-libvips:
name: Libvips tests
runs-on: ubuntu-24.04
runs-on: ubuntu-latest
needs:
- build
@ -226,6 +227,7 @@ jobs:
matrix:
ruby-version:
- '3.2'
- '3.3'
- '.ruby-version'
steps:
- uses: actions/checkout@v4
@ -252,7 +254,7 @@ jobs:
- name: Upload coverage reports to Codecov
if: matrix.ruby-version == '.ruby-version'
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@v5
with:
files: coverage/lcov/mastodon.lcov
env:
@ -304,6 +306,7 @@ jobs:
matrix:
ruby-version:
- '3.2'
- '3.3'
- '.ruby-version'
steps:
@ -420,6 +423,7 @@ jobs:
matrix:
ruby-version:
- '3.2'
- '3.3'
- '.ruby-version'
search-image:
- docker.elastic.co/elasticsearch/elasticsearch:7.17.13

2
.nvmrc
View file

@ -1 +1 @@
22.11
22.13

View file

@ -1,4 +1,7 @@
---
Style/ArrayIntersect:
Enabled: false
Style/ClassAndModuleChildren:
Enabled: false
@ -19,6 +22,13 @@ Style/HashSyntax:
EnforcedShorthandSyntax: either
EnforcedStyle: ruby19_no_mixed_keys
Style/IfUnlessModifier:
Exclude:
- '**/*.haml'
Style/KeywordArgumentsMerging:
Enabled: false
Style/NumericLiterals:
AllowedPatterns:
- \d{4}_\d{2}_\d{2}_\d{6}
@ -37,6 +47,9 @@ Style/RedundantFetchBlock:
Style/RescueStandardError:
EnforcedStyle: implicit
Style/SafeNavigationChainLength:
Enabled: false
Style/SymbolArray:
Enabled: false

View file

@ -1,6 +1,6 @@
# This configuration was generated by
# `rubocop --auto-gen-config --auto-gen-only-exclude --no-offense-counts --no-auto-gen-timestamp`
# using RuboCop version 1.66.1.
# using RuboCop version 1.69.2.
# The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new
@ -8,7 +8,7 @@
Lint/NonLocalExitFromIterator:
Exclude:
- 'app/helpers/jsonld_helper.rb'
- 'app/helpers/json_ld_helper.rb'
# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes.
Metrics/AbcSize:
@ -35,7 +35,6 @@ Rails/OutputSafety:
# Configuration parameters: AllowedVars.
Style/FetchEnvVar:
Exclude:
- 'app/lib/translation_service.rb'
- 'config/environments/production.rb'
- 'config/initializers/2_limited_federation_mode.rb'
- 'config/initializers/3_omniauth.rb'
@ -83,7 +82,7 @@ Style/MutableConstant:
# AllowedMethods: respond_to_missing?
Style/OptionalBooleanParameter:
Exclude:
- 'app/helpers/jsonld_helper.rb'
- 'app/helpers/json_ld_helper.rb'
- 'app/lib/admin/system_check/message.rb'
- 'app/lib/request.rb'
- 'app/lib/webfinger.rb'

View file

@ -1 +1 @@
3.3.6
3.4.1

View file

@ -2,6 +2,48 @@
All notable changes to this project will be documented in this file.
## [4.3.2] - 2024-12-03
### Added
- Add `tootctl feeds vacuum` (#33065 by @ClearlyClaire)
- Add error message when user tries to follow their own account (#31910 by @lenikadali)
- Add client_secret_expires_at to OAuth Applications (#30317 by @ThisIsMissEm)
### Changed
- Change design of Content Warnings and filters (#32543 by @ClearlyClaire)
### Fixed
- Fix processing incoming post edits with mentions to unresolvable accounts (#33129 by @ClearlyClaire)
- Fix error when including multiple instances of `embed.js` (#33107 by @YKWeyer)
- Fix inactive users' timelines being backfilled on follow and unsuspend (#33094 by @ClearlyClaire)
- Fix direct inbox delivery pushing posts into inactive followers' timelines (#33067 by @ClearlyClaire)
- Fix `TagFollow` records not being correctly handled in account operations (#33063 by @ClearlyClaire)
- Fix pushing hashtag-followed posts to feeds of inactive users (#33018 by @Gargron)
- Fix duplicate notifications in notification groups when using slow mode (#33014 by @ClearlyClaire)
- Fix posts made in the future being allowed to trend (#32996 by @ClearlyClaire)
- Fix uploading higher-than-wide GIF profile picture with libvips enabled (#32911 by @ClearlyClaire)
- Fix domain attribution field having autocorrect and autocapitalize enabled (#32903 by @ClearlyClaire)
- Fix titles being escaped twice (#32889 by @ClearlyClaire)
- Fix list creation limit check (#32869 by @ClearlyClaire)
- Fix error in `tootctl email_domain_blocks` when supplying `--with-dns-records` (#32863 by @mjankowski)
- Fix `min_id` and `max_id` causing error in search API (#32857 by @Gargron)
- Fix inefficiencies when processing removal of posts that use featured tags (#32787 by @ClearlyClaire)
- Fix alt-text pop-in not using the translated description (#32766 by @ClearlyClaire)
- Fix preview cards with long titles erroneously causing layout changes (#32678 by @ClearlyClaire)
- Fix embed modal layout on mobile (#32641 by @DismalShadowX)
- Fix and improve batch attachment deletion handling when using OpenStack Swift (#32637 by @hugogameiro)
- Fix blocks not being applied on link timeline (#32625 by @tribela)
- Fix follow counters being incorrectly changed (#32622 by @oneiros)
- Fix 'unknown' media attachment type rendering (#32613 and #32713 by @ThisIsMissEm and @renatolond)
- Fix tl language native name (#32606 by @seav)
### Security
- Update dependencies
## [4.3.1] - 2024-10-21
### Added
@ -93,7 +135,7 @@ The following changelog entries focus on changes visible to users, administrator
- **Add notifications of severed relationships** (#27511, #29665, #29668, #29670, #29700, #29714, #29712, and #29731 by @ClearlyClaire and @Gargron)\
Notify local users when they lose relationships as a result of a local moderator blocking a remote account or server, allowing the affected user to retrieve the list of broken relationships.\
Note that this does not notify remote users.\
This adds the `severed_relationships` notification type to the REST API and streaming, with a new [`relationship_severance_event` attribute](https://docs.joinmastodon.org/entities/Notification/#relationship_severance_event).
This adds the `severed_relationships` notification type to the REST API and streaming, with a new [`event` attribute](https://docs.joinmastodon.org/entities/Notification/#relationship_severance_event).
- **Add hover cards in web UI** (#30754, #30864, #30850, #30879, #30928, #30949, #30948, #30931, and #31300 by @ClearlyClaire, @Gargron, and @renchap)\
Hovering over an avatar or username will now display a hover card with the first two lines of the user's description and their first two profile fields.\
This can be disabled in the “Animations and accessibility” section of the preferences.

View file

@ -12,7 +12,7 @@ You can contribute in the following ways:
If your contributions are accepted into Mastodon, you can request to be paid through [our OpenCollective](https://opencollective.com/mastodon).
Please review the org-level [contribution guidelines] for high-level acceptance
criteria guidance.
criteria guidance and the [DEVELOPMENT] guide for environment-specific details.
[contribution guidelines]: https://github.com/mastodon/.github/blob/main/CONTRIBUTING.md
@ -53,3 +53,5 @@ It is not always possible to phrase every change in such a manner, but it is des
## Documentation
The [Mastodon documentation](https://docs.joinmastodon.org) is a statically generated site. You can [submit merge requests to mastodon/documentation](https://github.com/mastodon/documentation).
[DEVELOPMENT]: docs/DEVELOPMENT.md

View file

@ -10,9 +10,9 @@
ARG TARGETPLATFORM=${TARGETPLATFORM}
ARG BUILDPLATFORM=${BUILDPLATFORM}
# Ruby image to use for base image, change with [--build-arg RUBY_VERSION="3.3.x"]
# Ruby image to use for base image, change with [--build-arg RUBY_VERSION="3.4.x"]
# renovate: datasource=docker depName=docker.io/ruby
ARG RUBY_VERSION="3.3.6"
ARG RUBY_VERSION="3.4.1"
# # Node version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="20"]
# renovate: datasource=node-version depName=node
ARG NODE_MAJOR_VERSION="22"
@ -20,15 +20,15 @@ ARG NODE_MAJOR_VERSION="22"
ARG DEBIAN_VERSION="bookworm"
# Node image to use for base image based on combined variables (ex: 20-bookworm-slim)
FROM docker.io/node:${NODE_MAJOR_VERSION}-${DEBIAN_VERSION}-slim AS node
# Ruby image to use for base image based on combined variables (ex: 3.3.x-slim-bookworm)
# Ruby image to use for base image based on combined variables (ex: 3.4.x-slim-bookworm)
FROM docker.io/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"]
ARG MASTODON_VERSION_PRERELEASE=""
ARG MASTODON_VERSION_PRERELEASE="bark"
# Append build metadata or fork information to version.rb [--build-arg MASTODON_VERSION_METADATA="pr-123456"]
ARG MASTODON_VERSION_METADATA=""
ARG MASTODON_VERSION_METADATA="dev"
# Will be available as Mastodon::Version.source_commit
ARG SOURCE_COMMIT=""

18
Gemfile
View file

@ -1,12 +1,12 @@
# frozen_string_literal: true
source 'https://rubygems.org'
ruby '>= 3.2.0'
ruby '>= 3.2.0', '< 3.5.0'
gem 'propshaft'
gem 'puma', '~> 6.3'
gem 'rack', '~> 2.2.7'
gem 'rails', '~> 7.2.0'
gem 'rails', '~> 8.0'
gem 'thor', '~> 1.2'
gem 'dotenv'
@ -73,13 +73,13 @@ gem 'public_suffix', '~> 6.0'
gem 'pundit', '~> 2.3'
gem 'rack-attack', '~> 6.6'
gem 'rack-cors', '~> 2.0', require: 'rack/cors'
gem 'rails-i18n', '~> 7.0'
gem 'rails-i18n', '~> 8.0'
gem 'redcarpet', '~> 3.6'
gem 'redis', '~> 4.5', require: ['redis', 'redis/connection/hiredis']
gem 'redis-namespace', '~> 1.10'
gem 'rqrcode', '~> 2.2'
gem 'ruby-progressbar', '~> 1.13'
gem 'sanitize', '~> 6.0'
gem 'sanitize', '~> 7.0'
gem 'scenic', '~> 1.7'
gem 'sidekiq', '~> 6.5'
gem 'sidekiq-bulk', '~> 0.2.0'
@ -105,16 +105,16 @@ gem 'opentelemetry-api', '~> 1.4.0'
group :opentelemetry do
gem 'opentelemetry-exporter-otlp', '~> 0.29.0', require: false
gem 'opentelemetry-instrumentation-active_job', '~> 0.7.1', require: false
gem 'opentelemetry-instrumentation-active_model_serializers', '~> 0.20.1', require: false
gem 'opentelemetry-instrumentation-active_model_serializers', '~> 0.21.0', require: false
gem 'opentelemetry-instrumentation-concurrent_ruby', '~> 0.21.2', require: false
gem 'opentelemetry-instrumentation-excon', '~> 0.22.0', require: false
gem 'opentelemetry-instrumentation-faraday', '~> 0.24.1', require: false
gem 'opentelemetry-instrumentation-faraday', '~> 0.25.0', require: false
gem 'opentelemetry-instrumentation-http', '~> 0.23.2', require: false
gem 'opentelemetry-instrumentation-http_client', '~> 0.22.3', require: false
gem 'opentelemetry-instrumentation-net_http', '~> 0.22.4', require: false
gem 'opentelemetry-instrumentation-pg', '~> 0.29.0', require: false
gem 'opentelemetry-instrumentation-rack', '~> 0.25.0', require: false
gem 'opentelemetry-instrumentation-rails', '~> 0.33.0', require: false
gem 'opentelemetry-instrumentation-rails', '~> 0.34.0', require: false
gem 'opentelemetry-instrumentation-redis', '~> 0.25.3', require: false
gem 'opentelemetry-instrumentation-sidekiq', '~> 0.25.2', require: false
gem 'opentelemetry-sdk', '~> 1.4', require: false
@ -183,7 +183,7 @@ group :development do
gem 'letter_opener_web', '~> 3.0'
# Security analysis CLI tools
gem 'brakeman', '~> 6.0', require: false
gem 'brakeman', '~> 7.0', require: false
gem 'bundler-audit', '~> 0.9', require: false
# Linter CLI for HAML files
@ -222,7 +222,7 @@ gem 'concurrent-ruby', require: false
gem 'connection_pool', require: false
gem 'xorcist', '~> 1.1'
gem 'net-http', '~> 0.5.0'
gem 'net-http', '~> 0.6.0'
gem 'rubyzip', '~> 2.3'
gem 'hcaptcha', '~> 7.1'

View file

@ -10,46 +10,45 @@ GIT
GEM
remote: https://rubygems.org/
specs:
actioncable (7.2.2)
actionpack (= 7.2.2)
activesupport (= 7.2.2)
actioncable (8.0.1)
actionpack (= 8.0.1)
activesupport (= 8.0.1)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6)
actionmailbox (7.2.2)
actionpack (= 7.2.2)
activejob (= 7.2.2)
activerecord (= 7.2.2)
activestorage (= 7.2.2)
activesupport (= 7.2.2)
actionmailbox (8.0.1)
actionpack (= 8.0.1)
activejob (= 8.0.1)
activerecord (= 8.0.1)
activestorage (= 8.0.1)
activesupport (= 8.0.1)
mail (>= 2.8.0)
actionmailer (7.2.2)
actionpack (= 7.2.2)
actionview (= 7.2.2)
activejob (= 7.2.2)
activesupport (= 7.2.2)
actionmailer (8.0.1)
actionpack (= 8.0.1)
actionview (= 8.0.1)
activejob (= 8.0.1)
activesupport (= 8.0.1)
mail (>= 2.8.0)
rails-dom-testing (~> 2.2)
actionpack (7.2.2)
actionview (= 7.2.2)
activesupport (= 7.2.2)
actionpack (8.0.1)
actionview (= 8.0.1)
activesupport (= 8.0.1)
nokogiri (>= 1.8.5)
racc
rack (>= 2.2.4, < 3.2)
rack (>= 2.2.4)
rack-session (>= 1.0.1)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
useragent (~> 0.16)
actiontext (7.2.2)
actionpack (= 7.2.2)
activerecord (= 7.2.2)
activestorage (= 7.2.2)
activesupport (= 7.2.2)
actiontext (8.0.1)
actionpack (= 8.0.1)
activerecord (= 8.0.1)
activestorage (= 8.0.1)
activesupport (= 8.0.1)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
actionview (7.2.2)
activesupport (= 7.2.2)
actionview (8.0.1)
activesupport (= 8.0.1)
builder (~> 3.1)
erubi (~> 1.11)
rails-dom-testing (~> 2.2)
@ -59,22 +58,22 @@ GEM
activemodel (>= 4.1)
case_transform (>= 0.2)
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
activejob (7.2.2)
activesupport (= 7.2.2)
activejob (8.0.1)
activesupport (= 8.0.1)
globalid (>= 0.3.6)
activemodel (7.2.2)
activesupport (= 7.2.2)
activerecord (7.2.2)
activemodel (= 7.2.2)
activesupport (= 7.2.2)
activemodel (8.0.1)
activesupport (= 8.0.1)
activerecord (8.0.1)
activemodel (= 8.0.1)
activesupport (= 8.0.1)
timeout (>= 0.4.0)
activestorage (7.2.2)
actionpack (= 7.2.2)
activejob (= 7.2.2)
activerecord (= 7.2.2)
activesupport (= 7.2.2)
activestorage (8.0.1)
actionpack (= 8.0.1)
activejob (= 8.0.1)
activerecord (= 8.0.1)
activesupport (= 8.0.1)
marcel (~> 1.0)
activesupport (7.2.2)
activesupport (8.0.1)
base64
benchmark (>= 0.3)
bigdecimal
@ -86,6 +85,7 @@ GEM
minitest (>= 5.1)
securerandom (>= 0.3)
tzinfo (~> 2.0, >= 2.0.5)
uri (>= 0.13.1)
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
aes_key_wrap (1.1.0)
@ -94,8 +94,8 @@ GEM
ast (2.4.2)
attr_required (1.0.2)
aws-eventstream (1.3.0)
aws-partitions (1.1015.0)
aws-sdk-core (3.214.0)
aws-partitions (1.1032.0)
aws-sdk-core (3.214.1)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
@ -103,13 +103,13 @@ GEM
aws-sdk-kms (1.96.0)
aws-sdk-core (~> 3, >= 3.210.0)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.175.0)
aws-sdk-s3 (1.177.0)
aws-sdk-core (~> 3, >= 3.210.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
aws-sigv4 (1.10.1)
aws-eventstream (~> 1, >= 1.0.2)
azure-blob (0.5.3)
azure-blob (0.5.4)
rexml
base64 (0.2.0)
bcp47_spec (0.2.1)
@ -119,16 +119,16 @@ GEM
erubi (>= 1.0.0)
rack (>= 0.9.0)
rouge (>= 1.0.0)
bigdecimal (3.1.8)
bigdecimal (3.1.9)
bindata (2.5.0)
binding_of_caller (1.0.1)
debug_inspector (>= 1.2.0)
blurhash (0.1.8)
bootsnap (1.18.4)
msgpack (~> 1.2)
brakeman (6.2.2)
brakeman (7.0.0)
racc
browser (6.1.0)
browser (6.2.0)
brpoplpush-redis_script (0.1.3)
concurrent-ruby (~> 1.0, >= 1.0.5)
redis (>= 1.0, < 6)
@ -160,7 +160,7 @@ GEM
cocoon (1.2.15)
color_diff (0.1)
concurrent-ruby (1.3.4)
connection_pool (2.4.1)
connection_pool (2.5.0)
cose (1.3.1)
cbor (~> 0.5.9)
openssl-signature_algorithm (~> 1.0)
@ -168,15 +168,15 @@ GEM
bigdecimal
rexml
crass (1.0.6)
css_parser (1.19.1)
css_parser (1.21.0)
addressable
csv (3.3.0)
csv (3.3.2)
database_cleaner-active_record (2.2.0)
activerecord (>= 5.a)
database_cleaner-core (~> 2.0.0)
database_cleaner-core (2.0.1)
date (3.4.0)
debug (1.9.2)
date (3.4.1)
debug (1.10.0)
irb (~> 1.10)
reline (>= 0.3.8)
debug_inspector (1.2.0)
@ -199,9 +199,9 @@ GEM
activerecord (>= 4.2, < 9.0)
docile (1.4.1)
domain_name (0.6.20240107)
doorkeeper (5.8.0)
doorkeeper (5.8.1)
railties (>= 5)
dotenv (3.1.4)
dotenv (3.1.7)
drb (2.2.1)
elasticsearch (7.17.11)
elasticsearch-api (= 7.17.11)
@ -217,32 +217,32 @@ GEM
htmlentities (~> 4.3.3)
launchy (>= 2.1, < 4.0)
mail (~> 2.7)
erubi (1.13.0)
erubi (1.13.1)
et-orbi (1.2.11)
tzinfo
excon (0.112.0)
fabrication (2.31.0)
faker (3.5.1)
i18n (>= 1.8.11, < 2)
faraday (2.12.0)
faraday-net_http (>= 2.0, < 3.4)
faraday (2.12.2)
faraday-net_http (>= 2.0, < 3.5)
json
logger
faraday-httpclient (2.0.1)
httpclient (>= 2.2)
faraday-net_http (3.3.0)
net-http
faraday-net_http (3.4.0)
net-http (>= 0.5.0)
fast_blank (1.0.1)
fastimage (2.3.1)
ffi (1.17.0)
fastimage (2.4.0)
ffi (1.17.1)
ffi-compiler (1.3.2)
ffi (>= 1.15.5)
rake
flatware (2.3.3)
flatware (2.3.4)
drb
thor (< 2.0)
flatware-rspec (2.3.3)
flatware (= 2.3.3)
flatware-rspec (2.3.4)
flatware (= 2.3.4)
rspec (>= 3.6)
fog-core (2.5.0)
builder
@ -279,7 +279,7 @@ GEM
rainbow
rubocop (>= 1.0)
sysexits (~> 1.1)
hashdiff (1.1.1)
hashdiff (1.1.2)
hashie (5.0.0)
hcaptcha (7.1.0)
json
@ -294,7 +294,7 @@ GEM
http-cookie (~> 1.0)
http-form_data (~> 2.2)
llhttp-ffi (~> 0.5.0)
http-cookie (1.0.5)
http-cookie (1.0.8)
domain_name (~> 0.5)
http-form_data (2.3.0)
http_accept_language (2.1.1)
@ -318,8 +318,8 @@ GEM
inline_svg (1.10.0)
activesupport (>= 3.0)
nokogiri (>= 1.6)
io-console (0.7.2)
irb (1.14.1)
io-console (0.8.0)
irb (1.14.3)
rdoc (>= 4.0.0)
reline (>= 0.4.2)
jd-paperclip-azure (3.0.0)
@ -327,7 +327,7 @@ GEM
azure-blob (~> 0.5.2)
hashie (~> 5.0)
jmespath (1.6.2)
json (2.8.1)
json (2.9.1)
json-canonicalization (1.0.0)
json-jwt (1.15.3.1)
activesupport (>= 4.2)
@ -384,13 +384,13 @@ GEM
llhttp-ffi (0.5.0)
ffi-compiler (~> 1.0)
rake (~> 13.0)
logger (1.6.1)
logger (1.6.4)
lograge (0.14.0)
actionpack (>= 4)
activesupport (>= 4)
railties (>= 4)
request_store (~> 1.0)
loofah (2.23.1)
loofah (2.24.0)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
mail (2.8.1)
@ -406,16 +406,16 @@ GEM
mime-types (3.6.0)
logger
mime-types-data (~> 3.2015)
mime-types-data (3.2024.1105)
mime-types-data (3.2024.1203)
mini_mime (1.1.5)
mini_portile2 (2.8.8)
minitest (5.25.2)
minitest (5.25.4)
msgpack (1.7.5)
multi_json (1.15.0)
mutex_m (0.3.0)
net-http (0.5.0)
net-http (0.6.0)
uri
net-imap (0.5.1)
net-imap (0.5.4)
date
net-protocol
net-ldap (0.19.0)
@ -426,10 +426,10 @@ GEM
net-smtp (0.5.0)
net-protocol
nio4r (2.7.4)
nokogiri (1.16.8)
nokogiri (1.18.1)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
oj (3.16.7)
oj (3.16.9)
bigdecimal (>= 3.0)
ostruct (>= 0.2)
omniauth (2.1.2)
@ -460,13 +460,13 @@ GEM
validate_email
validate_url
webfinger (~> 1.2)
openssl (3.2.0)
openssl (3.2.1)
openssl-signature_algorithm (1.3.0)
openssl (> 2.0)
opentelemetry-api (1.4.0)
opentelemetry-common (0.21.0)
opentelemetry-api (~> 1.0)
opentelemetry-exporter-otlp (0.29.0)
opentelemetry-exporter-otlp (0.29.1)
google-protobuf (>= 3.18)
googleapis-common-protos-types (~> 1.3)
opentelemetry-api (~> 1.1)
@ -475,28 +475,29 @@ GEM
opentelemetry-semantic_conventions
opentelemetry-helpers-sql-obfuscation (0.2.1)
opentelemetry-common (~> 0.21)
opentelemetry-instrumentation-action_mailer (0.2.0)
opentelemetry-instrumentation-action_mailer (0.3.0)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-active_support (~> 0.1)
opentelemetry-instrumentation-active_support (~> 0.7)
opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-action_pack (0.10.0)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-rack (~> 0.21)
opentelemetry-instrumentation-action_view (0.7.3)
opentelemetry-instrumentation-action_view (0.8.0)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-active_support (~> 0.6)
opentelemetry-instrumentation-active_support (~> 0.7)
opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-active_job (0.7.8)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-active_model_serializers (0.20.2)
opentelemetry-instrumentation-active_model_serializers (0.21.1)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-active_support (>= 0.7.0)
opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-active_record (0.8.1)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-active_support (0.6.0)
opentelemetry-instrumentation-active_support (0.7.0)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-base (0.22.6)
@ -509,7 +510,7 @@ GEM
opentelemetry-instrumentation-excon (0.22.5)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-faraday (0.24.7)
opentelemetry-instrumentation-faraday (0.25.0)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-http (0.23.5)
@ -521,21 +522,21 @@ GEM
opentelemetry-instrumentation-net_http (0.22.8)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-pg (0.29.1)
opentelemetry-instrumentation-pg (0.29.2)
opentelemetry-api (~> 1.0)
opentelemetry-helpers-sql-obfuscation
opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-rack (0.25.0)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-rails (0.33.1)
opentelemetry-instrumentation-rails (0.34.0)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-action_mailer (~> 0.2.0)
opentelemetry-instrumentation-action_mailer (~> 0.3.0)
opentelemetry-instrumentation-action_pack (~> 0.10.0)
opentelemetry-instrumentation-action_view (~> 0.7.0)
opentelemetry-instrumentation-action_view (~> 0.8.0)
opentelemetry-instrumentation-active_job (~> 0.7.0)
opentelemetry-instrumentation-active_record (~> 0.8.0)
opentelemetry-instrumentation-active_support (~> 0.6.0)
opentelemetry-instrumentation-active_support (~> 0.7.0)
opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-redis (0.25.7)
opentelemetry-api (~> 1.0)
@ -545,7 +546,7 @@ GEM
opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-registry (0.3.1)
opentelemetry-api (~> 1.1)
opentelemetry-sdk (1.5.0)
opentelemetry-sdk (1.6.0)
opentelemetry-api (~> 1.1)
opentelemetry-common (~> 0.20)
opentelemetry-registry (~> 0.2)
@ -554,7 +555,8 @@ GEM
opentelemetry-api (~> 1.0)
orm_adapter (0.5.0)
ostruct (0.6.1)
ox (2.14.18)
ox (2.14.19)
bigdecimal (>= 3.0)
parallel (1.26.3)
parser (3.3.6.0)
ast (~> 2.4.1)
@ -578,7 +580,8 @@ GEM
activesupport (>= 7.0.0)
rack
railties (>= 7.0.0)
psych (5.2.0)
psych (5.2.2)
date
stringio
public_suffix (6.0.1)
puma (6.5.0)
@ -605,25 +608,25 @@ GEM
rack
rack-session (1.0.2)
rack (< 3)
rack-test (2.1.0)
rack-test (2.2.0)
rack (>= 1.3)
rackup (1.0.0)
rackup (1.0.1)
rack (< 3)
webrick
rails (7.2.2)
actioncable (= 7.2.2)
actionmailbox (= 7.2.2)
actionmailer (= 7.2.2)
actionpack (= 7.2.2)
actiontext (= 7.2.2)
actionview (= 7.2.2)
activejob (= 7.2.2)
activemodel (= 7.2.2)
activerecord (= 7.2.2)
activestorage (= 7.2.2)
activesupport (= 7.2.2)
rails (8.0.1)
actioncable (= 8.0.1)
actionmailbox (= 8.0.1)
actionmailer (= 8.0.1)
actionpack (= 8.0.1)
actiontext (= 8.0.1)
actionview (= 8.0.1)
activejob (= 8.0.1)
activemodel (= 8.0.1)
activerecord (= 8.0.1)
activestorage (= 8.0.1)
activesupport (= 8.0.1)
bundler (>= 1.15.0)
railties (= 7.2.2)
railties (= 8.0.1)
rails-controller-testing (1.0.5)
actionpack (>= 5.0.1.rc1)
actionview (>= 5.0.1.rc1)
@ -632,15 +635,15 @@ GEM
activesupport (>= 5.0.0)
minitest
nokogiri (>= 1.6)
rails-html-sanitizer (1.6.0)
rails-html-sanitizer (1.6.2)
loofah (~> 2.21)
nokogiri (~> 1.14)
rails-i18n (7.0.10)
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
rails-i18n (8.0.1)
i18n (>= 0.7, < 2)
railties (>= 6.0.0, < 8)
railties (7.2.2)
actionpack (= 7.2.2)
activesupport (= 7.2.2)
railties (>= 8.0.0, < 9)
railties (8.0.1)
actionpack (= 8.0.1)
activesupport (= 8.0.1)
irb (~> 1.13)
rackup (>= 1.0.0)
rake (>= 12.2)
@ -654,7 +657,7 @@ GEM
link_header (~> 0.0, >= 0.0.8)
rdf-normalize (0.7.0)
rdf (~> 3.3)
rdoc (6.7.0)
rdoc (6.10.0)
psych (>= 4.0.0)
redcarpet (3.6.0)
redis (4.8.1)
@ -662,15 +665,15 @@ GEM
redis (>= 4)
redlock (1.3.2)
redis (>= 3.0.0, < 6.0)
regexp_parser (2.9.2)
reline (0.5.11)
regexp_parser (2.10.0)
reline (0.6.0)
io-console (~> 0.5)
request_store (1.6.0)
request_store (1.7.0)
rack (>= 1.4)
responders (3.1.1)
actionpack (>= 5.2)
railties (>= 5.2)
rexml (3.3.9)
rexml (3.4.0)
rotp (6.3.0)
rouge (4.5.1)
rpam2 (4.0.2)
@ -705,30 +708,30 @@ GEM
rspec-expectations (~> 3.0)
rspec-mocks (~> 3.0)
sidekiq (>= 5, < 8)
rspec-support (3.13.1)
rubocop (1.66.1)
rspec-support (3.13.2)
rubocop (1.69.2)
json (~> 2.3)
language_server-protocol (>= 3.17.0)
parallel (~> 1.10)
parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 2.4, < 3.0)
rubocop-ast (>= 1.32.2, < 2.0)
regexp_parser (>= 2.9.3, < 3.0)
rubocop-ast (>= 1.36.2, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.32.3)
unicode-display_width (>= 2.4.0, < 4.0)
rubocop-ast (1.37.0)
parser (>= 3.3.1.0)
rubocop-capybara (2.21.0)
rubocop (~> 1.41)
rubocop-performance (1.22.1)
rubocop-performance (1.23.1)
rubocop (>= 1.48.1, < 2.0)
rubocop-ast (>= 1.31.1, < 2.0)
rubocop-rails (2.27.0)
rubocop-rails (2.28.0)
activesupport (>= 4.2.0)
rack (>= 1.1)
rubocop (>= 1.52.0, < 2.0)
rubocop-ast (>= 1.31.1, < 2.0)
rubocop-rspec (3.2.0)
rubocop-rspec (3.3.0)
rubocop (~> 1.61)
rubocop-rspec_rails (2.30.0)
rubocop (~> 1.61)
@ -741,25 +744,25 @@ GEM
ruby-vips (2.2.2)
ffi (~> 1.12)
logger
rubyzip (2.3.2)
rufus-scheduler (3.9.1)
fugit (~> 1.1, >= 1.1.6)
rubyzip (2.4.1)
rufus-scheduler (3.9.2)
fugit (~> 1.1, >= 1.11.1)
safety_net_attestation (0.4.0)
jwt (~> 2.0)
sanitize (6.1.3)
sanitize (7.0.0)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
nokogiri (>= 1.16.8)
scenic (1.8.0)
activerecord (>= 4.0.0)
railties (>= 4.0.0)
securerandom (0.3.2)
securerandom (0.4.1)
selenium-webdriver (4.27.0)
base64 (~> 0.2)
logger (~> 1.4)
rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2, < 3.0)
websocket (~> 1.0)
semantic_range (3.0.0)
semantic_range (3.1.0)
shoulda-matchers (6.4.0)
activesupport (>= 5.2.0)
sidekiq (6.5.12)
@ -806,10 +809,10 @@ GEM
unicode-display_width (>= 1.1.1, < 3)
terrapin (1.0.1)
climate_control
test-prof (1.4.2)
test-prof (1.4.4)
thor (1.3.2)
tilt (2.4.0)
timeout (0.4.2)
tilt (2.5.0)
timeout (0.4.3)
tpm-key_attestation (0.12.1)
bindata (~> 2.4)
openssl (> 2.0)
@ -835,8 +838,8 @@ GEM
unf_ext
unf_ext (0.0.9.1)
unicode-display_width (2.6.0)
uri (0.13.1)
useragent (0.16.10)
uri (1.0.2)
useragent (0.16.11)
validate_email (0.1.6)
activemodel (>= 3.0)
mail (>= 2.2.5)
@ -865,7 +868,7 @@ GEM
rack-proxy (>= 0.6.1)
railties (>= 5.2)
semantic_range (>= 2.3.0)
webrick (1.9.0)
webrick (1.9.1)
websocket (1.2.11)
websocket-driver (0.7.6)
websocket-extensions (>= 0.1.0)
@ -888,7 +891,7 @@ DEPENDENCIES
binding_of_caller (~> 1.0)
blurhash (~> 0.1)
bootsnap (~> 1.18.0)
brakeman (~> 6.0)
brakeman (~> 7.0)
browser
bundler-audit (~> 0.9)
capybara (~> 3.39)
@ -945,7 +948,7 @@ DEPENDENCIES
memory_profiler
mime-types (~> 3.6.0)
mutex_m
net-http (~> 0.5.0)
net-http (~> 0.6.0)
net-ldap (~> 0.18)
nokogiri (~> 1.15)
oj (~> 3.14)
@ -957,16 +960,16 @@ DEPENDENCIES
opentelemetry-api (~> 1.4.0)
opentelemetry-exporter-otlp (~> 0.29.0)
opentelemetry-instrumentation-active_job (~> 0.7.1)
opentelemetry-instrumentation-active_model_serializers (~> 0.20.1)
opentelemetry-instrumentation-active_model_serializers (~> 0.21.0)
opentelemetry-instrumentation-concurrent_ruby (~> 0.21.2)
opentelemetry-instrumentation-excon (~> 0.22.0)
opentelemetry-instrumentation-faraday (~> 0.24.1)
opentelemetry-instrumentation-faraday (~> 0.25.0)
opentelemetry-instrumentation-http (~> 0.23.2)
opentelemetry-instrumentation-http_client (~> 0.22.3)
opentelemetry-instrumentation-net_http (~> 0.22.4)
opentelemetry-instrumentation-pg (~> 0.29.0)
opentelemetry-instrumentation-rack (~> 0.25.0)
opentelemetry-instrumentation-rails (~> 0.33.0)
opentelemetry-instrumentation-rails (~> 0.34.0)
opentelemetry-instrumentation-redis (~> 0.25.3)
opentelemetry-instrumentation-sidekiq (~> 0.25.2)
opentelemetry-sdk (~> 1.4)
@ -983,9 +986,9 @@ DEPENDENCIES
rack-attack (~> 6.6)
rack-cors (~> 2.0)
rack-test (~> 2.1)
rails (~> 7.2.0)
rails (~> 8.0)
rails-controller-testing (~> 1.0)
rails-i18n (~> 7.0)
rails-i18n (~> 8.0)
rdf-normalize (~> 0.5)
redcarpet (~> 3.6)
redis (~> 4.5)
@ -1004,7 +1007,7 @@ DEPENDENCIES
ruby-progressbar (~> 1.13)
ruby-vips (~> 2.2)
rubyzip (~> 2.3)
sanitize (~> 6.0)
sanitize (~> 7.0)
scenic (~> 1.7)
selenium-webdriver
shoulda-matchers
@ -1031,7 +1034,7 @@ DEPENDENCIES
xorcist (~> 1.1)
RUBY VERSION
ruby 3.3.6p108
ruby 3.4.1p0
BUNDLED WITH
2.5.23
2.6.2

147
README.md
View file

@ -1,24 +1,24 @@
<h1><picture>
<source media="(prefers-color-scheme: dark)" srcset="./lib/assets/wordmark.dark.png?raw=true">
<source media="(prefers-color-scheme: light)" srcset="./lib/assets/wordmark.light.png?raw=true">
<img alt="Mastodon" src="./lib/assets/wordmark.light.png?raw=true" height="34">
</picture></h1>
> [!NOTE]
> Want to learn more about Mastodon?
> Click below to find out more in a video.
[![GitHub release](https://img.shields.io/github/release/mastodon/mastodon.svg)][releases]
[![Ruby Testing](https://github.com/mastodon/mastodon/actions/workflows/test-ruby.yml/badge.svg)](https://github.com/mastodon/mastodon/actions/workflows/test-ruby.yml)
[![Crowdin](https://d322cqt584bo4o.cloudfront.net/mastodon/localized.svg)][crowdin]
<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" />
</a>
</p>
[releases]: https://github.com/mastodon/mastodon/releases
[crowdin]: https://crowdin.com/project/mastodon
<p align="center">
<a style="text-decoration:none" href="https://github.com/mastodon/mastodon/releases">
<img src="https://img.shields.io/github/release/mastodon/mastodon.svg" alt="Release" /></a>
<a style="text-decoration:none" href="https://github.com/mastodon/mastodon/actions/workflows/test-ruby.yml">
<img src="https://github.com/mastodon/mastodon/actions/workflows/test-ruby.yml/badge.svg" alt="Ruby Testing" /></a>
<a style="text-decoration:none" href="https://crowdin.com/project/mastodon">
<img src="https://d322cqt584bo4o.cloudfront.net/mastodon/localized.svg" alt="Crowdin" /></a>
</p>
Mastodon is a **free, open-source social network server** based on ActivityPub where users can follow friends and discover new ones. On Mastodon, users can publish anything they want: links, pictures, text, and video. All Mastodon servers are interoperable as a federated network (users on one server can seamlessly communicate with users from another one, including non-Mastodon software that implements ActivityPub!)
Click below to **learn more** in a video:
[![Screenshot](https://blog.joinmastodon.org/2018/06/why-activitypub-is-the-future/ezgif-2-60f1b00403.gif)][youtube_demo]
[youtube_demo]: https://www.youtube.com/watch?v=IPSbNdBmWKE
## Navigation
- [Project homepage 🐘](https://joinmastodon.org)
@ -37,25 +37,15 @@ Click below to **learn more** in a video:
<img src="/app/javascript/images/elephant_ui_working.svg?raw=true" align="right" width="30%" />
### No vendor lock-in: Fully interoperable with any conforming platform
**No vendor lock-in: Fully interoperable with any conforming platform** - It doesn't have to be Mastodon; whatever implements ActivityPub is part of the social network! [Learn more](https://blog.joinmastodon.org/2018/06/why-activitypub-is-the-future/)
It doesn't have to be Mastodon; whatever implements ActivityPub is part of the social network! [Learn more](https://blog.joinmastodon.org/2018/06/why-activitypub-is-the-future/)
**Real-time, chronological timeline updates** - updates of people you're following appear in real-time in the UI via WebSockets. There's a firehose view as well!
### Real-time, chronological timeline updates
**Media attachments like images and short videos** - upload and view images and WebM/MP4 videos attached to the updates. Videos with no audio track are treated like GIFs; normal videos loop continuously!
Updates of people you're following appear in real-time in the UI via WebSockets. There's a firehose view as well!
**Safety and moderation tools** - Mastodon includes private posts, locked accounts, phrase filtering, muting, blocking, and all sorts of other features, along with a reporting and moderation system. [Learn more](https://blog.joinmastodon.org/2018/07/cage-the-mastodon/)
### Media attachments like images and short videos
Upload and view images and WebM/MP4 videos attached to the updates. Videos with no audio track are treated like GIFs; normal videos loop continuously!
### Safety and moderation tools
Mastodon includes private posts, locked accounts, phrase filtering, muting, blocking, and all sorts of other features, along with a reporting and moderation system. [Learn more](https://blog.joinmastodon.org/2018/07/cage-the-mastodon/)
### OAuth2 and a straightforward REST API
Mastodon acts as an OAuth2 provider, so 3rd party apps can use the REST and Streaming APIs. This results in a rich app ecosystem with a lot of choices!
**OAuth2 and a straightforward REST API** - Mastodon acts as an OAuth2 provider, so 3rd party apps can use the REST and Streaming APIs. This results in a rich app ecosystem with a lot of choices!
## Deployment
@ -74,85 +64,40 @@ Mastodon acts as an OAuth2 provider, so 3rd party apps can use the REST and Stre
The repository includes deployment configurations for **Docker and docker-compose** as well as specific platforms like **Heroku**, and **Scalingo**. For Helm charts, reference the [mastodon/chart repository](https://github.com/mastodon/chart). The [**standalone** installation guide](https://docs.joinmastodon.org/admin/install/) is available in the documentation.
## Development
### Vagrant
A **Vagrant** configuration is included for development purposes. To use it, complete the following steps:
- Install Vagrant and Virtualbox
- Install the `vagrant-hostsupdater` plugin: `vagrant plugin install vagrant-hostsupdater`
- Run `vagrant up`
- Run `vagrant ssh -c "cd /vagrant && bin/dev"`
- Open `http://mastodon.local` in your browser
### macOS
To set up **macOS** for native development, complete the following steps:
- Install [Homebrew] and run `brew install postgresql@14 redis imagemagick
libidn nvm` to install the required project dependencies
- Use a Ruby version manager to activate the ruby in `.ruby-version` and run
`nvm use` to activate the node version from `.nvmrc`
- Run the `bin/setup` script, which will install the required ruby gems and node
packages and prepare the database for local development
- Finally, run the `bin/dev` script which will launch services via `overmind`
(if installed) or `foreman`
### Docker
For production hosting and deployment with **Docker**, use the `Dockerfile` and
`docker-compose.yml` in the project root directory.
For local development, install and launch [Docker], and run:
```shell
docker compose -f .devcontainer/compose.yaml up -d
docker compose -f .devcontainer/compose.yaml exec app bin/setup
docker compose -f .devcontainer/compose.yaml exec app bin/dev
```
### Dev Containers
Within IDEs that support the [Development Containers] specification, start the
"Mastodon on local machine" container from the editor. The necessary `docker
compose` commands to build and setup the container should run automatically. For
**Visual Studio Code** this requires installing the [Dev Container extension].
### GitHub Codespaces
[GitHub Codespaces] provides a web-based version of VS Code and a cloud hosted
development environment configured with the software needed for this project.
[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)][codespace]
- Click the button to create a new codespace, and confirm the options
- Wait for the environment to build (takes a few minutes)
- When the editor is ready, run `bin/dev` in the terminal
- Wait for an _Open in Browser_ prompt. This will open Mastodon
- On the _Ports_ tab "stream" setting change _Port visibility_ → _Public_
## Contributing
Mastodon is **free, open-source software** licensed under **AGPLv3**.
You can open issues for bugs you've found or features you think are missing. You can also submit pull requests to this repository or submit translations using Crowdin. To get started, take a look at [CONTRIBUTING.md](CONTRIBUTING.md). If your contributions are accepted into Mastodon, you can request to be paid through [our OpenCollective](https://opencollective.com/mastodon).
You can open issues for bugs you've found or features you think are missing. You
can also submit pull requests to this repository or translations via Crowdin. To
get started, look at the [CONTRIBUTING] and [DEVELOPMENT] guides. For changes
accepted into Mastodon, you can request to be paid through our [OpenCollective].
**IRC channel**: #mastodon on irc.libera.chat
**IRC channel**: #mastodon on [`irc.libera.chat`](https://libera.chat)
## License
Copyright (C) 2016-2024 Eugen Rochko & other Mastodon contributors (see [AUTHORS.md](AUTHORS.md))
Copyright (c) 2016-2024 Eugen Rochko (+ [`mastodon authors`](AUTHORS.md))
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
Licensed under GNU Affero General Public License as stated in the [LICENSE](LICENSE):
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
```
Copyright (c) 2016-2024 Eugen Rochko & other Mastodon contributors
You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
This program is free software: you can redistribute it and/or modify it under
the terms of the GNU Affero General Public License as published by the Free
Software Foundation, either version 3 of the License, or (at your option) any
later version.
[codespace]: https://codespaces.new/mastodon/mastodon?quickstart=1&devcontainer_path=.devcontainer%2Fcodespaces%2Fdevcontainer.json
[Dev Container extension]: https://containers.dev/supporting#dev-containers
[Development Containers]: https://containers.dev/supporting
[Docker]: https://docs.docker.com
[GitHub Codespaces]: https://docs.github.com/en/codespaces
[Homebrew]: https://brew.sh
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
details.
You should have received a copy of the GNU Affero General Public License along
with this program. If not, see https://www.gnu.org/licenses/
```
[CONTRIBUTING]: CONTRIBUTING.md
[DEVELOPMENT]: docs/DEVELOPMENT.md
[OpenCollective]: https://opencollective.com/mastodon

2
Vagrantfile vendored
View file

@ -174,7 +174,7 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
if config.vm.networks.any? { |type, options| type == :private_network }
config.vm.synced_folder ".", "/vagrant", type: "nfs", mount_options: ['rw', 'actimeo=1']
else
config.vm.synced_folder ".", "/vagrant"
config.vm.synced_folder ".", "/vagrant", type: "rsync", create: true, rsync__args: ["--verbose", "--archive", "--delete", "-z"]
end
# Otherwise, you can access the site at http://localhost:3000 and http://localhost:4000 , http://localhost:8080

View file

@ -8,6 +8,7 @@ module Admin
layout 'admin'
before_action :set_cache_headers
before_action :set_referrer_policy_header
after_action :verify_authorized
@ -17,6 +18,10 @@ module Admin
response.cache_control.replace(private: true, no_store: true)
end
def set_referrer_policy_header
response.headers['Referrer-Policy'] = 'same-origin'
end
def set_user
@user = Account.find(params[:account_id]).user || raise(ActiveRecord::RecordNotFound)
end

View file

@ -6,7 +6,7 @@ module Admin
def index
authorize :software_update, :index?
@software_updates = SoftwareUpdate.all.sort_by(&:gem_version)
@software_updates = SoftwareUpdate.by_version
end
private

View file

@ -0,0 +1,18 @@
# frozen_string_literal: true
class Admin::TermsOfService::DistributionsController < Admin::BaseController
before_action :set_terms_of_service
def create
authorize @terms_of_service, :distribute?
@terms_of_service.touch(:notification_sent_at)
Admin::DistributeTermsOfServiceNotificationWorker.perform_async(@terms_of_service.id)
redirect_to admin_terms_of_service_index_path
end
private
def set_terms_of_service
@terms_of_service = TermsOfService.find(params[:terms_of_service_id])
end
end

View file

@ -0,0 +1,36 @@
# frozen_string_literal: true
class Admin::TermsOfService::DraftsController < Admin::BaseController
before_action :set_terms_of_service
def show
authorize :terms_of_service, :create?
end
def update
authorize @terms_of_service, :update?
@terms_of_service.published_at = Time.now.utc if params[:action_type] == 'publish'
if @terms_of_service.update(resource_params)
log_action(:publish, @terms_of_service) if @terms_of_service.published?
redirect_to @terms_of_service.published? ? admin_terms_of_service_index_path : admin_terms_of_service_draft_path
else
render :show
end
end
private
def set_terms_of_service
@terms_of_service = TermsOfService.draft.first || TermsOfService.new(text: current_terms_of_service&.text)
end
def current_terms_of_service
TermsOfService.live.first
end
def resource_params
params.require(:terms_of_service).permit(:text, :changelog)
end
end

View file

@ -0,0 +1,37 @@
# frozen_string_literal: true
class Admin::TermsOfService::GeneratesController < Admin::BaseController
before_action :set_instance_presenter
def show
authorize :terms_of_service, :create?
@generator = TermsOfService::Generator.new(
domain: @instance_presenter.domain,
admin_email: @instance_presenter.contact.email
)
end
def create
authorize :terms_of_service, :create?
@generator = TermsOfService::Generator.new(resource_params)
if @generator.valid?
TermsOfService.create!(text: @generator.render)
redirect_to admin_terms_of_service_draft_path
else
render :show
end
end
private
def set_instance_presenter
@instance_presenter = InstancePresenter.new
end
def resource_params
params.require(:terms_of_service_generator).permit(*TermsOfService::Generator::VARIABLES)
end
end

View file

@ -0,0 +1,8 @@
# frozen_string_literal: true
class Admin::TermsOfService::HistoriesController < Admin::BaseController
def show
authorize :terms_of_service, :index?
@terms_of_service = TermsOfService.published.all
end
end

View file

@ -0,0 +1,16 @@
# frozen_string_literal: true
class Admin::TermsOfService::PreviewsController < Admin::BaseController
before_action :set_terms_of_service
def show
authorize @terms_of_service, :distribute?
@user_count = @terms_of_service.scope_for_notification.count
end
private
def set_terms_of_service
@terms_of_service = TermsOfService.find(params[:terms_of_service_id])
end
end

View file

@ -0,0 +1,17 @@
# frozen_string_literal: true
class Admin::TermsOfService::TestsController < Admin::BaseController
before_action :set_terms_of_service
def create
authorize @terms_of_service, :distribute?
UserMailer.terms_of_service_changed(current_user, @terms_of_service).deliver_later!
redirect_to admin_terms_of_service_preview_path(@terms_of_service)
end
private
def set_terms_of_service
@terms_of_service = TermsOfService.find(params[:terms_of_service_id])
end
end

View file

@ -0,0 +1,8 @@
# frozen_string_literal: true
class Admin::TermsOfServiceController < Admin::BaseController
def index
authorize :terms_of_service, :index?
@terms_of_service = TermsOfService.live.first
end
end

View file

@ -0,0 +1,16 @@
# frozen_string_literal: true
class Api::V1::Instances::TermsOfServicesController < Api::V1::Instances::BaseController
before_action :set_terms_of_service
def show
cache_even_if_authenticated!
render json: @terms_of_service, serializer: REST::PrivacyPolicySerializer
end
private
def set_terms_of_service
@terms_of_service = TermsOfService.live.first!
end
end

View file

@ -15,7 +15,7 @@ class Api::V1::Polls::VotesController < Api::BaseController
private
def set_poll
@poll = Poll.attached.find(params[:poll_id])
@poll = Poll.find(params[:poll_id])
authorize @poll.status, :show?
rescue Mastodon::NotPermittedError
not_found

View file

@ -15,7 +15,7 @@ class Api::V1::PollsController < Api::BaseController
private
def set_poll
@poll = Poll.attached.find(params[:id])
@poll = Poll.find(params[:id])
authorize @poll.status, :show?
rescue Mastodon::NotPermittedError
not_found

View file

@ -27,7 +27,9 @@ class Api::V1::Trends::TagsController < Api::BaseController
end
def tags_from_trends
Trends.tags.query.allowed
scope = Trends.tags.query.allowed.in_locale(content_locale)
scope = scope.filtered_for(current_account) if user_signed_in?
scope
end
def next_path

View file

@ -80,10 +80,31 @@ class Api::V2::NotificationsController < Api::BaseController
return [] if @notifications.empty?
MastodonOTELTracer.in_span('Api::V2::NotificationsController#load_grouped_notifications') do
NotificationGroup.from_notifications(@notifications, pagination_range: (@notifications.last.id)..(@notifications.first.id), grouped_types: params[:grouped_types])
pagination_range = (@notifications.last.id)..@notifications.first.id
# If the page is incomplete, we know we are on the last page
if incomplete_page?
if paginating_up?
pagination_range = @notifications.last.id...(params[:max_id]&.to_i)
else
range_start = params[:since_id]&.to_i
range_start += 1 unless range_start.nil?
pagination_range = range_start..(@notifications.first.id)
end
end
NotificationGroup.from_notifications(@notifications, pagination_range: pagination_range, grouped_types: params[:grouped_types])
end
end
def incomplete_page?
@notifications.size < limit_param(DEFAULT_NOTIFICATIONS_LIMIT)
end
def paginating_up?
params[:min_id].present?
end
def browserable_account_notifications
current_account.notifications.without_suspended.browserable(
types: Array(browserable_params[:types]),

View file

@ -70,7 +70,13 @@ class ApplicationController < ActionController::Base
end
def require_functional!
redirect_to edit_user_registration_path unless current_user.functional?
return if current_user.functional?
if current_user.confirmed?
redirect_to edit_user_registration_path
else
redirect_to auth_setup_path
end
end
def skip_csrf_meta_tags?

View file

@ -142,4 +142,12 @@ class Auth::RegistrationsController < Devise::RegistrationsController
def set_cache_headers
response.cache_control.replace(private: true, no_store: true)
end
def is_flashing_format? # rubocop:disable Naming/PredicateName
if params[:action] == 'create'
false # Disable flash messages for sign-up
else
super
end
end
end

View file

@ -25,7 +25,7 @@ module Localized
end
def available_locale_or_nil(locale_name)
locale_name.to_sym if locale_name.present? && I18n.available_locales.include?(locale_name.to_sym)
locale_name.to_sym if locale_name.respond_to?(:to_sym) && I18n.available_locales.include?(locale_name.to_sym)
end
def content_locale

View file

@ -7,6 +7,7 @@ module WebAppControllerConcern
vary_by 'Accept, Accept-Language, Cookie'
before_action :redirect_unauthenticated_to_permalinks!
before_action :set_referer_header
content_security_policy do |p|
policy = ContentSecurityPolicy.new
@ -41,4 +42,10 @@ module WebAppControllerConcern
end
end
end
protected
def set_referer_header
response.set_header('Referrer-Policy', Setting.allow_referrer_origin ? 'origin' : 'same-origin')
end
end

View file

@ -1,10 +1,8 @@
# frozen_string_literal: true
class CustomCssController < ActionController::Base # rubocop:disable Rails/ApplicationController
before_action :set_user_roles
def show
expires_in 3.minutes, public: true
expires_in 1.month, public: true
render content_type: 'text/css'
end
@ -14,8 +12,4 @@ class CustomCssController < ActionController::Base # rubocop:disable Rails/Appli
Setting.custom_css
end
helper_method :custom_css_styles
def set_user_roles
@user_roles = UserRole.providing_styles
end
end

View file

@ -0,0 +1,11 @@
# frozen_string_literal: true
class TermsOfServiceController < ApplicationController
include WebAppControllerConcern
skip_before_action :require_functional!
def show
expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless user_signed_in?
end
end

View file

@ -238,6 +238,14 @@ module ApplicationHelper
I18n.t 'user_mailer.welcome.hashtags_recent_count', people: number_with_delimiter(people), count: people
end
def app_store_url_ios
'https://apps.apple.com/app/mastodon-for-iphone-and-ipad/id1571998974'
end
def app_store_url_android
'https://play.google.com/store/apps/details?id=org.joinmastodon.android'
end
private
def storage_host_var

View file

@ -64,6 +64,10 @@ module FormattingHelper
end
end
def markdown(text)
Redcarpet::Markdown.new(Redcarpet::Render::HTML, escape_html: true, no_images: true).render(text).html_safe # rubocop:disable Rails/OutputSafety
end
private
def wrapped_status_content_format(status)

View file

@ -23,8 +23,31 @@ module ThemeHelper
end
end
def custom_stylesheet
if active_custom_stylesheet.present?
stylesheet_link_tag(
custom_css_path(active_custom_stylesheet),
host: root_url,
media: :all,
skip_pipeline: true
)
end
end
private
def active_custom_stylesheet
if cached_custom_css_digest.present?
[:custom, cached_custom_css_digest.to_s.first(8)]
.compact_blank
.join('-')
end
end
def cached_custom_css_digest
Rails.cache.read(:setting_digest_custom_css)
end
def theme_color_for(theme)
theme == 'mastodon-light' ? Themes::THEME_COLORS[:light] : Themes::THEME_COLORS[:dark]
end

View file

@ -60,6 +60,10 @@ window.addEventListener('message', (e) => {
const data = e.data;
// Only set overflow to `hidden` once we got the expected `message` so the post can still be scrolled if
// embedded without parent Javascript support
document.body.style.overflow = 'hidden';
// We use a timeout to allow for the React page to render before calculating the height
afterInitialRender(() => {
window.parent.postMessage(

View file

@ -119,7 +119,11 @@ function loaded() {
formattedContent = dateFormat.format(datetime);
}
content.title = formattedContent;
const timeGiven = content.dateTime.includes('T');
content.title = timeGiven
? dateTimeFormat.format(datetime)
: dateFormat.format(datetime);
content.textContent = formattedContent;
});

View file

@ -2,6 +2,8 @@ import { useCallback } from 'react';
import { useHistory } from 'react-router-dom';
import { isFulfilled, isRejected } from '@reduxjs/toolkit';
import { openURL } from 'mastodon/actions/search';
import { useAppDispatch } from 'mastodon/store';
@ -28,12 +30,22 @@ export const useLinks = () => {
);
const handleMentionClick = useCallback(
(element: HTMLAnchorElement) => {
dispatch(
openURL(element.href, history, () => {
async (element: HTMLAnchorElement) => {
const result = await dispatch(openURL({ url: element.href }));
if (isFulfilled(result)) {
if (result.payload.accounts[0]) {
history.push(`/@${result.payload.accounts[0].acct}`);
} else if (result.payload.statuses[0]) {
history.push(
`/@${result.payload.statuses[0].account.acct}/${result.payload.statuses[0].id}`,
);
} else {
window.location.href = element.href;
}),
);
}
} else if (isRejected(result)) {
window.location.href = element.href;
}
},
[dispatch, history],
);
@ -48,7 +60,7 @@ export const useLinks = () => {
if (isMentionClick(target)) {
e.preventDefault();
handleMentionClick(target);
void handleMentionClick(target);
} else if (isHashtagClick(target)) {
e.preventDefault();
handleHashtagClick(target);

View file

@ -1,10 +1,12 @@
import { createPollFromServerJSON } from 'mastodon/models/poll';
import { importAccounts } from '../accounts_typed';
import { normalizeStatus, normalizePoll } from './normalizer';
import { normalizeStatus } from './normalizer';
import { importPolls } from './polls';
export const STATUS_IMPORT = 'STATUS_IMPORT';
export const STATUSES_IMPORT = 'STATUSES_IMPORT';
export const POLLS_IMPORT = 'POLLS_IMPORT';
export const FILTERS_IMPORT = 'FILTERS_IMPORT';
function pushUnique(array, object) {
@ -25,10 +27,6 @@ export function importFilters(filters) {
return { type: FILTERS_IMPORT, filters };
}
export function importPolls(polls) {
return { type: POLLS_IMPORT, polls };
}
export function importFetchedAccount(account) {
return importFetchedAccounts([account]);
}
@ -73,7 +71,7 @@ export function importFetchedStatuses(statuses) {
}
if (status.poll?.id) {
pushUnique(polls, normalizePoll(status.poll, getState().getIn(['polls', status.poll.id])));
pushUnique(polls, createPollFromServerJSON(status.poll, getState().polls.get(status.poll.id)));
}
if (status.card) {
@ -83,15 +81,9 @@ export function importFetchedStatuses(statuses) {
statuses.forEach(processStatus);
dispatch(importPolls(polls));
dispatch(importPolls({ polls }));
dispatch(importFetchedAccounts(accounts));
dispatch(importStatuses(normalStatuses));
dispatch(importFilters(filters));
};
}
export function importFetchedPoll(poll) {
return (dispatch, getState) => {
dispatch(importPolls([normalizePoll(poll, getState().getIn(['polls', poll.id]))]));
};
}

View file

@ -1,15 +1,12 @@
import escapeTextContentForBrowser from 'escape-html';
import { makeEmojiMap } from 'mastodon/models/custom_emoji';
import emojify from '../../features/emoji/emoji';
import { expandSpoilers } from '../../initial_state';
const domParser = new DOMParser();
const makeEmojiMap = emojis => emojis.reduce((obj, emoji) => {
obj[`:${emoji.shortcode}:`] = emoji;
return obj;
}, {});
export function searchTextFromRawStatus (status) {
const spoilerText = status.spoiler_text || '';
const searchContent = ([spoilerText, status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).concat(status.media_attachments.map(att => att.description)).join('\n\n').replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n');
@ -112,38 +109,6 @@ export function normalizeStatusTranslation(translation, status) {
return normalTranslation;
}
export function normalizePoll(poll, normalOldPoll) {
const normalPoll = { ...poll };
const emojiMap = makeEmojiMap(poll.emojis);
normalPoll.options = poll.options.map((option, index) => {
const normalOption = {
...option,
voted: poll.own_votes && poll.own_votes.includes(index),
titleHtml: emojify(escapeTextContentForBrowser(option.title), emojiMap),
};
if (normalOldPoll && normalOldPoll.getIn(['options', index, 'title']) === option.title) {
normalOption.translation = normalOldPoll.getIn(['options', index, 'translation']);
}
return normalOption;
});
return normalPoll;
}
export function normalizePollOptionTranslation(translation, poll) {
const emojiMap = makeEmojiMap(poll.get('emojis').toJS());
const normalTranslation = {
...translation,
titleHtml: emojify(escapeTextContentForBrowser(translation.title), emojiMap),
};
return normalTranslation;
}
export function normalizeAnnouncement(announcement) {
const normalAnnouncement = { ...announcement };
const emojiMap = makeEmojiMap(normalAnnouncement.emojis);

View file

@ -0,0 +1,7 @@
import { createAction } from '@reduxjs/toolkit';
import type { Poll } from 'mastodon/models/poll';
export const importPolls = createAction<{ polls: Poll[] }>(
'poll/importMultiple',
);

View file

@ -155,7 +155,7 @@ export const processNewNotificationForGroups = createAppAsyncThunk(
const showInColumn =
activeFilter === 'all'
? notificationShows[notification.type]
? notificationShows[notification.type] !== false
: activeFilter === notification.type;
if (!showInColumn) return;

View file

@ -1,69 +1,31 @@
import { IntlMessageFormat } from 'intl-messageformat';
import { defineMessages } from 'react-intl';
import { List as ImmutableList } from 'immutable';
import { compareId } from 'mastodon/compare_id';
import { usePendingItems as preferPendingItems } from 'mastodon/initial_state';
import api, { getLinks } from '../api';
import { unescapeHTML } from '../utils/html';
import { requestNotificationPermission } from '../utils/notifications';
import { fetchFollowRequests } from './accounts';
import {
importFetchedAccount,
importFetchedAccounts,
importFetchedStatus,
importFetchedStatuses,
} from './importer';
import { submitMarkers } from './markers';
import { notificationsUpdate } from "./notifications_typed";
import { register as registerPushNotifications } from './push_notifications';
import { saveSettings } from './settings';
export * from "./notifications_typed";
export const NOTIFICATIONS_UPDATE_NOOP = 'NOTIFICATIONS_UPDATE_NOOP';
export const NOTIFICATIONS_EXPAND_REQUEST = 'NOTIFICATIONS_EXPAND_REQUEST';
export const NOTIFICATIONS_EXPAND_SUCCESS = 'NOTIFICATIONS_EXPAND_SUCCESS';
export const NOTIFICATIONS_EXPAND_FAIL = 'NOTIFICATIONS_EXPAND_FAIL';
export const NOTIFICATIONS_FILTER_SET = 'NOTIFICATIONS_FILTER_SET';
export const NOTIFICATIONS_SCROLL_TOP = 'NOTIFICATIONS_SCROLL_TOP';
export const NOTIFICATIONS_LOAD_PENDING = 'NOTIFICATIONS_LOAD_PENDING';
export const NOTIFICATIONS_MOUNT = 'NOTIFICATIONS_MOUNT';
export const NOTIFICATIONS_UNMOUNT = 'NOTIFICATIONS_UNMOUNT';
export const NOTIFICATIONS_MARK_AS_READ = 'NOTIFICATIONS_MARK_AS_READ';
export const NOTIFICATIONS_SET_BROWSER_SUPPORT = 'NOTIFICATIONS_SET_BROWSER_SUPPORT';
export const NOTIFICATIONS_SET_BROWSER_PERMISSION = 'NOTIFICATIONS_SET_BROWSER_PERMISSION';
export const NOTIFICATION_REQUESTS_ACCEPT_REQUEST = 'NOTIFICATION_REQUESTS_ACCEPT_REQUEST';
export const NOTIFICATION_REQUESTS_ACCEPT_SUCCESS = 'NOTIFICATION_REQUESTS_ACCEPT_SUCCESS';
export const NOTIFICATION_REQUESTS_ACCEPT_FAIL = 'NOTIFICATION_REQUESTS_ACCEPT_FAIL';
export const NOTIFICATION_REQUESTS_DISMISS_REQUEST = 'NOTIFICATION_REQUESTS_DISMISS_REQUEST';
export const NOTIFICATION_REQUESTS_DISMISS_SUCCESS = 'NOTIFICATION_REQUESTS_DISMISS_SUCCESS';
export const NOTIFICATION_REQUESTS_DISMISS_FAIL = 'NOTIFICATION_REQUESTS_DISMISS_FAIL';
defineMessages({
mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' },
group: { id: 'notifications.group', defaultMessage: '{count} notifications' },
});
export const loadPending = () => ({
type: NOTIFICATIONS_LOAD_PENDING,
});
export function updateNotifications(notification, intlMessages, intlLocale) {
return (dispatch, getState) => {
const activeFilter = getState().getIn(['settings', 'notifications', 'quickFilter', 'active']);
const showInColumn = activeFilter === 'all' ? getState().getIn(['settings', 'notifications', 'shows', notification.type], true) : activeFilter === notification.type;
const showAlert = getState().getIn(['settings', 'notifications', 'alerts', notification.type], true);
const playSound = getState().getIn(['settings', 'notifications', 'sounds', notification.type], true);
@ -85,25 +47,9 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
dispatch(submitMarkers());
if (showInColumn) {
dispatch(importFetchedAccount(notification.account));
if (notification.status) {
dispatch(importFetchedStatus(notification.status));
}
if (notification.report) {
dispatch(importFetchedAccount(notification.report.target_account));
}
dispatch(notificationsUpdate({ notification, preferPendingItems, playSound: playSound && !filtered}));
} else if (playSound && !filtered) {
dispatch({
type: NOTIFICATIONS_UPDATE_NOOP,
meta: { sound: 'boop' },
});
}
// `notificationsUpdate` is still used in `user_lists` and `relationships` reducers
dispatch(importFetchedAccount(notification.account));
dispatch(notificationsUpdate({ notification, playSound: playSound && !filtered}));
// Desktop notifications
if (typeof window.Notification !== 'undefined' && showAlert && !filtered) {
@ -120,141 +66,8 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
};
}
const excludeTypesFromSettings = state => state.getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS();
const excludeTypesFromFilter = filter => {
const allTypes = ImmutableList([
'follow',
'follow_request',
'favourite',
'reblog',
'mention',
'poll',
'status',
'update',
'admin.sign_up',
'admin.report',
]);
return allTypes.filterNot(item => item === filter).toJS();
};
const noOp = () => {};
let expandNotificationsController = new AbortController();
export function expandNotifications({ maxId = undefined, forceLoad = false }) {
return async (dispatch, getState) => {
const activeFilter = getState().getIn(['settings', 'notifications', 'quickFilter', 'active']);
const notifications = getState().get('notifications');
const isLoadingMore = !!maxId;
if (notifications.get('isLoading')) {
if (forceLoad) {
expandNotificationsController.abort();
expandNotificationsController = new AbortController();
} else {
return;
}
}
const params = {
max_id: maxId,
exclude_types: activeFilter === 'all'
? excludeTypesFromSettings(getState())
: excludeTypesFromFilter(activeFilter),
};
if (!params.max_id && (notifications.get('items', ImmutableList()).size + notifications.get('pendingItems', ImmutableList()).size) > 0) {
const a = notifications.getIn(['pendingItems', 0, 'id']);
const b = notifications.getIn(['items', 0, 'id']);
if (a && b && compareId(a, b) > 0) {
params.since_id = a;
} else {
params.since_id = b || a;
}
}
const isLoadingRecent = !!params.since_id;
dispatch(expandNotificationsRequest(isLoadingMore));
try {
const response = await api().get('/api/v1/notifications', { params, signal: expandNotificationsController.signal });
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedAccounts(response.data.map(item => item.account)));
dispatch(importFetchedStatuses(response.data.map(item => item.status).filter(status => !!status)));
dispatch(importFetchedAccounts(response.data.filter(item => item.report).map(item => item.report.target_account)));
dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null, isLoadingMore, isLoadingRecent, isLoadingRecent && preferPendingItems));
dispatch(submitMarkers());
} catch(error) {
dispatch(expandNotificationsFail(error, isLoadingMore));
}
};
}
export function expandNotificationsRequest(isLoadingMore) {
return {
type: NOTIFICATIONS_EXPAND_REQUEST,
skipLoading: !isLoadingMore,
};
}
export function expandNotificationsSuccess(notifications, next, isLoadingMore, isLoadingRecent, usePendingItems) {
return {
type: NOTIFICATIONS_EXPAND_SUCCESS,
notifications,
next,
isLoadingRecent: isLoadingRecent,
usePendingItems,
skipLoading: !isLoadingMore,
};
}
export function expandNotificationsFail(error, isLoadingMore) {
return {
type: NOTIFICATIONS_EXPAND_FAIL,
error,
skipLoading: !isLoadingMore,
skipAlert: !isLoadingMore || error.name === 'AbortError',
};
}
export function scrollTopNotifications(top) {
return {
type: NOTIFICATIONS_SCROLL_TOP,
top,
};
}
export function setFilter (filterType) {
return dispatch => {
dispatch({
type: NOTIFICATIONS_FILTER_SET,
path: ['notifications', 'quickFilter', 'active'],
value: filterType,
});
dispatch(expandNotifications({ forceLoad: true }));
dispatch(saveSettings());
};
}
export const mountNotifications = () => ({
type: NOTIFICATIONS_MOUNT,
});
export const unmountNotifications = () => ({
type: NOTIFICATIONS_UNMOUNT,
});
export const markNotificationsAsRead = () => ({
type: NOTIFICATIONS_MARK_AS_READ,
});
// Browser support
export function setupBrowserNotifications() {
return dispatch => {

View file

@ -1,10 +0,0 @@
import { createAppAsyncThunk } from 'mastodon/store';
import { fetchNotifications } from './notification_groups';
export const initializeNotifications = createAppAsyncThunk(
'notifications/initialize',
(_, { dispatch }) => {
void dispatch(fetchNotifications());
},
);

View file

@ -9,7 +9,6 @@ export const notificationsUpdate = createAction(
...args
}: {
notification: ApiNotificationJSON;
usePendingItems: boolean;
playSound: boolean;
}) => ({
payload: args,

View file

@ -1,61 +0,0 @@
import api from '../api';
import { importFetchedPoll } from './importer';
export const POLL_VOTE_REQUEST = 'POLL_VOTE_REQUEST';
export const POLL_VOTE_SUCCESS = 'POLL_VOTE_SUCCESS';
export const POLL_VOTE_FAIL = 'POLL_VOTE_FAIL';
export const POLL_FETCH_REQUEST = 'POLL_FETCH_REQUEST';
export const POLL_FETCH_SUCCESS = 'POLL_FETCH_SUCCESS';
export const POLL_FETCH_FAIL = 'POLL_FETCH_FAIL';
export const vote = (pollId, choices) => (dispatch) => {
dispatch(voteRequest());
api().post(`/api/v1/polls/${pollId}/votes`, { choices })
.then(({ data }) => {
dispatch(importFetchedPoll(data));
dispatch(voteSuccess(data));
})
.catch(err => dispatch(voteFail(err)));
};
export const fetchPoll = pollId => (dispatch) => {
dispatch(fetchPollRequest());
api().get(`/api/v1/polls/${pollId}`)
.then(({ data }) => {
dispatch(importFetchedPoll(data));
dispatch(fetchPollSuccess(data));
})
.catch(err => dispatch(fetchPollFail(err)));
};
export const voteRequest = () => ({
type: POLL_VOTE_REQUEST,
});
export const voteSuccess = poll => ({
type: POLL_VOTE_SUCCESS,
poll,
});
export const voteFail = error => ({
type: POLL_VOTE_FAIL,
error,
});
export const fetchPollRequest = () => ({
type: POLL_FETCH_REQUEST,
});
export const fetchPollSuccess = poll => ({
type: POLL_FETCH_SUCCESS,
poll,
});
export const fetchPollFail = error => ({
type: POLL_FETCH_FAIL,
error,
});

View file

@ -0,0 +1,40 @@
import { apiGetPoll, apiPollVote } from 'mastodon/api/polls';
import type { ApiPollJSON } from 'mastodon/api_types/polls';
import { createPollFromServerJSON } from 'mastodon/models/poll';
import {
createAppAsyncThunk,
createDataLoadingThunk,
} from 'mastodon/store/typed_functions';
import { importPolls } from './importer/polls';
export const importFetchedPoll = createAppAsyncThunk(
'poll/importFetched',
(args: { poll: ApiPollJSON }, { dispatch, getState }) => {
const { poll } = args;
dispatch(
importPolls({
polls: [createPollFromServerJSON(poll, getState().polls.get(poll.id))],
}),
);
},
);
export const vote = createDataLoadingThunk(
'poll/vote',
({ pollId, choices }: { pollId: string; choices: string[] }) =>
apiPollVote(pollId, choices),
async (poll, { dispatch, discardLoadData }) => {
await dispatch(importFetchedPoll({ poll }));
return discardLoadData;
},
);
export const fetchPoll = createDataLoadingThunk(
'poll/fetch',
({ pollId }: { pollId: string }) => apiGetPoll(pollId),
async (poll, { dispatch }) => {
await dispatch(importFetchedPoll({ poll }));
},
);

View file

@ -1,215 +0,0 @@
import { fromJS } from 'immutable';
import { searchHistory } from 'mastodon/settings';
import api from '../api';
import { fetchRelationships } from './accounts';
import { importFetchedAccounts, importFetchedStatuses } from './importer';
export const SEARCH_CHANGE = 'SEARCH_CHANGE';
export const SEARCH_CLEAR = 'SEARCH_CLEAR';
export const SEARCH_SHOW = 'SEARCH_SHOW';
export const SEARCH_FETCH_REQUEST = 'SEARCH_FETCH_REQUEST';
export const SEARCH_FETCH_SUCCESS = 'SEARCH_FETCH_SUCCESS';
export const SEARCH_FETCH_FAIL = 'SEARCH_FETCH_FAIL';
export const SEARCH_EXPAND_REQUEST = 'SEARCH_EXPAND_REQUEST';
export const SEARCH_EXPAND_SUCCESS = 'SEARCH_EXPAND_SUCCESS';
export const SEARCH_EXPAND_FAIL = 'SEARCH_EXPAND_FAIL';
export const SEARCH_HISTORY_UPDATE = 'SEARCH_HISTORY_UPDATE';
export function changeSearch(value) {
return {
type: SEARCH_CHANGE,
value,
};
}
export function clearSearch() {
return {
type: SEARCH_CLEAR,
};
}
export function submitSearch(type) {
return (dispatch, getState) => {
const value = getState().getIn(['search', 'value']);
const signedIn = !!getState().getIn(['meta', 'me']);
if (value.length === 0) {
dispatch(fetchSearchSuccess({ accounts: [], statuses: [], hashtags: [] }, '', type));
return;
}
dispatch(fetchSearchRequest(type));
api().get('/api/v2/search', {
params: {
q: value,
resolve: signedIn,
limit: 11,
type,
},
}).then(response => {
if (response.data.accounts) {
dispatch(importFetchedAccounts(response.data.accounts));
}
if (response.data.statuses) {
dispatch(importFetchedStatuses(response.data.statuses));
}
dispatch(fetchSearchSuccess(response.data, value, type));
dispatch(fetchRelationships(response.data.accounts.map(item => item.id)));
}).catch(error => {
dispatch(fetchSearchFail(error));
});
};
}
export function fetchSearchRequest(searchType) {
return {
type: SEARCH_FETCH_REQUEST,
searchType,
};
}
export function fetchSearchSuccess(results, searchTerm, searchType) {
return {
type: SEARCH_FETCH_SUCCESS,
results,
searchType,
searchTerm,
};
}
export function fetchSearchFail(error) {
return {
type: SEARCH_FETCH_FAIL,
error,
};
}
export const expandSearch = type => (dispatch, getState) => {
const value = getState().getIn(['search', 'value']);
const offset = getState().getIn(['search', 'results', type]).size - 1;
dispatch(expandSearchRequest(type));
api().get('/api/v2/search', {
params: {
q: value,
type,
offset,
limit: 11,
},
}).then(({ data }) => {
if (data.accounts) {
dispatch(importFetchedAccounts(data.accounts));
}
if (data.statuses) {
dispatch(importFetchedStatuses(data.statuses));
}
dispatch(expandSearchSuccess(data, value, type));
dispatch(fetchRelationships(data.accounts.map(item => item.id)));
}).catch(error => {
dispatch(expandSearchFail(error));
});
};
export const expandSearchRequest = (searchType) => ({
type: SEARCH_EXPAND_REQUEST,
searchType,
});
export const expandSearchSuccess = (results, searchTerm, searchType) => ({
type: SEARCH_EXPAND_SUCCESS,
results,
searchTerm,
searchType,
});
export const expandSearchFail = error => ({
type: SEARCH_EXPAND_FAIL,
error,
});
export const showSearch = () => ({
type: SEARCH_SHOW,
});
export const openURL = (value, history, onFailure) => (dispatch, getState) => {
const signedIn = !!getState().getIn(['meta', 'me']);
if (!signedIn) {
if (onFailure) {
onFailure();
}
return;
}
dispatch(fetchSearchRequest());
api().get('/api/v2/search', { params: { q: value, resolve: true } }).then(response => {
if (response.data.accounts?.length > 0) {
dispatch(importFetchedAccounts(response.data.accounts));
history.push(`/@${response.data.accounts[0].acct}`);
} else if (response.data.statuses?.length > 0) {
dispatch(importFetchedStatuses(response.data.statuses));
history.push(`/@${response.data.statuses[0].account.acct}/${response.data.statuses[0].id}`);
} else if (onFailure) {
onFailure();
}
dispatch(fetchSearchSuccess(response.data, value));
}).catch(err => {
dispatch(fetchSearchFail(err));
if (onFailure) {
onFailure();
}
});
};
export const clickSearchResult = (q, type) => (dispatch, getState) => {
const previous = getState().getIn(['search', 'recent']);
if (previous.some(x => x.get('q') === q && x.get('type') === type)) {
return;
}
const me = getState().getIn(['meta', 'me']);
const current = previous.add(fromJS({ type, q })).takeLast(4);
searchHistory.set(me, current.toJS());
dispatch(updateSearchHistory(current));
};
export const forgetSearchResult = q => (dispatch, getState) => {
const previous = getState().getIn(['search', 'recent']);
const me = getState().getIn(['meta', 'me']);
const current = previous.filterNot(result => result.get('q') === q);
searchHistory.set(me, current.toJS());
dispatch(updateSearchHistory(current));
};
export const updateSearchHistory = recent => ({
type: SEARCH_HISTORY_UPDATE,
recent,
});
export const hydrateSearch = () => (dispatch, getState) => {
const me = getState().getIn(['meta', 'me']);
const history = searchHistory.get(me);
if (history !== null) {
dispatch(updateSearchHistory(history));
}
};

View file

@ -0,0 +1,148 @@
import { createAction } from '@reduxjs/toolkit';
import { apiGetSearch } from 'mastodon/api/search';
import type { ApiSearchType } from 'mastodon/api_types/search';
import type {
RecentSearch,
SearchType as RecentSearchType,
} from 'mastodon/models/search';
import { searchHistory } from 'mastodon/settings';
import {
createDataLoadingThunk,
createAppAsyncThunk,
} from 'mastodon/store/typed_functions';
import { fetchRelationships } from './accounts';
import { importFetchedAccounts, importFetchedStatuses } from './importer';
export const SEARCH_HISTORY_UPDATE = 'SEARCH_HISTORY_UPDATE';
export const submitSearch = createDataLoadingThunk(
'search/submit',
async ({ q, type }: { q: string; type?: ApiSearchType }, { getState }) => {
const signedIn = !!getState().meta.get('me');
return apiGetSearch({
q,
type,
resolve: signedIn,
limit: 11,
});
},
(data, { dispatch }) => {
if (data.accounts.length > 0) {
dispatch(importFetchedAccounts(data.accounts));
dispatch(fetchRelationships(data.accounts.map((account) => account.id)));
}
if (data.statuses.length > 0) {
dispatch(importFetchedStatuses(data.statuses));
}
return data;
},
{
useLoadingBar: false,
},
);
export const expandSearch = createDataLoadingThunk(
'search/expand',
async ({ type }: { type: ApiSearchType }, { getState }) => {
const q = getState().search.q;
const results = getState().search.results;
const offset = results?.[type].length;
return apiGetSearch({
q,
type,
limit: 10,
offset,
});
},
(data, { dispatch }) => {
if (data.accounts.length > 0) {
dispatch(importFetchedAccounts(data.accounts));
dispatch(fetchRelationships(data.accounts.map((account) => account.id)));
}
if (data.statuses.length > 0) {
dispatch(importFetchedStatuses(data.statuses));
}
return data;
},
{
useLoadingBar: true,
},
);
export const openURL = createDataLoadingThunk(
'search/openURL',
({ url }: { url: string }) =>
apiGetSearch({
q: url,
resolve: true,
limit: 1,
}),
(data, { dispatch }) => {
if (data.accounts.length > 0) {
dispatch(importFetchedAccounts(data.accounts));
} else if (data.statuses.length > 0) {
dispatch(importFetchedStatuses(data.statuses));
}
return data;
},
{
useLoadingBar: true,
},
);
export const clickSearchResult = createAppAsyncThunk(
'search/clickResult',
(
{ q, type }: { q: string; type?: RecentSearchType },
{ dispatch, getState },
) => {
const previous = getState().search.recent;
if (previous.some((x) => x.q === q && x.type === type)) {
return;
}
const me = getState().meta.get('me') as string;
const current = [{ type, q }, ...previous].slice(0, 4);
searchHistory.set(me, current);
dispatch(updateSearchHistory(current));
},
);
export const forgetSearchResult = createAppAsyncThunk(
'search/forgetResult',
(q: string, { dispatch, getState }) => {
const previous = getState().search.recent;
const me = getState().meta.get('me') as string;
const current = previous.filter((result) => result.q !== q);
searchHistory.set(me, current);
dispatch(updateSearchHistory(current));
},
);
export const updateSearchHistory = createAction<RecentSearch[]>(
'search/updateHistory',
);
export const hydrateSearch = createAppAsyncThunk(
'search/hydrate',
(_args, { dispatch, getState }) => {
const me = getState().meta.get('me') as string;
const history = searchHistory.get(me) as RecentSearch[] | null;
if (history !== null) {
dispatch(updateSearchHistory(history));
}
},
);

View file

@ -1,4 +1,4 @@
import { Iterable, fromJS } from 'immutable';
import { fromJS, isIndexed } from 'immutable';
import { hydrateCompose } from './compose';
import { importFetchedAccounts } from './importer';
@ -9,8 +9,7 @@ export const STORE_HYDRATE_LAZY = 'STORE_HYDRATE_LAZY';
const convertState = rawState =>
fromJS(rawState, (k, v) =>
Iterable.isIndexed(v) ? v.toList() : v.toMap());
isIndexed(v) ? v.toList() : v.toMap());
export function hydrateStore(rawState) {
return dispatch => {

View file

@ -11,7 +11,7 @@ import {
} from './announcements';
import { updateConversations } from './conversations';
import { processNewNotificationForGroups, refreshStaleNotificationGroups, pollRecentNotifications as pollRecentGroupNotifications } from './notification_groups';
import { updateNotifications, expandNotifications } from './notifications';
import { updateNotifications } from './notifications';
import { updateStatus } from './statuses';
import {
updateTimeline,
@ -107,9 +107,6 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
break;
}
case 'notifications_merged': {
const state = getState();
if (state.notifications.top || !state.notifications.mounted)
dispatch(expandNotifications({ forceLoad: true, maxId: undefined }));
dispatch(refreshStaleNotificationGroups());
break;
}

View file

@ -1,9 +1,5 @@
import api, { getLinks } from '../api';
export const HASHTAG_FETCH_REQUEST = 'HASHTAG_FETCH_REQUEST';
export const HASHTAG_FETCH_SUCCESS = 'HASHTAG_FETCH_SUCCESS';
export const HASHTAG_FETCH_FAIL = 'HASHTAG_FETCH_FAIL';
export const FOLLOWED_HASHTAGS_FETCH_REQUEST = 'FOLLOWED_HASHTAGS_FETCH_REQUEST';
export const FOLLOWED_HASHTAGS_FETCH_SUCCESS = 'FOLLOWED_HASHTAGS_FETCH_SUCCESS';
export const FOLLOWED_HASHTAGS_FETCH_FAIL = 'FOLLOWED_HASHTAGS_FETCH_FAIL';
@ -12,39 +8,6 @@ export const FOLLOWED_HASHTAGS_EXPAND_REQUEST = 'FOLLOWED_HASHTAGS_EXPAND_REQUES
export const FOLLOWED_HASHTAGS_EXPAND_SUCCESS = 'FOLLOWED_HASHTAGS_EXPAND_SUCCESS';
export const FOLLOWED_HASHTAGS_EXPAND_FAIL = 'FOLLOWED_HASHTAGS_EXPAND_FAIL';
export const HASHTAG_FOLLOW_REQUEST = 'HASHTAG_FOLLOW_REQUEST';
export const HASHTAG_FOLLOW_SUCCESS = 'HASHTAG_FOLLOW_SUCCESS';
export const HASHTAG_FOLLOW_FAIL = 'HASHTAG_FOLLOW_FAIL';
export const HASHTAG_UNFOLLOW_REQUEST = 'HASHTAG_UNFOLLOW_REQUEST';
export const HASHTAG_UNFOLLOW_SUCCESS = 'HASHTAG_UNFOLLOW_SUCCESS';
export const HASHTAG_UNFOLLOW_FAIL = 'HASHTAG_UNFOLLOW_FAIL';
export const fetchHashtag = name => (dispatch) => {
dispatch(fetchHashtagRequest());
api().get(`/api/v1/tags/${name}`).then(({ data }) => {
dispatch(fetchHashtagSuccess(name, data));
}).catch(err => {
dispatch(fetchHashtagFail(err));
});
};
export const fetchHashtagRequest = () => ({
type: HASHTAG_FETCH_REQUEST,
});
export const fetchHashtagSuccess = (name, tag) => ({
type: HASHTAG_FETCH_SUCCESS,
name,
tag,
});
export const fetchHashtagFail = error => ({
type: HASHTAG_FETCH_FAIL,
error,
});
export const fetchFollowedHashtags = () => (dispatch) => {
dispatch(fetchFollowedHashtagsRequest());
@ -116,57 +79,3 @@ export function expandFollowedHashtagsFail(error) {
error,
};
}
export const followHashtag = name => (dispatch) => {
dispatch(followHashtagRequest(name));
api().post(`/api/v1/tags/${name}/follow`).then(({ data }) => {
dispatch(followHashtagSuccess(name, data));
}).catch(err => {
dispatch(followHashtagFail(name, err));
});
};
export const followHashtagRequest = name => ({
type: HASHTAG_FOLLOW_REQUEST,
name,
});
export const followHashtagSuccess = (name, tag) => ({
type: HASHTAG_FOLLOW_SUCCESS,
name,
tag,
});
export const followHashtagFail = (name, error) => ({
type: HASHTAG_FOLLOW_FAIL,
name,
error,
});
export const unfollowHashtag = name => (dispatch) => {
dispatch(unfollowHashtagRequest(name));
api().post(`/api/v1/tags/${name}/unfollow`).then(({ data }) => {
dispatch(unfollowHashtagSuccess(name, data));
}).catch(err => {
dispatch(unfollowHashtagFail(name, err));
});
};
export const unfollowHashtagRequest = name => ({
type: HASHTAG_UNFOLLOW_REQUEST,
name,
});
export const unfollowHashtagSuccess = (name, tag) => ({
type: HASHTAG_UNFOLLOW_SUCCESS,
name,
tag,
});
export const unfollowHashtagFail = (name, error) => ({
type: HASHTAG_UNFOLLOW_FAIL,
name,
error,
});

View file

@ -0,0 +1,17 @@
import { apiGetTag, apiFollowTag, apiUnfollowTag } from 'mastodon/api/tags';
import { createDataLoadingThunk } from 'mastodon/store/typed_functions';
export const fetchHashtag = createDataLoadingThunk(
'tags/fetch',
({ tagId }: { tagId: string }) => apiGetTag(tagId),
);
export const followHashtag = createDataLoadingThunk(
'tags/follow',
({ tagId }: { tagId: string }) => apiFollowTag(tagId),
);
export const unfollowHashtag = createDataLoadingThunk(
'tags/unfollow',
({ tagId }: { tagId: string }) => apiUnfollowTag(tagId),
);

View file

@ -0,0 +1,11 @@
import { apiRequestGet } from 'mastodon/api';
import type {
ApiTermsOfServiceJSON,
ApiPrivacyPolicyJSON,
} from 'mastodon/api_types/instance';
export const apiGetTermsOfService = () =>
apiRequestGet<ApiTermsOfServiceJSON>('v1/instance/terms_of_service');
export const apiGetPrivacyPolicy = () =>
apiRequestGet<ApiPrivacyPolicyJSON>('v1/instance/privacy_policy');

View file

@ -0,0 +1,10 @@
import { apiRequestGet, apiRequestPost } from 'mastodon/api';
import type { ApiPollJSON } from 'mastodon/api_types/polls';
export const apiGetPoll = (pollId: string) =>
apiRequestGet<ApiPollJSON>(`/v1/polls/${pollId}`);
export const apiPollVote = (pollId: string, choices: string[]) =>
apiRequestPost<ApiPollJSON>(`/v1/polls/${pollId}/votes`, {
choices,
});

View file

@ -0,0 +1,16 @@
import { apiRequestGet } from 'mastodon/api';
import type {
ApiSearchType,
ApiSearchResultsJSON,
} from 'mastodon/api_types/search';
export const apiGetSearch = (params: {
q: string;
resolve?: boolean;
type?: ApiSearchType;
limit?: number;
offset?: number;
}) =>
apiRequestGet<ApiSearchResultsJSON>('v2/search', {
...params,
});

View file

@ -0,0 +1,11 @@
import { apiRequestPost, apiRequestGet } from 'mastodon/api';
import type { ApiHashtagJSON } from 'mastodon/api_types/tags';
export const apiGetTag = (tagId: string) =>
apiRequestGet<ApiHashtagJSON>(`v1/tags/${tagId}`);
export const apiFollowTag = (tagId: string) =>
apiRequestPost<ApiHashtagJSON>(`v1/tags/${tagId}/follow`);
export const apiUnfollowTag = (tagId: string) =>
apiRequestPost<ApiHashtagJSON>(`v1/tags/${tagId}/unfollow`);

View file

@ -0,0 +1,9 @@
export interface ApiTermsOfServiceJSON {
updated_at: string;
content: string;
}
export interface ApiPrivacyPolicyJSON {
updated_at: string;
content: string;
}

View file

@ -18,6 +18,6 @@ export interface ApiPollJSON {
options: ApiPollOptionJSON[];
emojis: ApiCustomEmojiJSON[];
voted: boolean;
own_votes: number[];
voted?: boolean;
own_votes?: number[];
}

View file

@ -0,0 +1,11 @@
import type { ApiAccountJSON } from './accounts';
import type { ApiStatusJSON } from './statuses';
import type { ApiHashtagJSON } from './tags';
export type ApiSearchType = 'accounts' | 'statuses' | 'hashtags';
export interface ApiSearchResultsJSON {
accounts: ApiAccountJSON[];
statuses: ApiStatusJSON[];
hashtags: ApiHashtagJSON[];
}

View file

@ -0,0 +1,13 @@
interface ApiHistoryJSON {
day: string;
accounts: string;
uses: string;
}
export interface ApiHashtagJSON {
id: string;
name: string;
url: string;
history: [ApiHistoryJSON, ...ApiHistoryJSON[]];
following?: boolean;
}

View file

@ -36,7 +36,7 @@ export default class AttachmentList extends ImmutablePureComponent {
return (
<li key={attachment.get('id')}>
<a href={displayUrl} target='_blank' rel='noopener noreferrer'>
<a href={displayUrl} target='_blank' rel='noopener'>
{compact && <Icon id='link' icon={LinkIcon} />}
{compact && ' ' }
{displayUrl ? filename(displayUrl) : <FormattedMessage id='attachments_list.unprocessed' defaultMessage='(unprocessed)' />}

View file

@ -124,7 +124,7 @@ class DropdownMenu extends PureComponent {
return (
<li className={classNames('dropdown-menu__item', { 'dropdown-menu__item--dangerous': dangerous })} key={`${text}-${i}`}>
<a href={href} target={target} data-method={method} rel='noopener noreferrer' role='button' tabIndex={0} ref={i === 0 ? this.setFocusRef : null} onClick={this.handleClick} onKeyPress={this.handleItemKeyPress} data-index={i}>
<a href={href} target={target} data-method={method} rel='noopener' role='button' tabIndex={0} ref={i === 0 ? this.setFocusRef : null} onClick={this.handleClick} onKeyPress={this.handleItemKeyPress} data-index={i}>
{text}
</a>
</li>

View file

@ -98,7 +98,7 @@ export default class ErrorBoundary extends PureComponent {
)}
</p>
<p className='error-boundary__footer'>Mastodon v{version} · <a href={source_url} rel='noopener noreferrer' target='_blank'><FormattedMessage id='errors.unexpected_crash.report_issue' defaultMessage='Report issue' /></a> · <button onClick={this.handleCopyStackTrace} className={copied ? 'copied' : ''}><FormattedMessage id='errors.unexpected_crash.copy_stacktrace' defaultMessage='Copy stacktrace to clipboard' /></button></p>
<p className='error-boundary__footer'>Mastodon v{version} · <a href={source_url} rel='noopener' target='_blank'><FormattedMessage id='errors.unexpected_crash.report_issue' defaultMessage='Report issue' /></a> · <button onClick={this.handleCopyStackTrace} className={copied ? 'copied' : ''}><FormattedMessage id='errors.unexpected_crash.copy_stacktrace' defaultMessage='Copy stacktrace to clipboard' /></button></p>
</div>
<Helmet>

View file

@ -88,7 +88,7 @@ export const FollowButton: React.FC<{
<a
href='/settings/profile'
target='_blank'
rel='noreferrer noopener'
rel='noopener'
className='button button-secondary'
>
{label}

View file

@ -12,6 +12,7 @@ import { Sparklines, SparklinesCurve } from 'react-sparklines';
import { ShortNumber } from 'mastodon/components/short_number';
import { Skeleton } from 'mastodon/components/skeleton';
import type { Hashtag as HashtagType } from 'mastodon/models/tags';
interface SilentErrorBoundaryProps {
children: React.ReactNode;
@ -80,6 +81,22 @@ export const ImmutableHashtag = ({ hashtag }: ImmutableHashtagProps) => (
/>
);
export const CompatibilityHashtag: React.FC<{
hashtag: HashtagType;
}> = ({ hashtag }) => (
<Hashtag
name={hashtag.name}
to={`/tags/${hashtag.name}`}
people={
(hashtag.history[0].accounts as unknown as number) * 1 +
((hashtag.history[1]?.accounts ?? 0) as unknown as number) * 1
}
history={hashtag.history
.map((day) => (day.uses as unknown as number) * 1)
.reverse()}
/>
);
export interface HashtagProps {
className?: string;
description?: React.ReactNode;

View file

@ -106,7 +106,7 @@ class Item extends PureComponent {
if (attachment.get('type') === 'unknown') {
return (
<div className={classNames('media-gallery__item', { standalone, 'media-gallery__item--tall': height === 100, 'media-gallery__item--wide': width === 100 })} key={attachment.get('id')}>
<a className='media-gallery__item-thumbnail' href={attachment.get('remote_url') || attachment.get('url')} style={{ cursor: 'pointer' }} title={description} lang={lang} target='_blank' rel='noopener noreferrer'>
<a className='media-gallery__item-thumbnail' href={attachment.get('remote_url') || attachment.get('url')} style={{ cursor: 'pointer' }} title={description} lang={lang} target='_blank' rel='noopener'>
<Blurhash
hash={attachment.get('blurhash')}
className='media-gallery__preview'
@ -138,7 +138,7 @@ class Item extends PureComponent {
href={attachment.get('remote_url') || originalUrl}
onClick={this.handleClick}
target='_blank'
rel='noopener noreferrer'
rel='noopener'
>
<img
src={previewUrl}

View file

@ -33,15 +33,10 @@ const messages = defineMessages({
},
});
const makeEmojiMap = record => record.get('emojis').reduce((obj, emoji) => {
obj[`:${emoji.get('shortcode')}:`] = emoji.toJS();
return obj;
}, {});
class Poll extends ImmutablePureComponent {
static propTypes = {
identity: identityContextPropShape,
poll: ImmutablePropTypes.map.isRequired,
poll: ImmutablePropTypes.record.isRequired,
status: ImmutablePropTypes.map.isRequired,
lang: PropTypes.string,
intl: PropTypes.object.isRequired,
@ -150,7 +145,7 @@ class Poll extends ImmutablePureComponent {
let titleHtml = option.getIn(['translation', 'titleHtml']) || option.get('titleHtml');
if (!titleHtml) {
const emojiMap = makeEmojiMap(poll);
const emojiMap = emojiMap(poll);
titleHtml = emojify(escapeTextContentForBrowser(title), emojiMap);
}

View file

@ -1,18 +0,0 @@
import { FormattedMessage } from 'react-intl';
import illustration from '@/images/elephant_ui_working.svg';
const RegenerationIndicator = () => (
<div className='regeneration-indicator'>
<div className='regeneration-indicator__figure'>
<img src={illustration} alt='' />
</div>
<div className='regeneration-indicator__label'>
<FormattedMessage id='regeneration_indicator.label' tagName='strong' defaultMessage='Loading&hellip;' />
<FormattedMessage id='regeneration_indicator.sublabel' defaultMessage='Your home feed is being prepared!' />
</div>
</div>
);
export default RegenerationIndicator;

View file

@ -0,0 +1,26 @@
import { FormattedMessage } from 'react-intl';
import { GIF } from './gif';
export const RegenerationIndicator: React.FC = () => (
<div className='regeneration-indicator'>
<GIF
src='/loading.gif'
staticSrc='/loading.png'
className='regeneration-indicator__figure'
/>
<div className='regeneration-indicator__label'>
<strong>
<FormattedMessage
id='regeneration_indicator.preparing_your_home_feed'
defaultMessage='Preparing your home feed…'
/>
</strong>
<FormattedMessage
id='regeneration_indicator.please_stand_by'
defaultMessage='Please stand by.'
/>
</div>
</div>
);

View file

@ -1,6 +1,6 @@
import { Component } from 'react';
import type { IntlShape } from 'react-intl';
import type { MessageDescriptor, PrimitiveType, IntlShape } from 'react-intl';
import { injectIntl, defineMessages } from 'react-intl';
const messages = defineMessages({
@ -102,7 +102,13 @@ const getUnitDelay = (units: string) => {
};
export const timeAgoString = (
intl: Pick<IntlShape, 'formatDate' | 'formatMessage'>,
intl: {
formatDate: IntlShape['formatDate'];
formatMessage: (
{ id, defaultMessage }: MessageDescriptor,
values?: Record<string, PrimitiveType>,
) => string;
},
date: Date,
now: number,
year: number,

View file

@ -42,7 +42,7 @@ class ServerBanner extends PureComponent {
return (
<div className='server-banner'>
<div className='server-banner__introduction'>
<FormattedMessage id='server_banner.is_one_of_many' defaultMessage='{domain} is one of the many independent Mastodon servers you can use to participate in the fediverse.' values={{ domain: <strong>{domain}</strong>, mastodon: <a href='https://joinmastodon.org' target='_blank'>Mastodon</a> }} />
<FormattedMessage id='server_banner.is_one_of_many' defaultMessage='{domain} is one of the many independent Mastodon servers you can use to participate in the fediverse.' values={{ domain: <strong>{domain}</strong>, mastodon: <a href='https://joinmastodon.org' target='_blank' rel='noopener'>Mastodon</a> }} />
</div>
<Link to='/about'>

View file

@ -167,7 +167,12 @@ class Status extends ImmutablePureComponent {
handleClick = e => {
e.preventDefault();
this.handleHotkeyOpen(e);
if (e?.button === 0 && !(e?.ctrlKey || e?.metaKey)) {
this._openStatus();
} else if (e?.button === 1 || (e?.button === 0 && (e?.ctrlKey || e?.metaKey))) {
this._openStatus(true);
}
};
handleMouseUp = e => {
@ -275,7 +280,11 @@ class Status extends ImmutablePureComponent {
this.props.onMention(this._properStatus().get('account'));
};
handleHotkeyOpen = (e) => {
handleHotkeyOpen = () => {
this._openStatus();
};
_openStatus = (newTab = false) => {
if (this.props.onClick) {
this.props.onClick();
return;
@ -290,10 +299,10 @@ class Status extends ImmutablePureComponent {
const path = `/@${status.getIn(['account', 'acct'])}/${status.get('id')}`;
if (e?.button === 0 && !(e?.ctrlKey || e?.metaKey)) {
if (newTab) {
window.open(path, '_blank', 'noopener');
} else {
history.push(path);
} else if (e?.button === 1 || (e?.button === 0 && (e?.ctrlKey || e?.metaKey))) {
window.open(path, '_blank', 'noreferrer noopener');
}
};
@ -384,6 +393,7 @@ class Status extends ImmutablePureComponent {
toggleHidden: this.handleHotkeyToggleHidden,
toggleSensitive: this.handleHotkeyToggleSensitive,
openMedia: this.handleHotkeyOpenMedia,
onTranslate: this.handleTranslate,
};
let media, statusAvatar, prepend, rebloggedByText;

View file

@ -47,6 +47,7 @@ const messages = defineMessages({
cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
favourite: { id: 'status.favourite', defaultMessage: 'Favorite' },
removeFavourite: { id: 'status.remove_favourite', defaultMessage: 'Remove from favorites' },
bookmark: { id: 'status.bookmark', defaultMessage: 'Bookmark' },
removeBookmark: { id: 'status.remove_bookmark', defaultMessage: 'Remove bookmark' },
open: { id: 'status.open', defaultMessage: 'Expand this status' },
@ -271,8 +272,6 @@ class StatusActionBar extends ImmutablePureComponent {
if (signedIn) {
menu.push(null);
menu.push({ text: intl.formatMessage(status.get('bookmarked') ? messages.removeBookmark : messages.bookmark), action: this.handleBookmarkClick });
if (writtenByMe && pinnableStatus) {
menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
}
@ -371,6 +370,9 @@ class StatusActionBar extends ImmutablePureComponent {
reblogIconComponent = RepeatDisabledIcon;
}
const bookmarkTitle = intl.formatMessage(status.get('bookmarked') ? messages.removeBookmark : messages.bookmark);
const favouriteTitle = intl.formatMessage(status.get('favourited') ? messages.removeFavourite : messages.favourite);
const isReply = status.get('in_reply_to_account_id') === status.getIn(['account', 'id']);
return (
@ -382,10 +384,10 @@ class StatusActionBar extends ImmutablePureComponent {
<IconButton className={classNames('status__action-bar__button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon='retweet' iconComponent={reblogIconComponent} onClick={this.handleReblogClick} counter={withCounters ? status.get('reblogs_count') : undefined} />
</div>
<div className='status__action-bar__button-wrapper'>
<IconButton className='status__action-bar__button star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' iconComponent={status.get('favourited') ? StarIcon : StarBorderIcon} onClick={this.handleFavouriteClick} counter={withCounters ? status.get('favourites_count') : undefined} />
<IconButton className='status__action-bar__button star-icon' animate active={status.get('favourited')} title={favouriteTitle} icon='star' iconComponent={status.get('favourited') ? StarIcon : StarBorderIcon} onClick={this.handleFavouriteClick} counter={withCounters ? status.get('favourites_count') : undefined} />
</div>
<div className='status__action-bar__button-wrapper'>
<IconButton className='status__action-bar__button bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' iconComponent={status.get('bookmarked') ? BookmarkIcon : BookmarkBorderIcon} onClick={this.handleBookmarkClick} />
<IconButton className='status__action-bar__button bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={bookmarkTitle} icon='bookmark' iconComponent={status.get('bookmarked') ? BookmarkIcon : BookmarkBorderIcon} onClick={this.handleBookmarkClick} />
</div>
<div className='status__action-bar__button-wrapper'>
<DropdownMenuContainer

View file

@ -38,7 +38,7 @@ class TranslateButton extends PureComponent {
if (translation) {
const language = preloadedLanguages.find(lang => lang[0] === translation.get('detected_source_language'));
const languageName = language ? language[2] : translation.get('detected_source_language');
const languageName = language ? language[1] : translation.get('detected_source_language');
const provider = translation.get('provider');
return (

View file

@ -6,7 +6,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import { debounce } from 'lodash';
import { TIMELINE_GAP, TIMELINE_SUGGESTIONS } from 'mastodon/actions/timelines';
import RegenerationIndicator from 'mastodon/components/regeneration_indicator';
import { RegenerationIndicator } from 'mastodon/components/regeneration_indicator';
import { InlineFollowSuggestions } from 'mastodon/features/home_timeline/components/inline_follow_suggestions';
import StatusContainer from '../containers/status_container';

View file

@ -1,6 +1,7 @@
import { Provider } from 'react-redux';
import { fetchCustomEmojis } from 'mastodon/actions/custom_emojis';
import { fetchServer } from 'mastodon/actions/server';
import { hydrateStore } from 'mastodon/actions/store';
import { Router } from 'mastodon/components/router';
import Compose from 'mastodon/features/standalone/compose';
@ -13,6 +14,7 @@ if (initialState) {
}
store.dispatch(fetchCustomEmojis());
store.dispatch(fetchServer());
const ComposeContainer = () => (
<IntlProvider>

View file

@ -9,14 +9,14 @@ import Poll from 'mastodon/components/poll';
const mapDispatchToProps = (dispatch, { pollId }) => ({
refresh: debounce(
() => {
dispatch(fetchPoll(pollId));
dispatch(fetchPoll({ pollId }));
},
1000,
{ leading: true },
),
onVote (choices) {
dispatch(vote(pollId, choices));
dispatch(vote({ pollId, choices }));
},
onInteractionModal (type, status) {
@ -32,7 +32,7 @@ const mapDispatchToProps = (dispatch, { pollId }) => ({
});
const mapStateToProps = (state, { pollId }) => ({
poll: state.getIn(['polls', pollId]),
poll: state.polls.get(pollId),
});
export default connect(mapStateToProps, mapDispatchToProps)(Poll);

View file

@ -18,7 +18,7 @@ import Column from 'mastodon/components/column';
import { Icon } from 'mastodon/components/icon';
import { ServerHeroImage } from 'mastodon/components/server_hero_image';
import { Skeleton } from 'mastodon/components/skeleton';
import LinkFooter from 'mastodon/features/ui/components/link_footer';
import { LinkFooter} from 'mastodon/features/ui/components/link_footer';
const messages = defineMessages({
title: { id: 'column.about', defaultMessage: 'About' },
@ -123,7 +123,7 @@ class About extends PureComponent {
<div className='about__header'>
<ServerHeroImage blurhash={server.getIn(['thumbnail', 'blurhash'])} src={server.getIn(['thumbnail', 'url'])} srcSet={server.getIn(['thumbnail', 'versions'])?.map((value, key) => `${value} ${key.replace('@', '')}`).join(', ')} className='about__header__hero' />
<h1>{isLoading ? <Skeleton width='10ch' /> : server.get('domain')}</h1>
<p><FormattedMessage id='about.powered_by' defaultMessage='Decentralized social media powered by {mastodon}' values={{ mastodon: <a href='https://joinmastodon.org' className='about__mail' target='_blank'>Mastodon</a> }} /></p>
<p><FormattedMessage id='about.powered_by' defaultMessage='Decentralized social media powered by {mastodon}' values={{ mastodon: <a href='https://joinmastodon.org' className='about__mail' target='_blank' rel='noopener'>Mastodon</a> }} /></p>
</div>
<div className='about__meta'>

View file

@ -6,6 +6,7 @@ import classNames from 'classnames';
import { Helmet } from 'react-helmet';
import { NavLink, withRouter } from 'react-router-dom';
import { isFulfilled, isRejected } from '@reduxjs/toolkit';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
@ -215,8 +216,20 @@ class Header extends ImmutablePureComponent {
const link = e.currentTarget;
onOpenURL(link.href, history, () => {
window.location = link.href;
onOpenURL(link.href).then((result) => {
if (isFulfilled(result)) {
if (result.payload.accounts[0]) {
history.push(`/@${result.payload.accounts[0].acct}`);
} else if (result.payload.statuses[0]) {
history.push(`/@${result.payload.statuses[0].account.acct}/${result.payload.statuses[0].id}`);
} else {
window.location = link.href;
}
} else if (isRejected(result)) {
window.location = link.href;
}
}).catch(() => {
// Nothing
});
}
};
@ -421,7 +434,7 @@ class Header extends ImmutablePureComponent {
<div className='account__header__bar'>
<div className='account__header__tabs'>
<a className='avatar' href={account.get('avatar')} rel='noopener noreferrer' target='_blank' onClick={this.handleAvatarClick}>
<a className='avatar' href={account.get('avatar')} rel='noopener' target='_blank' onClick={this.handleAvatarClick}>
<Avatar account={suspended || hidden ? undefined : account} size={90} />
</a>

View file

@ -144,8 +144,8 @@ const mapDispatchToProps = (dispatch) => ({
}));
},
onOpenURL (url, routerHistory, onFailure) {
dispatch(openURL(url, routerHistory, onFailure));
onOpenURL (url) {
return dispatch(openURL({ url }));
},
});

View file

@ -1,402 +0,0 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import { defineMessages, injectIntl, FormattedMessage, FormattedList } from 'react-intl';
import classNames from 'classnames';
import { withRouter } from 'react-router-dom';
import ImmutablePropTypes from 'react-immutable-proptypes';
import CancelIcon from '@/material-icons/400-24px/cancel-fill.svg?react';
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
import SearchIcon from '@/material-icons/400-24px/search.svg?react';
import { Icon } from 'mastodon/components/icon';
import { identityContextPropShape, withIdentity } from 'mastodon/identity_context';
import { domain, searchEnabled } from 'mastodon/initial_state';
import { HASHTAG_REGEX } from 'mastodon/utils/hashtags';
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
const messages = defineMessages({
placeholder: { id: 'search.placeholder', defaultMessage: 'Search' },
placeholderSignedIn: { id: 'search.search_or_paste', defaultMessage: 'Search or paste URL' },
});
const labelForRecentSearch = search => {
switch(search.get('type')) {
case 'account':
return `@${search.get('q')}`;
case 'hashtag':
return `#${search.get('q')}`;
default:
return search.get('q');
}
};
class Search extends PureComponent {
static propTypes = {
identity: identityContextPropShape,
value: PropTypes.string.isRequired,
recent: ImmutablePropTypes.orderedSet,
submitted: PropTypes.bool,
onChange: PropTypes.func.isRequired,
onSubmit: PropTypes.func.isRequired,
onOpenURL: PropTypes.func.isRequired,
onClickSearchResult: PropTypes.func.isRequired,
onForgetSearchResult: PropTypes.func.isRequired,
onClear: PropTypes.func.isRequired,
onShow: PropTypes.func.isRequired,
openInRoute: PropTypes.bool,
intl: PropTypes.object.isRequired,
singleColumn: PropTypes.bool,
...WithRouterPropTypes,
};
state = {
expanded: false,
selectedOption: -1,
options: [],
};
defaultOptions = [
{ key: 'prompt-has', label: <><mark>has:</mark> <FormattedList type='disjunction' value={['media', 'poll', 'embed']} /></>, action: e => { e.preventDefault(); this._insertText('has:'); } },
{ key: 'prompt-is', label: <><mark>is:</mark> <FormattedList type='disjunction' value={['reply', 'sensitive']} /></>, action: e => { e.preventDefault(); this._insertText('is:'); } },
{ key: 'prompt-language', label: <><mark>language:</mark> <FormattedMessage id='search_popout.language_code' defaultMessage='ISO language code' /></>, action: e => { e.preventDefault(); this._insertText('language:'); } },
{ key: 'prompt-from', label: <><mark>from:</mark> <FormattedMessage id='search_popout.user' defaultMessage='user' /></>, action: e => { e.preventDefault(); this._insertText('from:'); } },
{ key: 'prompt-before', label: <><mark>before:</mark> <FormattedMessage id='search_popout.specific_date' defaultMessage='specific date' /></>, action: e => { e.preventDefault(); this._insertText('before:'); } },
{ key: 'prompt-during', label: <><mark>during:</mark> <FormattedMessage id='search_popout.specific_date' defaultMessage='specific date' /></>, action: e => { e.preventDefault(); this._insertText('during:'); } },
{ key: 'prompt-after', label: <><mark>after:</mark> <FormattedMessage id='search_popout.specific_date' defaultMessage='specific date' /></>, action: e => { e.preventDefault(); this._insertText('after:'); } },
{ key: 'prompt-in', label: <><mark>in:</mark> <FormattedList type='disjunction' value={['all', 'library', 'public']} /></>, action: e => { e.preventDefault(); this._insertText('in:'); } }
];
setRef = c => {
this.searchForm = c;
};
handleChange = ({ target }) => {
const { onChange } = this.props;
onChange(target.value);
this._calculateOptions(target.value);
};
handleClear = e => {
const { value, submitted, onClear } = this.props;
e.preventDefault();
if (value.length > 0 || submitted) {
onClear();
this.setState({ options: [], selectedOption: -1 });
}
};
handleKeyDown = (e) => {
const { selectedOption } = this.state;
const options = searchEnabled ? this._getOptions().concat(this.defaultOptions) : this._getOptions();
switch(e.key) {
case 'Escape':
e.preventDefault();
this._unfocus();
break;
case 'ArrowDown':
e.preventDefault();
if (options.length > 0) {
this.setState({ selectedOption: Math.min(selectedOption + 1, options.length - 1) });
}
break;
case 'ArrowUp':
e.preventDefault();
if (options.length > 0) {
this.setState({ selectedOption: Math.max(selectedOption - 1, -1) });
}
break;
case 'Enter':
e.preventDefault();
if (selectedOption === -1) {
this._submit();
} else if (options.length > 0) {
options[selectedOption].action(e);
}
break;
case 'Delete':
if (selectedOption > -1 && options.length > 0) {
const search = options[selectedOption];
if (typeof search.forget === 'function') {
e.preventDefault();
search.forget(e);
}
}
break;
}
};
handleFocus = () => {
const { onShow, singleColumn } = this.props;
this.setState({ expanded: true, selectedOption: -1 });
onShow();
if (this.searchForm && !singleColumn) {
const { left, right } = this.searchForm.getBoundingClientRect();
if (left < 0 || right > (window.innerWidth || document.documentElement.clientWidth)) {
this.searchForm.scrollIntoView();
}
}
};
handleBlur = () => {
this.setState({ expanded: false, selectedOption: -1 });
};
handleHashtagClick = () => {
const { value, onClickSearchResult, history } = this.props;
const query = value.trim().replace(/^#/, '');
history.push(`/tags/${query}`);
onClickSearchResult(query, 'hashtag');
this._unfocus();
};
handleAccountClick = () => {
const { value, onClickSearchResult, history } = this.props;
const query = value.trim().replace(/^@/, '');
history.push(`/@${query}`);
onClickSearchResult(query, 'account');
this._unfocus();
};
handleURLClick = () => {
const { value, onOpenURL, history } = this.props;
onOpenURL(value, history);
this._unfocus();
};
handleStatusSearch = () => {
this._submit('statuses');
};
handleAccountSearch = () => {
this._submit('accounts');
};
handleRecentSearchClick = search => {
const { onChange, history } = this.props;
if (search.get('type') === 'account') {
history.push(`/@${search.get('q')}`);
} else if (search.get('type') === 'hashtag') {
history.push(`/tags/${search.get('q')}`);
} else {
onChange(search.get('q'));
this._submit(search.get('type'));
}
this._unfocus();
};
handleForgetRecentSearchClick = search => {
const { onForgetSearchResult } = this.props;
onForgetSearchResult(search.get('q'));
};
_unfocus () {
document.querySelector('.ui').parentElement.focus();
}
_insertText (text) {
const { value, onChange } = this.props;
if (value === '') {
onChange(text);
} else if (value[value.length - 1] === ' ') {
onChange(`${value}${text}`);
} else {
onChange(`${value} ${text}`);
}
}
_submit (type) {
const { onSubmit, openInRoute, value, onClickSearchResult, history } = this.props;
onSubmit(type);
if (value) {
onClickSearchResult(value, type);
}
if (openInRoute) {
history.push('/search');
}
this._unfocus();
}
_getOptions () {
const { options } = this.state;
if (options.length > 0) {
return options;
}
const { recent } = this.props;
return recent.toArray().map(search => ({
key: `${search.get('type')}/${search.get('q')}`,
label: labelForRecentSearch(search),
action: () => this.handleRecentSearchClick(search),
forget: e => {
e.stopPropagation();
this.handleForgetRecentSearchClick(search);
},
}));
}
_calculateOptions (value) {
const { signedIn } = this.props.identity;
const trimmedValue = value.trim();
const options = [];
if (trimmedValue.length > 0) {
const couldBeURL = trimmedValue.startsWith('https://') && !trimmedValue.includes(' ');
if (couldBeURL) {
options.push({ key: 'open-url', label: <FormattedMessage id='search.quick_action.open_url' defaultMessage='Open URL in Mastodon' />, action: this.handleURLClick });
}
const couldBeHashtag = (trimmedValue.startsWith('#') && trimmedValue.length > 1) || trimmedValue.match(HASHTAG_REGEX);
if (couldBeHashtag) {
options.push({ key: 'go-to-hashtag', label: <FormattedMessage id='search.quick_action.go_to_hashtag' defaultMessage='Go to hashtag {x}' values={{ x: <mark>#{trimmedValue.replace(/^#/, '')}</mark> }} />, action: this.handleHashtagClick });
}
const couldBeUsername = trimmedValue.match(/^@?[a-z0-9_-]+(@[^\s]+)?$/i);
if (couldBeUsername) {
options.push({ key: 'go-to-account', label: <FormattedMessage id='search.quick_action.go_to_account' defaultMessage='Go to profile {x}' values={{ x: <mark>@{trimmedValue.replace(/^@/, '')}</mark> }} />, action: this.handleAccountClick });
}
const couldBeStatusSearch = searchEnabled;
if (couldBeStatusSearch && signedIn) {
options.push({ key: 'status-search', label: <FormattedMessage id='search.quick_action.status_search' defaultMessage='Posts matching {x}' values={{ x: <mark>{trimmedValue}</mark> }} />, action: this.handleStatusSearch });
}
const couldBeUserSearch = true;
if (couldBeUserSearch) {
options.push({ key: 'account-search', label: <FormattedMessage id='search.quick_action.account_search' defaultMessage='Profiles matching {x}' values={{ x: <mark>{trimmedValue}</mark> }} />, action: this.handleAccountSearch });
}
}
this.setState({ options });
}
render () {
const { intl, value, submitted, recent } = this.props;
const { expanded, options, selectedOption } = this.state;
const { signedIn } = this.props.identity;
const hasValue = value.length > 0 || submitted;
return (
<div className={classNames('search', { active: expanded })}>
<input
ref={this.setRef}
className='search__input'
type='text'
placeholder={intl.formatMessage(signedIn ? messages.placeholderSignedIn : messages.placeholder)}
aria-label={intl.formatMessage(signedIn ? messages.placeholderSignedIn : messages.placeholder)}
value={value}
onChange={this.handleChange}
onKeyDown={this.handleKeyDown}
onFocus={this.handleFocus}
onBlur={this.handleBlur}
/>
<div role='button' tabIndex={0} className='search__icon' onClick={this.handleClear}>
<Icon id='search' icon={SearchIcon} className={hasValue ? '' : 'active'} />
<Icon id='times-circle' icon={CancelIcon} className={hasValue ? 'active' : ''} aria-label={intl.formatMessage(messages.placeholder)} />
</div>
<div className='search__popout'>
{options.length === 0 && (
<>
<h4><FormattedMessage id='search_popout.recent' defaultMessage='Recent searches' /></h4>
<div className='search__popout__menu'>
{recent.size > 0 ? this._getOptions().map(({ label, key, action, forget }, i) => (
<button key={key} onMouseDown={action} className={classNames('search__popout__menu__item search__popout__menu__item--flex', { selected: selectedOption === i })}>
<span>{label}</span>
<button className='icon-button' onMouseDown={forget}><Icon id='times' icon={CloseIcon} /></button>
</button>
)) : (
<div className='search__popout__menu__message'>
<FormattedMessage id='search.no_recent_searches' defaultMessage='No recent searches' />
</div>
)}
</div>
</>
)}
{options.length > 0 && (
<>
<h4><FormattedMessage id='search_popout.quick_actions' defaultMessage='Quick actions' /></h4>
<div className='search__popout__menu'>
{options.map(({ key, label, action }, i) => (
<button key={key} onMouseDown={action} className={classNames('search__popout__menu__item', { selected: selectedOption === i })}>
{label}
</button>
))}
</div>
</>
)}
<h4><FormattedMessage id='search_popout.options' defaultMessage='Search options' /></h4>
{searchEnabled && signedIn ? (
<div className='search__popout__menu'>
{this.defaultOptions.map(({ key, label, action }, i) => (
<button key={key} onMouseDown={action} className={classNames('search__popout__menu__item', { selected: selectedOption === ((options.length || recent.size) + i) })}>
{label}
</button>
))}
</div>
) : (
<div className='search__popout__menu__message'>
{searchEnabled ? (
<FormattedMessage id='search_popout.full_text_search_logged_out_message' defaultMessage='Only available when logged in.' />
) : (
<FormattedMessage id='search_popout.full_text_search_disabled_message' defaultMessage='Not available on {domain}.' values={{ domain }} />
)}
</div>
)}
</div>
</div>
);
}
}
export default withRouter(withIdentity(injectIntl(Search)));

View file

@ -0,0 +1,593 @@
import { useCallback, useState, useRef } from 'react';
import {
defineMessages,
useIntl,
FormattedMessage,
FormattedList,
} from 'react-intl';
import classNames from 'classnames';
import { useHistory } from 'react-router-dom';
import { isFulfilled } from '@reduxjs/toolkit';
import CancelIcon from '@/material-icons/400-24px/cancel-fill.svg?react';
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
import SearchIcon from '@/material-icons/400-24px/search.svg?react';
import {
clickSearchResult,
forgetSearchResult,
openURL,
} from 'mastodon/actions/search';
import { Icon } from 'mastodon/components/icon';
import { useIdentity } from 'mastodon/identity_context';
import { domain, searchEnabled } from 'mastodon/initial_state';
import type { RecentSearch, SearchType } from 'mastodon/models/search';
import { useAppSelector, useAppDispatch } from 'mastodon/store';
import { HASHTAG_REGEX } from 'mastodon/utils/hashtags';
const messages = defineMessages({
placeholder: { id: 'search.placeholder', defaultMessage: 'Search' },
placeholderSignedIn: {
id: 'search.search_or_paste',
defaultMessage: 'Search or paste URL',
},
});
const labelForRecentSearch = (search: RecentSearch) => {
switch (search.type) {
case 'account':
return `@${search.q}`;
case 'hashtag':
return `#${search.q}`;
default:
return search.q;
}
};
const unfocus = () => {
document.querySelector('.ui')?.parentElement?.focus();
};
interface SearchOption {
key: string;
label: React.ReactNode;
action: (e: React.MouseEvent | React.KeyboardEvent) => void;
forget?: (e: React.MouseEvent | React.KeyboardEvent) => void;
}
export const Search: React.FC<{
singleColumn: boolean;
initialValue?: string;
}> = ({ singleColumn, initialValue }) => {
const intl = useIntl();
const recent = useAppSelector((state) => state.search.recent);
const { signedIn } = useIdentity();
const dispatch = useAppDispatch();
const history = useHistory();
const searchInputRef = useRef<HTMLInputElement>(null);
const [value, setValue] = useState(initialValue ?? '');
const hasValue = value.length > 0;
const [expanded, setExpanded] = useState(false);
const [selectedOption, setSelectedOption] = useState(-1);
const [quickActions, setQuickActions] = useState<SearchOption[]>([]);
const searchOptions: SearchOption[] = [];
if (searchEnabled) {
searchOptions.push(
{
key: 'prompt-has',
label: (
<>
<mark>has:</mark>{' '}
<FormattedList
type='disjunction'
value={['media', 'poll', 'embed']}
/>
</>
),
action: (e) => {
e.preventDefault();
insertText('has:');
},
},
{
key: 'prompt-is',
label: (
<>
<mark>is:</mark>{' '}
<FormattedList type='disjunction' value={['reply', 'sensitive']} />
</>
),
action: (e) => {
e.preventDefault();
insertText('is:');
},
},
{
key: 'prompt-language',
label: (
<>
<mark>language:</mark>{' '}
<FormattedMessage
id='search_popout.language_code'
defaultMessage='ISO language code'
/>
</>
),
action: (e) => {
e.preventDefault();
insertText('language:');
},
},
{
key: 'prompt-from',
label: (
<>
<mark>from:</mark>{' '}
<FormattedMessage id='search_popout.user' defaultMessage='user' />
</>
),
action: (e) => {
e.preventDefault();
insertText('from:');
},
},
{
key: 'prompt-before',
label: (
<>
<mark>before:</mark>{' '}
<FormattedMessage
id='search_popout.specific_date'
defaultMessage='specific date'
/>
</>
),
action: (e) => {
e.preventDefault();
insertText('before:');
},
},
{
key: 'prompt-during',
label: (
<>
<mark>during:</mark>{' '}
<FormattedMessage
id='search_popout.specific_date'
defaultMessage='specific date'
/>
</>
),
action: (e) => {
e.preventDefault();
insertText('during:');
},
},
{
key: 'prompt-after',
label: (
<>
<mark>after:</mark>{' '}
<FormattedMessage
id='search_popout.specific_date'
defaultMessage='specific date'
/>
</>
),
action: (e) => {
e.preventDefault();
insertText('after:');
},
},
{
key: 'prompt-in',
label: (
<>
<mark>in:</mark>{' '}
<FormattedList
type='disjunction'
value={['all', 'library', 'public']}
/>
</>
),
action: (e) => {
e.preventDefault();
insertText('in:');
},
},
);
}
const recentOptions: SearchOption[] = recent.map((search) => ({
key: `${search.type}/${search.q}`,
label: labelForRecentSearch(search),
action: () => {
setValue(search.q);
if (search.type === 'account') {
history.push(`/@${search.q}`);
} else if (search.type === 'hashtag') {
history.push(`/tags/${search.q}`);
} else {
const queryParams = new URLSearchParams({ q: search.q });
if (search.type) queryParams.set('type', search.type);
history.push({ pathname: '/search', search: queryParams.toString() });
}
unfocus();
},
forget: (e) => {
e.stopPropagation();
void dispatch(forgetSearchResult(search.q));
},
}));
const navigableOptions = hasValue
? quickActions.concat(searchOptions)
: recentOptions.concat(quickActions, searchOptions);
const insertText = (text: string) => {
setValue((currentValue) => {
if (currentValue === '') {
return text;
} else if (currentValue.endsWith(' ')) {
return `${currentValue}${text}`;
} else {
return `${currentValue} ${text}`;
}
});
};
const submit = useCallback(
(q: string, type?: SearchType) => {
void dispatch(clickSearchResult({ q, type }));
const queryParams = new URLSearchParams({ q });
if (type) queryParams.set('type', type);
history.push({ pathname: '/search', search: queryParams.toString() });
unfocus();
},
[dispatch, history],
);
const handleChange = useCallback(
({ target: { value } }: React.ChangeEvent<HTMLInputElement>) => {
setValue(value);
const trimmedValue = value.trim();
const newQuickActions = [];
if (trimmedValue.length > 0) {
const couldBeURL =
trimmedValue.startsWith('https://') && !trimmedValue.includes(' ');
if (couldBeURL) {
newQuickActions.push({
key: 'open-url',
label: (
<FormattedMessage
id='search.quick_action.open_url'
defaultMessage='Open URL in Mastodon'
/>
),
action: async () => {
const result = await dispatch(openURL({ url: trimmedValue }));
if (isFulfilled(result)) {
if (result.payload.accounts[0]) {
history.push(`/@${result.payload.accounts[0].acct}`);
} else if (result.payload.statuses[0]) {
history.push(
`/@${result.payload.statuses[0].account.acct}/${result.payload.statuses[0].id}`,
);
}
}
unfocus();
},
});
}
const couldBeHashtag =
(trimmedValue.startsWith('#') && trimmedValue.length > 1) ||
trimmedValue.match(HASHTAG_REGEX);
if (couldBeHashtag) {
newQuickActions.push({
key: 'go-to-hashtag',
label: (
<FormattedMessage
id='search.quick_action.go_to_hashtag'
defaultMessage='Go to hashtag {x}'
values={{ x: <mark>#{trimmedValue.replace(/^#/, '')}</mark> }}
/>
),
action: () => {
const query = trimmedValue.replace(/^#/, '');
history.push(`/tags/${query}`);
void dispatch(clickSearchResult({ q: query, type: 'hashtag' }));
unfocus();
},
});
}
const couldBeUsername = /^@?[a-z0-9_-]+(@[^\s]+)?$/i.exec(trimmedValue);
if (couldBeUsername) {
newQuickActions.push({
key: 'go-to-account',
label: (
<FormattedMessage
id='search.quick_action.go_to_account'
defaultMessage='Go to profile {x}'
values={{ x: <mark>@{trimmedValue.replace(/^@/, '')}</mark> }}
/>
),
action: () => {
const query = trimmedValue.replace(/^@/, '');
history.push(`/@${query}`);
void dispatch(clickSearchResult({ q: query, type: 'account' }));
unfocus();
},
});
}
const couldBeStatusSearch = searchEnabled;
if (couldBeStatusSearch && signedIn) {
newQuickActions.push({
key: 'status-search',
label: (
<FormattedMessage
id='search.quick_action.status_search'
defaultMessage='Posts matching {x}'
values={{ x: <mark>{trimmedValue}</mark> }}
/>
),
action: () => {
submit(trimmedValue, 'statuses');
},
});
}
newQuickActions.push({
key: 'account-search',
label: (
<FormattedMessage
id='search.quick_action.account_search'
defaultMessage='Profiles matching {x}'
values={{ x: <mark>{trimmedValue}</mark> }}
/>
),
action: () => {
submit(trimmedValue, 'accounts');
},
});
}
setQuickActions(newQuickActions);
},
[dispatch, history, signedIn, setValue, setQuickActions, submit],
);
const handleClear = useCallback(() => {
setValue('');
setQuickActions([]);
setSelectedOption(-1);
}, [setValue, setQuickActions, setSelectedOption]);
const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => {
switch (e.key) {
case 'Escape':
e.preventDefault();
unfocus();
break;
case 'ArrowDown':
e.preventDefault();
if (navigableOptions.length > 0) {
setSelectedOption(
Math.min(selectedOption + 1, navigableOptions.length - 1),
);
}
break;
case 'ArrowUp':
e.preventDefault();
if (navigableOptions.length > 0) {
setSelectedOption(Math.max(selectedOption - 1, -1));
}
break;
case 'Enter':
e.preventDefault();
if (selectedOption === -1) {
submit(value);
} else if (navigableOptions.length > 0) {
navigableOptions[selectedOption]?.action(e);
}
break;
case 'Delete':
if (selectedOption > -1 && navigableOptions.length > 0) {
const search = navigableOptions[selectedOption];
if (typeof search?.forget === 'function') {
e.preventDefault();
search.forget(e);
}
}
break;
}
},
[navigableOptions, value, selectedOption, setSelectedOption, submit],
);
const handleFocus = useCallback(() => {
setExpanded(true);
setSelectedOption(-1);
if (searchInputRef.current && !singleColumn) {
const { left, right } = searchInputRef.current.getBoundingClientRect();
if (
left < 0 ||
right > (window.innerWidth || document.documentElement.clientWidth)
) {
searchInputRef.current.scrollIntoView();
}
}
}, [setExpanded, setSelectedOption, singleColumn]);
const handleBlur = useCallback(() => {
setExpanded(false);
setSelectedOption(-1);
}, [setExpanded, setSelectedOption]);
return (
<form className={classNames('search', { active: expanded })}>
<input
ref={searchInputRef}
className='search__input'
type='text'
placeholder={intl.formatMessage(
signedIn ? messages.placeholderSignedIn : messages.placeholder,
)}
aria-label={intl.formatMessage(
signedIn ? messages.placeholderSignedIn : messages.placeholder,
)}
value={value}
onChange={handleChange}
onKeyDown={handleKeyDown}
onFocus={handleFocus}
onBlur={handleBlur}
/>
<button type='button' className='search__icon' onClick={handleClear}>
<Icon
id='search'
icon={SearchIcon}
className={hasValue ? '' : 'active'}
/>
<Icon
id='times-circle'
icon={CancelIcon}
className={hasValue ? 'active' : ''}
aria-label={intl.formatMessage(messages.placeholder)}
/>
</button>
<div className='search__popout'>
{!hasValue && (
<>
<h4>
<FormattedMessage
id='search_popout.recent'
defaultMessage='Recent searches'
/>
</h4>
<div className='search__popout__menu'>
{recentOptions.length > 0 ? (
recentOptions.map(({ label, key, action, forget }, i) => (
<button
key={key}
onMouseDown={action}
className={classNames(
'search__popout__menu__item search__popout__menu__item--flex',
{ selected: selectedOption === i },
)}
>
<span>{label}</span>
<button className='icon-button' onMouseDown={forget}>
<Icon id='times' icon={CloseIcon} />
</button>
</button>
))
) : (
<div className='search__popout__menu__message'>
<FormattedMessage
id='search.no_recent_searches'
defaultMessage='No recent searches'
/>
</div>
)}
</div>
</>
)}
{quickActions.length > 0 && (
<>
<h4>
<FormattedMessage
id='search_popout.quick_actions'
defaultMessage='Quick actions'
/>
</h4>
<div className='search__popout__menu'>
{quickActions.map(({ key, label, action }, i) => (
<button
key={key}
onMouseDown={action}
className={classNames('search__popout__menu__item', {
selected: selectedOption === i,
})}
>
{label}
</button>
))}
</div>
</>
)}
<h4>
<FormattedMessage
id='search_popout.options'
defaultMessage='Search options'
/>
</h4>
{searchEnabled && signedIn ? (
<div className='search__popout__menu'>
{searchOptions.map(({ key, label, action }, i) => (
<button
key={key}
onMouseDown={action}
className={classNames('search__popout__menu__item', {
selected:
selectedOption ===
(quickActions.length || recent.length) + i,
})}
>
{label}
</button>
))}
</div>
) : (
<div className='search__popout__menu__message'>
{searchEnabled ? (
<FormattedMessage
id='search_popout.full_text_search_logged_out_message'
defaultMessage='Only available when logged in.'
/>
) : (
<FormattedMessage
id='search_popout.full_text_search_disabled_message'
defaultMessage='Not available on {domain}.'
values={{ domain }}
/>
)}
</div>
)}
</div>
</form>
);
};

View file

@ -1,93 +0,0 @@
import { useCallback } from 'react';
import { FormattedMessage } from 'react-intl';
import FindInPageIcon from '@/material-icons/400-24px/find_in_page.svg?react';
import PeopleIcon from '@/material-icons/400-24px/group.svg?react';
import TagIcon from '@/material-icons/400-24px/tag.svg?react';
import { expandSearch } from 'mastodon/actions/search';
import { Account } from 'mastodon/components/account';
import { Icon } from 'mastodon/components/icon';
import { LoadMore } from 'mastodon/components/load_more';
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
import { SearchSection } from 'mastodon/features/explore/components/search_section';
import { useAppDispatch, useAppSelector } from 'mastodon/store';
import { ImmutableHashtag as Hashtag } from '../../../components/hashtag';
import StatusContainer from '../../../containers/status_container';
const INITIAL_PAGE_LIMIT = 10;
const withoutLastResult = list => {
if (list.size > INITIAL_PAGE_LIMIT && list.size % INITIAL_PAGE_LIMIT === 1) {
return list.skipLast(1);
} else {
return list;
}
};
export const SearchResults = () => {
const results = useAppSelector((state) => state.getIn(['search', 'results']));
const isLoading = useAppSelector((state) => state.getIn(['search', 'isLoading']));
const dispatch = useAppDispatch();
const handleLoadMoreAccounts = useCallback(() => {
dispatch(expandSearch('accounts'));
}, [dispatch]);
const handleLoadMoreStatuses = useCallback(() => {
dispatch(expandSearch('statuses'));
}, [dispatch]);
const handleLoadMoreHashtags = useCallback(() => {
dispatch(expandSearch('hashtags'));
}, [dispatch]);
let accounts, statuses, hashtags;
if (results.get('accounts') && results.get('accounts').size > 0) {
accounts = (
<SearchSection title={<><Icon id='users' icon={PeopleIcon} /><FormattedMessage id='search_results.accounts' defaultMessage='Profiles' /></>}>
{withoutLastResult(results.get('accounts')).map(accountId => <Account key={accountId} id={accountId} />)}
{(results.get('accounts').size > INITIAL_PAGE_LIMIT && results.get('accounts').size % INITIAL_PAGE_LIMIT === 1) && <LoadMore visible onClick={handleLoadMoreAccounts} />}
</SearchSection>
);
}
if (results.get('hashtags') && results.get('hashtags').size > 0) {
hashtags = (
<SearchSection title={<><Icon id='hashtag' icon={TagIcon} /><FormattedMessage id='search_results.hashtags' defaultMessage='Hashtags' /></>}>
{withoutLastResult(results.get('hashtags')).map(hashtag => <Hashtag key={hashtag.get('name')} hashtag={hashtag} />)}
{(results.get('hashtags').size > INITIAL_PAGE_LIMIT && results.get('hashtags').size % INITIAL_PAGE_LIMIT === 1) && <LoadMore visible onClick={handleLoadMoreHashtags} />}
</SearchSection>
);
}
if (results.get('statuses') && results.get('statuses').size > 0) {
statuses = (
<SearchSection title={<><Icon id='quote-right' icon={FindInPageIcon} /><FormattedMessage id='search_results.statuses' defaultMessage='Posts' /></>}>
{withoutLastResult(results.get('statuses')).map(statusId => <StatusContainer key={statusId} id={statusId} />)}
{(results.get('statuses').size > INITIAL_PAGE_LIMIT && results.get('statuses').size % INITIAL_PAGE_LIMIT === 1) && <LoadMore visible onClick={handleLoadMoreStatuses} />}
</SearchSection>
);
}
return (
<div className='search-results'>
{!accounts && !hashtags && !statuses && (
isLoading ? (
<LoadingIndicator />
) : (
<div className='empty-column-indicator'>
<FormattedMessage id='search_results.nothing_found' defaultMessage='Could not find anything for these search terms' />
</div>
)
)}
{accounts}
{hashtags}
{statuses}
</div>
);
};

View file

@ -1,59 +0,0 @@
import { createSelector } from '@reduxjs/toolkit';
import { connect } from 'react-redux';
import {
changeSearch,
clearSearch,
submitSearch,
showSearch,
openURL,
clickSearchResult,
forgetSearchResult,
} from 'mastodon/actions/search';
import Search from '../components/search';
const getRecentSearches = createSelector(
state => state.getIn(['search', 'recent']),
recent => recent.reverse(),
);
const mapStateToProps = state => ({
value: state.getIn(['search', 'value']),
submitted: state.getIn(['search', 'submitted']),
recent: getRecentSearches(state),
});
const mapDispatchToProps = dispatch => ({
onChange (value) {
dispatch(changeSearch(value));
},
onClear () {
dispatch(clearSearch());
},
onSubmit (type) {
dispatch(submitSearch(type));
},
onShow () {
dispatch(showSearch());
},
onOpenURL (q, routerHistory) {
dispatch(openURL(q, routerHistory));
},
onClickSearchResult (q, type) {
dispatch(clickSearchResult(q, type));
},
onForgetSearchResult (q) {
dispatch(forgetSearchResult(q));
},
});
export default connect(mapStateToProps, mapDispatchToProps)(Search);

View file

@ -9,8 +9,6 @@ import { Link } from 'react-router-dom';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { connect } from 'react-redux';
import spring from 'react-motion/lib/spring';
import PeopleIcon from '@/material-icons/400-24px/group.svg?react';
import HomeIcon from '@/material-icons/400-24px/home-fill.svg?react';
import LogoutIcon from '@/material-icons/400-24px/logout.svg?react';
@ -26,11 +24,9 @@ import elephantUIPlane from '../../../images/elephant_ui_plane.svg';
import { changeComposing, mountCompose, unmountCompose } from '../../actions/compose';
import { mascot } from '../../initial_state';
import { isMobile } from '../../is_mobile';
import Motion from '../ui/util/optional_motion';
import { SearchResults } from './components/search_results';
import { Search } from './components/search';
import ComposeFormContainer from './containers/compose_form_container';
import SearchContainer from './containers/search_container';
const messages = defineMessages({
start: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
@ -43,9 +39,8 @@ const messages = defineMessages({
compose: { id: 'navigation_bar.compose', defaultMessage: 'Compose new post' },
});
const mapStateToProps = (state, ownProps) => ({
const mapStateToProps = (state) => ({
columns: state.getIn(['settings', 'columns']),
showSearch: ownProps.multiColumn ? state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']) : false,
});
class Compose extends PureComponent {
@ -54,7 +49,6 @@ class Compose extends PureComponent {
dispatch: PropTypes.func.isRequired,
columns: ImmutablePropTypes.list.isRequired,
multiColumn: PropTypes.bool,
showSearch: PropTypes.bool,
intl: PropTypes.object.isRequired,
};
@ -88,7 +82,7 @@ class Compose extends PureComponent {
};
render () {
const { multiColumn, showSearch, intl } = this.props;
const { multiColumn, intl } = this.props;
if (multiColumn) {
const { columns } = this.props;
@ -113,7 +107,7 @@ class Compose extends PureComponent {
<a href='/auth/sign_out' className='drawer__tab' title={intl.formatMessage(messages.logout)} aria-label={intl.formatMessage(messages.logout)} onClick={this.handleLogoutClick}><Icon id='sign-out' icon={LogoutIcon} /></a>
</nav>
{multiColumn && <SearchContainer /> }
{multiColumn && <Search /> }
<div className='drawer__pager'>
<div className='drawer__inner' onFocus={this.onFocus}>
@ -123,14 +117,6 @@ class Compose extends PureComponent {
<img alt='' draggable='false' src={mascot || elephantUIPlane} />
</div>
</div>
<Motion defaultStyle={{ x: -100 }} style={{ x: spring(showSearch ? 0 : -100, { stiffness: 210, damping: 20 }) }}>
{({ x }) => (
<div className='drawer__inner darker' style={{ transform: `translateX(${x}%)`, visibility: x === -100 ? 'hidden' : 'visible' }}>
<SearchResults />
</div>
)}
</Motion>
</div>
</div>
);

View file

@ -90,8 +90,8 @@ describe('emoji', () => {
});
it('keeps ordering as expected (issue fixed by PR 20677)', () => {
expect(emojify('<p>💕 <a class="hashtag" href="https://example.com/tags/foo" rel="nofollow noopener noreferrer" target="_blank">#<span>foo</span></a> test: foo.</p>'))
.toEqual('<p><picture><img draggable="false" class="emojione" alt="💕" title=":two_hearts:" src="/emoji/1f495.svg"></picture> <a class="hashtag" href="https://example.com/tags/foo" rel="nofollow noopener noreferrer" target="_blank">#<span>foo</span></a> test: foo.</p>');
expect(emojify('<p>💕 <a class="hashtag" href="https://example.com/tags/foo" rel="nofollow noopener" target="_blank">#<span>foo</span></a> test: foo.</p>'))
.toEqual('<p><picture><img draggable="false" class="emojione" alt="💕" title=":two_hearts:" src="/emoji/1f495.svg"></picture> <a class="hashtag" href="https://example.com/tags/foo" rel="nofollow noopener" target="_blank">#<span>foo</span></a> test: foo.</p>');
});
});
});

View file

@ -1,20 +0,0 @@
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
export const SearchSection = ({ title, onClickMore, children }) => (
<div className='search-results__section'>
<div className='search-results__section__header'>
<h3>{title}</h3>
{onClickMore && <button onClick={onClickMore}><FormattedMessage id='search_results.see_all' defaultMessage='See all' /></button>}
</div>
{children}
</div>
);
SearchSection.propTypes = {
title: PropTypes.node.isRequired,
onClickMore: PropTypes.func,
children: PropTypes.children,
};

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