mirror of
https://github.com/glitch-soc/mastodon.git
synced 2026-03-29 03:00:33 +02:00
Compare commits
293 Commits
300d62f1c4
...
v4.4.15
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f3bbf47574 | ||
|
|
f55e3b14b3 | ||
|
|
7c28ff90fc | ||
|
|
b02a2abdf8 | ||
|
|
6567ceb8de | ||
|
|
f0ddc208a8 | ||
|
|
91ea49b7ff | ||
|
|
6c6f042643 | ||
|
|
c4f69f4823 | ||
|
|
844c5d45f8 | ||
|
|
04e390654e | ||
|
|
3b517fc8c5 | ||
|
|
9f14bac22a | ||
|
|
9f358a40c3 | ||
|
|
8d25d3b9c2 | ||
|
|
7df317be1d | ||
|
|
32825cc39a | ||
|
|
0cd84e46e6 | ||
|
|
33d6e6acef | ||
|
|
c1f809bfa7 | ||
|
|
d018d9db57 | ||
|
|
d31b2df458 | ||
|
|
6d30e68295 | ||
|
|
bfe56212b6 | ||
|
|
397c42d4af | ||
|
|
d7d2f1c2d7 | ||
|
|
f0b875f9c3 | ||
|
|
f926bf9a42 | ||
|
|
1e5ac0f491 | ||
|
|
e4e5d0cd2d | ||
|
|
19edc6a264 | ||
|
|
5bcbc77e75 | ||
|
|
bae1da6f73 | ||
|
|
b89d6e256b | ||
|
|
7de301922b | ||
|
|
418370d561 | ||
|
|
eb453afe8d | ||
|
|
3871f3a399 | ||
|
|
9eeeb1b31d | ||
|
|
fa51ec5364 | ||
|
|
5419594d3d | ||
|
|
8a46c747db | ||
|
|
f6ac245a84 | ||
|
|
1b0be1a725 | ||
|
|
68c08114b9 | ||
|
|
04903a45ce | ||
|
|
d9bad1c407 | ||
|
|
cbb1085855 | ||
|
|
3920feb8bd | ||
|
|
4dbe15654a | ||
|
|
27e06cdf20 | ||
|
|
6ac8b52ccc | ||
|
|
7ee99bbe81 | ||
|
|
6c1e77ff1f | ||
|
|
2588d1ab47 | ||
|
|
c0f8640252 | ||
|
|
92be0fd12e | ||
|
|
8450ebc7e8 | ||
|
|
3d27ec34ac | ||
|
|
d3551e1ab6 | ||
|
|
0b9c741dac | ||
|
|
a8c9923df9 | ||
|
|
f32067dc56 | ||
|
|
36981fadf0 | ||
|
|
b60edf59d8 | ||
|
|
3a87a63abf | ||
|
|
ef4d722d6a | ||
|
|
68e30985ca | ||
|
|
1702290786 | ||
|
|
abb3a02ce2 | ||
|
|
25c79f526a | ||
|
|
f5890040e1 | ||
|
|
740f262e38 | ||
|
|
4d2b6795a4 | ||
|
|
6558a1e07a | ||
|
|
b64101ee64 | ||
|
|
0bde273b3d | ||
|
|
22fe977ffe | ||
|
|
8e945bef2a | ||
|
|
d5f12debe0 | ||
|
|
5d0ec718fd | ||
|
|
c7aa312307 | ||
|
|
dc1d4eda7c | ||
|
|
931a29b4f3 | ||
|
|
99b2307350 | ||
|
|
375f2e6ebf | ||
|
|
f0a1da78ba | ||
|
|
b554ecfcb4 | ||
|
|
50244ba682 | ||
|
|
01cf5c103d | ||
|
|
5bda54d15a | ||
|
|
07f5573cd6 | ||
|
|
2b0b537152 | ||
|
|
c49e261ad0 | ||
|
|
915bcb267f | ||
|
|
ff37011057 | ||
|
|
8f5e95a159 | ||
|
|
16ee628d24 | ||
|
|
64a0b060a8 | ||
|
|
fa52f4361a | ||
|
|
7f7d6697c1 | ||
|
|
c2fb12d22d | ||
|
|
2dc4552229 | ||
|
|
95868643a2 | ||
|
|
7c46fdfbf1 | ||
|
|
8965e1bfa9 | ||
|
|
1e27ab0885 | ||
|
|
cef2c50a71 | ||
|
|
a54af6b06c | ||
|
|
81cf6715de | ||
|
|
d7f4eca801 | ||
|
|
adf291631e | ||
|
|
cbef4c9e65 | ||
|
|
1631fb80e8 | ||
|
|
8477bec2f2 | ||
|
|
6796765363 | ||
|
|
044a20f12d | ||
|
|
15d05121df | ||
|
|
07dea4e140 | ||
|
|
3c2570c88a | ||
|
|
81955c10b1 | ||
|
|
e4bdbccba8 | ||
|
|
958d4df6cf | ||
|
|
21d4abf7cc | ||
|
|
d7d6407d41 | ||
|
|
a186bad399 | ||
|
|
67575e59e6 | ||
|
|
d9113976c8 | ||
|
|
3386f225e4 | ||
|
|
97bc82c710 | ||
|
|
0e7cb713d1 | ||
|
|
670316499f | ||
|
|
3c725240fd | ||
|
|
d8ddf95485 | ||
|
|
4c12c2ed60 | ||
|
|
636ecd1d03 | ||
|
|
cb0065cfe9 | ||
|
|
6ae1b4fae9 | ||
|
|
fd7adcc9d4 | ||
|
|
3c5f07c28e | ||
|
|
048a42b8a7 | ||
|
|
c475623418 | ||
|
|
dc6d8f8825 | ||
|
|
dd0647ca45 | ||
|
|
70e2eb49df | ||
|
|
bef28b2e51 | ||
|
|
0b66bd591f | ||
|
|
a94d7bf520 | ||
|
|
c8551a3eca | ||
|
|
df322e50c0 | ||
|
|
1f3588a5a7 | ||
|
|
06c2393805 | ||
|
|
4e85b9073b | ||
|
|
cd573a346d | ||
|
|
793296b5eb | ||
|
|
661dbede1c | ||
|
|
89f423aa00 | ||
|
|
19c89f1bbd | ||
|
|
c966d75600 | ||
|
|
6f1fd0c2a7 | ||
|
|
68c219e753 | ||
|
|
a795743c3f | ||
|
|
1ab5ea9bfb | ||
|
|
48f55e3224 | ||
|
|
6044270d69 | ||
|
|
be1bd91e6d | ||
|
|
34cd5a716f | ||
|
|
ec5128bc1f | ||
|
|
28a79eb239 | ||
|
|
1e2a9167bc | ||
|
|
1947f3a18b | ||
|
|
5a46e3a234 | ||
|
|
75ba0f757a | ||
|
|
62ed8d633e | ||
|
|
02d92d3b68 | ||
|
|
2a87fe5860 | ||
|
|
00c61317d3 | ||
|
|
c0f9e7f4c3 | ||
|
|
1137a0ca3a | ||
|
|
1faf520ce4 | ||
|
|
8777443c9b | ||
|
|
bd6d1f0e3f | ||
|
|
1a1a23f6f0 | ||
|
|
a48567784c | ||
|
|
b71216a08a | ||
|
|
36974aaa99 | ||
|
|
567f337db3 | ||
|
|
97f118013a | ||
|
|
ea5d1f0297 | ||
|
|
7a862d3308 | ||
|
|
1675eab561 | ||
|
|
5f4116a311 | ||
|
|
0741381670 | ||
|
|
e61900cadc | ||
|
|
cbb9a4dbe3 | ||
|
|
4ef0ce033e | ||
|
|
48315a719d | ||
|
|
45932983fc | ||
|
|
1ed3e4cc77 | ||
|
|
5478ef9b32 | ||
|
|
e2592419d9 | ||
|
|
e330447b0e | ||
|
|
b15861528c | ||
|
|
83dc7dc16e | ||
|
|
7d3cc51148 | ||
|
|
cabb33bc49 | ||
|
|
ba6f4de3e4 | ||
|
|
6af9646bbd | ||
|
|
fcb7917344 | ||
|
|
208cb8276a | ||
|
|
4ae47f4263 | ||
|
|
08b2f255fc | ||
|
|
8242f06eca | ||
|
|
5429351889 | ||
|
|
6ff4e83937 | ||
|
|
5e639d7384 | ||
|
|
26e524836d | ||
|
|
cfc4bb1dc0 | ||
|
|
d7099b1b38 | ||
|
|
69fb382424 | ||
|
|
47d469ec5e | ||
|
|
4d3e2efb69 | ||
|
|
92fc7a30dc | ||
|
|
4311369ab8 | ||
|
|
18653ce15d | ||
|
|
71a35d3953 | ||
|
|
e69c1479a8 | ||
|
|
77d2cdb302 | ||
|
|
c727197760 | ||
|
|
d6859c9658 | ||
|
|
7a9e98f4d6 | ||
|
|
7924a27ae7 | ||
|
|
d664b9d8ff | ||
|
|
4558cfadd8 | ||
|
|
713965467d | ||
|
|
aec6d0f807 | ||
|
|
e103815d2d | ||
|
|
d73b9fba90 | ||
|
|
a89d11bc08 | ||
|
|
a250928934 | ||
|
|
1d1b17b04b | ||
|
|
2aff51013c | ||
|
|
8c3c1faaec | ||
|
|
a2888f1bb2 | ||
|
|
77fe044f03 | ||
|
|
da0cc0f5b9 | ||
|
|
ee83f3a8b9 | ||
|
|
7ae78b1032 | ||
|
|
c4b7c3bdda | ||
|
|
a79dbf8334 | ||
|
|
ef6f5f9357 | ||
|
|
f65f6ad6f1 | ||
|
|
c0e242cb73 | ||
|
|
6c1cc5b25a | ||
|
|
ec6f93ae45 | ||
|
|
609a40181e | ||
|
|
93ce44d21d | ||
|
|
fb3ff194b5 | ||
|
|
81b363b338 | ||
|
|
1151b05c2d | ||
|
|
f96743fcfb | ||
|
|
7a51ad7ebd | ||
|
|
06a46e77b8 | ||
|
|
69e14246b8 | ||
|
|
174370dec2 | ||
|
|
18e08bf493 | ||
|
|
c1794fb948 | ||
|
|
061c966ab3 | ||
|
|
326f6bc12a | ||
|
|
bd442485d0 | ||
|
|
333a17a478 | ||
|
|
388e09e1a3 | ||
|
|
2dcededcf0 | ||
|
|
2db8a328cd | ||
|
|
b4a950c2fc | ||
|
|
194645aada | ||
|
|
48aaecec7b | ||
|
|
6cac651ff2 | ||
|
|
385dd5ea37 | ||
|
|
0c5ce23ae4 | ||
|
|
cb937a920e | ||
|
|
7051458467 | ||
|
|
025abf7325 | ||
|
|
28373a9c88 | ||
|
|
42884d8727 | ||
|
|
000ff9c05f | ||
|
|
921af5d27d | ||
|
|
878e1e65eb | ||
|
|
06f5f270cc | ||
|
|
961c22a6fd | ||
|
|
07b4fa55c8 | ||
|
|
041bce9ed6 | ||
|
|
d7a08d81b6 |
4
.github/workflows/build-releases.yml
vendored
4
.github/workflows/build-releases.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
# Only tag with latest when ran against the latest stable branch
|
||||
# This needs to be updated after each minor version release
|
||||
flavor: |
|
||||
latest=${{ startsWith(github.ref, 'refs/tags/v4.3.') }}
|
||||
latest=false
|
||||
tags: |
|
||||
type=pep440,pattern={{raw}}
|
||||
type=pep440,pattern=v{{major}}.{{minor}}
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
# Only tag with latest when ran against the latest stable branch
|
||||
# This needs to be updated after each minor version release
|
||||
flavor: |
|
||||
latest=${{ startsWith(github.ref, 'refs/tags/v4.3.') }}
|
||||
latest=false
|
||||
tags: |
|
||||
type=pep440,pattern={{raw}}
|
||||
type=pep440,pattern=v{{major}}.{{minor}}
|
||||
|
||||
@@ -9,7 +9,7 @@ permissions:
|
||||
jobs:
|
||||
download-translations-stable:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'mastodon/mastodon'
|
||||
if: github.repository == 'glitch-soc/mastodon'
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
|
||||
1
.github/workflows/crowdin-upload.yml
vendored
1
.github/workflows/crowdin-upload.yml
vendored
@@ -14,6 +14,7 @@ on:
|
||||
- config/locales-glitch/devise.en.yml
|
||||
- config/locales-glitch/doorkeeper.en.yml
|
||||
- .github/workflows/crowdin-upload.yml
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
upload-translations:
|
||||
|
||||
@@ -21,11 +21,11 @@ Metrics/BlockNesting:
|
||||
|
||||
# Configuration parameters: AllowedMethods, AllowedPatterns.
|
||||
Metrics/CyclomaticComplexity:
|
||||
Max: 25
|
||||
Enabled: false
|
||||
|
||||
# Configuration parameters: AllowedMethods, AllowedPatterns.
|
||||
Metrics/PerceivedComplexity:
|
||||
Max: 27
|
||||
Enabled: false
|
||||
|
||||
# This cop supports safe autocorrection (--autocorrect).
|
||||
# Configuration parameters: AllowedVars, DefaultToNil.
|
||||
|
||||
262
CHANGELOG.md
262
CHANGELOG.md
@@ -2,7 +2,258 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [4.4.0] - UNRELEASED
|
||||
## [4.4.15] - 2026-03-24
|
||||
|
||||
### Security
|
||||
|
||||
- Fix insufficient checks on quote authorizations ([GHSA-q4g8-82c5-9h33](https://github.com/mastodon/mastodon/security/advisories/GHSA-q4g8-82c5-9h33))
|
||||
- Fix open redirect in legacy path handler ([GHSA-xqw8-4j56-5hj6](https://github.com/mastodon/mastodon/security/advisories/GHSA-xqw8-4j56-5hj6))
|
||||
- Updated dependencies
|
||||
|
||||
### Added
|
||||
|
||||
- Add for searching already-known private GtS posts (#38057 by @ClearlyClaire)
|
||||
|
||||
### Changed
|
||||
|
||||
- Change media description length limit for remote media attachments from 1500 to 10000 characters (#37921 by @ClearlyClaire)
|
||||
- Change HTTP signatures to skip the `Accept` header (#38132 by @ClearlyClaire)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix some model definitions in `tootctl maintenance fix-duplicates` (#38214 by @ClearlyClaire)
|
||||
- Fix overly strict checks for current username on account migration page (#38183 by @mjankowski)
|
||||
- Fix OpenStack Swift Keystone token rate limiting (#38145 by @hugogameiro)
|
||||
- Fix poll expiration notification being re-triggered on implicit updates (#38078 by @ClearlyClaire)
|
||||
- Fix incorrect translation string in webauthn mailers (#38062 by @mjankowski)
|
||||
- Fix username availability check being wrongly applied on race conditions (#37975 by @ClearlyClaire)
|
||||
- Fix hover card unintentionally being shown in some cases (#38039 and #38112 by @diondiondion)
|
||||
- Fix existing posts not being removed from lists when a list member is unfollowed (#38048 by @ClearlyClaire)
|
||||
|
||||
## [4.4.14] - 2026-02-24
|
||||
|
||||
### Security
|
||||
|
||||
- Reject unconfirmed FASPs (#37926 by @oneiros, [GHSA-qgmm-vr4c-ggjg](https://github.com/mastodon/mastodon/security/advisories/GHSA-qgmm-vr4c-ggjg))
|
||||
- Re-use custom socket class for FASP requests (#37925 by @oneiros, [GHSA-46w6-g98f-wxqm](https://github.com/mastodon/mastodon/security/advisories/GHSA-46w6-g98f-wxqm))
|
||||
|
||||
### Added
|
||||
|
||||
- Add `--suspended-only` option to `tootctl emoji purge` (#37828 and #37861 by @ClearlyClaire and @mjankowski)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix custom emojis not being purged on domain suspension (#37808 by @ClearlyClaire)
|
||||
- Fix processing of object updates with duplicate hashtags (#37756 by @ClearlyClaire)
|
||||
|
||||
## [4.4.13] - 2026-02-03
|
||||
|
||||
### Security
|
||||
|
||||
- Fix ActivityPub collection caching logic for pinned posts and featured tags not checking blocked accounts ([GHSA-ccpr-m53r-mfwr](https://github.com/mastodon/mastodon/security/advisories/GHSA-ccpr-m53r-mfwr))
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix relationship cache not being cleared when handling account migrations (#37664 by @ClearlyClaire)
|
||||
- Fix error when encountering invalid tag in updated object (#37635 by @ClearlyClaire)
|
||||
- Fix recycled connections not being immediately closed (#37335 and #37674 by @ClearlyClaire and @shleeable)
|
||||
|
||||
## [4.4.12] - 2026-01-20
|
||||
|
||||
### Security
|
||||
|
||||
- Fix missing limits on various federated properties [GHSA-gg8q-rcg7-p79g](https://github.com/mastodon/mastodon/security/advisories/GHSA-gg8q-rcg7-p79g)
|
||||
- Fix remote user suspension bypass [GHSA-5h2f-wg8j-xqwp](https://github.com/mastodon/mastodon/security/advisories/GHSA-5h2f-wg8j-xqwp)
|
||||
- Fix missing length limits on some user-provided fields [GHSA-6x3w-9g92-gvf3](https://github.com/mastodon/mastodon/security/advisories/GHSA-6x3w-9g92-gvf3)
|
||||
- Fix missing access check for push notification settings update [GHSA-f3q8-7vw3-69v4](https://github.com/mastodon/mastodon/security/advisories/GHSA-f3q8-7vw3-69v4)
|
||||
|
||||
### Changed
|
||||
|
||||
- Skip tombstone creation on deleting from 404 (#37533 by @ClearlyClaire)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix potential duplicate handling of quote accept/reject/delete (#37537 by @ClearlyClaire)
|
||||
- Fix `FeedManager#filter_from_home` error when handling a reblog of a deleted status (#37486 by @ClearlyClaire)
|
||||
- Fix needlessly complicated SQL query in status batch removal (#37469 by @ClearlyClaire)
|
||||
- Fix `Vary` parsing in cache control enforcement (#37426 by @MegaManSec)
|
||||
- Fix thread-unsafe ActivityPub activity dispatch (#37423 by @MegaManSec)
|
||||
- Fix SignatureParser accepting duplicate parameters in HTTP Signature header (#37375 by @shleeable)
|
||||
|
||||
## [4.4.11] - 2026-01-07
|
||||
|
||||
### Security
|
||||
|
||||
- Fix SSRF protection bypass ([GHSA](https://github.com/mastodon/mastodon/security/advisories/GHSA-xfrj-c749-jxxq))
|
||||
- Fix missing ownership check in severed relationships controller ([GHSA](https://github.com/mastodon/mastodon/security/advisories/GHSA-ww85-x9cp-5v24))
|
||||
|
||||
### Changed
|
||||
|
||||
- Change HTTP Signature verification status from 401 to 503 on temporary failure to get remote actor (#37221 by @ClearlyClaire)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix mentions of domain-blocked users being processed (#37257 by @ClearlyClaire)
|
||||
|
||||
## [4.4.10] - 2025-12-08
|
||||
|
||||
### Security
|
||||
|
||||
- Fix inconsistent error handling leaking information on existence of private posts ([GHSA-gwhw-gcjx-72v8](https://github.com/mastodon/mastodon/security/advisories/GHSA-gwhw-gcjx-72v8))
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix YouTube embeds by sending referer (#37126 by @ChaosExAnima)
|
||||
- Fix YouTube iframe not being able to start at a defined time (#26584 by @BrunoViveiros)
|
||||
- Fix streamed quoted polls not being hydrated correctly (#37118 by @ClearlyClaire)
|
||||
- Fix error handling when re-fetching already-known statuses (#37077 by @ClearlyClaire)
|
||||
- Fix known expensive S3 batch delete operation failing because of short timeouts (#37004 by @ClearlyClaire)
|
||||
|
||||
## [4.4.9] - 2025-11-20
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix `tootctl upgrade storage-schema` failing with `ArgumentError` (#36914 by @shugo)
|
||||
- Fix old previously-undiscovered posts being treated as new when receiving an `Update` (#36848 by @ClearlyClaire)
|
||||
- Fix filters not being applied to quotes in detailed view (#36843 by @ClearlyClaire)
|
||||
|
||||
## [4.4.8] - 2025-10-21
|
||||
|
||||
### Security
|
||||
|
||||
- Fix quote control bypass ([GHSA-8h43-rcqj-wpc6](https://github.com/mastodon/mastodon/security/advisories/GHSA-8h43-rcqj-wpc6))
|
||||
|
||||
## [4.4.7] - 2025-10-15
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix forwarder being called with `nil` status when quote post is soft-deleted (#36463 by @ClearlyClaire)
|
||||
- Fix moderation warning e-mails that include posts (#36462 by @ClearlyClaire)
|
||||
- Fix allow_referrer_origin typo (#36460 by @ShadowJonathan)
|
||||
|
||||
## [4.4.6] - 2025-10-13
|
||||
|
||||
### Security
|
||||
|
||||
- Update dependencies `rack` and `uri`
|
||||
- Fix streaming server connection not being closed on user suspension (by @ThisIsMissEm, [GHSA-r2fh-jr9c-9pxh](https://github.com/mastodon/mastodon/security/advisories/GHSA-r2fh-jr9c-9pxh))
|
||||
- Fix password change through admin CLI not invalidating existing sessions and access tokens (by @ThisIsMissEm, [GHSA-f3q3-rmf7-9655](https://github.com/mastodon/mastodon/security/advisories/GHSA-f3q3-rmf7-9655))
|
||||
- Fix streaming server allowing access to public timelines even without the `read` or `read:statuses` OAuth scopes (by @ThisIsMissEm, [GHSA-7gwh-mw97-qjgp](https://github.com/mastodon/mastodon/security/advisories/GHSA-7gwh-mw97-qjgp))
|
||||
|
||||
### Added
|
||||
|
||||
- Add support for processing quotes of deleted posts signaled through a `Tombstone` (#36381 by @ClearlyClaire)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix quote post state sometimes not being updated through streaming server (#36408 by @ClearlyClaire)
|
||||
- Fix inconsistent “pending tags” count on admin dashboard (#36404 by @mjankowski)
|
||||
- Fix JSON payload being potentially mutated when processing interaction policies (#36392 by @ClearlyClaire)
|
||||
- Fix quotes not being displayed in email notifications (#36379 by @diondiondion)
|
||||
- Fix redirect to external object when URL is missing or malformed (#36347 by @ClearlyClaire)
|
||||
- Fix quotes not being displayed in the featured carousel (#36335 by @diondiondion)
|
||||
|
||||
## [4.4.5] - 2025-09-23
|
||||
|
||||
### Security
|
||||
|
||||
- Update dependencies
|
||||
|
||||
### Added
|
||||
|
||||
- Add support for `has:quote` in search (#36217 by @ClearlyClaire)
|
||||
|
||||
### Changed
|
||||
|
||||
- Change quoted posts from silenced accounts to use a click-through rather than being hidden (#36166 and #36167 by @ClearlyClaire)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix processing of out-of-order `Update` as implicit updates (#36190 by @ClearlyClaire)
|
||||
- Fix getting `Create` and `Update` out of order (#36176 by @ClearlyClaire)
|
||||
- Fix quotes with Content Warnings but no text being shown without Content Warnings (#36150 by @ClearlyClaire)
|
||||
|
||||
## [4.4.4] - 2025-09-16
|
||||
|
||||
### Security
|
||||
|
||||
- Update dependencies
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix missing memoization in `Web::PushNotificationWorker` (#36085 by @ClearlyClaire)
|
||||
- Fix unresponsive areas around GIFV modals in some cases (#36059 by @ClearlyClaire)
|
||||
- Fix missing `beforeUnload` confirmation when a poll is being authored (#36030 by @ClearlyClaire)
|
||||
- Fix processing of remote edited statuses with new media and no text (#35970 by @unfokus)
|
||||
- Fix polls not being displayed in moderation interface (#35644 and #35933 by @ThisIsMissEm)
|
||||
- Fix WebUI handling of deleted quoted posts (#35909 and #35918 by @ClearlyClaire and @diondiondion)
|
||||
- Fix “Edit” and “Delete & Redraft” on a poll not inserting empty option (#35892 by @ClearlyClaire)
|
||||
- Fix loading of some compatibility CSS on some configurations (#35876 by @shleeable)
|
||||
- Fix HttpLog not being enabled with `RAILS_LOG_LEVEL=debug` (#35833 by @mjankowski)
|
||||
- Fix self-destruct scheduler behavior on some Redis setups (#35823 by @ClearlyClaire)
|
||||
- Fix `tootctl admin create` not bypassing reserved username checks (#35779 by @ClearlyClaire)
|
||||
- Fix interaction policy changes in implicit updates not being saved (#35751 by @ClearlyClaire)
|
||||
- Fix quote revocation not being streamed (#35710 by @ClearlyClaire)
|
||||
- Fix export of large user archives by enabling Zip64 (#35850 by @ClearlyClaire)
|
||||
|
||||
### Changed
|
||||
|
||||
- Change labels for quote policy settings (#35893 by @ClearlyClaire)
|
||||
- Change standalone “Share” page to redirect to web interface after posting (#35763 by @ChaosExAnima)
|
||||
|
||||
## [4.4.3] - 2025-08-05
|
||||
|
||||
### Security
|
||||
|
||||
- Update dependencies
|
||||
- Fix incorrect rate-limit handling [GHSA-84ch-6436-c7mg](https://github.com/mastodon/mastodon/security/advisories/GHSA-84ch-6436-c7mg)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix race condition caused by ActiveRecord query cache in `Create` critical path (#35662 by @ClearlyClaire)
|
||||
- Fix race condition caused by quote post processing (#35657 by @ClearlyClaire)
|
||||
- Fix WebUI crashing for accounts with `null` URL (#35651 by @ClearlyClaire)
|
||||
- Fix friends-of-friends recommendations suggesting already-requested accounts (#35604 by @ClearlyClaire)
|
||||
- Fix synchronous recursive fetching of deeply-nested quoted posts (#35600 by @ClearlyClaire)
|
||||
- Fix “Expand this post” link including user `@undefined` (#35478 by @ClearlyClaire)
|
||||
|
||||
### Changed
|
||||
|
||||
- Change `StatusReachFinder` to consider quotes as well as reblogs (#35601 by @ClearlyClaire)
|
||||
- Add restrictions on which quote posts can trend (#35507 by @ClearlyClaire)
|
||||
- Change quote verification to not bypass authorization flow for mentions (#35528 by @ClearlyClaire)
|
||||
|
||||
## [4.4.2] - 2025-07-23
|
||||
|
||||
### Security
|
||||
|
||||
- Update dependencies
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix menu not clickable in Firefox (#35390 and #35414 by @diondiondion)
|
||||
- Add `lang` attribute to current composer language in alt text modal (#35412 by @diondiondion)
|
||||
- Fix quote posts styling on notifications page (#35411 by @diondiondion)
|
||||
- Improve a11y of custom select menus in notifications settings (#35403 by @diondiondion)
|
||||
- Fix selected item in poll select menus is unreadable in Firefox (#35402 by @diondiondion)
|
||||
- Update age limit wording (#35387 by @diondiondion)
|
||||
- Fix support for quote verification in implicit status updates (#35384 by @ClearlyClaire)
|
||||
- Improve `Dropdown` component accessibility (#35373 by @diondiondion)
|
||||
- Fix processing some incoming quotes failing because of missing JSON-LD context (#35354 and #35380 by @ClearlyClaire)
|
||||
- Make bio hashtags open the local page instead of the remote instance (#35349 by @ChaosExAnima)
|
||||
- Fix styling of external log-in button (#35320 by @ClearlyClaire)
|
||||
|
||||
## [4.4.1] - 2025-07-09
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix nearly every sub-directory being crawled as part of Vite build (#35323 by @ClearlyClaire)
|
||||
- Fix assets not building when Redis is unavailable (#35321 by @oneiros)
|
||||
- Fix replying from media modal or pop-in-player tagging user `@undefined` (#35317 by @ClearlyClaire)
|
||||
- Fix support for special characters in various environment variables (#35314 by @mjankowski and @ClearlyClaire)
|
||||
- Fix some database migrations failing for indexes manually removed by admins (#35309 by @mjankowski)
|
||||
|
||||
## [4.4.0] - 2025-07-08
|
||||
|
||||
### Added
|
||||
|
||||
@@ -38,7 +289,7 @@ All notable changes to this project will be documented in this file.
|
||||
Server administrators can now chose to opt in to transmit referrer information when following an external link. Only the domain name is transmitted, not the referrer path.
|
||||
- Add double tap to zoom and swipe to dismiss to media modal in web UI (#34210 by @Gargron)
|
||||
- Add link from Web UI for Hashtags to the Moderation UI (#31448 by @ThisIsMissEm)
|
||||
- **Add terms of service** (#33055, #33233, #33230, #33703, #33699, #33994, #33993, #34105, #34122, #34200, #34527, #35053, #35115, #35126 and #35127 by @ClearlyClaire, @Gargron, @mjankowski, and @oneiros)\
|
||||
- **Add terms of service** (#33055, #33233, #33230, #33703, #33699, #33994, #33993, #34105, #34122, #34200, #34527, #35053, #35115, #35126, #35127 and #35233 by @ClearlyClaire, @Gargron, @mjankowski, and @oneiros)\
|
||||
Server administrators can now fill in Terms of Service and notify their users of upcoming changes.
|
||||
- Add optional bulk mailer settings (#35191 and #35203 by @oneiros)\
|
||||
This adds the optional environment variables `BULK_SMTP_PORT`, `BULK_SMTP_SERVER`, `BULK_SMTP_LOGIN` and so on analogous to `SMTP_PORT`, `SMTP_SERVER`, `SMTP_LOGIN` and related SMTP configuration environment variables.\
|
||||
@@ -51,7 +302,7 @@ All notable changes to this project will be documented in this file.
|
||||
- Add ability to dismiss alt text badge by tapping it in web UI (#33737 by @Gargron)
|
||||
- Add loading indicator to timeline gap indicators in web UI (#33762 by @Gargron)
|
||||
- Add interaction modal when trying to interact with a poll while logged out (#32609 by @ThisIsMissEm)
|
||||
- **Add experimental FASP support** (#34031, #34415, #34765, #34965, #34964, #34033 and #35218 by @oneiros)\
|
||||
- **Add experimental FASP support** (#34031, #34415, #34765, #34965, #34964, #34033, #35218, #35262 and #35263 by @oneiros)\
|
||||
This is a first step towards supporting “Fediverse Auxiliary Service Providers” (https://github.com/mastodon/fediverse_auxiliary_service_provider_specifications). This is mostly interesting to developers who would like to implement their own FASP, but also includes the capability to share data with a discovery provider (see https://www.fediscovery.org).
|
||||
- Add ability for admins to send announcements to all users via email (#33928 and #34411 by @ClearlyClaire)\
|
||||
This is meant for critical announcements only, as this will potentially send a lot of emails and cannot be opted out of by users.
|
||||
@@ -64,7 +315,7 @@ All notable changes to this project will be documented in this file.
|
||||
- Add dropdown menu with quick actions to lists of accounts in web UI (#34391, #34709, and #34767 by @Gargron, @diondiondion, and @mkljczk)
|
||||
- Add support for displaying “year in review” notification in web UI (#32710, #32765, #32709, #32807, #32914, #33148, and #33882 by @Gargron and @mjankowski)\
|
||||
Note that the notification is currently not generated automatically, and at the moment requires a manual undocumented administrator action.
|
||||
- Add experimental support for receiving HTTP Message Signatures (RFC9421) (#34814, #35033 and #35109 by @oneiros)\
|
||||
- Add experimental support for receiving HTTP Message Signatures (RFC9421) (#34814, #35033, #35109 and #35278 by @oneiros)\
|
||||
For now, this needs to be explicitly enabled through the `http_message_signatures` feature flag (`EXPERIMENTAL_FEATURES=http_message_signatures`). This currently only covers verifying such signatures (inbound HTTP requests), not issuing them (outbound HTTP requests).
|
||||
- Add experimental Async Refreshes API (#34918 by @oneiros)
|
||||
- Add experimental server-side feature to fetch remote replies (#32615, #34147, #34149, #34151, #34615, #34682, and #34702 by @ClearlyClaire and @sneakers-the-rat)\
|
||||
@@ -218,6 +469,7 @@ All notable changes to this project will be documented in this file.
|
||||
- Fix admin dashboard crash on specific Elasticsearch connection errors (#34683 by @ClearlyClaire)
|
||||
- Fix OIDC account creation failing for long display names (#34639 by @defnull)
|
||||
- Fix use of the deprecated `/api/v1/instance` endpoint in the moderation interface (#34613 by @renchap)
|
||||
- Fix inaccessible “Clear search” button (#35152 and #35281 by @diondiondion)
|
||||
- Fix search operators sometimes getting lost (#35190 by @ClearlyClaire)
|
||||
- Fix directory scroll position reset (#34560 by @przucidlo)
|
||||
- Fix needlessly complex SVG paths for oEmbed and logo (#34538 by @edent)
|
||||
@@ -232,7 +484,7 @@ All notable changes to this project will be documented in this file.
|
||||
- Fix extra space under left-indented vertical videos (#34313 by @ClearlyClaire)
|
||||
- Fix glitchy iOS media attachment drag interactions (#35057 by @diondiondion)
|
||||
- Fix zoomed images being blurry in Safari (#35052 by @diondiondion)
|
||||
- Fix redundant focus stop within status component in Web UI and make focus style more noticeable (#35037, #35051, #35096 and #35150 by @diondiondion)
|
||||
- Fix redundant focus stop within status component in Web UI and make focus style more noticeable (#35037, #35051, #35096, #35150 and #35251 by @diondiondion)
|
||||
- Fix digits in media player time readout not having a consistent width (#35038 by @diondiondion)
|
||||
- Fix wrong text color for “Open in advanced web interface” banner in high-contrast theme (#35032 by @diondiondion)
|
||||
- Fix hover card for limited accounts not hiding information as expected (#35024 by @diondiondion)
|
||||
|
||||
@@ -48,3 +48,23 @@ Mastodon requires all `POST` requests to be signed, and MAY require `GET` reques
|
||||
### Additional documentation
|
||||
|
||||
- [Mastodon documentation](https://docs.joinmastodon.org/)
|
||||
|
||||
## Size limits
|
||||
|
||||
Mastodon imposes a few hard limits on federated content.
|
||||
These limits are intended to be very generous and way above what the Mastodon user experience is optimized for, so as to accomodate future changes and unusual or unforeseen usage patterns, while still providing some limits for performance reasons.
|
||||
The following table attempts to summary those limits.
|
||||
|
||||
| Limited property | Size limit | Consequence of exceeding the limit |
|
||||
| ------------------------------------------------------------- | ---------- | ---------------------------------- |
|
||||
| Serialized JSON-LD | 1MB | **Activity is rejected/dropped** |
|
||||
| Profile fields (actor `PropertyValue` attachments) name/value | 2047 | Field name/value is truncated |
|
||||
| Number of profile fields (actor `PropertyValue` attachments) | 50 | Fields list is truncated |
|
||||
| Poll options (number of `anyOf`/`oneOf` in a `Question`) | 500 | Items list is truncated |
|
||||
| Account username (actor `preferredUsername`) length | 2048 | **Actor will be rejected** |
|
||||
| Account display name (actor `name`) length | 2048 | Display name will be truncated |
|
||||
| Account note (actor `summary`) length | 20kB | Account note will be truncated |
|
||||
| Account `attributionDomains` | 256 | List will be truncated |
|
||||
| Account aliases (actor `alsoKnownAs`) | 256 | List will be truncated |
|
||||
| Custom emoji shortcode (`Emoji` `name`) | 2048 | Emoji will be rejected |
|
||||
| Media descriptions (`name`/`summary`) | 10000 | Description will be truncated |
|
||||
|
||||
5
Gemfile
5
Gemfile
@@ -28,7 +28,7 @@ gem 'bootsnap', '~> 1.18.0', require: false
|
||||
gem 'browser'
|
||||
gem 'charlock_holmes', '~> 0.7.7'
|
||||
gem 'chewy', '~> 7.3'
|
||||
gem 'devise', '~> 4.9'
|
||||
gem 'devise'
|
||||
gem 'devise-two-factor'
|
||||
|
||||
group :pam_authentication, optional: true do
|
||||
@@ -159,6 +159,9 @@ group :test do
|
||||
|
||||
# Stub web requests for specs
|
||||
gem 'webmock', '~> 3.18'
|
||||
|
||||
# Websocket driver for testing integration between rails/sidekiq and streaming
|
||||
gem 'websocket-driver', '~> 0.8', require: false
|
||||
end
|
||||
|
||||
group :development do
|
||||
|
||||
143
Gemfile.lock
143
Gemfile.lock
@@ -10,29 +10,29 @@ GIT
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
actioncable (8.0.2)
|
||||
actionpack (= 8.0.2)
|
||||
activesupport (= 8.0.2)
|
||||
actioncable (8.0.4.1)
|
||||
actionpack (= 8.0.4.1)
|
||||
activesupport (= 8.0.4.1)
|
||||
nio4r (~> 2.0)
|
||||
websocket-driver (>= 0.6.1)
|
||||
zeitwerk (~> 2.6)
|
||||
actionmailbox (8.0.2)
|
||||
actionpack (= 8.0.2)
|
||||
activejob (= 8.0.2)
|
||||
activerecord (= 8.0.2)
|
||||
activestorage (= 8.0.2)
|
||||
activesupport (= 8.0.2)
|
||||
actionmailbox (8.0.4.1)
|
||||
actionpack (= 8.0.4.1)
|
||||
activejob (= 8.0.4.1)
|
||||
activerecord (= 8.0.4.1)
|
||||
activestorage (= 8.0.4.1)
|
||||
activesupport (= 8.0.4.1)
|
||||
mail (>= 2.8.0)
|
||||
actionmailer (8.0.2)
|
||||
actionpack (= 8.0.2)
|
||||
actionview (= 8.0.2)
|
||||
activejob (= 8.0.2)
|
||||
activesupport (= 8.0.2)
|
||||
actionmailer (8.0.4.1)
|
||||
actionpack (= 8.0.4.1)
|
||||
actionview (= 8.0.4.1)
|
||||
activejob (= 8.0.4.1)
|
||||
activesupport (= 8.0.4.1)
|
||||
mail (>= 2.8.0)
|
||||
rails-dom-testing (~> 2.2)
|
||||
actionpack (8.0.2)
|
||||
actionview (= 8.0.2)
|
||||
activesupport (= 8.0.2)
|
||||
actionpack (8.0.4.1)
|
||||
actionview (= 8.0.4.1)
|
||||
activesupport (= 8.0.4.1)
|
||||
nokogiri (>= 1.8.5)
|
||||
rack (>= 2.2.4)
|
||||
rack-session (>= 1.0.1)
|
||||
@@ -40,15 +40,15 @@ GEM
|
||||
rails-dom-testing (~> 2.2)
|
||||
rails-html-sanitizer (~> 1.6)
|
||||
useragent (~> 0.16)
|
||||
actiontext (8.0.2)
|
||||
actionpack (= 8.0.2)
|
||||
activerecord (= 8.0.2)
|
||||
activestorage (= 8.0.2)
|
||||
activesupport (= 8.0.2)
|
||||
actiontext (8.0.4.1)
|
||||
actionpack (= 8.0.4.1)
|
||||
activerecord (= 8.0.4.1)
|
||||
activestorage (= 8.0.4.1)
|
||||
activesupport (= 8.0.4.1)
|
||||
globalid (>= 0.6.0)
|
||||
nokogiri (>= 1.8.5)
|
||||
actionview (8.0.2)
|
||||
activesupport (= 8.0.2)
|
||||
actionview (8.0.4.1)
|
||||
activesupport (= 8.0.4.1)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.11)
|
||||
rails-dom-testing (~> 2.2)
|
||||
@@ -58,22 +58,22 @@ GEM
|
||||
activemodel (>= 4.1)
|
||||
case_transform (>= 0.2)
|
||||
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
|
||||
activejob (8.0.2)
|
||||
activesupport (= 8.0.2)
|
||||
activejob (8.0.4.1)
|
||||
activesupport (= 8.0.4.1)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (8.0.2)
|
||||
activesupport (= 8.0.2)
|
||||
activerecord (8.0.2)
|
||||
activemodel (= 8.0.2)
|
||||
activesupport (= 8.0.2)
|
||||
activemodel (8.0.4.1)
|
||||
activesupport (= 8.0.4.1)
|
||||
activerecord (8.0.4.1)
|
||||
activemodel (= 8.0.4.1)
|
||||
activesupport (= 8.0.4.1)
|
||||
timeout (>= 0.4.0)
|
||||
activestorage (8.0.2)
|
||||
actionpack (= 8.0.2)
|
||||
activejob (= 8.0.2)
|
||||
activerecord (= 8.0.2)
|
||||
activesupport (= 8.0.2)
|
||||
activestorage (8.0.4.1)
|
||||
actionpack (= 8.0.4.1)
|
||||
activejob (= 8.0.4.1)
|
||||
activerecord (= 8.0.4.1)
|
||||
activesupport (= 8.0.4.1)
|
||||
marcel (~> 1.0)
|
||||
activesupport (8.0.2)
|
||||
activesupport (8.0.4.1)
|
||||
base64
|
||||
benchmark (>= 0.3)
|
||||
bigdecimal
|
||||
@@ -82,7 +82,7 @@ GEM
|
||||
drb
|
||||
i18n (>= 1.6, < 2)
|
||||
logger (>= 1.4.2)
|
||||
minitest (>= 5.1)
|
||||
minitest (>= 5.1, < 6)
|
||||
securerandom (>= 0.3)
|
||||
tzinfo (~> 2.0, >= 2.0.5)
|
||||
uri (>= 0.13.1)
|
||||
@@ -184,16 +184,16 @@ GEM
|
||||
irb (~> 1.10)
|
||||
reline (>= 0.3.8)
|
||||
debug_inspector (1.2.0)
|
||||
devise (4.9.4)
|
||||
devise (5.0.3)
|
||||
bcrypt (~> 3.0)
|
||||
orm_adapter (~> 0.1)
|
||||
railties (>= 4.1.0)
|
||||
railties (>= 7.0)
|
||||
responders
|
||||
warden (~> 1.2.3)
|
||||
devise-two-factor (6.1.0)
|
||||
activesupport (>= 7.0, < 8.1)
|
||||
devise (~> 4.0)
|
||||
railties (>= 7.0, < 8.1)
|
||||
devise-two-factor (6.4.0)
|
||||
activesupport (>= 7.2, < 8.2)
|
||||
devise (>= 4.0, < 6.0)
|
||||
railties (>= 7.2, < 8.2)
|
||||
rotp (~> 6.0)
|
||||
devise_pam_authenticatable2 (9.2.0)
|
||||
devise (>= 4.0.0)
|
||||
@@ -233,7 +233,7 @@ GEM
|
||||
fabrication (3.0.0)
|
||||
faker (3.5.1)
|
||||
i18n (>= 1.8.11, < 2)
|
||||
faraday (2.13.1)
|
||||
faraday (2.14.1)
|
||||
faraday-net_http (>= 2.0, < 3.5)
|
||||
json
|
||||
logger
|
||||
@@ -458,7 +458,7 @@ GEM
|
||||
net-smtp (0.5.1)
|
||||
net-protocol
|
||||
nio4r (2.7.4)
|
||||
nokogiri (1.18.8)
|
||||
nokogiri (1.18.10)
|
||||
mini_portile2 (~> 2.8.2)
|
||||
racc (~> 1.4)
|
||||
oj (3.16.11)
|
||||
@@ -494,7 +494,7 @@ GEM
|
||||
tzinfo
|
||||
validate_url
|
||||
webfinger (~> 2.0)
|
||||
openssl (3.3.0)
|
||||
openssl (3.3.1)
|
||||
openssl-signature_algorithm (1.3.0)
|
||||
openssl (> 2.0)
|
||||
opentelemetry-api (1.5.0)
|
||||
@@ -642,7 +642,7 @@ GEM
|
||||
activesupport (>= 3.0.0)
|
||||
raabro (1.4.0)
|
||||
racc (1.8.1)
|
||||
rack (3.1.16)
|
||||
rack (3.1.20)
|
||||
rack-attack (6.7.0)
|
||||
rack (>= 1.0, < 4)
|
||||
rack-cors (3.0.0)
|
||||
@@ -668,20 +668,20 @@ GEM
|
||||
rack (>= 1.3)
|
||||
rackup (2.2.1)
|
||||
rack (>= 3)
|
||||
rails (8.0.2)
|
||||
actioncable (= 8.0.2)
|
||||
actionmailbox (= 8.0.2)
|
||||
actionmailer (= 8.0.2)
|
||||
actionpack (= 8.0.2)
|
||||
actiontext (= 8.0.2)
|
||||
actionview (= 8.0.2)
|
||||
activejob (= 8.0.2)
|
||||
activemodel (= 8.0.2)
|
||||
activerecord (= 8.0.2)
|
||||
activestorage (= 8.0.2)
|
||||
activesupport (= 8.0.2)
|
||||
rails (8.0.4.1)
|
||||
actioncable (= 8.0.4.1)
|
||||
actionmailbox (= 8.0.4.1)
|
||||
actionmailer (= 8.0.4.1)
|
||||
actionpack (= 8.0.4.1)
|
||||
actiontext (= 8.0.4.1)
|
||||
actionview (= 8.0.4.1)
|
||||
activejob (= 8.0.4.1)
|
||||
activemodel (= 8.0.4.1)
|
||||
activerecord (= 8.0.4.1)
|
||||
activestorage (= 8.0.4.1)
|
||||
activesupport (= 8.0.4.1)
|
||||
bundler (>= 1.15.0)
|
||||
railties (= 8.0.2)
|
||||
railties (= 8.0.4.1)
|
||||
rails-dom-testing (2.2.0)
|
||||
activesupport (>= 5.0.0)
|
||||
minitest
|
||||
@@ -692,13 +692,14 @@ GEM
|
||||
rails-i18n (8.0.1)
|
||||
i18n (>= 0.7, < 2)
|
||||
railties (>= 8.0.0, < 9)
|
||||
railties (8.0.2)
|
||||
actionpack (= 8.0.2)
|
||||
activesupport (= 8.0.2)
|
||||
railties (8.0.4.1)
|
||||
actionpack (= 8.0.4.1)
|
||||
activesupport (= 8.0.4.1)
|
||||
irb (~> 1.13)
|
||||
rackup (>= 1.0.0)
|
||||
rake (>= 12.2)
|
||||
thor (~> 1.0, >= 1.2.2)
|
||||
tsort (>= 0.2)
|
||||
zeitwerk (~> 2.6)
|
||||
rainbow (3.1.1)
|
||||
rake (13.3.0)
|
||||
@@ -725,7 +726,7 @@ GEM
|
||||
responders (3.1.1)
|
||||
actionpack (>= 5.2)
|
||||
railties (>= 5.2)
|
||||
rexml (3.4.1)
|
||||
rexml (3.4.4)
|
||||
rotp (6.3.0)
|
||||
rouge (4.5.2)
|
||||
rpam2 (4.0.2)
|
||||
@@ -801,7 +802,7 @@ GEM
|
||||
ruby-prof (1.7.2)
|
||||
base64
|
||||
ruby-progressbar (1.13.0)
|
||||
ruby-saml (1.18.0)
|
||||
ruby-saml (1.18.1)
|
||||
nokogiri (>= 1.13.10)
|
||||
rexml
|
||||
ruby-vips (2.2.4)
|
||||
@@ -869,13 +870,14 @@ GEM
|
||||
terrapin (1.1.0)
|
||||
climate_control
|
||||
test-prof (1.4.4)
|
||||
thor (1.3.2)
|
||||
thor (1.4.0)
|
||||
tilt (2.6.0)
|
||||
timeout (0.4.3)
|
||||
tpm-key_attestation (0.14.1)
|
||||
bindata (~> 2.4)
|
||||
openssl (> 2.0)
|
||||
openssl-signature_algorithm (~> 1.0)
|
||||
tsort (0.2.0)
|
||||
tty-color (0.6.0)
|
||||
tty-cursor (0.7.1)
|
||||
tty-prompt (0.23.1)
|
||||
@@ -899,7 +901,7 @@ GEM
|
||||
unicode-display_width (3.1.4)
|
||||
unicode-emoji (~> 4.0, >= 4.0.4)
|
||||
unicode-emoji (4.0.4)
|
||||
uri (1.0.3)
|
||||
uri (1.0.4)
|
||||
useragent (0.16.11)
|
||||
validate_url (1.0.15)
|
||||
activemodel (>= 3.0.0)
|
||||
@@ -932,7 +934,7 @@ GEM
|
||||
crack (>= 0.3.2)
|
||||
hashdiff (>= 0.4.0, < 2.0.0)
|
||||
webrick (1.9.1)
|
||||
websocket-driver (0.7.7)
|
||||
websocket-driver (0.8.0)
|
||||
base64
|
||||
websocket-extensions (>= 0.1.0)
|
||||
websocket-extensions (0.1.5)
|
||||
@@ -970,7 +972,7 @@ DEPENDENCIES
|
||||
csv (~> 3.2)
|
||||
database_cleaner-active_record
|
||||
debug (~> 1.8)
|
||||
devise (~> 4.9)
|
||||
devise
|
||||
devise-two-factor
|
||||
devise_pam_authenticatable2 (~> 9.2)
|
||||
discard (~> 1.2)
|
||||
@@ -1096,6 +1098,7 @@ DEPENDENCIES
|
||||
webauthn (~> 3.0)
|
||||
webmock (~> 3.18)
|
||||
webpush!
|
||||
websocket-driver (~> 0.8)
|
||||
xorcist (~> 1.1)
|
||||
|
||||
RUBY VERSION
|
||||
|
||||
10
SECURITY.md
10
SECURITY.md
@@ -13,8 +13,8 @@ A "vulnerability in Mastodon" is a vulnerability in the code distributed through
|
||||
|
||||
## Supported Versions
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | --------- |
|
||||
| 4.3.x | Yes |
|
||||
| 4.2.x | Yes |
|
||||
| < 4.2 | No |
|
||||
| Version | Supported |
|
||||
| ------- | ---------------- |
|
||||
| 4.4.x | Yes |
|
||||
| 4.3.x | Until 2026-05-06 |
|
||||
| < 4.3 | No |
|
||||
|
||||
@@ -4,17 +4,31 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController
|
||||
vary_by -> { 'Signature' if authorized_fetch_mode? }
|
||||
|
||||
before_action :require_account_signature!, if: :authorized_fetch_mode?
|
||||
before_action :check_authorization
|
||||
before_action :set_items
|
||||
before_action :set_size
|
||||
before_action :set_type
|
||||
|
||||
def show
|
||||
expires_in 3.minutes, public: public_fetch_mode?
|
||||
render_with_cache json: collection_presenter, content_type: 'application/activity+json', serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter
|
||||
|
||||
if @unauthorized
|
||||
render json: collection_presenter, content_type: 'application/activity+json', serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter
|
||||
else
|
||||
render_with_cache json: collection_presenter, content_type: 'application/activity+json', serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_authorization
|
||||
# Because in public fetch mode we cache the response, there would be no
|
||||
# benefit from performing the check below, since a blocked account or domain
|
||||
# would likely be served the cache from the reverse proxy anyway
|
||||
|
||||
@unauthorized = authorized_fetch_mode? && !signed_request_account.nil? && (@account.blocking?(signed_request_account) || (!signed_request_account.domain.nil? && @account.domain_blocking?(signed_request_account.domain)))
|
||||
end
|
||||
|
||||
def set_items
|
||||
case params[:id]
|
||||
when 'featured'
|
||||
@@ -57,11 +71,7 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController
|
||||
end
|
||||
|
||||
def for_signed_account
|
||||
# Because in public fetch mode we cache the response, there would be no
|
||||
# benefit from performing the check below, since a blocked account or domain
|
||||
# would likely be served the cache from the reverse proxy anyway
|
||||
|
||||
if authorized_fetch_mode? && !signed_request_account.nil? && (@account.blocking?(signed_request_account) || (!signed_request_account.domain.nil? && @account.domain_blocking?(signed_request_account.domain)))
|
||||
if @unauthorized
|
||||
[]
|
||||
else
|
||||
yield
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
class ActivityPub::InboxesController < ActivityPub::BaseController
|
||||
include JsonLdHelper
|
||||
|
||||
before_action :skip_large_payload
|
||||
before_action :skip_unknown_actor_activity
|
||||
before_action :require_actor_signature!
|
||||
skip_before_action :authenticate_user!
|
||||
@@ -16,6 +17,10 @@ class ActivityPub::InboxesController < ActivityPub::BaseController
|
||||
|
||||
private
|
||||
|
||||
def skip_large_payload
|
||||
head 413 if request.content_length > ActivityPub::Activity::MAX_JSON_SIZE
|
||||
end
|
||||
|
||||
def skip_unknown_actor_activity
|
||||
head 202 if unknown_affected_account?
|
||||
end
|
||||
|
||||
@@ -22,7 +22,7 @@ class ActivityPub::LikesController < ActivityPub::BaseController
|
||||
def set_status
|
||||
@status = @account.statuses.find(params[:status_id])
|
||||
authorize @status, :show?
|
||||
rescue Mastodon::NotPermittedError
|
||||
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||
not_found
|
||||
end
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ class ActivityPub::RepliesController < ActivityPub::BaseController
|
||||
def set_status
|
||||
@status = @account.statuses.find(params[:status_id])
|
||||
authorize @status, :show?
|
||||
rescue Mastodon::NotPermittedError
|
||||
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||
not_found
|
||||
end
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ class ActivityPub::SharesController < ActivityPub::BaseController
|
||||
def set_status
|
||||
@status = @account.statuses.find(params[:status_id])
|
||||
authorize @status, :show?
|
||||
rescue Mastodon::NotPermittedError
|
||||
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||
not_found
|
||||
end
|
||||
|
||||
|
||||
@@ -14,16 +14,20 @@ module Admin
|
||||
def create
|
||||
authorize @account, :show?
|
||||
|
||||
account_action = Admin::AccountAction.new(resource_params)
|
||||
account_action.target_account = @account
|
||||
account_action.current_account = current_account
|
||||
@account_action = Admin::AccountAction.new(resource_params)
|
||||
@account_action.target_account = @account
|
||||
@account_action.current_account = current_account
|
||||
|
||||
account_action.save!
|
||||
|
||||
if account_action.with_report?
|
||||
redirect_to admin_reports_path, notice: I18n.t('admin.reports.processed_msg', id: resource_params[:report_id])
|
||||
if @account_action.save
|
||||
if @account_action.with_report?
|
||||
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
|
||||
else
|
||||
redirect_to admin_account_path(@account.id)
|
||||
@warning_presets = AccountWarningPreset.all
|
||||
|
||||
render :new
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -9,10 +9,16 @@ module Admin
|
||||
|
||||
@pending_appeals_count = Appeal.pending.async_count
|
||||
@pending_reports_count = Report.unresolved.async_count
|
||||
@pending_tags_count = Tag.pending_review.async_count
|
||||
@pending_tags_count = pending_tags.async_count
|
||||
@pending_users_count = User.pending.async_count
|
||||
@system_checks = Admin::SystemCheck.perform(current_user)
|
||||
@time_period = (29.days.ago.to_date...Time.now.utc.to_date)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def pending_tags
|
||||
::Trends::TagFilter.new(status: :pending_review).results
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -47,7 +47,7 @@ class Api::Fasp::BaseController < ApplicationController
|
||||
provider = nil
|
||||
|
||||
Linzer.verify!(request.rack_request, no_older_than: 5.minutes) do |keyid|
|
||||
provider = Fasp::Provider.find(keyid)
|
||||
provider = Fasp::Provider.confirmed.find(keyid)
|
||||
Linzer.new_ed25519_public_key(provider.provider_public_key_pem, keyid)
|
||||
end
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ class Api::V1::Polls::VotesController < Api::BaseController
|
||||
def set_poll
|
||||
@poll = Poll.find(params[:poll_id])
|
||||
authorize @poll.status, :show?
|
||||
rescue Mastodon::NotPermittedError
|
||||
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||
not_found
|
||||
end
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ class Api::V1::PollsController < Api::BaseController
|
||||
def set_poll
|
||||
@poll = Poll.find(params[:id])
|
||||
authorize @poll.status, :show?
|
||||
rescue Mastodon::NotPermittedError
|
||||
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||
not_found
|
||||
end
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ class Api::V1::Statuses::BaseController < Api::BaseController
|
||||
def set_status
|
||||
@status = Status.find(params[:status_id])
|
||||
authorize @status, :show?
|
||||
rescue Mastodon::NotPermittedError
|
||||
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||
not_found
|
||||
end
|
||||
end
|
||||
|
||||
@@ -23,7 +23,7 @@ class Api::V1::Statuses::BookmarksController < Api::V1::Statuses::BaseController
|
||||
bookmark&.destroy!
|
||||
|
||||
render json: @status, serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new([@status], current_account.id, bookmarks_map: { @status.id => false })
|
||||
rescue Mastodon::NotPermittedError
|
||||
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||
not_found
|
||||
end
|
||||
end
|
||||
|
||||
@@ -25,7 +25,7 @@ class Api::V1::Statuses::FavouritesController < Api::V1::Statuses::BaseControlle
|
||||
|
||||
relationships = StatusRelationshipsPresenter.new([@status], current_account.id, favourites_map: { @status.id => false }, attributes_map: { @status.id => { favourites_count: count } })
|
||||
render json: @status, serializer: REST::StatusSerializer, relationships: relationships
|
||||
rescue Mastodon::NotPermittedError
|
||||
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||
not_found
|
||||
end
|
||||
end
|
||||
|
||||
@@ -36,7 +36,7 @@ class Api::V1::Statuses::ReblogsController < Api::V1::Statuses::BaseController
|
||||
|
||||
relationships = StatusRelationshipsPresenter.new([@status], current_account.id, reblogs_map: { @reblog.id => false }, attributes_map: { @reblog.id => { reblogs_count: count } })
|
||||
render json: @reblog, serializer: REST::StatusSerializer, relationships: relationships
|
||||
rescue Mastodon::NotPermittedError
|
||||
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||
not_found
|
||||
end
|
||||
|
||||
@@ -45,7 +45,7 @@ class Api::V1::Statuses::ReblogsController < Api::V1::Statuses::BaseController
|
||||
def set_reblog
|
||||
@reblog = Status.find(params[:status_id])
|
||||
authorize @reblog, :show?
|
||||
rescue Mastodon::NotPermittedError
|
||||
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||
not_found
|
||||
end
|
||||
|
||||
|
||||
@@ -129,7 +129,7 @@ class Api::V1::StatusesController < Api::BaseController
|
||||
def set_status
|
||||
@status = Status.find(params[:id])
|
||||
authorize @status, :show?
|
||||
rescue Mastodon::NotPermittedError
|
||||
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||
not_found
|
||||
end
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ class Api::Web::EmbedsController < Api::Web::BaseController
|
||||
def set_status
|
||||
@status = Status.find(params[:id])
|
||||
authorize @status, :show?
|
||||
rescue Mastodon::NotPermittedError
|
||||
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||
not_found
|
||||
end
|
||||
end
|
||||
|
||||
@@ -62,7 +62,7 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController
|
||||
end
|
||||
|
||||
def set_push_subscription
|
||||
@push_subscription = ::Web::PushSubscription.find(params[:id])
|
||||
@push_subscription = ::Web::PushSubscription.where(user_id: active_session.user_id).find(params[:id])
|
||||
end
|
||||
|
||||
def subscription_params
|
||||
|
||||
@@ -186,14 +186,14 @@ class Auth::SessionsController < Devise::SessionsController
|
||||
"2fa_auth_attempts:#{user.id}:#{Time.now.utc.hour}"
|
||||
end
|
||||
|
||||
def respond_to_on_destroy
|
||||
def respond_to_on_destroy(**)
|
||||
respond_to do |format|
|
||||
format.json do
|
||||
render json: {
|
||||
redirect_to: after_sign_out_path_for(resource_name),
|
||||
}, status: 200
|
||||
end
|
||||
format.all { super }
|
||||
format.all { super(**) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -21,7 +21,7 @@ class AuthorizeInteractionsController < ApplicationController
|
||||
def set_resource
|
||||
@resource = located_resource
|
||||
authorize(@resource, :show?) if @resource.is_a?(Status)
|
||||
rescue Mastodon::NotPermittedError
|
||||
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||
not_found
|
||||
end
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ module CacheConcern
|
||||
# from being used as cache keys, while allowing to `Vary` on them (to not serve
|
||||
# anonymous cached data to authenticated requests when authentication matters)
|
||||
def enforce_cache_control!
|
||||
vary = response.headers['Vary']&.split&.map { |x| x.strip.downcase }
|
||||
vary = response.headers['Vary'].to_s.split(',').map { |x| x.strip.downcase }.reject(&:empty?)
|
||||
return unless vary.present? && %w(cookie authorization signature).any? { |header| vary.include?(header) && request.headers[header].present? }
|
||||
|
||||
response.cache_control.replace(private: true, no_store: true)
|
||||
|
||||
@@ -64,13 +64,19 @@ module SignatureVerification
|
||||
return (@signed_request_actor = actor) if signed_request.verified?(actor)
|
||||
|
||||
fail_with! "Verification failed for #{actor.to_log_human_identifier} #{actor.uri}"
|
||||
rescue Mastodon::MalformedHeaderError => e
|
||||
@signature_verification_failure_code = 400
|
||||
fail_with! e.message
|
||||
rescue Mastodon::SignatureVerificationError => e
|
||||
fail_with! e.message
|
||||
rescue *Mastodon::HTTP_CONNECTION_ERRORS => e
|
||||
@signature_verification_failure_code ||= 503
|
||||
fail_with! "Failed to fetch remote data: #{e.message}"
|
||||
rescue Mastodon::UnexpectedResponseError
|
||||
@signature_verification_failure_code ||= 503
|
||||
fail_with! 'Failed to fetch remote data (got unexpected reply from server)'
|
||||
rescue Stoplight::Error::RedLight
|
||||
@signature_verification_failure_code ||= 503
|
||||
fail_with! 'Fetching attempt skipped because of recent connection failure'
|
||||
end
|
||||
|
||||
|
||||
@@ -50,6 +50,13 @@ module WebAppControllerConcern
|
||||
return unless current_user&.require_tos_interstitial?
|
||||
|
||||
@terms_of_service = TermsOfService.published.first
|
||||
|
||||
# Handle case where terms of service have been removed from the database
|
||||
if @terms_of_service.nil?
|
||||
current_user.update(require_tos_interstitial: false)
|
||||
return
|
||||
end
|
||||
|
||||
render 'terms_of_service_interstitial/show', layout: 'auth'
|
||||
end
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ class MediaController < ApplicationController
|
||||
|
||||
def verify_permitted_status!
|
||||
authorize @media_attachment.status, :show?
|
||||
rescue Mastodon::NotPermittedError
|
||||
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||
not_found
|
||||
end
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ class SeveredRelationshipsController < ApplicationController
|
||||
private
|
||||
|
||||
def set_event
|
||||
@event = AccountRelationshipSeveranceEvent.find(params[:id])
|
||||
@event = AccountRelationshipSeveranceEvent.where(account: current_account).find(params[:id])
|
||||
end
|
||||
|
||||
def following_data
|
||||
|
||||
@@ -59,7 +59,7 @@ class StatusesController < ApplicationController
|
||||
def set_status
|
||||
@status = @account.statuses.find(params[:id])
|
||||
authorize @status, :show?
|
||||
rescue Mastodon::NotPermittedError
|
||||
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||
not_found
|
||||
end
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ module ApplicationHelper
|
||||
|
||||
def provider_sign_in_link(provider)
|
||||
label = Devise.omniauth_configs[provider]&.strategy&.display_name.presence || I18n.t("auth.providers.#{provider}", default: provider.to_s.chomp('_oauth2').capitalize)
|
||||
link_to label, omniauth_authorize_path(:user, provider), class: "button button-#{provider}", method: :post
|
||||
link_to label, omniauth_authorize_path(:user, provider), class: "btn button-#{provider}", method: :post
|
||||
end
|
||||
|
||||
def locale_direction
|
||||
@@ -102,6 +102,16 @@ module ApplicationHelper
|
||||
policy(record).public_send(:"#{action}?")
|
||||
end
|
||||
|
||||
def conditional_link_to(condition, name, options = {}, html_options = {}, &block)
|
||||
if condition && !current_page?(block_given? ? name : options)
|
||||
link_to(name, options, html_options, &block)
|
||||
elsif block_given?
|
||||
content_tag(:span, options, html_options, &block)
|
||||
else
|
||||
content_tag(:span, name, html_options)
|
||||
end
|
||||
end
|
||||
|
||||
def material_symbol(icon, attributes = {})
|
||||
safe_join(
|
||||
[
|
||||
|
||||
@@ -27,6 +27,12 @@ module ContextHelper
|
||||
suspended: { 'toot' => 'http://joinmastodon.org/ns#', 'suspended' => 'toot:suspended' },
|
||||
attribution_domains: { 'toot' => 'http://joinmastodon.org/ns#', 'attributionDomains' => { '@id' => 'toot:attributionDomains', '@type' => '@id' } },
|
||||
quote_requests: { 'QuoteRequest' => 'https://w3id.org/fep/044f#QuoteRequest' },
|
||||
quotes: {
|
||||
'quote' => 'https://w3id.org/fep/044f#quote',
|
||||
'quoteUri' => 'http://fedibird.com/ns#quoteUri',
|
||||
'_misskey_quote' => 'https://misskey-hub.net/ns#_misskey_quote',
|
||||
'quoteAuthorization' => { '@id' => 'https://w3id.org/fep/044f#quoteAuthorization', '@type' => '@id' },
|
||||
},
|
||||
interaction_policies: {
|
||||
'gts' => 'https://gotosocial.org/ns#',
|
||||
'interactionPolicy' => { '@id' => 'gts:interactionPolicy', '@type' => '@id' },
|
||||
|
||||
@@ -179,15 +179,25 @@ function loaded() {
|
||||
({ target }) => {
|
||||
if (!(target instanceof HTMLInputElement)) return;
|
||||
|
||||
if (target.value && target.value.length > 0) {
|
||||
const checkedUsername = target.value;
|
||||
if (checkedUsername && checkedUsername.length > 0) {
|
||||
axios
|
||||
.get('/api/v1/accounts/lookup', { params: { acct: target.value } })
|
||||
.get('/api/v1/accounts/lookup', {
|
||||
params: { acct: checkedUsername },
|
||||
})
|
||||
.then(() => {
|
||||
target.setCustomValidity(formatMessage(messages.usernameTaken));
|
||||
// Only update the validity if the result is for the currently-typed username
|
||||
if (checkedUsername === target.value) {
|
||||
target.setCustomValidity(formatMessage(messages.usernameTaken));
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
.catch(() => {
|
||||
target.setCustomValidity('');
|
||||
// Only update the validity if the result is for the currently-typed username
|
||||
if (checkedUsername === target.value) {
|
||||
target.setCustomValidity('');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
target.setCustomValidity('');
|
||||
|
||||
@@ -101,12 +101,17 @@ export const ensureComposeIsVisible = (getState) => {
|
||||
};
|
||||
|
||||
export function setComposeToStatus(status, text, spoiler_text, content_type) {
|
||||
return{
|
||||
type: COMPOSE_SET_STATUS,
|
||||
status,
|
||||
text,
|
||||
spoiler_text,
|
||||
content_type,
|
||||
return (dispatch, getState) => {
|
||||
const maxOptions = getState().server.getIn(['server', 'configuration', 'polls', 'max_options']);
|
||||
|
||||
dispatch({
|
||||
type: COMPOSE_SET_STATUS,
|
||||
status,
|
||||
text,
|
||||
spoiler_text,
|
||||
content_type,
|
||||
maxOptions
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -193,8 +198,9 @@ export function directCompose(account) {
|
||||
|
||||
/**
|
||||
* @param {null | string} overridePrivacy
|
||||
* @param {undefined | Function} successCallback
|
||||
*/
|
||||
export function submitCompose(overridePrivacy = null) {
|
||||
export function submitCompose(overridePrivacy = null, successCallback = undefined) {
|
||||
return function (dispatch, getState) {
|
||||
let status = getState().getIn(['compose', 'text'], '');
|
||||
const media = getState().getIn(['compose', 'media_attachments']);
|
||||
@@ -259,6 +265,9 @@ export function submitCompose(overridePrivacy = null) {
|
||||
|
||||
dispatch(insertIntoTagHistory(response.data.tags, status));
|
||||
dispatch(submitComposeSuccess({ ...response.data }));
|
||||
if (typeof successCallback === 'function') {
|
||||
successCallback(response.data);
|
||||
}
|
||||
|
||||
// To make the app more responsive, immediately push the status
|
||||
// into the columns
|
||||
|
||||
@@ -21,6 +21,15 @@ export function normalizeFilterResult(result) {
|
||||
return normalResult;
|
||||
}
|
||||
|
||||
function stripQuoteFallback(text) {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.innerHTML = text;
|
||||
|
||||
wrapper.querySelector('.quote-inline')?.remove();
|
||||
|
||||
return wrapper.innerHTML;
|
||||
}
|
||||
|
||||
export function normalizeStatus(status, normalOldStatus, settings) {
|
||||
const normalStatus = { ...status };
|
||||
|
||||
@@ -78,6 +87,11 @@ export function normalizeStatus(status, normalOldStatus, settings) {
|
||||
normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(spoilerText), emojiMap);
|
||||
normalStatus.hidden = (spoilerText.length > 0 || normalStatus.sensitive) && autoHideCW(settings, spoilerText);
|
||||
|
||||
// Remove quote fallback link from the DOM so it doesn't mess with paragraph margins
|
||||
if (normalStatus.quote) {
|
||||
normalStatus.contentHtml = stripQuoteFallback(normalStatus.contentHtml);
|
||||
}
|
||||
|
||||
if (normalStatus.url && !(normalStatus.url.startsWith('http://') || normalStatus.url.startsWith('https://'))) {
|
||||
normalStatus.url = null;
|
||||
}
|
||||
@@ -117,6 +131,11 @@ export function normalizeStatusTranslation(translation, status) {
|
||||
spoiler_text: translation.spoiler_text,
|
||||
};
|
||||
|
||||
// Remove quote fallback link from the DOM so it doesn't mess with paragraph margins
|
||||
if (status.get('quote')) {
|
||||
normalTranslation.contentHtml = stripQuoteFallback(normalTranslation.contentHtml);
|
||||
}
|
||||
|
||||
return normalTranslation;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { browserHistory } from 'flavours/glitch/components/router';
|
||||
import api from '../api';
|
||||
|
||||
import { ensureComposeIsVisible, setComposeToStatus } from './compose';
|
||||
import { importFetchedStatus, importFetchedStatuses, importFetchedAccount } from './importer';
|
||||
import { importFetchedStatus, importFetchedAccount } from './importer';
|
||||
import { fetchContext } from './statuses_typed';
|
||||
import { deleteFromTimelines } from './timelines';
|
||||
|
||||
@@ -48,7 +48,18 @@ export function fetchStatusRequest(id, skipLoading) {
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchStatus(id, forceFetch = false, alsoFetchContext = true) {
|
||||
/**
|
||||
* @param {string} id
|
||||
* @param {Object} [options]
|
||||
* @param {boolean} [options.forceFetch]
|
||||
* @param {boolean} [options.alsoFetchContext]
|
||||
* @param {string | null | undefined} [options.parentQuotePostId]
|
||||
*/
|
||||
export function fetchStatus(id, {
|
||||
forceFetch = false,
|
||||
alsoFetchContext = true,
|
||||
parentQuotePostId,
|
||||
} = {}) {
|
||||
return (dispatch, getState) => {
|
||||
const skipLoading = !forceFetch && getState().getIn(['statuses', id], null) !== null;
|
||||
|
||||
@@ -66,7 +77,9 @@ export function fetchStatus(id, forceFetch = false, alsoFetchContext = true) {
|
||||
dispatch(importFetchedStatus(response.data));
|
||||
dispatch(fetchStatusSuccess(skipLoading));
|
||||
}).catch(error => {
|
||||
dispatch(fetchStatusFail(id, error, skipLoading));
|
||||
dispatch(fetchStatusFail(id, error, skipLoading, parentQuotePostId));
|
||||
if (error.status === 404)
|
||||
dispatch(deleteFromTimelines(id));
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -78,22 +91,28 @@ export function fetchStatusSuccess(skipLoading) {
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchStatusFail(id, error, skipLoading) {
|
||||
export function fetchStatusFail(id, error, skipLoading, parentQuotePostId) {
|
||||
return {
|
||||
type: STATUS_FETCH_FAIL,
|
||||
id,
|
||||
error,
|
||||
parentQuotePostId,
|
||||
skipLoading,
|
||||
skipAlert: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function redraft(status, raw_text, content_type) {
|
||||
return {
|
||||
type: REDRAFT,
|
||||
status,
|
||||
raw_text,
|
||||
content_type,
|
||||
return (dispatch, getState) => {
|
||||
const maxOptions = getState().server.getIn(['server', 'configuration', 'polls', 'max_options']);
|
||||
|
||||
dispatch({
|
||||
type: REDRAFT,
|
||||
status,
|
||||
raw_text,
|
||||
content_type,
|
||||
maxOptions,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { apiRequestPost } from 'flavours/glitch/api';
|
||||
import type { Status, StatusVisibility } from 'flavours/glitch/models/status';
|
||||
import type { ApiStatusJSON } from 'flavours/glitch/api_types/statuses';
|
||||
import type { StatusVisibility } from 'flavours/glitch/models/status';
|
||||
|
||||
export const apiReblog = (statusId: string, visibility: StatusVisibility) =>
|
||||
apiRequestPost<{ reblog: Status }>(`v1/statuses/${statusId}/reblog`, {
|
||||
apiRequestPost<{ reblog: ApiStatusJSON }>(`v1/statuses/${statusId}/reblog`, {
|
||||
visibility,
|
||||
});
|
||||
|
||||
export const apiUnreblog = (statusId: string) =>
|
||||
apiRequestPost<Status>(`v1/statuses/${statusId}/unreblog`);
|
||||
apiRequestPost<ApiStatusJSON>(`v1/statuses/${statusId}/unreblog`);
|
||||
|
||||
@@ -1,12 +1,30 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { useLinks } from 'flavours/glitch/hooks/useLinks';
|
||||
|
||||
export const AccountBio: React.FC<{
|
||||
interface AccountBioProps {
|
||||
note: string;
|
||||
className: string;
|
||||
}> = ({ note, className }) => {
|
||||
const handleClick = useLinks();
|
||||
dropdownAccountId?: string;
|
||||
}
|
||||
|
||||
if (note.length === 0 || note === '<p></p>') {
|
||||
export const AccountBio: React.FC<AccountBioProps> = ({
|
||||
note,
|
||||
className,
|
||||
dropdownAccountId,
|
||||
}) => {
|
||||
const handleClick = useLinks(!!dropdownAccountId);
|
||||
const handleNodeChange = useCallback(
|
||||
(node: HTMLDivElement | null) => {
|
||||
if (!dropdownAccountId || !node || node.childNodes.length === 0) {
|
||||
return;
|
||||
}
|
||||
addDropdownToHashtags(node, dropdownAccountId);
|
||||
},
|
||||
[dropdownAccountId],
|
||||
);
|
||||
|
||||
if (note.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -15,6 +33,28 @@ export const AccountBio: React.FC<{
|
||||
className={`${className} translate`}
|
||||
dangerouslySetInnerHTML={{ __html: note }}
|
||||
onClickCapture={handleClick}
|
||||
ref={handleNodeChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
function addDropdownToHashtags(node: HTMLElement | null, accountId: string) {
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
for (const childNode of node.childNodes) {
|
||||
if (!(childNode instanceof HTMLElement)) {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
childNode instanceof HTMLAnchorElement &&
|
||||
(childNode.classList.contains('hashtag') ||
|
||||
childNode.innerText.startsWith('#')) &&
|
||||
!childNode.dataset.menuHashtag
|
||||
) {
|
||||
childNode.dataset.menuHashtag = accountId;
|
||||
} else if (childNode.childNodes.length > 0) {
|
||||
addDropdownToHashtags(childNode, accountId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
useCallback,
|
||||
cloneElement,
|
||||
Children,
|
||||
useId,
|
||||
} from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
@@ -16,6 +17,7 @@ import Overlay from 'react-overlays/Overlay';
|
||||
import type {
|
||||
OffsetValue,
|
||||
UsePopperOptions,
|
||||
Placement,
|
||||
} from 'react-overlays/esm/usePopper';
|
||||
|
||||
import { fetchRelationships } from 'flavours/glitch/actions/accounts';
|
||||
@@ -295,6 +297,11 @@ interface DropdownProps<Item = MenuItem> {
|
||||
title?: string;
|
||||
disabled?: boolean;
|
||||
scrollable?: boolean;
|
||||
placement?: Placement;
|
||||
/**
|
||||
* Prevent the `ScrollableList` with this scrollKey
|
||||
* from being scrolled while the dropdown is open
|
||||
*/
|
||||
scrollKey?: string;
|
||||
status?: ImmutableMap<string, unknown>;
|
||||
forceDropdown?: boolean;
|
||||
@@ -316,6 +323,7 @@ export const Dropdown = <Item = MenuItem,>({
|
||||
title = 'Menu',
|
||||
disabled,
|
||||
scrollable,
|
||||
placement = 'bottom',
|
||||
status,
|
||||
forceDropdown = false,
|
||||
renderItem,
|
||||
@@ -331,16 +339,15 @@ export const Dropdown = <Item = MenuItem,>({
|
||||
);
|
||||
const [currentId] = useState(id++);
|
||||
const open = currentId === openDropdownId;
|
||||
const activeElement = useRef<HTMLElement | null>(null);
|
||||
const targetRef = useRef<HTMLButtonElement | null>(null);
|
||||
const buttonRef = useRef<HTMLButtonElement | null>(null);
|
||||
const menuId = useId();
|
||||
const prefetchAccountId = status
|
||||
? status.getIn(['account', 'id'])
|
||||
: undefined;
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
if (activeElement.current) {
|
||||
activeElement.current.focus({ preventScroll: true });
|
||||
activeElement.current = null;
|
||||
if (buttonRef.current) {
|
||||
buttonRef.current.focus({ preventScroll: true });
|
||||
}
|
||||
|
||||
dispatch(
|
||||
@@ -375,7 +382,7 @@ export const Dropdown = <Item = MenuItem,>({
|
||||
[handleClose, onItemClick, items],
|
||||
);
|
||||
|
||||
const handleClick = useCallback(
|
||||
const toggleDropdown = useCallback(
|
||||
(e: React.MouseEvent | React.KeyboardEvent) => {
|
||||
const { type } = e;
|
||||
|
||||
@@ -423,38 +430,6 @@ export const Dropdown = <Item = MenuItem,>({
|
||||
],
|
||||
);
|
||||
|
||||
const handleMouseDown = useCallback(() => {
|
||||
if (!open && document.activeElement instanceof HTMLElement) {
|
||||
activeElement.current = document.activeElement;
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
const handleButtonKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent) => {
|
||||
switch (e.key) {
|
||||
case ' ':
|
||||
case 'Enter':
|
||||
handleMouseDown();
|
||||
break;
|
||||
}
|
||||
},
|
||||
[handleMouseDown],
|
||||
);
|
||||
|
||||
const handleKeyPress = useCallback(
|
||||
(e: React.KeyboardEvent) => {
|
||||
switch (e.key) {
|
||||
case ' ':
|
||||
case 'Enter':
|
||||
handleClick(e);
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
break;
|
||||
}
|
||||
},
|
||||
[handleClick],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (currentId === openDropdownId) {
|
||||
@@ -465,14 +440,16 @@ export const Dropdown = <Item = MenuItem,>({
|
||||
|
||||
let button: React.ReactElement;
|
||||
|
||||
const buttonProps = {
|
||||
disabled,
|
||||
onClick: toggleDropdown,
|
||||
'aria-expanded': open,
|
||||
'aria-controls': menuId,
|
||||
ref: buttonRef,
|
||||
};
|
||||
|
||||
if (children) {
|
||||
button = cloneElement(Children.only(children), {
|
||||
onClick: handleClick,
|
||||
onMouseDown: handleMouseDown,
|
||||
onKeyDown: handleButtonKeyDown,
|
||||
onKeyPress: handleKeyPress,
|
||||
ref: targetRef,
|
||||
});
|
||||
button = cloneElement(Children.only(children), buttonProps);
|
||||
} else if (icon && iconComponent) {
|
||||
button = (
|
||||
<IconButton
|
||||
@@ -480,12 +457,7 @@ export const Dropdown = <Item = MenuItem,>({
|
||||
iconComponent={iconComponent}
|
||||
title={title}
|
||||
active={open}
|
||||
disabled={disabled}
|
||||
onClick={handleClick}
|
||||
onMouseDown={handleMouseDown}
|
||||
onKeyDown={handleButtonKeyDown}
|
||||
onKeyPress={handleKeyPress}
|
||||
ref={targetRef}
|
||||
{...buttonProps}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
@@ -499,13 +471,13 @@ export const Dropdown = <Item = MenuItem,>({
|
||||
<Overlay
|
||||
show={open}
|
||||
offset={offset}
|
||||
placement='bottom'
|
||||
placement={placement}
|
||||
flip
|
||||
target={targetRef}
|
||||
target={buttonRef}
|
||||
popperConfig={popperConfig}
|
||||
>
|
||||
{({ props, arrowProps, placement }) => (
|
||||
<div {...props}>
|
||||
<div {...props} id={menuId}>
|
||||
<div className={`dropdown-animation dropdown-menu ${placement}`}>
|
||||
<div
|
||||
className={`dropdown-menu__arrow ${placement}`}
|
||||
|
||||
@@ -20,7 +20,7 @@ import { useDrag } from '@use-gesture/react';
|
||||
import { expandAccountFeaturedTimeline } from '@/flavours/glitch/actions/timelines';
|
||||
import { Icon } from '@/flavours/glitch/components/icon';
|
||||
import { IconButton } from '@/flavours/glitch/components/icon_button';
|
||||
import StatusContainer from '@/flavours/glitch/containers/status_container';
|
||||
import { StatusQuoteManager } from '@/flavours/glitch/components/status_quoted';
|
||||
import { usePrevious } from '@/flavours/glitch/hooks/usePrevious';
|
||||
import { useAppDispatch, useAppSelector } from '@/flavours/glitch/store';
|
||||
import ChevronLeftIcon from '@/material-icons/400-24px/chevron_left.svg?react';
|
||||
@@ -218,12 +218,7 @@ const FeaturedCarouselItem: React.FC<
|
||||
ref={handleRef}
|
||||
{...props}
|
||||
>
|
||||
<StatusContainer
|
||||
// @ts-expect-error inferred props are wrong
|
||||
id={statusId}
|
||||
contextType='account'
|
||||
withCounters
|
||||
/>
|
||||
<StatusQuoteManager id={statusId} contextType='account' withCounters />
|
||||
</animated.div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -14,6 +14,10 @@ import { useTimeout } from 'flavours/glitch/hooks/useTimeout';
|
||||
const offset = [-12, 4] as OffsetValue;
|
||||
const enterDelay = 750;
|
||||
const leaveDelay = 150;
|
||||
// Only open the card if the mouse was moved within this time,
|
||||
// to avoid triggering the card without intentional mouse movement
|
||||
// (e.g. when content changed underneath the mouse cursor)
|
||||
const activeMovementThreshold = 150;
|
||||
const popperConfig = { strategy: 'fixed' } as UsePopperOptions;
|
||||
|
||||
const isHoverCardAnchor = (element: HTMLElement) =>
|
||||
@@ -26,6 +30,7 @@ export const HoverCardController: React.FC = () => {
|
||||
const cardRef = useRef<HTMLDivElement | null>(null);
|
||||
const [setLeaveTimeout, cancelLeaveTimeout] = useTimeout();
|
||||
const [setEnterTimeout, cancelEnterTimeout, delayEnterTimeout] = useTimeout();
|
||||
const [setMoveTimeout, cancelMoveTimeout] = useTimeout();
|
||||
const [setScrollTimeout] = useTimeout();
|
||||
const location = useLocation();
|
||||
|
||||
@@ -42,6 +47,8 @@ export const HoverCardController: React.FC = () => {
|
||||
|
||||
useEffect(() => {
|
||||
let isScrolling = false;
|
||||
let isUsingTouch = false;
|
||||
let isActiveMouseMovement = false;
|
||||
let currentAnchor: HTMLElement | null = null;
|
||||
let currentTitle: string | null = null;
|
||||
|
||||
@@ -60,6 +67,12 @@ export const HoverCardController: React.FC = () => {
|
||||
setAccountId(undefined);
|
||||
};
|
||||
|
||||
const handleTouchStart = () => {
|
||||
// Keeping track of touch events to prevent the
|
||||
// hover card from being displayed on touch devices
|
||||
isUsingTouch = true;
|
||||
};
|
||||
|
||||
const handleMouseEnter = (e: MouseEvent) => {
|
||||
const { target } = e;
|
||||
|
||||
@@ -69,8 +82,14 @@ export const HoverCardController: React.FC = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Bail out if we're scrolling, a touch is active,
|
||||
// or if there was no active mouse movement
|
||||
if (isScrolling || !isActiveMouseMovement || isUsingTouch) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We've entered an anchor
|
||||
if (!isScrolling && isHoverCardAnchor(target)) {
|
||||
if (isHoverCardAnchor(target)) {
|
||||
cancelLeaveTimeout();
|
||||
|
||||
currentAnchor?.removeAttribute('aria-describedby');
|
||||
@@ -85,10 +104,7 @@ export const HoverCardController: React.FC = () => {
|
||||
}
|
||||
|
||||
// We've entered the hover card
|
||||
if (
|
||||
!isScrolling &&
|
||||
(target === currentAnchor || target === cardRef.current)
|
||||
) {
|
||||
if (target === currentAnchor || target === cardRef.current) {
|
||||
cancelLeaveTimeout();
|
||||
}
|
||||
};
|
||||
@@ -127,9 +143,23 @@ export const HoverCardController: React.FC = () => {
|
||||
};
|
||||
|
||||
const handleMouseMove = () => {
|
||||
if (isUsingTouch) {
|
||||
isUsingTouch = false;
|
||||
}
|
||||
|
||||
delayEnterTimeout(enterDelay);
|
||||
|
||||
cancelMoveTimeout();
|
||||
isActiveMouseMovement = true;
|
||||
setMoveTimeout(() => {
|
||||
isActiveMouseMovement = false;
|
||||
}, activeMovementThreshold);
|
||||
};
|
||||
|
||||
document.body.addEventListener('touchstart', handleTouchStart, {
|
||||
passive: true,
|
||||
});
|
||||
|
||||
document.body.addEventListener('mouseenter', handleMouseEnter, {
|
||||
passive: true,
|
||||
capture: true,
|
||||
@@ -151,6 +181,7 @@ export const HoverCardController: React.FC = () => {
|
||||
});
|
||||
|
||||
return () => {
|
||||
document.body.removeEventListener('touchstart', handleTouchStart);
|
||||
document.body.removeEventListener('mouseenter', handleMouseEnter);
|
||||
document.body.removeEventListener('mousemove', handleMouseMove);
|
||||
document.body.removeEventListener('mouseleave', handleMouseLeave);
|
||||
@@ -166,6 +197,8 @@ export const HoverCardController: React.FC = () => {
|
||||
setOpen,
|
||||
setAccountId,
|
||||
setAnchor,
|
||||
setMoveTimeout,
|
||||
cancelMoveTimeout,
|
||||
]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -14,7 +14,6 @@ interface Props {
|
||||
onClick?: React.MouseEventHandler<HTMLButtonElement>;
|
||||
onMouseDown?: React.MouseEventHandler<HTMLButtonElement>;
|
||||
onKeyDown?: React.KeyboardEventHandler<HTMLButtonElement>;
|
||||
onKeyPress?: React.KeyboardEventHandler<HTMLButtonElement>;
|
||||
active?: boolean;
|
||||
expanded?: boolean;
|
||||
style?: React.CSSProperties;
|
||||
@@ -47,7 +46,6 @@ export const IconButton = forwardRef<HTMLButtonElement, Props>(
|
||||
activeStyle,
|
||||
onClick,
|
||||
onKeyDown,
|
||||
onKeyPress,
|
||||
onMouseDown,
|
||||
active = false,
|
||||
disabled = false,
|
||||
@@ -89,16 +87,6 @@ export const IconButton = forwardRef<HTMLButtonElement, Props>(
|
||||
[disabled, onClick],
|
||||
);
|
||||
|
||||
const handleKeyPress: React.KeyboardEventHandler<HTMLButtonElement> =
|
||||
useCallback(
|
||||
(e) => {
|
||||
if (!disabled) {
|
||||
onKeyPress?.(e);
|
||||
}
|
||||
},
|
||||
[disabled, onKeyPress],
|
||||
);
|
||||
|
||||
const handleMouseDown: React.MouseEventHandler<HTMLButtonElement> =
|
||||
useCallback(
|
||||
(e) => {
|
||||
@@ -166,7 +154,6 @@ export const IconButton = forwardRef<HTMLButtonElement, Props>(
|
||||
onClick={handleClick}
|
||||
onMouseDown={handleMouseDown}
|
||||
onKeyDown={handleKeyDown}
|
||||
onKeyPress={handleKeyPress} // eslint-disable-line @typescript-eslint/no-deprecated
|
||||
style={buttonStyle}
|
||||
tabIndex={tabIndex}
|
||||
disabled={disabled}
|
||||
|
||||
@@ -190,7 +190,7 @@ class StatusContent extends PureComponent {
|
||||
link.classList.add('unhandled-link');
|
||||
|
||||
link.setAttribute('target', '_blank');
|
||||
link.setAttribute('rel', 'noopener nofollow noreferrer');
|
||||
link.setAttribute('rel', 'noopener nofollow');
|
||||
|
||||
try {
|
||||
if (tagLinks && isLinkMisleading(link)) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
@@ -12,12 +12,15 @@ import ArticleIcon from '@/material-icons/400-24px/article.svg?react';
|
||||
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
|
||||
import { Icon } from 'flavours/glitch/components/icon';
|
||||
import StatusContainer from 'flavours/glitch/containers/status_container';
|
||||
import { domain } from 'flavours/glitch/initial_state';
|
||||
import type { Status } from 'flavours/glitch/models/status';
|
||||
import type { RootState } from 'flavours/glitch/store';
|
||||
import { useAppDispatch, useAppSelector } from 'flavours/glitch/store';
|
||||
|
||||
import { revealAccount } from '../actions/accounts_typed';
|
||||
import { fetchStatus } from '../actions/statuses';
|
||||
import { makeGetStatus } from '../selectors';
|
||||
import { getAccountHidden } from '../selectors/accounts';
|
||||
|
||||
const MAX_QUOTE_POSTS_NESTING_LEVEL = 1;
|
||||
|
||||
@@ -37,9 +40,7 @@ const QuoteWrapper: React.FC<{
|
||||
);
|
||||
};
|
||||
|
||||
const NestedQuoteLink: React.FC<{
|
||||
status: Status;
|
||||
}> = ({ status }) => {
|
||||
const NestedQuoteLink: React.FC<{ status: Status }> = ({ status }) => {
|
||||
const accountId = status.get('account') as string;
|
||||
const account = useAppSelector((state) =>
|
||||
accountId ? state.accounts.get(accountId) : undefined,
|
||||
@@ -75,24 +76,74 @@ type GetStatusSelector = (
|
||||
props: { id?: string | null; contextType?: string },
|
||||
) => Status | null;
|
||||
|
||||
const LimitedAccountHint: React.FC<{ accountId: string }> = ({ accountId }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const reveal = useCallback(() => {
|
||||
dispatch(revealAccount({ id: accountId }));
|
||||
}, [dispatch, accountId]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormattedMessage
|
||||
id='status.quote_error.limited_account_hint.title'
|
||||
defaultMessage='This account has been hidden by the moderators of {domain}.'
|
||||
values={{ domain }}
|
||||
/>
|
||||
<button onClick={reveal} className='link-button'>
|
||||
<FormattedMessage
|
||||
id='status.quote_error.limited_account_hint.action'
|
||||
defaultMessage='Show anyway'
|
||||
/>
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const QuotedStatus: React.FC<{
|
||||
quote: QuoteMap;
|
||||
contextType?: string;
|
||||
parentQuotePostId?: string | null;
|
||||
variant?: 'full' | 'link';
|
||||
nestingLevel?: number;
|
||||
}> = ({ quote, contextType, nestingLevel = 1, variant = 'full' }) => {
|
||||
}> = ({
|
||||
quote,
|
||||
contextType,
|
||||
parentQuotePostId,
|
||||
nestingLevel = 1,
|
||||
variant = 'full',
|
||||
}) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const quoteState = useAppSelector((state) =>
|
||||
parentQuotePostId
|
||||
? state.statuses.getIn([parentQuotePostId, 'quote', 'state'])
|
||||
: quote.get('state'),
|
||||
);
|
||||
|
||||
const quotedStatusId = quote.get('quoted_status');
|
||||
const quoteState = quote.get('state');
|
||||
const status = useAppSelector((state) =>
|
||||
quotedStatusId ? state.statuses.get(quotedStatusId) : undefined,
|
||||
);
|
||||
|
||||
const shouldLoadQuote = !status?.get('isLoading') && quoteState !== 'deleted';
|
||||
|
||||
const accountId: string | null = status?.get('account', null) as
|
||||
| string
|
||||
| null;
|
||||
|
||||
const hiddenAccount = useAppSelector(
|
||||
(state) => accountId && getAccountHidden(state, accountId),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!status && quotedStatusId) {
|
||||
dispatch(fetchStatus(quotedStatusId));
|
||||
if (shouldLoadQuote && quotedStatusId) {
|
||||
dispatch(
|
||||
fetchStatus(quotedStatusId, {
|
||||
parentQuotePostId,
|
||||
alsoFetchContext: false,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}, [status, quotedStatusId, dispatch]);
|
||||
}, [shouldLoadQuote, quotedStatusId, parentQuotePostId, dispatch]);
|
||||
|
||||
// In order to find out whether the quoted post should be completely hidden
|
||||
// due to a matching filter, we run it through the selector used by `status_container`.
|
||||
@@ -147,6 +198,8 @@ export const QuotedStatus: React.FC<{
|
||||
defaultMessage='This post cannot be displayed.'
|
||||
/>
|
||||
);
|
||||
} else if (hiddenAccount && accountId) {
|
||||
quoteError = <LimitedAccountHint accountId={accountId} />;
|
||||
}
|
||||
|
||||
if (quoteError) {
|
||||
@@ -173,6 +226,7 @@ export const QuotedStatus: React.FC<{
|
||||
{canRenderChildQuote && (
|
||||
<QuotedStatus
|
||||
quote={childQuote}
|
||||
parentQuotePostId={quotedStatusId}
|
||||
contextType={contextType}
|
||||
variant={
|
||||
nestingLevel === MAX_QUOTE_POSTS_NESTING_LEVEL ? 'link' : 'full'
|
||||
@@ -209,7 +263,11 @@ export const StatusQuoteManager = (props: StatusQuoteManagerProps) => {
|
||||
return (
|
||||
/* @ts-expect-error Status is not yet typed */
|
||||
<StatusContainer {...props}>
|
||||
<QuotedStatus quote={quote} contextType={props.contextType} />
|
||||
<QuotedStatus
|
||||
quote={quote}
|
||||
parentQuotePostId={status?.get('id') as string}
|
||||
contextType={props.contextType}
|
||||
/>
|
||||
</StatusContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -179,15 +179,25 @@ function loaded() {
|
||||
({ target }) => {
|
||||
if (!(target instanceof HTMLInputElement)) return;
|
||||
|
||||
if (target.value && target.value.length > 0) {
|
||||
const checkedUsername = target.value;
|
||||
if (checkedUsername && checkedUsername.length > 0) {
|
||||
axios
|
||||
.get('/api/v1/accounts/lookup', { params: { acct: target.value } })
|
||||
.get('/api/v1/accounts/lookup', {
|
||||
params: { acct: checkedUsername },
|
||||
})
|
||||
.then(() => {
|
||||
target.setCustomValidity(formatMessage(messages.usernameTaken));
|
||||
// Only update the validity if the result is for the currently-typed username
|
||||
if (checkedUsername === target.value) {
|
||||
target.setCustomValidity(formatMessage(messages.usernameTaken));
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
.catch(() => {
|
||||
target.setCustomValidity('');
|
||||
// Only update the validity if the result is for the currently-typed username
|
||||
if (checkedUsername === target.value) {
|
||||
target.setCustomValidity('');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
target.setCustomValidity('');
|
||||
|
||||
@@ -6,6 +6,7 @@ import classNames from 'classnames';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
|
||||
import { AccountBio } from '@/flavours/glitch/components/account_bio';
|
||||
import CheckIcon from '@/material-icons/400-24px/check.svg?react';
|
||||
import LockIcon from '@/material-icons/400-24px/lock.svg?react';
|
||||
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
|
||||
@@ -777,7 +778,6 @@ export const AccountHeader: React.FC<{
|
||||
);
|
||||
}
|
||||
|
||||
const content = { __html: account.note_emojified };
|
||||
const displayNameHtml = { __html: account.display_name_html };
|
||||
const fields = account.fields;
|
||||
const isLocal = !account.acct.includes('@');
|
||||
@@ -901,12 +901,11 @@ export const AccountHeader: React.FC<{
|
||||
<AccountNote accountId={accountId} />
|
||||
)}
|
||||
|
||||
{account.note.length > 0 && account.note !== '<p></p>' && (
|
||||
<div
|
||||
className='account__header__content translate'
|
||||
dangerouslySetInnerHTML={content}
|
||||
/>
|
||||
)}
|
||||
<AccountBio
|
||||
note={account.note_emojified}
|
||||
dropdownAccountId={accountId}
|
||||
className='account__header__content'
|
||||
/>
|
||||
|
||||
<div className='account__header__fields'>
|
||||
<dl>
|
||||
|
||||
@@ -261,7 +261,9 @@ export const AltTextModal = forwardRef<ModalRef, Props & Partial<RestoreProps>>(
|
||||
);
|
||||
const lang = useAppSelector(
|
||||
(state) =>
|
||||
(state.compose as ImmutableMap<string, unknown>).get('lang') as string,
|
||||
(state.compose as ImmutableMap<string, unknown>).get(
|
||||
'language',
|
||||
) as string,
|
||||
);
|
||||
const focusX =
|
||||
(media?.getIn(['meta', 'focus', 'x'], 0) as number | undefined) ?? 0;
|
||||
|
||||
@@ -81,6 +81,7 @@ class ComposeForm extends ImmutablePureComponent {
|
||||
singleColumn: PropTypes.bool,
|
||||
lang: PropTypes.string,
|
||||
maxChars: PropTypes.number,
|
||||
redirectOnSuccess: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
@@ -336,7 +337,7 @@ class ComposeForm extends ImmutablePureComponent {
|
||||
>
|
||||
{intl.formatMessage(
|
||||
this.props.isEditing ?
|
||||
messages.saveChanges :
|
||||
messages.saveChanges :
|
||||
(this.props.isInReply ? messages.reply : messages.publish)
|
||||
)}
|
||||
</Button>
|
||||
|
||||
@@ -47,10 +47,6 @@ const labelForRecentSearch = (search: RecentSearch) => {
|
||||
}
|
||||
};
|
||||
|
||||
const unfocus = () => {
|
||||
document.querySelector('.ui')?.parentElement?.focus();
|
||||
};
|
||||
|
||||
const ClearButton: React.FC<{
|
||||
onClick: () => void;
|
||||
hasValue: boolean;
|
||||
@@ -107,6 +103,11 @@ export const Search: React.FC<{
|
||||
}, [initialValue]);
|
||||
const searchOptions: SearchOption[] = [];
|
||||
|
||||
const unfocus = useCallback(() => {
|
||||
document.querySelector('.ui')?.parentElement?.focus();
|
||||
setExpanded(false);
|
||||
}, []);
|
||||
|
||||
if (searchEnabled) {
|
||||
searchOptions.push(
|
||||
{
|
||||
@@ -282,7 +283,7 @@ export const Search: React.FC<{
|
||||
history.push({ pathname: '/search', search: queryParams.toString() });
|
||||
unfocus();
|
||||
},
|
||||
[dispatch, history],
|
||||
[dispatch, history, unfocus],
|
||||
);
|
||||
|
||||
const handleChange = useCallback(
|
||||
@@ -402,7 +403,7 @@ export const Search: React.FC<{
|
||||
|
||||
setQuickActions(newQuickActions);
|
||||
},
|
||||
[dispatch, history, signedIn, setValue, setQuickActions, submit],
|
||||
[signedIn, dispatch, unfocus, history, submit],
|
||||
);
|
||||
|
||||
const handleClear = useCallback(() => {
|
||||
@@ -410,7 +411,7 @@ export const Search: React.FC<{
|
||||
setQuickActions([]);
|
||||
setSelectedOption(-1);
|
||||
unfocus();
|
||||
}, [setValue, setQuickActions, setSelectedOption]);
|
||||
}, [unfocus]);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent) => {
|
||||
@@ -461,7 +462,7 @@ export const Search: React.FC<{
|
||||
break;
|
||||
}
|
||||
},
|
||||
[navigableOptions, value, selectedOption, setSelectedOption, submit],
|
||||
[unfocus, navigableOptions, selectedOption, submit, value],
|
||||
);
|
||||
|
||||
const handleFocus = useCallback(() => {
|
||||
@@ -481,12 +482,38 @@ export const Search: React.FC<{
|
||||
}, [setExpanded, setSelectedOption, singleColumn]);
|
||||
|
||||
const handleBlur = useCallback(() => {
|
||||
setExpanded(false);
|
||||
setSelectedOption(-1);
|
||||
}, [setExpanded, setSelectedOption]);
|
||||
}, [setSelectedOption]);
|
||||
|
||||
const formRef = useRef<HTMLFormElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// If the search popover is expanded, close it when tabbing or
|
||||
// clicking outside of it or the search form, while allowing
|
||||
// tabbing or clicking inside of the popover
|
||||
if (expanded) {
|
||||
function closeOnLeave(event: FocusEvent | MouseEvent) {
|
||||
const form = formRef.current;
|
||||
const isClickInsideForm =
|
||||
form &&
|
||||
(form === event.target || form.contains(event.target as Node));
|
||||
if (!isClickInsideForm) {
|
||||
setExpanded(false);
|
||||
}
|
||||
}
|
||||
document.addEventListener('focusin', closeOnLeave);
|
||||
document.addEventListener('click', closeOnLeave);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('focusin', closeOnLeave);
|
||||
document.removeEventListener('click', closeOnLeave);
|
||||
};
|
||||
}
|
||||
return () => null;
|
||||
}, [expanded]);
|
||||
|
||||
return (
|
||||
<form className={classNames('search', { active: expanded })}>
|
||||
<form ref={formRef} className={classNames('search', { active: expanded })}>
|
||||
<input
|
||||
ref={searchInputRef}
|
||||
className='search__input'
|
||||
@@ -506,7 +533,7 @@ export const Search: React.FC<{
|
||||
|
||||
<ClearButton hasValue={hasValue} onClick={handleClear} />
|
||||
|
||||
<div className='search__popout'>
|
||||
<div className='search__popout' tabIndex={-1}>
|
||||
{!hasValue && (
|
||||
<>
|
||||
<h4>
|
||||
|
||||
@@ -56,7 +56,7 @@ const mapStateToProps = state => ({
|
||||
maxChars: state.getIn(['server', 'server', 'configuration', 'statuses', 'max_characters'], 500),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
const mapDispatchToProps = (dispatch, props) => ({
|
||||
|
||||
onChange (text) {
|
||||
dispatch(changeCompose(text));
|
||||
@@ -69,7 +69,11 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
modalProps: { overridePrivacy },
|
||||
}));
|
||||
} else {
|
||||
dispatch(submitCompose(overridePrivacy));
|
||||
dispatch(submitCompose(overridePrivacy, (status) => {
|
||||
if (props.redirectOnSuccess) {
|
||||
window.location.assign(status.url);
|
||||
}
|
||||
}));
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -53,16 +53,22 @@ export const MoreLink: React.FC = () => {
|
||||
|
||||
const menu = useMemo(() => {
|
||||
const arr: MenuItem[] = [
|
||||
{ text: intl.formatMessage(messages.filters), href: '/filters' },
|
||||
{ text: intl.formatMessage(messages.mutes), to: '/mutes' },
|
||||
{ text: intl.formatMessage(messages.blocks), to: '/blocks' },
|
||||
{
|
||||
text: intl.formatMessage(messages.domainBlocks),
|
||||
to: '/domain_blocks',
|
||||
href: '/filters',
|
||||
text: intl.formatMessage(messages.filters),
|
||||
},
|
||||
{
|
||||
to: '/mutes',
|
||||
text: intl.formatMessage(messages.mutes),
|
||||
},
|
||||
{
|
||||
to: '/blocks',
|
||||
text: intl.formatMessage(messages.blocks),
|
||||
},
|
||||
{
|
||||
to: '/domain_blocks',
|
||||
text: intl.formatMessage(messages.domainBlocks),
|
||||
},
|
||||
];
|
||||
|
||||
arr.push(
|
||||
null,
|
||||
{
|
||||
href: '/settings/privacy',
|
||||
@@ -80,7 +86,7 @@ export const MoreLink: React.FC = () => {
|
||||
href: '/settings/export',
|
||||
text: intl.formatMessage(messages.importExport),
|
||||
},
|
||||
);
|
||||
];
|
||||
|
||||
if (canManageReports(permissions)) {
|
||||
arr.push(null, {
|
||||
@@ -109,7 +115,7 @@ export const MoreLink: React.FC = () => {
|
||||
}, [intl, dispatch, permissions]);
|
||||
|
||||
return (
|
||||
<Dropdown items={menu}>
|
||||
<Dropdown items={menu} placement='bottom-start'>
|
||||
<button className='column-link column-link--transparent'>
|
||||
<Icon id='' icon={MoreHorizIcon} className='column-link__icon' />
|
||||
|
||||
|
||||
@@ -466,6 +466,7 @@ export const CollapsibleNavigationPanel: React.FC = () => {
|
||||
filterTaps: true,
|
||||
bounds: isLtrDir ? { left: 0 } : { right: 0 },
|
||||
rubberband: true,
|
||||
enabled: openable,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -122,98 +122,93 @@ export const PolicyControls: React.FC = () => {
|
||||
value={notificationPolicy.for_not_following}
|
||||
onChange={handleFilterNotFollowing}
|
||||
options={options}
|
||||
>
|
||||
<strong>
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='notifications.policy.filter_not_following_title'
|
||||
defaultMessage="People you don't follow"
|
||||
/>
|
||||
</strong>
|
||||
<span className='hint'>
|
||||
}
|
||||
hint={
|
||||
<FormattedMessage
|
||||
id='notifications.policy.filter_not_following_hint'
|
||||
defaultMessage='Until you manually approve them'
|
||||
/>
|
||||
</span>
|
||||
</SelectWithLabel>
|
||||
}
|
||||
/>
|
||||
|
||||
<SelectWithLabel
|
||||
value={notificationPolicy.for_not_followers}
|
||||
onChange={handleFilterNotFollowers}
|
||||
options={options}
|
||||
>
|
||||
<strong>
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='notifications.policy.filter_not_followers_title'
|
||||
defaultMessage='People not following you'
|
||||
/>
|
||||
</strong>
|
||||
<span className='hint'>
|
||||
}
|
||||
hint={
|
||||
<FormattedMessage
|
||||
id='notifications.policy.filter_not_followers_hint'
|
||||
defaultMessage='Including people who have been following you fewer than {days, plural, one {one day} other {# days}}'
|
||||
values={{ days: 3 }}
|
||||
/>
|
||||
</span>
|
||||
</SelectWithLabel>
|
||||
}
|
||||
/>
|
||||
|
||||
<SelectWithLabel
|
||||
value={notificationPolicy.for_new_accounts}
|
||||
onChange={handleFilterNewAccounts}
|
||||
options={options}
|
||||
>
|
||||
<strong>
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='notifications.policy.filter_new_accounts_title'
|
||||
defaultMessage='New accounts'
|
||||
/>
|
||||
</strong>
|
||||
<span className='hint'>
|
||||
}
|
||||
hint={
|
||||
<FormattedMessage
|
||||
id='notifications.policy.filter_new_accounts.hint'
|
||||
defaultMessage='Created within the past {days, plural, one {one day} other {# days}}'
|
||||
values={{ days: 30 }}
|
||||
/>
|
||||
</span>
|
||||
</SelectWithLabel>
|
||||
}
|
||||
/>
|
||||
|
||||
<SelectWithLabel
|
||||
value={notificationPolicy.for_private_mentions}
|
||||
onChange={handleFilterPrivateMentions}
|
||||
options={options}
|
||||
>
|
||||
<strong>
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='notifications.policy.filter_private_mentions_title'
|
||||
defaultMessage='Unsolicited private mentions'
|
||||
/>
|
||||
</strong>
|
||||
<span className='hint'>
|
||||
}
|
||||
hint={
|
||||
<FormattedMessage
|
||||
id='notifications.policy.filter_private_mentions_hint'
|
||||
defaultMessage="Filtered unless it's in reply to your own mention or if you follow the sender"
|
||||
/>
|
||||
</span>
|
||||
</SelectWithLabel>
|
||||
}
|
||||
/>
|
||||
|
||||
<SelectWithLabel
|
||||
value={notificationPolicy.for_limited_accounts}
|
||||
onChange={handleFilterLimitedAccounts}
|
||||
options={options}
|
||||
>
|
||||
<strong>
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='notifications.policy.filter_limited_accounts_title'
|
||||
defaultMessage='Moderated accounts'
|
||||
/>
|
||||
</strong>
|
||||
<span className='hint'>
|
||||
}
|
||||
hint={
|
||||
<FormattedMessage
|
||||
id='notifications.policy.filter_limited_accounts_hint'
|
||||
defaultMessage='Limited by server moderators'
|
||||
/>
|
||||
</span>
|
||||
</SelectWithLabel>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { useCallback, useState, useRef } from 'react';
|
||||
import { useCallback, useState, useRef, useId } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
@@ -16,6 +16,8 @@ interface DropdownProps {
|
||||
options: SelectItem[];
|
||||
disabled?: boolean;
|
||||
onChange: (value: string) => void;
|
||||
'aria-labelledby': string;
|
||||
'aria-describedby'?: string;
|
||||
placement?: Placement;
|
||||
}
|
||||
|
||||
@@ -24,51 +26,33 @@ const Dropdown: React.FC<DropdownProps> = ({
|
||||
options,
|
||||
disabled,
|
||||
onChange,
|
||||
'aria-labelledby': ariaLabelledBy,
|
||||
'aria-describedby': ariaDescribedBy,
|
||||
placement: initialPlacement = 'bottom-end',
|
||||
}) => {
|
||||
const activeElementRef = useRef<Element | null>(null);
|
||||
const containerRef = useRef(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
const [isOpen, setOpen] = useState<boolean>(false);
|
||||
const [placement, setPlacement] = useState<Placement>(initialPlacement);
|
||||
|
||||
const handleToggle = useCallback(() => {
|
||||
if (
|
||||
isOpen &&
|
||||
activeElementRef.current &&
|
||||
activeElementRef.current instanceof HTMLElement
|
||||
) {
|
||||
activeElementRef.current.focus({ preventScroll: true });
|
||||
}
|
||||
|
||||
setOpen(!isOpen);
|
||||
}, [isOpen, setOpen]);
|
||||
|
||||
const handleMouseDown = useCallback(() => {
|
||||
if (!isOpen) activeElementRef.current = document.activeElement;
|
||||
}, [isOpen]);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent) => {
|
||||
switch (e.key) {
|
||||
case ' ':
|
||||
case 'Enter':
|
||||
if (!isOpen) activeElementRef.current = document.activeElement;
|
||||
break;
|
||||
}
|
||||
},
|
||||
[isOpen],
|
||||
);
|
||||
const uniqueId = useId();
|
||||
const menuId = `${uniqueId}-menu`;
|
||||
const buttonLabelId = `${uniqueId}-button`;
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
if (
|
||||
isOpen &&
|
||||
activeElementRef.current &&
|
||||
activeElementRef.current instanceof HTMLElement
|
||||
)
|
||||
activeElementRef.current.focus({ preventScroll: true });
|
||||
if (isOpen && buttonRef.current) {
|
||||
buttonRef.current.focus({ preventScroll: true });
|
||||
}
|
||||
setOpen(false);
|
||||
}, [isOpen]);
|
||||
|
||||
const handleToggle = useCallback(() => {
|
||||
if (isOpen) {
|
||||
handleClose();
|
||||
} else {
|
||||
setOpen(true);
|
||||
}
|
||||
}, [isOpen, handleClose]);
|
||||
|
||||
const handleOverlayEnter = useCallback(
|
||||
(state: Partial<PopperState>) => {
|
||||
if (state.placement) setPlacement(state.placement);
|
||||
@@ -82,13 +66,18 @@ const Dropdown: React.FC<DropdownProps> = ({
|
||||
<div ref={containerRef}>
|
||||
<button
|
||||
type='button'
|
||||
ref={buttonRef}
|
||||
onClick={handleToggle}
|
||||
onMouseDown={handleMouseDown}
|
||||
onKeyDown={handleKeyDown}
|
||||
disabled={disabled}
|
||||
aria-expanded={isOpen}
|
||||
aria-controls={menuId}
|
||||
aria-labelledby={`${ariaLabelledBy} ${buttonLabelId}`}
|
||||
aria-describedby={ariaDescribedBy}
|
||||
className={classNames('dropdown-button', { active: isOpen })}
|
||||
>
|
||||
<span className='dropdown-button__label'>{valueOption?.text}</span>
|
||||
<span id={buttonLabelId} className='dropdown-button__label'>
|
||||
{valueOption?.text}
|
||||
</span>
|
||||
<Icon id='down' icon={ArrowDropDownIcon} />
|
||||
</button>
|
||||
|
||||
@@ -101,7 +90,7 @@ const Dropdown: React.FC<DropdownProps> = ({
|
||||
popperConfig={{ strategy: 'fixed', onFirstUpdate: handleOverlayEnter }}
|
||||
>
|
||||
{({ props, placement }) => (
|
||||
<div {...props}>
|
||||
<div {...props} id={menuId}>
|
||||
<div
|
||||
className={`dropdown-animation privacy-dropdown__dropdown ${placement}`}
|
||||
>
|
||||
@@ -123,6 +112,8 @@ const Dropdown: React.FC<DropdownProps> = ({
|
||||
interface Props {
|
||||
value: string;
|
||||
options: SelectItem[];
|
||||
label: string | React.ReactElement;
|
||||
hint: string | React.ReactElement;
|
||||
disabled?: boolean;
|
||||
onChange: (value: string) => void;
|
||||
}
|
||||
@@ -130,13 +121,26 @@ interface Props {
|
||||
export const SelectWithLabel: React.FC<PropsWithChildren<Props>> = ({
|
||||
value,
|
||||
options,
|
||||
label,
|
||||
hint,
|
||||
disabled,
|
||||
children,
|
||||
onChange,
|
||||
}) => {
|
||||
const uniqueId = useId();
|
||||
const labelId = `${uniqueId}-label`;
|
||||
const descId = `${uniqueId}-desc`;
|
||||
|
||||
return (
|
||||
// This label is only used for its click-forwarding behaviour,
|
||||
// accessible names are assigned manually
|
||||
// eslint-disable-next-line jsx-a11y/label-has-associated-control
|
||||
<label className='app-form__toggle'>
|
||||
<div className='app-form__toggle__label'>{children}</div>
|
||||
<div className='app-form__toggle__label'>
|
||||
<strong id={labelId}>{label}</strong>
|
||||
<span className='hint' id={descId}>
|
||||
{hint}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className='app-form__toggle__toggle'>
|
||||
<div>
|
||||
@@ -144,6 +148,8 @@ export const SelectWithLabel: React.FC<PropsWithChildren<Props>> = ({
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
aria-labelledby={labelId}
|
||||
aria-describedby={descId}
|
||||
options={options}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
@@ -24,6 +24,10 @@ import { openModal } from 'flavours/glitch/actions/modal';
|
||||
import { IconButton } from 'flavours/glitch/components/icon_button';
|
||||
import { useIdentity } from 'flavours/glitch/identity_context';
|
||||
import { me } from 'flavours/glitch/initial_state';
|
||||
import type { Account } from 'flavours/glitch/models/account';
|
||||
import type { Status } from 'flavours/glitch/models/status';
|
||||
import { makeGetStatus } from 'flavours/glitch/selectors';
|
||||
import type { RootState } from 'flavours/glitch/store';
|
||||
import { useAppSelector, useAppDispatch } from 'flavours/glitch/store';
|
||||
|
||||
const messages = defineMessages({
|
||||
@@ -50,6 +54,11 @@ const messages = defineMessages({
|
||||
open: { id: 'status.open', defaultMessage: 'Expand this status' },
|
||||
});
|
||||
|
||||
type GetStatusSelector = (
|
||||
state: RootState,
|
||||
props: { id?: string | null; contextType?: string },
|
||||
) => Status | null;
|
||||
|
||||
export const Footer: React.FC<{
|
||||
statusId: string;
|
||||
withOpenButton?: boolean;
|
||||
@@ -59,11 +68,9 @@ export const Footer: React.FC<{
|
||||
const intl = useIntl();
|
||||
const history = useHistory();
|
||||
const dispatch = useAppDispatch();
|
||||
const status = useAppSelector((state) => state.statuses.get(statusId));
|
||||
const accountId = status?.get('account') as string | undefined;
|
||||
const account = useAppSelector((state) =>
|
||||
accountId ? state.accounts.get(accountId) : undefined,
|
||||
);
|
||||
const getStatus = useMemo(() => makeGetStatus(), []) as GetStatusSelector;
|
||||
const status = useAppSelector((state) => getStatus(state, { id: statusId }));
|
||||
const account = status?.get('account') as Account | undefined;
|
||||
const askReplyConfirmation = useAppSelector(
|
||||
(state) => (state.compose.get('text') as string).trim().length !== 0,
|
||||
);
|
||||
|
||||
@@ -5,7 +5,7 @@ import ModalContainer from 'flavours/glitch/features/ui/containers/modal_contain
|
||||
|
||||
const Compose = () => (
|
||||
<>
|
||||
<ComposeFormContainer autoFocus withoutNavigation />
|
||||
<ComposeFormContainer autoFocus withoutNavigation redirectOnSuccess />
|
||||
<AlertsController />
|
||||
<ModalContainer />
|
||||
<LoadingBarContainer className='loading-bar' />
|
||||
|
||||
@@ -38,7 +38,7 @@ const Embed: React.FC<{ id: string }> = ({ id }) => {
|
||||
const dispatchRenderSignal = useRenderSignal();
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchStatus(id, false, false));
|
||||
dispatch(fetchStatus(id, { alsoFetchContext: false }));
|
||||
}, [dispatch, id]);
|
||||
|
||||
const handleToggleHidden = useCallback(() => {
|
||||
|
||||
@@ -26,18 +26,23 @@ const getHostname = url => {
|
||||
|
||||
const domParser = new DOMParser();
|
||||
|
||||
const addAutoPlay = html => {
|
||||
const handleIframeUrl = (html, url, providerName) => {
|
||||
const document = domParser.parseFromString(html, 'text/html').documentElement;
|
||||
const iframe = document.querySelector('iframe');
|
||||
const startTime = new URL(url).searchParams.get('t')
|
||||
|
||||
if (iframe) {
|
||||
if (iframe.src.indexOf('?') !== -1) {
|
||||
iframe.src += '&';
|
||||
} else {
|
||||
iframe.src += '?';
|
||||
const iframeUrl = new URL(iframe.src)
|
||||
|
||||
iframeUrl.searchParams.set('autoplay', 1)
|
||||
iframeUrl.searchParams.set('auto_play', 1)
|
||||
|
||||
if (providerName === 'YouTube') {
|
||||
iframeUrl.searchParams.set('start', startTime || '');
|
||||
iframe.referrerPolicy = 'strict-origin-when-cross-origin';
|
||||
}
|
||||
|
||||
iframe.src += 'autoplay=1&auto_play=1';
|
||||
iframe.src = iframeUrl.href
|
||||
|
||||
// DOM parser creates html/body elements around original HTML fragment,
|
||||
// so we need to get innerHTML out of the body and not the entire document
|
||||
@@ -103,7 +108,7 @@ export default class Card extends PureComponent {
|
||||
|
||||
renderVideo () {
|
||||
const { card } = this.props;
|
||||
const content = { __html: addAutoPlay(card.get('html')) };
|
||||
const content = { __html: handleIframeUrl(card.get('html'), card.get('url'), card.get('provider_name')) };
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -418,7 +418,11 @@ export const DetailedStatus: React.FC<{
|
||||
{hashtagBar}
|
||||
|
||||
{status.get('quote') && (
|
||||
<QuotedStatus quote={status.get('quote')} />
|
||||
<QuotedStatus
|
||||
quote={status.get('quote')}
|
||||
parentQuotePostId={status.get('id')}
|
||||
contextType='thread'
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -38,7 +38,7 @@ class FilterModal extends ImmutablePureComponent {
|
||||
|
||||
handleSuccess = () => {
|
||||
const { dispatch, statusId } = this.props;
|
||||
dispatch(fetchStatus(statusId, true));
|
||||
dispatch(fetchStatus(statusId, {forceFetch: true}));
|
||||
this.setState({ isSubmitting: false, isSubmitted: true, step: 'submitted' });
|
||||
};
|
||||
|
||||
|
||||
@@ -201,8 +201,6 @@ class MediaModal extends ImmutablePureComponent {
|
||||
preview={image.get('preview_url')}
|
||||
blurhash={image.get('blurhash')}
|
||||
src={image.get('url')}
|
||||
width={image.get('width')}
|
||||
height={image.get('height')}
|
||||
frameRate={image.getIn(['meta', 'original', 'frame_rate'])}
|
||||
startTime={currentTime || 0}
|
||||
startPlaying={autoPlay || false}
|
||||
@@ -218,8 +216,6 @@ class MediaModal extends ImmutablePureComponent {
|
||||
return (
|
||||
<GIFV
|
||||
src={image.get('url')}
|
||||
width={width}
|
||||
height={height}
|
||||
key={image.get('url')}
|
||||
alt={description}
|
||||
lang={lang}
|
||||
|
||||
@@ -92,8 +92,7 @@ const messages = defineMessages({
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
layout: state.getIn(['meta', 'layout']),
|
||||
hasComposingText: state.getIn(['compose', 'text']).trim().length !== 0,
|
||||
hasMediaAttachments: state.getIn(['compose', 'media_attachments']).size > 0,
|
||||
hasComposingContents: state.getIn(['compose', 'text']).trim().length !== 0 || state.getIn(['compose', 'media_attachments']).size > 0 || state.getIn(['compose', 'poll']) !== null,
|
||||
canUploadMore: !state.getIn(['compose', 'media_attachments']).some(x => ['audio', 'video'].includes(x.get('type'))) && state.getIn(['compose', 'media_attachments']).size < 4,
|
||||
isWide: state.getIn(['local_settings', 'stretch']),
|
||||
fullWidthColumns: state.getIn(['local_settings', 'fullwidth_columns']),
|
||||
@@ -284,8 +283,7 @@ class UI extends PureComponent {
|
||||
fullWidthColumns: PropTypes.bool,
|
||||
systemFontUi: PropTypes.bool,
|
||||
isComposing: PropTypes.bool,
|
||||
hasComposingText: PropTypes.bool,
|
||||
hasMediaAttachments: PropTypes.bool,
|
||||
hasComposingContents: PropTypes.bool,
|
||||
canUploadMore: PropTypes.bool,
|
||||
intl: PropTypes.object.isRequired,
|
||||
unreadNotifications: PropTypes.number,
|
||||
@@ -304,11 +302,11 @@ class UI extends PureComponent {
|
||||
};
|
||||
|
||||
handleBeforeUnload = e => {
|
||||
const { intl, dispatch, hasComposingText, hasMediaAttachments } = this.props;
|
||||
const { intl, dispatch, hasComposingContents } = this.props;
|
||||
|
||||
dispatch(synchronouslySubmitMarkers());
|
||||
|
||||
if (hasComposingText || hasMediaAttachments) {
|
||||
if (hasComposingContents) {
|
||||
// Setting returnValue to any string causes confirmation dialog.
|
||||
// Many browsers no longer display this text to users,
|
||||
// but we set user-friendly message for other browsers, e.g. Edge.
|
||||
|
||||
@@ -8,13 +8,14 @@ import { openURL } from 'flavours/glitch/actions/search';
|
||||
import { useAppDispatch } from 'flavours/glitch/store';
|
||||
|
||||
const isMentionClick = (element: HTMLAnchorElement) =>
|
||||
element.classList.contains('mention');
|
||||
element.classList.contains('mention') &&
|
||||
!element.classList.contains('hashtag');
|
||||
|
||||
const isHashtagClick = (element: HTMLAnchorElement) =>
|
||||
element.textContent?.[0] === '#' ||
|
||||
element.previousSibling?.textContent?.endsWith('#');
|
||||
|
||||
export const useLinks = () => {
|
||||
export const useLinks = (skipHashtags?: boolean) => {
|
||||
const history = useHistory();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
@@ -61,12 +62,12 @@ export const useLinks = () => {
|
||||
if (isMentionClick(target)) {
|
||||
e.preventDefault();
|
||||
void handleMentionClick(target);
|
||||
} else if (isHashtagClick(target)) {
|
||||
} else if (isHashtagClick(target) && !skipHashtags) {
|
||||
e.preventDefault();
|
||||
handleHashtagClick(target);
|
||||
}
|
||||
},
|
||||
[handleMentionClick, handleHashtagClick],
|
||||
[skipHashtags, handleMentionClick, handleHashtagClick],
|
||||
);
|
||||
|
||||
return handleClick;
|
||||
|
||||
@@ -6,12 +6,8 @@
|
||||
"account.view_full_profile": "عرض الملف الشخصي كاملاً",
|
||||
"boost_modal.missing_description": "هذا المنشور يحتوي على وسائط بلا وصف",
|
||||
"column.favourited_by": "المفضلة من قبل",
|
||||
"column.heading": "متنوعة",
|
||||
"column.reblogged_by": "المرقى من قبل",
|
||||
"column.subheading": "خيارات متنوعة",
|
||||
"column_header.profile": "الملف الشخصي",
|
||||
"column_subheading.lists": "القوائم",
|
||||
"column_subheading.navigation": "التنقل",
|
||||
"community.column_settings.allow_local_only": "إظهار المنشورات المحلية فقط",
|
||||
"compose.attach.doodle": "الرسوم و التخمين",
|
||||
"compose.change_federation": "تغيير اعدادات الفيديرالية",
|
||||
@@ -36,8 +32,6 @@
|
||||
"keyboard_shortcuts.secondary_toot": "لإرسال التبويق باستخدام إعدادات الخصوصية الثانوية",
|
||||
"moved_to_warning": "عُلِّم هذا الحساب بأنه انتقل إلى {moved_to_link}، لذا قد لا يقبل متابعات جديدة.",
|
||||
"navigation_bar.app_settings": "إعدادات التطبيق",
|
||||
"navigation_bar.keyboard_shortcuts": "اختصارات لوحة المفاتيح",
|
||||
"navigation_bar.misc": "متنوع",
|
||||
"settings.always_show_spoilers_field": "تمكين دائما حقل تحذير المحتوى",
|
||||
"settings.close": "إغلاق",
|
||||
"settings.content_warnings": "Content warnings",
|
||||
|
||||
@@ -6,12 +6,8 @@
|
||||
"account.view_full_profile": "Zobrazit celý profil",
|
||||
"boost_modal.missing_description": "Příspěvek obsahuje obrázky bez popisků",
|
||||
"column.favourited_by": "Oblíbeno uživatelem",
|
||||
"column.heading": "Různé",
|
||||
"column.reblogged_by": "Boostnuto uživatelem",
|
||||
"column.subheading": "Různé",
|
||||
"column_header.profile": "Profil",
|
||||
"column_subheading.lists": "Seznamy",
|
||||
"column_subheading.navigation": "Navigace",
|
||||
"community.column_settings.allow_local_only": "Zobrazit pouze místní tooty",
|
||||
"compose.attach.doodle": "Nakreslete něco",
|
||||
"compose.change_federation": "Změnit nastavení federace",
|
||||
@@ -45,8 +41,6 @@
|
||||
"keyboard_shortcuts.secondary_toot": "pro odeslání příspěvku s sekundárním nastavením soukromí",
|
||||
"moved_to_warning": "Tento účet je označen jako přesunut na {moved_to_link}, a proto nemusí přijímat nové sledování.",
|
||||
"navigation_bar.app_settings": "Nastavení aplikace",
|
||||
"navigation_bar.keyboard_shortcuts": "Klávesové zkratky",
|
||||
"navigation_bar.misc": "Různé",
|
||||
"notifications.column_settings.filter_bar.show_bar": "Zobrazit panel filtrů",
|
||||
"settings.always_show_spoilers_field": "Vždy zobrazit pole pro varování o obsahu",
|
||||
"settings.close": "Zavřít",
|
||||
|
||||
@@ -6,12 +6,8 @@
|
||||
"account.view_full_profile": "Dangos proffil cyfan",
|
||||
"boost_modal.missing_description": "Mae'r tŵt yma'n cynnwys ychydig gyfryngau heb ddisgrifiad",
|
||||
"column.favourited_by": "Wedi'i hoffi gan",
|
||||
"column.heading": "Misg",
|
||||
"column.reblogged_by": "Wedi'i bŵstio gan",
|
||||
"column.subheading": "Opsiynnau arall",
|
||||
"column_header.profile": "Proffil",
|
||||
"column_subheading.lists": "Rhestri",
|
||||
"column_subheading.navigation": "Llywio",
|
||||
"community.column_settings.allow_local_only": "Dangos tŵtiau lleol yn unig",
|
||||
"compose.content-type.html": "HTML",
|
||||
"compose.content-type.markdown": "Markdown",
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
"home.column_settings.advanced": "Avanceret",
|
||||
"home.column_settings.show_direct": "Vis private omtaler",
|
||||
"navigation_bar.app_settings": "Appindstillinger",
|
||||
"navigation_bar.misc": "Diverse",
|
||||
"settings.always_show_spoilers_field": "Vis altid feltet til indholdsadvarsel",
|
||||
"settings.close": "Luk",
|
||||
"settings.content_warnings": "Indholdsadvarsler",
|
||||
|
||||
@@ -6,12 +6,8 @@
|
||||
"account.view_full_profile": "Vollständiges Profil anzeigen",
|
||||
"boost_modal.missing_description": "Dieser Toot enthält Medien ohne Beschreibung",
|
||||
"column.favourited_by": "Favorisiert von",
|
||||
"column.heading": "Sonstiges",
|
||||
"column.reblogged_by": "Geteilt von",
|
||||
"column.subheading": "Sonstige Optionen",
|
||||
"column_header.profile": "Profil",
|
||||
"column_subheading.lists": "Listen",
|
||||
"column_subheading.navigation": "Navigation",
|
||||
"community.column_settings.allow_local_only": "Nur-lokale Toots anzeigen",
|
||||
"compose.attach.doodle": "Male etwas",
|
||||
"compose.change_federation": "Föderationseinstellungen ändern",
|
||||
@@ -45,8 +41,6 @@
|
||||
"keyboard_shortcuts.secondary_toot": "Toot mit sekundärer Privatsphäreeinstellung absenden",
|
||||
"moved_to_warning": "Dieses Konto ist als verschoben zu {moved_to_link} markiert und akzeptiert daher keine neuen Follower.",
|
||||
"navigation_bar.app_settings": "App-Einstellungen",
|
||||
"navigation_bar.keyboard_shortcuts": "Tastaturkürzel",
|
||||
"navigation_bar.misc": "Sonstiges",
|
||||
"notifications.column_settings.filter_bar.show_bar": "Filterleiste anzeigen",
|
||||
"settings.always_show_spoilers_field": "Das Inhaltswarnungs-Feld immer aktivieren",
|
||||
"settings.close": "Schließen",
|
||||
|
||||
@@ -1,4 +1,92 @@
|
||||
{
|
||||
"settings.content_warnings": "Content warnings",
|
||||
"settings.preferences": "Preferences"
|
||||
"about.fork_disclaimer": "Το Glitch-soc είναι ελεύθερο λογισμικό ανοιχτού κώδικα που είναι αντιγραφή από το Mastodon.",
|
||||
"account.disclaimer_full": "Οι παρακάτω πληροφορίες μπορεί να αντικατοπτρίζουν το προφίλ του χρήστη ελλιπώς.",
|
||||
"account.follows": "Ακολουθεί",
|
||||
"account.suspended_disclaimer_full": "Αυτός ο χρήστης έχει ανασταλεί από έναν συντονιστή.",
|
||||
"account.view_full_profile": "Προβολή πλήρους προφίλ",
|
||||
"boost_modal.missing_description": "Αυτό το τουτ περιέχει κάποια μέσα χωρίς περιγραφή",
|
||||
"column.favourited_by": "Αγαπήθηκε από",
|
||||
"column.reblogged_by": "Ενισχύθηκε από",
|
||||
"column_header.profile": "Προφίλ",
|
||||
"community.column_settings.allow_local_only": "Εμφάνιση τοπικών τουτ",
|
||||
"compose.attach.doodle": "Σχεδίασε κάτι",
|
||||
"compose.change_federation": "Αλλαγή ρυθμίσεων ομοσπονδίας",
|
||||
"compose.content-type.change": "Αλλαγή προηγμένων επιλογών μορφοποίησης",
|
||||
"compose.content-type.html": "HTML",
|
||||
"compose.content-type.html_meta": "Μορφοποίηση των αναρτήσεων σας με χρήση HTML",
|
||||
"compose.content-type.markdown": "Markdown",
|
||||
"compose.content-type.markdown_meta": "Μορφοποίηση των αναρτήσεων σας με χρήση Markdown",
|
||||
"compose.content-type.plain": "Απλό κείμενο",
|
||||
"compose.content-type.plain_meta": "Γράψιμο χωρίς προηγμένη μορφοποίηση",
|
||||
"compose.disable_threaded_mode": "Απενεργοποίηση λειτουργίας νημάτων",
|
||||
"compose.enable_threaded_mode": "Ενεργοποίηση λειτουργίας νημάτων",
|
||||
"compose_form.sensitive.hide": "{count, plural, one {Επισήμανση πολυμέσου ως ευαίσθητο} other {Επισήμανση πολυμέσων ως ευαίσθητα}}",
|
||||
"compose_form.sensitive.marked": "{count, plural, one {Το πολυμέσο έχει σημανθεί ως ευαίσθητο} other {Τα πολυμέσα έχουν σημανθεί ως ευαίσθητα}}",
|
||||
"compose_form.sensitive.unmarked": "{count, plural, one {Το πολυμέσο δεν έχει σημανθεί ως ευαίσθητο} other {Τα πολυμέσα δεν έχουν σημανθεί ως ευαίσθητα}}",
|
||||
"confirmation_modal.do_not_ask_again": "Να μην ζητηθεί επιβεβαίωση ξανά",
|
||||
"confirmations.deprecated_settings.confirm": "Χρήση προτιμήσεων του Mastodon",
|
||||
"direct.group_by_conversations": "Ομαδοποίηση ανά συζήτηση",
|
||||
"favourite_modal.favourite": "Αγάπησε την ανάρτηση;",
|
||||
"federation.federated.long": "Επιτρέψτε σε αυτήν την ανάρτηση να φτάσει σε άλλους διακομιστές",
|
||||
"federation.local_only.long": "Αποτρέψτε αυτήν την ανάρτηση να φτάσει σε άλλους διακομιστές",
|
||||
"federation.local_only.short": "Τοπικά μόνο",
|
||||
"firehose.column_settings.allow_local_only": "Εμφάνιση τοπικών αναρτήσεων σε \"Όλα\"",
|
||||
"home.column_settings.advanced": "Για προχωρημένους",
|
||||
"home.column_settings.filter_regex": "Φιλτράρισμα βάσει των κανονικών εκφράσεων",
|
||||
"home.column_settings.show_direct": "Εμφάνιση ιδιωτικών επισημάνσεων",
|
||||
"home.settings": "Ρυθμίσεις στήλης",
|
||||
"keyboard_shortcuts.bookmark": "για να σελιδοποιήσει",
|
||||
"keyboard_shortcuts.secondary_toot": "για αποστολή τουτ χρησιμοποιώντας δευτερεύουσα ρύθμιση απορρήτου",
|
||||
"moved_to_warning": "Αυτός ο λογαριασμός έχει σημανθεί ως μετακινημένος στο {moved_to_link} και έτσι δεν μπορεί να δεχτεί νέους ακολούθους.",
|
||||
"navigation_bar.app_settings": "Ρυθμίσεις εφαρμογής",
|
||||
"notifications.column_settings.filter_bar.show_bar": "Εμφάνιση μπάρας φίλτρου",
|
||||
"settings.always_show_spoilers_field": "Πάντα ενεργοποίηση του πεδίου Προειδοποίηση Περιεχομένου",
|
||||
"settings.close": "Κλείσιμο",
|
||||
"settings.compose_box_opts": "Πλαίσιο σύνθεσης",
|
||||
"settings.content_warnings": "Προειδοποιήσεις περιεχομένου",
|
||||
"settings.content_warnings.regexp": "Κανονική έκφραση (regex)",
|
||||
"settings.content_warnings_filter": "Προειδοποιήσεις περιεχομένου να μην ξεδιπλώνονται αυτόματα:",
|
||||
"settings.content_warnings_shared_state": "Εμφάνιση/απόκρυψη περιεχομένου όλων των αντιγράφων ταυτόχρονα",
|
||||
"settings.content_warnings_unfold_opts": "Επιλογές αυτόματου ξεδιπλώματος",
|
||||
"settings.deprecated_setting": "Αυτή η ρύθμιση τώρα ελέγχεται από τις {settings_page_link} του Mastodon",
|
||||
"settings.enable_content_warnings_auto_unfold": "Αυτόματη ξεδίπλωμα προειδοποιήσεων περιεχομένου",
|
||||
"settings.general": "Γενικά",
|
||||
"settings.layout_opts": "Επιλογές διάταξης",
|
||||
"settings.media": "Πολυμέσα",
|
||||
"settings.notifications.tab_badge": "Σήμα μη αναγνωσμένων ειδοποιήσεων",
|
||||
"settings.notifications.tab_badge.hint": "Εμφανίζει ένα σήμα για μη αναγνωσμένες ειδοποιήσεις στα εικονίδια της στήλης όταν η στήλη ειδοποιήσεων δεν είναι ανοιχτή",
|
||||
"settings.notifications_opts": "Ρυθμίσεις ειδοποιήσεων",
|
||||
"settings.pop_in_left": "Αριστερά",
|
||||
"settings.pop_in_right": "Δεξιά",
|
||||
"settings.preferences": "Προτιμήσεις χρήστη",
|
||||
"settings.preselect_on_reply": "Προεπιλογή ονομάτων χρηστών στην απάντηση",
|
||||
"settings.preselect_on_reply_hint": "Όταν απαντάτε σε μια συνομιλία με πολλαπλούς συμμετέχοντες, προεπιλέξτε τα ονόματα χρηστών μετά τον πρώτο",
|
||||
"settings.rewrite_mentions": "Ξαναγράψε επισημάνσεις στις προβαλλόμενες καταστάσεις",
|
||||
"settings.rewrite_mentions_acct": "Ξαναγράψε με όνομα χρήστη και τομέα (όταν ο λογαριασμός είναι απομακρυσμένος)",
|
||||
"settings.rewrite_mentions_no": "Μην ξαναγράψεις επισημάνσεις",
|
||||
"settings.rewrite_mentions_username": "Ξαναγράψε με όνομα χρήστη",
|
||||
"settings.shared_settings_link": "προτιμήσεις χρήστη",
|
||||
"settings.side_arm": "Δευτερεύον κουμπί τουτ:",
|
||||
"settings.side_arm.none": "Κανένα",
|
||||
"settings.side_arm_reply_mode": "Όταν απαντάτε σε ένα τουτ, το κουμπί δευτερεύοντος τουτ πρέπει να:",
|
||||
"settings.side_arm_reply_mode.copy": "Αντιγράψει τη ρύθμιση απορρήτου του τουτ που απαντάται",
|
||||
"settings.side_arm_reply_mode.keep": "Διατηρήσει το καθορισμένο απόρρητο",
|
||||
"settings.side_arm_reply_mode.restrict": "Περιορίσει τη ρύθμιση απορρήτου σε εκείνη του τουτ που απαντάται",
|
||||
"settings.status_icons": "Εικονίδια τουτ",
|
||||
"settings.status_icons_language": "Ένδειξη γλώσσας",
|
||||
"settings.status_icons_local_only": "Ένδειξη τοπικό μόνο",
|
||||
"settings.status_icons_media": "Ενδείξεις μέσων και δημοσκοπήσεων",
|
||||
"settings.status_icons_reply": "Ένδειξη απαντήσεων",
|
||||
"settings.status_icons_visibility": "Ένδειξη απορρήτου τουτ",
|
||||
"settings.tag_misleading_links": "Σήμανση παραπλανητικών συνδέσμων",
|
||||
"settings.tag_misleading_links.hint": "Προσθήκη οπτικής ένδειξης με τον εξυπηρετητή στόχο του συνδέσμου σε κάθε σύνδεσμο που δεν αναφέρεται ρητά",
|
||||
"status.filtered": "Φιλτραρισμένο",
|
||||
"status.has_audio": "Διαθέτει συνημμένα αρχεία ήχου",
|
||||
"status.has_pictures": "Διαθέτει συνημμένες εικόνες",
|
||||
"status.has_preview_card": "Διαθέτει συνημμένη κάρτα προεπισκόπησης",
|
||||
"status.has_video": "Διαθέτει συνημμένα βίντεο",
|
||||
"status.hide": "Απόκρυψη ανάρτησης",
|
||||
"status.in_reply_to": "Αυτό το τουτ είναι απάντηση",
|
||||
"status.is_poll": "Αυτό το τουτ είναι δημοσκόπηση",
|
||||
"status.show_filter_reason": "Εμφάνιση ούτως ή άλλως"
|
||||
}
|
||||
|
||||
@@ -6,12 +6,8 @@
|
||||
"account.view_full_profile": "Vidi plenan profilon",
|
||||
"boost_modal.missing_description": "Ĉi tiu afiŝo enhavas plurmedion, ke ne havas priskribon",
|
||||
"column.favourited_by": "Stelumita per",
|
||||
"column.heading": "Diversaj aferoj",
|
||||
"column.reblogged_by": "Diskonigita de",
|
||||
"column.subheading": "Diversaj agordoj",
|
||||
"column_header.profile": "Profilo",
|
||||
"column_subheading.lists": "Listoj",
|
||||
"column_subheading.navigation": "Navigado",
|
||||
"community.column_settings.allow_local_only": "Montri nur-lokajn afiŝojn",
|
||||
"compose.attach.doodle": "Skribu ion",
|
||||
"compose.change_federation": "Ŝanĝi agordojn de federacio",
|
||||
@@ -45,8 +41,6 @@
|
||||
"keyboard_shortcuts.secondary_toot": "sendi afiŝon per dua agordo de privateco",
|
||||
"moved_to_warning": "Ĉi tiun konton sigelas, kiel movinta al {moved_to_link}, kaj pro ne permesos novajn sekvantojn.",
|
||||
"navigation_bar.app_settings": "Agordoj de aplikaĵo",
|
||||
"navigation_bar.keyboard_shortcuts": "Fulmoklavoj",
|
||||
"navigation_bar.misc": "Aliaj",
|
||||
"notifications.column_settings.filter_bar.show_bar": "Montri mezuron de filtrilo",
|
||||
"settings.always_show_spoilers_field": "Ĉiam ŝaltiĝu la arealo de Enhava Averto",
|
||||
"settings.close": "Fermi",
|
||||
|
||||
@@ -6,12 +6,8 @@
|
||||
"account.view_full_profile": "Ver perfil completo",
|
||||
"boost_modal.missing_description": "Esta publicación contiene medios sin descripción",
|
||||
"column.favourited_by": "Marcado como favorito por",
|
||||
"column.heading": "Misc",
|
||||
"column.reblogged_by": "Impulsado por",
|
||||
"column.subheading": "Opciones misceláneas",
|
||||
"column_header.profile": "Perfil",
|
||||
"column_subheading.lists": "Listas",
|
||||
"column_subheading.navigation": "Navegación",
|
||||
"community.column_settings.allow_local_only": "Mostrar sólo toots locales",
|
||||
"compose.attach.doodle": "Dibujá algo",
|
||||
"compose.change_federation": "Cambiar configuración de la federación",
|
||||
@@ -45,8 +41,6 @@
|
||||
"keyboard_shortcuts.secondary_toot": "para enviar un toot usando lac onfiguración de privacidad secundaria",
|
||||
"moved_to_warning": "Esta cuenta está marcada como movida a {moved_to_link}, y por lo tanto no aceptará nuevos seguimientos.",
|
||||
"navigation_bar.app_settings": "Ajustes de aplicación",
|
||||
"navigation_bar.keyboard_shortcuts": "Atajos de teclado",
|
||||
"navigation_bar.misc": "Misc",
|
||||
"notifications.column_settings.filter_bar.show_bar": "Mostrar barra de filtros",
|
||||
"settings.always_show_spoilers_field": "Siempre mostrar el campo de advertencia de contenido",
|
||||
"settings.close": "Cerrar",
|
||||
|
||||
@@ -6,12 +6,8 @@
|
||||
"account.view_full_profile": "Ver perfil completo",
|
||||
"boost_modal.missing_description": "Esta publicación contiene medios sin descripción",
|
||||
"column.favourited_by": "Marcado como favorito por",
|
||||
"column.heading": "Misc",
|
||||
"column.reblogged_by": "Impulsado por",
|
||||
"column.subheading": "Opciones misceláneas",
|
||||
"column_header.profile": "Perfil",
|
||||
"column_subheading.lists": "Listas",
|
||||
"column_subheading.navigation": "Navegación",
|
||||
"community.column_settings.allow_local_only": "Mostrar sólo toots locales",
|
||||
"compose.attach.doodle": "Dibujar algo",
|
||||
"compose.change_federation": "Cambiar configuración de la federación",
|
||||
@@ -41,8 +37,6 @@
|
||||
"keyboard_shortcuts.secondary_toot": "para enviar un toot usando lac onfiguración de privacidad secundaria",
|
||||
"moved_to_warning": "Esta cuenta está marcada como movida a {moved_to_link}, y por lo tanto no aceptará nuevos seguimientos.",
|
||||
"navigation_bar.app_settings": "Ajustes de aplicación",
|
||||
"navigation_bar.keyboard_shortcuts": "Atajos de teclado",
|
||||
"navigation_bar.misc": "Misc",
|
||||
"settings.always_show_spoilers_field": "Siempre mostrar el campo de advertencia de contenido",
|
||||
"settings.close": "Cerrar",
|
||||
"settings.compose_box_opts": "Cuadro de redacción",
|
||||
|
||||
@@ -6,12 +6,8 @@
|
||||
"account.view_full_profile": "Ver perfil completo",
|
||||
"boost_modal.missing_description": "Esta publicación contiene medios sin descripción",
|
||||
"column.favourited_by": "Marcado como favorito por",
|
||||
"column.heading": "Misc",
|
||||
"column.reblogged_by": "Impulsado por",
|
||||
"column.subheading": "Opciones misceláneas",
|
||||
"column_header.profile": "Perfil",
|
||||
"column_subheading.lists": "Listas",
|
||||
"column_subheading.navigation": "Navegación",
|
||||
"community.column_settings.allow_local_only": "Mostrar toots solo-locales",
|
||||
"compose.attach.doodle": "Dibujar algo",
|
||||
"compose.change_federation": "Cambiar configuración de la federación",
|
||||
@@ -41,8 +37,6 @@
|
||||
"keyboard_shortcuts.secondary_toot": "para enviar un toot usando lac onfiguración de privacidad secundaria",
|
||||
"moved_to_warning": "Esta cuenta está marcada como movida a {moved_to_link}, y por lo tanto no aceptará nuevos seguimientos.",
|
||||
"navigation_bar.app_settings": "Ajustes de la aplicación",
|
||||
"navigation_bar.keyboard_shortcuts": "Atajos de teclado",
|
||||
"navigation_bar.misc": "Misc",
|
||||
"settings.always_show_spoilers_field": "Siempre mostrar el campo de advertencia de contenido",
|
||||
"settings.close": "Cerrar",
|
||||
"settings.compose_box_opts": "Cuadro de redacción",
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
"compose.content-type.plain": "متن ساده",
|
||||
"home.column_settings.advanced": "پیشرفته",
|
||||
"navigation_bar.app_settings": "تنظیمات کاره",
|
||||
"navigation_bar.keyboard_shortcuts": "میانبرهای صفحهکلید",
|
||||
"settings.close": "بستن",
|
||||
"settings.content_warnings": "هشدارهای محتوا",
|
||||
"settings.media": "رسانه",
|
||||
|
||||
@@ -6,12 +6,8 @@
|
||||
"account.view_full_profile": "Voir le profil complet",
|
||||
"boost_modal.missing_description": "Ce post contient des médias sans description",
|
||||
"column.favourited_by": "Ajouté en favori par",
|
||||
"column.heading": "Divers",
|
||||
"column.reblogged_by": "Partagé par",
|
||||
"column.subheading": "Autres options",
|
||||
"column_header.profile": "Profil",
|
||||
"column_subheading.lists": "Listes",
|
||||
"column_subheading.navigation": "Navigation",
|
||||
"community.column_settings.allow_local_only": "Afficher seulement les posts locaux",
|
||||
"compose.attach.doodle": "Dessinez quelque chose",
|
||||
"compose.change_federation": "Changer les paramètres de fédération",
|
||||
@@ -24,6 +20,9 @@
|
||||
"compose.content-type.plain_meta": "Écrire sans formatage avancé",
|
||||
"compose.disable_threaded_mode": "Désactiver le mode thread",
|
||||
"compose.enable_threaded_mode": "Activer le mode thread",
|
||||
"compose_form.sensitive.hide": "{count, plural, one {Marquer le média comme sensible} other {Marquer les médias comme sensibles}}",
|
||||
"compose_form.sensitive.marked": "{count, plural, one {Le média est marqué comme sensible} other {Les médias sont marqués comme sensibles}}",
|
||||
"compose_form.sensitive.unmarked": "{count, plural, one {Le média n’est pas marqué comme sensible} other {Les médias ne sont pas marqué comme sensible}}",
|
||||
"confirmation_modal.do_not_ask_again": "Ne plus demander confirmation",
|
||||
"confirmations.deprecated_settings.confirm": "Utiliser les préférences de Mastodon",
|
||||
"confirmations.deprecated_settings.message": "Certaines {app_settings} de glitch-soc que vous utilisez ont été remplacées par les {preferences} de Mastodon et seront remplacées :",
|
||||
@@ -42,8 +41,7 @@
|
||||
"keyboard_shortcuts.secondary_toot": "Envoyer le post en utilisant les paramètres secondaires de confidentialité",
|
||||
"moved_to_warning": "Ce compte a déménagé vers {moved_to_link} et ne peut donc plus accepter de nouveaux abonné·e·s.",
|
||||
"navigation_bar.app_settings": "Paramètres de l'application",
|
||||
"navigation_bar.keyboard_shortcuts": "Raccourcis clavier",
|
||||
"navigation_bar.misc": "Autres",
|
||||
"notifications.column_settings.filter_bar.show_bar": "Afficher la barre de filtre",
|
||||
"settings.always_show_spoilers_field": "Toujours activer le champ de rédaction de l'avertissement de contenu",
|
||||
"settings.close": "Fermer",
|
||||
"settings.compose_box_opts": "Zone de rédaction",
|
||||
@@ -57,6 +55,8 @@
|
||||
"settings.content_warnings_unfold_opts": "Options de dépliement automatique",
|
||||
"settings.deprecated_setting": "Cette option est maintenant définie par les {settings_page_link} de Mastodon",
|
||||
"settings.enable_content_warnings_auto_unfold": "Déplier automatiquement les avertissements de contenu",
|
||||
"settings.fullwidth_view": "Étirer les colonnes sur toute la largeur (mode bureau uniquement)",
|
||||
"settings.fullwidth_view_hint": "Étire les colonnes pour remplir tout l'espace disponible.",
|
||||
"settings.general": "Général",
|
||||
"settings.hicolor_privacy_icons": "Indicateurs de confidentialité en couleurs",
|
||||
"settings.hicolor_privacy_icons.hint": "Affiche les indicateurs de confidentialité dans des couleurs facilement distinguables",
|
||||
@@ -105,11 +105,14 @@
|
||||
"settings.tag_misleading_links.hint": "Ajouter une indication visuelle avec l'hôte cible du lien à chaque lien ne le mentionnant pas explicitement",
|
||||
"settings.wide_view": "Vue élargie (mode ordinateur uniquement)",
|
||||
"settings.wide_view_hint": "Étire les colonnes pour mieux remplir l'espace disponible.",
|
||||
"status.filtered": "Filtré",
|
||||
"status.has_audio": "Contient des fichiers audio attachés",
|
||||
"status.has_pictures": "Contient des images attachées",
|
||||
"status.has_preview_card": "Contient une carte de prévisualisation attachée",
|
||||
"status.has_video": "Contient des vidéos attachées",
|
||||
"status.hide": "Masquer la publication",
|
||||
"status.in_reply_to": "Ce post est une réponse",
|
||||
"status.is_poll": "Ce post est un sondage",
|
||||
"status.local_only": "Visible uniquement depuis votre instance"
|
||||
"status.local_only": "Visible uniquement depuis votre instance",
|
||||
"status.show_filter_reason": "Afficher quand même"
|
||||
}
|
||||
|
||||
@@ -2,16 +2,12 @@
|
||||
"about.fork_disclaimer": "Glitch-soc est un logiciel gratuit et open source, fork de Mastodon.",
|
||||
"account.disclaimer_full": "Les informations ci-dessous peuvent être incomplètes.",
|
||||
"account.follows": "Abonnements",
|
||||
"account.suspended_disclaimer_full": "Cet utilisateur a été suspendu par un modérateur.",
|
||||
"account.suspended_disclaimer_full": "Ce compte a été suspendu par un modérateur.",
|
||||
"account.view_full_profile": "Voir le profil complet",
|
||||
"boost_modal.missing_description": "Ce post contient des médias sans description",
|
||||
"column.favourited_by": "Ajouté en favori par",
|
||||
"column.heading": "Divers",
|
||||
"column.reblogged_by": "Partagé par",
|
||||
"column.subheading": "Autres options",
|
||||
"column_header.profile": "Profil",
|
||||
"column_subheading.lists": "Listes",
|
||||
"column_subheading.navigation": "Navigation",
|
||||
"community.column_settings.allow_local_only": "Afficher seulement les posts locaux",
|
||||
"compose.attach.doodle": "Dessinez quelque chose",
|
||||
"compose.change_federation": "Changer les paramètres de fédération",
|
||||
@@ -20,10 +16,13 @@
|
||||
"compose.content-type.html_meta": "Formatez vos messages en HTML",
|
||||
"compose.content-type.markdown": "Markdown",
|
||||
"compose.content-type.markdown_meta": "Formatez vos messages en Markdown",
|
||||
"compose.content-type.plain": "Text brut",
|
||||
"compose.content-type.plain": "Texte brut",
|
||||
"compose.content-type.plain_meta": "Écrire sans formatage avancé",
|
||||
"compose.disable_threaded_mode": "Désactiver le mode thread",
|
||||
"compose.enable_threaded_mode": "Activer le mode thread",
|
||||
"compose_form.sensitive.hide": "{count, plural, one {Marquer le média comme sensible} other {Marquer les médias comme sensibles}}",
|
||||
"compose_form.sensitive.marked": "{count, plural, one {Le média est marqué comme sensible} other {Les médias sont marqués comme sensibles}}",
|
||||
"compose_form.sensitive.unmarked": "{count, plural, one {Le média n’est pas marqué comme sensible} other {Les médias ne sont pas marqué comme sensible}}",
|
||||
"confirmation_modal.do_not_ask_again": "Ne plus demander confirmation",
|
||||
"confirmations.deprecated_settings.confirm": "Utiliser les préférences de Mastodon",
|
||||
"confirmations.deprecated_settings.message": "Certaines {app_settings} de glitch-soc que vous utilisez ont été remplacées par les {preferences} de Mastodon et seront remplacées :",
|
||||
@@ -42,8 +41,7 @@
|
||||
"keyboard_shortcuts.secondary_toot": "Envoyer le post en utilisant les paramètres secondaires de confidentialité",
|
||||
"moved_to_warning": "Ce compte a déménagé vers {moved_to_link} et ne peut donc plus accepter de nouveaux abonné·e·s.",
|
||||
"navigation_bar.app_settings": "Paramètres de l'application",
|
||||
"navigation_bar.keyboard_shortcuts": "Raccourcis clavier",
|
||||
"navigation_bar.misc": "Autres",
|
||||
"notifications.column_settings.filter_bar.show_bar": "Afficher la barre de filtre",
|
||||
"settings.always_show_spoilers_field": "Toujours activer le champ de rédaction de l'avertissement de contenu",
|
||||
"settings.close": "Fermer",
|
||||
"settings.compose_box_opts": "Zone de rédaction",
|
||||
@@ -57,6 +55,8 @@
|
||||
"settings.content_warnings_unfold_opts": "Options de dépliement automatique",
|
||||
"settings.deprecated_setting": "Cette option est maintenant définie par les {settings_page_link} de Mastodon",
|
||||
"settings.enable_content_warnings_auto_unfold": "Déplier automatiquement les avertissements de contenu",
|
||||
"settings.fullwidth_view": "Étirer les colonnes sur toute la largeur (mode bureau uniquement)",
|
||||
"settings.fullwidth_view_hint": "Étire les colonnes pour remplir tout l'espace disponible.",
|
||||
"settings.general": "Général",
|
||||
"settings.hicolor_privacy_icons": "Indicateurs de confidentialité en couleurs",
|
||||
"settings.hicolor_privacy_icons.hint": "Affiche les indicateurs de confidentialité dans des couleurs facilement distinguables",
|
||||
@@ -76,7 +76,7 @@
|
||||
"settings.pop_in_player": "Activer le lecteur pop-in",
|
||||
"settings.pop_in_position": "Position du lecteur pop-in :",
|
||||
"settings.pop_in_right": "Droite",
|
||||
"settings.preferences": "Preferences",
|
||||
"settings.preferences": "Préferences",
|
||||
"settings.prepend_cw_re": "Préfixer les avertissements avec \"re: \" lors d'une réponse",
|
||||
"settings.preselect_on_reply": "Présélectionner les noms d’utilisateur·rices lors de la réponse",
|
||||
"settings.preselect_on_reply_hint": "Présélectionner les noms d'utilisateurs après le premier lors d'une réponse à une conversation à plusieurs participants",
|
||||
@@ -105,11 +105,14 @@
|
||||
"settings.tag_misleading_links.hint": "Ajouter une indication visuelle avec l'hôte cible du lien à chaque lien ne le mentionnant pas explicitement",
|
||||
"settings.wide_view": "Vue élargie (mode ordinateur uniquement)",
|
||||
"settings.wide_view_hint": "Étire les colonnes pour mieux remplir l'espace disponible.",
|
||||
"status.filtered": "Filtré",
|
||||
"status.has_audio": "Contient des fichiers audio attachés",
|
||||
"status.has_pictures": "Contient des images attachées",
|
||||
"status.has_preview_card": "Contient une carte de prévisualisation attachée",
|
||||
"status.has_video": "Contient des vidéos attachées",
|
||||
"status.hide": "Masquer la publication",
|
||||
"status.in_reply_to": "Ce post est une réponse",
|
||||
"status.is_poll": "Ce post est un sondage",
|
||||
"status.local_only": "Visible uniquement depuis votre instance"
|
||||
"status.local_only": "Visible uniquement depuis votre instance",
|
||||
"status.show_filter_reason": "Afficher quand même"
|
||||
}
|
||||
|
||||
@@ -6,12 +6,8 @@
|
||||
"account.view_full_profile": "Ver perfil completo",
|
||||
"boost_modal.missing_description": "Esta publicación contén multimedia sen descrición",
|
||||
"column.favourited_by": "Favorecida por",
|
||||
"column.heading": "Varios",
|
||||
"column.reblogged_by": "Promovida por",
|
||||
"column.subheading": "Opcións diversas",
|
||||
"column_header.profile": "Perfil",
|
||||
"column_subheading.lists": "Listas",
|
||||
"column_subheading.navigation": "Navegación",
|
||||
"community.column_settings.allow_local_only": "Ver só mensaxes de ámbito local",
|
||||
"compose.attach.doodle": "Debuxa algo",
|
||||
"compose.change_federation": "Cambiar axustes de federación",
|
||||
@@ -45,8 +41,6 @@
|
||||
"keyboard_shortcuts.secondary_toot": "para enviar publicación usando axuste secundario de privacidade",
|
||||
"moved_to_warning": "Esta conta moveuse a {moved_to_link}, e non acepta novos seguimentos.",
|
||||
"navigation_bar.app_settings": "Axustes da app",
|
||||
"navigation_bar.keyboard_shortcuts": "Atallos de teclado",
|
||||
"navigation_bar.misc": "Varios",
|
||||
"notifications.column_settings.filter_bar.show_bar": "Amosar barra de filtros",
|
||||
"settings.always_show_spoilers_field": "Activar sempre o campo de Aviso sobre o contido CW",
|
||||
"settings.close": "Pechar",
|
||||
|
||||
@@ -6,12 +6,8 @@
|
||||
"account.view_full_profile": "Tampilkan profil lengkap",
|
||||
"boost_modal.missing_description": "Toot ini berisi beberapa media tanpa deskripsi",
|
||||
"column.favourited_by": "Disukai oleh",
|
||||
"column.heading": "Lainnya",
|
||||
"column.reblogged_by": "Dibagikan oleh",
|
||||
"column.subheading": "Opsi lain-lain",
|
||||
"column_header.profile": "Profil",
|
||||
"column_subheading.lists": "Daftar",
|
||||
"column_subheading.navigation": "Penelusuran",
|
||||
"community.column_settings.allow_local_only": "Tampilkan toot lokal saja",
|
||||
"compose.change_federation": "Ubah pengaturan federasi",
|
||||
"compose.content-type.change": "Ubah opsi pemformatan lanjutan",
|
||||
@@ -38,8 +34,6 @@
|
||||
"keyboard_shortcuts.secondary_toot": "untuk mengirim toot menggunakan pengaturan privasi sekunder",
|
||||
"moved_to_warning": "Akun ini ditandai sebagai dipindahkan ke {moved_to_link}, dan karenanya tidak menerima pengikut baru.",
|
||||
"navigation_bar.app_settings": "Pengaturan aplikasi",
|
||||
"navigation_bar.keyboard_shortcuts": "Pintasan keyboard",
|
||||
"navigation_bar.misc": "Lainnya",
|
||||
"settings.always_show_spoilers_field": "Selalu aktifkan bidang Peringatan Konten",
|
||||
"settings.close": "Tutup",
|
||||
"settings.compose_box_opts": "Kotak tulis",
|
||||
@@ -49,6 +43,7 @@
|
||||
"settings.content_warnings.regexp": "Ekspresi reguler",
|
||||
"settings.content_warnings_filter": "Peringatan konten yang tidak akan dibuka secara otomatis:",
|
||||
"settings.content_warnings_shared_state": "Tampilkan/sembunyikan konten semua salinan sekaligus",
|
||||
"settings.pop_in_right": "Kanan",
|
||||
"settings.preferences": "Preferences",
|
||||
"settings.status_icons_reply": "Indikator balasan",
|
||||
"settings.status_icons_visibility": "Indikator privasi toot",
|
||||
|
||||
@@ -6,12 +6,8 @@
|
||||
"account.view_full_profile": "正確な情報を見る",
|
||||
"boost_modal.missing_description": "このトゥートには少なくとも1つの画像に説明が付与されていません",
|
||||
"column.favourited_by": "お気に入りしたユーザー",
|
||||
"column.heading": "その他",
|
||||
"column.reblogged_by": "ブーストしたユーザー",
|
||||
"column.subheading": "その他のオプション",
|
||||
"column_header.profile": "プロフィール",
|
||||
"column_subheading.lists": "リスト",
|
||||
"column_subheading.navigation": "ナビゲーション",
|
||||
"community.column_settings.allow_local_only": "ローカル限定投稿を表示する",
|
||||
"compose.content-type.html": "HTML",
|
||||
"compose.content-type.html_meta": "投稿に HTML を使用する",
|
||||
@@ -33,8 +29,6 @@
|
||||
"keyboard_shortcuts.secondary_toot": "セカンダリートゥートの公開範囲でトゥートする",
|
||||
"moved_to_warning": "このアカウント{moved_to_link}に引っ越したため、新しいフォロワーを受け入れていません。",
|
||||
"navigation_bar.app_settings": "アプリ設定",
|
||||
"navigation_bar.keyboard_shortcuts": "キーボードショートカット",
|
||||
"navigation_bar.misc": "その他",
|
||||
"settings.always_show_spoilers_field": "常にコンテンツワーニング設定を表示する(指定がない場合は通常投稿)",
|
||||
"settings.close": "閉じる",
|
||||
"settings.compose_box_opts": "コンポーズボックス設定",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"compose.attach.doodle": "Ssuneɣ-d kra",
|
||||
"navigation_bar.keyboard_shortcuts": "Inezgumen n unasiw",
|
||||
"settings.close": "Mdel",
|
||||
"settings.content_warnings": "Content warnings",
|
||||
"settings.preferences": "Preferences"
|
||||
|
||||
@@ -6,12 +6,8 @@
|
||||
"account.view_full_profile": "전체 프로필 보기",
|
||||
"boost_modal.missing_description": "이 게시물은 설명이 없는 미디어를 포함하고 있습니다",
|
||||
"column.favourited_by": "즐겨찾기 한 사람",
|
||||
"column.heading": "기타",
|
||||
"column.reblogged_by": "부스트 한 사람",
|
||||
"column.subheading": "다양한 옵션",
|
||||
"column_header.profile": "프로필",
|
||||
"column_subheading.lists": "리스트",
|
||||
"column_subheading.navigation": "탐색",
|
||||
"community.column_settings.allow_local_only": "로컬 전용 글 보기",
|
||||
"compose.attach.doodle": "뭔가 그려보세요",
|
||||
"compose.change_federation": "연합 설정 변경",
|
||||
@@ -45,8 +41,6 @@
|
||||
"keyboard_shortcuts.secondary_toot": "보조 프라이버시 설정으로 글 보내기",
|
||||
"moved_to_warning": "이 계정은 {moved_to_link}로 이동한 것으로 표시되었고, 새 팔로우를 받지 않는 것 같습니다.",
|
||||
"navigation_bar.app_settings": "앱 설정",
|
||||
"navigation_bar.keyboard_shortcuts": "키보드 단축기",
|
||||
"navigation_bar.misc": "다양한 옵션들",
|
||||
"notifications.column_settings.filter_bar.show_bar": "필터 막대 표시",
|
||||
"settings.always_show_spoilers_field": "열람주의 항목을 언제나 활성화",
|
||||
"settings.close": "닫기",
|
||||
|
||||
@@ -6,12 +6,8 @@
|
||||
"account.view_full_profile": "Peržiūrėti visą profilį",
|
||||
"boost_modal.missing_description": "Šis įrašas turi šiek tiek medijų be aprašų.",
|
||||
"column.favourited_by": "Pamėgo",
|
||||
"column.heading": "Įvairūs",
|
||||
"column.reblogged_by": "Pasidalino",
|
||||
"column.subheading": "Įvairios parinktys",
|
||||
"column_header.profile": "Profilis",
|
||||
"column_subheading.lists": "Sąrašai",
|
||||
"column_subheading.navigation": "Naršymas",
|
||||
"community.column_settings.allow_local_only": "Rodyti tik vietinius įrašus",
|
||||
"compose.attach.doodle": "Nupiešti kažką",
|
||||
"compose.change_federation": "Keisti federacijos nustatymus",
|
||||
@@ -45,8 +41,6 @@
|
||||
"keyboard_shortcuts.secondary_toot": "siųsti įrašą naudojant antrinį privatumo nustatymą",
|
||||
"moved_to_warning": "Ši paskyra pažymėta kaip perkelta į {moved_to_link}, todėl negali priimti naujų sekimų.",
|
||||
"navigation_bar.app_settings": "Programėlės nustatymai",
|
||||
"navigation_bar.keyboard_shortcuts": "Spartieji klavišai",
|
||||
"navigation_bar.misc": "Įvairūs",
|
||||
"notifications.column_settings.filter_bar.show_bar": "Rodyti filtro juostą",
|
||||
"settings.always_show_spoilers_field": "Visada įjungti turinio įspėjimo lauką",
|
||||
"settings.close": "Užverti",
|
||||
|
||||
@@ -3,12 +3,8 @@
|
||||
"account.view_full_profile": "Volledig profiel weergeven",
|
||||
"boost_modal.missing_description": "Deze toot bevat media zonder beschrijving",
|
||||
"column.favourited_by": "Favoriet door",
|
||||
"column.heading": "Overige",
|
||||
"column.reblogged_by": "Geboost door",
|
||||
"column.subheading": "Diverse opties",
|
||||
"column_header.profile": "Profiel",
|
||||
"column_subheading.lists": "Lijsten",
|
||||
"column_subheading.navigation": "Navigatie",
|
||||
"community.column_settings.allow_local_only": "Toon alleen lokale toots",
|
||||
"compose.content-type.html": "HTML",
|
||||
"compose.content-type.markdown": "Markdown",
|
||||
|
||||
@@ -6,12 +6,8 @@
|
||||
"account.view_full_profile": "Pokaż pełny profil",
|
||||
"boost_modal.missing_description": "Ten wpis zawiera multimedialne załączniki bez opisu",
|
||||
"column.favourited_by": "Polubiony przez",
|
||||
"column.heading": "Różne",
|
||||
"column.reblogged_by": "Podbity przez",
|
||||
"column.subheading": "Różne opcje",
|
||||
"column_header.profile": "Profil",
|
||||
"column_subheading.lists": "Listy",
|
||||
"column_subheading.navigation": "Nawigacja",
|
||||
"community.column_settings.allow_local_only": "Pokazuj wyłącznie wpisy lokalne",
|
||||
"compose.content-type.html": "HTML",
|
||||
"compose.content-type.markdown": "Markdown",
|
||||
@@ -28,8 +24,6 @@
|
||||
"keyboard_shortcuts.secondary_toot": "aby opublikować wpis używając dodatkowych ustawień prywatności",
|
||||
"moved_to_warning": "To konto oznaczone jest jako przeniesione do {moved_to_link} i może z tego powodu nie akceptować nowych obserwujących.",
|
||||
"navigation_bar.app_settings": "Ustawienia aplikacji",
|
||||
"navigation_bar.keyboard_shortcuts": "Skróty klawiszowe",
|
||||
"navigation_bar.misc": "Różne",
|
||||
"settings.always_show_spoilers_field": "Zawsze pokazuj pole ostrzeżenia o zawartości",
|
||||
"settings.close": "Zamknij",
|
||||
"settings.compose_box_opts": "Pole edycji",
|
||||
|
||||
@@ -6,12 +6,8 @@
|
||||
"account.view_full_profile": "Ver o perfil completo",
|
||||
"boost_modal.missing_description": "Este toot contém algumas mídias sem descrição",
|
||||
"column.favourited_by": "Favoritado por",
|
||||
"column.heading": "Diversos",
|
||||
"column.reblogged_by": "Inpulsionado por",
|
||||
"column.subheading": "Opções diversas",
|
||||
"column_header.profile": "Perfil",
|
||||
"column_subheading.lists": "Listas",
|
||||
"column_subheading.navigation": "Navegação",
|
||||
"community.column_settings.allow_local_only": "Mostrar os toots apenas locais",
|
||||
"compose.content-type.html": "HTML",
|
||||
"compose.content-type.markdown": "Markdown",
|
||||
@@ -28,8 +24,6 @@
|
||||
"keyboard_shortcuts.secondary_toot": "para enviar toot usando a configuração de privacidade secundária",
|
||||
"moved_to_warning": "Esta conta foi como movida para {moved_to_link} e, portanto, pode não aceitar novos seguidores.",
|
||||
"navigation_bar.app_settings": "Configurações do aplicativo",
|
||||
"navigation_bar.keyboard_shortcuts": "Atalhos de teclado",
|
||||
"navigation_bar.misc": "Diversos",
|
||||
"settings.always_show_spoilers_field": "Sempre ativar o campo Aviso de Conteúdo",
|
||||
"settings.close": "Fechar",
|
||||
"settings.compose_box_opts": "Caixa de composição",
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
"account.view_full_profile": "Ver o perfil completo",
|
||||
"boost_modal.missing_description": "Este post contém alguns media sem descrição",
|
||||
"column.favourited_by": "Adicionado aos favoritos de",
|
||||
"column.heading": "Diversos",
|
||||
"moved_to_warning": "Esta conta mudou-se para {moved_to_link} e, portanto, pode não aceitar novos seguidores.",
|
||||
"notifications.column_settings.filter_bar.show_bar": "Mostrar barra de filtros",
|
||||
"settings.always_show_spoilers_field": "Mostrar sempre o campo Aviso de Conteúdo",
|
||||
|
||||
@@ -6,12 +6,8 @@
|
||||
"account.view_full_profile": "Открыть полный профиль",
|
||||
"boost_modal.missing_description": "Этот пост содержит медиафайлы без описания",
|
||||
"column.favourited_by": "Добавили в избранное",
|
||||
"column.heading": "Прочее",
|
||||
"column.reblogged_by": "Продвинули",
|
||||
"column.subheading": "Прочие разделы",
|
||||
"column_header.profile": "Профиль",
|
||||
"column_subheading.lists": "Списки",
|
||||
"column_subheading.navigation": "Навигация",
|
||||
"community.column_settings.allow_local_only": "Показывать нефедерируемые посты",
|
||||
"compose.attach.doodle": "Нарисовать что-нибудь",
|
||||
"compose.change_federation": "Изменить настройки федерации",
|
||||
@@ -39,8 +35,6 @@
|
||||
"keyboard_shortcuts.bookmark": "добавить закладку",
|
||||
"moved_to_warning": "Этот аккаунт переехал на {moved_to_link}, и скорее всего не принимает новых подписчиков.",
|
||||
"navigation_bar.app_settings": "Настройки приложения",
|
||||
"navigation_bar.keyboard_shortcuts": "Сочетания клавиш",
|
||||
"navigation_bar.misc": "Прочее",
|
||||
"notifications.column_settings.filter_bar.show_bar": "Показать панель фильтров",
|
||||
"settings.always_show_spoilers_field": "Всегда ставить предупреждение о содержании",
|
||||
"settings.close": "Закрыть",
|
||||
|
||||
@@ -6,12 +6,8 @@
|
||||
"account.view_full_profile": "View full profile",
|
||||
"boost_modal.missing_description": "This post contains some media withoot description",
|
||||
"column.favourited_by": "Favouritit by",
|
||||
"column.heading": "Misc",
|
||||
"column.reblogged_by": "Boosted by",
|
||||
"column.subheading": "Miscellaneous options",
|
||||
"column_header.profile": "Profile",
|
||||
"column_subheading.lists": "Lists",
|
||||
"column_subheading.navigation": "Navigation",
|
||||
"community.column_settings.allow_local_only": "Show local-only posts",
|
||||
"compose.attach.doodle": "Draw something",
|
||||
"compose.change_federation": "Change federation settins",
|
||||
@@ -45,8 +41,6 @@
|
||||
"keyboard_shortcuts.secondary_toot": "tae send post using secondary privacy settin",
|
||||
"moved_to_warning": "This account is marked as moved tae {moved_to_link}, an' may not accept new follows.",
|
||||
"navigation_bar.app_settings": "App settins",
|
||||
"navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
|
||||
"navigation_bar.misc": "Misc",
|
||||
"notifications.column_settings.filter_bar.show_bar": "Show filter bar",
|
||||
"settings.always_show_spoilers_field": "Always enable the Content Warning field",
|
||||
"settings.close": "Close",
|
||||
|
||||
@@ -4,12 +4,8 @@
|
||||
"account.view_full_profile": "Visa full profil",
|
||||
"boost_modal.missing_description": "Denna toot innehåller viss media utan beskrivning",
|
||||
"column.favourited_by": "Favoritmarkerad av",
|
||||
"column.heading": "Övrigt",
|
||||
"column.reblogged_by": "Boostad av",
|
||||
"column.subheading": "Övriga val",
|
||||
"column_header.profile": "Profil",
|
||||
"column_subheading.lists": "Listor",
|
||||
"column_subheading.navigation": "Navigering",
|
||||
"community.column_settings.allow_local_only": "Visa endast lokala toots",
|
||||
"compose.content-type.html": "HTML",
|
||||
"compose.content-type.markdown": "Markdown",
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
{
|
||||
"about.fork_disclaimer": ".",
|
||||
"account.disclaimer_full": ".",
|
||||
"account.follows": "",
|
||||
"community.column_settings.allow_local_only": "",
|
||||
"account.follows": "ติดตาม",
|
||||
"settings.content_warnings": "Content warnings",
|
||||
"settings.preferences": "Preferences"
|
||||
}
|
||||
|
||||
@@ -5,12 +5,8 @@
|
||||
"account.view_full_profile": "Tam görünüm",
|
||||
"boost_modal.missing_description": "Bu gönderi açıklaması olmayan medya içerir",
|
||||
"column.favourited_by": "Tarafından favorilere eklendi",
|
||||
"column.heading": "Diğer",
|
||||
"column.reblogged_by": "Tarafından yükseltildi",
|
||||
"column.subheading": "Diğer seçenekler",
|
||||
"column_header.profile": "Profil",
|
||||
"column_subheading.lists": "Listeler",
|
||||
"column_subheading.navigation": "Gezinme",
|
||||
"community.column_settings.allow_local_only": "Sadece yerel gönderileri göster",
|
||||
"compose.content-type.html": "HTML",
|
||||
"compose.content-type.markdown": "Markdown modu",
|
||||
|
||||
@@ -6,12 +6,8 @@
|
||||
"account.view_full_profile": "Переглянути повний профіль",
|
||||
"boost_modal.missing_description": "Цей дмух містить деякі медіа без опису",
|
||||
"column.favourited_by": "Уподобані",
|
||||
"column.heading": "Різне",
|
||||
"column.reblogged_by": "Поширено",
|
||||
"column.subheading": "Інші параметри",
|
||||
"column_header.profile": "Профіль",
|
||||
"column_subheading.lists": "Списки",
|
||||
"column_subheading.navigation": "Навігація",
|
||||
"community.column_settings.allow_local_only": "Показувати тільки локальні дмухи",
|
||||
"compose.content-type.html": "HTML",
|
||||
"compose.content-type.markdown": "Markdown",
|
||||
@@ -29,8 +25,6 @@
|
||||
"keyboard_shortcuts.secondary_toot": "надсилати повідомлення з використанням вторинних налаштувань конфіденційності",
|
||||
"moved_to_warning": "Цей обліковий запис позначено як переміщений до {moved_to_link} і тому не може прийняти нових підписок.",
|
||||
"navigation_bar.app_settings": "Налаштування програми",
|
||||
"navigation_bar.keyboard_shortcuts": "Комбінації клавіш",
|
||||
"navigation_bar.misc": "Різне",
|
||||
"settings.always_show_spoilers_field": "Завжди вмикати попередження про вміст",
|
||||
"settings.close": "Закрити",
|
||||
"settings.compose_box_opts": "Compose box",
|
||||
|
||||
@@ -6,12 +6,8 @@
|
||||
"account.view_full_profile": "查看完整资料",
|
||||
"boost_modal.missing_description": "这条嘟文未包含媒体描述",
|
||||
"column.favourited_by": "点赞",
|
||||
"column.heading": "杂项",
|
||||
"column.reblogged_by": "转嘟",
|
||||
"column.subheading": "其他选项",
|
||||
"column_header.profile": "资料卡",
|
||||
"column_subheading.lists": "列表",
|
||||
"column_subheading.navigation": "导航",
|
||||
"community.column_settings.allow_local_only": "只显示仅本站可见的嘟文",
|
||||
"compose.attach.doodle": "涂鸦",
|
||||
"compose.change_federation": "更改联动设置",
|
||||
@@ -45,8 +41,6 @@
|
||||
"keyboard_shortcuts.secondary_toot": "使用另一隐私设置发送嘟文",
|
||||
"moved_to_warning": "此账户已被标记为移至 {moved_to_link},并且似乎没有收到新粉丝。",
|
||||
"navigation_bar.app_settings": "应用设置",
|
||||
"navigation_bar.keyboard_shortcuts": "键盘快捷键",
|
||||
"navigation_bar.misc": "杂项",
|
||||
"notifications.column_settings.filter_bar.show_bar": "显示筛选栏",
|
||||
"settings.always_show_spoilers_field": "始终显示内容警告框",
|
||||
"settings.close": "关闭",
|
||||
|
||||
@@ -6,12 +6,8 @@
|
||||
"account.view_full_profile": "查看完整個人資料",
|
||||
"boost_modal.missing_description": "此貼文包含未加說明的媒體檔案",
|
||||
"column.favourited_by": "誰按了最愛",
|
||||
"column.heading": "雜項",
|
||||
"column.reblogged_by": "被誰轉貼",
|
||||
"column.subheading": "其他選項",
|
||||
"column_header.profile": "個人檔案",
|
||||
"column_subheading.lists": "列表",
|
||||
"column_subheading.navigation": "導覽",
|
||||
"community.column_settings.allow_local_only": "顯示僅限本地的貼文",
|
||||
"compose.attach.doodle": "塗鴉",
|
||||
"compose.change_federation": "變更聯邦設定",
|
||||
@@ -24,6 +20,9 @@
|
||||
"compose.content-type.plain_meta": "不使用進階格式撰寫",
|
||||
"compose.disable_threaded_mode": "停用貼文串模式",
|
||||
"compose.enable_threaded_mode": "啟用貼文串模式",
|
||||
"compose_form.sensitive.hide": "{count, plural, other {將媒體標記為敏感內容}}",
|
||||
"compose_form.sensitive.marked": "{count, plural, other {媒體已被標記為敏感內容}}",
|
||||
"compose_form.sensitive.unmarked": "{count, plural, other {媒體未被標記為敏感內容}}",
|
||||
"confirmation_modal.do_not_ask_again": "不要再顯示確認訊息",
|
||||
"confirmations.deprecated_settings.confirm": "使用 Mastodon 偏好",
|
||||
"confirmations.deprecated_settings.message": "您正在使用的某些特定於 glitch-soc 設備的 {app_settings} 已被 Mastodon {preferences} 所取代,並將被覆蓋:",
|
||||
@@ -42,8 +41,6 @@
|
||||
"keyboard_shortcuts.secondary_toot": "使用次要隱私設定來發布貼文",
|
||||
"moved_to_warning": "此帳戶已標記為移至 {moved_to_link},因此可能不接受新的追隨者。",
|
||||
"navigation_bar.app_settings": "應用程式設定",
|
||||
"navigation_bar.keyboard_shortcuts": "鍵盤快速鍵",
|
||||
"navigation_bar.misc": "雜項",
|
||||
"notifications.column_settings.filter_bar.show_bar": "顯示過濾器",
|
||||
"settings.always_show_spoilers_field": "永遠啟用內容警告欄位",
|
||||
"settings.close": "關閉",
|
||||
@@ -58,6 +55,8 @@
|
||||
"settings.content_warnings_unfold_opts": "自動展開選項",
|
||||
"settings.deprecated_setting": "此設定現在已由 Mastodon 的 {settings_page_link} 控制。",
|
||||
"settings.enable_content_warnings_auto_unfold": "自動展開內容警告",
|
||||
"settings.fullwidth_view": "將欄位延展至全寬(僅限桌面模式)",
|
||||
"settings.fullwidth_view_hint": "將欄位延展以填滿所有可用空間。",
|
||||
"settings.general": "一般設定",
|
||||
"settings.hicolor_privacy_icons": "隱私圖示使用對比色",
|
||||
"settings.hicolor_privacy_icons.hint": "用明亮且易於區分的顏色顯示隱私圖示",
|
||||
|
||||
@@ -126,6 +126,9 @@ export function createAccountFromServerJSON(serverJSON: ApiAccountJSON) {
|
||||
? accountJSON.username
|
||||
: accountJSON.display_name;
|
||||
|
||||
const accountNote =
|
||||
accountJSON.note && accountJSON.note !== '<p></p>' ? accountJSON.note : '';
|
||||
|
||||
return AccountFactory({
|
||||
...accountJSON,
|
||||
moved: moved?.id,
|
||||
@@ -142,8 +145,8 @@ export function createAccountFromServerJSON(serverJSON: ApiAccountJSON) {
|
||||
escapeTextContentForBrowser(displayName),
|
||||
emojiMap,
|
||||
),
|
||||
note_emojified: emojify(accountJSON.note, emojiMap),
|
||||
note_plain: unescapeHTML(accountJSON.note),
|
||||
note_emojified: emojify(accountNote, emojiMap),
|
||||
note_plain: unescapeHTML(accountNote),
|
||||
url:
|
||||
accountJSON.url.startsWith('http://') ||
|
||||
accountJSON.url.startsWith('https://')
|
||||
|
||||
@@ -619,8 +619,13 @@ export const composeReducer = (state = initialState, action) => {
|
||||
}
|
||||
|
||||
if (action.status.get('poll')) {
|
||||
let options = ImmutableList(action.status.get('poll').options.map(x => x.title));
|
||||
if (options.size < action.maxOptions) {
|
||||
options = options.push('');
|
||||
}
|
||||
|
||||
map.set('poll', ImmutableMap({
|
||||
options: ImmutableList(action.status.get('poll').options.map(x => x.title)),
|
||||
options: options,
|
||||
multiple: action.status.get('poll').multiple,
|
||||
expires_in: expiresInFromExpiresAt(action.status.get('poll').expires_at),
|
||||
}));
|
||||
@@ -650,8 +655,13 @@ export const composeReducer = (state = initialState, action) => {
|
||||
}
|
||||
|
||||
if (action.status.get('poll')) {
|
||||
let options = ImmutableList(action.status.get('poll').options.map(x => x.title));
|
||||
if (options.size < action.maxOptions) {
|
||||
options = options.push('');
|
||||
}
|
||||
|
||||
map.set('poll', ImmutableMap({
|
||||
options: ImmutableList(action.status.get('poll').options.map(x => x.title)),
|
||||
options: options,
|
||||
multiple: action.status.get('poll').multiple,
|
||||
expires_in: expiresInFromExpiresAt(action.status.get('poll').expires_at),
|
||||
}));
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user