Compare commits

..

242 Commits

Author SHA1 Message Date
c92b5559a9
Merge pull request #137 from Y-zu-don-maintenance-org/4.1.18
4.1.18
2024-07-05 18:59:51 +09:00
Claire
ff90ebffaa
Bump version to v4.1.18 (#30911) 2024-07-04 16:46:39 +02:00
Claire
a1c7aae28a
Merge pull request from GHSA-xjvf-fm67-4qc3 2024-07-04 16:45:52 +02:00
Claire
34aeef3453
Merge pull request from GHSA-58x8-3qxw-6hm7
* Fix insufficient permission checking for public timeline endpoints

Note that this changes unauthenticated access failure code from 401 to 422

* Add more tests for public timelines

* Require user token in `/api/v1/statuses/:id/translate` and `/api/v1/scheduled_statuses`
2024-07-04 16:26:49 +02:00
Claire
122740047a
Merge pull request from GHSA-vp5r-5pgw-jwqx
* Fix streaming sessions not being closed when revoking access to an app

* Add tests for GHSA-7w3c-p9j8-mq3x
2024-07-04 16:11:28 +02:00
Claire
4b45333aff fix: Return HTTP 422 when scheduled status time is less than 5 minutes (#30584) 2024-07-03 11:13:40 +02:00
David Roetzel
6cf83a2a64 Improve encoding detection for link cards (#30780) 2024-07-03 11:13:40 +02:00
Eugen Rochko
2a5819e8bb Change search modifiers to be case-insensitive (#30865) 2024-07-03 11:13:40 +02:00
David Roetzel
815680bd13 Add size limit for link preview URLs (#30854) 2024-07-03 11:13:40 +02:00
Claire
d8e8437a29 Update dependency rails 2024-07-02 16:20:04 +02:00
Tim Rogers
839147e099 Added check for STATSD_ADDR setting to emit a warning and proceed rather than crashing if the address is unreachable (#30691) 2024-07-02 16:20:04 +02:00
Claire
8e924e4338 Fix /admin/accounts/:account_id/statuses/:id for edited posts with media attachments (#30819) 2024-07-02 16:20:04 +02:00
Claire
2ee88a99d9 Change PWA start URL from /home to / (#27377) 2024-06-18 16:05:53 +02:00
Claire
1cad857f14
Bump version to v4.1.17 (#30472) 2024-05-30 15:49:14 +02:00
Claire
95ebcff98e Fix rate-limiting incorrectly triggering a session cookie on most endpoints (#30483) 2024-05-30 15:20:04 +02:00
Claire
d770b61a74
Merge pull request from GHSA-c2r5-cfqr-c553
* Add hardening monkey-patch to prevent IP spoofing on misconfigured installations

* Remove rack-attack safelist
2024-05-30 14:24:29 +02:00
Claire
020228ddba
Merge pull request from GHSA-q3rg-xx5v-4mxh 2024-05-30 14:14:04 +02:00
Claire
e292a28933
Merge pull request from GHSA-5fq7-3p3j-9vrf 2024-05-30 14:03:13 +02:00
Claire
ba240cea0c Normalize language code of incoming posts (#30403) 2024-05-29 15:31:34 +02:00
Claire
257f9abd56 Fix leaking Elasticsearch connections in Sidekiq processes (#30450) 2024-05-29 15:31:34 +02:00
Claire
b4e3a789b1 Update dependency rexml to 3.2.8 2024-05-29 15:31:34 +02:00
Claire
b39fbe7c83 Update dependency nokogiri to 1.16.5 2024-05-17 12:30:07 +02:00
Claire
c717b7da99 Update dependency puma to 5.6.8 2024-05-17 12:30:07 +02:00
Claire
13bbcdf4d4 Update dependency json-jwt to 1.15.3.1 2024-05-17 12:30:07 +02:00
Claire
3aec33f5a2 Fix off-by-one in tootctl media commands (#30306) 2024-05-17 12:30:07 +02:00
Emelia Smith
984d7d3dc8 Fix missing destory audit logs for Domain Allows (#30125) 2024-05-17 12:30:07 +02:00
Claire
33a50884e5 Fix not being able to block a subdomain of an already-blocked domain through the API (#30119) 2024-05-17 12:30:07 +02:00
Claire
70c4d70dbe Fix Idempotency-Key ignored when scheduling a post (#30084) 2024-05-17 12:30:07 +02:00
Tim Rogers
a6089cdfca Fixed crash when supplying FFMPEG_BINARY environment variable (#30022) 2024-05-17 12:30:07 +02:00
Claire
5973d7a4b6 Remove caching in cache_collection (#29862) 2024-05-17 12:30:07 +02:00
Claire
ba5551fd1d Improve email address validation (#29838) 2024-05-17 12:30:07 +02:00
Matt Jankowski
8ce403a85b Fix results/query in api/v1/featured_tags/suggestions (#29597) 2024-05-17 12:30:07 +02:00
Jeong Arm
3ff575f54c Normalize idna domain before account unblock domain (#29530) 2024-05-17 12:30:07 +02:00
Claire
affbb10566 Fix admin account created by mastodon:setup not being auto-approved (#29379) 2024-05-17 12:30:07 +02:00
Emelia Smith
209632a0fd Return domain block digests from admin domain blocks API (#29092) 2024-05-17 12:30:07 +02:00
Claire
079d3e5189 Add fallback redirection when getting a webfinger query WEB_DOMAIN@WEB_DOMAIN (#28592) 2024-05-17 12:30:07 +02:00
Matt Jankowski
57b72cccc4 Fix reference to non-existent var in CLI maintenance command (#28363) 2024-05-17 12:30:07 +02:00
Claire
37adb144db
Fix auto close registration mail (#30323) 2024-05-16 11:52:02 +02:00
Claire
142dd34b68
Fix CI not actually running ruby tests in 4.1 branch (#30321) 2024-05-16 11:28:04 +02:00
Claire
c2d8666bbf
Bump version to v4.1.16 (#29371) 2024-02-23 14:09:38 +01:00
Claire
d3c4441af8
Fix processing of Link objects in Image objects (#29364) 2024-02-23 09:53:09 +01:00
Claire
f0541adbd4
Fix link verifications when page size exceeds 1MB (#29362) 2024-02-22 19:12:57 +01:00
Claire
3fecb36739
Change registrations to be disabled by default for new servers (#29354) 2024-02-22 18:28:41 +01:00
Claire
c7312411b8 Fix auto-close email being sent to users with devops permissions instead of settings permissions (#29356) 2024-02-22 18:28:28 +01:00
Claire
2fc87611be Automatically switch from open to approved registrations in absence of moderators (#29337) 2024-02-22 18:28:28 +01:00
Claire
1629ac4c81
Update dependencies (#29350) 2024-02-22 14:52:07 +01:00
Claire
54ae3d5ca5
Add basic CI to 4.1 branch (#29351) 2024-02-22 14:38:11 +01:00
fbef81ab51 add missing env 2024-02-22 20:58:30 +09:00
0eb421cc64 Revert "Add reject pattern to Admin setting"
This reverts commit 0cd5faaa9d.
2024-02-22 20:46:08 +09:00
c2e185162d Revert "fix typo"
This reverts commit af41ff0e2b.
2024-02-22 20:46:00 +09:00
93cd53398a Revert "add i18n"
This reverts commit 0ca146a155.
2024-02-22 20:45:50 +09:00
0ca146a155 add i18n 2024-02-22 20:24:24 +09:00
af41ff0e2b fix typo 2024-02-22 20:20:07 +09:00
noellabo
0cd5faaa9d Add reject pattern to Admin setting 2024-02-22 20:15:49 +09:00
Sho Kusano
c2f59a2848 :sad: 2024-02-18 23:07:24 +09:00
Sho Kusano
6e76cbb0e4 Reject spammer 2024-02-18 22:34:53 +09:00
1b06c5befc
Merge pull request #134 from Y-zu-don-maintenance-org/features/v4.1.15
Features/v4.1.15
2024-02-17 10:18:09 +09:00
Claire
b7b03e8d26 Bump version to v4.1.15 2024-02-16 11:57:15 +01:00
Claire
a07fff079b
Merge pull request from GHSA-jhrq-qvrm-qr36
* Fix insufficient Content-Type checking of fetched ActivityStreams objects

* Allow JSON-LD documents with multiple profiles
2024-02-16 11:56:12 +01:00
Claire
6f29d50aa5 Update dependency pg to 1.5.5 2024-02-16 09:42:31 +01:00
Claire
9e5af6bb58 Fix user creation failure handling in OAuth paths (#29207)
Co-authored-by: Matt Jankowski <matt@jankowski.online>
2024-02-14 23:16:39 +01:00
ec77396ddd
Merge pull request #133 from Y-zu-don-maintenance-org/features/v4.1.14
Features/v4.1.14
2024-02-15 05:53:04 +09:00
Claire
6499850ac4 Bump version to v4.1.14 2024-02-14 15:16:55 +01:00
Claire
6f36b633a7
Merge pull request from GHSA-vm39-j3vx-pch3
* Prevent different identities from a same SSO provider from accessing a same account

* Lock auth provider changes behind `ALLOW_UNSAFE_AUTH_PROVIDER_REATTACH=true`

* Rename methods to avoid confusion between OAuth and OmniAuth
2024-02-14 15:16:07 +01:00
Claire
d807b3960e
Merge pull request from GHSA-7w3c-p9j8-mq3x
* Ensure destruction of OAuth Applications notifies streaming

Due to doorkeeper using a dependent: delete_all relationship, the destroy of an OAuth Application bypassed the existing AccessTokenExtension callbacks for announcing destructing of access tokens.

* Ensure password resets revoke access to Streaming API

* Improve performance of deleting OAuth tokens

---------

Co-authored-by: Emelia Smith <ThisIsMissEm@users.noreply.github.com>
2024-02-14 15:15:34 +01:00
Claire
2f6518cae2 Add sidekiq_unique_jobs:delete_all_locks task and disable sidekiq-unique-jobs UI by default (#29199) 2024-02-14 13:17:55 +01:00
Emelia Smith
cdbe2855f3 Disable administrative doorkeeper routes (#29187) 2024-02-14 11:34:46 +01:00
blah
fdde3cdb4e Update dependency sidekiq-unique-jobs to 7.1.33 2024-02-14 11:34:46 +01:00
blah
ce9c641d9a Update dependency nokogiri to 1.16.2 2024-02-14 11:26:27 +01:00
5e2bc7aa95
Merge pull request #130 from Y-zu-don-maintenance-org/features/v4.1.13
Merge pull request from GHSA-3fjr-858r-92rw
2024-02-02 21:32:09 +09:00
2ab80bc511
Merge pull request #129 from Y-zu-don-maintenance-org/features/v4.1.12
Features/v4.1.12
2024-02-02 21:31:08 +09:00
Claire
5799bc4af7
Merge pull request from GHSA-3fjr-858r-92rw
* Fix insufficient origin validation

* Bump version to v4.1.13
2024-02-01 15:56:46 +01:00
Claire
fc4e2eca9f Bump version to v4.1.12 2024-01-24 15:31:06 +01:00
Claire
2e8943aecd Add rate-limit of TOTP authentication attempts at controller level (#28801) 2024-01-24 15:31:06 +01:00
Claire
e6072a8d13 Fix error when processing remote files with unusually long names (#28823) 2024-01-24 15:31:06 +01:00
Claire
460e4fbdd6 Fix processing of compacted single-item JSON-LD collections (#28816) 2024-01-24 15:31:06 +01:00
Jonathan de Jong
de60322711 Retry 401 errors on replies fetching (#28788)
Co-authored-by: Claire <claire.github-309c@sitedethib.com>
2024-01-24 15:31:06 +01:00
Jeong Arm
90bb870680 Ignore RecordNotUnique errors in LinkCrawlWorker (#28748) 2024-01-24 15:31:06 +01:00
Claire
9292d998fe Fix Mastodon not correctly processing HTTP Signatures with query strings (#28476) 2024-01-24 15:31:06 +01:00
Claire
92643f48de Convert signature verification specs to request specs (#28443) 2024-01-24 15:31:06 +01:00
Claire
458620bdd4 Fix potential redirection loop of streaming endpoint (#28665) 2024-01-24 15:31:06 +01:00
Claire
a1a71263e0 Fix streaming API redirection ignoring the port of streaming_api_base_url (#28558) 2024-01-24 15:31:06 +01:00
MitarashiDango
4c5575e8e0 Fix Undo Announce activity is not sent, when not followed by the reblogged post author (#18482)
Co-authored-by: Claire <claire.github-309c@sitedethib.com>
2024-01-24 15:31:06 +01:00
Claire
a2ddd849e2 Fix LinkCrawlWorker error when encountering empty OEmbed response (#28268) 2024-01-24 15:31:06 +01:00
Claire
2e4d43933d
Fix SQL query in /api/v1/directory (#28412) 2023-12-18 11:03:20 +01:00
Claire
363bedd050 Bump version to v4.1.11 2023-12-04 15:28:02 +01:00
Claire
cc94c70970 Clamp dates when serializing to Elasticsearch API (#28081) 2023-12-04 15:28:02 +01:00
Claire
613d00706c Change GIF max matrix size error to explicitly mention GIF files (#27927) 2023-12-04 15:28:02 +01:00
Jonathan de Jong
8bbe2b970f Have Follow activities bypass availability (#27586)
Co-authored-by: Claire <claire.github-309c@sitedethib.com>
2023-12-04 15:28:02 +01:00
Claire
803e15a3cf Fix incoming status creation date not being restricted to standard ISO8601 (#27655) 2023-12-04 15:28:02 +01:00
Claire
1d835c9423 Fix posts from force-sensitized accounts being able to trend (#27620) 2023-12-04 15:28:02 +01:00
Claire
ab68df9af0 Fix hashtag matching pattern matching some URLs (#27584) 2023-12-04 15:28:02 +01:00
Claire
a89a25714d Fix some link anchors being recognized as hashtags (#27271) 2023-12-04 15:28:02 +01:00
Claire
1210524a3d Fix processing LDSigned activities from actors with unknown public keys (#27474) 2023-12-04 15:28:02 +01:00
Claire
ff3a9dad0d Fix error and incorrect URLs in /api/v1/accounts/:id/featured_tags for remote accounts (#27459) 2023-12-04 15:28:02 +01:00
Claire
3ef0a19bac Fix report processing notice not mentioning the report number when performing a custom action (#27442) 2023-12-04 15:28:02 +01:00
Claire
78e457614c Change Content-Security-Policy to be tighter on media paths (#26889) 2023-12-04 15:28:02 +01:00
393ae412db
Merge pull request #127 from Y-zu-don-maintenance-org/features/4.1.9
4.1.10
2023-10-11 18:15:31 +09:00
45b7276b9f Merge tag 'v4.1.10' into features/4.1.9 2023-10-11 18:14:30 +09:00
70cf68fc6e Merge tag 'v4.1.9' into features/4.1.9 2023-10-11 18:13:24 +09:00
Claire
1e896e99d2
Update dependencies (#27354) 2023-10-10 15:32:42 +02:00
Claire
df60d04dc1 Bump version to v4.1.10 2023-10-10 13:51:56 +02:00
Matt Jankowski
335982325e Dont match mention in url query string (#25656)
Co-authored-by: Claire <claire.github-309c@sitedethib.com>
2023-10-10 13:51:56 +02:00
Claire
15c5727f71 Add a short-lived lock to trend refresh scheduler (#27253) 2023-10-10 13:51:56 +02:00
David Aaron
f8154cf732 Change min age of backup policy from 1 week to 6 days (#27200) 2023-10-10 13:51:56 +02:00
Jakob Gillich
45669ac5e6 Fix importer returning negative row estimates (#27258) 2023-10-10 13:51:56 +02:00
Claire
8d73fbee87 Change some worker lock TTLs (#27246) 2023-10-10 13:51:56 +02:00
Claire
f1d3eda159 Fix filtering audit log for entries about disabling 2FA (#27186) 2023-10-10 13:51:56 +02:00
Essem
c97fbabb61 Properly remove tIME chunk from PNG uploads (#27111) 2023-10-10 13:51:56 +02:00
Claire
f2fff6be66 Fix crash when filtering for “dormant” relationships (#27306) 2023-10-10 13:51:56 +02:00
Claire
b40c42fd1e Fix inefficient queries in “Follows and followers” as well as several admin pages (#27116) 2023-10-10 13:51:56 +02:00
Claire
9950e59578
Disable setting the latest tag for 4.1 docker builds (#27023) 2023-09-21 18:14:24 +02:00
Claire
e4c0aaf626
Bump version to v4.1.9 (#26997) 2023-09-20 17:25:05 +02:00
Claire
5d93c5f019
Fix post translation erroring out (v4.1.x) (#26990) 2023-09-20 15:59:57 +02:00
Claire
af0ee12908
Disable ruby linting for 4.1.x branch (#26993) 2023-09-20 12:54:08 +02:00
Claire
46bd58f74d Bump version to v4.1.8 2023-09-19 17:01:44 +02:00
Claire
d6c0ae995c Fix post edits not being forwarded as expected (#26936) 2023-09-19 17:01:44 +02:00
Claire
5fd89e53d2 Fix moderator rights inconsistencies (#26729) 2023-09-19 17:01:44 +02:00
Claire
5caade9fb0 Fix crash when encountering invalid URL (#26814) 2023-09-19 17:01:44 +02:00
Claire
34959eccd2 Fix cached posts including stale stats (#26409) 2023-09-19 17:01:44 +02:00
Nicolai Søborg
21bf42bca1 Fix frame_rate for videos where ffprobe reports 0/0 (#26500) 2023-09-19 17:01:44 +02:00
yufushiro
7802837885 Fix unexpected audio stream transcoding when uploaded video is eligible to passthrough (#26608)
Co-authored-by: Claire <claire.github-309c@sitedethib.com>
2023-09-19 17:01:44 +02:00
Claire
48ee3ae13d
Merge pull request from GHSA-v3xf-c9qf-j667 2023-09-19 16:53:58 +02:00
Claire
5f9511c389
Merge pull request from GHSA-2693-xr3m-jhqr 2023-09-19 16:53:21 +02:00
Claire
38a5d92f38
Change Dockerfile to upgrade packages when building (#26929)
Co-authored-by: Renaud Chaput <renchap@gmail.com>
2023-09-18 08:32:04 +02:00
Claire
7f7e068975
Update actions for stable-4.1 (#26815)
Co-authored-by: Renaud Chaput <renchap@gmail.com>
2023-09-06 12:19:02 +02:00
Claire
5f88a2d70b Bump version to v4.1.7 2023-09-05 19:16:09 +02:00
Emelia Smith
cf80d54cba Allow reports with long comments from remote instances, but truncate (#25028) 2023-09-05 19:16:09 +02:00
Daniel M Brasil
ea7fa048f3 Fix /api/v1/timelines/tag/:hashtag allowing for unauthenticated access when public preview is disabled (#26237) 2023-09-05 19:16:09 +02:00
Claire
6339806f05 Fix blocking subdomains of an already-blocked domain (#26392) 2023-09-05 19:16:09 +02:00
Claire
86afbf25d0 Change text extraction in PlainTextFormatter to be faster (#26727) 2023-09-05 19:16:09 +02:00
Claire
1ad64b5557
Backport container build changes to the stable-4.1 branch (#26738)
Co-authored-by: Renaud Chaput <renchap@gmail.com>
2023-08-31 19:54:10 +02:00
Claire
ac7d40b561 Bump version to v4.1.6 2023-07-31 14:33:06 +02:00
Renaud Chaput
2fc6117d1b Fix missing return values in streaming (#26233) 2023-07-31 14:33:06 +02:00
Emelia Smith
2eb1a5b7b6 Fix: Streaming server memory leak in HTTP EventSource cleanup (#26228) 2023-07-31 14:33:06 +02:00
Claire
6c321bb5e1 Fix incorrect connect timeout in outgoing requests (#26116) 2023-07-31 14:33:06 +02:00
Emelia Smith
da230600ac Refactor streaming's filtering logic & improve documentation (#26213) 2023-07-31 14:33:06 +02:00
Claire
1792be342a Fix wrong filters sometimes applying in streaming (#26159) 2023-07-31 14:33:06 +02:00
Claire
ebf4f034c2 Bump version to v4.1.5 2023-07-21 16:07:43 +02:00
Claire
889102013f Fix CSP headers being unintendedly wide (#26105) 2023-07-21 16:07:43 +02:00
Claire
d94a2c8aca Change request timeout handling to use a longer deadline (#26055) 2023-07-21 16:07:43 +02:00
Claire
efd066670d Fix moderation interface for remote instances with a .zip TLD (#25885) 2023-07-21 16:07:43 +02:00
Claire
13ec425b72 Fix remote accounts being possibly persisted to database with incomplete protocol values (#25886) 2023-07-21 16:07:43 +02:00
Michael Stanclift
7a99f0744d Fix trending publishers table not rendering correctly on narrow screens (#25945) 2023-07-21 16:07:43 +02:00
Claire
69c8f26946
Add check preventing Sidekiq workers from running with Makara configured (#25850)
Co-authored-by: Eugen Rochko <eugen@zeonfederated.com>
2023-07-21 14:18:04 +02:00
2c3b5a9d0c fix show hints 2023-07-10 16:20:49 +00:00
d646011a17 remove setting_show_tab_bar_label 2023-07-10 16:16:13 +00:00
090f82f070
Merge pull request #120 from Y-zu-don-maintenance-org/features/v4.1.4
Features/v4.1.4
2023-07-08 23:41:57 +09:00
Claire
3f5af768c8 Bump version to v4.1.4 2023-07-07 19:37:21 +02:00
Claire
cb8ab46302 Update dependencies 2023-07-07 19:37:21 +02:00
Claire
53b979d5c7 Fix processing of media files with unusual names (#25788) 2023-07-07 19:37:21 +02:00
Claire
f2bbac3f9f Fix crash in admin interface when viewing a remote user with verified links (#25796) 2023-07-07 19:37:21 +02:00
Claire
015ed99612 Fix branding:generate_app_icons failing because of disallowed ICO coder (#25794) 2023-07-07 19:37:21 +02:00
nemobis
cf58535193 Fix typo in CHANGELOG.md (#25764) 2023-07-07 19:37:21 +02:00
ff17262aff
Merge pull request #119 from Y-zu-don-maintenance-org/features/v4.1.3
Features/v4.1.3
2023-07-06 22:39:03 +09:00
Claire
0d5781ca76 Bump version to v4.1.3 2023-07-06 15:07:20 +02:00
Claire
32ebeed59b
Merge pull request from GHSA-55j9-c3mp-6fcq 2023-07-06 15:06:50 +02:00
Claire
e75ad1de0f
Merge pull request from GHSA-9pxv-6qvf-pjwc
* Fix timeout handling of outbound HTTP requests

* Use CLOCK_MONOTONIC instead of Time.now
2023-07-06 15:06:24 +02:00
Claire
0aa0b71f2c
Merge pull request from GHSA-9928-3cp5-93fm
* Fix attachments getting processed despite failing content-type validation

* Add a restrictive ImageMagick security policy tailored for Mastodon

* Fix misdetection of MP3 files with large cover art

* Reject unprocessable audio/video files instead of keeping them unchanged
2023-07-06 15:05:05 +02:00
Claire
c4f2609f7a
Merge pull request from GHSA-ccm4-vgcc-73hp
* Tighten allowed HTML in oEmbed-based preview cards

* Sanitize preview cards at render time

* Add `sandbox` attribute to preview card iframes
2023-07-06 15:03:33 +02:00
Claire
9b6c0cac7d Add hardened headers to user-uploaded files (#25756) 2023-07-06 14:32:26 +02:00
Claire
fac2c9eb7d Update rack, rails, nokogiri and doorkeeper gems 2023-07-06 13:45:40 +02:00
Claire
a3d69a2c5d Fix OAuth apps page crashing when listing apps with certain admin API scopes (#25713) 2023-07-06 13:45:40 +02:00
Renaud Chaput
8eb1bb8ba6 Allow carets in URL search params (#25216) 2023-07-06 13:45:40 +02:00
Vyr Cossont
652ff76462 Fix Redis client and type errors introduced in #24285 (#24342) 2023-07-06 13:45:40 +02:00
Vyr Cossont
6f484fbbd2 IndexingScheduler: fetch and import in batches (#24285)
Co-authored-by: Claire <claire.github-309c@sitedethib.com>
2023-07-06 13:45:40 +02:00
Claire
79f5b8f156 Fix ResolveURLService not resolving local URLs for remote content (#25637) 2023-07-06 13:45:40 +02:00
Claire
f8930a67a0 Change /api/v1/statuses/:id/history to always return at least one item (#25510) 2023-07-06 13:45:40 +02:00
Claire
e65e3a6d14 Add finer permission requirements for managing webhooks (#25463) 2023-07-06 13:45:40 +02:00
Claire
8acbfc6ab1 Fix wrong view being displayed when a webhook fails validation (#25464) 2023-07-06 13:45:40 +02:00
Emelia Smith
3ef53958b2 Prevent UserCleanupScheduler from overwhelming streaming (#25519) 2023-07-06 13:45:40 +02:00
Daniel M Brasil
fd1ffd72eb Fix incorrect pagination headers in /api/v2/admin/accounts (#25477) 2023-07-06 13:45:40 +02:00
Claire
7bd34f8b23 Fix infinite loop in AccountsStatusesCleanupScheduler (#24840) 2023-07-06 13:45:40 +02:00
Claire
7012bf6ed3 Improve automatic post cleanup worker performances (#24785) 2023-07-06 13:45:40 +02:00
Claire
d9e45f2fa9 Fix AccountsStatusesCleanupScheduler not spreading deletes across accounts correctly (#24607) 2023-07-06 13:45:40 +02:00
Claire
0e139e3c4d Change automatic post deletion thresholds and load detection (#24614) 2023-07-06 13:45:40 +02:00
Emelia Smith
23e7b4d28d Fix logging of messages that are binary before closing their connection (#25361) 2023-07-06 13:45:40 +02:00
Emelia Smith
e78ee582f7 Fix performance of streaming by parsing message JSON once (#25278) 2023-07-06 13:45:40 +02:00
Claire
a197fc094f Fix CSP headers when S3_ALIAS_HOST includes a path component (#25273) 2023-07-06 13:45:40 +02:00
Daniel M Brasil
bd7cbeeadf Fix tootctl accounts approve --number N not aproving N earliest registrations (#24605) 2023-07-06 13:45:40 +02:00
Claire
2779bce9a2 Add fallback redirection when getting a webfinger query LOCAL_DOMAIN@LOCAL_DOMAIN (#23600)
Co-authored-by: Eugen Rochko <eugen@zeonfederated.com>
2023-07-06 13:45:40 +02:00
Claire
210ff36860 Change AccessTokensVacuum to also delete expired tokens (#24868) 2023-07-06 13:45:40 +02:00
Claire
99c2bbbec9 Change profile updates to be sent to recently-mentioned servers (#24852) 2023-07-06 13:45:40 +02:00
Claire
7e58779300 Fix reports not being closed when performing batch suspensions (#24988) 2023-07-06 13:45:40 +02:00
Claire
cca464bce3 Fix being able to vote on your own polls (#25015) 2023-07-06 13:45:40 +02:00
Claire
1301af60e0 Fix race condition when reblogging a status (#25016) 2023-07-06 13:45:40 +02:00
Claire
f962e83856 Change OpenGraph-based embeds to allow fullscreen (#25058) 2023-07-06 13:45:40 +02:00
Claire
b3cbcd7447 Fix “Authorized applications” inefficiently and incorrectly getting last use date (#25060) 2023-07-06 13:45:40 +02:00
Claire
72d96bf17a Remove invalid X-Frame-Options: ALLOWALL (#25070) 2023-07-06 13:45:40 +02:00
Claire
b1ac3562df Change Identity to not destroy associated User on destroy (#25098) 2023-07-06 13:45:40 +02:00
Claire
4c6c790f80 Fix /api/v1/conversations sometimes returning empty accounts (#25499) 2023-07-06 13:45:40 +02:00
Claire
036ac5b5c9 Fix ArgumentError when loading newer Private Mentions (#25399) 2023-07-06 13:45:40 +02:00
Claire
3e1724e972 Fix multiple N+1s in ConversationsController (#25134) 2023-07-06 13:45:40 +02:00
Claire
bc8592627b Fix user archive takeouts when using OpenStack Swift (#24431) 2023-07-06 13:45:40 +02:00
ca0dbf2c50
Merge pull request #118 from Y-zu-don-maintenance-org/features/v4.1.2
add pinktheme
2023-07-04 15:13:05 +09:00
0f7e94a055 add pinktheme 2023-07-04 06:11:29 +00:00
9667505b46
Merge pull request #117 from Y-zu-don-maintenance-org/features/v4.1.2
change favicon
2023-07-02 21:16:02 +09:00
c5e6544f6a change favicon 2023-07-02 12:14:36 +00:00
7a78300faf
Delete dependabot.yml 2023-07-02 18:08:02 +09:00
338733ac10
Merge pull request #116 from Y-zu-don-maintenance-org/features/v4.1.2
fix:カラムを狭くするとメニューが表示されない問題
2023-07-02 17:19:43 +09:00
af554529f5 fix:カラムを狭くするとメニューが表示されない問題 2023-07-02 08:18:55 +00:00
6ee650bd26
Merge pull request #115 from Y-zu-don-maintenance-org/features/v4.1.2
bug fix
2023-07-02 17:10:14 +09:00
9f9e3234dd 不要な文章を削除 2023-07-02 08:09:16 +00:00
4fe20bae9c fix:シングルカラムで崩れる問題 2023-07-02 08:09:04 +00:00
169b80234e MastodonロゴをYづドンのロゴに変更 2023-07-02 06:52:19 +00:00
d05a0c8fa3 下タブバーの実装 2023-07-02 06:22:56 +00:00
d5ef4dff60 update favicon to y-zu color 2023-06-30 21:16:24 +09:00
0923806964 update missing.png to haruhi 2023-06-30 21:12:40 +09:00
1b02b4bfde Merge remote-tracking branch 'accelforce/custom/quote' into features/v4.1.2 2023-06-30 20:52:31 +09:00
a221c8e874 UtilBtnsの縦スペースを狭く 2023-06-30 20:33:51 +09:00
fbd972e447 fix risa 2023-06-30 20:31:28 +09:00
90d36dd2a0 Update risa 2023-06-30 20:26:43 +09:00
1e32a46edd りさ姉ボタンの実装 2023-06-30 20:21:43 +09:00
eb6f1f0826 fix:焼却ボタン類が崩れる問題 2023-06-30 20:13:10 +09:00
cce2fb6b96 誤字盛ボタン・はるきん焼却ボタンの実装 2023-06-30 20:05:50 +09:00
158f9604ea 投稿 to トゥート 2023-06-30 19:52:56 +09:00
0770a48d0a 2048文字に対応 2023-06-30 19:21:28 +09:00
Claire
4b9e4f6398 Bump version to v4.1.2 2023-04-04 12:41:27 +02:00
Claire
b9f271364e Fix unescaped user input in LDAP query (#24379)
Fix CVE-2023-28853
2023-04-04 12:41:27 +02:00
Claire
4eaa6d58b2 Change root Chewy strategy to emit a warning instead of erroring out in production mode (#24327) 2023-04-04 12:41:27 +02:00
Claire
51572ac615 Fix invalid/expired invites being processed on sign-up (#24337) 2023-04-04 12:41:27 +02:00
Sai
01617534fa Update Ruby to 3.0.6 (#24334) 2023-04-04 12:41:27 +02:00
Robert R George
af6eb37c70 Wrap db:setup with Chewy.strategy(:mastodon) (#24302) 2023-04-04 12:41:27 +02:00
Eugen Rochko
590df443f1 Bump blurhash from 0.1.6 to 0.1.7 (#23517) 2023-04-04 12:41:27 +02:00
Claire
ae64c5b7ec Fix user archive takeout when using OpenStack Swift or S3 providers with no ACL support (#24200) 2023-04-04 12:41:27 +02:00
Claire
3c82c4e780 Fix crash in tootctl commands making use of parallelization when Elasticsearch is enabled (#24182) 2023-04-04 12:41:27 +02:00
kyori19
fa7943f5e5
Fix overlapping avatar in quote container 2022-11-15 15:51:50 +00:00
kyori19
8559edcee5
Merge remote-tracking branch 'mastodon/main' into custom/quote 2022-11-15 13:45:34 +00:00
kyori19
ce62e633c8
Adjust avatar display inside quote container 2022-11-15 13:41:36 +00:00
kyori19
5d59901b8d
Fix removed otherAccounts referred in status.js 2022-11-15 05:25:11 +00:00
kyori19
ae2190b9a5
Merge remote-tracking branch 'mastodon/main' into custom/quote
# Conflicts:
#	app/javascript/mastodon/components/status.js
#	app/javascript/mastodon/components/status_action_bar.js
#	app/javascript/mastodon/components/status_content.js
#	app/javascript/mastodon/containers/timeline_container.js
#	app/javascript/mastodon/features/status/components/detailed_status.js
#	app/javascript/styles/mastodon/components.scss
2022-11-15 01:48:20 +00:00
kyori19
4be8ece78d
Merge remote-tracking branch 'mastodon/main' into custom/quote 2022-11-05 19:56:50 +00:00
kyori19
bef7f9a21c
Fix quote inline not hidden on WebUI 2022-05-11 14:05:11 +00:00
kyori19
ac517abef2
Fix append_quote uses TagManager instead of ActivityPub::TagManager 2022-05-11 04:58:20 +00:00
kyori19
0c206d8711
Merge remote-tracking branch 'tootsuite/main' into custom/quote 2022-05-10 02:47:45 +00:00
kyori19
faf1aadad2
Fix NoMethodError 2022-02-27 04:50:03 +00:00
kyori19
c15001381b
Merge remote-tracking branch 'tootsuite/main' into custom/quote
# Conflicts:
#	app/controllers/api/v1/statuses_controller.rb
#	app/javascript/mastodon/actions/compose.js
#	app/javascript/mastodon/actions/notifications.js
#	app/javascript/mastodon/components/status_action_bar.js
#	app/javascript/mastodon/containers/status_container.js
#	app/javascript/mastodon/features/compose/containers/reply_indicator_container.js
#	app/javascript/mastodon/reducers/compose.js
2022-02-27 02:32:06 +00:00
kyori19
725569e8ab
Merge remote-tracking branch 'tootsuite/main' into custom/quote 2022-02-03 13:31:02 +00:00
kyori19
71530857a5
Merge remote-tracking branch 'tootsuite/main' into custom/quote
# Conflicts:
#	app/services/fetch_link_card_service.rb
2022-02-01 16:28:14 +00:00
kyori19
e7eab1a6ae
Merge remote-tracking branch 'tootsuite/main' into custom/quote
# Conflicts:
#	app/javascript/mastodon/components/status.js
#	app/javascript/mastodon/features/status/components/detailed_status.js
#	app/serializers/rest/instance_serializer.rb
2021-10-12 16:53:07 +00:00
kyori19
043ab77449 Merge remote-tracking branch 'tootsuite/main' into custom/quote 2021-07-09 04:27:34 +00:00
kyori19
2aa6ec14fe Implement quote feature 2021-01-23 03:03:21 +09:00
350 changed files with 6067 additions and 2017 deletions

View File

@ -1,225 +0,0 @@
version: 2.1
orbs:
ruby: circleci/ruby@2.0.0
node: circleci/node@5.0.3
executors:
default:
parameters:
ruby-version:
type: string
docker:
- image: cimg/ruby:<< parameters.ruby-version >>
environment:
BUNDLE_JOBS: 3
BUNDLE_RETRY: 3
CONTINUOUS_INTEGRATION: true
DB_HOST: localhost
DB_USER: root
DISABLE_SIMPLECOV: true
RAILS_ENV: test
- image: cimg/postgres:14.5
environment:
POSTGRES_USER: root
POSTGRES_HOST_AUTH_METHOD: trust
- image: cimg/redis:7.0
commands:
install-system-dependencies:
steps:
- run:
name: Install system dependencies
command: |
sudo apt-get update
sudo apt-get install -y libicu-dev libidn11-dev
install-ruby-dependencies:
parameters:
ruby-version:
type: string
steps:
- run:
command: |
bundle config clean 'true'
bundle config frozen 'true'
bundle config without 'development production'
name: Set bundler settings
- ruby/install-deps:
bundler-version: '2.3.26'
key: ruby<< parameters.ruby-version >>-gems-v1
wait-db:
steps:
- run:
command: dockerize -wait tcp://localhost:5432 -wait tcp://localhost:6379 -timeout 1m
name: Wait for PostgreSQL and Redis
jobs:
build:
docker:
- image: cimg/ruby:3.0-node
environment:
RAILS_ENV: test
steps:
- checkout
- install-system-dependencies
- install-ruby-dependencies:
ruby-version: '3.0'
- node/install-packages:
cache-version: v1
pkg-manager: yarn
- run:
command: |
export NODE_OPTIONS=--openssl-legacy-provider
./bin/rails assets:precompile
name: Precompile assets
- persist_to_workspace:
paths:
- public/assets
- public/packs-test
root: .
test:
parameters:
ruby-version:
type: string
executor:
name: default
ruby-version: << parameters.ruby-version >>
environment:
ALLOW_NOPAM: true
PAM_ENABLED: true
PAM_DEFAULT_SERVICE: pam_test
PAM_CONTROLLED_SERVICE: pam_test_controlled
parallelism: 4
steps:
- checkout
- install-system-dependencies
- run:
command: sudo apt-get install -y ffmpeg imagemagick libpam-dev
name: Install additional system dependencies
- run:
command: bundle config with 'pam_authentication'
name: Enable PAM authentication
- install-ruby-dependencies:
ruby-version: << parameters.ruby-version >>
- attach_workspace:
at: .
- wait-db
- run:
command: ./bin/rails db:create db:schema:load db:seed
name: Load database schema
- ruby/rspec-test
test-migrations:
executor:
name: default
ruby-version: '3.0'
steps:
- checkout
- install-system-dependencies
- install-ruby-dependencies:
ruby-version: '3.0'
- wait-db
- run:
command: ./bin/rails db:create
name: Create database
- run:
command: ./bin/rails db:migrate VERSION=20171010025614
name: Run migrations up to v2.0.0
- run:
command: ./bin/rails tests:migrations:populate_v2
name: Populate database with test data
- run:
command: ./bin/rails db:migrate VERSION=20180514140000
name: Run migrations up to v2.4.0
- run:
command: ./bin/rails tests:migrations:populate_v2_4
name: Populate database with test data
- run:
command: ./bin/rails db:migrate VERSION=20180707154237
name: Run migrations up to v2.4.3
- run:
command: ./bin/rails tests:migrations:populate_v2_4_3
name: Populate database with test data
- run:
command: ./bin/rails db:migrate
name: Run all remaining migrations
- run:
command: ./bin/rails tests:migrations:check_database
name: Check migration result
test-two-step-migrations:
executor:
name: default
ruby-version: '3.0'
steps:
- checkout
- install-system-dependencies
- install-ruby-dependencies:
ruby-version: '3.0'
- wait-db
- run:
command: ./bin/rails db:create
name: Create database
- run:
command: ./bin/rails db:migrate VERSION=20171010025614
name: Run migrations up to v2.0.0
- run:
command: ./bin/rails tests:migrations:populate_v2
name: Populate database with test data
- run:
command: ./bin/rails db:migrate VERSION=20180514140000
name: Run pre-deployment migrations up to v2.4.0
environment:
SKIP_POST_DEPLOYMENT_MIGRATIONS: true
- run:
command: ./bin/rails tests:migrations:populate_v2_4
name: Populate database with test data
- run:
command: ./bin/rails db:migrate VERSION=20180707154237
name: Run migrations up to v2.4.3
environment:
SKIP_POST_DEPLOYMENT_MIGRATIONS: true
- run:
command: ./bin/rails tests:migrations:populate_v2_4_3
name: Populate database with test data
- run:
command: ./bin/rails db:migrate
name: Run all remaining pre-deployment migrations
environment:
SKIP_POST_DEPLOYMENT_MIGRATIONS: true
- run:
command: ./bin/rails db:migrate
name: Run all post-deployment migrations
- run:
command: ./bin/rails tests:migrations:check_database
name: Check migration result
workflows:
version: 2
build-and-test:
jobs:
- build
- test:
matrix:
parameters:
ruby-version:
- '2.7'
- '3.0'
name: test-ruby<< matrix.ruby-version >>
requires:
- build
- test-migrations:
requires:
- build
- test-two-step-migrations:
requires:
- build
- node/run:
cache-version: v1
name: test-webui
pkg-manager: yarn
requires:
- build
version: '16.18'
yarn-run: test:jest

View File

@ -1,30 +0,0 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: npm
directory: '/'
schedule:
interval: weekly
open-pull-requests-limit: 99
allow:
- dependency-type: direct
- package-ecosystem: bundler
directory: '/'
schedule:
interval: weekly
open-pull-requests-limit: 99
allow:
- dependency-type: direct
- package-ecosystem: github-actions
directory: '/'
schedule:
interval: weekly
open-pull-requests-limit: 99
allow:
- dependency-type: direct

View File

@ -0,0 +1,92 @@
on:
workflow_call:
inputs:
platforms:
required: true
type: string
cache:
type: boolean
default: true
use_native_arm64_builder:
type: boolean
push_to_images:
type: string
flavor:
type: string
tags:
type: string
labels:
type: string
jobs:
build-image:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: docker/setup-qemu-action@v2
if: contains(inputs.platforms, 'linux/arm64') && !inputs.use_native_arm64_builder
- uses: docker/setup-buildx-action@v2
id: buildx
if: ${{ !(inputs.use_native_arm64_builder && contains(inputs.platforms, 'linux/arm64')) }}
- name: Start a local Docker Builder
if: inputs.use_native_arm64_builder && contains(inputs.platforms, 'linux/arm64')
run: |
docker run --rm -d --name buildkitd -p 1234:1234 --privileged moby/buildkit:latest --addr tcp://0.0.0.0:1234
- uses: docker/setup-buildx-action@v2
id: buildx-native
if: inputs.use_native_arm64_builder && contains(inputs.platforms, 'linux/arm64')
with:
driver: remote
endpoint: tcp://localhost:1234
platforms: linux/amd64
append: |
- endpoint: tcp://${{ vars.DOCKER_BUILDER_HETZNER_ARM64_01_HOST }}:13865
platforms: linux/arm64
name: mastodon-docker-builder-arm64-01
driver-opts:
- servername=mastodon-docker-builder-arm64-01
env:
BUILDER_NODE_1_AUTH_TLS_CACERT: ${{ secrets.DOCKER_BUILDER_HETZNER_ARM64_01_CACERT }}
BUILDER_NODE_1_AUTH_TLS_CERT: ${{ secrets.DOCKER_BUILDER_HETZNER_ARM64_01_CERT }}
BUILDER_NODE_1_AUTH_TLS_KEY: ${{ secrets.DOCKER_BUILDER_HETZNER_ARM64_01_KEY }}
- name: Log in to Docker Hub
if: contains(inputs.push_to_images, 'tootsuite')
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Log in to the Github Container registry
if: contains(inputs.push_to_images, 'ghcr.io')
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/metadata-action@v4
id: meta
if: ${{ inputs.push_to_images != '' }}
with:
images: ${{ inputs.push_to_images }}
flavor: ${{ inputs.flavor }}
tags: ${{ inputs.tags }}
labels: ${{ inputs.labels }}
- uses: docker/build-push-action@v4
with:
context: .
platforms: ${{ inputs.platforms }}
provenance: false
builder: ${{ steps.buildx.outputs.name || steps.buildx-native.outputs.name }}
push: ${{ inputs.push_to_images != '' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: ${{ inputs.cache && 'type=gha' || '' }}
cache-to: ${{ inputs.cache && 'type=gha,mode=max' || '' }}

View File

@ -1,70 +0,0 @@
name: Build container image
on:
workflow_dispatch:
push:
branches:
- 'main'
tags:
- '*'
pull_request:
paths:
- .github/workflows/build-image.yml
- Dockerfile
permissions:
contents: read
packages: write
jobs:
build-image:
runs-on: ubuntu-latest
concurrency:
group: ${{ github.ref }}
cancel-in-progress: true
steps:
- uses: actions/checkout@v3
- uses: hadolint/hadolint-action@v3.1.0
- uses: docker/setup-qemu-action@v2
- uses: docker/setup-buildx-action@v2
- name: Log in to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
if: github.repository == 'mastodon/mastodon' && github.event_name != 'pull_request'
- name: Log in to the Github Container registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
if: github.repository == 'mastodon/mastodon' && github.event_name != 'pull_request'
- uses: docker/metadata-action@v4
id: meta
with:
images: |
tootsuite/mastodon
ghcr.io/mastodon/mastodon
flavor: |
latest=auto
tags: |
type=edge,branch=main
type=pep440,pattern={{raw}}
type=pep440,pattern=v{{major}}.{{minor}}
type=ref,event=pr
- uses: docker/build-push-action@v4
with:
context: .
platforms: linux/amd64,linux/arm64
provenance: false
builder: ${{ steps.buildx.outputs.name }}
push: ${{ github.repository == 'mastodon/mastodon' && github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

27
.github/workflows/build-releases.yml vendored Normal file
View File

@ -0,0 +1,27 @@
name: Build container release images
on:
push:
tags:
- '*'
permissions:
contents: read
packages: write
jobs:
build-image:
uses: ./.github/workflows/build-container-image.yml
with:
platforms: linux/amd64,linux/arm64
use_native_arm64_builder: true
push_to_images: |
tootsuite/mastodon
ghcr.io/mastodon/mastodon
# Do not use cache when building releases, so apt update is always ran and the release always contain the latest packages
cache: false
flavor: |
latest=false
tags: |
type=pep440,pattern={{raw}}
type=pep440,pattern=v{{major}}.{{minor}}
secrets: inherit

View File

@ -1,41 +0,0 @@
name: Ruby Linting
on:
push:
branches-ignore:
- 'dependabot/**'
paths:
- 'Gemfile*'
- '.rubocop.yml'
- '**/*.rb'
- '**/*.rake'
- '.github/workflows/lint-ruby.yml'
pull_request:
paths:
- 'Gemfile*'
- '.rubocop.yml'
- '**/*.rb'
- '**/*.rake'
- '.github/workflows/lint-ruby.yml'
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set-up RuboCop Problem Mathcher
uses: r7kamura/rubocop-problem-matchers-action@v1
- name: Run rubocop
uses: github/super-linter@v4
env:
DEFAULT_BRANCH: main
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
LINTER_RULES_PATH: .
RUBY_CONFIG_FILE: .rubocop.yml
VALIDATE_ALL_CODEBASE: false
VALIDATE_RUBY: true

15
.github/workflows/test-image-build.yml vendored Normal file
View File

@ -0,0 +1,15 @@
name: Test container image build
on:
pull_request:
permissions:
contents: read
jobs:
build-image:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
uses: ./.github/workflows/build-container-image.yml
with:
platforms: linux/amd64 # Testing only on native platform so it is performant

153
.github/workflows/test-ruby.yml vendored Normal file
View File

@ -0,0 +1,153 @@
name: Ruby Testing
on:
push:
branches-ignore:
- 'dependabot/**'
- 'renovate/**'
pull_request:
env:
BUNDLE_CLEAN: true
BUNDLE_FROZEN: true
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
strategy:
fail-fast: true
matrix:
mode:
- production
- test
env:
RAILS_ENV: ${{ matrix.mode }}
BUNDLE_WITH: ${{ matrix.mode }}
OTP_SECRET: precompile_placeholder
SECRET_KEY_BASE: precompile_placeholder
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v3
with:
cache: yarn
node-version-file: '.nvmrc'
- name: Install native Ruby dependencies
run: |
sudo apt-get update
sudo apt-get install -y libicu-dev libidn11-dev
- name: Set up bundler cache
uses: ruby/setup-ruby@v1
with:
ruby-version: .ruby-version
bundler-cache: true
- run: yarn --frozen-lockfile --production
- name: Precompile assets
# Previously had set this, but it's not supported
# export NODE_OPTIONS=--openssl-legacy-provider
run: |-
./bin/rails assets:precompile
- uses: actions/upload-artifact@v3
if: matrix.mode == 'test'
with:
path: |-
./public/assets
./public/packs-test
name: ${{ github.sha }}
retention-days: 0
test:
runs-on: ubuntu-latest
needs:
- build
services:
postgres:
image: postgres:14-alpine
env:
POSTGRES_PASSWORD: postgres
POSTGRES_USER: postgres
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
redis:
image: redis:7-alpine
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 6379:6379
env:
DB_HOST: localhost
DB_USER: postgres
DB_PASS: postgres
DISABLE_SIMPLECOV: true
RAILS_ENV: test
ALLOW_NOPAM: true
PAM_ENABLED: true
PAM_DEFAULT_SERVICE: pam_test
PAM_CONTROLLED_SERVICE: pam_test_controlled
OIDC_ENABLED: true
OIDC_SCOPE: read
SAML_ENABLED: true
CAS_ENABLED: true
BUNDLE_WITH: 'pam_authentication test'
CI_JOBS: ${{ matrix.ci_job }}/4
strategy:
fail-fast: false
matrix:
ruby-version:
- '.ruby-version'
ci_job:
- 1
- 2
- 3
- 4
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v3
with:
path: './public'
name: ${{ github.sha }}
- name: Update package index
run: sudo apt-get update
- name: Install native Ruby dependencies
run: sudo apt-get install -y libicu-dev libidn11-dev
- name: Install additional system dependencies
run: sudo apt-get install -y ffmpeg imagemagick libpam-dev
- name: Set up bundler cache
uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby-version}}
bundler-cache: true
- name: Load database schema
run: './bin/rails db:create db:schema:load db:seed'
- run: bin/rspec

View File

@ -1 +1 @@
3.0.4
3.0.6

View File

@ -3,6 +3,299 @@ Changelog
All notable changes to this project will be documented in this file.
## [4.1.18] - 2024-07-04
### Security
- Fix incorrect permission checking on multiple API endpoints ([GHSA-58x8-3qxw-6hm7](https://github.com/mastodon/mastodon/security/advisories/GHSA-58x8-3qxw-6hm7))
- Fix incorrect authorship checking when processing some activities (CVE-2024-37903, [GHSA-xjvf-fm67-4qc3](https://github.com/mastodon/mastodon/security/advisories/GHSA-xjvf-fm67-4qc3))
- Fix ongoing streaming sessions not being invalidated when application tokens get revoked ([GHSA-vp5r-5pgw-jwqx](https://github.com/mastodon/mastodon/security/advisories/GHSA-vp5r-5pgw-jwqx))
- Update dependencies
### Changed
- Change preview cards generation to skip unusually long URLs ([oneiros](https://github.com/mastodon/mastodon/pull/30854))
- Change search modifiers to be case-insensitive ([Gargron](https://github.com/mastodon/mastodon/pull/30865))
- Change `STATSD_ADDR` handling to emit a warning rather than crashing if the address is unreachable ([timothyjrogers](https://github.com/mastodon/mastodon/pull/30691))
- Change PWA start URL from `/home` to `/` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27377))
### Fixed
- Fix scheduled statuses scheduled in less than 5 minutes being immediately published ([danielmbrasil](https://github.com/mastodon/mastodon/pull/30584))
- Fix encoding detection for link cards ([oneiros](https://github.com/mastodon/mastodon/pull/30780))
- Fix `/admin/accounts/:account_id/statuses/:id` for edited posts with media attachments ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/30819))
## [4.1.17] - 2024-05-30
### Security
- Update dependencies
- Fix private mention filtering ([GHSA-5fq7-3p3j-9vrf](https://github.com/mastodon/mastodon/security/advisories/GHSA-5fq7-3p3j-9vrf))
- Fix password change endpoint not being rate-limited ([GHSA-q3rg-xx5v-4mxh](https://github.com/mastodon/mastodon/security/advisories/GHSA-q3rg-xx5v-4mxh))
- Add hardening around rate-limit bypass ([GHSA-c2r5-cfqr-c553](https://github.com/mastodon/mastodon/security/advisories/GHSA-c2r5-cfqr-c553))
### Added
- Add fallback redirection when getting a webfinger query `WEB_DOMAIN@WEB_DOMAIN` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28592))
- Add `digest` attribute to `Admin::DomainBlock` entity in REST API ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/29092))
### Removed
- Remove superfluous application-level caching in some controllers ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/29862))
### Fixed
- Fix leaking Elasticsearch connections in Sidekiq processes ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/30450))
- Fix language of remote posts not being recognized when using unusual casing ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/30403))
- Fix off-by-one in `tootctl media` commands ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/30306))
- Fix removal of allowed domains (in `LIMITED_FEDERATION_MODE`) not being recorded in the audit log ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/30125))
- Fix not being able to block a subdomain of an already-blocked domain through the API ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/30119))
- Fix `Idempotency-Key` being ignored when scheduling a post ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/30084))
- Fix crash when supplying the `FFMPEG_BINARY` environment variable ([timothyjrogers](https://github.com/mastodon/mastodon/pull/30022))
- Fix improper email address validation ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/29838))
- Fix results/query in `api/v1/featured_tags/suggestions` ([mjankowski](https://github.com/mastodon/mastodon/pull/29597))
- Fix unblocking internationalized domain names under certain conditions ([tribela](https://github.com/mastodon/mastodon/pull/29530))
- Fix admin account created by `mastodon:setup` not being auto-approved ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/29379))
- Fix reference to non-existent var in CLI maintenance command ([mjankowski](https://github.com/mastodon/mastodon/pull/28363))
## [4.1.16] - 2024-02-23
### Added
- Add hourly task to automatically require approval for new registrations in the absence of moderators ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/29318), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/29355))
In order to prevent future abandoned Mastodon servers from being used for spam, harassment and other malicious activity, Mastodon will now automatically switch new user registrations to require moderator approval whenever they are left open and no activity (including non-moderation actions from apps) from any logged-in user with permission to access moderation reports has been detected in a full week.
When this happens, users with the permission to change server settings will receive an email notification.
This feature is disabled when `EMAIL_DOMAIN_ALLOWLIST` is used, and can also be disabled with `DISABLE_AUTOMATIC_SWITCHING_TO_APPROVED_REGISTRATIONS=true`.
### Changed
- Change registrations to be closed by default on new installations ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/29280))
If you are running a server and never changed your registrations mode from the default, updating will automatically close your registrations.
Simply re-enable them through the administration interface or using `tootctl settings registrations open` if you want to enable them again.
### Fixed
- Fix processing of remote ActivityPub actors making use of `Link` objects as `Image` `url` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/29335))
- Fix link verifications when page size exceeds 1MB ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/29358))
## [4.1.15] - 2024-02-16
### Fixed
- Fix OmniAuth tests and edge cases in error handling ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/29201), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/29207))
### Security
- Fix insufficient checking of remote posts ([GHSA-jhrq-qvrm-qr36](https://github.com/mastodon/mastodon/security/advisories/GHSA-jhrq-qvrm-qr36))
## [4.1.14] - 2024-02-14
### Security
- Update the `sidekiq-unique-jobs` dependency (see [GHSA-cmh9-rx85-xj38](https://github.com/mhenrixon/sidekiq-unique-jobs/security/advisories/GHSA-cmh9-rx85-xj38))
In addition, we have disabled the web interface for `sidekiq-unique-jobs` out of caution.
If you need it, you can re-enable it by setting `ENABLE_SIDEKIQ_UNIQUE_JOBS_UI=true`.
If you only need to clear all locks, you can now use `bundle exec rake sidekiq_unique_jobs:delete_all_locks`.
- Update the `nokogiri` dependency (see [GHSA-xc9x-jj77-9p9j](https://github.com/sparklemotion/nokogiri/security/advisories/GHSA-xc9x-jj77-9p9j))
- Disable administrative Doorkeeper routes ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/29187))
- Fix ongoing streaming sessions not being invalidated when applications get deleted in some cases ([GHSA-7w3c-p9j8-mq3x](https://github.com/mastodon/mastodon/security/advisories/GHSA-7w3c-p9j8-mq3x))
In some rare cases, the streaming server was not notified of access tokens revocation on application deletion.
- Change external authentication behavior to never reattach a new identity to an existing user by default ([GHSA-vm39-j3vx-pch3](https://github.com/mastodon/mastodon/security/advisories/GHSA-vm39-j3vx-pch3))
Up until now, Mastodon has allowed new identities from external authentication providers to attach to an existing local user based on their verified e-mail address.
This allowed upgrading users from a database-stored password to an external authentication provider, or move from one authentication provider to another.
However, this behavior may be unexpected, and means that when multiple authentication providers are configured, the overall security would be that of the least secure authentication provider.
For these reasons, this behavior is now locked under the `ALLOW_UNSAFE_AUTH_PROVIDER_REATTACH` environment variable.
In addition, regardless of this environment variable, Mastodon will refuse to attach two identities from the same authentication provider to the same account.
## [4.1.13] - 2024-02-01
### Security
- Fix insufficient origin validation (CVE-2024-23832, [GHSA-3fjr-858r-92rw](https://github.com/mastodon/mastodon/security/advisories/GHSA-3fjr-858r-92rw))
## [4.1.12] - 2024-01-24
### Fixed
- Fix error when processing remote files with unusually long names ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28823))
- Fix processing of compacted single-item JSON-LD collections ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28816))
- Retry 401 errors on replies fetching ([ShadowJonathan](https://github.com/mastodon/mastodon/pull/28788))
- Fix `RecordNotUnique` errors in LinkCrawlWorker ([tribela](https://github.com/mastodon/mastodon/pull/28748))
- Fix Mastodon not correctly processing HTTP Signatures with query strings ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28443), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/28476))
- Fix potential redirection loop of streaming endpoint ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28665))
- Fix streaming API redirection ignoring the port of `streaming_api_base_url` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28558))
- Fix `Undo Announce` activity not being sent to non-follower authors ([MitarashiDango](https://github.com/mastodon/mastodon/pull/18482))
- Fix `LinkCrawlWorker` error when encountering empty OEmbed response ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28268))
### Security
- Add rate-limit of TOTP authentication attempts at controller level ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28801))
## [4.1.11] - 2023-12-04
### Changed
- Change GIF max matrix size error to explicitly mention GIF files ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27927))
- Change `Follow` activities delivery to bypass availability check ([ShadowJonathan](https://github.com/mastodon/mastodon/pull/27586))
- Change Content-Security-Policy to be tighter on media paths ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26889))
### Fixed
- Fix incoming status creation date not being restricted to standard ISO8601 ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27655), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/28081))
- Fix posts from force-sensitized accounts being able to trend ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27620))
- Fix processing LDSigned activities from actors with unknown public keys ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27474))
- Fix error and incorrect URLs in `/api/v1/accounts/:id/featured_tags` for remote accounts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27459))
- Fix report processing notice not mentioning the report number when performing a custom action ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27442))
- Fix some link anchors being recognized as hashtags ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27271), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/27584))
## [4.1.10] - 2023-10-10
### Changed
- Change some worker lock TTLs to be shorter-lived ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27246))
- Change user archive export allowed period from 7 days to 6 days ([suddjian](https://github.com/mastodon/mastodon/pull/27200))
### Fixed
- Fix mentions being matched in some URL query strings ([mjankowski](https://github.com/mastodon/mastodon/pull/25656))
- Fix multiple instances of the trend refresh scheduler sometimes running at once ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27253))
- Fix importer returning negative row estimates ([jgillich](https://github.com/mastodon/mastodon/pull/27258))
- Fix filtering audit log for entries about disabling 2FA ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27186))
- Fix tIME chunk not being properly removed from PNG uploads ([TheEssem](https://github.com/mastodon/mastodon/pull/27111))
- Fix inefficient queries in “Follows and followers” as well as several admin pages ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27116), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/27306))
## [4.1.9] - 2023-09-20
### Fixed
- Fix post translation erroring out ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26990))
## [4.1.8] - 2023-09-19
### Fixed
- Fix post edits not being forwarded as expected ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26936))
- Fix moderator rights inconsistencies ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26729))
- Fix crash when encountering invalid URL ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26814))
- Fix cached posts including stale stats ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26409))
- Fix uploading of video files for which `ffprobe` reports `0/0` average framerate ([NicolaiSoeborg](https://github.com/mastodon/mastodon/pull/26500))
- Fix unexpected audio stream transcoding when uploaded video is eligible to passthrough ([yufushiro](https://github.com/mastodon/mastodon/pull/26608))
### Security
- Fix missing HTML sanitization in translation API (CVE-2023-42452)
- Fix incorrect domain name normalization (CVE-2023-42451)
## [4.1.7] - 2023-09-05
### Changed
- Change remote report processing to accept reports with long comments, but truncate them ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/25028))
### Fixed
- **Fix blocking subdomains of an already-blocked domain** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26392))
- Fix `/api/v1/timelines/tag/:hashtag` allowing for unauthenticated access when public preview is disabled ([danielmbrasil](https://github.com/mastodon/mastodon/pull/26237))
- Fix inefficiencies in `PlainTextFormatter` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26727))
## [4.1.6] - 2023-07-31
### Fixed
- Fix memory leak in streaming server ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/26228))
- Fix wrong filters sometimes applying in streaming ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26159), [ThisIsMissEm](https://github.com/mastodon/mastodon/pull/26213), [renchap](https://github.com/mastodon/mastodon/pull/26233))
- Fix incorrect connect timeout in outgoing requests ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26116))
## [4.1.5] - 2023-07-21
### Added
- Add check preventing Sidekiq workers from running with Makara configured ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25850))
### Changed
- Change request timeout handling to use a longer deadline ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26055))
### Fixed
- Fix moderation interface for remote instances with a .zip TLD ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25886))
- Fix remote accounts being possibly persisted to database with incomplete protocol values ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25886))
- Fix trending publishers table not rendering correctly on narrow screens ([vmstan](https://github.com/mastodon/mastodon/pull/25945))
### Security
- Fix CSP headers being unintentionally wide ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26105))
## [4.1.4] - 2023-07-07
### Fixed
- Fix branding:generate_app_icons failing because of disallowed ICO coder ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25794))
- Fix crash in admin interface when viewing a remote user with verified links ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25796))
- Fix processing of media files with unusual names ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25788))
## [4.1.3] - 2023-07-06
### Added
- Add fallback redirection when getting a webfinger query `LOCAL_DOMAIN@LOCAL_DOMAIN` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23600))
### Changed
- Change OpenGraph-based embeds to allow fullscreen ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25058))
- Change AccessTokensVacuum to also delete expired tokens ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24868))
- Change profile updates to be sent to recently-mentioned servers ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24852))
- Change automatic post deletion thresholds and load detection ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24614))
- Change `/api/v1/statuses/:id/history` to always return at least one item ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25510))
- Change auto-linking to allow carets in URL query params ([renchap](https://github.com/mastodon/mastodon/pull/25216))
### Removed
- Remove invalid `X-Frame-Options: ALLOWALL` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25070))
### Fixed
- Fix wrong view being displayed when a webhook fails validation ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25464))
- Fix soft-deleted post cleanup scheduler overwhelming the streaming server ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/25519))
- Fix incorrect pagination headers in `/api/v2/admin/accounts` ([danielmbrasil](https://github.com/mastodon/mastodon/pull/25477))
- Fix multiple inefficiencies in automatic post cleanup worker ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24607), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24785), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24840))
- Fix performance of streaming by parsing message JSON once ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/25278), [ThisIsMissEm](https://github.com/mastodon/mastodon/pull/25361))
- Fix CSP headers when `S3_ALIAS_HOST` includes a path component ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25273))
- Fix `tootctl accounts approve --number N` not approving N earliest registrations ([danielmbrasil](https://github.com/mastodon/mastodon/pull/24605))
- Fix reports not being closed when performing batch suspensions ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24988))
- Fix being able to vote on your own polls ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25015))
- Fix race condition when reblogging a status ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25016))
- Fix “Authorized applications” inefficiently and incorrectly getting last use date ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25060))
- Fix “Authorized applications” crashing when listing apps with certain admin API scopes ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25713))
- Fix multiple N+1s in ConversationsController ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25134), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25399), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25499))
- Fix user archive takeouts when using OpenStack Swift ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24431))
- Fix searching for remote content by URL not working under certain conditions ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25637))
- Fix inefficiencies in indexing content for search ([VyrCossont](https://github.com/mastodon/mastodon/pull/24285), [VyrCossont](https://github.com/mastodon/mastodon/pull/24342))
### Security
- Add finer permission requirements for managing webhooks ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25463))
- Update dependencies
- Add hardening headers for user-uploaded files ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25756))
- Fix verified links possibly hiding important parts of the URL (CVE-2023-36462)
- Fix timeout handling of outbound HTTP requests (CVE-2023-36461)
- Fix arbitrary file creation through media processing (CVE-2023-36460)
- Fix possible XSS in preview cards (CVE-2023-36459)
## [4.1.2] - 2023-04-04
### Fixed
- Fix crash in `tootctl` commands making use of parallelization when Elasticsearch is enabled ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24182), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24377))
- Fix crash in `db:setup` when Elasticsearch is enabled ([rrgeorge](https://github.com/mastodon/mastodon/pull/24302))
- Fix user archive takeout when using OpenStack Swift or S3 providers with no ACL support ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24200))
- Fix invalid/expired invites being processed on sign-up ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24337))
### Security
- Update Ruby to 3.0.6 due to ReDoS vulnerabilities ([saizai](https://github.com/mastodon/mastodon/pull/24334))
- Fix unescaped user input in LDAP query ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24379))
## [4.1.1] - 2023-03-16
### Added

View File

@ -2,7 +2,7 @@
# This needs to be bullseye-slim because the Ruby image is built on bullseye-slim
ARG NODE_VERSION="16.18.1-bullseye-slim"
FROM ghcr.io/moritzheiber/ruby-jemalloc:3.0.4-slim as ruby
FROM ghcr.io/moritzheiber/ruby-jemalloc:3.0.6-slim as ruby
FROM node:${NODE_VERSION} as build
COPY --link --from=ruby /opt/ruby /opt/ruby
@ -17,6 +17,7 @@ COPY Gemfile* package.json yarn.lock /opt/mastodon/
# hadolint ignore=DL3008
RUN apt-get update && \
apt-get -yq dist-upgrade && \
apt-get install -y --no-install-recommends build-essential \
ca-certificates \
git \

View File

@ -158,3 +158,4 @@ gem 'concurrent-ruby', require: false
gem 'connection_pool', require: false
gem 'xorcist', '~> 1.1'
gem 'cocoon', '~> 1.2'
gem 'mail', '~> 2.8'

View File

@ -10,40 +10,40 @@ GIT
GEM
remote: https://rubygems.org/
specs:
actioncable (6.1.7.2)
actionpack (= 6.1.7.2)
activesupport (= 6.1.7.2)
actioncable (6.1.7.8)
actionpack (= 6.1.7.8)
activesupport (= 6.1.7.8)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
actionmailbox (6.1.7.2)
actionpack (= 6.1.7.2)
activejob (= 6.1.7.2)
activerecord (= 6.1.7.2)
activestorage (= 6.1.7.2)
activesupport (= 6.1.7.2)
actionmailbox (6.1.7.8)
actionpack (= 6.1.7.8)
activejob (= 6.1.7.8)
activerecord (= 6.1.7.8)
activestorage (= 6.1.7.8)
activesupport (= 6.1.7.8)
mail (>= 2.7.1)
actionmailer (6.1.7.2)
actionpack (= 6.1.7.2)
actionview (= 6.1.7.2)
activejob (= 6.1.7.2)
activesupport (= 6.1.7.2)
actionmailer (6.1.7.8)
actionpack (= 6.1.7.8)
actionview (= 6.1.7.8)
activejob (= 6.1.7.8)
activesupport (= 6.1.7.8)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
actionpack (6.1.7.2)
actionview (= 6.1.7.2)
activesupport (= 6.1.7.2)
actionpack (6.1.7.8)
actionview (= 6.1.7.8)
activesupport (= 6.1.7.8)
rack (~> 2.0, >= 2.0.9)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0)
actiontext (6.1.7.2)
actionpack (= 6.1.7.2)
activerecord (= 6.1.7.2)
activestorage (= 6.1.7.2)
activesupport (= 6.1.7.2)
actiontext (6.1.7.8)
actionpack (= 6.1.7.8)
activerecord (= 6.1.7.8)
activestorage (= 6.1.7.8)
activesupport (= 6.1.7.8)
nokogiri (>= 1.8.5)
actionview (6.1.7.2)
activesupport (= 6.1.7.2)
actionview (6.1.7.8)
activesupport (= 6.1.7.8)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
@ -54,22 +54,22 @@ GEM
case_transform (>= 0.2)
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
active_record_query_trace (1.8)
activejob (6.1.7.2)
activesupport (= 6.1.7.2)
activejob (6.1.7.8)
activesupport (= 6.1.7.8)
globalid (>= 0.3.6)
activemodel (6.1.7.2)
activesupport (= 6.1.7.2)
activerecord (6.1.7.2)
activemodel (= 6.1.7.2)
activesupport (= 6.1.7.2)
activestorage (6.1.7.2)
actionpack (= 6.1.7.2)
activejob (= 6.1.7.2)
activerecord (= 6.1.7.2)
activesupport (= 6.1.7.2)
activemodel (6.1.7.8)
activesupport (= 6.1.7.8)
activerecord (6.1.7.8)
activemodel (= 6.1.7.8)
activesupport (= 6.1.7.8)
activestorage (6.1.7.8)
actionpack (= 6.1.7.8)
activejob (= 6.1.7.8)
activerecord (= 6.1.7.8)
activesupport (= 6.1.7.8)
marcel (~> 1.0)
mini_mime (>= 1.1.0)
activesupport (6.1.7.2)
activesupport (6.1.7.8)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
@ -105,6 +105,7 @@ GEM
aws-sigv4 (~> 1.4)
aws-sigv4 (1.5.2)
aws-eventstream (~> 1, >= 1.0.2)
base64 (0.2.0)
bcrypt (3.1.17)
better_errors (2.9.1)
coderay (>= 1.0.0)
@ -120,8 +121,7 @@ GEM
bindata (2.4.14)
binding_of_caller (1.0.0)
debug_inspector (>= 0.0.1)
blurhash (0.1.6)
ffi (~> 1.14)
blurhash (0.1.7)
bootsnap (1.16.0)
msgpack (~> 1.2)
brakeman (5.4.0)
@ -174,7 +174,7 @@ GEM
cocoon (1.2.15)
coderay (1.1.3)
color_diff (0.1)
concurrent-ruby (1.2.0)
concurrent-ruby (1.2.3)
connection_pool (2.3.0)
cose (1.2.1)
cbor (~> 0.5.9)
@ -184,7 +184,7 @@ GEM
crass (1.0.6)
css_parser (1.12.0)
addressable
date (3.3.3)
date (3.3.4)
debug_inspector (1.0.0)
devise (4.8.1)
bcrypt (~> 3.0)
@ -207,7 +207,7 @@ GEM
docile (1.4.0)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
doorkeeper (5.6.4)
doorkeeper (5.6.6)
railties (>= 5)
dotenv (2.8.1)
dotenv-rails (2.8.1)
@ -331,7 +331,7 @@ GEM
jmespath (1.6.2)
json (2.6.3)
json-canonicalization (0.3.0)
json-jwt (1.15.3)
json-jwt (1.15.3.1)
activesupport (>= 4.2)
aes_key_wrap
bindata
@ -389,14 +389,14 @@ GEM
loofah (2.19.1)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
mail (2.8.0.1)
mail (2.8.1)
mini_mime (>= 0.1.1)
net-imap
net-pop
net-smtp
makara (0.5.1)
activerecord (>= 5.2.0)
marcel (1.0.2)
marcel (1.0.4)
mario-redis-lock (1.2.1)
redis (>= 3.0.5)
matrix (0.4.2)
@ -405,28 +405,28 @@ GEM
mime-types (3.4.1)
mime-types-data (~> 3.2015)
mime-types-data (3.2022.0105)
mini_mime (1.1.2)
mini_portile2 (2.8.1)
mini_mime (1.1.5)
mini_portile2 (2.8.7)
minitest (5.17.0)
msgpack (1.6.0)
multi_json (1.15.0)
multipart-post (2.1.1)
net-imap (0.3.4)
net-imap (0.3.7)
date
net-protocol
net-ldap (0.17.1)
net-pop (0.1.2)
net-protocol
net-protocol (0.2.1)
net-protocol (0.2.2)
timeout
net-scp (4.0.0.rc1)
net-ssh (>= 2.6.5, < 8.0.0)
net-smtp (0.3.3)
net-smtp (0.3.4)
net-protocol
net-ssh (7.0.1)
nio4r (2.5.8)
nokogiri (1.14.1)
mini_portile2 (~> 2.8.0)
nio4r (2.5.9)
nokogiri (1.16.6)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
nsa (0.2.8)
activesupport (>= 4.2, < 7)
@ -469,7 +469,7 @@ GEM
parslet (2.0.0)
pastel (0.8.0)
tty-color (~> 0.5)
pg (1.4.5)
pg (1.4.6)
pghero (3.1.0)
activerecord (>= 6)
pkg-config (1.5.1)
@ -492,13 +492,13 @@ GEM
pry-rails (0.3.9)
pry (>= 0.10.4)
public_suffix (5.0.1)
puma (5.6.5)
puma (5.6.8)
nio4r (~> 2.0)
pundit (2.3.0)
activesupport (>= 3.0.0)
raabro (1.4.0)
racc (1.6.2)
rack (2.2.6.2)
racc (1.7.3)
rack (2.2.9)
rack-attack (6.6.1)
rack (>= 1.0, < 3)
rack-cors (1.1.1)
@ -513,20 +513,20 @@ GEM
rack
rack-test (2.0.2)
rack (>= 1.3)
rails (6.1.7.2)
actioncable (= 6.1.7.2)
actionmailbox (= 6.1.7.2)
actionmailer (= 6.1.7.2)
actionpack (= 6.1.7.2)
actiontext (= 6.1.7.2)
actionview (= 6.1.7.2)
activejob (= 6.1.7.2)
activemodel (= 6.1.7.2)
activerecord (= 6.1.7.2)
activestorage (= 6.1.7.2)
activesupport (= 6.1.7.2)
rails (6.1.7.8)
actioncable (= 6.1.7.8)
actionmailbox (= 6.1.7.8)
actionmailer (= 6.1.7.8)
actionpack (= 6.1.7.8)
actiontext (= 6.1.7.8)
actionview (= 6.1.7.8)
activejob (= 6.1.7.8)
activemodel (= 6.1.7.8)
activerecord (= 6.1.7.8)
activestorage (= 6.1.7.8)
activesupport (= 6.1.7.8)
bundler (>= 1.15.0)
railties (= 6.1.7.2)
railties (= 6.1.7.8)
sprockets-rails (>= 2.0.0)
rails-controller-testing (1.0.5)
actionpack (>= 5.0.1.rc1)
@ -542,9 +542,9 @@ GEM
railties (>= 6.0.0, < 7)
rails-settings-cached (0.6.6)
rails (>= 4.2.0)
railties (6.1.7.2)
actionpack (= 6.1.7.2)
activesupport (= 6.1.7.2)
railties (6.1.7.8)
actionpack (= 6.1.7.8)
activesupport (= 6.1.7.8)
method_source
rake (>= 12.2)
thor (~> 1.0)
@ -566,7 +566,8 @@ GEM
responders (3.0.1)
actionpack (>= 5.0)
railties (>= 5.0)
rexml (3.2.5)
rexml (3.2.8)
strscan (>= 3.0.9)
rotp (6.2.0)
rpam2 (4.0.2)
rqrcode (2.1.2)
@ -628,14 +629,14 @@ GEM
fugit (~> 1.1, >= 1.1.6)
safety_net_attestation (0.4.0)
jwt (~> 2.0)
sanitize (6.0.1)
sanitize (6.0.2)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
scenic (1.7.0)
activerecord (>= 4.0.0)
railties (>= 4.0.0)
semantic_range (3.0.0)
sidekiq (6.5.8)
sidekiq (6.5.12)
connection_pool (>= 2.2.5, < 3)
rack (~> 2.0)
redis (>= 4.5.0, < 5)
@ -646,7 +647,7 @@ GEM
rufus-scheduler (~> 3.2)
sidekiq (>= 4, < 7)
tilt (>= 1.4.0)
sidekiq-unique-jobs (7.1.29)
sidekiq-unique-jobs (7.1.33)
brpoplpush-redis_script (> 0.1.1, <= 2.0.0)
concurrent-ruby (~> 1.0, >= 1.0.5)
redis (< 5.0)
@ -664,7 +665,8 @@ GEM
simplecov-html (0.12.3)
simplecov_json_formatter (0.1.4)
smart_properties (1.17.0)
sprockets (3.7.2)
sprockets (3.7.3)
base64
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
sprockets-rails (3.4.2)
@ -680,6 +682,7 @@ GEM
redlock (~> 1.0)
strong_migrations (0.7.9)
activerecord (>= 5)
strscan (3.1.0)
swd (1.3.0)
activesupport (>= 3)
attr_required (>= 0.0.5)
@ -689,9 +692,9 @@ GEM
unicode-display_width (>= 1.1.1, < 3)
terrapin (0.6.0)
climate_control (>= 0.0.3, < 1.0)
thor (1.2.1)
thor (1.2.2)
tilt (2.0.11)
timeout (0.3.1)
timeout (0.3.2)
tpm-key_attestation (0.11.0)
bindata (~> 2.4)
openssl (> 2.0, < 3.1)
@ -747,14 +750,14 @@ GEM
rack-proxy (>= 0.6.1)
railties (>= 5.2)
semantic_range (>= 2.3.0)
websocket-driver (0.7.5)
websocket-driver (0.7.6)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
wisper (2.0.1)
xorcist (1.1.3)
xpath (3.2.0)
nokogiri (~> 1.8)
zeitwerk (2.6.6)
zeitwerk (2.6.16)
PLATFORMS
ruby
@ -817,6 +820,7 @@ DEPENDENCIES
letter_opener_web (~> 2.0)
link_header (~> 0.0)
lograge (~> 0.12)
mail (~> 2.8)
makara (~> 0.5)
mario-redis-lock (~> 1.2)
memory_profiler

View File

@ -10,8 +10,8 @@ A "vulnerability in Mastodon" is a vulnerability in the code distributed through
## Supported Versions
| Version | Supported |
| ------- | ----------|
| 4.0.x | Yes |
| 3.5.x | Yes |
| < 3.5 | No |
| Version | Supported |
| ------- | ---------------- |
| 4.2.x | Yes |
| 4.1.x | Yes |
| < 4.1 | No |

View File

@ -1,6 +1,8 @@
# frozen_string_literal: true
class AccountsIndex < Chewy::Index
include DatetimeClampingConcern
settings index: { refresh_interval: '30s' }, analysis: {
analyzer: {
content: {
@ -38,6 +40,6 @@ class AccountsIndex < Chewy::Index
field :following_count, type: 'long', value: ->(account) { account.following_count }
field :followers_count, type: 'long', value: ->(account) { account.followers_count }
field :last_status_at, type: 'date', value: ->(account) { account.last_status_at || account.created_at }
field :last_status_at, type: 'date', value: ->(account) { clamp_date(account.last_status_at || account.created_at) }
end
end

View File

@ -0,0 +1,14 @@
# frozen_string_literal: true
module DatetimeClampingConcern
extend ActiveSupport::Concern
MIN_ISO8601_DATETIME = '0000-01-01T00:00:00Z'.to_datetime.freeze
MAX_ISO8601_DATETIME = '9999-12-31T23:59:59Z'.to_datetime.freeze
class_methods do
def clamp_date(datetime)
datetime.clamp(MIN_ISO8601_DATETIME, MAX_ISO8601_DATETIME)
end
end
end

View File

@ -1,6 +1,8 @@
# frozen_string_literal: true
class TagsIndex < Chewy::Index
include DatetimeClampingConcern
settings index: { refresh_interval: '30s' }, analysis: {
analyzer: {
content: {
@ -36,6 +38,6 @@ class TagsIndex < Chewy::Index
field :reviewed, type: 'boolean', value: ->(tag) { tag.reviewed? }
field :usage, type: 'long', value: ->(tag, crutches) { tag.history.aggregate(crutches.time_period).accounts }
field :last_status_at, type: 'date', value: ->(tag) { tag.last_status_at || tag.created_at }
field :last_status_at, type: 'date', value: ->(tag) { clamp_date(tag.last_status_at || tag.created_at) }
end
end

View File

@ -21,7 +21,7 @@ module Admin
account_action.save!
if account_action.with_report?
redirect_to admin_reports_path, notice: I18n.t('admin.reports.processed_msg', id: params[:report_id])
redirect_to admin_reports_path, notice: I18n.t('admin.reports.processed_msg', id: resource_params[:report_id])
else
redirect_to admin_account_path(@account.id)
end

View File

@ -25,6 +25,8 @@ class Admin::DomainAllowsController < Admin::BaseController
def destroy
authorize @domain_allow, :destroy?
UnallowDomainService.new.call(@domain_allow)
log_action :destroy, @domain_allow
redirect_to admin_instances_path, notice: I18n.t('admin.domain_allows.destroyed_msg')
end

View File

@ -37,7 +37,7 @@ module Admin
@domain_block.errors.delete(:domain)
render :new
else
if existing_domain_block.present?
if existing_domain_block.present? && existing_domain_block.domain == TagManager.instance.normalize_domain(@domain_block.domain.strip)
@domain_block = existing_domain_block
@domain_block.update(resource_params)
end

View File

@ -20,6 +20,7 @@ module Admin
authorize :webhook, :create?
@webhook = Webhook.new(resource_params)
@webhook.current_account = current_account
if @webhook.save
redirect_to admin_webhook_path(@webhook)
@ -39,10 +40,12 @@ module Admin
def update
authorize @webhook, :update?
@webhook.current_account = current_account
if @webhook.update(resource_params)
redirect_to admin_webhook_path(@webhook)
else
render :show
render :edit
end
end

View File

@ -7,8 +7,11 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
after_action :insert_pagination_headers, unless: -> { truthy_param?(:pinned) }
def index
@statuses = load_statuses
render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
@statuses = load_statuses
account_ids = @statuses.filter(&:quote?).map { |status| status.quote.account_id }.uniq
render json: @statuses, each_serializer: REST::StatusSerializer,
relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id),
account_relationships: AccountRelationshipsPresenter.new(account_ids, current_user&.account_id)
end
private

View File

@ -19,10 +19,11 @@ class Api::V1::Admin::DomainBlocksController < Api::BaseController
def create
authorize :domain_block, :create?
@domain_block = DomainBlock.new(resource_params)
existing_domain_block = resource_params[:domain].present? ? DomainBlock.rule_for(resource_params[:domain]) : nil
return render json: existing_domain_block, serializer: REST::Admin::ExistingDomainBlockErrorSerializer, status: 422 if existing_domain_block.present?
return render json: existing_domain_block, serializer: REST::Admin::ExistingDomainBlockErrorSerializer, status: 422 if conflicts_with_existing_block?(@domain_block, existing_domain_block)
@domain_block = DomainBlock.create!(resource_params)
@domain_block.save!
DomainBlockWorker.perform_async(@domain_block.id)
log_action :create, @domain_block
render json: @domain_block, serializer: REST::Admin::DomainBlockSerializer
@ -55,6 +56,10 @@ class Api::V1::Admin::DomainBlocksController < Api::BaseController
private
def conflicts_with_existing_block?(domain_block, existing_domain_block)
existing_domain_block.present? && (existing_domain_block.domain == TagManager.instance.normalize_domain(domain_block.domain) || !domain_block.stricter_than?(existing_domain_block))
end
def set_domain_blocks
@domain_blocks = filtered_domain_blocks.order(id: :desc).to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
end

View File

@ -6,8 +6,11 @@ class Api::V1::BookmarksController < Api::BaseController
after_action :insert_pagination_headers
def index
@statuses = load_statuses
render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
@statuses = load_statuses
account_ids = @statuses.filter(&:quote?).map { |status| status.quote.account_id }.uniq
render json: @statuses, each_serializer: REST::StatusSerializer,
relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id),
account_relationships: AccountRelationshipsPresenter.new(account_ids, current_user&.account_id)
end
private

View File

@ -11,7 +11,7 @@ class Api::V1::ConversationsController < Api::BaseController
def index
@conversations = paginated_conversations
render json: @conversations, each_serializer: REST::ConversationSerializer
render json: @conversations, each_serializer: REST::ConversationSerializer, relationships: StatusRelationshipsPresenter.new(@conversations.map(&:last_status), current_user&.account_id)
end
def read
@ -32,6 +32,19 @@ class Api::V1::ConversationsController < Api::BaseController
def paginated_conversations
AccountConversation.where(account: current_account)
.includes(
account: :account_stat,
last_status: [
:media_attachments,
:preview_cards,
:status_stat,
:tags,
{
active_mentions: [account: :account_stat],
account: :account_stat,
},
]
)
.to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
end

View File

@ -6,8 +6,11 @@ class Api::V1::FavouritesController < Api::BaseController
after_action :insert_pagination_headers
def index
@statuses = load_statuses
render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
@statuses = load_statuses
account_ids = @statuses.filter(&:quote?).map { |status| status.quote.account_id }.uniq
render json: @statuses, each_serializer: REST::StatusSerializer,
relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id),
account_relationships: AccountRelationshipsPresenter.new(account_ids, current_user&.account_id)
end
private

View File

@ -12,6 +12,10 @@ class Api::V1::FeaturedTags::SuggestionsController < Api::BaseController
private
def set_recently_used_tags
@recently_used_tags = Tag.recently_used(current_account).where.not(id: current_account.featured_tags).limit(10)
@recently_used_tags = Tag.recently_used(current_account).where.not(id: featured_tag_ids).limit(10)
end
def featured_tag_ids
current_account.featured_tags.pluck(:tag_id)
end
end

View File

@ -6,6 +6,7 @@ class Api::V1::ScheduledStatusesController < Api::BaseController
before_action -> { doorkeeper_authorize! :read, :'read:statuses' }, except: [:update, :destroy]
before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only: [:update, :destroy]
before_action :require_user!
before_action :set_statuses, only: :index
before_action :set_status, except: :index

View File

@ -7,11 +7,15 @@ class Api::V1::Statuses::HistoriesController < Api::BaseController
before_action :set_status
def show
render json: @status.edits.includes(:account, status: [:account]), each_serializer: REST::StatusEditSerializer
render json: status_edits, each_serializer: REST::StatusEditSerializer
end
private
def status_edits
@status.edits.includes(:account, status: [:account]).to_a.presence || [@status.build_snapshot(at_time: @status.edited_at || @status.created_at)]
end
def set_status
@status = Status.find(params[:status_id])
authorize @status, :show?

View File

@ -2,6 +2,8 @@
class Api::V1::Statuses::ReblogsController < Api::BaseController
include Authorization
include Redisable
include Lockable
before_action -> { doorkeeper_authorize! :write, :'write:statuses' }
before_action :require_user!
@ -10,7 +12,9 @@ class Api::V1::Statuses::ReblogsController < Api::BaseController
override_rate_limit_headers :create, family: :statuses
def create
@status = ReblogService.new.call(current_account, @reblog, reblog_params)
with_lock("reblog:#{current_account.id}:#{@reblog.id}") do
@status = ReblogService.new.call(current_account, @reblog, reblog_params)
end
render json: @status, serializer: REST::StatusSerializer
end

View File

@ -4,6 +4,7 @@ class Api::V1::Statuses::TranslationsController < Api::BaseController
include Authorization
before_action -> { doorkeeper_authorize! :read, :'read:statuses' }
before_action :require_user!
before_action :set_status
before_action :set_translation

View File

@ -44,10 +44,13 @@ class Api::V1::StatusesController < Api::BaseController
loaded_ancestors = cache_collection(ancestors_results, Status)
loaded_descendants = cache_collection(descendants_results, Status)
@context = Context.new(ancestors: loaded_ancestors, descendants: loaded_descendants)
statuses = [@status] + @context.ancestors + @context.descendants
@context = Context.new(ancestors: loaded_ancestors, descendants: loaded_descendants)
statuses = [@status] + @context.ancestors + @context.descendants
account_ids = statuses.filter(&:quote?).map { |status| status.quote.account_id }.uniq
render json: @context, serializer: REST::ContextSerializer, relationships: StatusRelationshipsPresenter.new(statuses, current_user&.account_id)
render json: @context, serializer: REST::ContextSerializer,
relationships: StatusRelationshipsPresenter.new(statuses, current_user&.account_id),
account_relationships: AccountRelationshipsPresenter.new(account_ids, current_user&.account_id)
end
def create
@ -64,7 +67,8 @@ class Api::V1::StatusesController < Api::BaseController
application: doorkeeper_token.application,
poll: status_params[:poll],
idempotency: request.headers['Idempotency-Key'],
with_rate_limit: true
with_rate_limit: true,
quote_id: status_params[:quote_id].presence,
)
render json: @status, serializer: @status.is_a?(ScheduledStatus) ? REST::ScheduledStatusSerializer : REST::StatusSerializer
@ -128,6 +132,7 @@ class Api::V1::StatusesController < Api::BaseController
:visibility,
:language,
:scheduled_at,
:quote_id,
media_ids: [],
media_attributes: [
:id,

View File

@ -2,7 +2,7 @@
class Api::V1::StreamingController < Api::BaseController
def index
if Rails.configuration.x.streaming_api_base_url == request.host
if same_host?
not_found
else
redirect_to streaming_api_url, status: 301
@ -11,9 +11,16 @@ class Api::V1::StreamingController < Api::BaseController
private
def same_host?
base_url = Addressable::URI.parse(Rails.configuration.x.streaming_api_base_url)
request.host == base_url.host && request.port == (base_url.port || 80)
end
def streaming_api_url
Addressable::URI.parse(request.url).tap do |uri|
uri.host = Addressable::URI.parse(Rails.configuration.x.streaming_api_base_url).host
base_url = Addressable::URI.parse(Rails.configuration.x.streaming_api_base_url)
uri.host = base_url.host
uri.port = base_url.port
end.to_s
end
end

View File

@ -6,11 +6,13 @@ class Api::V1::Timelines::HomeController < Api::BaseController
after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
def show
@statuses = load_statuses
@statuses = load_statuses
account_ids = @statuses.filter(&:quote?).map { |status| status.quote.account_id }.uniq
render json: @statuses,
each_serializer: REST::StatusSerializer,
relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id),
account_relationships: AccountRelationshipsPresenter.new(account_ids, current_user&.account_id),
status: account_home_feed.regenerating? ? 206 : 200
end

View File

@ -9,9 +9,12 @@ class Api::V1::Timelines::ListController < Api::BaseController
after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
def show
account_ids = @statuses.filter(&:quote?).map { |status| status.quote.account_id }.uniq
render json: @statuses,
each_serializer: REST::StatusSerializer,
relationships: StatusRelationshipsPresenter.new(@statuses, current_user.account_id)
relationships: StatusRelationshipsPresenter.new(@statuses, current_user.account_id),
account_relationships: AccountRelationshipsPresenter.new(account_ids, current_user&.account_id)
end
private

View File

@ -1,12 +1,17 @@
# frozen_string_literal: true
class Api::V1::Timelines::PublicController < Api::BaseController
before_action -> { authorize_if_got_token! :read, :'read:statuses' }
before_action :require_user!, only: [:show], if: :require_auth?
after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
def show
@statuses = load_statuses
render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
@statuses = load_statuses
account_ids = @statuses.filter(&:quote?).map { |status| status.quote.account_id }.uniq
render json: @statuses, each_serializer: REST::StatusSerializer,
relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id),
account_relationships: AccountRelationshipsPresenter.new(account_ids, current_user&.account_id)
end
private

View File

@ -1,16 +1,26 @@
# frozen_string_literal: true
class Api::V1::Timelines::TagController < Api::BaseController
before_action -> { authorize_if_got_token! :read, :'read:statuses' }
before_action :require_user!, if: :require_auth?
before_action :load_tag
after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
def show
@statuses = load_statuses
render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
@statuses = load_statuses
account_ids = @statuses.filter(&:quote?).map { |status| status.quote.account_id }.uniq
render json: @statuses, each_serializer: REST::StatusSerializer,
relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id),
account_relationships: AccountRelationshipsPresenter.new(account_ids, current_user&.account_id)
end
private
def require_auth?
!Setting.timeline_preview
end
def load_tag
@tag = Tag.find_normalized(params[:id])
end

View File

@ -18,6 +18,14 @@ class Api::V2::Admin::AccountsController < Api::V1::Admin::AccountsController
private
def next_path
api_v2_admin_accounts_url(pagination_params(max_id: pagination_max_id)) if records_continue?
end
def prev_path
api_v2_admin_accounts_url(pagination_params(min_id: pagination_since_id)) unless @accounts.empty?
end
def filtered_accounts
AccountFilter.new(translated_filter_params).results
end

View File

@ -5,7 +5,7 @@ class Auth::OmniauthCallbacksController < Devise::OmniauthCallbacksController
def self.provides_callback_for(provider)
define_method provider do
@user = User.find_for_oauth(request.env['omniauth.auth'], current_user)
@user = User.find_for_omniauth(request.env['omniauth.auth'], current_user)
if @user.persisted?
LoginActivity.create(
@ -24,6 +24,9 @@ class Auth::OmniauthCallbacksController < Devise::OmniauthCallbacksController
session["devise.#{provider}_data"] = request.env['omniauth.auth']
redirect_to new_user_registration_url
end
rescue ActiveRecord::RecordInvalid
flash[:alert] = I18n.t('devise.failure.omniauth_user_creation_failure') if is_navigational_format?
redirect_to new_user_session_url
end
end

View File

@ -48,7 +48,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
super(hash)
resource.locale = I18n.locale
resource.invite_code = params[:invite_code] if resource.invite_code.blank?
resource.invite_code = @invite&.code if resource.invite_code.blank?
resource.registration_form_time = session[:registration_form_time]
resource.sign_up_ip = request.remote_ip

View File

@ -1,6 +1,10 @@
# frozen_string_literal: true
class Auth::SessionsController < Devise::SessionsController
include Redisable
MAX_2FA_ATTEMPTS_PER_HOUR = 10
layout 'auth'
skip_before_action :require_no_authentication, only: [:create]
@ -136,9 +140,23 @@ class Auth::SessionsController < Devise::SessionsController
session.delete(:attempt_user_updated_at)
end
def clear_2fa_attempt_from_user(user)
redis.del(second_factor_attempts_key(user))
end
def check_second_factor_rate_limits(user)
attempts, = redis.multi do |multi|
multi.incr(second_factor_attempts_key(user))
multi.expire(second_factor_attempts_key(user), 1.hour)
end
attempts >= MAX_2FA_ATTEMPTS_PER_HOUR
end
def on_authentication_success(user, security_measure)
@on_authentication_success_called = true
clear_2fa_attempt_from_user(user)
clear_attempt_from_session
user.update_sign_in!(new_sign_in: true)
@ -170,4 +188,8 @@ class Auth::SessionsController < Devise::SessionsController
user_agent: request.user_agent
)
end
def second_factor_attempts_key(user)
"2fa_auth_attempts:#{user.id}:#{Time.now.utc.hour}"
end
end

View File

@ -13,7 +13,11 @@ class BackupsController < ApplicationController
when :s3
redirect_to @backup.dump.expiring_url(10)
when :fog
redirect_to @backup.dump.expiring_url(Time.now.utc + 10)
if Paperclip::Attachment.default_options.dig(:fog_credentials, :openstack_temp_url_key).present?
redirect_to @backup.dump.expiring_url(Time.now.utc + 10)
else
redirect_to full_asset_url(@backup.dump.url)
end
when :filesystem
redirect_to full_asset_url(@backup.dump.url)
end

View File

@ -28,29 +28,19 @@ module CacheConcern
response.headers['Vary'] = public_fetch_mode? ? 'Accept' : 'Accept, Signature'
end
# TODO: Rename this method, as it does not perform any caching anymore.
def cache_collection(raw, klass)
return raw unless klass.respond_to?(:with_includes)
return raw unless klass.respond_to?(:preload_cacheable_associations)
raw = raw.cache_ids.to_a if raw.is_a?(ActiveRecord::Relation)
return [] if raw.empty?
records = raw.to_a
cached_keys_with_value = Rails.cache.read_multi(*raw).transform_keys(&:id)
uncached_ids = raw.map(&:id) - cached_keys_with_value.keys
klass.preload_cacheable_associations(records)
klass.reload_stale_associations!(cached_keys_with_value.values) if klass.respond_to?(:reload_stale_associations!)
unless uncached_ids.empty?
uncached = klass.where(id: uncached_ids).with_includes.index_by(&:id)
uncached.each_value do |item|
Rails.cache.write(item, item)
end
end
raw.filter_map { |item| cached_keys_with_value[item.id] || uncached[item.id] }
records
end
# TODO: Rename this method, as it does not perform any caching anymore.
def cache_collection_paginated_by_id(raw, klass, limit, options)
cache_collection raw.cache_ids.to_a_paginated_by_id(limit, options), klass
cache_collection raw.to_a_paginated_by_id(limit, options), klass
end
end

View File

@ -91,14 +91,23 @@ module SignatureVerification
raise SignatureVerificationError, "Public key not found for key #{signature_params['keyId']}" if actor.nil?
signature = Base64.decode64(signature_params['signature'])
compare_signed_string = build_signed_string
compare_signed_string = build_signed_string(include_query_string: true)
return actor unless verify_signature(actor, signature, compare_signed_string).nil?
# Compatibility quirk with older Mastodon versions
compare_signed_string = build_signed_string(include_query_string: false)
return actor unless verify_signature(actor, signature, compare_signed_string).nil?
actor = stoplight_wrap_request { actor_refresh_key!(actor) }
raise SignatureVerificationError, "Could not refresh public key #{signature_params['keyId']}" if actor.nil?
compare_signed_string = build_signed_string(include_query_string: true)
return actor unless verify_signature(actor, signature, compare_signed_string).nil?
# Compatibility quirk with older Mastodon versions
compare_signed_string = build_signed_string(include_query_string: false)
return actor unless verify_signature(actor, signature, compare_signed_string).nil?
fail_with! "Verification failed for #{actor.to_log_human_identifier} #{actor.uri} using rsa-sha256 (RSASSA-PKCS1-v1_5 with SHA-256)", signed_string: compare_signed_string, signature: signature_params['signature']
@ -177,16 +186,24 @@ module SignatureVerification
nil
end
def build_signed_string
def build_signed_string(include_query_string: true)
signed_headers.map do |signed_header|
if signed_header == Request::REQUEST_TARGET
"#{Request::REQUEST_TARGET}: #{request.method.downcase} #{request.path}"
elsif signed_header == '(created)'
case signed_header
when Request::REQUEST_TARGET
if include_query_string
"#{Request::REQUEST_TARGET}: #{request.method.downcase} #{request.original_fullpath}"
else
# Current versions of Mastodon incorrectly omit the query string from the (request-target) pseudo-header.
# Therefore, temporarily support such incorrect signatures for compatibility.
# TODO: remove eventually some time after release of the fixed version
"#{Request::REQUEST_TARGET}: #{request.method.downcase} #{request.path}"
end
when '(created)'
raise SignatureVerificationError, 'Invalid pseudo-header (created) for rsa-sha256' unless signature_algorithm == 'hs2019'
raise SignatureVerificationError, 'Pseudo-header (created) used but corresponding argument missing' if signature_params['created'].blank?
"(created): #{signature_params['created']}"
elsif signed_header == '(expires)'
when '(expires)'
raise SignatureVerificationError, 'Invalid pseudo-header (expires) for rsa-sha256' unless signature_algorithm == 'hs2019'
raise SignatureVerificationError, 'Pseudo-header (expires) used but corresponding argument missing' if signature_params['expires'].blank?
@ -246,7 +263,7 @@ module SignatureVerification
stoplight_wrap_request { ResolveAccountService.new.call(key_id.gsub(/\Aacct:/, ''), suppress_errors: false) }
elsif !ActivityPub::TagManager.instance.local_uri?(key_id)
account = ActivityPub::TagManager.instance.uri_to_actor(key_id)
account ||= stoplight_wrap_request { ActivityPub::FetchRemoteKeyService.new.call(key_id, id: false, suppress_errors: false) }
account ||= stoplight_wrap_request { ActivityPub::FetchRemoteKeyService.new.call(key_id, suppress_errors: false) }
account
end
rescue Mastodon::PrivateNetworkAddressError => e

View File

@ -65,6 +65,11 @@ module TwoFactorAuthenticationConcern
end
def authenticate_with_two_factor_via_otp(user)
if check_second_factor_rate_limits(user)
flash.now[:alert] = I18n.t('users.rate_limited')
return prompt_for_two_factor(user)
end
if valid_otp_attempt?(user)
on_authentication_success(user, :otp)
else

View File

@ -46,6 +46,6 @@ class MediaController < ApplicationController
end
def allow_iframing
response.headers['X-Frame-Options'] = 'ALLOWALL'
response.headers.delete('X-Frame-Options')
end
end

View File

@ -8,12 +8,15 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio
before_action :require_not_suspended!, only: :destroy
before_action :set_body_classes
before_action :set_last_used_at_by_app, only: :index, unless: -> { request.format == :json }
skip_before_action :require_functional!
include Localized
def destroy
Web::PushSubscription.unsubscribe_for(params[:id], current_resource_owner)
Doorkeeper::Application.find_by(id: params[:id])&.close_streaming_sessions(current_resource_owner)
super
end
@ -30,4 +33,14 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio
def require_not_suspended!
forbidden if current_account.suspended?
end
def set_last_used_at_by_app
@last_used_at_by_app = Doorkeeper::AccessToken
.select('DISTINCT ON (application_id) application_id, last_used_at')
.where(resource_owner_id: current_resource_owner.id)
.where.not(last_used_at: nil)
.order(application_id: :desc, last_used_at: :desc)
.pluck(:application_id, :last_used_at)
.to_h
end
end

View File

@ -55,6 +55,7 @@ class Settings::PreferencesController < Settings::BaseController
:setting_trends,
:setting_crop_images,
:setting_always_send_emails,
:setting_place_tab_bar_at_bottom,
notification_emails: %i(follow follow_request reblog favourite mention report pending_account trending_tag appeal),
interactions: %i(must_be_follower must_be_following must_be_following_dm)
)

View File

@ -43,7 +43,7 @@ class StatusesController < ApplicationController
return not_found if @status.hidden? || @status.reblog?
expires_in 180, public: true
response.headers['X-Frame-Options'] = 'ALLOWALL'
response.headers.delete('X-Frame-Options')
render layout: 'embedded'
end

View File

@ -18,7 +18,14 @@ module WellKnown
private
def set_account
@account = Account.find_local!(username_from_resource)
username = username_from_resource
@account = begin
if username == Rails.configuration.x.local_domain || username == Rails.configuration.x.web_domain
Account.representative
else
Account.find_local!(username)
end
end
end
def username_from_resource

View File

@ -23,6 +23,7 @@ module ContextHelper
voters_count: { 'toot' => 'http://joinmastodon.org/ns#', 'votersCount' => 'toot:votersCount' },
olm: { 'toot' => 'http://joinmastodon.org/ns#', 'Device' => 'toot:Device', 'Ed25519Signature' => 'toot:Ed25519Signature', 'Ed25519Key' => 'toot:Ed25519Key', 'Curve25519Key' => 'toot:Curve25519Key', 'EncryptedMessage' => 'toot:EncryptedMessage', 'publicKeyBase64' => 'toot:publicKeyBase64', 'deviceId' => 'toot:deviceId', 'claim' => { '@type' => '@id', '@id' => 'toot:claim' }, 'fingerprintKey' => { '@type' => '@id', '@id' => 'toot:fingerprintKey' }, 'identityKey' => { '@type' => '@id', '@id' => 'toot:identityKey' }, 'devices' => { '@type' => '@id', '@id' => 'toot:devices' }, 'messageFranking' => 'toot:messageFranking', 'messageType' => 'toot:messageType', 'cipherText' => 'toot:cipherText' },
suspended: { 'toot' => 'http://joinmastodon.org/ns#', 'suspended' => 'toot:suspended' },
quoteUrl: { 'quoteUrl' => 'as:quoteUrl' },
}.freeze
def full_context

View File

@ -15,7 +15,12 @@ module FormattingHelper
module_function :extract_status_plain_text
def status_content_format(status)
html_aware_format(status.text, status.local?, preloaded_accounts: [status.account] + (status.respond_to?(:active_mentions) ? status.active_mentions.map(&:account) : []))
html_aware_format(
status.text,
status.local?,
preloaded_accounts: [status.account] + (status.respond_to?(:active_mentions) ? status.active_mentions.map(&:account) : []),
quote: status.respond_to?(:quote) && status.quote,
)
end
def rss_status_content_format(status)
@ -58,6 +63,10 @@ module FormattingHelper
end
def account_field_value_format(field, with_rel_me: true)
html_aware_format(field.value, field.account.local?, with_rel_me: with_rel_me, with_domains: true, multiline: false)
if field.verified? && !field.account.local?
TextFormatter.shortened_link(field.value_for_verification)
else
html_aware_format(field.value, field.account.local?, with_rel_me: with_rel_me, with_domains: true, multiline: false)
end
end
end

View File

@ -157,8 +157,8 @@ module JsonLdHelper
end
end
def fetch_resource(uri, id, on_behalf_of = nil)
unless id
def fetch_resource(uri, id_is_known, on_behalf_of = nil, request_options: {})
unless id_is_known
json = fetch_resource_without_id_validation(uri, on_behalf_of)
return if !json.is_a?(Hash) || unsupported_uri_scheme?(json['id'])
@ -166,17 +166,29 @@ module JsonLdHelper
uri = json['id']
end
json = fetch_resource_without_id_validation(uri, on_behalf_of)
json = fetch_resource_without_id_validation(uri, on_behalf_of, request_options: request_options)
json.present? && json['id'] == uri ? json : nil
end
def fetch_resource_without_id_validation(uri, on_behalf_of = nil, raise_on_temporary_error = false)
def fetch_resource_without_id_validation(uri, on_behalf_of = nil, raise_on_temporary_error = false, request_options: {})
on_behalf_of ||= Account.representative
build_request(uri, on_behalf_of).perform do |response|
build_request(uri, on_behalf_of, options: request_options).perform do |response|
raise Mastodon::UnexpectedResponseError, response unless response_successful?(response) || response_error_unsalvageable?(response) || !raise_on_temporary_error
body_to_json(response.body_with_limit) if response.code == 200
body_to_json(response.body_with_limit) if response.code == 200 && valid_activitypub_content_type?(response)
end
end
def valid_activitypub_content_type?(response)
return true if response.mime_type == 'application/activity+json'
# When the mime type is `application/ld+json`, we need to check the profile,
# but `http.rb` does not parse it for us.
return false unless response.mime_type == 'application/ld+json'
response.headers[HTTP::Headers::CONTENT_TYPE]&.split(';')&.map(&:strip)&.any? do |str|
str.start_with?('profile="') && str[9...-1].split.include?('https://www.w3.org/ns/activitystreams')
end
end
@ -206,8 +218,8 @@ module JsonLdHelper
response.code == 501 || ((400...500).cover?(response.code) && ![401, 408, 429].include?(response.code))
end
def build_request(uri, on_behalf_of = nil)
Request.new(:get, uri).tap do |request|
def build_request(uri, on_behalf_of = nil, options: {})
Request.new(:get, uri, **options).tap do |request|
request.on_behalf_of(on_behalf_of) if on_behalf_of
request.add_headers('Accept' => 'application/activity+json, application/ld+json')
end

Binary file not shown.

Before

Width:  |  Height:  |  Size: 588 B

After

Width:  |  Height:  |  Size: 444 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 735 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1,89 @@
import { changeCompose } from '../actions/compose';
export const UTILBTNS_GOJI = 'UTILBTNS_GOJI';
export const UTILBTNS_HARUKIN = 'UTILBTNS_HARUKIN';
export const UTILBTNS_RISA = 'UTILBTNS_RISA';
export function submitGoji (textarea) {
return function (dispatch, getState) {
if (!textarea.value) {
let text = [
"#ゴジモリィィィィイイ",
":goji:"
].join("\r\n");
dispatch(submitGojiRequest());
dispatch(changeCompose(text));
textarea.focus();
}
}
}
export function submitGojiRequest () {
return {
type: UTILBTNS_GOJI
}
}
export function submitHarukin (textarea) {
return function (dispatch, getState) {
const HARUKINS = [":harukin: ", ":harukin_old: ", ":harukin_ika: ", ":harukin_tako: "];
const MAX = 6;
if (!textarea.value) {
let text = "";
let quantity = Math.round(Math.random() * MAX + 1);
let type = Math.round(Math.random() * (HARUKINS.length - 1));
let harukin = HARUKINS[type];
switch (quantity) {
default:
text = [
harukin.repeat(quantity),
"🔥 ".repeat(quantity)
].join("\r\n");
break;
case MAX + 1:
text = `${harukin}💕\r\n`.repeat(6);
break;
}
dispatch(submitHarukinRequest());
dispatch(changeCompose(text));
textarea.focus();
}
}
}
export function submitHarukinRequest () {
return {
type: UTILBTNS_HARUKIN
}
}
export function submitRisa (textarea) {
return function (dispatch, getState) {
if (!textarea.value) {
let text = [
"@risa2 "
].join("\r\n");
dispatch(submitRisaRequest());
dispatch(changeCompose(text));
textarea.focus();
}
}
}
export function submitRisaRequest () {
return {
type: UTILBTNS_RISA
}
}

View File

@ -1,5 +1,7 @@
import { fetchRelationships } from './accounts';
import api, { getLinks } from '../api';
import { importFetchedStatuses } from './importer';
import { uniq } from '../utils/uniq';
export const BOOKMARKED_STATUSES_FETCH_REQUEST = 'BOOKMARKED_STATUSES_FETCH_REQUEST';
export const BOOKMARKED_STATUSES_FETCH_SUCCESS = 'BOOKMARKED_STATUSES_FETCH_SUCCESS';
@ -20,6 +22,7 @@ export function fetchBookmarkedStatuses() {
api(getState).get('/api/v1/bookmarks').then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedStatuses(response.data));
dispatch(fetchRelationships(uniq(response.data.map(item => item.reblog ? item.reblog.account.id : item.account.id))));
dispatch(fetchBookmarkedStatusesSuccess(response.data, next ? next.uri : null));
}).catch(error => {
dispatch(fetchBookmarkedStatusesFail(error));
@ -61,6 +64,7 @@ export function expandBookmarkedStatuses() {
api(getState).get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedStatuses(response.data));
dispatch(fetchRelationships(uniq(response.data.map(item => item.reblog ? item.reblog.account.id : item.account.id))));
dispatch(expandBookmarkedStatusesSuccess(response.data, next ? next.uri : null));
}).catch(error => {
dispatch(expandBookmarkedStatusesFail(error));

View File

@ -22,6 +22,8 @@ export const COMPOSE_SUBMIT_SUCCESS = 'COMPOSE_SUBMIT_SUCCESS';
export const COMPOSE_SUBMIT_FAIL = 'COMPOSE_SUBMIT_FAIL';
export const COMPOSE_REPLY = 'COMPOSE_REPLY';
export const COMPOSE_REPLY_CANCEL = 'COMPOSE_REPLY_CANCEL';
export const COMPOSE_QUOTE = 'COMPOSE_QUOTE';
export const COMPOSE_QUOTE_CANCEL = 'COMPOSE_QUOTE_CANCEL';
export const COMPOSE_DIRECT = 'COMPOSE_DIRECT';
export const COMPOSE_MENTION = 'COMPOSE_MENTION';
export const COMPOSE_RESET = 'COMPOSE_RESET';
@ -120,6 +122,23 @@ export function cancelReplyCompose() {
};
}
export function quoteCompose(status, routerHistory) {
return (dispatch, getState) => {
dispatch({
type: COMPOSE_QUOTE,
status: status,
});
ensureComposeIsVisible(getState, routerHistory);
};
};
export function cancelQuoteCompose() {
return {
type: COMPOSE_QUOTE_CANCEL,
};
};
export function resetCompose() {
return {
type: COMPOSE_RESET,
@ -193,6 +212,7 @@ export function submitCompose(routerHistory) {
visibility: getState().getIn(['compose', 'privacy']),
poll: getState().getIn(['compose', 'poll'], null),
language: getState().getIn(['compose', 'language']),
quote_id: getState().getIn(['compose', 'quote_from'], null),
},
headers: {
'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']),

View File

@ -1,5 +1,7 @@
import { fetchRelationships } from './accounts';
import api, { getLinks } from '../api';
import { importFetchedStatuses } from './importer';
import { uniq } from '../utils/uniq';
export const FAVOURITED_STATUSES_FETCH_REQUEST = 'FAVOURITED_STATUSES_FETCH_REQUEST';
export const FAVOURITED_STATUSES_FETCH_SUCCESS = 'FAVOURITED_STATUSES_FETCH_SUCCESS';
@ -20,6 +22,7 @@ export function fetchFavouritedStatuses() {
api(getState).get('/api/v1/favourites').then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedStatuses(response.data));
dispatch(fetchRelationships(uniq(response.data.map(item => item.reblog ? item.reblog.account.id : item.account.id))));
dispatch(fetchFavouritedStatusesSuccess(response.data, next ? next.uri : null));
}).catch(error => {
dispatch(fetchFavouritedStatusesFail(error));
@ -64,6 +67,7 @@ export function expandFavouritedStatuses() {
api(getState).get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedStatuses(response.data));
dispatch(fetchRelationships(uniq(response.data.map(item => item.reblog ? item.reblog.account.id : item.account.id))));
dispatch(expandFavouritedStatusesSuccess(response.data, next ? next.uri : null));
}).catch(error => {
dispatch(expandFavouritedStatusesFail(error));

View File

@ -80,6 +80,10 @@ export function importFetchedStatuses(statuses) {
processStatus(status.reblog);
}
if (status.quote && status.quote.id) {
processStatus(status.quote);
}
if (status.poll && status.poll.id) {
pushUnique(polls, normalizePoll(status.poll));
}

View File

@ -75,6 +75,8 @@ export function normalizeStatus(status, normalOldStatus) {
normalStatus.spoilerHtml = normalOldStatus.get('spoilerHtml');
normalStatus.spoiler_text = normalOldStatus.get('spoiler_text');
normalStatus.hidden = normalOldStatus.get('hidden');
normalStatus.quote = normalOldStatus.get('quote');
normalStatus.quote_hidden = normalOldStatus.get('quote_hidden');
} else {
// If the status has a CW but no contents, treat the CW as if it were the
// status' contents, to avoid having a CW toggle with seemingly no effect.
@ -91,6 +93,11 @@ export function normalizeStatus(status, normalOldStatus) {
normalStatus.contentHtml = emojify(normalStatus.content, emojiMap);
normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(spoilerText), emojiMap);
normalStatus.hidden = expandSpoilers ? false : spoilerText.length > 0 || normalStatus.sensitive;
if (status.quote) {
normalStatus.quote = normalizeStatus(status.quote, null);
normalStatus.quote_hidden = normalStatus.quote.hidden;
}
}
return normalStatus;

View File

@ -28,6 +28,9 @@ export const STATUS_REVEAL = 'STATUS_REVEAL';
export const STATUS_HIDE = 'STATUS_HIDE';
export const STATUS_COLLAPSE = 'STATUS_COLLAPSE';
export const QUOTE_REVEAL = 'QUOTE_REVEAL';
export const QUOTE_HIDE = 'QUOTE_HIDE';
export const REDRAFT = 'REDRAFT';
export const STATUS_FETCH_SOURCE_REQUEST = 'STATUS_FETCH_SOURCE_REQUEST';
@ -347,3 +350,25 @@ export const undoStatusTranslation = id => ({
type: STATUS_TRANSLATE_UNDO,
id,
});
export function hideQuote(ids) {
if (!Array.isArray(ids)) {
ids = [ids];
}
return {
type: QUOTE_HIDE,
ids,
};
};
export function revealQuote(ids) {
if (!Array.isArray(ids)) {
ids = [ids];
}
return {
type: QUOTE_REVEAL,
ids,
};
};

View File

@ -1,9 +1,11 @@
import { fetchRelationships } from './accounts';
import { importFetchedStatus, importFetchedStatuses } from './importer';
import { submitMarkers } from './markers';
import api, { getLinks } from 'mastodon/api';
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
import compareId from 'mastodon/compare_id';
import { usePendingItems as preferPendingItems } from 'mastodon/initial_state';
import { uniq } from '../utils/uniq';
export const TIMELINE_UPDATE = 'TIMELINE_UPDATE';
export const TIMELINE_DELETE = 'TIMELINE_DELETE';
@ -39,6 +41,7 @@ export function updateTimeline(timeline, status, accept) {
}
dispatch(importFetchedStatus(status));
dispatch(fetchRelationships([status.reblog ? status.reblog.account.id : status.account.id, status.quote ? status.quote.account.id : null].filter(x => x)));
dispatch({
type: TIMELINE_UPDATE,
@ -111,6 +114,7 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) {
api(getState).get(path, { params }).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedStatuses(response.data));
dispatch(fetchRelationships(uniq(response.data.map(item => [item.reblog ? item.reblog.account.id : item.account.id, item.quote ? item.quote.account.id : null]).flat().filter(x => x))));
dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null, response.status === 206, isLoadingRecent, isLoadingMore, isLoadingRecent && preferPendingItems));
if (timelineId === 'home') {

View File

@ -1,10 +1,7 @@
import React from 'react';
const Logo = () => (
<svg viewBox='0 0 261 66' className='logo' role='img'>
<title>Mastodon</title>
<use xlinkHref='#logo-symbol-wordmark' />
</svg>
<img src="/y-zu-logo.svg" height="32px" />
);
export default Logo;

View File

@ -236,10 +236,12 @@ class MediaGallery extends React.PureComponent {
visible: PropTypes.bool,
autoplay: PropTypes.bool,
onToggleVisibility: PropTypes.func,
quote: PropTypes.bool,
};
static defaultProps = {
standalone: false,
quote: false,
};
state = {
@ -310,7 +312,7 @@ class MediaGallery extends React.PureComponent {
}
render () {
const { media, intl, sensitive, height, defaultWidth, standalone, autoplay } = this.props;
const { media, intl, sensitive, height, defaultWidth, standalone, autoplay, quote } = this.props;
const { visible } = this.state;
const width = this.state.width || defaultWidth;
@ -329,6 +331,10 @@ class MediaGallery extends React.PureComponent {
style.height = height;
}
if (quote && style.height) {
style.height /= 2;
}
const size = media.take(4).size;
const uncached = media.every(attachment => attachment.get('type') === 'unknown');

View File

@ -1,4 +1,5 @@
import React from 'react';
import { connect } from 'react-redux';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import Avatar from './avatar';
@ -22,6 +23,29 @@ import PictureInPicturePlaceholder from 'mastodon/components/picture_in_picture_
// to use the progress bar to show download progress
import Bundle from '../features/ui/components/bundle';
export const mapStateToProps = (state, props) => {
let status = props.status;
if (status === null) {
return null;
}
if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') {
status = status.get('reblog');
}
if (status.get('quote', null) === null) {
return {
quoteMuted: !!status.get('quote_id', null),
};
}
const id = status.getIn(['quote', 'account', 'id'], null);
return {
quoteMuted: id !== null && (state.getIn(['relationships', id, 'muting']) || state.getIn(['relationships', id, 'blocking']) || state.getIn(['relationships', id, 'blocked_by']) || state.getIn(['relationships', id, 'domain_blocking']) || status.getIn(['quote', 'muted'])),
};
};
export const textForScreenReader = (intl, status, rebloggedByText = false) => {
const displayName = status.getIn(['account', 'display_name']);
@ -59,7 +83,61 @@ const messages = defineMessages({
edited: { id: 'status.edited', defaultMessage: 'Edited {date}' },
});
export default @injectIntl
export const quote = (status, muted, quoteMuted, handleQuoteClick, handleExpandedQuoteToggle, identity, media, router, contextType = 'home') => {
const quoteStatus = status.get('quote', null);
if (!quoteStatus) {
return null;
}
const quoteInner = (() => {
const hideUnlisted = quoteStatus.get('visibility') === 'unlisted'
&& ['public', 'community', 'hashtag'].includes(contextType);
if (quoteMuted || hideUnlisted) {
const content = (() => {
if (quoteMuted) {
return (
<FormattedMessage id='status.muted_quote' defaultMessage='Muted quote' />
);
}
return (
<button onClick={handleQuoteClick}>
<FormattedMessage id='status.unlisted_quote' defaultMessage='Unlisted quote' />
</button>
);
})();
return (
<div className={classNames('status__content', { 'muted-quote': quoteMuted, 'unlisted-quote': hideUnlisted, 'status__content--with-action': router })}>
{content}
</div>
);
}
return (
<div>
<div className='status__info'>
{identity(quoteStatus, null, null, true)}
</div>
<StatusContent status={quoteStatus} onClick={handleQuoteClick} expanded={!status.get('quote_hidden')} onExpandedToggle={handleExpandedQuoteToggle} quote />
{media(quoteStatus, true)}
</div>
);
})();
return (
<div
className={classNames('quote-status', `status-${quoteStatus.get('visibility')}`, { muted: muted })}
data-id={quoteStatus.get('id')}
dataurl={quoteStatus.get('url')}
>
{quoteInner}
</div>
);
};
export default @connect(mapStateToProps) @injectIntl
class Status extends ImmutablePureComponent {
static contextTypes = {
@ -87,7 +165,9 @@ class Status extends ImmutablePureComponent {
onToggleCollapsed: PropTypes.func,
onTranslate: PropTypes.func,
onInteractionModal: PropTypes.func,
onQuoteToggleHidden: PropTypes.func,
muted: PropTypes.bool,
quoteMuted: PropTypes.bool,
hidden: PropTypes.bool,
unread: PropTypes.bool,
onMoveUp: PropTypes.func,
@ -103,6 +183,7 @@ class Status extends ImmutablePureComponent {
inUse: PropTypes.bool,
available: PropTypes.bool,
}),
contextType: PropTypes.string,
};
// Avoid checking props that are functions (and whose equality will always
@ -114,10 +195,12 @@ class Status extends ImmutablePureComponent {
'hidden',
'unread',
'pictureInPicture',
'quoteMuted',
];
state = {
showMedia: defaultMediaVisibility(this.props.status),
showQuoteMedia: defaultMediaVisibility(this.props.status ? this.props.status.get('quote', null) : null),
statusId: undefined,
forceFilter: undefined,
};
@ -126,6 +209,7 @@ class Status extends ImmutablePureComponent {
if (nextProps.status && nextProps.status.get('id') !== prevState.statusId) {
return {
showMedia: defaultMediaVisibility(nextProps.status),
showQuoteMedia: defaultMediaVisibility(nextProps.status ? nextProps.status.get('quote', null) : null),
statusId: nextProps.status.get('id'),
};
} else {
@ -137,6 +221,10 @@ class Status extends ImmutablePureComponent {
this.setState({ showMedia: !this.state.showMedia });
};
handleToggleQuoteMediaVisibility = () => {
this.setState({ showQuoteMedia: !this.state.showQuoteMedia });
}
handleClick = e => {
if (e && (e.button !== 0 || e.ctrlKey || e.metaKey)) {
return;
@ -165,6 +253,15 @@ class Status extends ImmutablePureComponent {
this._openProfile(proper);
};
handleQuoteClick = () => {
if (!this.context.router) {
return;
}
const { status } = this.props;
this.context.router.history.push(`/statuses/${status.getIn(['reblog', 'quote', 'id'], status.getIn(['quote', 'id']))}`);
}
handleExpandedToggle = () => {
this.props.onToggleHidden(this._properStatus());
};
@ -177,6 +274,10 @@ class Status extends ImmutablePureComponent {
this.props.onTranslate(this._properStatus());
};
handleExpandedQuoteToggle = () => {
this.props.onQuoteToggleHidden(this._properStatus());
}
renderLoadingMediaGallery () {
return <div className='media-gallery' style={{ height: '110px' }} />;
}
@ -309,10 +410,9 @@ class Status extends ImmutablePureComponent {
};
render () {
let media = null;
let statusAvatar, prepend, rebloggedByText;
let prepend, rebloggedByText;
const { intl, hidden, featured, unread, showThread, scrollKey, pictureInPicture } = this.props;
const { intl, hidden, featured, unread, showThread, scrollKey, pictureInPicture, quoteMuted, contextType } = this.props;
let { status, account, ...other } = this.props;
@ -397,103 +497,123 @@ class Status extends ImmutablePureComponent {
);
}
if (pictureInPicture.get('inUse')) {
media = <PictureInPicturePlaceholder width={this.props.cachedMediaWidth} />;
} else if (status.get('media_attachments').size > 0) {
if (this.props.muted) {
media = (
<AttachmentList
const media = (status, quote = false) => {
if (pictureInPicture.get('inUse')) {
return <PictureInPicturePlaceholder width={this.props.cachedMediaWidth} />;
} else if (status.get('media_attachments').size > 0) {
if (this.props.muted) {
return (
<AttachmentList
compact
media={status.get('media_attachments')}
/>
);
} else if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
const attachment = status.getIn(['media_attachments', 0]);
return (
<Bundle fetchComponent={Audio} loading={this.renderLoadingAudioPlayer}>
{Component => (
<Component
src={attachment.get('url')}
alt={attachment.get('description')}
poster={attachment.get('preview_url') || status.getIn(['account', 'avatar_static'])}
backgroundColor={attachment.getIn(['meta', 'colors', 'background'])}
foregroundColor={attachment.getIn(['meta', 'colors', 'foreground'])}
accentColor={attachment.getIn(['meta', 'colors', 'accent'])}
duration={attachment.getIn(['meta', 'original', 'duration'], 0)}
width={this.props.cachedMediaWidth}
height={110}
cacheWidth={this.props.cacheMediaWidth}
deployPictureInPicture={pictureInPicture.get('available') ? this.handleDeployPictureInPicture : undefined}
sensitive={status.get('sensitive')}
blurhash={attachment.get('blurhash')}
visible={this.state.showMedia}
onToggleVisibility={this.handleToggleMediaVisibility}
quote={quote}
/>
)}
</Bundle>
);
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
const attachment = status.getIn(['media_attachments', 0]);
return (
<Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer}>
{Component => (
<Component
preview={attachment.get('preview_url')}
frameRate={attachment.getIn(['meta', 'original', 'frame_rate'])}
blurhash={attachment.get('blurhash')}
src={attachment.get('url')}
alt={attachment.get('description')}
width={this.props.cachedMediaWidth}
height={110}
inline
sensitive={status.get('sensitive')}
onOpenVideo={this.handleOpenVideo}
cacheWidth={this.props.cacheMediaWidth}
deployPictureInPicture={pictureInPicture.get('available') ? this.handleDeployPictureInPicture : undefined}
visible={quote ? this.state.showQuoteMedia : this.state.showMedia}
onToggleVisibility={quote ? this.handleToggleQuoteMediaVisibility : this.handleToggleMediaVisibility}
quote={quote}
/>
)}
</Bundle>
);
} else {
return (
<Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery}>
{Component => (
<Component
media={status.get('media_attachments')}
sensitive={status.get('sensitive')}
height={110}
onOpenMedia={this.handleOpenMedia}
cacheWidth={this.props.cacheMediaWidth}
defaultWidth={this.props.cachedMediaWidth}
visible={quote ? this.state.showQuoteMedia : this.state.showMedia}
onToggleVisibility={quote ? this.handleToggleQuoteMediaVisibility : this.handleToggleMediaVisibility}
quote={quote}
/>
)}
</Bundle>
);
}
} else if (status.get('spoiler_text').length === 0 && status.get('card') && !this.props.muted) {
return (
<Card
onOpenMedia={this.handleOpenMedia}
card={status.get('card')}
compact
media={status.get('media_attachments')}
cacheWidth={this.props.cacheMediaWidth}
defaultWidth={this.props.cachedMediaWidth}
sensitive={status.get('sensitive')}
quote={quote}
/>
);
} else if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
const attachment = status.getIn(['media_attachments', 0]);
media = (
<Bundle fetchComponent={Audio} loading={this.renderLoadingAudioPlayer} >
{Component => (
<Component
src={attachment.get('url')}
alt={attachment.get('description')}
poster={attachment.get('preview_url') || status.getIn(['account', 'avatar_static'])}
backgroundColor={attachment.getIn(['meta', 'colors', 'background'])}
foregroundColor={attachment.getIn(['meta', 'colors', 'foreground'])}
accentColor={attachment.getIn(['meta', 'colors', 'accent'])}
duration={attachment.getIn(['meta', 'original', 'duration'], 0)}
width={this.props.cachedMediaWidth}
height={110}
cacheWidth={this.props.cacheMediaWidth}
deployPictureInPicture={pictureInPicture.get('available') ? this.handleDeployPictureInPicture : undefined}
sensitive={status.get('sensitive')}
blurhash={attachment.get('blurhash')}
visible={this.state.showMedia}
onToggleVisibility={this.handleToggleMediaVisibility}
/>
)}
</Bundle>
);
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
const attachment = status.getIn(['media_attachments', 0]);
media = (
<Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} >
{Component => (
<Component
preview={attachment.get('preview_url')}
frameRate={attachment.getIn(['meta', 'original', 'frame_rate'])}
blurhash={attachment.get('blurhash')}
src={attachment.get('url')}
alt={attachment.get('description')}
width={this.props.cachedMediaWidth}
height={110}
inline
sensitive={status.get('sensitive')}
onOpenVideo={this.handleOpenVideo}
cacheWidth={this.props.cacheMediaWidth}
deployPictureInPicture={pictureInPicture.get('available') ? this.handleDeployPictureInPicture : undefined}
visible={this.state.showMedia}
onToggleVisibility={this.handleToggleMediaVisibility}
/>
)}
</Bundle>
);
} else {
media = (
<Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery}>
{Component => (
<Component
media={status.get('media_attachments')}
sensitive={status.get('sensitive')}
height={110}
onOpenMedia={this.handleOpenMedia}
cacheWidth={this.props.cacheMediaWidth}
defaultWidth={this.props.cachedMediaWidth}
visible={this.state.showMedia}
onToggleVisibility={this.handleToggleMediaVisibility}
/>
)}
</Bundle>
);
}
} else if (status.get('spoiler_text').length === 0 && status.get('card') && !this.props.muted) {
media = (
<Card
onOpenMedia={this.handleOpenMedia}
card={status.get('card')}
compact
cacheWidth={this.props.cacheMediaWidth}
defaultWidth={this.props.cachedMediaWidth}
sensitive={status.get('sensitive')}
/>
);
}
if (account === undefined || account === null) {
statusAvatar = <Avatar account={status.get('account')} size={46} />;
} else {
statusAvatar = <AvatarOverlay account={status.get('account')} friend={account} />;
}
return null;
};
const statusAvatar = (status, account) => {
if (account === undefined || account === null) {
return <Avatar account={status.get('account')} size={46} />;
} else {
return <AvatarOverlay account={status.get('account')} friend={account} />;
}
};
const identity = (status, account) => (
<a onClick={this.handleAccountClick} href={`/@${status.getIn(['account', 'acct'])}`} title={status.getIn(['account', 'acct'])} className='status__display-name' target='_blank' rel='noopener noreferrer'>
<div className='status__avatar'>
{statusAvatar(status, account)}
</div>
<DisplayName account={status.get('account')} />
</a>
);
const visibilityIconInfo = {
'public': { icon: 'globe', text: intl.formatMessage(messages.public_short) },
@ -516,13 +636,7 @@ class Status extends ImmutablePureComponent {
<RelativeTimestamp timestamp={status.get('created_at')} />{status.get('edited_at') && <abbr title={intl.formatMessage(messages.edited, { date: intl.formatDate(status.get('edited_at'), { hour12: false, year: 'numeric', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }) })}> *</abbr>}
</a>
<a onClick={this.handleAccountClick} href={`/@${status.getIn(['account', 'acct'])}`} title={status.getIn(['account', 'acct'])} className='status__display-name' target='_blank' rel='noopener noreferrer'>
<div className='status__avatar'>
{statusAvatar}
</div>
<DisplayName account={status.get('account')} />
</a>
{identity(status, account, false)}
</div>
<StatusContent
@ -535,7 +649,9 @@ class Status extends ImmutablePureComponent {
onCollapsedToggle={this.handleCollapsedToggle}
/>
{media}
{media(status)}
{quote(status, this.props.muted, quoteMuted, this.handleQuoteClick, this.handleExpandedQuoteToggle, identity, media, this.context.router, contextType)}
<StatusActionBar scrollKey={scrollKey} status={status} account={account} onFilter={matchedFilters ? this.handleFilterClick : null} {...other} />
</div>

View File

@ -26,6 +26,8 @@ const messages = defineMessages({
reblog_private: { id: 'status.reblog_private', defaultMessage: 'Boost with original visibility' },
cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
quote: { id: 'status.quote', defaultMessage: 'Quote' },
cannot_quote: { id: 'status.cannot_quote', defaultMessage: 'This post cannot be quoted' },
favourite: { id: 'status.favourite', defaultMessage: 'Favourite' },
bookmark: { id: 'status.bookmark', defaultMessage: 'Bookmark' },
removeBookmark: { id: 'status.remove_bookmark', defaultMessage: 'Remove bookmark' },
@ -68,6 +70,7 @@ class StatusActionBar extends ImmutablePureComponent {
onReply: PropTypes.func,
onFavourite: PropTypes.func,
onReblog: PropTypes.func,
onQuote: PropTypes.func,
onDelete: PropTypes.func,
onDirect: PropTypes.func,
onMention: PropTypes.func,
@ -138,6 +141,10 @@ class StatusActionBar extends ImmutablePureComponent {
}
};
handleQuoteClick = () => {
this.props.onQuote(this.props.status, this.context.router.history);
}
handleBookmarkClick = () => {
this.props.onBookmark(this.props.status);
};
@ -231,6 +238,14 @@ class StatusActionBar extends ImmutablePureComponent {
this.props.onFilter();
};
static quoteTitle = (intl, messages, publicStatus) => {
if (publicStatus) {
return intl.formatMessage(messages.quote);
} else {
return intl.formatMessage(messages.cannot_quote);
}
}
render () {
const { status, relationship, intl, withDismiss, withCounters, scrollKey } = this.props;
const { signedIn, permissions } = this.context.identity;
@ -361,7 +376,8 @@ class StatusActionBar extends ImmutablePureComponent {
<div className='status__action-bar'>
<IconButton className='status__action-bar__button' title={replyTitle} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} counter={status.get('replies_count')} obfuscateCount />
<IconButton className={classNames('status__action-bar__button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon='retweet' onClick={this.handleReblogClick} counter={withCounters ? status.get('reblogs_count') : undefined} />
<IconButton className='status__action-bar__button star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} counter={withCounters ? status.get('favourites_count') : undefined} />
<IconButton className='status__action-bar__button star-icon' animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} counter={withCounters ? status.get('favourites_count') : undefined} />
<IconButton className='status__action-bar__button' disabled={!publicStatus} title={StatusActionBar.quoteTitle(intl, messages, publicStatus)} icon='quote-right' onClick={this.handleQuoteClick} />
<IconButton className='status__action-bar__button bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} />
{shareButton}

View File

@ -9,6 +9,7 @@ import Icon from 'mastodon/components/icon';
import { autoPlayGif, languages as preloadedLanguages, translationEnabled } from 'mastodon/initial_state';
const MAX_HEIGHT = 706; // 22px * 32 (+ 2px padding at the top)
const QUOTE_MAX_HEIGHT = 112; // 22px * 5 (+ 2px padding at the top)
class TranslateButton extends React.PureComponent {
@ -64,6 +65,7 @@ class StatusContent extends React.PureComponent {
collapsable: PropTypes.bool,
onCollapsedToggle: PropTypes.func,
intl: PropTypes.object,
quote: PropTypes.bool,
};
state = {
@ -107,12 +109,12 @@ class StatusContent extends React.PureComponent {
}
if (status.get('collapsed', null) === null && onCollapsedToggle) {
const { collapsable, onClick } = this.props;
const { collapsable, onClick, quote } = this.props;
const collapsed =
collapsable
&& onClick
&& node.clientHeight > MAX_HEIGHT
&& node.clientHeight > (quote ? QUOTE_MAX_HEIGHT : MAX_HEIGHT)
&& status.get('spoiler_text').length === 0;
onCollapsedToggle(collapsed);
@ -216,7 +218,7 @@ class StatusContent extends React.PureComponent {
};
render () {
const { status, intl } = this.props;
const { status, intl, quote } = this.props;
const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden;
const renderReadMore = this.props.onClick && status.get('collapsed');
@ -245,6 +247,12 @@ class StatusContent extends React.PureComponent {
<PollContainer pollId={status.get('poll')} />
);
if (quote) {
const doc = new DOMParser().parseFromString(content.__html, 'text/html').documentElement;
Array.from(doc.querySelectorAll('br')).forEach(nl => nl.replaceWith(' '));
content.__html = doc.outerHTML;
}
if (status.get('spoiler_text').length > 0) {
let mentionsPlaceholder = '';

View File

@ -4,6 +4,7 @@ import Status from '../components/status';
import { makeGetStatus, makeGetPictureInPicture } from '../selectors';
import {
replyCompose,
quoteCompose,
mentionCompose,
directCompose,
} from '../actions/compose';
@ -27,6 +28,8 @@ import {
editStatus,
translateStatus,
undoStatusTranslation,
hideQuote,
revealQuote,
} from '../actions/statuses';
import {
unmuteAccount,
@ -58,6 +61,8 @@ const messages = defineMessages({
replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
editConfirm: { id: 'confirmations.edit.confirm', defaultMessage: 'Edit' },
editMessage: { id: 'confirmations.edit.message', defaultMessage: 'Editing now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
quoteConfirm: { id: 'confirmations.quote.confirm', defaultMessage: 'Quote' },
quoteMessage: { id: 'confirmations.quote.message', defaultMessage: 'Quoting now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' },
});
@ -107,6 +112,22 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
}
},
onQuote (status, router) {
dispatch((_, getState) => {
let state = getState();
if (state.getIn(['compose', 'text']).trim().length !== 0) {
dispatch(openModal('CONFIRM', {
message: intl.formatMessage(messages.quoteMessage),
confirm: intl.formatMessage(messages.quoteConfirm),
onConfirm: () => dispatch(quoteCompose(status, router)),
}));
} else {
dispatch(quoteCompose(status, router));
}
});
},
onFavourite (status) {
if (status.get('favourited')) {
dispatch(unfavourite(status));
@ -234,6 +255,14 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
dispatch(toggleStatusCollapse(status.get('id'), isCollapsed));
},
onQuoteToggleHidden (status) {
if (status.get('quote_hidden')) {
dispatch(revealQuote(status.get('id')));
} else {
dispatch(hideQuote(status.get('id')));
}
},
onBlockDomain (domain) {
dispatch(openModal('CONFIRM', {
message: <FormattedMessage id='confirmations.domain_block.message' defaultMessage='Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.' values={{ domain: <strong>{domain}</strong> }} />,

View File

@ -50,6 +50,7 @@ class Audio extends React.PureComponent {
volume: PropTypes.number,
muted: PropTypes.bool,
deployPictureInPicture: PropTypes.func,
quote: PropTypes.bool,
};
state = {
@ -94,7 +95,11 @@ class Audio extends React.PureComponent {
_setDimensions () {
const width = this.player.offsetWidth;
const height = this.props.fullscreen ? this.player.offsetHeight : (width / (16/9));
let height = this.props.fullscreen ? this.player.offsetHeight : (width / (16/9));
if (this.props.quote) {
height /= 2;
}
if (this.props.cacheWidth) {
this.props.cacheWidth(width);

View File

@ -29,6 +29,9 @@ const messages = defineMessages({
publish: { id: 'compose_form.publish', defaultMessage: 'Publish' },
publishLoud: { id: 'compose_form.publish_loud', defaultMessage: '{publish}!' },
saveChanges: { id: 'compose_form.save_changes', defaultMessage: 'Save changes' },
utilBtns_goji: { id: 'compose_form.utilBtns_goji', defaultMessage: 'Typo!!!' },
utilBtns_harukin: { id: 'compose_form.utilBtns_harukin', defaultMessage: 'Burn Harukin' },
utilBtns_risa: { id: 'compose_form.utilBtns_risa', defaultMessage: 'Risa' }
});
export default @injectIntl
@ -65,6 +68,9 @@ class ComposeForm extends ImmutablePureComponent {
isInReply: PropTypes.bool,
singleColumn: PropTypes.bool,
lang: PropTypes.string,
onGojiSubmit: PropTypes.func.isRequired,
onHarukinSubmit: PropTypes.func.isRequired,
onRisaSubmit: PropTypes.func.isRequired,
};
static defaultProps = {
@ -90,7 +96,7 @@ class ComposeForm extends ImmutablePureComponent {
const fulltext = this.getFulltextForCharacterCounting();
const isOnlyWhitespace = fulltext.length !== 0 && fulltext.trim().length === 0;
return !(isSubmitting || isUploading || isChangingUpload || length(fulltext) > 500 || (isOnlyWhitespace && !anyMedia));
return !(isSubmitting || isUploading || isChangingUpload || length(fulltext) > 2048 || (isOnlyWhitespace && !anyMedia));
};
handleSubmit = (e) => {
@ -206,6 +212,10 @@ class ComposeForm extends ImmutablePureComponent {
this.props.onPickEmoji(position, data, needsSpace);
};
handleOnGojiSubmit = () => this.props.onGojiSubmit(this.autosuggestTextarea.textarea);
handleOnHarukinSubmit = () => this.props.onHarukinSubmit(this.autosuggestTextarea.textarea);
handleOnRisaSubmit = () => this.props.onRisaSubmit(this.autosuggestTextarea.textarea);
render () {
const { intl, onPaste, autoFocus } = this.props;
const disabled = this.props.isSubmitting;
@ -225,6 +235,7 @@ class ComposeForm extends ImmutablePureComponent {
<WarningContainer />
<ReplyIndicatorContainer />
<ReplyIndicatorContainer quote />
<div className={`spoiler-input ${this.props.spoiler ? 'spoiler-input--visible' : ''}`} ref={this.setRef} aria-hidden={!this.props.spoiler}>
<AutosuggestInput
@ -280,12 +291,13 @@ class ComposeForm extends ImmutablePureComponent {
</div>
<div className='character-counter__wrapper'>
<CharacterCounter max={500} text={this.getFulltextForCharacterCounting()} />
<CharacterCounter max={2048} text={this.getFulltextForCharacterCounting()} />
</div>
</div>
<div className='compose-form__publish'>
<div className='compose-form__publish-button-wrapper'>
<Button className="compose-form__utilBtns-risa" text={intl.formatMessage(messages.utilBtns_risa)} onClick={this.handleOnRisaSubmit} block />
<Button
type='submit'
text={publishText}
@ -294,6 +306,10 @@ class ComposeForm extends ImmutablePureComponent {
/>
</div>
</div>
<div className="compose-form__utilBtns">
<Button className="compose-form__utilBtns-goji" text={intl.formatMessage(messages.utilBtns_goji)} onClick={this.handleOnGojiSubmit} block />
<Button className="compose-form__utilBtns-harukin" text={intl.formatMessage(messages.utilBtns_harukin)} onClick={this.handleOnHarukinSubmit} block />
</div>
</form>
);
}

View File

@ -6,6 +6,7 @@ import IconButton from '../../../components/icon_button';
import DisplayName from '../../../components/display_name';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import classNames from 'classnames';
import AttachmentList from 'mastodon/components/attachment_list';
const messages = defineMessages({
@ -23,6 +24,7 @@ class ReplyIndicator extends ImmutablePureComponent {
status: ImmutablePropTypes.map,
onCancel: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
quote: PropTypes.bool,
};
handleClick = () => {
@ -37,7 +39,7 @@ class ReplyIndicator extends ImmutablePureComponent {
};
render () {
const { status, intl } = this.props;
const { status, intl, quote } = this.props;
if (!status) {
return null;
@ -46,7 +48,7 @@ class ReplyIndicator extends ImmutablePureComponent {
const content = { __html: status.get('contentHtml') };
return (
<div className='reply-indicator'>
<div className={classNames('reply-indicator', { 'quote-indicator': quote })}>
<div className='reply-indicator__header'>
<div className='reply-indicator__cancel'><IconButton title={intl.formatMessage(messages.cancel)} icon='times' onClick={this.handleClick} inverted /></div>

View File

@ -10,6 +10,11 @@ import {
insertEmojiCompose,
uploadCompose,
} from '../../../actions/compose';
import {
submitGoji,
submitHarukin,
submitRisa
} from '../../../actions/UtilBtns';
const mapStateToProps = state => ({
text: state.getIn(['compose', 'text']),
@ -63,6 +68,18 @@ const mapDispatchToProps = (dispatch) => ({
dispatch(insertEmojiCompose(position, data, needsSpace));
},
onRisaSubmit (textarea) {
dispatch(submitRisa(textarea));
},
onGojiSubmit (textarea) {
dispatch(submitGoji(textarea));
},
onHarukinSubmit (textarea) {
dispatch(submitHarukin(textarea));
},
});
export default connect(mapStateToProps, mapDispatchToProps)(ComposeForm);

View File

@ -1,22 +1,23 @@
import { connect } from 'react-redux';
import { cancelReplyCompose } from '../../../actions/compose';
import { cancelReplyCompose, cancelQuoteCompose } from '../../../actions/compose';
import { makeGetStatus } from '../../../selectors';
import ReplyIndicator from '../components/reply_indicator';
const makeMapStateToProps = () => {
const getStatus = makeGetStatus();
const mapStateToProps = state => {
const mapStateToProps = (state, props) => {
let statusId = state.getIn(['compose', 'id'], null);
let editing = true;
if (statusId === null) {
statusId = state.getIn(['compose', 'in_reply_to']);
statusId = state.getIn(['compose', props.quote ? 'quote_from' : 'in_reply_to']);
editing = false;
}
return {
status: getStatus(state, { id: statusId }),
quote: props.quote,
editing,
};
};
@ -26,8 +27,8 @@ const makeMapStateToProps = () => {
const mapDispatchToProps = dispatch => ({
onCancel () {
dispatch(cancelReplyCompose());
onCancel (quote) {
dispatch(quote ? cancelQuoteCompose() : cancelReplyCompose());
},
});

View File

@ -4,10 +4,11 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import IconButton from 'mastodon/components/icon_button';
import StatusActionBar from 'mastodon/components/status_action_bar';
import classNames from 'classnames';
import { me, boostModal } from 'mastodon/initial_state';
import { defineMessages, injectIntl } from 'react-intl';
import { replyCompose } from 'mastodon/actions/compose';
import { replyCompose, quoteCompose } from 'mastodon/actions/compose';
import { reblog, favourite, unreblog, unfavourite } from 'mastodon/actions/interactions';
import { makeGetStatus } from 'mastodon/selectors';
import { initBoostModal } from 'mastodon/actions/boosts';
@ -20,9 +21,13 @@ const messages = defineMessages({
reblog_private: { id: 'status.reblog_private', defaultMessage: 'Boost with original visibility' },
cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
quote: { id: 'status.quote', defaultMessage: 'Quote' },
cannot_quote: { id: 'status.cannot_quote', defaultMessage: 'This post cannot be quoted' },
favourite: { id: 'status.favourite', defaultMessage: 'Favourite' },
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
quoteConfirm: { id: 'confirmations.quote.confirm', defaultMessage: 'Quote' },
quoteMessage: { id: 'confirmations.quote.message', defaultMessage: 'Quoting now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
open: { id: 'status.open', defaultMessage: 'Expand this status' },
});
@ -135,6 +140,31 @@ class Footer extends ImmutablePureComponent {
}
};
_performQuote = () => {
const { dispatch, status, onClose } = this.props;
const { router } = this.context;
if (onClose) {
onClose();
}
dispatch(quoteCompose(status, router.history));
}
handleQuoteClick = () => {
const { dispatch, askReplyConfirmation, intl } = this.props;
if (askReplyConfirmation) {
dispatch(openModal('CONFIRM', {
message: intl.formatMessage(messages.quoteMessage),
confirm: intl.formatMessage(messages.quoteConfirm),
onConfirm: this._performQuote,
}));
} else {
this._performQuote();
}
}
handleOpenClick = e => {
const { router } = this.context;

View File

@ -2,6 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import IconButton from '../../../components/icon_button';
import StatusActionBar from '../../../components/status_action_bar';
import ImmutablePropTypes from 'react-immutable-proptypes';
import DropdownMenuContainer from '../../../containers/dropdown_menu_container';
import { defineMessages, injectIntl } from 'react-intl';
@ -20,6 +21,8 @@ const messages = defineMessages({
reblog_private: { id: 'status.reblog_private', defaultMessage: 'Boost with original visibility' },
cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
quote: { id: 'status.quote', defaultMessage: 'Quote' },
cannot_quote: { id: 'status.cannot_quote', defaultMessage: 'This post cannot be quoted' },
favourite: { id: 'status.favourite', defaultMessage: 'Favourite' },
bookmark: { id: 'status.bookmark', defaultMessage: 'Bookmark' },
more: { id: 'status.more', defaultMessage: 'More' },
@ -61,6 +64,7 @@ class ActionBar extends React.PureComponent {
relationship: ImmutablePropTypes.map,
onReply: PropTypes.func.isRequired,
onReblog: PropTypes.func.isRequired,
onQuote: PropTypes.func,
onFavourite: PropTypes.func.isRequired,
onBookmark: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
@ -88,6 +92,10 @@ class ActionBar extends React.PureComponent {
this.props.onReblog(this.props.status, e);
};
handleQuoteClick = () => {
this.props.onQuote(this.props.status, this.context.router.history);
}
handleFavouriteClick = () => {
this.props.onFavourite(this.props.status);
};
@ -286,6 +294,7 @@ class ActionBar extends React.PureComponent {
<div className='detailed-status__button'><IconButton title={intl.formatMessage(messages.reply)} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} /></div>
<div className='detailed-status__button' ><IconButton className={classNames({ reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon='retweet' onClick={this.handleReblogClick} /></div>
<div className='detailed-status__button'><IconButton className='star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} /></div>
<div className='detailed-status__button'><IconButton disabled={!publicStatus} title={StatusActionBar.quoteTitle(intl, messages, publicStatus)} icon='quote-right' onClick={this.handleQuoteClick} /></div>
<div className='detailed-status__button'><IconButton className='bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} /></div>
{shareButton}

View File

@ -68,6 +68,7 @@ export default class Card extends React.PureComponent {
defaultWidth: PropTypes.number,
cacheWidth: PropTypes.func,
sensitive: PropTypes.bool,
quote: PropTypes.bool,
};
static defaultProps = {
@ -184,7 +185,7 @@ export default class Card extends React.PureComponent {
}
render () {
const { card, maxDescription, compact } = this.props;
const { card, maxDescription, compact, quote } = this.props;
const { width, embedded, revealed } = this.state;
if (card === null) {
@ -197,7 +198,11 @@ export default class Card extends React.PureComponent {
const className = classnames('status-card', { horizontal, compact, interactive });
const title = interactive ? <a className='status-card__title' href={card.get('url')} title={card.get('title')} rel='noopener noreferrer' target='_blank'><strong>{card.get('title')}</strong></a> : <strong className='status-card__title' title={card.get('title')}>{card.get('title')}</strong>;
const ratio = card.get('width') / card.get('height');
const height = (compact && !embedded) ? (width / (16 / 9)) : (width / ratio);
let height = (compact && !embedded) ? (width / (16 / 9)) : (width / ratio);
if (quote && height) {
height /= 2;
}
const description = (
<div className='status-card__content'>

View File

@ -1,8 +1,10 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import Avatar from '../../../components/avatar';
import DisplayName from '../../../components/display_name';
import { mapStateToProps, quote } from '../../../components/status';
import StatusContent from '../../../components/status_content';
import MediaGallery from '../../../components/media_gallery';
import { Link } from 'react-router-dom';
@ -25,7 +27,7 @@ const messages = defineMessages({
direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' },
});
export default @injectIntl
export default @connect(mapStateToProps) @injectIntl
class DetailedStatus extends ImmutablePureComponent {
static contextTypes = {
@ -37,17 +39,21 @@ class DetailedStatus extends ImmutablePureComponent {
onOpenMedia: PropTypes.func.isRequired,
onOpenVideo: PropTypes.func.isRequired,
onToggleHidden: PropTypes.func.isRequired,
onQuoteToggleHidden: PropTypes.func.isRequired,
onTranslate: PropTypes.func.isRequired,
measureHeight: PropTypes.bool,
onHeightChange: PropTypes.func,
domain: PropTypes.string.isRequired,
compact: PropTypes.bool,
quoteMuted: PropTypes.bool,
showMedia: PropTypes.bool,
showQuoteMedia: PropTypes.bool,
pictureInPicture: ImmutablePropTypes.contains({
inUse: PropTypes.bool,
available: PropTypes.bool,
}),
onToggleMediaVisibility: PropTypes.func,
onQuoteToggleMediaVisibility: PropTypes.func,
};
state = {
@ -56,8 +62,9 @@ class DetailedStatus extends ImmutablePureComponent {
handleAccountClick = (e) => {
if (e.button === 0 && !(e.ctrlKey || e.metaKey) && this.context.router) {
const acct = e.currentTarget.getAttribute('data-acct');
e.preventDefault();
this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`);
this.context.router.history.push(`/@${acct}`);
}
e.stopPropagation();
@ -71,6 +78,19 @@ class DetailedStatus extends ImmutablePureComponent {
this.props.onToggleHidden(this.props.status);
};
handleExpandedQuoteToggle = () => {
this.props.onQuoteToggleHidden(this.props.status);
}
handleQuoteClick = () => {
if (!this.context.router) {
return;
}
const { status } = this.props;
this.context.router.history.push(`/statuses/${status.getIn(['quote', 'id'])}`);
}
_measureHeight (heightJustChanged) {
if (this.props.measureHeight && this.node) {
scheduleIdleTask(() => this.node && this.setState({ height: Math.ceil(this.node.scrollHeight) + 1 }));
@ -112,13 +132,12 @@ class DetailedStatus extends ImmutablePureComponent {
render () {
const status = (this.props.status && this.props.status.get('reblog')) ? this.props.status.get('reblog') : this.props.status;
const outerStyle = { boxSizing: 'border-box' };
const { intl, compact, pictureInPicture } = this.props;
const { intl, compact, pictureInPicture, quoteMuted } = this.props;
if (!status) {
return null;
}
let media = '';
let applicationLink = '';
let reblogLink = '';
let reblogIcon = 'retweet';
@ -129,63 +148,84 @@ class DetailedStatus extends ImmutablePureComponent {
outerStyle.height = `${this.state.height}px`;
}
if (pictureInPicture.get('inUse')) {
media = <PictureInPicturePlaceholder />;
} else if (status.get('media_attachments').size > 0) {
if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
const attachment = status.getIn(['media_attachments', 0]);
const identity = (status, _0, _1, quote = false) => (
<a href={`/@${status.getIn(['account', 'acct'])}`} onClick={this.handleAccountClick} data-acct={status.getIn(['account', 'acct'])} className='detailed-status__display-name'>
<div className='detailed-status__display-avatar'><Avatar account={status.get('account')} size={quote ? 18 : 46} /></div>
<DisplayName account={status.get('account')} localDomain={this.props.domain} />
</a>
);
media = (
<Audio
src={attachment.get('url')}
alt={attachment.get('description')}
duration={attachment.getIn(['meta', 'original', 'duration'], 0)}
poster={attachment.get('preview_url') || status.getIn(['account', 'avatar_static'])}
backgroundColor={attachment.getIn(['meta', 'colors', 'background'])}
foregroundColor={attachment.getIn(['meta', 'colors', 'foreground'])}
accentColor={attachment.getIn(['meta', 'colors', 'accent'])}
sensitive={status.get('sensitive')}
visible={this.props.showMedia}
blurhash={attachment.get('blurhash')}
height={150}
onToggleVisibility={this.props.onToggleMediaVisibility}
/>
);
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
const attachment = status.getIn(['media_attachments', 0]);
const media = (status, quote = false) => {
if (pictureInPicture.get('inUse')) {
return <PictureInPicturePlaceholder />;
} else if (status.get('media_attachments').size > 0) {
if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
const attachment = status.getIn(['media_attachments', 0]);
media = (
<Video
preview={attachment.get('preview_url')}
frameRate={attachment.getIn(['meta', 'original', 'frame_rate'])}
blurhash={attachment.get('blurhash')}
src={attachment.get('url')}
alt={attachment.get('description')}
width={300}
height={150}
inline
onOpenVideo={this.handleOpenVideo}
return (
<Audio
src={attachment.get('url')}
alt={attachment.get('description')}
duration={attachment.getIn(['meta', 'original', 'duration'], 0)}
poster={attachment.get('preview_url') || status.getIn(['account', 'avatar_static'])}
backgroundColor={attachment.getIn(['meta', 'colors', 'background'])}
foregroundColor={attachment.getIn(['meta', 'colors', 'foreground'])}
accentColor={attachment.getIn(['meta', 'colors', 'accent'])}
sensitive={status.get('sensitive')}
visible={this.props.showMedia}
blurhash={attachment.get('blurhash')}
height={150}
onToggleVisibility={this.props.onToggleMediaVisibility}
quote={quote}
/>
);
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
const attachment = status.getIn(['media_attachments', 0]);
return (
<Video
preview={attachment.get('preview_url')}
frameRate={attachment.getIn(['meta', 'original', 'frame_rate'])}
blurhash={attachment.get('blurhash')}
src={attachment.get('url')}
alt={attachment.get('description')}
width={300}
height={150}
inline
onOpenVideo={this.handleOpenVideo}
sensitive={status.get('sensitive')}
visible={this.props.showMedia}
onToggleVisibility={this.props.onToggleMediaVisibility}
quote={quote}
/>
);
} else {
return (
<MediaGallery
standalone
sensitive={status.get('sensitive')}
media={status.get('media_attachments')}
height={300}
onOpenMedia={this.props.onOpenMedia}
visible={this.props.showMedia}
onToggleVisibility={this.props.onToggleMediaVisibility}
quote={quote}
/>
);
}
} else if (status.get('spoiler_text').length === 0) {
return (
<Card
sensitive={status.get('sensitive')}
visible={this.props.showMedia}
onToggleVisibility={this.props.onToggleMediaVisibility}
/>
);
} else {
media = (
<MediaGallery
standalone
sensitive={status.get('sensitive')}
media={status.get('media_attachments')}
height={300}
onOpenMedia={this.props.onOpenMedia}
visible={this.props.showMedia}
onToggleVisibility={this.props.onToggleMediaVisibility}
card={status.get('card', null)}
quote={quote}
/>
);
}
} else if (status.get('spoiler_text').length === 0) {
media = <Card sensitive={status.get('sensitive')} onOpenMedia={this.props.onOpenMedia} card={status.get('card', null)} />;
}
return null;
};
if (status.get('application')) {
applicationLink = <React.Fragment> · <a className='detailed-status__application' href={status.getIn(['application', 'website'])} target='_blank' rel='noopener noreferrer'>{status.getIn(['application', 'name'])}</a></React.Fragment>;
@ -261,10 +301,7 @@ class DetailedStatus extends ImmutablePureComponent {
return (
<div style={outerStyle}>
<div ref={this.setRef} className={classNames('detailed-status', `detailed-status-${status.get('visibility')}`, { compact })}>
<a href={`/@${status.getIn(['account', 'acct'])}`} onClick={this.handleAccountClick} className='detailed-status__display-name'>
<div className='detailed-status__display-avatar'><Avatar account={status.get('account')} size={46} /></div>
<DisplayName account={status.get('account')} localDomain={this.props.domain} />
</a>
{identity(status, null, null, false)}
<StatusContent
status={status}
@ -273,7 +310,9 @@ class DetailedStatus extends ImmutablePureComponent {
onTranslate={this.handleTranslate}
/>
{media}
{media(status, false)}
{quote(status, false, quoteMuted, this.handleQuoteClick, this.handleExpandedQuoteToggle, identity, media, this.context.router)}
<div className='detailed-status__meta'>
<a className='detailed-status__datetime' href={`/@${status.getIn(['account', 'acct'])}\/${status.get('id')}`} target='_blank' rel='noopener noreferrer'>

View File

@ -20,6 +20,8 @@ import {
deleteStatus,
hideStatus,
revealStatus,
hideQuote,
revealQuote,
} from '../../../actions/statuses';
import { initMuteModal } from '../../../actions/mutes';
import { initBlockModal } from '../../../actions/blocks';
@ -165,6 +167,14 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
}
},
onQuoteToggleHidden (status) {
if (status.get('quote_hidden')) {
dispatch(revealQuote(status.get('id')));
} else {
dispatch(hideQuote(status.get('id')));
}
},
});
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(DetailedStatus));

View File

@ -23,6 +23,7 @@ import {
} from '../../actions/interactions';
import {
replyCompose,
quoteCompose,
mentionCompose,
directCompose,
} from '../../actions/compose';
@ -35,6 +36,8 @@ import {
revealStatus,
translateStatus,
undoStatusTranslation,
hideQuote,
revealQuote,
} from '../../actions/statuses';
import {
unblockAccount,
@ -73,6 +76,8 @@ const messages = defineMessages({
detailedStatus: { id: 'status.detailed_status', defaultMessage: 'Detailed conversation view' },
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
quoteConfirm: { id: 'confirmations.quote.confirm', defaultMessage: 'Quote' },
quoteMessage: { id: 'confirmations.quote.message', defaultMessage: 'Quoting now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' },
});
@ -205,6 +210,7 @@ class Status extends ImmutablePureComponent {
state = {
fullscreen: false,
showMedia: defaultMediaVisibility(this.props.status),
showQuoteMedia: defaultMediaVisibility(this.props.status ? this.props.status.get('quote', null) : null),
loadedStatusId: undefined,
};
@ -227,7 +233,11 @@ class Status extends ImmutablePureComponent {
}
if (nextProps.status && nextProps.status.get('id') !== this.state.loadedStatusId) {
this.setState({ showMedia: defaultMediaVisibility(nextProps.status), loadedStatusId: nextProps.status.get('id') });
this.setState({
showMedia: defaultMediaVisibility(nextProps.status),
showQuoteMedia: defaultMediaVisibility(nextProps.status.get('quote', null)),
loadedStatusId: nextProps.status.get('id'),
});
}
}
@ -235,6 +245,10 @@ class Status extends ImmutablePureComponent {
this.setState({ showMedia: !this.state.showMedia });
};
handleToggleQuoteMediaVisibility = () => {
this.setState({ showQuoteMedia: !this.state.showQuoteMedia });
}
handleFavouriteClick = (status) => {
const { dispatch } = this.props;
const { signedIn } = this.context.identity;
@ -285,6 +299,19 @@ class Status extends ImmutablePureComponent {
}
};
handleQuoteClick = (status) => {
let { askReplyConfirmation, dispatch, intl } = this.props;
if (askReplyConfirmation) {
dispatch(openModal('CONFIRM', {
message: intl.formatMessage(messages.quoteMessage),
confirm: intl.formatMessage(messages.quoteConfirm),
onConfirm: () => dispatch(quoteCompose(status, this.context.router.history)),
}));
} else {
dispatch(quoteCompose(status, this.context.router.history));
}
}
handleModalReblog = (status, privacy) => {
this.props.dispatch(reblog(status, privacy));
};
@ -388,6 +415,14 @@ class Status extends ImmutablePureComponent {
}
};
handleQuoteToggleHidden = (status) => {
if (status.get('quote_hidden')) {
this.props.dispatch(revealQuote(status.get('id')));
} else {
this.props.dispatch(hideQuote(status.get('id')));
}
}
handleToggleAll = () => {
const { status, ancestorsIds, descendantsIds } = this.props;
const statusIds = [status.get('id')].concat(ancestorsIds.toJS(), descendantsIds.toJS());
@ -640,9 +675,12 @@ class Status extends ImmutablePureComponent {
onOpenMedia={this.handleOpenMedia}
onToggleHidden={this.handleToggleHidden}
onTranslate={this.handleTranslate}
onQuoteToggleHidden={this.handleQuoteToggleHidden}
domain={domain}
showMedia={this.state.showMedia}
showQuoteMedia={this.state.showQuoteMedia}
onToggleMediaVisibility={this.handleToggleMediaVisibility}
onToggleQuoteMediaVisibility={this.handleToggleQuoteMediaVisibility}
pictureInPicture={pictureInPicture}
/>
@ -652,6 +690,7 @@ class Status extends ImmutablePureComponent {
onReply={this.handleReplyClick}
onFavourite={this.handleFavouriteClick}
onReblog={this.handleReblogClick}
onQuote={this.handleQuoteClick}
onBookmark={this.handleBookmarkClick}
onDelete={this.handleDeleteClick}
onEdit={this.handleEditClick}

View File

@ -21,8 +21,15 @@ import {
} from '../../ui/util/async-components';
import ComposePanel from './compose_panel';
import NavigationPanel from './navigation_panel';
import TabsBar from './tabs_bar';
import { place_tab_bar_at_bottom } from 'mastodon/initial_state';
import { supportsPassiveEvents } from 'detect-passive-events';
import { scrollRight } from '../../../scroll';
import { Link } from 'react-router-dom';
import classNames from 'classnames';
import Icon from 'mastodon/components/icon';
const shouldHideFAB = path => path.match(/^\/statuses\/|^\/search|^\/getting-started/);
const componentMap = {
'COMPOSE': Compose,
@ -138,26 +145,55 @@ export default class ColumnsArea extends ImmutablePureComponent {
const { renderComposePanel } = this.state;
if (singleColumn) {
return (
<div className='columns-area__panels'>
<div className='columns-area__panels__pane columns-area__panels__pane--compositional'>
<div className='columns-area__panels__pane__inner'>
{renderComposePanel && <ComposePanel />}
if (place_tab_bar_at_bottom) {
return (
<div className='columns-area__panels tab-ber-bottom'>
<div className='columns-area__panels__pane columns-area__panels__pane--compositional'>
<div className='columns-area__panels__pane__inner'>
{renderComposePanel && <ComposePanel />}
</div>
</div>
<div className='columns-area__panels__main timeline'>
<div className='tabs-bar__wrapper'><div id='tabs-bar__portal' /></div>
<div className='columns-area columns-area--mobile'>{children}</div>
</div>
<div className='columns-area__panels__main navber'>
{location.pathname !== '/publish' && <Link to='/publish' className='button bottom_right'><Icon id='pencil' fixedWidth /></Link>}
<TabsBar key='tabs' />
</div>
<div className='columns-area__panels__pane columns-area__panels__pane--start columns-area__panels__pane--navigational columns-area__panels__pane-tab-ber'>
<div className='columns-area__panels__pane__inner'>
<NavigationPanel />
</div>
</div>
</div>
);
} else {
return (
<div className='columns-area__panels'>
<div className='columns-area__panels__pane columns-area__panels__pane--compositional'>
<div className='columns-area__panels__pane__inner'>
{renderComposePanel && <ComposePanel />}
</div>
</div>
<div className='columns-area__panels__main'>
<div className='tabs-bar__wrapper'><div id='tabs-bar__portal' /></div>
<div className='columns-area columns-area--mobile'>{children}</div>
</div>
<div className='columns-area__panels__pane columns-area__panels__pane--start columns-area__panels__pane--navigational'>
<div className='columns-area__panels__pane__inner'>
<NavigationPanel />
</div>
</div>
</div>
<div className='columns-area__panels__main'>
<div className='tabs-bar__wrapper'><div id='tabs-bar__portal' /></div>
<div className='columns-area columns-area--mobile'>{children}</div>
</div>
<div className='columns-area__panels__pane columns-area__panels__pane--start columns-area__panels__pane--navigational'>
<div className='columns-area__panels__pane__inner'>
<NavigationPanel />
</div>
</div>
</div>
);
);
}
}
return (

View File

@ -0,0 +1,96 @@
import React from 'react';
import PropTypes from 'prop-types';
import { NavLink, withRouter } from 'react-router-dom';
import { FormattedMessage, injectIntl } from 'react-intl';
import NotificationsCounterIcon from './notifications_counter_icon';
import { place_tab_bar_at_bottom, show_tab_bar_label } from 'mastodon/initial_state';
import classNames from 'classnames';
import { debounce } from 'lodash';
import { isUserTouching } from '../../../is_mobile';
import Icon from 'mastodon/components/icon';
export const links = [
<NavLink className='tabs-bar__link' to='/home' data-preview-title-id='column.home' data-preview-icon='home' ><Icon id='home' fixedWidth /></NavLink>,
<NavLink className='tabs-bar__link' to='/notifications' data-preview-title-id='column.notifications' data-preview-icon='bell' ><NotificationsCounterIcon /></NavLink>,
<NavLink className='tabs-bar__link' to='/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><Icon id='users' fixedWidth /></NavLink>,
<NavLink className='tabs-bar__link' exact to='/public' data-preview-title-id='column.public' data-preview-icon='globe' ><Icon id='globe' fixedWidth /></NavLink>,
<NavLink className='tabs-bar__link optional' to='/search' data-preview-title-id='tabs_bar.search' data-preview-icon='bell' ><Icon id='search' fixedWidth /></NavLink>,
<NavLink className='tabs-bar__link hamburger' to='/getting-started' data-preview-title-id='getting_started.heading' data-preview-icon='bars' ><Icon id='bars' fixedWidth /></NavLink>,
];
export function getIndex (path) {
return links.findIndex(link => link.props.to === path);
}
export function getLink (index) {
return links[index].props.to;
}
export default @injectIntl
@withRouter
class TabsBar extends React.PureComponent {
static propTypes = {
intl: PropTypes.object.isRequired,
history: PropTypes.object.isRequired,
};
setRef = ref => {
this.node = ref;
}
handleClick = (e) => {
// Only apply optimization for touch devices, which we assume are slower
// We thus avoid the 250ms delay for non-touch devices and the lag for touch devices
if (isUserTouching()) {
e.preventDefault();
e.persist();
requestAnimationFrame(() => {
const tabs = Array(...this.node.querySelectorAll('.tabs-bar__link'));
const currentTab = tabs.find(tab => tab.classList.contains('active'));
const nextTab = tabs.find(tab => tab.contains(e.target));
const { props: { to } } = links[Array(...this.node.childNodes).indexOf(nextTab)];
if (currentTab !== nextTab) {
if (currentTab) {
currentTab.classList.remove('active');
}
const listener = debounce(() => {
nextTab.removeEventListener('transitionend', listener);
this.props.history.push(to);
}, 50);
nextTab.addEventListener('transitionend', listener);
nextTab.classList.add('active');
}
});
}
}
static contextTypes = {
router: PropTypes.object.isRequired,
identity: PropTypes.object.isRequired,
};
static propTypes = {
intl: PropTypes.object.isRequired,
};
render () {
const { intl: { formatMessage } } = this.props;
return (
<div className='tabs-bar__wrapper'>
<nav className={classNames('tabs-bar', { 'bottom-bar': place_tab_bar_at_bottom })} ref={this.setRef}>
{links.map(link => React.cloneElement(link, { key: link.props.to, className: classNames(link.props.className, { 'short-label': show_tab_bar_label }), onClick: this.handleClick, 'aria-label': formatMessage({ id: link.props['data-preview-title-id'] }) }))}
</nav>
<div id='tabs-bar__portal' />
</div>
);
}
}

View File

@ -121,6 +121,7 @@ class Video extends React.PureComponent {
autoPlay: PropTypes.bool,
volume: PropTypes.number,
muted: PropTypes.bool,
quote: PropTypes.bool,
componentIndex: PropTypes.number,
autoFocus: PropTypes.bool,
};
@ -524,7 +525,7 @@ class Video extends React.PureComponent {
}
render () {
const { preview, src, inline, onOpenVideo, onCloseVideo, intl, alt, detailed, sensitive, editable, blurhash, autoFocus } = this.props;
const { preview, src, inline, onOpenVideo, onCloseVideo, intl, alt, detailed, sensitive, editable, blurhash, autoFocus, quote } = this.props;
const { containerWidth, currentTime, duration, volume, buffer, dragging, paused, fullscreen, hovered, muted, revealed } = this.state;
const progress = Math.min((currentTime / duration) * 100, 100);
const playerStyle = {};
@ -538,6 +539,11 @@ class Video extends React.PureComponent {
playerStyle.height = height;
}
if (quote && height) {
height /= 2;
playerStyle.height = height;
}
let preload;
if (this.props.currentTime || fullscreen || dragging) {

View File

@ -135,5 +135,7 @@ export const version = getMeta('version');
export const translationEnabled = getMeta('translation_enabled');
export const languages = initialState?.languages;
export const statusPageUrl = getMeta('status_page_url');
export const place_tab_bar_at_bottom = getMeta('place_tab_bar_at_bottom');
export const show_tab_bar_label = getMeta('show_tab_bar_label');
export default initialState;

View File

@ -607,6 +607,14 @@
"defaultMessage": "This post cannot be boosted",
"id": "status.cannot_reblog"
},
{
"defaultMessage": "Quote",
"id": "status.quote"
},
{
"defaultMessage": "This post cannot be quoted",
"id": "status.cannot_quote"
},
{
"defaultMessage": "Favourite",
"id": "status.favourite"
@ -764,6 +772,14 @@
{
"defaultMessage": "Replied to {name}",
"id": "status.replied_to"
},
{
"defaultMessage": "Muted quote",
"id": "status.muted_quote"
},
{
"defaultMessage": "Unlisted quote",
"id": "status.unlisted_quote"
}
],
"path": "app/javascript/mastodon/components/status.json"
@ -3716,6 +3732,14 @@
"defaultMessage": "Detailed conversation view",
"id": "status.detailed_status"
},
{
"defaultMessage": "Quote",
"id": "confirmations.quote.confirm"
},
{
"defaultMessage": "Quoting now will overwrite the message you are currently composing. Are you sure you want to proceed?",
"id": "confirmations.quote.message"
},
{
"defaultMessage": "Reply",
"id": "confirmations.reply.confirm"
@ -4335,4 +4359,4 @@
],
"path": "app/javascript/mastodon/features/video/index.json"
}
]
]

View File

@ -169,6 +169,8 @@
"confirmations.mute.confirm": "Mute",
"confirmations.mute.explanation": "This will hide posts from them and posts mentioning them, but it will still allow them to see your posts and follow you.",
"confirmations.mute.message": "Are you sure you want to mute {name}?",
"confirmations.quote.confirm": "Quote",
"confirmations.quote.message": "Quoting now will overwrite the message you are currently composing. Are you sure you want to proceed?",
"confirmations.redraft.confirm": "Delete & redraft",
"confirmations.redraft.message": "Are you sure you want to delete this post and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.",
"confirmations.reply.confirm": "Reply",
@ -553,6 +555,7 @@
"status.block": "Block @{name}",
"status.bookmark": "Bookmark",
"status.cancel_reblog_private": "Unboost",
"status.cannot_quote": "This post cannot be quoted",
"status.cannot_reblog": "This post cannot be boosted",
"status.copy": "Copy link to post",
"status.delete": "Delete",
@ -574,9 +577,11 @@
"status.more": "More",
"status.mute": "Mute @{name}",
"status.mute_conversation": "Mute conversation",
"status.muted_quote": "Muted quote",
"status.open": "Expand this post",
"status.pin": "Pin on profile",
"status.pinned": "Pinned post",
"status.quote": "Quote",
"status.read_more": "Read more",
"status.reblog": "Boost",
"status.reblog_private": "Boost with original visibility",
@ -599,6 +604,7 @@
"status.translate": "Translate",
"status.translated_from_with": "Translated from {lang} using {provider}",
"status.uncached_media_warning": "Not available",
"status.unlisted_quote": "Unlisted quote",
"status.unmute_conversation": "Unmute conversation",
"status.unpin": "Unpin from profile",
"subscribed_languages.lead": "Only posts in selected languages will appear on your home and list timelines after the change. Select none to receive posts in all languages.",

View File

@ -21,13 +21,13 @@
"account.browse_more_on_origin_server": "リモートで表示",
"account.cancel_follow_request": "フォローリクエストの取り消し",
"account.direct": "@{name}さんにダイレクトメッセージ",
"account.disable_notifications": "@{name}さんの投稿時の通知を停止",
"account.disable_notifications": "@{name}さんのトゥート時の通知を停止",
"account.domain_blocked": "ドメインブロック中",
"account.edit_profile": "プロフィール編集",
"account.enable_notifications": "@{name}さんの投稿時に通知",
"account.enable_notifications": "@{name}さんのトゥート時に通知",
"account.endorse": "プロフィールで紹介する",
"account.featured_tags.last_status_at": "最終投稿 {date}",
"account.featured_tags.last_status_never": "投稿がありません",
"account.featured_tags.last_status_at": "最終トゥート {date}",
"account.featured_tags.last_status_never": "トゥートがありません",
"account.featured_tags.title": "{name}の注目ハッシュタグ",
"account.follow": "フォロー",
"account.followers": "フォロワー",
@ -50,14 +50,14 @@
"account.mute_notifications": "@{name}さんからの通知を受け取らない",
"account.muted": "ミュート済み",
"account.open_original_page": "元のページを開く",
"account.posts": "投稿",
"account.posts_with_replies": "投稿と返信",
"account.posts": "トゥート",
"account.posts_with_replies": "トゥートと返信",
"account.report": "@{name}さんを通報",
"account.requested": "フォロー承認待ちです。クリックしてキャンセル",
"account.requested_follow": "{name}さんがあなたにフォローリクエストしました",
"account.share": "@{name}さんのプロフィールを共有する",
"account.show_reblogs": "@{name}さんからのブーストを表示",
"account.statuses_counter": "{counter} 投稿",
"account.statuses_counter": "{counter} トゥート",
"account.unblock": "@{name}さんのブロックを解除",
"account.unblock_domain": "{domain}のブロックを解除",
"account.unblock_short": "ブロック解除",
@ -111,7 +111,7 @@
"column.lists": "リスト",
"column.mutes": "ミュートしたユーザー",
"column.notifications": "通知",
"column.pins": "固定された投稿",
"column.pins": "固定されたトゥート",
"column.public": "連合タイムライン",
"column_back_button.label": "戻る",
"column_header.hide_settings": "設定を隠す",
@ -127,9 +127,9 @@
"compose.language.change": "言語を変更",
"compose.language.search": "言語を検索...",
"compose_form.direct_message_warning_learn_more": "もっと詳しく",
"compose_form.encryption_warning": "Mastodonの投稿はエンドツーエンド暗号化に対応していません。安全に送受信されるべき情報をMastodonで共有しないでください。",
"compose_form.hashtag_warning": "この投稿は公開設定ではないのでハッシュタグの一覧に表示されません。公開投稿だけがハッシュタグで検索できます。",
"compose_form.lock_disclaimer": "あなたのアカウントは{locked}になっていません。誰でもあなたをフォローすることができ、フォロワー限定の投稿を見ることができます。",
"compose_form.encryption_warning": "Mastodonのトゥートはエンドツーエンド暗号化に対応していません。安全に送受信されるべき情報をMastodonで共有しないでください。",
"compose_form.hashtag_warning": "このトゥートは公開設定ではないのでハッシュタグの一覧に表示されません。公開トゥートだけがハッシュタグで検索できます。",
"compose_form.lock_disclaimer": "あなたのアカウントは{locked}になっていません。誰でもあなたをフォローすることができ、フォロワー限定のトゥートを見ることができます。",
"compose_form.lock_disclaimer.lock": "承認制",
"compose_form.placeholder": "今なにしてる?",
"compose_form.poll.add_option": "追加",
@ -138,8 +138,8 @@
"compose_form.poll.remove_option": "この項目を削除",
"compose_form.poll.switch_to_multiple": "複数選択に変更",
"compose_form.poll.switch_to_single": "単一選択に変更",
"compose_form.publish": "投稿",
"compose_form.publish_form": "投稿",
"compose_form.publish": "トゥート",
"compose_form.publish_form": "トゥート",
"compose_form.publish_loud": "{publish}",
"compose_form.save_changes": "変更を保存",
"compose_form.sensitive.hide": "メディアを閲覧注意にする",
@ -148,6 +148,9 @@
"compose_form.spoiler.marked": "本文は警告の後ろに隠されます",
"compose_form.spoiler.unmarked": "本文は隠されていません",
"compose_form.spoiler_placeholder": "ここに警告を書いてください",
"compose_form.utilBtns_goji": "誤字盛!",
"compose_form.utilBtns_harukin": "はるきん焼却",
"compose_form.utilBtns_risa": "りさ姉",
"confirmation_modal.cancel": "キャンセル",
"confirmations.block.block_and_report": "ブロックし通報",
"confirmations.block.confirm": "ブロック",
@ -165,10 +168,12 @@
"confirmations.logout.confirm": "ログアウト",
"confirmations.logout.message": "本当にログアウトしますか?",
"confirmations.mute.confirm": "ミュート",
"confirmations.mute.explanation": "これにより相手の投稿と返信は見えなくなりますが、相手はあなたをフォローし続け投稿を見ることができます。",
"confirmations.mute.explanation": "これにより相手のトゥートと返信は見えなくなりますが、相手はあなたをフォローし続けトゥートを見ることができます。",
"confirmations.mute.message": "本当に{name}さんをミュートしますか?",
"confirmations.quote.confirm": "引用",
"confirmations.quote.message": "今引用すると現在作成中のメッセージが上書きされます。本当に実行しますか?",
"confirmations.redraft.confirm": "削除して下書きに戻す",
"confirmations.redraft.message": "本当にこの投稿を削除して下書きに戻しますか? この投稿へのお気に入り登録やブーストは失われ、返信は孤立することになります。",
"confirmations.redraft.message": "本当にこのトゥートを削除して下書きに戻しますか? このトゥートへのお気に入り登録やブーストは失われ、返信は孤立することになります。",
"confirmations.reply.confirm": "返信",
"confirmations.reply.message": "今返信すると現在作成中のメッセージが上書きされます。本当に実行しますか?",
"confirmations.unfollow.confirm": "フォロー解除",
@ -185,12 +190,12 @@
"directory.recently_active": "最近の活動順",
"disabled_account_banner.account_settings": "アカウント設定",
"disabled_account_banner.text": "あなたのアカウント『{disabledAccount}』は現在無効になっています。",
"dismissable_banner.community_timeline": "これらは{domain}がホストしている人たちの最新の公開投稿です。",
"dismissable_banner.community_timeline": "これらは{domain}がホストしている人たちの最新の公開トゥートです。",
"dismissable_banner.dismiss": "閉じる",
"dismissable_banner.explore_links": "これらのニュース記事は現在分散型ネットワークの他のサーバーの人たちに話されています。",
"dismissable_banner.explore_statuses": "分散型ネットワーク内の他のサーバーのこれらの投稿は現在このサーバー上で注目されています。",
"dismissable_banner.explore_statuses": "分散型ネットワーク内の他のサーバーのこれらのトゥートは現在このサーバー上で注目されています。",
"dismissable_banner.explore_tags": "これらのハッシュタグは現在分散型ネットワークの他のサーバーの人たちに話されています。",
"dismissable_banner.public_timeline": "これらの投稿はこのサーバーが知っている分散型ネットワークの他のサーバーの人たちの最新の公開投稿です。",
"dismissable_banner.public_timeline": "これらのトゥートはこのサーバーが知っている分散型ネットワークの他のサーバーの人たちの最新の公開トゥートです。",
"embed.instructions": "下記のコードをコピーしてウェブサイトに埋め込みます。",
"embed.preview": "表示例:",
"emoji_button.activity": "活動",
@ -209,7 +214,7 @@
"emoji_button.symbols": "記号",
"emoji_button.travel": "旅行と場所",
"empty_column.account_suspended": "アカウントは停止されています",
"empty_column.account_timeline": "投稿がありません!",
"empty_column.account_timeline": "トゥートがありません!",
"empty_column.account_unavailable": "プロフィールは利用できません",
"empty_column.blocks": "まだ誰もブロックしていません。",
"empty_column.bookmarked_statuses": "まだ何もブックマーク登録していません。ブックマーク登録するとここに表示されます。",
@ -225,11 +230,11 @@
"empty_column.hashtag": "このハッシュタグはまだ使われていません。",
"empty_column.home": "ホームタイムラインはまだ空っぽです。誰かフォローして埋めてみましょう。 {suggestions}",
"empty_column.home.suggestions": "おすすめを見る",
"empty_column.list": "このリストにはまだなにもありません。このリストのメンバーが新しい投稿をするとここに表示されます。",
"empty_column.list": "このリストにはまだなにもありません。このリストのメンバーが新しいトゥートをするとここに表示されます。",
"empty_column.lists": "まだリストがありません。リストを作るとここに表示されます。",
"empty_column.mutes": "まだ誰もミュートしていません。",
"empty_column.notifications": "まだ通知がありません。他の人とふれ合って会話を始めましょう。",
"empty_column.public": "ここにはまだ何もありません! 公開で何かを投稿したり、他のサーバーのユーザーをフォローしたりしていっぱいにしましょう",
"empty_column.public": "ここにはまだ何もありません! 公開で何かをトゥートしたり、他のサーバーのユーザーをフォローしたりしていっぱいにしましょう",
"error.unexpected_crash.explanation": "不具合かブラウザの互換性問題のため、このページを正しく表示できませんでした。",
"error.unexpected_crash.explanation_addons": "このページは正しく表示できませんでした。このエラーはブラウザのアドオンや自動翻訳ツールによって引き起こされることがあります。",
"error.unexpected_crash.next_steps": "ページの再読み込みをお試しください。それでも解決しない場合、別のブラウザかアプリを使えば使用できることがあります。",
@ -240,27 +245,27 @@
"explore.suggested_follows": "おすすめ",
"explore.title": "エクスプローラー",
"explore.trending_links": "ニュース",
"explore.trending_statuses": "投稿",
"explore.trending_statuses": "トゥート",
"explore.trending_tags": "ハッシュタグ",
"filter_modal.added.context_mismatch_explanation": "このフィルターカテゴリーはあなたがアクセスした投稿のコンテキストには適用されません。この投稿のコンテキストでもフィルターを適用するにはフィルターを編集する必要があります。",
"filter_modal.added.context_mismatch_explanation": "このフィルターカテゴリーはあなたがアクセスしたトゥートのコンテキストには適用されません。このトゥートのコンテキストでもフィルターを適用するにはフィルターを編集する必要があります。",
"filter_modal.added.context_mismatch_title": "コンテキストが一致しません!",
"filter_modal.added.expired_explanation": "このフィルターカテゴリは有効期限が切れています。適用するには有効期限を更新してください。",
"filter_modal.added.expired_title": "フィルターの有効期限が切れています!",
"filter_modal.added.review_and_configure": "このフィルターカテゴリーを確認して設定するには、{settings_link}に移動します。",
"filter_modal.added.review_and_configure_title": "フィルター設定",
"filter_modal.added.settings_link": "設定",
"filter_modal.added.short_explanation": "この投稿はフィルターカテゴリー『{title}』に追加されました。",
"filter_modal.added.short_explanation": "このトゥートはフィルターカテゴリー『{title}』に追加されました。",
"filter_modal.added.title": "フィルターを追加しました!",
"filter_modal.select_filter.context_mismatch": "このコンテキストには当てはまりません",
"filter_modal.select_filter.expired": "期限切れ",
"filter_modal.select_filter.prompt_new": "新しいカテゴリー: {name}",
"filter_modal.select_filter.search": "検索または新規作成",
"filter_modal.select_filter.subtitle": "既存のカテゴリーを使用するか新規作成します",
"filter_modal.select_filter.title": "この投稿をフィルターする",
"filter_modal.title.status": "投稿をフィルターする",
"filter_modal.select_filter.title": "このトゥートをフィルターする",
"filter_modal.title.status": "トゥートをフィルターする",
"follow_recommendations.done": "完了",
"follow_recommendations.heading": "投稿を見たい人をフォローしてください!ここにおすすめがあります。",
"follow_recommendations.lead": "あなたがフォローしている人の投稿は、ホームフィードに時系列で表示されます。いつでも簡単に解除できるので、気軽にフォローしてみてください!",
"follow_recommendations.heading": "トゥートを見たい人をフォローしてください!ここにおすすめがあります。",
"follow_recommendations.lead": "あなたがフォローしている人のトゥートは、ホームフィードに時系列で表示されます。いつでも簡単に解除できるので、気軽にフォローしてみてください!",
"follow_request.authorize": "許可",
"follow_request.reject": "拒否",
"follow_requests.unlocked_explanation": "あなたのアカウントは承認制ではありませんが、{domain}のスタッフはこれらのアカウントからのフォローリクエストの確認が必要であると判断しました。",
@ -291,18 +296,18 @@
"home.column_settings.show_replies": "返信表示",
"home.hide_announcements": "お知らせを隠す",
"home.show_announcements": "お知らせを表示",
"interaction_modal.description.favourite": "Mastodonのアカウントでこの投稿をお気に入りに入れて投稿者に感謝を知らせたり保存することができます。",
"interaction_modal.description.follow": "Mastodonのアカウントで{name}さんをフォローしてホームフィードで投稿を受け取れます。",
"interaction_modal.description.reblog": "Mastodonのアカウントでこの投稿をブーストして自分のフォロワーに共有できます。",
"interaction_modal.description.reply": "Mastodonのアカウントでこの投稿に反応できます。",
"interaction_modal.description.favourite": "Mastodonのアカウントでこのトゥートをお気に入りに入れてトゥート者に感謝を知らせたり保存することができます。",
"interaction_modal.description.follow": "Mastodonのアカウントで{name}さんをフォローしてホームフィードでトゥートを受け取れます。",
"interaction_modal.description.reblog": "Mastodonのアカウントでこのトゥートをブーストして自分のフォロワーに共有できます。",
"interaction_modal.description.reply": "Mastodonのアカウントでこのトゥートに反応できます。",
"interaction_modal.on_another_server": "別のサーバー",
"interaction_modal.on_this_server": "このサーバー",
"interaction_modal.other_server_instructions": "このURLをお気に入りのMastodonアプリやMastodonサーバーのWebインターフェースの検索フィールドにコピーして貼り付けます。",
"interaction_modal.preamble": "Mastodonは分散化されているためアカウントを持っていなくても別のMastodonサーバーまたは互換性のあるプラットフォームでホストされているアカウントを使用できます。",
"interaction_modal.title.favourite": "{name}さんの投稿をお気に入り",
"interaction_modal.title.favourite": "{name}さんのトゥートをお気に入り",
"interaction_modal.title.follow": "{name}さんをフォロー",
"interaction_modal.title.reblog": "{name}さんの投稿をブースト",
"interaction_modal.title.reply": "{name}さんの投稿にリプライ",
"interaction_modal.title.reblog": "{name}さんのトゥートをブースト",
"interaction_modal.title.reply": "{name}さんのトゥートにリプライ",
"intervals.full.days": "{number}日",
"intervals.full.hours": "{number}時間",
"intervals.full.minutes": "{number}分",
@ -310,11 +315,11 @@
"keyboard_shortcuts.blocked": "ブロックしたユーザーのリストを開く",
"keyboard_shortcuts.boost": "ブースト",
"keyboard_shortcuts.column": "左からn番目のカラムの最新に移動",
"keyboard_shortcuts.compose": "投稿の入力欄に移動",
"keyboard_shortcuts.compose": "トゥートの入力欄に移動",
"keyboard_shortcuts.description": "説明",
"keyboard_shortcuts.direct": "ダイレクトメッセージのカラムを開く",
"keyboard_shortcuts.down": "カラム内一つ下に移動",
"keyboard_shortcuts.enter": "投稿の詳細を表示",
"keyboard_shortcuts.enter": "トゥートの詳細を表示",
"keyboard_shortcuts.favourite": "お気に入り",
"keyboard_shortcuts.favourites": "お気に入り登録のリストを開く",
"keyboard_shortcuts.federated": "連合タイムラインを開く",
@ -328,7 +333,7 @@
"keyboard_shortcuts.my_profile": "自分のプロフィールを開く",
"keyboard_shortcuts.notifications": "通知カラムを開く",
"keyboard_shortcuts.open_media": "メディアを開く",
"keyboard_shortcuts.pinned": "固定した投稿のリストを開く",
"keyboard_shortcuts.pinned": "固定したトゥートのリストを開く",
"keyboard_shortcuts.profile": "プロフィールを開く",
"keyboard_shortcuts.reply": "返信",
"keyboard_shortcuts.requests": "フォローリクエストのリストを開く",
@ -337,8 +342,8 @@
"keyboard_shortcuts.start": "\"スタート\" カラムを開く",
"keyboard_shortcuts.toggle_hidden": "CWで隠れた文を見る/隠す",
"keyboard_shortcuts.toggle_sensitivity": "非表示のメディアを見る/隠す",
"keyboard_shortcuts.toot": "新規投稿",
"keyboard_shortcuts.unfocus": "投稿の入力欄・検索欄から離れる",
"keyboard_shortcuts.toot": "新規トゥート",
"keyboard_shortcuts.unfocus": "トゥートの入力欄・検索欄から離れる",
"keyboard_shortcuts.up": "カラム内一つ上に移動",
"lightbox.close": "閉じる",
"lightbox.compress": "画像ビューボックスを閉じる",
@ -373,7 +378,7 @@
"navigation_bar.blocks": "ブロックしたユーザー",
"navigation_bar.bookmarks": "ブックマーク",
"navigation_bar.community_timeline": "ローカルタイムライン",
"navigation_bar.compose": "投稿の新規作成",
"navigation_bar.compose": "トゥートの新規作成",
"navigation_bar.direct": "ダイレクトメッセージ",
"navigation_bar.discover": "見つける",
"navigation_bar.domain_blocks": "ブロックしたドメイン",
@ -388,7 +393,7 @@
"navigation_bar.logout": "ログアウト",
"navigation_bar.mutes": "ミュートしたユーザー",
"navigation_bar.personal": "個人用",
"navigation_bar.pins": "固定した投稿",
"navigation_bar.pins": "固定したトゥート",
"navigation_bar.preferences": "ユーザー設定",
"navigation_bar.public_timeline": "連合タイムライン",
"navigation_bar.search": "検索",
@ -396,15 +401,15 @@
"not_signed_in_indicator.not_signed_in": "この機能を使うにはログインする必要があります。",
"notification.admin.report": "{name}さんが{target}さんを通報しました",
"notification.admin.sign_up": "{name}さんがサインアップしました",
"notification.favourite": "{name}さんがあなたの投稿をお気に入りに登録しました",
"notification.favourite": "{name}さんがあなたのトゥートに╰( ^o^)╮-=ニ=一=三★しました",
"notification.follow": "{name}さんにフォローされました",
"notification.follow_request": "{name}さんがあなたにフォローリクエストしました",
"notification.mention": "{name}さんがあなたに返信しました",
"notification.own_poll": "アンケートが終了しました",
"notification.poll": "アンケートが終了しました",
"notification.reblog": "{name}さんがあなたの投稿をブーストしました",
"notification.status": "{name}さんが投稿しました",
"notification.update": "{name}さんが投稿を編集しました",
"notification.reblog": "{name}さんがあなたのトゥートをブーストしました",
"notification.status": "{name}さんがトゥートしました",
"notification.update": "{name}さんがトゥートを編集しました",
"notifications.clear": "通知を消去",
"notifications.clear_confirmation": "本当に通知を消去しますか?",
"notifications.column_settings.admin.report": "新しい通報:",
@ -422,7 +427,7 @@
"notifications.column_settings.reblog": "ブースト:",
"notifications.column_settings.show": "カラムに表示",
"notifications.column_settings.sound": "通知音を再生",
"notifications.column_settings.status": "新しい投稿:",
"notifications.column_settings.status": "新しいトゥート:",
"notifications.column_settings.unread_notifications.category": "未読の通知:",
"notifications.column_settings.unread_notifications.highlight": "未読の通知を強調表示",
"notifications.column_settings.update": "編集:",
@ -479,20 +484,20 @@
"relative_time.today": "今日",
"reply_indicator.cancel": "キャンセル",
"report.block": "ブロック",
"report.block_explanation": "相手の投稿が表示されなくなります。相手はあなたの投稿を見ることやフォローすることができません。相手はブロックされていることがわかります。",
"report.block_explanation": "相手のトゥートが表示されなくなります。相手はあなたのトゥートを見ることやフォローすることができません。相手はブロックされていることがわかります。",
"report.categories.other": "その他",
"report.categories.spam": "スパム",
"report.categories.violation": "サーバーのルールに違反",
"report.category.subtitle": "近いものを選択してください",
"report.category.title": "この{type}について教えてください",
"report.category.title_account": "プロフィール",
"report.category.title_status": "投稿",
"report.category.title_status": "トゥート",
"report.close": "完了",
"report.comment.title": "その他に私たちに伝えておくべき事はありますか?",
"report.forward": "{target}に転送する",
"report.forward_hint": "このアカウントは別のサーバーに所属しています。通報内容を匿名で転送しますか?",
"report.mute": "ミュート",
"report.mute_explanation": "相手の投稿は表示されなくなります。相手は引き続きあなたをフォローして、あなたの投稿を表示することができますが、ミュートされていることはわかりません。",
"report.mute_explanation": "相手のトゥートは表示されなくなります。相手は引き続きあなたをフォローして、あなたのトゥートを表示することができますが、ミュートされていることはわかりません。",
"report.next": "次へ",
"report.placeholder": "追加コメント",
"report.reasons.dislike": "興味がありません",
@ -506,7 +511,7 @@
"report.rules.subtitle": "当てはまるものをすべて選んでください:",
"report.rules.title": "どのルールに違反していますか?",
"report.statuses.subtitle": "当てはまるものをすべて選んでください:",
"report.statuses.title": "この通報を裏付けるような投稿はありますか?",
"report.statuses.title": "この通報を裏付けるようなトゥートはありますか?",
"report.submit": "通報する",
"report.target": "{target}さんを通報する",
"report.thanks.take_action": "次のような方法はいかがでしょうか?",
@ -514,8 +519,8 @@
"report.thanks.title": "見えないようにしたいですか?",
"report.thanks.title_actionable": "ご報告ありがとうございます、追って確認します。",
"report.unfollow": "@{name}さんのフォローを解除",
"report.unfollow_explanation": "このアカウントをフォローしています。ホームフィードに彼らの投稿を表示しないようにするには、彼らのフォローを外してください。",
"report_notification.attached_statuses": "{count, plural, one {{count}件の投稿} other {{count}件の投稿}}が添付されました。",
"report.unfollow_explanation": "このアカウントをフォローしています。ホームフィードに彼らのトゥートを表示しないようにするには、彼らのフォローを外してください。",
"report_notification.attached_statuses": "{count, plural, one {{count}件のトゥート} other {{count}件のトゥート}}が添付されました。",
"report_notification.categories.other": "その他",
"report_notification.categories.spam": "スパム",
"report_notification.categories.violation": "ルール違反",
@ -523,17 +528,17 @@
"search.placeholder": "検索",
"search.search_or_paste": "検索またはURLを入力",
"search_popout.search_format": "高度な検索フォーマット",
"search_popout.tips.full_text": "表示名やユーザー名、ハッシュタグのほか、あなたの投稿やお気に入り、ブーストした投稿、返信に一致する単純なテキスト。",
"search_popout.tips.full_text": "表示名やユーザー名、ハッシュタグのほか、あなたのトゥートやお気に入り、ブーストしたトゥート、返信に一致する単純なテキスト。",
"search_popout.tips.hashtag": "ハッシュタグ",
"search_popout.tips.status": "投稿",
"search_popout.tips.status": "トゥート",
"search_popout.tips.text": "表示名やユーザー名、ハッシュタグに一致する単純なテキスト",
"search_popout.tips.user": "ユーザー",
"search_results.accounts": "人々",
"search_results.all": "すべて",
"search_results.hashtags": "ハッシュタグ",
"search_results.nothing_found": "この検索条件では何も見つかりませんでした",
"search_results.statuses": "投稿",
"search_results.statuses_fts_disabled": "このサーバーでは投稿本文の検索は利用できません。",
"search_results.statuses": "トゥート",
"search_results.statuses_fts_disabled": "このサーバーではトゥート本文の検索は利用できません。",
"search_results.title": "『{q}』の検索結果",
"search_results.total": "{count, number}件の結果",
"server_banner.about_active_users": "過去30日間にこのサーバーを使用している人 (月間アクティブユーザー)",
@ -544,15 +549,16 @@
"server_banner.server_stats": "サーバーの情報",
"sign_in_banner.create_account": "アカウント作成",
"sign_in_banner.sign_in": "ログイン",
"sign_in_banner.text": "ログインしてプロファイルやハッシュタグ、お気に入りをフォローしたり、投稿を共有したり、返信したり、別のサーバーのアカウントと交流したりできます。",
"sign_in_banner.text": "ログインしてプロファイルやハッシュタグ、お気に入りをフォローしたり、トゥートを共有したり、返信したり、別のサーバーのアカウントと交流したりできます。",
"status.admin_account": "@{name}さんのモデレーション画面を開く",
"status.admin_domain": "{domain}のモデレーション画面を開く",
"status.admin_status": "この投稿をモデレーション画面で開く",
"status.admin_status": "このトゥートをモデレーション画面で開く",
"status.block": "@{name}さんをブロック",
"status.bookmark": "ブックマーク",
"status.cancel_reblog_private": "ブースト解除",
"status.cannot_reblog": "この投稿はブーストできません",
"status.copy": "投稿へのリンクをコピー",
"status.cannot_reblog": "このトゥートはブーストできません",
"status.copy": "トゥートへのリンクをコピー",
"status.cannot_quote": "このトゥートは引用できません",
"status.delete": "削除",
"status.detailed_status": "詳細な会話ビュー",
"status.direct": "@{name}さんにダイレクトメッセージ",
@ -561,20 +567,22 @@
"status.edited_x_times": "{count}回編集",
"status.embed": "埋め込み",
"status.favourite": "お気に入り",
"status.filter": "この投稿をフィルターする",
"status.filter": "このトゥートをフィルターする",
"status.filtered": "フィルターされました",
"status.hide": "投稿を非表示",
"status.hide": "トゥートを非表示",
"status.history.created": "{name}さんが{date}に作成",
"status.history.edited": "{name}さんが{date}に編集",
"status.load_more": "もっと見る",
"status.media_hidden": "非表示のメディア",
"status.mention": "@{name}さんに投稿",
"status.mention": "@{name}さんにトゥート",
"status.more": "もっと見る",
"status.mute": "@{name}さんをミュート",
"status.mute_conversation": "会話をミュート",
"status.muted_quote": "ミュートされた引用",
"status.open": "詳細を表示",
"status.pin": "プロフィールに固定表示",
"status.pinned": "固定された投稿",
"status.pinned": "固定されたトゥート",
"status.quote": "引用",
"status.read_more": "もっと見る",
"status.reblog": "ブースト",
"status.reblog_private": "ブースト",
@ -597,9 +605,10 @@
"status.translate": "翻訳",
"status.translated_from_with": "{provider}を使って{lang}から翻訳",
"status.uncached_media_warning": "利用できません",
"status.unlisted_quote": "未収載の引用",
"status.unmute_conversation": "会話のミュートを解除",
"status.unpin": "プロフィールへの固定を解除",
"subscribed_languages.lead": "選択した言語の投稿だけがホームとリストのタイムラインに表示されます。全ての言語の投稿を受け取る場合は全てのチェックを外して下さい。",
"subscribed_languages.lead": "選択した言語のトゥートだけがホームとリストのタイムラインに表示されます。全ての言語のトゥートを受け取る場合は全てのチェックを外して下さい。",
"subscribed_languages.save": "変更を保存",
"subscribed_languages.target": "{target}さんの購読言語を変更します",
"suggestions.dismiss": "隠す",
@ -616,10 +625,10 @@
"timeline_hint.remote_resource_not_displayed": "他のサーバーの{resource}は表示されません。",
"timeline_hint.resources.followers": "フォロワー",
"timeline_hint.resources.follows": "フォロー",
"timeline_hint.resources.statuses": "以前の投稿",
"timeline_hint.resources.statuses": "以前のトゥート",
"trends.counter_by_accounts": "過去{days, plural, one {{days}日} other {{days}日}}に{count, plural, one {{counter}人} other {{counter} 人}}",
"trends.trending_now": "トレンドタグ",
"ui.beforeunload": "Mastodonから離れると送信前の投稿は失われます。",
"ui.beforeunload": "Mastodonから離れると送信前のトゥートは失われます。",
"units.short.billion": "{count}B",
"units.short.million": "{count}M",
"units.short.thousand": "{count}K",

View File

@ -4,6 +4,8 @@ import {
COMPOSE_CHANGE,
COMPOSE_REPLY,
COMPOSE_REPLY_CANCEL,
COMPOSE_QUOTE,
COMPOSE_QUOTE_CANCEL,
COMPOSE_DIRECT,
COMPOSE_MENTION,
COMPOSE_SUBMIT_REQUEST,
@ -67,6 +69,8 @@ const initialState = ImmutableMap({
caretPosition: null,
preselectDate: null,
in_reply_to: null,
quote_from: null,
quote_from_url: null,
is_composing: false,
is_submitting: false,
is_changing_upload: false,
@ -119,6 +123,8 @@ function clearAll(state) {
map.set('is_submitting', false);
map.set('is_changing_upload', false);
map.set('in_reply_to', null);
map.set('quote_from', null);
map.set('quote_from_url', null);
map.set('privacy', state.get('default_privacy'));
map.set('sensitive', state.get('default_sensitive'));
map.set('language', state.get('default_language'));
@ -250,6 +256,17 @@ const expiresInFromExpiresAt = expires_at => {
return [300, 1800, 3600, 21600, 86400, 259200, 604800].find(expires_in => expires_in >= delta) || 24 * 3600;
};
const rejectQuoteAltText = html => {
const fragment = domParser.parseFromString(html, 'text/html').documentElement;
const quote_inline = fragment.querySelector('span.quote-inline');
if (quote_inline) {
quote_inline.remove();
}
return fragment.innerHTML;
};
const mergeLocalHashtagResults = (suggestions, prefix, tagHistory) => {
prefix = prefix.toLowerCase();
if (suggestions.length < 4) {
@ -323,10 +340,20 @@ export default function compose(state = initialState, action) {
case COMPOSE_COMPOSING_CHANGE:
return state.set('is_composing', action.value);
case COMPOSE_REPLY:
case COMPOSE_QUOTE:
return state.withMutations(map => {
map.set('id', null);
map.set('in_reply_to', action.status.get('id'));
map.set('text', statusToTextMentions(state, action.status));
if (action.type === COMPOSE_REPLY) {
map.set('in_reply_to', action.status.get('id'));
map.set('quote_from', null);
map.set('quote_from_url', null);
map.set('text', statusToTextMentions(state, action.status));
} else {
map.set('in_reply_to', null);
map.set('quote_from', action.status.get('id'));
map.set('quote_from_url', action.status.get('url'));
map.set('text', '');
}
map.set('privacy', privacyPreference(action.status.get('visibility'), state.get('default_privacy')));
map.set('focusDate', new Date());
map.set('caretPosition', null);
@ -358,6 +385,7 @@ export default function compose(state = initialState, action) {
case COMPOSE_UPLOAD_CHANGE_REQUEST:
return state.set('is_changing_upload', true);
case COMPOSE_REPLY_CANCEL:
case COMPOSE_QUOTE_CANCEL:
case COMPOSE_RESET:
case COMPOSE_SUBMIT_SUCCESS:
return clearAll(state);
@ -456,8 +484,10 @@ export default function compose(state = initialState, action) {
}));
case REDRAFT:
return state.withMutations(map => {
map.set('text', action.raw_text || unescapeHTML(expandMentions(action.status)));
map.set('text', action.raw_text || unescapeHTML(rejectQuoteAltText(expandMentions(action.status))));
map.set('in_reply_to', action.status.get('in_reply_to_id'));
map.set('quote_from', action.status.getIn(['quote', 'id']));
map.set('quote_from_url', action.status.getIn(['quote', 'url']));
map.set('privacy', action.status.get('visibility'));
map.set('media_attachments', action.status.get('media_attachments').map((media) => media.set('unattached', true)));
map.set('focusDate', new Date());

View File

@ -10,6 +10,7 @@ import {
import {
COMPOSE_MENTION,
COMPOSE_REPLY,
COMPOSE_QUOTE,
COMPOSE_DIRECT,
} from '../actions/compose';
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
@ -37,6 +38,7 @@ export default function search(state = initialState, action) {
case SEARCH_SHOW:
return state.set('hidden', false);
case COMPOSE_REPLY:
case COMPOSE_QUOTE:
case COMPOSE_MENTION:
case COMPOSE_DIRECT:
return state.set('hidden', true);

View File

@ -17,6 +17,8 @@ import {
STATUS_TRANSLATE_UNDO,
STATUS_FETCH_REQUEST,
STATUS_FETCH_FAIL,
QUOTE_REVEAL,
QUOTE_HIDE,
} from '../actions/statuses';
import { TIMELINE_DELETE } from '../actions/timelines';
import { STATUS_IMPORT, STATUSES_IMPORT } from '../actions/importer';
@ -83,6 +85,14 @@ export default function statuses(state = initialState, action) {
});
case STATUS_COLLAPSE:
return state.setIn([action.id, 'collapsed'], action.isCollapsed);
case QUOTE_REVEAL:
return state.withMutations(map => {
action.ids.forEach(id => map.setIn([id, 'quote_hidden'], false));
});
case QUOTE_HIDE:
return state.withMutations(map => {
action.ids.forEach(id => map.setIn([id, 'quote_hidden'], true));
});
case TIMELINE_DELETE:
return deleteStatus(state, action.id, action.references);
case STATUS_TRANSLATE_SUCCESS:

View File

@ -2,6 +2,7 @@ import { createSelector } from 'reselect';
import { List as ImmutableList, Map as ImmutableMap } from 'immutable';
import { toServerSideType } from 'mastodon/utils/filters';
import { me } from '../initial_state';
import {reblogRequest} from '../actions/interactions';
const getAccountBase = (state, id) => state.getIn(['accounts', id], null);
const getAccountCounters = (state, id) => state.getIn(['accounts_counters', id], null);
@ -35,22 +36,54 @@ export const makeGetStatus = () => {
[
(state, { id }) => state.getIn(['statuses', id]),
(state, { id }) => state.getIn(['statuses', state.getIn(['statuses', id, 'reblog'])]),
(state, { id }) => state.getIn(['statuses', state.getIn(['statuses', id, 'quote_id'])]),
(state, { id }) => state.getIn(['accounts', state.getIn(['statuses', id, 'account'])]),
(state, { id }) => state.getIn(['accounts', state.getIn(['statuses', state.getIn(['statuses', id, 'reblog']), 'account'])]),
(state, { id }) => state.getIn(['accounts', state.getIn(['statuses', state.getIn(['statuses', id, 'quote_id']), 'account'])]),
(state, { id }) => state.getIn(['accounts', state.getIn(['statuses', state.getIn(['statuses', id, 'reblog']), 'quote', 'account'])]),
(state, { id }) => state.getIn(['relationships', state.getIn(['statuses', id, 'account'])]),
(state, { id }) => state.getIn(['relationships', state.getIn(['statuses', state.getIn(['statuses', id, 'reblog']), 'account'])]),
(state, { id }) => state.getIn(['relationships', state.getIn(['statuses', state.getIn(['statuses', id, 'quote_id']), 'account'])]),
(state, { id }) => state.getIn(['accounts', state.getIn(['accounts', state.getIn(['statuses', id, 'account']), 'moved'])]),
(state, { id }) => state.getIn(['accounts', state.getIn(['accounts', state.getIn(['statuses', state.getIn(['statuses', id, 'reblog']), 'account']), 'moved'])]),
(state, { id }) => state.getIn(['accounts', state.getIn(['accounts', state.getIn(['statuses', state.getIn(['statuses', id, 'quote_id']), 'account']), 'moved'])]),
getFilters,
],
(statusBase, statusReblog, accountBase, accountReblog, filters) => {
(statusBase, statusReblog, statusQuote, accountBase, accountReblog, accountQuote, accountReblogQuote, relationshipBase, relationshipReblog, relationshipQuote, movedBase, movedReblog, movedQuote, filters) => {
if (!statusBase || statusBase.get('isLoading')) {
return null;
}
accountBase = accountBase.withMutations(map => {
map.set('relationship', relationshipBase);
map.set('moved', movedBase);
});
if (statusReblog) {
accountReblog = accountReblog.withMutations(map => {
map.set('relationship', relationshipReblog);
map.set('moved', movedReblog);
});
statusReblog = statusReblog.set('account', accountReblog);
} else {
statusReblog = null;
}
if (statusQuote) {
accountQuote = accountQuote.withMutations(map => {
map.set('relationship', relationshipQuote);
map.set('moved', movedQuote);
});
statusQuote = statusQuote.set('account', accountQuote);
} else {
statusQuote = null;
}
if (statusReblog && accountReblogQuote) {
statusReblog = statusReblog.setIn(['quote', 'account'], accountReblogQuote);
}
let filtered = false;
if ((accountReblog || accountBase).get('id') !== me && filters) {
let filterResults = statusReblog?.get('filtered') || statusBase.get('filtered') || ImmutableList();
@ -65,6 +98,7 @@ export const makeGetStatus = () => {
return statusBase.withMutations(map => {
map.set('reblog', statusReblog);
map.set('quote', statusQuote);
map.set('account', accountBase);
map.set('matched_filters', filtered);
});

View File

@ -0,0 +1 @@
export const uniq = (array) => array.filter((x, i, self) => self.indexOf(x) === i);

View File

@ -322,6 +322,29 @@ function main() {
}
});
});
delegate(document, '.quote-status', 'click', ({ target }) => {
if (target.closest('.status__content__spoiler-link') ||
target.closest('.media-gallery') ||
target.closest('.video-player') ||
target.closest('.audio-player')) {
return false;
}
let url = target.closest('.quote-status').getAttribute('dataurl');
if (target.closest('.status__display-name')) {
url = target.closest('.status__display-name').getAttribute('href');
} else if (target.closest('.status-card')) {
url = target.closest('.status-card').getAttribute('href');
}
if (window.location.hostname === url.split('/')[2].split(':')[0]) {
window.location.href = url;
} else {
window.open(url, 'blank');
}
return false;
});
}
loadPolyfills()

View File

@ -23,3 +23,5 @@
@import 'mastodon/dashboard';
@import 'mastodon/rtl';
@import 'mastodon/accessibility';
@import 'plugin';

View File

@ -0,0 +1,3 @@
@import 'light-pink/variables';
@import 'application';
@import 'mastodon-light/diff';

View File

@ -0,0 +1,44 @@
// Dependent colors
$black: #000000;
$white: #ffffff;
$classic-base-color: #6e202f;
$classic-primary-color: #ffa7ae;
$classic-secondary-color: #faeef1;
$classic-highlight-color: #ff375b;
// Differences
$success-green: lighten(#3c754d, 8%);
$base-overlay-background: $white !default;
$valid-value-color: $success-green !default;
$ui-base-color: $classic-secondary-color !default;
$ui-base-lighter-color: #ffe1e9;
$ui-primary-color: #f1adbf;
$ui-secondary-color: $classic-base-color !default;
$ui-highlight-color: $classic-highlight-color !default;
$primary-text-color: $black !default;
$darker-text-color: $classic-base-color !default;
$highlight-text-color: darken($ui-highlight-color, 8%) !default;
$dark-text-color: #6e202f;
$action-button-color: #ffa7ae;
$inverted-text-color: $black !default;
$lighter-text-color: $classic-base-color !default;
$light-text-color: #ffa7ae;
// Newly added colors
$account-background-color: $white !default;
// Invert darkened and lightened colors
@function darken($color, $amount) {
@return hsl(hue($color), saturation($color), lightness($color) + $amount);
}
@function lighten($color, $amount) {
@return hsl(hue($color), saturation($color), lightness($color) - $amount);
}
$emojis-requiring-inversion: 'chains';

View File

@ -736,10 +736,29 @@ body > [data-popper-placement] {
justify-content: flex-end;
min-width: 0;
flex: 0 0 auto;
padding-top: 10px;
.compose-form__publish-button-wrapper {
overflow: hidden;
padding-top: 15px;
button {
display: inline-block;
width: auto;
margin-right: 0.5em;
}
button:last-child {
margin-right: auto;
}
}
}
.compose-form__utilBtns {
padding-top: 10px;
* {
margin-bottom: 0.5em;
}
}
}
@ -808,6 +827,10 @@ body > [data-popper-placement] {
min-height: 23px;
overflow-y: auto;
flex: 0 2 auto;
&.quote-indicator {
background: $success-green;
}
}
.reply-indicator__header {
@ -989,6 +1012,10 @@ body > [data-popper-placement] {
.status__content.status__content--collapsed {
max-height: 22px * 15; // 15 lines is roughly above 500 characters
.quote-status & {
max-height: 22px * 5;
}
}
.status__content__read-more-button {
@ -1061,6 +1088,69 @@ body > [data-popper-placement] {
}
}
.quote-status {
border: solid 1px $ui-base-lighter-color;
border-radius: 4px !important;
padding: 5px !important;
margin-top: 8px;
position: relative;
.muted-quote,
.unlisted-quote button {
color: $dark-text-color;
font-size: 15px;
width: 100%;
border: 0;
padding: 0;
}
.muted-quote {
text-align: center;
cursor: default;
}
.unlisted-quote button {
background-color: transparent;
cursor: pointer;
appearance: none;
}
.status__avatar,
.detailed-status__display-avatar {
position: absolute;
top: 5px !important;
left: 5px !important;
}
.display-name {
padding-left: 56px;
}
.detailed-status__display-name {
margin-bottom: 0;
line-height: unset;
strong,
span {
display: inline;
}
}
.status__content__text {
p {
display: inline;
&::after {
content: ' ';
}
}
}
}
.quote-inline {
display: none;
}
.focusable {
&:focus {
outline: 0;
@ -1076,9 +1166,12 @@ body > [data-popper-placement] {
.status {
padding: 16px;
min-height: 54px;
border-bottom: 1px solid lighten($ui-base-color, 8%);
cursor: auto;
&:not(.quote-status) {
border-bottom: 1px solid lighten($ui-base-color, 8%);
}
@keyframes fade {
0% { opacity: 0; }
100% { opacity: 1; }

View File

@ -40,7 +40,7 @@
&:last-child {
.detailed-status,
.status,
.status:not(.quote-status),
.load-more {
border-bottom: 0;
border-radius: 0 0 4px 4px;
@ -63,9 +63,18 @@
}
}
.detailed-status .quote-status {
width: 100%;
}
.quote-status {
margin-top: 15px;
cursor: pointer;
}
@media screen and (max-width: 740px) {
.detailed-status,
.status,
.status:not(.quote-status),
.load-more {
border-radius: 0 !important;
}
@ -77,6 +86,10 @@
}
}
.standalone-timeline .quote-status {
cursor: pointer;
}
.button.logo-button {
flex: 0 auto;
font-size: 14px;

View File

@ -0,0 +1,45 @@
// ここから下タブバーの実装
//投稿ボタン
.columns-area__panels__main .button.bottom_right {
position: fixed;
right: 18px;
bottom: 65px;
display: flex;
align-items: center;
justify-content: center;
width: 64px;
height: 64px;
font-size: 24px;
border-radius: 50%;
}
.tab-ber-bottom .navber {
display: none;
}
@media screen and (max-width: 630px) {
.tab-ber-bottom .timeline{
width:100%;
height: calc(100% - 55px);
margin: 0 0 50px 0;
}
.tab-ber-bottom .navber{
display: flex;
width:100%;
bottom: 0;
position: fixed;
}
.tab-ber-bottom .navber .tabs-bar__wrapper{
bottom: 0;
width: 100%;
}
.columns-area__panels__pane-tab-ber{
display: none;
}
}
//ここまで下タブバーの実装

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