Compare commits
135 commits
bef44f856e
...
088fd46b67
Author | SHA1 | Date | |
---|---|---|---|
Izalia Mae | 088fd46b67 | ||
7acf26e777 | |||
2093436349 | |||
0f4637981b | |||
e5ae75bf6a | |||
2f112432e6 | |||
ffea668076 | |||
853d0f28c5 | |||
78b822c61d | |||
cf3ad10e75 | |||
3074338d79 | |||
a5a00d7f7a | |||
dd58db64d8 | |||
6883fddb19 | |||
4725191d3c | |||
83a8efa9ca | |||
dcdf081c6f | |||
3cf60ba267 | |||
624d7ae51d | |||
6319845141 | |||
3579c9a842 | |||
24f446d70b | |||
7424dd0010 | |||
637a7c78e6 | |||
aed9d4f567 | |||
23a2451576 | |||
95fb53c53e | |||
54e798a5a0 | |||
958955cda4 | |||
98779535fe | |||
77c2ea1f0f | |||
f2a6e71bb6 | |||
448be26b34 | |||
9b795a25cd | |||
368d6fe54f | |||
3c76f1f6c2 | |||
628dcbb732 | |||
a4090ab646 | |||
0d20b38da7 | |||
3fd3e88b25 | |||
a1abda39dd | |||
8180f7ba19 | |||
c4a5e0ca0e | |||
13e9d91ba7 | |||
4894deca7e | |||
26c2b401a5 | |||
01405bc6f8 | |||
a3f176423f | |||
3f74235ac5 | |||
00cc1536f2 | |||
55e368c02f | |||
b5c6a116a7 | |||
9b4afb320a | |||
9205b4e32f | |||
a5fd2fe1cb | |||
c87b1a20c7 | |||
473fed2cdf | |||
60abcb3c4c | |||
3588fbc766 | |||
cb4e28f405 | |||
68dcbcb7bf | |||
30e895299c | |||
3970a6f433 | |||
343e1fe8e9 | |||
4b92e59f4f | |||
d1387579b9 | |||
9b3e22c40d | |||
7e6ffa085f | |||
1b2ef60cec | |||
b034dc42be | |||
0405be69d2 | |||
d4f590d6bb | |||
41517a4845 | |||
fcc4c9b34a | |||
472fd4307f | |||
c16aadf718 | |||
6ae97bba25 | |||
302fcb9788 | |||
9b32ca583e | |||
c6cda209d5 | |||
8276274bf6 | |||
23fcf7869e | |||
d047e93f47 | |||
0512780e0d | |||
6a9c74a7af | |||
cfb9450d20 | |||
1554e0e66a | |||
13227e1daf | |||
ab59743c13 | |||
d66dfc7b3c | |||
0e8f8a1a1c | |||
507e1d22f5 | |||
745bdb11a0 | |||
d35fe3d5e3 | |||
f33e22ae4c | |||
ff70e50199 | |||
332a411fad | |||
afd0d424da | |||
f79c200f7e | |||
21a1a8ee88 | |||
b52dc5f69d | |||
a3a5aa1597 | |||
598888a7c4 | |||
cef87ba86c | |||
f4a6365f55 | |||
ebe2c10932 | |||
a36dfbb2aa | |||
3e63fcd4f0 | |||
15b88a83ab | |||
0c689b9d01 | |||
fd33bcb3b2 | |||
ae62e5fa53 | |||
a65f86ae55 | |||
2ba14097ff | |||
7101bc534c | |||
f9655d6850 | |||
f0fd8c5c38 | |||
167b073087 | |||
2f4dae26ee | |||
a66cf52448 | |||
973e4756e8 | |||
f6e34ca134 | |||
932a22219a | |||
a7bf439cfd | |||
cff7d967f9 | |||
fdabfb9d0e | |||
b6a928cd04 | |||
9765d2b3f8 | |||
aefefc74c4 | |||
2bcb081ce8 | |||
2195f21524 | |||
105e1f0ca6 | |||
85ec615393 | |||
264655c53a | |||
d11d15748c |
2
.github/workflows/build-image.yml
vendored
2
.github/workflows/build-image.yml
vendored
|
@ -17,7 +17,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: hadolint/hadolint-action@v3.0.0
|
||||
- uses: hadolint/hadolint-action@v3.1.0
|
||||
- uses: docker/setup-qemu-action@v2
|
||||
- uses: docker/setup-buildx-action@v2
|
||||
- uses: docker/login-action@v2
|
||||
|
|
185
CHANGELOG.md
185
CHANGELOG.md
|
@ -3,6 +3,191 @@ Changelog
|
|||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [4.1.0] - UNRELEASED
|
||||
|
||||
### Added
|
||||
|
||||
- **Add support for importing/exporting server-wide domain blocks** ([enbylenore](https://github.com/mastodon/mastodon/pull/20597), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/21471), [dariusk](https://github.com/mastodon/mastodon/pull/22803), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/21470))
|
||||
- Add listing of followed hashtags ([connorshea](https://github.com/mastodon/mastodon/pull/21773))
|
||||
- Add support for editing media description and focus point of already-sent posts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20878))
|
||||
- Add follow request banner on account header ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20785))
|
||||
- Add confirmation screen when handling reports ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22375), [Gargron](https://github.com/mastodon/mastodon/pull/23156), [tribela](https://github.com/mastodon/mastodon/pull/23178))
|
||||
- Add option to make the landing page be `/about` even when trends are enabled ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20808))
|
||||
- Add `noindex` setting back to the admin interface ([prplecake](https://github.com/mastodon/mastodon/pull/22205))
|
||||
- Add instance peers API endpoint toggle back to the admin interface ([dariusk](https://github.com/mastodon/mastodon/pull/22810))
|
||||
- Add instance activity API endpoint toggle back to the admin interface ([dariusk](https://github.com/mastodon/mastodon/pull/22833))
|
||||
- Add `account.approved` webhook ([Saiv46](https://github.com/mastodon/mastodon/pull/22938))
|
||||
- Add 12 hours option to polls ([Pleclown](https://github.com/mastodon/mastodon/pull/21131))
|
||||
- Add dropdown menu item to open admin interface for remote domains ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21895))
|
||||
- Add `--remove-headers`, `--prune-profiles` and `--include-follows` flags to `tootctl media remove` ([evanphilip](https://github.com/mastodon/mastodon/pull/22149))
|
||||
- Add `--email` and `--dry-run` options to `tootctl accounts delete` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22328))
|
||||
- Add `tootctl accounts migrate` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22330))
|
||||
- Add `tootctl accounts prune` ([tribela](https://github.com/mastodon/mastodon/pull/18397))
|
||||
- Add `tootctl domains purge` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22063))
|
||||
- Add `SIDEKIQ_CONCURRENCY` environment variable ([muffinista](https://github.com/mastodon/mastodon/pull/19589))
|
||||
- Add `MIN_THREADS` environment variable to set minimum Puma threads ([jimeh](https://github.com/mastodon/mastodon/pull/21048))
|
||||
- Add explanation text to log-in page ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20946))
|
||||
- Add user profile OpenGraph tag on post pages ([bramus](https://github.com/mastodon/mastodon/pull/21423))
|
||||
- Add maskable icon support for Android ([workeffortwaste](https://github.com/mastodon/mastodon/pull/20904))
|
||||
- Add Belarusian to supported languages ([Mixaill](https://github.com/mastodon/mastodon/pull/22022))
|
||||
- Add Western Frisian to supported languages ([ykzts](https://github.com/mastodon/mastodon/pull/18602))
|
||||
- Add Montenegrin to the language picker ([ayefries](https://github.com/mastodon/mastodon/pull/21013))
|
||||
- Add Southern Sami and Lule Sami to the language picker ([Jullan-M](https://github.com/mastodon/mastodon/pull/21262))
|
||||
- Add logging for Rails cache timeouts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21667))
|
||||
- Add color highlight for active hashtag “follow” button ([MFTabriz](https://github.com/mastodon/mastodon/pull/21629))
|
||||
- Add brotli compression to `assets:precompile` ([Izorkin](https://github.com/mastodon/mastodon/pull/19025))
|
||||
- Add “disabled” account filter to the `/admin/accounts` UI ([tribela](https://github.com/mastodon/mastodon/pull/21282))
|
||||
- Add transparency to modal background for accessibility ([edent](https://github.com/mastodon/mastodon/pull/18081))
|
||||
- Add `title` attribute to video elements in media attachments ([bramus](https://github.com/mastodon/mastodon/pull/21420))
|
||||
- Add left and right margins to emojis ([dsblank](https://github.com/mastodon/mastodon/pull/20464))
|
||||
- Add `reading:autoplay:gifs` to `/api/v1/preferences` ([j-f1](https://github.com/mastodon/mastodon/pull/22706))
|
||||
- Add `hide_collections` parameter to `/api/v1/accounts/credentials` ([CarlSchwan](https://github.com/mastodon/mastodon/pull/22790))
|
||||
- Add `policy` attribute to web push subscription objects in `/api/v1/push/subscriptions` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23210))
|
||||
- Add more specific error messages to HTTP signature verification ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21617))
|
||||
- Add Storj DCS to cloud object storage options in the `mastodon:setup` rake task ([jtolio](https://github.com/mastodon/mastodon/pull/21929))
|
||||
- Add checkmark symbol in the checkbox for sensitive media ([sidp](https://github.com/mastodon/mastodon/pull/22795))
|
||||
- Add missing accessibility attributes to logout link in modals ([kytta](https://github.com/mastodon/mastodon/pull/22549))
|
||||
- Add missing accessibility attributes to “Hide image” button in `MediaGallery` ([hs4man21](https://github.com/mastodon/mastodon/pull/22513))
|
||||
- Add missing accessibility attributes to hide content warning field when disabled ([hs4man21](https://github.com/mastodon/mastodon/pull/22568))
|
||||
- Add `aria-hidden` to footer circle dividers to improve accessibility ([hs4man21](https://github.com/mastodon/mastodon/pull/22576))
|
||||
- Add `lang` attribute to compose form inputs ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23240))
|
||||
|
||||
### Changed
|
||||
|
||||
- **Ensure exact match is the first result in hashtag searches** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21315))
|
||||
- Change account search to return followed accounts first ([dariusk](https://github.com/mastodon/mastodon/pull/22956))
|
||||
- Change batch account suspension to create a strike ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20897))
|
||||
- Change default reply language to match the default language when replying to a translated post ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22272))
|
||||
- Change misleading wording about waitlists ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20850))
|
||||
- Increase width of the unread notification border ([connorshea](https://github.com/mastodon/mastodon/pull/21692))
|
||||
- Change new post notification button on profiles to make it more apparent when it is enabled ([tribela](https://github.com/mastodon/mastodon/pull/22541))
|
||||
- Change trending tags admin interface to always show batch action controls ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23013))
|
||||
- Change wording of some OAuth scope descriptions ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22491))
|
||||
- Change wording of admin report handling actions ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18388))
|
||||
- Change confirm prompts for relationships management ([tribela](https://github.com/mastodon/mastodon/pull/19411))
|
||||
- Change language surrounding disability in prompts for media descriptions ([hs4man21](https://github.com/mastodon/mastodon/pull/20923))
|
||||
- Change confusing wording in the sign in banner ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22490))
|
||||
- Change account moderation notes to make links clickable ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22553))
|
||||
- Change email address input to be read-only for logged-in users when requesting a new confirmation e-mail ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23247))
|
||||
- Save avatar or header correctly even if the other one fails ([tribela](https://github.com/mastodon/mastodon/pull/18465))
|
||||
- Change `referrer-policy` to `same-origin` application-wide ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23014), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/23037))
|
||||
- Add 'private' to `Cache-Control`, match Rails expectations ([daxtens](https://github.com/mastodon/mastodon/pull/20608))
|
||||
- Make the button that expands the compose form differentiable from the button that publishes a post ([Tak](https://github.com/mastodon/mastodon/pull/20864))
|
||||
- Change automatic post deletion configuration to be accessible to moved users ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20774))
|
||||
- Make tag following idempotent ([trwnh](https://github.com/mastodon/mastodon/pull/20860), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/21285))
|
||||
- Use buildx functions for faster builds ([inductor](https://github.com/mastodon/mastodon/pull/20692))
|
||||
- Split off Dockerfile components for faster builds ([moritzheiber](https://github.com/mastodon/mastodon/pull/20933), [ineffyble](https://github.com/mastodon/mastodon/pull/20948), [BtbN](https://github.com/mastodon/mastodon/pull/21028))
|
||||
- Change last occurrence of “silence” to “limit” in UI text ([cincodenada](https://github.com/mastodon/mastodon/pull/20637))
|
||||
- Change “hide toot” to “hide post” ([seanthegeek](https://github.com/mastodon/mastodon/pull/22385))
|
||||
- Don't allow URLs that contain non-normalized paths to be verified ([dgl](https://github.com/mastodon/mastodon/pull/20999))
|
||||
- Change the “Trending now” header to be a link to the Explore page ([connorshea](https://github.com/mastodon/mastodon/pull/21759))
|
||||
- Change PostgreSQL connection timeout from 2 minutes to 15 seconds ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21790))
|
||||
- Make handle more easily selectable on profile page ([cadars](https://github.com/mastodon/mastodon/pull/21479))
|
||||
- Allow admins to refresh remotely-suspended accounts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22327))
|
||||
- Change dropdown menu to contain “Copy link to post” even for non-public posts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21316))
|
||||
- Allow adding relays in secure mode and limited federation mode ([ineffyble](https://github.com/mastodon/mastodon/pull/22324))
|
||||
- Change timestamps to be displayed using the user's timezone throughout the moderation interface ([FrancisMurillo](https://github.com/mastodon/mastodon/pull/21878), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/22555))
|
||||
- Change CSP directives on API to be tight and concise ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20960))
|
||||
- Change web UI to not autofocus the compose form ([raboof](https://github.com/mastodon/mastodon/pull/16517))
|
||||
- Change idempotency key handling for posting when database access is slow ([lambda](https://github.com/mastodon/mastodon/pull/21840))
|
||||
- Change remote media files to be downloaded outside of transactions ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21796))
|
||||
- Improve contrast of charts in “poll has ended” notifications ([j-f1](https://github.com/mastodon/mastodon/pull/22575))
|
||||
- Change OEmbed detection and validation to be somewhat more lenient ([ineffyble](https://github.com/mastodon/mastodon/pull/22533))
|
||||
- Widen ElasticSearch version detection to not display a warning for OpenSearch ([VyrCossont](https://github.com/mastodon/mastodon/pull/22422), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/23064))
|
||||
- Change link verification to allow pages larger than 1MB as long as the link is in the first 1MB ([untitaker](https://github.com/mastodon/mastodon/pull/22879))
|
||||
- Update default Node.js version to Node.js 16 ([ineffyble](https://github.com/mastodon/mastodon/pull/22223), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/22342))
|
||||
|
||||
### Removed
|
||||
|
||||
- Officially remove support for Ruby 2.6 ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21477))
|
||||
- Remove `object-fit` polyfill used for old versions of Microsoft Edge ([shuuji3](https://github.com/mastodon/mastodon/pull/22693))
|
||||
- Remove empty `title` tag from mailer layout ([nametoolong](https://github.com/mastodon/mastodon/pull/23078))
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Fix changing domain block severity not undoing individual account effects** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22135))
|
||||
- Fix suspension worker crashing on S3-compatible setups without ACL support ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22487))
|
||||
- Fix possible race conditions when suspending/unsuspending accounts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22363))
|
||||
- Fix being stuck in edit mode when deleting the edited status ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22126))
|
||||
- Fix filters not being applied to some notification types ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23211))
|
||||
- Fix some performance issues with `/admin/instances` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21907))
|
||||
- Fix some pre-4.0 admin audit logs ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22091))
|
||||
- Fix moderation audit log items for warnings having incorrect links ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23242))
|
||||
- Fix account activation being sometimes triggered before email confirmation ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23245))
|
||||
- Fix missing OAuth scopes for admin APIs ([trwnh](https://github.com/mastodon/mastodon/pull/20918), [trwnh](https://github.com/mastodon/mastodon/pull/20979))
|
||||
- Fix voter count not being cleared when a poll is reset ([afontenot](https://github.com/mastodon/mastodon/pull/21700))
|
||||
- Fix attachments of edited statuses not being fetched ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21565))
|
||||
- Fix irreversible and whole_word parameters handling in `/api/v1/filters` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21988))
|
||||
- Fix 500 error when marking posts as sensitive while some of them are deleted ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22134))
|
||||
- Fix expanded statuses not always being scrolled into view ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21797))
|
||||
- Fix not being able to scroll the remote interaction modal on small screens ([xendke](https://github.com/mastodon/mastodon/pull/21763))
|
||||
- Fix audio player volume control on Safari ([minacle](https://github.com/mastodon/mastodon/pull/23187))
|
||||
- Fix disappearing “Explore” tabs on Safari ([nyura](https://github.com/mastodon/mastodon/pull/20917), [ykzts](https://github.com/mastodon/mastodon/pull/20982))
|
||||
- Fix wrong padding in RTL layout ([Gargron](https://github.com/mastodon/mastodon/pull/23157))
|
||||
- Fix drag & drop upload area display in single-column mode ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23217))
|
||||
- Fix being unable to get a single EmailDomainBlock from the admin API ([trwnh](https://github.com/mastodon/mastodon/pull/20846))
|
||||
- Fix pagination of followed tags ([trwnh](https://github.com/mastodon/mastodon/pull/20861))
|
||||
- Fix dropdown menu positions when scrolling ([sidp](https://github.com/mastodon/mastodon/pull/22916), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/23062))
|
||||
- Fix email with empty domain name labels passing validation ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23246))
|
||||
- Fix mysterious registration failure when “Require a reason to join” is set with open registrations ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22127))
|
||||
- Fix attachment rendering of edited posts in OpenGraph ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22270))
|
||||
- Fix invalid/empty RSS feed link on account pages ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20772))
|
||||
- Fix error in `VerifyLinkService` when processing links with no href ([joshuap](https://github.com/mastodon/mastodon/pull/20741))
|
||||
- Fix error in `VerifyLinkService` when processing links with invalid URLs ([untitaker](https://github.com/mastodon/mastodon/pull/23204))
|
||||
- Fix media uploads with FFmpeg 5 ([dead10ck](https://github.com/mastodon/mastodon/pull/21191))
|
||||
- Fix sensitive flag not being set when replying to a post with a content warning under certain conditions ([kedamaDQ](https://github.com/mastodon/mastodon/pull/21724))
|
||||
- Fix “Share @user's profile” profile menu item not working ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21490))
|
||||
- Fix crash and incorrect behavior in `tootctl domains crawl` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19004))
|
||||
- Fix autoplay on iOS ([jamesadney](https://github.com/mastodon/mastodon/pull/21422))
|
||||
- Fix spaces not being stripped in admin account search ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21324))
|
||||
- Fix spaces not being stripped when adding relays ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22655))
|
||||
- Fix infinite loading spinner instead of soft 404 for non-existing remote accounts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21303))
|
||||
- Fix minor visual issue with the top border of verified account fields ([j-f1](https://github.com/mastodon/mastodon/pull/22006))
|
||||
- Fix pending account approval and rejection not being recorded in the admin audit log ([FrancisMurillo](https://github.com/mastodon/mastodon/pull/22088))
|
||||
- Fix “Sign up” button with closed registrations not opening modal on mobile ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22060))
|
||||
- Fix UI header overflowing on mobile ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21783))
|
||||
- Fix 500 error when trying to migrate to an invalid address ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21462))
|
||||
- Fix crash when trying to fetch unobtainable avatar of user using external authentication ([lochiiconnectivity](https://github.com/mastodon/mastodon/pull/22462))
|
||||
- Fix potential duplicate statuses in Explore tab ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22121))
|
||||
- Fix deprecation warning in `tootctl accounts rotate` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22120))
|
||||
- Fix missing style in warning and strike cards ([AtelierSnek](https://github.com/mastodon/mastodon/pull/22177), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/22302))
|
||||
- Fix wasteful request to `/api/v1/custom_emojis` when not logged in ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22326))
|
||||
- Fix replies sometimes being delivered to user-blocked domains ([tribela](https://github.com/mastodon/mastodon/pull/22117))
|
||||
- Fix admin dashboard crash when using some ElasticSearch replacements ([cortices](https://github.com/mastodon/mastodon/pull/21006))
|
||||
- Fix profile avatar being slightly offset into left border ([RiedleroD](https://github.com/mastodon/mastodon/pull/20994))
|
||||
- Fix N+1 queries in `NotificationsController` ([nametoolong](https://github.com/mastodon/mastodon/pull/21202))
|
||||
- Fix being unable to react to announcements with the keycap number sign emoji ([kescherCode](https://github.com/mastodon/mastodon/pull/22231))
|
||||
- Fix height computation of post embeds ([hodgesmr](https://github.com/mastodon/mastodon/pull/22141))
|
||||
- Fix accessibility issue of the search bar due to hidden placeholder ([alexstine](https://github.com/mastodon/mastodon/pull/21275))
|
||||
- Fix layout change handler not being removed due to a typo ([nschonni](https://github.com/mastodon/mastodon/pull/21829))
|
||||
- Fix typo in the default `S3_HOSTNAME` used in the `mastodon:setup` rake task ([danp](https://github.com/mastodon/mastodon/pull/19932))
|
||||
- Fix the top action bar appearing in the multi-column layout ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20943))
|
||||
- Fix inability to use local LibreTranslate without setting `ALLOWED_PRIVATE_ADDRESSES` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21926))
|
||||
- Fix punycoded local domains not being prettified in initial state ([Tritlo](https://github.com/mastodon/mastodon/pull/21440))
|
||||
- Fix CSP violation warning by removing inline CSS from SVG logo ([luxiaba](https://github.com/mastodon/mastodon/pull/20814))
|
||||
- Fix margin for search field on medium window size ([minacle](https://github.com/mastodon/mastodon/pull/21606))
|
||||
- Fix search popout scrolling with the page in single-column mode ([rgroothuijsen](https://github.com/mastodon/mastodon/pull/16463))
|
||||
- Fix minor status cache hydration discrepancy ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19879))
|
||||
- Fix `・` detection in hashtags ([parthoghosh24](https://github.com/mastodon/mastodon/pull/22888))
|
||||
- Fix hashtag follows bypassing user blocks ([tribela](https://github.com/mastodon/mastodon/pull/22849))
|
||||
- Fix moved accounts being incorrectly redirected to account settings when trying to view a remote profile ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22497))
|
||||
- Fix site upload validations ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22479))
|
||||
- Fix “Add new domain block” button using last submitted search value instead of the current one ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22485))
|
||||
- Fix misleading hashtag warning when posting with “Followers only” or “Mentioned people only” visibility ([n0toose](https://github.com/mastodon/mastodon/pull/22827))
|
||||
- Fix embedded posts with videos grabbing focus ([Akkiesoft](https://github.com/mastodon/mastodon/pull/22778))
|
||||
- Fix `$` not being escaped in `.env.production` files generated by the `mastodon:setup` rake task ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23012), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/23072))
|
||||
- Fix sanitizer parsing link text as HTML when stripping unsupported links ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22558))
|
||||
- Fix `scheduled_at` input not using `datetime-local` when editing announcements ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21896))
|
||||
- Fix REST API serializer for `Account` not including `moved` when the moved account has itself moved ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22483))
|
||||
- Fix `/api/v1/admin/trends/tags` using wrong serializer ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18943))
|
||||
- Fix situations in which instance actor can be set to a Mastodon-incompatible name ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22307))
|
||||
|
||||
### Security
|
||||
|
||||
- Add `form-action` CSP directive ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20781), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/20958), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/20962))
|
||||
- Fix unbounded recursion in account discovery ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22025))
|
||||
- Revoke all authorized applications on password reset ([FrancisMurillo](https://github.com/mastodon/mastodon/pull/21325))
|
||||
|
||||
## [4.0.2] - 2022-11-15
|
||||
### Fixed
|
||||
|
||||
|
|
6
Gemfile
6
Gemfile
|
@ -10,12 +10,12 @@ gem 'puma', '~> 5.6'
|
|||
gem 'rails', '~> 6.1.7'
|
||||
gem 'sprockets', '~> 3.7.2'
|
||||
gem 'thor', '~> 1.2'
|
||||
gem 'rack', '~> 2.2.5'
|
||||
gem 'rack', '~> 2.2.6'
|
||||
|
||||
gem 'hamlit-rails', '~> 0.2'
|
||||
gem 'pg', '~> 1.4'
|
||||
gem 'makara', '~> 0.5'
|
||||
gem 'pghero', '~> 2.8'
|
||||
gem 'pghero'
|
||||
gem 'dotenv-rails', '~> 2.8'
|
||||
|
||||
gem 'aws-sdk-s3', '~> 1.117', require: false
|
||||
|
@ -60,7 +60,7 @@ gem 'idn-ruby', require: 'idn'
|
|||
gem 'kaminari', '~> 1.2'
|
||||
gem 'link_header', '~> 0.0'
|
||||
gem 'mime-types', '~> 3.4.1', require: 'mime/types/columnar'
|
||||
gem 'nokogiri', '~> 1.13'
|
||||
gem 'nokogiri', '~> 1.14'
|
||||
gem 'nsa', '~> 0.2'
|
||||
gem 'oj', '~> 3.13'
|
||||
gem 'ox', '~> 2.14'
|
||||
|
|
160
Gemfile.lock
160
Gemfile.lock
|
@ -10,40 +10,40 @@ GIT
|
|||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
actioncable (6.1.7)
|
||||
actionpack (= 6.1.7)
|
||||
activesupport (= 6.1.7)
|
||||
actioncable (6.1.7.1)
|
||||
actionpack (= 6.1.7.1)
|
||||
activesupport (= 6.1.7.1)
|
||||
nio4r (~> 2.0)
|
||||
websocket-driver (>= 0.6.1)
|
||||
actionmailbox (6.1.7)
|
||||
actionpack (= 6.1.7)
|
||||
activejob (= 6.1.7)
|
||||
activerecord (= 6.1.7)
|
||||
activestorage (= 6.1.7)
|
||||
activesupport (= 6.1.7)
|
||||
actionmailbox (6.1.7.1)
|
||||
actionpack (= 6.1.7.1)
|
||||
activejob (= 6.1.7.1)
|
||||
activerecord (= 6.1.7.1)
|
||||
activestorage (= 6.1.7.1)
|
||||
activesupport (= 6.1.7.1)
|
||||
mail (>= 2.7.1)
|
||||
actionmailer (6.1.7)
|
||||
actionpack (= 6.1.7)
|
||||
actionview (= 6.1.7)
|
||||
activejob (= 6.1.7)
|
||||
activesupport (= 6.1.7)
|
||||
actionmailer (6.1.7.1)
|
||||
actionpack (= 6.1.7.1)
|
||||
actionview (= 6.1.7.1)
|
||||
activejob (= 6.1.7.1)
|
||||
activesupport (= 6.1.7.1)
|
||||
mail (~> 2.5, >= 2.5.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
actionpack (6.1.7)
|
||||
actionview (= 6.1.7)
|
||||
activesupport (= 6.1.7)
|
||||
actionpack (6.1.7.1)
|
||||
actionview (= 6.1.7.1)
|
||||
activesupport (= 6.1.7.1)
|
||||
rack (~> 2.0, >= 2.0.9)
|
||||
rack-test (>= 0.6.3)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
||||
actiontext (6.1.7)
|
||||
actionpack (= 6.1.7)
|
||||
activerecord (= 6.1.7)
|
||||
activestorage (= 6.1.7)
|
||||
activesupport (= 6.1.7)
|
||||
actiontext (6.1.7.1)
|
||||
actionpack (= 6.1.7.1)
|
||||
activerecord (= 6.1.7.1)
|
||||
activestorage (= 6.1.7.1)
|
||||
activesupport (= 6.1.7.1)
|
||||
nokogiri (>= 1.8.5)
|
||||
actionview (6.1.7)
|
||||
activesupport (= 6.1.7)
|
||||
actionview (6.1.7.1)
|
||||
activesupport (= 6.1.7.1)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
|
@ -54,22 +54,22 @@ GEM
|
|||
case_transform (>= 0.2)
|
||||
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
|
||||
active_record_query_trace (1.8)
|
||||
activejob (6.1.7)
|
||||
activesupport (= 6.1.7)
|
||||
activejob (6.1.7.1)
|
||||
activesupport (= 6.1.7.1)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (6.1.7)
|
||||
activesupport (= 6.1.7)
|
||||
activerecord (6.1.7)
|
||||
activemodel (= 6.1.7)
|
||||
activesupport (= 6.1.7)
|
||||
activestorage (6.1.7)
|
||||
actionpack (= 6.1.7)
|
||||
activejob (= 6.1.7)
|
||||
activerecord (= 6.1.7)
|
||||
activesupport (= 6.1.7)
|
||||
activemodel (6.1.7.1)
|
||||
activesupport (= 6.1.7.1)
|
||||
activerecord (6.1.7.1)
|
||||
activemodel (= 6.1.7.1)
|
||||
activesupport (= 6.1.7.1)
|
||||
activestorage (6.1.7.1)
|
||||
actionpack (= 6.1.7.1)
|
||||
activejob (= 6.1.7.1)
|
||||
activerecord (= 6.1.7.1)
|
||||
activesupport (= 6.1.7.1)
|
||||
marcel (~> 1.0)
|
||||
mini_mime (>= 1.1.0)
|
||||
activesupport (6.1.7)
|
||||
activesupport (6.1.7.1)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
i18n (>= 1.6, < 2)
|
||||
minitest (>= 5.1)
|
||||
|
@ -174,7 +174,7 @@ GEM
|
|||
cocoon (1.2.15)
|
||||
coderay (1.1.3)
|
||||
color_diff (0.1)
|
||||
concurrent-ruby (1.1.10)
|
||||
concurrent-ruby (1.2.0)
|
||||
connection_pool (2.3.0)
|
||||
cose (1.2.1)
|
||||
cbor (~> 0.5.9)
|
||||
|
@ -184,6 +184,7 @@ GEM
|
|||
crass (1.0.6)
|
||||
css_parser (1.12.0)
|
||||
addressable
|
||||
date (3.3.3)
|
||||
debug_inspector (1.0.0)
|
||||
devise (4.8.1)
|
||||
bcrypt (~> 3.0)
|
||||
|
@ -223,7 +224,7 @@ GEM
|
|||
faraday (~> 1)
|
||||
multi_json
|
||||
encryptor (3.0.0)
|
||||
erubi (1.11.0)
|
||||
erubi (1.12.0)
|
||||
et-orbi (1.2.7)
|
||||
tzinfo
|
||||
excon (0.95.0)
|
||||
|
@ -282,7 +283,7 @@ GEM
|
|||
addressable (~> 2.7)
|
||||
omniauth (>= 1.9, < 3)
|
||||
openid_connect (~> 1.2)
|
||||
globalid (1.0.0)
|
||||
globalid (1.0.1)
|
||||
activesupport (>= 5.0)
|
||||
hamlit (2.13.0)
|
||||
temple (>= 0.8.2)
|
||||
|
@ -389,8 +390,11 @@ GEM
|
|||
loofah (2.19.1)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.5.9)
|
||||
mail (2.7.1)
|
||||
mail (2.8.0.1)
|
||||
mini_mime (>= 0.1.1)
|
||||
net-imap
|
||||
net-pop
|
||||
net-smtp
|
||||
makara (0.5.1)
|
||||
activerecord (>= 5.2.0)
|
||||
marcel (1.0.2)
|
||||
|
@ -403,12 +407,17 @@ GEM
|
|||
mime-types-data (~> 3.2015)
|
||||
mime-types-data (3.2022.0105)
|
||||
mini_mime (1.1.2)
|
||||
mini_portile2 (2.8.0)
|
||||
mini_portile2 (2.8.1)
|
||||
minitest (5.17.0)
|
||||
msgpack (1.6.0)
|
||||
multi_json (1.15.0)
|
||||
multipart-post (2.1.1)
|
||||
net-imap (0.3.4)
|
||||
date
|
||||
net-protocol
|
||||
net-ldap (0.17.1)
|
||||
net-pop (0.1.2)
|
||||
net-protocol
|
||||
net-protocol (0.1.3)
|
||||
timeout
|
||||
net-scp (4.0.0.rc1)
|
||||
|
@ -417,7 +426,7 @@ GEM
|
|||
net-protocol
|
||||
net-ssh (7.0.1)
|
||||
nio4r (2.5.8)
|
||||
nokogiri (1.13.10)
|
||||
nokogiri (1.14.0)
|
||||
mini_portile2 (~> 2.8.0)
|
||||
racc (~> 1.4)
|
||||
nsa (0.2.8)
|
||||
|
@ -453,16 +462,16 @@ GEM
|
|||
openssl-signature_algorithm (1.2.1)
|
||||
openssl (> 2.0, < 3.1)
|
||||
orm_adapter (0.5.0)
|
||||
ox (2.14.12)
|
||||
ox (2.14.13)
|
||||
parallel (1.22.1)
|
||||
parser (3.1.3.0)
|
||||
parser (3.2.0.0)
|
||||
ast (~> 2.4.1)
|
||||
parslet (2.0.0)
|
||||
pastel (0.8.0)
|
||||
tty-color (~> 0.5)
|
||||
pg (1.4.5)
|
||||
pghero (2.8.3)
|
||||
activerecord (>= 5)
|
||||
pghero (3.1.0)
|
||||
activerecord (>= 6)
|
||||
pkg-config (1.5.1)
|
||||
posix-spawn (0.3.15)
|
||||
premailer (1.18.0)
|
||||
|
@ -488,8 +497,8 @@ GEM
|
|||
pundit (2.3.0)
|
||||
activesupport (>= 3.0.0)
|
||||
raabro (1.4.0)
|
||||
racc (1.6.1)
|
||||
rack (2.2.5)
|
||||
racc (1.6.2)
|
||||
rack (2.2.6.2)
|
||||
rack-attack (6.6.1)
|
||||
rack (>= 1.0, < 3)
|
||||
rack-cors (1.1.1)
|
||||
|
@ -504,20 +513,20 @@ GEM
|
|||
rack
|
||||
rack-test (2.0.2)
|
||||
rack (>= 1.3)
|
||||
rails (6.1.7)
|
||||
actioncable (= 6.1.7)
|
||||
actionmailbox (= 6.1.7)
|
||||
actionmailer (= 6.1.7)
|
||||
actionpack (= 6.1.7)
|
||||
actiontext (= 6.1.7)
|
||||
actionview (= 6.1.7)
|
||||
activejob (= 6.1.7)
|
||||
activemodel (= 6.1.7)
|
||||
activerecord (= 6.1.7)
|
||||
activestorage (= 6.1.7)
|
||||
activesupport (= 6.1.7)
|
||||
rails (6.1.7.1)
|
||||
actioncable (= 6.1.7.1)
|
||||
actionmailbox (= 6.1.7.1)
|
||||
actionmailer (= 6.1.7.1)
|
||||
actionpack (= 6.1.7.1)
|
||||
actiontext (= 6.1.7.1)
|
||||
actionview (= 6.1.7.1)
|
||||
activejob (= 6.1.7.1)
|
||||
activemodel (= 6.1.7.1)
|
||||
activerecord (= 6.1.7.1)
|
||||
activestorage (= 6.1.7.1)
|
||||
activesupport (= 6.1.7.1)
|
||||
bundler (>= 1.15.0)
|
||||
railties (= 6.1.7)
|
||||
railties (= 6.1.7.1)
|
||||
sprockets-rails (>= 2.0.0)
|
||||
rails-controller-testing (1.0.5)
|
||||
actionpack (>= 5.0.1.rc1)
|
||||
|
@ -533,9 +542,9 @@ GEM
|
|||
railties (>= 6.0.0, < 7)
|
||||
rails-settings-cached (0.6.6)
|
||||
rails (>= 4.2.0)
|
||||
railties (6.1.7)
|
||||
actionpack (= 6.1.7)
|
||||
activesupport (= 6.1.7)
|
||||
railties (6.1.7.1)
|
||||
actionpack (= 6.1.7.1)
|
||||
activesupport (= 6.1.7.1)
|
||||
method_source
|
||||
rake (>= 12.2)
|
||||
thor (~> 1.0)
|
||||
|
@ -551,7 +560,7 @@ GEM
|
|||
redis (>= 4)
|
||||
redlock (1.3.2)
|
||||
redis (>= 3.0.0, < 6.0)
|
||||
regexp_parser (2.6.1)
|
||||
regexp_parser (2.6.2)
|
||||
request_store (1.5.1)
|
||||
rack (>= 1.4)
|
||||
responders (3.0.1)
|
||||
|
@ -586,18 +595,20 @@ GEM
|
|||
rspec-support (3.11.1)
|
||||
rspec_junit_formatter (0.6.0)
|
||||
rspec-core (>= 2, < 4, != 2.12.0)
|
||||
rubocop (1.42.0)
|
||||
rubocop (1.44.0)
|
||||
json (~> 2.3)
|
||||
parallel (~> 1.10)
|
||||
parser (>= 3.1.2.1)
|
||||
parser (>= 3.2.0.0)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
regexp_parser (>= 1.8, < 3.0)
|
||||
rexml (>= 3.2.5, < 4.0)
|
||||
rubocop-ast (>= 1.24.1, < 2.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 1.4.0, < 3.0)
|
||||
unicode-display_width (>= 2.4.0, < 3.0)
|
||||
rubocop-ast (1.24.1)
|
||||
parser (>= 3.1.1.0)
|
||||
rubocop-capybara (2.17.0)
|
||||
rubocop (~> 1.41)
|
||||
rubocop-performance (1.15.2)
|
||||
rubocop (>= 1.7.0, < 2.0)
|
||||
rubocop-ast (>= 0.4.0)
|
||||
|
@ -605,8 +616,9 @@ GEM
|
|||
activesupport (>= 4.2.0)
|
||||
rack (>= 1.1)
|
||||
rubocop (>= 1.33.0, < 2.0)
|
||||
rubocop-rspec (2.16.0)
|
||||
rubocop-rspec (2.18.1)
|
||||
rubocop (~> 1.33)
|
||||
rubocop-capybara (~> 2.17)
|
||||
ruby-progressbar (1.11.0)
|
||||
ruby-saml (1.13.0)
|
||||
nokogiri (>= 1.10.5)
|
||||
|
@ -704,7 +716,7 @@ GEM
|
|||
unf (0.1.4)
|
||||
unf_ext
|
||||
unf_ext (0.0.8.2)
|
||||
unicode-display_width (2.3.0)
|
||||
unicode-display_width (2.4.2)
|
||||
uniform_notifier (1.16.0)
|
||||
validate_email (0.1.6)
|
||||
activemodel (>= 3.0)
|
||||
|
@ -811,7 +823,7 @@ DEPENDENCIES
|
|||
memory_profiler
|
||||
mime-types (~> 3.4.1)
|
||||
net-ldap (~> 0.17)
|
||||
nokogiri (~> 1.13)
|
||||
nokogiri (~> 1.14)
|
||||
nsa (~> 0.2)
|
||||
oj (~> 3.13)
|
||||
omniauth (~> 1.9)
|
||||
|
@ -821,7 +833,7 @@ DEPENDENCIES
|
|||
ox (~> 2.14)
|
||||
parslet
|
||||
pg (~> 1.4)
|
||||
pghero (~> 2.8)
|
||||
pghero
|
||||
pkg-config (~> 1.5)
|
||||
posix-spawn
|
||||
premailer-rails
|
||||
|
@ -831,7 +843,7 @@ DEPENDENCIES
|
|||
public_suffix (~> 5.0)
|
||||
puma (~> 5.6)
|
||||
pundit (~> 2.3)
|
||||
rack (~> 2.2.5)
|
||||
rack (~> 2.2.6)
|
||||
rack-attack (~> 6.6)
|
||||
rack-cors (~> 1.1)
|
||||
rack-test (~> 2.0)
|
||||
|
|
|
@ -21,7 +21,7 @@ module Admin
|
|||
account_action.save!
|
||||
|
||||
if account_action.with_report?
|
||||
redirect_to admin_reports_path
|
||||
redirect_to admin_reports_path, notice: I18n.t('admin.reports.processed_msg', id: params[:report_id])
|
||||
else
|
||||
redirect_to admin_account_path(@account.id)
|
||||
end
|
||||
|
|
|
@ -23,9 +23,7 @@ module Admin
|
|||
@import = Admin::Import.new(import_params)
|
||||
return render :new unless @import.validate
|
||||
|
||||
parse_import_data!(export_headers)
|
||||
|
||||
@data.take(Admin::Import::ROWS_PROCESSING_LIMIT).each do |row|
|
||||
@import.csv_rows.each do |row|
|
||||
domain = row['#domain'].strip
|
||||
next if DomainAllow.allowed?(domain)
|
||||
|
||||
|
|
|
@ -23,24 +23,30 @@ module Admin
|
|||
@import = Admin::Import.new(import_params)
|
||||
return render :new unless @import.validate
|
||||
|
||||
parse_import_data!(export_headers)
|
||||
|
||||
@global_private_comment = I18n.t('admin.export_domain_blocks.import.private_comment_template', source: @import.data_file_name, date: I18n.l(Time.now.utc))
|
||||
|
||||
@form = Form::DomainBlockBatch.new
|
||||
@domain_blocks = @data.take(Admin::Import::ROWS_PROCESSING_LIMIT).filter_map do |row|
|
||||
@domain_blocks = @import.csv_rows.filter_map do |row|
|
||||
domain = row['#domain'].strip
|
||||
next if DomainBlock.rule_for(domain).present?
|
||||
|
||||
domain_block = DomainBlock.new(domain: domain,
|
||||
severity: row['#severity'].strip,
|
||||
reject_media: row['#reject_media'].strip,
|
||||
reject_reports: row['#reject_reports'].strip,
|
||||
severity: row.fetch('#severity', :suspend),
|
||||
reject_media: row.fetch('#reject_media', false),
|
||||
reject_reports: row.fetch('#reject_reports', false),
|
||||
private_comment: @global_private_comment,
|
||||
public_comment: row['#public_comment']&.strip,
|
||||
obfuscate: row['#obfuscate'].strip)
|
||||
public_comment: row['#public_comment'],
|
||||
obfuscate: row.fetch('#obfuscate', false))
|
||||
|
||||
domain_block if domain_block.valid?
|
||||
if domain_block.invalid?
|
||||
flash.now[:alert] = I18n.t('admin.export_domain_blocks.invalid_domain_block', error: domain_block.errors.full_messages.join(', '))
|
||||
next
|
||||
end
|
||||
|
||||
domain_block
|
||||
rescue ArgumentError => e
|
||||
flash.now[:alert] = I18n.t('admin.export_domain_blocks.invalid_domain_block', error: e.message)
|
||||
next
|
||||
end
|
||||
|
||||
@warning_domains = Instance.where(domain: @domain_blocks.map(&:domain)).where('EXISTS (SELECT 1 FROM follows JOIN accounts ON follows.account_id = accounts.id OR follows.target_account_id = accounts.id WHERE accounts.domain = instances.domain)').pluck(:domain)
|
||||
|
|
|
@ -3,6 +3,11 @@
|
|||
class Admin::Reports::ActionsController < Admin::BaseController
|
||||
before_action :set_report
|
||||
|
||||
def preview
|
||||
authorize @report, :show?
|
||||
@moderation_action = action_from_button
|
||||
end
|
||||
|
||||
def create
|
||||
authorize @report, :show?
|
||||
|
||||
|
@ -13,7 +18,8 @@ class Admin::Reports::ActionsController < Admin::BaseController
|
|||
status_ids: @report.status_ids,
|
||||
current_account: current_account,
|
||||
report_id: @report.id,
|
||||
send_email_notification: !@report.spam?
|
||||
send_email_notification: !@report.spam?,
|
||||
text: params[:text]
|
||||
)
|
||||
|
||||
status_batch_action.save!
|
||||
|
@ -23,13 +29,16 @@ class Admin::Reports::ActionsController < Admin::BaseController
|
|||
report_id: @report.id,
|
||||
target_account: @report.target_account,
|
||||
current_account: current_account,
|
||||
send_email_notification: !@report.spam?
|
||||
send_email_notification: !@report.spam?,
|
||||
text: params[:text]
|
||||
)
|
||||
|
||||
account_action.save!
|
||||
else
|
||||
return redirect_to admin_report_path(@report), alert: I18n.t('admin.reports.unknown_action_msg', action: action_from_button)
|
||||
end
|
||||
|
||||
redirect_to admin_reports_path
|
||||
redirect_to admin_reports_path, notice: I18n.t('admin.reports.processed_msg', id: @report.id)
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -47,6 +56,8 @@ class Admin::Reports::ActionsController < Admin::BaseController
|
|||
'silence'
|
||||
elsif params[:suspend]
|
||||
'suspend'
|
||||
elsif params[:moderation_action]
|
||||
params[:moderation_action]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -21,7 +21,17 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController
|
|||
private
|
||||
|
||||
def account_params
|
||||
params.permit(:display_name, :note, :avatar, :header, :locked, :bot, :discoverable, fields_attributes: [:name, :value])
|
||||
params.permit(
|
||||
:display_name,
|
||||
:note,
|
||||
:avatar,
|
||||
:header,
|
||||
:locked,
|
||||
:bot,
|
||||
:discoverable,
|
||||
:hide_collections,
|
||||
fields_attributes: [:name, :value]
|
||||
)
|
||||
end
|
||||
|
||||
def user_settings_params
|
||||
|
|
|
@ -3,6 +3,14 @@
|
|||
class Api::V1::Admin::Trends::TagsController < Api::V1::Trends::TagsController
|
||||
before_action -> { authorize_if_got_token! :'admin:read' }
|
||||
|
||||
def index
|
||||
if current_user&.can?(:manage_taxonomies)
|
||||
render json: @tags, each_serializer: REST::Admin::TagSerializer
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def enabled?
|
||||
|
|
|
@ -80,6 +80,7 @@ class Api::V1::StatusesController < Api::BaseController
|
|||
current_account.id,
|
||||
text: status_params[:status],
|
||||
media_ids: status_params[:media_ids],
|
||||
media_attributes: status_params[:media_attributes],
|
||||
sensitive: status_params[:sensitive],
|
||||
language: status_params[:language],
|
||||
spoiler_text: status_params[:spoiler_text],
|
||||
|
@ -131,6 +132,12 @@ class Api::V1::StatusesController < Api::BaseController
|
|||
:scheduled_at,
|
||||
:content_type,
|
||||
media_ids: [],
|
||||
media_attributes: [
|
||||
:id,
|
||||
:thumbnail,
|
||||
:description,
|
||||
:focus,
|
||||
],
|
||||
poll: [
|
||||
:multiple,
|
||||
:hide_totals,
|
||||
|
|
|
@ -26,14 +26,4 @@ module AdminExportControllerConcern
|
|||
def import_params
|
||||
params.require(:admin_import).permit(:data)
|
||||
end
|
||||
|
||||
def import_data_path
|
||||
params[:admin_import][:data].path
|
||||
end
|
||||
|
||||
def parse_import_data!(default_headers)
|
||||
data = CSV.read(import_data_path, headers: true, encoding: 'UTF-8')
|
||||
data = CSV.read(import_data_path, headers: default_headers, encoding: 'UTF-8') unless data.headers&.first&.strip&.include?(default_headers[0])
|
||||
@data = data.reject(&:blank?)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -46,11 +46,11 @@ module SignatureVerification
|
|||
end
|
||||
|
||||
def require_account_signature!
|
||||
render plain: signature_verification_failure_reason, status: signature_verification_failure_code unless signed_request_account
|
||||
render json: signature_verification_failure_reason, status: signature_verification_failure_code unless signed_request_account
|
||||
end
|
||||
|
||||
def require_actor_signature!
|
||||
render plain: signature_verification_failure_reason, status: signature_verification_failure_code unless signed_request_actor
|
||||
render json: signature_verification_failure_reason, status: signature_verification_failure_code unless signed_request_actor
|
||||
end
|
||||
|
||||
def signed_request?
|
||||
|
@ -97,11 +97,11 @@ module SignatureVerification
|
|||
|
||||
actor = stoplight_wrap_request { actor_refresh_key!(actor) }
|
||||
|
||||
raise SignatureVerificationError, "Public key not found for key #{signature_params['keyId']}" if actor.nil?
|
||||
raise SignatureVerificationError, "Could not refresh public key #{signature_params['keyId']}" if actor.nil?
|
||||
|
||||
return actor unless verify_signature(actor, signature, compare_signed_string).nil?
|
||||
|
||||
fail_with! "Verification failed for #{actor.to_log_human_identifier} #{actor.uri} using rsa-sha256 (RSASSA-PKCS1-v1_5 with SHA-256)"
|
||||
fail_with! "Verification failed for #{actor.to_log_human_identifier} #{actor.uri} using rsa-sha256 (RSASSA-PKCS1-v1_5 with SHA-256)", signed_string: compare_signed_string, signature: signature_params['signature']
|
||||
rescue SignatureVerificationError => e
|
||||
fail_with! e.message
|
||||
rescue HTTP::Error, OpenSSL::SSL::SSLError => e
|
||||
|
@ -118,8 +118,8 @@ module SignatureVerification
|
|||
|
||||
private
|
||||
|
||||
def fail_with!(message)
|
||||
@signature_verification_failure_reason = message
|
||||
def fail_with!(message, **options)
|
||||
@signature_verification_failure_reason = { error: message }.merge(options)
|
||||
@signed_request_actor = nil
|
||||
end
|
||||
|
||||
|
@ -209,8 +209,8 @@ module SignatureVerification
|
|||
end
|
||||
|
||||
expires_time = Time.at(signature_params['expires'].to_i).utc if signature_params['expires'].present?
|
||||
rescue ArgumentError
|
||||
return false
|
||||
rescue ArgumentError => e
|
||||
raise SignatureVerificationError, "Invalid Date header: #{e.message}"
|
||||
end
|
||||
|
||||
expires_time ||= created_time + 5.minutes unless created_time.nil?
|
||||
|
|
|
@ -7,17 +7,12 @@ module WebAppControllerConcern
|
|||
prepend_before_action :redirect_unauthenticated_to_permalinks!
|
||||
before_action :set_pack
|
||||
before_action :set_app_body_class
|
||||
before_action :set_referrer_policy_header
|
||||
end
|
||||
|
||||
def set_app_body_class
|
||||
@body_classes = 'app-body'
|
||||
end
|
||||
|
||||
def set_referrer_policy_header
|
||||
response.headers['Referrer-Policy'] = 'origin'
|
||||
end
|
||||
|
||||
def redirect_unauthenticated_to_permalinks!
|
||||
return if user_signed_in? # NOTE: Different from upstream because we allow moved users to log in
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ module Admin::ActionLogsHelper
|
|||
when 'Status'
|
||||
link_to log.human_identifier, log.permalink
|
||||
when 'AccountWarning'
|
||||
link_to log.human_identifier, admin_account_path(log.target_id)
|
||||
link_to log.human_identifier, disputes_strike_path(log.target_id)
|
||||
when 'Announcement'
|
||||
link_to truncate(log.human_identifier), edit_admin_announcement_path(log.target_id)
|
||||
when 'IpBlock', 'Instance', 'CustomEmoji'
|
||||
|
|
|
@ -181,6 +181,18 @@ export function submitCompose(routerHistory) {
|
|||
|
||||
dispatch(submitComposeRequest());
|
||||
|
||||
// If we're editing a post with media attachments, those have not
|
||||
// necessarily been changed on the server. Do it now in the same
|
||||
// API call.
|
||||
let media_attributes;
|
||||
if (statusId !== null) {
|
||||
media_attributes = media.map(item => ({
|
||||
id: item.get('id'),
|
||||
description: item.get('description'),
|
||||
focus: item.get('focus'),
|
||||
}));
|
||||
}
|
||||
|
||||
api(getState).request({
|
||||
url: statusId === null ? '/api/v1/statuses' : `/api/v1/statuses/${statusId}`,
|
||||
method: statusId === null ? 'post' : 'put',
|
||||
|
@ -189,6 +201,7 @@ export function submitCompose(routerHistory) {
|
|||
content_type: getState().getIn(['compose', 'content_type']),
|
||||
in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null),
|
||||
media_ids: media.map(item => item.get('id')),
|
||||
media_attributes,
|
||||
sensitive: getState().getIn(['compose', 'sensitive']) || (spoilerText.length > 0 && media.size !== 0),
|
||||
spoiler_text: spoilerText,
|
||||
visibility: getState().getIn(['compose', 'privacy']),
|
||||
|
@ -415,11 +428,31 @@ export function changeUploadCompose(id, params) {
|
|||
return (dispatch, getState) => {
|
||||
dispatch(changeUploadComposeRequest());
|
||||
|
||||
api(getState).put(`/api/v1/media/${id}`, params).then(response => {
|
||||
dispatch(changeUploadComposeSuccess(response.data));
|
||||
}).catch(error => {
|
||||
dispatch(changeUploadComposeFail(id, error));
|
||||
});
|
||||
let media = getState().getIn(['compose', 'media_attachments']).find((item) => item.get('id') === id);
|
||||
|
||||
// Editing already-attached media is deferred to editing the post itself.
|
||||
// For simplicity's sake, fake an API reply.
|
||||
if (media && !media.get('unattached')) {
|
||||
let { description, focus } = params;
|
||||
const data = media.toJS();
|
||||
|
||||
if (description) {
|
||||
data.description = description;
|
||||
}
|
||||
|
||||
if (focus) {
|
||||
focus = focus.split(',');
|
||||
data.meta = { focus: { x: parseFloat(focus[0]), y: parseFloat(focus[1]) } };
|
||||
}
|
||||
|
||||
dispatch(changeUploadComposeSuccess(data, true));
|
||||
} else {
|
||||
api(getState).put(`/api/v1/media/${id}`, params).then(response => {
|
||||
dispatch(changeUploadComposeSuccess(response.data, false));
|
||||
}).catch(error => {
|
||||
dispatch(changeUploadComposeFail(id, error));
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -430,10 +463,11 @@ export function changeUploadComposeRequest() {
|
|||
};
|
||||
};
|
||||
|
||||
export function changeUploadComposeSuccess(media) {
|
||||
export function changeUploadComposeSuccess(media, attached) {
|
||||
return {
|
||||
type: COMPOSE_UPLOAD_CHANGE_SUCCESS,
|
||||
media: media,
|
||||
attached: attached,
|
||||
skipLoading: true,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
export const DROPDOWN_MENU_OPEN = 'DROPDOWN_MENU_OPEN';
|
||||
export const DROPDOWN_MENU_CLOSE = 'DROPDOWN_MENU_CLOSE';
|
||||
|
||||
export function openDropdownMenu(id, placement, keyboard, scroll_key) {
|
||||
return { type: DROPDOWN_MENU_OPEN, id, placement, keyboard, scroll_key };
|
||||
export function openDropdownMenu(id, keyboard, scroll_key) {
|
||||
return { type: DROPDOWN_MENU_OPEN, id, keyboard, scroll_key };
|
||||
}
|
||||
|
||||
export function closeDropdownMenu(id) {
|
||||
|
|
|
@ -1,9 +1,17 @@
|
|||
import api from '../api';
|
||||
import api, { getLinks } from '../api';
|
||||
|
||||
export const HASHTAG_FETCH_REQUEST = 'HASHTAG_FETCH_REQUEST';
|
||||
export const HASHTAG_FETCH_SUCCESS = 'HASHTAG_FETCH_SUCCESS';
|
||||
export const HASHTAG_FETCH_FAIL = 'HASHTAG_FETCH_FAIL';
|
||||
|
||||
export const FOLLOWED_HASHTAGS_FETCH_REQUEST = 'FOLLOWED_HASHTAGS_FETCH_REQUEST';
|
||||
export const FOLLOWED_HASHTAGS_FETCH_SUCCESS = 'FOLLOWED_HASHTAGS_FETCH_SUCCESS';
|
||||
export const FOLLOWED_HASHTAGS_FETCH_FAIL = 'FOLLOWED_HASHTAGS_FETCH_FAIL';
|
||||
|
||||
export const FOLLOWED_HASHTAGS_EXPAND_REQUEST = 'FOLLOWED_HASHTAGS_EXPAND_REQUEST';
|
||||
export const FOLLOWED_HASHTAGS_EXPAND_SUCCESS = 'FOLLOWED_HASHTAGS_EXPAND_SUCCESS';
|
||||
export const FOLLOWED_HASHTAGS_EXPAND_FAIL = 'FOLLOWED_HASHTAGS_EXPAND_FAIL';
|
||||
|
||||
export const HASHTAG_FOLLOW_REQUEST = 'HASHTAG_FOLLOW_REQUEST';
|
||||
export const HASHTAG_FOLLOW_SUCCESS = 'HASHTAG_FOLLOW_SUCCESS';
|
||||
export const HASHTAG_FOLLOW_FAIL = 'HASHTAG_FOLLOW_FAIL';
|
||||
|
@ -37,6 +45,78 @@ export const fetchHashtagFail = error => ({
|
|||
error,
|
||||
});
|
||||
|
||||
export const fetchFollowedHashtags = () => (dispatch, getState) => {
|
||||
dispatch(fetchFollowedHashtagsRequest());
|
||||
|
||||
api(getState).get('/api/v1/followed_tags').then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(fetchFollowedHashtagsSuccess(response.data, next ? next.uri : null));
|
||||
}).catch(err => {
|
||||
dispatch(fetchFollowedHashtagsFail(err));
|
||||
});
|
||||
};
|
||||
|
||||
export function fetchFollowedHashtagsRequest() {
|
||||
return {
|
||||
type: FOLLOWED_HASHTAGS_FETCH_REQUEST,
|
||||
};
|
||||
};
|
||||
|
||||
export function fetchFollowedHashtagsSuccess(followed_tags, next) {
|
||||
return {
|
||||
type: FOLLOWED_HASHTAGS_FETCH_SUCCESS,
|
||||
followed_tags,
|
||||
next,
|
||||
};
|
||||
};
|
||||
|
||||
export function fetchFollowedHashtagsFail(error) {
|
||||
return {
|
||||
type: FOLLOWED_HASHTAGS_FETCH_FAIL,
|
||||
error,
|
||||
};
|
||||
};
|
||||
|
||||
export function expandFollowedHashtags() {
|
||||
return (dispatch, getState) => {
|
||||
const url = getState().getIn(['followed_tags', 'next']);
|
||||
|
||||
if (url === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(expandFollowedHashtagsRequest());
|
||||
|
||||
api(getState).get(url).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(expandFollowedHashtagsSuccess(response.data, next ? next.uri : null));
|
||||
}).catch(error => {
|
||||
dispatch(expandFollowedHashtagsFail(error));
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
export function expandFollowedHashtagsRequest() {
|
||||
return {
|
||||
type: FOLLOWED_HASHTAGS_EXPAND_REQUEST,
|
||||
};
|
||||
};
|
||||
|
||||
export function expandFollowedHashtagsSuccess(followed_tags, next) {
|
||||
return {
|
||||
type: FOLLOWED_HASHTAGS_EXPAND_SUCCESS,
|
||||
followed_tags,
|
||||
next,
|
||||
};
|
||||
};
|
||||
|
||||
export function expandFollowedHashtagsFail(error) {
|
||||
return {
|
||||
type: FOLLOWED_HASHTAGS_EXPAND_FAIL,
|
||||
error,
|
||||
};
|
||||
};
|
||||
|
||||
export const followHashtag = name => (dispatch, getState) => {
|
||||
dispatch(followHashtagRequest(name));
|
||||
|
||||
|
|
|
@ -50,7 +50,7 @@ export default class Trends extends React.PureComponent {
|
|||
<Hashtag
|
||||
key={hashtag.name}
|
||||
name={hashtag.name}
|
||||
href={`/admin/tags/${hashtag.id}`}
|
||||
href={hashtag.id === undefined ? undefined : `/admin/tags/${hashtag.id}`}
|
||||
people={hashtag.history[0].accounts * 1 + hashtag.history[1].accounts * 1}
|
||||
uses={hashtag.history[0].uses * 1 + hashtag.history[1].uses * 1}
|
||||
history={hashtag.history.reverse().map(day => day.uses)}
|
||||
|
|
|
@ -50,6 +50,7 @@ export default class AutosuggestInput extends ImmutablePureComponent {
|
|||
id: PropTypes.string,
|
||||
searchTokens: PropTypes.arrayOf(PropTypes.string),
|
||||
maxLength: PropTypes.number,
|
||||
lang: PropTypes.string,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
|
@ -185,7 +186,7 @@ export default class AutosuggestInput extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
render () {
|
||||
const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, className, id, maxLength } = this.props;
|
||||
const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, className, id, maxLength, lang } = this.props;
|
||||
const { suggestionsHidden } = this.state;
|
||||
|
||||
return (
|
||||
|
@ -210,6 +211,7 @@ export default class AutosuggestInput extends ImmutablePureComponent {
|
|||
id={id}
|
||||
className={className}
|
||||
maxLength={maxLength}
|
||||
lang={lang}
|
||||
/>
|
||||
</label>
|
||||
|
||||
|
|
|
@ -48,6 +48,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
|
|||
onKeyDown: PropTypes.func,
|
||||
onPaste: PropTypes.func.isRequired,
|
||||
autoFocus: PropTypes.bool,
|
||||
lang: PropTypes.string,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
|
@ -192,7 +193,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
render () {
|
||||
const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, children } = this.props;
|
||||
const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, lang, children } = this.props;
|
||||
const { suggestionsHidden } = this.state;
|
||||
|
||||
return [
|
||||
|
@ -216,6 +217,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
|
|||
onPaste={this.onPaste}
|
||||
dir='auto'
|
||||
aria-autocomplete='list'
|
||||
lang={lang}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
|
|
@ -2,9 +2,7 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import IconButton from './icon_button';
|
||||
import Overlay from 'react-overlays/lib/Overlay';
|
||||
import Motion from '../features/ui/util/optional_motion';
|
||||
import spring from 'react-motion/lib/spring';
|
||||
import Overlay from 'react-overlays/Overlay';
|
||||
import { supportsPassiveEvents } from 'detect-passive-events';
|
||||
import classNames from 'classnames';
|
||||
import { CircularProgress } from 'flavours/glitch/components/loading_indicator';
|
||||
|
@ -24,9 +22,6 @@ class DropdownMenu extends React.PureComponent {
|
|||
scrollable: PropTypes.bool,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
style: PropTypes.object,
|
||||
placement: PropTypes.string,
|
||||
arrowOffsetLeft: PropTypes.string,
|
||||
arrowOffsetTop: PropTypes.string,
|
||||
openedViaKeyboard: PropTypes.bool,
|
||||
renderItem: PropTypes.func,
|
||||
renderHeader: PropTypes.func,
|
||||
|
@ -35,11 +30,6 @@ class DropdownMenu extends React.PureComponent {
|
|||
|
||||
static defaultProps = {
|
||||
style: {},
|
||||
placement: 'bottom',
|
||||
};
|
||||
|
||||
state = {
|
||||
mounted: false,
|
||||
};
|
||||
|
||||
handleDocumentClick = e => {
|
||||
|
@ -56,8 +46,6 @@ class DropdownMenu extends React.PureComponent {
|
|||
if (this.focusedItem && this.props.openedViaKeyboard) {
|
||||
this.focusedItem.focus({ preventScroll: true });
|
||||
}
|
||||
|
||||
this.setState({ mounted: true });
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
|
@ -139,40 +127,28 @@ class DropdownMenu extends React.PureComponent {
|
|||
}
|
||||
|
||||
render () {
|
||||
const { items, style, placement, arrowOffsetLeft, arrowOffsetTop, scrollable, renderHeader, loading } = this.props;
|
||||
const { mounted } = this.state;
|
||||
const { items, scrollable, renderHeader, loading } = this.props;
|
||||
|
||||
let renderItem = this.props.renderItem || this.renderItem;
|
||||
|
||||
return (
|
||||
<Motion defaultStyle={{ opacity: 0, scaleX: 0.85, scaleY: 0.75 }} style={{ opacity: spring(1, { damping: 35, stiffness: 400 }), scaleX: spring(1, { damping: 35, stiffness: 400 }), scaleY: spring(1, { damping: 35, stiffness: 400 }) }}>
|
||||
{({ opacity, scaleX, scaleY }) => (
|
||||
// It should not be transformed when mounting because the resulting
|
||||
// size will be used to determine the coordinate of the menu by
|
||||
// react-overlays
|
||||
<div className={`dropdown-menu ${placement}`} style={{ ...style, opacity: opacity, transform: mounted ? `scale(${scaleX}, ${scaleY})` : null }} ref={this.setRef}>
|
||||
<div className={`dropdown-menu__arrow ${placement}`} style={{ left: arrowOffsetLeft, top: arrowOffsetTop }} />
|
||||
<div className={classNames('dropdown-menu__container', { 'dropdown-menu__container--loading': loading })} ref={this.setRef}>
|
||||
{loading && (
|
||||
<CircularProgress size={30} strokeWidth={3.5} />
|
||||
)}
|
||||
|
||||
<div className={classNames('dropdown-menu__container', { 'dropdown-menu__container--loading': loading })}>
|
||||
{loading && (
|
||||
<CircularProgress size={30} strokeWidth={3.5} />
|
||||
)}
|
||||
|
||||
{!loading && renderHeader && (
|
||||
<div className='dropdown-menu__container__header'>
|
||||
{renderHeader(items)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && (
|
||||
<ul className={classNames('dropdown-menu__container__list', { 'dropdown-menu__container__list--scrollable': scrollable })}>
|
||||
{items.map((option, i) => renderItem(option, i, { onClick: this.handleClick, onKeyPress: this.handleItemKeyPress }))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
{!loading && renderHeader && (
|
||||
<div className='dropdown-menu__container__header'>
|
||||
{renderHeader(items)}
|
||||
</div>
|
||||
)}
|
||||
</Motion>
|
||||
|
||||
{!loading && (
|
||||
<ul className={classNames('dropdown-menu__container__list', { 'dropdown-menu__container__list--scrollable': scrollable })}>
|
||||
{items.map((option, i) => renderItem(option, i, { onClick: this.handleClick, onKeyPress: this.handleItemKeyPress }))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -197,7 +173,6 @@ export default class Dropdown extends React.PureComponent {
|
|||
isUserTouching: PropTypes.func,
|
||||
onOpen: PropTypes.func.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
dropdownPlacement: PropTypes.string,
|
||||
openDropdownId: PropTypes.number,
|
||||
openedViaKeyboard: PropTypes.bool,
|
||||
renderItem: PropTypes.func,
|
||||
|
@ -213,13 +188,11 @@ export default class Dropdown extends React.PureComponent {
|
|||
id: id++,
|
||||
};
|
||||
|
||||
handleClick = ({ target, type }) => {
|
||||
handleClick = ({ type }) => {
|
||||
if (this.state.id === this.props.openDropdownId) {
|
||||
this.handleClose();
|
||||
} else {
|
||||
const { top } = target.getBoundingClientRect();
|
||||
const placement = top * 2 < innerHeight ? 'bottom' : 'top';
|
||||
this.props.onOpen(this.state.id, this.handleItemClick, placement, type !== 'click');
|
||||
this.props.onOpen(this.state.id, this.handleItemClick, type !== 'click');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -303,7 +276,6 @@ export default class Dropdown extends React.PureComponent {
|
|||
disabled,
|
||||
loading,
|
||||
scrollable,
|
||||
dropdownPlacement,
|
||||
openDropdownId,
|
||||
openedViaKeyboard,
|
||||
children,
|
||||
|
@ -314,7 +286,6 @@ export default class Dropdown extends React.PureComponent {
|
|||
const open = this.state.id === openDropdownId;
|
||||
|
||||
const button = children ? React.cloneElement(React.Children.only(children), {
|
||||
ref: this.setTargetRef,
|
||||
onClick: this.handleClick,
|
||||
onMouseDown: this.handleMouseDown,
|
||||
onKeyDown: this.handleButtonKeyDown,
|
||||
|
@ -326,7 +297,6 @@ export default class Dropdown extends React.PureComponent {
|
|||
active={open}
|
||||
disabled={disabled}
|
||||
size={size}
|
||||
ref={this.setTargetRef}
|
||||
onClick={this.handleClick}
|
||||
onMouseDown={this.handleMouseDown}
|
||||
onKeyDown={this.handleButtonKeyDown}
|
||||
|
@ -336,19 +306,27 @@ export default class Dropdown extends React.PureComponent {
|
|||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{button}
|
||||
|
||||
<Overlay show={open} placement={dropdownPlacement} target={this.findTarget}>
|
||||
<DropdownMenu
|
||||
items={items}
|
||||
loading={loading}
|
||||
scrollable={scrollable}
|
||||
onClose={this.handleClose}
|
||||
openedViaKeyboard={openedViaKeyboard}
|
||||
renderItem={renderItem}
|
||||
renderHeader={renderHeader}
|
||||
onItemClick={this.handleItemClick}
|
||||
/>
|
||||
<span ref={this.setTargetRef}>
|
||||
{button}
|
||||
</span>
|
||||
<Overlay show={open} offset={[5, 5]} placement={'bottom'} flip target={this.findTarget} popperConfig={{ strategy: 'fixed' }}>
|
||||
{({ props, arrowProps, placement }) => (
|
||||
<div {...props}>
|
||||
<div className={`dropdown-animation dropdown-menu ${placement}`}>
|
||||
<div className={`dropdown-menu__arrow ${placement}`} {...arrowProps} />
|
||||
<DropdownMenu
|
||||
items={items}
|
||||
loading={loading}
|
||||
scrollable={scrollable}
|
||||
onClose={this.handleClose}
|
||||
openedViaKeyboard={openedViaKeyboard}
|
||||
renderItem={renderItem}
|
||||
renderHeader={renderHeader}
|
||||
onItemClick={this.handleItemClick}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Overlay>
|
||||
</React.Fragment>
|
||||
);
|
||||
|
|
|
@ -4,7 +4,6 @@ import { fetchHistory } from 'flavours/glitch/actions/history';
|
|||
import DropdownMenu from 'flavours/glitch/components/dropdown_menu';
|
||||
|
||||
const mapStateToProps = (state, { statusId }) => ({
|
||||
dropdownPlacement: state.getIn(['dropdown_menu', 'placement']),
|
||||
openDropdownId: state.getIn(['dropdown_menu', 'openId']),
|
||||
openedViaKeyboard: state.getIn(['dropdown_menu', 'keyboard']),
|
||||
items: state.getIn(['history', statusId, 'items']),
|
||||
|
@ -13,9 +12,9 @@ const mapStateToProps = (state, { statusId }) => ({
|
|||
|
||||
const mapDispatchToProps = (dispatch, { statusId }) => ({
|
||||
|
||||
onOpen (id, onItemClick, dropdownPlacement, keyboard) {
|
||||
onOpen (id, onItemClick, keyboard) {
|
||||
dispatch(fetchHistory(statusId));
|
||||
dispatch(openDropdownMenu(id, dropdownPlacement, keyboard));
|
||||
dispatch(openDropdownMenu(id, keyboard));
|
||||
},
|
||||
|
||||
onClose (id) {
|
||||
|
|
|
@ -5,18 +5,17 @@ import DropdownMenu from 'flavours/glitch/components/dropdown_menu';
|
|||
import { isUserTouching } from '../is_mobile';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
dropdownPlacement: state.getIn(['dropdown_menu', 'placement']),
|
||||
openDropdownId: state.getIn(['dropdown_menu', 'openId']),
|
||||
openedViaKeyboard: state.getIn(['dropdown_menu', 'keyboard']),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch, { status, items, scrollKey }) => ({
|
||||
onOpen(id, onItemClick, dropdownPlacement, keyboard) {
|
||||
onOpen(id, onItemClick, keyboard) {
|
||||
dispatch(isUserTouching() ? openModal('ACTIONS', {
|
||||
status,
|
||||
actions: items,
|
||||
onClick: onItemClick,
|
||||
}) : openDropdownMenu(id, dropdownPlacement, keyboard, scrollKey));
|
||||
}) : openDropdownMenu(id, keyboard, scrollKey));
|
||||
},
|
||||
|
||||
onClose(id) {
|
||||
|
|
|
@ -45,6 +45,7 @@ const messages = defineMessages({
|
|||
follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
|
||||
favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' },
|
||||
lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' },
|
||||
followed_tags: { id: 'navigation_bar.followed_tags', defaultMessage: 'Followed hashtags' },
|
||||
blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' },
|
||||
domain_blocks: { id: 'navigation_bar.domain_blocks', defaultMessage: 'Blocked domains' },
|
||||
mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' },
|
||||
|
@ -188,7 +189,7 @@ class Header extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
if (account.getIn(['relationship', 'requested']) || account.getIn(['relationship', 'following'])) {
|
||||
bellBtn = <IconButton icon='bell-o' size={24} active={account.getIn(['relationship', 'notifying'])} title={intl.formatMessage(account.getIn(['relationship', 'notifying']) ? messages.disableNotifications : messages.enableNotifications, { name: account.get('username') })} onClick={this.props.onNotifyToggle} />;
|
||||
bellBtn = <IconButton icon={account.getIn(['relationship', 'notifying']) ? 'bell' : 'bell-o'} size={24} active={account.getIn(['relationship', 'notifying'])} title={intl.formatMessage(account.getIn(['relationship', 'notifying']) ? messages.disableNotifications : messages.enableNotifications, { name: account.get('username') })} onClick={this.props.onNotifyToggle} />;
|
||||
}
|
||||
|
||||
if (me !== account.get('id')) {
|
||||
|
@ -245,6 +246,7 @@ class Header extends ImmutablePureComponent {
|
|||
menu.push({ text: intl.formatMessage(messages.follow_requests), to: '/follow_requests' });
|
||||
menu.push({ text: intl.formatMessage(messages.favourites), to: '/favourites' });
|
||||
menu.push({ text: intl.formatMessage(messages.lists), to: '/lists' });
|
||||
menu.push({ text: intl.formatMessage(messages.followed_tags), to: '/followed_tags' });
|
||||
menu.push(null);
|
||||
menu.push({ text: intl.formatMessage(messages.mutes), to: '/mutes' });
|
||||
menu.push({ text: intl.formatMessage(messages.blocks), to: '/blocks' });
|
||||
|
|
|
@ -59,7 +59,7 @@ class Audio extends React.PureComponent {
|
|||
duration: null,
|
||||
paused: true,
|
||||
muted: false,
|
||||
volume: 0.5,
|
||||
volume: 1,
|
||||
dragging: false,
|
||||
revealed: this.props.visible !== undefined ? this.props.visible : (displayMedia !== 'hide_all' && !this.props.sensitive || displayMedia === 'show_all'),
|
||||
};
|
||||
|
@ -80,8 +80,8 @@ class Audio extends React.PureComponent {
|
|||
_pack() {
|
||||
return {
|
||||
src: this.props.src,
|
||||
volume: this.audio.volume,
|
||||
muted: this.audio.muted,
|
||||
volume: this.state.volume,
|
||||
muted: this.state.muted,
|
||||
currentTime: this.audio.currentTime,
|
||||
poster: this.props.poster,
|
||||
backgroundColor: this.props.backgroundColor,
|
||||
|
@ -117,7 +117,8 @@ class Audio extends React.PureComponent {
|
|||
this.audio = c;
|
||||
|
||||
if (this.audio) {
|
||||
this.setState({ volume: this.audio.volume, muted: this.audio.muted });
|
||||
this.audio.volume = 1;
|
||||
this.audio.muted = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -208,7 +209,9 @@ class Audio extends React.PureComponent {
|
|||
const muted = !this.state.muted;
|
||||
|
||||
this.setState({ muted }, () => {
|
||||
this.audio.muted = muted;
|
||||
if (this.gainNode) {
|
||||
this.gainNode.gain.value = muted ? 0 : this.state.volume;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -286,7 +289,9 @@ class Audio extends React.PureComponent {
|
|||
|
||||
if(!isNaN(x)) {
|
||||
this.setState({ volume: x }, () => {
|
||||
this.audio.volume = x;
|
||||
if (this.gainNode) {
|
||||
this.gainNode.gain.value = this.state.muted ? 0 : x;
|
||||
}
|
||||
});
|
||||
}
|
||||
}, 15);
|
||||
|
@ -319,20 +324,12 @@ class Audio extends React.PureComponent {
|
|||
}
|
||||
|
||||
handleLoadedData = () => {
|
||||
const { autoPlay, currentTime, volume, muted } = this.props;
|
||||
const { autoPlay, currentTime } = this.props;
|
||||
|
||||
if (currentTime) {
|
||||
this.audio.currentTime = currentTime;
|
||||
}
|
||||
|
||||
if (volume !== undefined) {
|
||||
this.audio.volume = volume;
|
||||
}
|
||||
|
||||
if (muted !== undefined) {
|
||||
this.audio.muted = muted;
|
||||
}
|
||||
|
||||
if (autoPlay) {
|
||||
this.togglePlay();
|
||||
}
|
||||
|
@ -342,11 +339,16 @@ class Audio extends React.PureComponent {
|
|||
const AudioContext = window.AudioContext || window.webkitAudioContext;
|
||||
const context = new AudioContext();
|
||||
const source = context.createMediaElementSource(this.audio);
|
||||
const gainNode = context.createGain();
|
||||
|
||||
gainNode.gain.value = this.state.muted ? 0 : this.state.volume;
|
||||
|
||||
this.visualizer.setAudioContext(context, source);
|
||||
source.connect(context.destination);
|
||||
source.connect(gainNode);
|
||||
gainNode.connect(context.destination);
|
||||
|
||||
this.audioContext = context;
|
||||
this.gainNode = gainNode;
|
||||
}
|
||||
|
||||
handleDownload = () => {
|
||||
|
|
|
@ -12,6 +12,7 @@ const messages = defineMessages({
|
|||
follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
|
||||
favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' },
|
||||
lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' },
|
||||
followed_tags: { id: 'navigation_bar.followed_tags', defaultMessage: 'Followed hashtags' },
|
||||
blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' },
|
||||
domain_blocks: { id: 'navigation_bar.domain_blocks', defaultMessage: 'Hidden domains' },
|
||||
mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' },
|
||||
|
@ -46,6 +47,7 @@ class ActionBar extends React.PureComponent {
|
|||
menu.push({ text: intl.formatMessage(messages.favourites), to: '/favourites' });
|
||||
menu.push({ text: intl.formatMessage(messages.bookmarks), to: '/bookmarks' });
|
||||
menu.push({ text: intl.formatMessage(messages.lists), to: '/lists' });
|
||||
menu.push({ text: intl.formatMessage(messages.followed_tags), to: '/followed_tags' });
|
||||
menu.push(null);
|
||||
menu.push({ text: intl.formatMessage(messages.mutes), to: '/mutes' });
|
||||
menu.push({ text: intl.formatMessage(messages.blocks), to: '/blocks' });
|
||||
|
|
|
@ -61,6 +61,7 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
anyMedia: PropTypes.bool,
|
||||
isInReply: PropTypes.bool,
|
||||
singleColumn: PropTypes.bool,
|
||||
lang: PropTypes.string,
|
||||
|
||||
advancedOptions: ImmutablePropTypes.map,
|
||||
layout: PropTypes.string,
|
||||
|
@ -325,6 +326,7 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
searchTokens={[':']}
|
||||
id='glitch.composer.spoiler.input'
|
||||
className='spoiler-input__input'
|
||||
lang={this.props.lang}
|
||||
autoFocus={false}
|
||||
/>
|
||||
</div>
|
||||
|
@ -343,6 +345,7 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
onSuggestionSelected={this.onSuggestionSelected}
|
||||
onPaste={onPaste}
|
||||
autoFocus={!showSearch && !isMobile(window.innerWidth, layout)}
|
||||
lang={this.props.lang}
|
||||
>
|
||||
<EmojiPickerDropdown onPickEmoji={handleEmojiPick} />
|
||||
<TextareaIcons advancedOptions={advancedOptions} />
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Overlay from 'react-overlays/lib/Overlay';
|
||||
import Overlay from 'react-overlays/Overlay';
|
||||
|
||||
// Components.
|
||||
import IconButton from 'flavours/glitch/components/icon_button';
|
||||
|
@ -45,7 +45,7 @@ export default class ComposerOptionsDropdown extends React.PureComponent {
|
|||
};
|
||||
|
||||
// Toggles opening and closing the dropdown.
|
||||
handleToggle = ({ target, type }) => {
|
||||
handleToggle = ({ type }) => {
|
||||
const { onModalOpen } = this.props;
|
||||
const { open } = this.state;
|
||||
|
||||
|
@ -59,11 +59,9 @@ export default class ComposerOptionsDropdown extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
const { top } = target.getBoundingClientRect();
|
||||
if (this.state.open && this.activeElement) {
|
||||
this.activeElement.focus({ preventScroll: true });
|
||||
}
|
||||
this.setState({ placement: top * 2 < innerHeight ? 'bottom' : 'top' });
|
||||
this.setState({ open: !this.state.open, openedViaKeyboard: type !== 'click' });
|
||||
}
|
||||
}
|
||||
|
@ -158,6 +156,18 @@ export default class ComposerOptionsDropdown extends React.PureComponent {
|
|||
};
|
||||
}
|
||||
|
||||
setTargetRef = c => {
|
||||
this.target = c;
|
||||
}
|
||||
|
||||
findTarget = () => {
|
||||
return this.target;
|
||||
}
|
||||
|
||||
handleOverlayEnter = (state) => {
|
||||
this.setState({ placement: state.placement });
|
||||
}
|
||||
|
||||
// Rendering.
|
||||
render () {
|
||||
const {
|
||||
|
@ -179,6 +189,7 @@ export default class ComposerOptionsDropdown extends React.PureComponent {
|
|||
<div
|
||||
className={classNames('privacy-dropdown', placement, { active: open })}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
ref={this.setTargetRef}
|
||||
>
|
||||
<div className={classNames('privacy-dropdown__value', { active })}>
|
||||
<IconButton
|
||||
|
@ -204,18 +215,26 @@ export default class ComposerOptionsDropdown extends React.PureComponent {
|
|||
containerPadding={20}
|
||||
placement={placement}
|
||||
show={open}
|
||||
target={this}
|
||||
flip
|
||||
target={this.findTarget}
|
||||
container={container}
|
||||
popperConfig={{ strategy: 'fixed', onFirstUpdate: this.handleOverlayEnter }}
|
||||
>
|
||||
<DropdownMenu
|
||||
items={items}
|
||||
renderItemContents={renderItemContents}
|
||||
onChange={onChange}
|
||||
onClose={this.handleClose}
|
||||
value={value}
|
||||
openedViaKeyboard={this.state.openedViaKeyboard}
|
||||
closeOnChange={closeOnChange}
|
||||
/>
|
||||
{({ props, placement }) => (
|
||||
<div {...props}>
|
||||
<div className={`dropdown-animation privacy-dropdown__dropdown ${placement}`}>
|
||||
<DropdownMenu
|
||||
items={items}
|
||||
renderItemContents={renderItemContents}
|
||||
onChange={onChange}
|
||||
onClose={this.handleClose}
|
||||
value={value}
|
||||
openedViaKeyboard={this.state.openedViaKeyboard}
|
||||
closeOnChange={closeOnChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Overlay>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
// Package imports.
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import spring from 'react-motion/lib/spring';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import classNames from 'classnames';
|
||||
|
||||
|
@ -10,15 +9,8 @@ import Icon from 'flavours/glitch/components/icon';
|
|||
|
||||
// Utils.
|
||||
import { withPassive } from 'flavours/glitch/utils/dom_helpers';
|
||||
import Motion from '../../ui/util/optional_motion';
|
||||
import { assignHandlers } from 'flavours/glitch/utils/react_helpers';
|
||||
|
||||
// The spring to use with our motion.
|
||||
const springMotion = spring(1, {
|
||||
damping: 35,
|
||||
stiffness: 400,
|
||||
});
|
||||
|
||||
// The component.
|
||||
export default class ComposerOptionsDropdownContent extends React.PureComponent {
|
||||
|
||||
|
@ -44,7 +36,6 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent
|
|||
};
|
||||
|
||||
state = {
|
||||
mounted: false,
|
||||
value: this.props.openedViaKeyboard ? this.props.items[0].name : undefined,
|
||||
};
|
||||
|
||||
|
@ -56,7 +47,7 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent
|
|||
}
|
||||
|
||||
// Stores our node in `this.node`.
|
||||
handleRef = (node) => {
|
||||
setRef = (node) => {
|
||||
this.node = node;
|
||||
}
|
||||
|
||||
|
@ -69,7 +60,6 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent
|
|||
} else {
|
||||
this.node.firstChild.focus({ preventScroll: true });
|
||||
}
|
||||
this.setState({ mounted: true });
|
||||
}
|
||||
|
||||
// On unmounting, we remove our listeners.
|
||||
|
@ -191,7 +181,6 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent
|
|||
|
||||
// Rendering.
|
||||
render () {
|
||||
const { mounted } = this.state;
|
||||
const {
|
||||
items,
|
||||
onChange,
|
||||
|
@ -201,36 +190,9 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent
|
|||
|
||||
// The result.
|
||||
return (
|
||||
<Motion
|
||||
defaultStyle={{
|
||||
opacity: 0,
|
||||
scaleX: 0.85,
|
||||
scaleY: 0.75,
|
||||
}}
|
||||
style={{
|
||||
opacity: springMotion,
|
||||
scaleX: springMotion,
|
||||
scaleY: springMotion,
|
||||
}}
|
||||
>
|
||||
{({ opacity, scaleX, scaleY }) => (
|
||||
// It should not be transformed when mounting because the resulting
|
||||
// size will be used to determine the coordinate of the menu by
|
||||
// react-overlays
|
||||
<div
|
||||
className='privacy-dropdown__dropdown'
|
||||
ref={this.handleRef}
|
||||
role='listbox'
|
||||
style={{
|
||||
...style,
|
||||
opacity: opacity,
|
||||
transform: mounted ? `scale(${scaleX}, ${scaleY})` : null,
|
||||
}}
|
||||
>
|
||||
{!!items && items.map((item, i) => this.renderItem(item, i))}
|
||||
</div>
|
||||
)}
|
||||
</Motion>
|
||||
<div style={{ ...style }} role='listbox' ref={this.setRef}>
|
||||
{!!items && items.map((item, i) => this.renderItem(item, i))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import { EmojiPicker as EmojiPickerAsync } from '../../ui/util/async-components';
|
||||
import Overlay from 'react-overlays/lib/Overlay';
|
||||
import Overlay from 'react-overlays/Overlay';
|
||||
import classNames from 'classnames';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { supportsPassiveEvents } from 'detect-passive-events';
|
||||
|
@ -155,9 +155,6 @@ class EmojiPickerMenu extends React.PureComponent {
|
|||
onClose: PropTypes.func.isRequired,
|
||||
onPick: PropTypes.func.isRequired,
|
||||
style: PropTypes.object,
|
||||
placement: PropTypes.string,
|
||||
arrowOffsetLeft: PropTypes.string,
|
||||
arrowOffsetTop: PropTypes.string,
|
||||
intl: PropTypes.object.isRequired,
|
||||
skinTone: PropTypes.number.isRequired,
|
||||
onSkinTone: PropTypes.func.isRequired,
|
||||
|
@ -326,14 +323,13 @@ class EmojiPickerDropdown extends React.PureComponent {
|
|||
state = {
|
||||
active: false,
|
||||
loading: false,
|
||||
placement: null,
|
||||
};
|
||||
|
||||
setRef = (c) => {
|
||||
this.dropdown = c;
|
||||
}
|
||||
|
||||
onShowDropdown = ({ target }) => {
|
||||
onShowDropdown = () => {
|
||||
this.setState({ active: true });
|
||||
|
||||
if (!EmojiPicker) {
|
||||
|
@ -348,9 +344,6 @@ class EmojiPickerDropdown extends React.PureComponent {
|
|||
this.setState({ loading: false, active: false });
|
||||
});
|
||||
}
|
||||
|
||||
const { top } = target.getBoundingClientRect();
|
||||
this.setState({ placement: top * 2 < innerHeight ? 'bottom' : 'top' });
|
||||
}
|
||||
|
||||
onHideDropdown = () => {
|
||||
|
@ -384,7 +377,7 @@ class EmojiPickerDropdown extends React.PureComponent {
|
|||
render () {
|
||||
const { intl, onPickEmoji, onSkinTone, skinTone, frequentlyUsedEmojis, button } = this.props;
|
||||
const title = intl.formatMessage(messages.emoji);
|
||||
const { active, loading, placement } = this.state;
|
||||
const { active, loading } = this.state;
|
||||
|
||||
return (
|
||||
<div className='emoji-picker-dropdown' onKeyDown={this.handleKeyDown}>
|
||||
|
@ -396,16 +389,22 @@ class EmojiPickerDropdown extends React.PureComponent {
|
|||
/>}
|
||||
</div>
|
||||
|
||||
<Overlay show={active} placement={placement} target={this.findTarget}>
|
||||
<EmojiPickerMenu
|
||||
custom_emojis={this.props.custom_emojis}
|
||||
loading={loading}
|
||||
onClose={this.onHideDropdown}
|
||||
onPick={onPickEmoji}
|
||||
onSkinTone={onSkinTone}
|
||||
skinTone={skinTone}
|
||||
frequentlyUsedEmojis={frequentlyUsedEmojis}
|
||||
/>
|
||||
<Overlay show={active} placement={'bottom'} target={this.findTarget} popperConfig={{ strategy: 'fixed' }}>
|
||||
{({ props, placement })=> (
|
||||
<div {...props} style={{ ...props.style, width: 299 }}>
|
||||
<div className={`dropdown-animation ${placement}`}>
|
||||
<EmojiPickerMenu
|
||||
custom_emojis={this.props.custom_emojis}
|
||||
loading={loading}
|
||||
onClose={this.onHideDropdown}
|
||||
onPick={onPickEmoji}
|
||||
onSkinTone={onSkinTone}
|
||||
skinTone={skinTone}
|
||||
frequentlyUsedEmojis={frequentlyUsedEmojis}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Overlay>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -2,9 +2,7 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import { injectIntl, defineMessages } from 'react-intl';
|
||||
import TextIconButton from './text_icon_button';
|
||||
import Overlay from 'react-overlays/lib/Overlay';
|
||||
import Motion from 'flavours/glitch/features/ui/util/optional_motion';
|
||||
import spring from 'react-motion/lib/spring';
|
||||
import Overlay from 'react-overlays/Overlay';
|
||||
import { supportsPassiveEvents } from 'detect-passive-events';
|
||||
import classNames from 'classnames';
|
||||
import { languages as preloadedLanguages } from 'flavours/glitch/initial_state';
|
||||
|
@ -22,10 +20,8 @@ const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
|
|||
class LanguageDropdownMenu extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
style: PropTypes.object,
|
||||
value: PropTypes.string.isRequired,
|
||||
frequentlyUsedLanguages: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
placement: PropTypes.string.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
languages: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.string)),
|
||||
|
@ -37,7 +33,6 @@ class LanguageDropdownMenu extends React.PureComponent {
|
|||
};
|
||||
|
||||
state = {
|
||||
mounted: false,
|
||||
searchValue: '',
|
||||
};
|
||||
|
||||
|
@ -50,7 +45,6 @@ class LanguageDropdownMenu extends React.PureComponent {
|
|||
componentDidMount () {
|
||||
document.addEventListener('click', this.handleDocumentClick, false);
|
||||
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
||||
this.setState({ mounted: true });
|
||||
|
||||
// Because of https://github.com/react-bootstrap/react-bootstrap/issues/2614 we need
|
||||
// to wait for a frame before focusing
|
||||
|
@ -222,29 +216,22 @@ class LanguageDropdownMenu extends React.PureComponent {
|
|||
}
|
||||
|
||||
render () {
|
||||
const { style, placement, intl } = this.props;
|
||||
const { mounted, searchValue } = this.state;
|
||||
const { intl } = this.props;
|
||||
const { searchValue } = this.state;
|
||||
const isSearching = searchValue !== '';
|
||||
const results = this.search();
|
||||
|
||||
return (
|
||||
<Motion defaultStyle={{ opacity: 0, scaleX: 0.85, scaleY: 0.75 }} style={{ opacity: spring(1, { damping: 35, stiffness: 400 }), scaleX: spring(1, { damping: 35, stiffness: 400 }), scaleY: spring(1, { damping: 35, stiffness: 400 }) }}>
|
||||
{({ opacity, scaleX, scaleY }) => (
|
||||
// It should not be transformed when mounting because the resulting
|
||||
// size will be used to determine the coordinate of the menu by
|
||||
// react-overlays
|
||||
<div className={`language-dropdown__dropdown ${placement}`} style={{ ...style, opacity: opacity, transform: mounted ? `scale(${scaleX}, ${scaleY})` : null }} ref={this.setRef}>
|
||||
<div className='emoji-mart-search'>
|
||||
<input type='search' value={searchValue} onChange={this.handleSearchChange} onKeyDown={this.handleSearchKeyDown} placeholder={intl.formatMessage(messages.search)} />
|
||||
<button className='emoji-mart-search-icon' disabled={!isSearching} aria-label={intl.formatMessage(messages.clear)} onClick={this.handleClear}>{!isSearching ? loupeIcon : deleteIcon}</button>
|
||||
</div>
|
||||
<div ref={this.setRef}>
|
||||
<div className='emoji-mart-search'>
|
||||
<input type='search' value={searchValue} onChange={this.handleSearchChange} onKeyDown={this.handleSearchKeyDown} placeholder={intl.formatMessage(messages.search)} />
|
||||
<button type='button' className='emoji-mart-search-icon' disabled={!isSearching} aria-label={intl.formatMessage(messages.clear)} onClick={this.handleClear}>{!isSearching ? loupeIcon : deleteIcon}</button>
|
||||
</div>
|
||||
|
||||
<div className='language-dropdown__dropdown__results emoji-mart-scroll' role='listbox' ref={this.setListRef}>
|
||||
{results.map(this.renderItem)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Motion>
|
||||
<div className='language-dropdown__dropdown__results emoji-mart-scroll' role='listbox' ref={this.setListRef}>
|
||||
{results.map(this.renderItem)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -266,14 +253,11 @@ class LanguageDropdown extends React.PureComponent {
|
|||
placement: 'bottom',
|
||||
};
|
||||
|
||||
handleToggle = ({ target }) => {
|
||||
const { top } = target.getBoundingClientRect();
|
||||
|
||||
handleToggle = () => {
|
||||
if (this.state.open && this.activeElement) {
|
||||
this.activeElement.focus({ preventScroll: true });
|
||||
}
|
||||
|
||||
this.setState({ placement: top * 2 < innerHeight ? 'bottom' : 'top' });
|
||||
this.setState({ open: !this.state.open });
|
||||
}
|
||||
|
||||
|
@ -293,13 +277,25 @@ class LanguageDropdown extends React.PureComponent {
|
|||
onChange(value);
|
||||
}
|
||||
|
||||
setTargetRef = c => {
|
||||
this.target = c;
|
||||
}
|
||||
|
||||
findTarget = () => {
|
||||
return this.target;
|
||||
}
|
||||
|
||||
handleOverlayEnter = (state) => {
|
||||
this.setState({ placement: state.placement });
|
||||
}
|
||||
|
||||
render () {
|
||||
const { value, intl, frequentlyUsedLanguages } = this.props;
|
||||
const { open, placement } = this.state;
|
||||
|
||||
return (
|
||||
<div className={classNames('privacy-dropdown', { active: open })}>
|
||||
<div className='privacy-dropdown__value'>
|
||||
<div className={classNames('privacy-dropdown', placement, { active: open })}>
|
||||
<div className='privacy-dropdown__value' ref={this.setTargetRef} >
|
||||
<TextIconButton
|
||||
className='privacy-dropdown__value-icon'
|
||||
label={value && value.toUpperCase()}
|
||||
|
@ -309,15 +305,20 @@ class LanguageDropdown extends React.PureComponent {
|
|||
/>
|
||||
</div>
|
||||
|
||||
<Overlay show={open} placement={placement} target={this}>
|
||||
<LanguageDropdownMenu
|
||||
value={value}
|
||||
frequentlyUsedLanguages={frequentlyUsedLanguages}
|
||||
onClose={this.handleClose}
|
||||
onChange={this.handleChange}
|
||||
placement={placement}
|
||||
intl={intl}
|
||||
/>
|
||||
<Overlay show={open} placement={'bottom'} flip target={this.findTarget} popperConfig={{ strategy: 'fixed', onFirstUpdate: this.handleOverlayEnter }}>
|
||||
{({ props, placement }) => (
|
||||
<div {...props}>
|
||||
<div className={`dropdown-animation language-dropdown__dropdown ${placement}`} >
|
||||
<LanguageDropdownMenu
|
||||
value={value}
|
||||
frequentlyUsedLanguages={frequentlyUsedLanguages}
|
||||
onClose={this.handleClose}
|
||||
onChange={this.handleChange}
|
||||
intl={intl}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Overlay>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -3,13 +3,12 @@ import classNames from 'classnames';
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import spring from 'react-motion/lib/spring';
|
||||
import {
|
||||
injectIntl,
|
||||
FormattedMessage,
|
||||
defineMessages,
|
||||
} from 'react-intl';
|
||||
import Overlay from 'react-overlays/lib/Overlay';
|
||||
import Overlay from 'react-overlays/Overlay';
|
||||
|
||||
// Components.
|
||||
import Icon from 'flavours/glitch/components/icon';
|
||||
|
@ -17,7 +16,6 @@ import Icon from 'flavours/glitch/components/icon';
|
|||
// Utils.
|
||||
import { focusRoot } from 'flavours/glitch/utils/dom_helpers';
|
||||
import { searchEnabled } from 'flavours/glitch/initial_state';
|
||||
import Motion from '../../ui/util/optional_motion';
|
||||
|
||||
const messages = defineMessages({
|
||||
placeholder: { id: 'search.placeholder', defaultMessage: 'Search' },
|
||||
|
@ -26,31 +24,20 @@ const messages = defineMessages({
|
|||
|
||||
class SearchPopout extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
style: PropTypes.object,
|
||||
};
|
||||
|
||||
render () {
|
||||
const { style } = this.props;
|
||||
const extraInformation = searchEnabled ? <FormattedMessage id='search_popout.tips.full_text' defaultMessage='Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.' /> : <FormattedMessage id='search_popout.tips.text' defaultMessage='Simple text returns matching display names, usernames and hashtags' />;
|
||||
return (
|
||||
<div style={{ ...style, position: 'absolute', width: 285, zIndex: 2 }}>
|
||||
<Motion defaultStyle={{ opacity: 0, scaleX: 0.85, scaleY: 0.75 }} style={{ opacity: spring(1, { damping: 35, stiffness: 400 }), scaleX: spring(1, { damping: 35, stiffness: 400 }), scaleY: spring(1, { damping: 35, stiffness: 400 }) }}>
|
||||
{({ opacity, scaleX, scaleY }) => (
|
||||
<div className='search-popout' style={{ opacity: opacity, transform: `scale(${scaleX}, ${scaleY})` }}>
|
||||
<h4><FormattedMessage id='search_popout.search_format' defaultMessage='Advanced search format' /></h4>
|
||||
<div className='search-popout'>
|
||||
<h4><FormattedMessage id='search_popout.search_format' defaultMessage='Advanced search format' /></h4>
|
||||
|
||||
<ul>
|
||||
<li><em>#example</em> <FormattedMessage id='search_popout.tips.hashtag' defaultMessage='hashtag' /></li>
|
||||
<li><em>@username@domain</em> <FormattedMessage id='search_popout.tips.user' defaultMessage='user' /></li>
|
||||
<li><em>URL</em> <FormattedMessage id='search_popout.tips.user' defaultMessage='user' /></li>
|
||||
<li><em>URL</em> <FormattedMessage id='search_popout.tips.status' defaultMessage='status' /></li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li><em>#example</em> <FormattedMessage id='search_popout.tips.hashtag' defaultMessage='hashtag' /></li>
|
||||
<li><em>@username@domain</em> <FormattedMessage id='search_popout.tips.user' defaultMessage='user' /></li>
|
||||
<li><em>URL</em> <FormattedMessage id='search_popout.tips.user' defaultMessage='user' /></li>
|
||||
<li><em>URL</em> <FormattedMessage id='search_popout.tips.status' defaultMessage='status' /></li>
|
||||
</ul>
|
||||
|
||||
{extraInformation}
|
||||
</div>
|
||||
)}
|
||||
</Motion>
|
||||
{extraInformation}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -136,6 +123,10 @@ class Search extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
findTarget = () => {
|
||||
return this.searchForm;
|
||||
}
|
||||
|
||||
render () {
|
||||
const { intl, value, submitted } = this.props;
|
||||
const { expanded } = this.state;
|
||||
|
@ -161,8 +152,14 @@ class Search extends React.PureComponent {
|
|||
<Icon id='search' className={hasValue ? '' : 'active'} />
|
||||
<Icon id='times-circle' className={hasValue ? 'active' : ''} />
|
||||
</div>
|
||||
<Overlay show={expanded && !hasValue} placement='bottom' target={this} container={this}>
|
||||
<SearchPopout />
|
||||
<Overlay show={expanded && !hasValue} placement='bottom' target={this.findTarget} popperConfig={{ strategy: 'fixed' }}>
|
||||
{({ props, placement }) => (
|
||||
<div {...props} style={{ ...props.style, width: 285, zIndex: 2 }}>
|
||||
<div className={`dropdown-animation ${placement}`}>
|
||||
<SearchPopout />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Overlay>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -43,13 +43,13 @@ export default class Upload extends ImmutablePureComponent {
|
|||
{({ scale }) => (
|
||||
<div className='compose-form__upload-thumbnail' style={{ transform: `scale(${scale})`, backgroundImage: `url(${media.get('preview_url')})`, backgroundPosition: `${x}% ${y}%` }}>
|
||||
<div className='compose-form__upload__actions'>
|
||||
<button className='icon-button' onClick={this.handleUndoClick}><Icon id='times' /> <FormattedMessage id='upload_form.undo' defaultMessage='Delete' /></button>
|
||||
{!!media.get('unattached') && (<button className='icon-button' onClick={this.handleFocalPointClick}><Icon id='pencil' /> <FormattedMessage id='upload_form.edit' defaultMessage='Edit' /></button>)}
|
||||
<button type='button' className='icon-button' onClick={this.handleUndoClick}><Icon id='times' /> <FormattedMessage id='upload_form.undo' defaultMessage='Delete' /></button>
|
||||
<button type='button' className='icon-button' onClick={this.handleFocalPointClick}><Icon id='pencil' /> <FormattedMessage id='upload_form.edit' defaultMessage='Edit' /></button>
|
||||
</div>
|
||||
|
||||
{(media.get('description') || '').length === 0 && !!media.get('unattached') && (
|
||||
{(media.get('description') || '').length === 0 && (
|
||||
<div className='compose-form__upload__warning'>
|
||||
<button className='icon-button' onClick={this.handleFocalPointClick}><Icon id='info-circle' /> <FormattedMessage id='upload_form.description_missing' defaultMessage='No description added' /></button>
|
||||
<button type='button' className='icon-button' onClick={this.handleFocalPointClick}><Icon id='info-circle' /> <FormattedMessage id='upload_form.description_missing' defaultMessage='No description added' /></button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -70,6 +70,7 @@ function mapStateToProps (state) {
|
|||
mediaDescriptionConfirmation: state.getIn(['local_settings', 'confirm_missing_media_description']),
|
||||
preselectOnReply: state.getIn(['local_settings', 'preselect_on_reply']),
|
||||
isInReply: state.getIn(['compose', 'in_reply_to']) !== null,
|
||||
lang: state.getIn(['compose', 'language']),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
import { debounce } from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import { connect } from 'react-redux';
|
||||
import ColumnHeader from 'flavours/glitch/components/column_header';
|
||||
import ScrollableList from 'flavours/glitch/components/scrollable_list';
|
||||
import Column from 'flavours/glitch/features/ui/components/column';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import Hashtag from 'flavours/glitch/components/hashtag';
|
||||
import { expandFollowedHashtags, fetchFollowedHashtags } from 'flavours/glitch/actions/tags';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'followed_tags', defaultMessage: 'Followed hashtags' },
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
hashtags: state.getIn(['followed_tags', 'items']),
|
||||
isLoading: state.getIn(['followed_tags', 'isLoading'], true),
|
||||
hasMore: !!state.getIn(['followed_tags', 'next']),
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
@injectIntl
|
||||
class FollowedTags extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
hashtags: ImmutablePropTypes.list,
|
||||
isLoading: PropTypes.bool,
|
||||
hasMore: PropTypes.bool,
|
||||
multiColumn: PropTypes.bool,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.props.dispatch(fetchFollowedHashtags());
|
||||
};
|
||||
|
||||
handleLoadMore = debounce(() => {
|
||||
this.props.dispatch(expandFollowedHashtags());
|
||||
}, 300, { leading: true });
|
||||
|
||||
render () {
|
||||
const { intl, hashtags, isLoading, hasMore, multiColumn } = this.props;
|
||||
|
||||
const emptyMessage = <FormattedMessage id='empty_column.followed_tags' defaultMessage='You have not followed any hashtags yet. When you do, they will show up here.' />;
|
||||
|
||||
return (
|
||||
<Column bindToDocument={!multiColumn}>
|
||||
<ColumnHeader
|
||||
icon='hashtag'
|
||||
title={intl.formatMessage(messages.heading)}
|
||||
showBackButton
|
||||
multiColumn={multiColumn}
|
||||
/>
|
||||
|
||||
<ScrollableList
|
||||
scrollKey='followed_tags'
|
||||
emptyMessage={emptyMessage}
|
||||
hasMore={hasMore}
|
||||
isLoading={isLoading}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
bindToDocument={!multiColumn}
|
||||
>
|
||||
{hashtags.map((hashtag) => (
|
||||
<Hashtag
|
||||
key={hashtag.get('name')}
|
||||
name={hashtag.get('name')}
|
||||
to={`/tags/${hashtag.get('name')}`}
|
||||
withGraph={false}
|
||||
// Taken from ImmutableHashtag. Should maybe refactor ImmutableHashtag to accept more options?
|
||||
people={hashtag.getIn(['history', 0, 'accounts']) * 1 + hashtag.getIn(['history', 1, 'accounts']) * 1}
|
||||
history={hashtag.get('history').reverse().map((day) => day.get('uses')).toArray()}
|
||||
/>
|
||||
))}
|
||||
</ScrollableList>
|
||||
|
||||
<Helmet>
|
||||
<meta name='robots' content='noindex' />
|
||||
</Helmet>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -124,6 +124,7 @@ export default class Notification extends ImmutablePureComponent {
|
|||
onMoveDown={onMoveDown}
|
||||
onMoveUp={onMoveUp}
|
||||
onMention={onMention}
|
||||
contextType='notifications'
|
||||
getScrollPosition={getScrollPosition}
|
||||
updateScrollBottom={updateScrollBottom}
|
||||
cachedMediaWidth={this.props.cachedMediaWidth}
|
||||
|
@ -146,6 +147,7 @@ export default class Notification extends ImmutablePureComponent {
|
|||
onMoveDown={onMoveDown}
|
||||
onMoveUp={onMoveUp}
|
||||
onMention={onMention}
|
||||
contextType='notifications'
|
||||
getScrollPosition={getScrollPosition}
|
||||
updateScrollBottom={updateScrollBottom}
|
||||
cachedMediaWidth={this.props.cachedMediaWidth}
|
||||
|
@ -168,6 +170,7 @@ export default class Notification extends ImmutablePureComponent {
|
|||
onMoveDown={onMoveDown}
|
||||
onMoveUp={onMoveUp}
|
||||
onMention={onMention}
|
||||
contextType='notifications'
|
||||
getScrollPosition={getScrollPosition}
|
||||
updateScrollBottom={updateScrollBottom}
|
||||
cachedMediaWidth={this.props.cachedMediaWidth}
|
||||
|
@ -190,6 +193,7 @@ export default class Notification extends ImmutablePureComponent {
|
|||
onMoveDown={onMoveDown}
|
||||
onMoveUp={onMoveUp}
|
||||
onMention={onMention}
|
||||
contextType='notifications'
|
||||
getScrollPosition={getScrollPosition}
|
||||
updateScrollBottom={updateScrollBottom}
|
||||
cachedMediaWidth={this.props.cachedMediaWidth}
|
||||
|
@ -212,6 +216,7 @@ export default class Notification extends ImmutablePureComponent {
|
|||
onMoveDown={onMoveDown}
|
||||
onMoveUp={onMoveUp}
|
||||
onMention={onMention}
|
||||
contextType='notifications'
|
||||
getScrollPosition={getScrollPosition}
|
||||
updateScrollBottom={updateScrollBottom}
|
||||
cachedMediaWidth={this.props.cachedMediaWidth}
|
||||
|
|
|
@ -320,7 +320,7 @@ class FocalPointModal extends ImmutablePureComponent {
|
|||
<React.Fragment>
|
||||
<label className='setting-text-label' htmlFor='upload-modal__thumbnail'><FormattedMessage id='upload_form.thumbnail' defaultMessage='Change thumbnail' /></label>
|
||||
|
||||
<Button disabled={isUploadingThumbnail} text={intl.formatMessage(messages.chooseImage)} onClick={this.handleFileInputClick} />
|
||||
<Button disabled={isUploadingThumbnail || !media.get('unattached')} text={intl.formatMessage(messages.chooseImage)} onClick={this.handleFileInputClick} />
|
||||
|
||||
<label>
|
||||
<span style={{ display: 'none' }}>{intl.formatMessage(messages.chooseImage)}</span>
|
||||
|
|
|
@ -52,6 +52,8 @@ class LinkFooter extends React.PureComponent {
|
|||
const canInvite = signedIn && ((permissions & PERMISSION_INVITE_USERS) === PERMISSION_INVITE_USERS);
|
||||
const canProfileDirectory = profileDirectory;
|
||||
|
||||
const DividingCircle = <span aria-hidden>{' · '}</span>;
|
||||
|
||||
return (
|
||||
<div className='link-footer'>
|
||||
<p>
|
||||
|
@ -60,17 +62,17 @@ class LinkFooter extends React.PureComponent {
|
|||
<Link key='about' to='/about'><FormattedMessage id='footer.about' defaultMessage='About' /></Link>
|
||||
{canInvite && (
|
||||
<>
|
||||
{' · '}
|
||||
{DividingCircle}
|
||||
<a key='invites' href='/invites' target='_blank'><FormattedMessage id='footer.invite' defaultMessage='Invite people' /></a>
|
||||
</>
|
||||
)}
|
||||
{canProfileDirectory && (
|
||||
<>
|
||||
{' · '}
|
||||
{DividingCircle}
|
||||
<Link key='directory' to='/directory'><FormattedMessage id='footer.directory' defaultMessage='Profiles directory' /></Link>
|
||||
</>
|
||||
)}
|
||||
{' · '}
|
||||
{DividingCircle}
|
||||
<Link key='privacy-policy' to='/privacy-policy'><FormattedMessage id='footer.privacy_policy' defaultMessage='Privacy policy' /></Link>
|
||||
</p>
|
||||
|
||||
|
@ -78,13 +80,13 @@ class LinkFooter extends React.PureComponent {
|
|||
<strong>Mastodon</strong>:
|
||||
{' '}
|
||||
<a href='https://joinmastodon.org' target='_blank'><FormattedMessage id='footer.about' defaultMessage='About' /></a>
|
||||
{' · '}
|
||||
{DividingCircle}
|
||||
<a href='https://joinmastodon.org/apps' target='_blank'><FormattedMessage id='footer.get_app' defaultMessage='Get the app' /></a>
|
||||
{' · '}
|
||||
{DividingCircle}
|
||||
<Link to='/keyboard-shortcuts'><FormattedMessage id='footer.keyboard_shortcuts' defaultMessage='Keyboard shortcuts' /></Link>
|
||||
{' · '}
|
||||
{DividingCircle}
|
||||
<a href={source_url} rel='noopener noreferrer' target='_blank'><FormattedMessage id='footer.source_code' defaultMessage='View source code' /></a>
|
||||
{' · '}
|
||||
{DividingCircle}
|
||||
v{version}
|
||||
</p>
|
||||
</div>
|
||||
|
|
|
@ -30,7 +30,7 @@ const SignInBanner = () => {
|
|||
|
||||
return (
|
||||
<div className='sign-in-banner'>
|
||||
<p><FormattedMessage id='sign_in_banner.text' defaultMessage='Sign in to follow profiles or hashtags, favourite, share and reply to posts, or interact from your account on a different server.' /></p>
|
||||
<p><FormattedMessage id='sign_in_banner.text' defaultMessage='Sign in to follow profiles or hashtags, favourite, share and reply to posts. You can also interact from your account on a different server.' /></p>
|
||||
<a href='/auth/sign_in' className='button button--block'><FormattedMessage id='sign_in_banner.sign_in' defaultMessage='Sign in' /></a>
|
||||
{signupButton}
|
||||
</div>
|
||||
|
|
|
@ -50,6 +50,7 @@ export default class VideoModal extends ImmutablePureComponent {
|
|||
autoPlay={options.autoPlay}
|
||||
volume={options.defaultVolume}
|
||||
onCloseVideo={onClose}
|
||||
autoFocus
|
||||
detailed
|
||||
alt={media.get('description')}
|
||||
/>
|
||||
|
|
|
@ -42,6 +42,7 @@ import {
|
|||
FollowRequests,
|
||||
FavouritedStatuses,
|
||||
BookmarkedStatuses,
|
||||
FollowedTags,
|
||||
ListTimeline,
|
||||
Blocks,
|
||||
DomainBlocks,
|
||||
|
@ -56,7 +57,7 @@ import {
|
|||
PrivacyPolicy,
|
||||
} from './util/async-components';
|
||||
import { HotKeys } from 'react-hotkeys';
|
||||
import initialState, { me, owner, singleUserMode, showTrends } from '../../initial_state';
|
||||
import initialState, { me, owner, singleUserMode, showTrends, trendsAsLanding } from '../../initial_state';
|
||||
import { closeOnboarding, INTRODUCTION_VERSION } from 'flavours/glitch/actions/onboarding';
|
||||
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
@ -177,7 +178,7 @@ class SwitchingColumnsArea extends React.PureComponent {
|
|||
}
|
||||
} else if (singleUserMode && owner && initialState?.accounts[owner]) {
|
||||
redirect = <Redirect from='/' to={`/@${initialState.accounts[owner].username}`} exact />;
|
||||
} else if (showTrends) {
|
||||
} else if (showTrends && trendsAsLanding) {
|
||||
redirect = <Redirect from='/' to='/explore' exact />;
|
||||
} else {
|
||||
redirect = <Redirect from='/' to='/about' exact />;
|
||||
|
@ -230,6 +231,7 @@ class SwitchingColumnsArea extends React.PureComponent {
|
|||
<WrappedRoute path='/follow_requests' component={FollowRequests} content={children} />
|
||||
<WrappedRoute path='/blocks' component={Blocks} content={children} />
|
||||
<WrappedRoute path='/domain_blocks' component={DomainBlocks} content={children} />
|
||||
<WrappedRoute path='/followed_tags' component={FollowedTags} content={children} />
|
||||
<WrappedRoute path='/mutes' component={Mutes} content={children} />
|
||||
<WrappedRoute path='/lists' component={Lists} content={children} />
|
||||
<WrappedRoute path='/getting-started-misc' component={GettingStartedMisc} content={children} />
|
||||
|
|
|
@ -98,6 +98,10 @@ export function FavouritedStatuses () {
|
|||
return import(/* webpackChunkName: "flavours/glitch/async/favourited_statuses" */'flavours/glitch/features/favourited_statuses');
|
||||
}
|
||||
|
||||
export function FollowedTags () {
|
||||
return import(/* webpackChunkName: "flavours/glitch/async/followed_tags" */'flavours/glitch/features/followed_tags');
|
||||
}
|
||||
|
||||
export function BookmarkedStatuses () {
|
||||
return import(/* webpackChunkName: "flavours/glitch/async/bookmarked_statuses" */'flavours/glitch/features/bookmarked_statuses');
|
||||
}
|
||||
|
|
|
@ -124,6 +124,7 @@ class Video extends React.PureComponent {
|
|||
volume: PropTypes.number,
|
||||
muted: PropTypes.bool,
|
||||
componentIndex: PropTypes.number,
|
||||
autoFocus: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
|
@ -537,7 +538,7 @@ class Video extends React.PureComponent {
|
|||
}
|
||||
|
||||
render () {
|
||||
const { preview, src, inline, onOpenVideo, onCloseVideo, intl, alt, letterbox, fullwidth, detailed, sensitive, editable, blurhash } = this.props;
|
||||
const { preview, src, inline, onOpenVideo, onCloseVideo, intl, alt, letterbox, fullwidth, detailed, sensitive, editable, blurhash, autoFocus } = this.props;
|
||||
const { containerWidth, currentTime, duration, volume, buffer, dragging, paused, fullscreen, hovered, muted, revealed } = this.state;
|
||||
const progress = Math.min((currentTime / duration) * 100, 100);
|
||||
const playerStyle = {};
|
||||
|
@ -635,7 +636,7 @@ class Video extends React.PureComponent {
|
|||
|
||||
<div className='video-player__buttons-bar'>
|
||||
<div className='video-player__buttons left'>
|
||||
<button type='button' title={intl.formatMessage(paused ? messages.play : messages.pause)} aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} className='player-button' onClick={this.togglePlay} autoFocus={detailed}><Icon id={paused ? 'play' : 'pause'} fixedWidth /></button>
|
||||
<button type='button' title={intl.formatMessage(paused ? messages.play : messages.pause)} aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} className='player-button' onClick={this.togglePlay} autoFocus={autoFocus}><Icon id={paused ? 'play' : 'pause'} fixedWidth /></button>
|
||||
<button type='button' title={intl.formatMessage(muted ? messages.unmute : messages.mute)} aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} className='player-button' onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button>
|
||||
|
||||
<div className={classNames('video-player__volume', { active: this.state.hovered })} onMouseDown={this.handleVolumeMouseDown} ref={this.setVolumeRef}>
|
||||
|
|
|
@ -75,6 +75,7 @@
|
|||
* @property {boolean} timeline_preview
|
||||
* @property {string} title
|
||||
* @property {boolean} trends
|
||||
* @property {boolean} trends_as_landing_page
|
||||
* @property {boolean} unfollow_modal
|
||||
* @property {boolean} use_blurhash
|
||||
* @property {boolean=} use_pending_items
|
||||
|
@ -134,6 +135,7 @@ export const singleUserMode = getMeta('single_user_mode');
|
|||
export const source_url = getMeta('source_url');
|
||||
export const timelinePreview = getMeta('timeline_preview');
|
||||
export const title = getMeta('title');
|
||||
export const trendsAsLanding = getMeta('trends_as_landing_page');
|
||||
export const unfollowModal = getMeta('unfollow_modal');
|
||||
export const useBlurhash = getMeta('use_blurhash');
|
||||
export const usePendingItems = getMeta('use_pending_items');
|
||||
|
|
|
@ -551,7 +551,7 @@ export default function compose(state = initialState, action) {
|
|||
.setIn(['media_modal', 'dirty'], false)
|
||||
.update('media_attachments', list => list.map(item => {
|
||||
if (item.get('id') === action.media.id) {
|
||||
return fromJS(action.media).set('unattached', true);
|
||||
return fromJS(action.media).set('unattached', !action.attached);
|
||||
}
|
||||
|
||||
return item;
|
||||
|
|
|
@ -4,12 +4,12 @@ import {
|
|||
DROPDOWN_MENU_CLOSE,
|
||||
} from '../actions/dropdown_menu';
|
||||
|
||||
const initialState = Immutable.Map({ openId: null, placement: null, keyboard: false, scroll_key: null });
|
||||
const initialState = Immutable.Map({ openId: null, keyboard: false, scroll_key: null });
|
||||
|
||||
export default function dropdownMenu(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case DROPDOWN_MENU_OPEN:
|
||||
return state.merge({ openId: action.id, placement: action.placement, keyboard: action.keyboard, scroll_key: action.scroll_key });
|
||||
return state.merge({ openId: action.id, keyboard: action.keyboard, scroll_key: action.scroll_key });
|
||||
case DROPDOWN_MENU_CLOSE:
|
||||
return state.get('openId') === action.id ? state.set('openId', null).set('scroll_key', null) : state;
|
||||
default:
|
||||
|
|
42
app/javascript/flavours/glitch/reducers/followed_tags.js
Normal file
42
app/javascript/flavours/glitch/reducers/followed_tags.js
Normal file
|
@ -0,0 +1,42 @@
|
|||
import {
|
||||
FOLLOWED_HASHTAGS_FETCH_REQUEST,
|
||||
FOLLOWED_HASHTAGS_FETCH_SUCCESS,
|
||||
FOLLOWED_HASHTAGS_FETCH_FAIL,
|
||||
FOLLOWED_HASHTAGS_EXPAND_REQUEST,
|
||||
FOLLOWED_HASHTAGS_EXPAND_SUCCESS,
|
||||
FOLLOWED_HASHTAGS_EXPAND_FAIL,
|
||||
} from 'flavours/glitch/actions/tags';
|
||||
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
|
||||
|
||||
const initialState = ImmutableMap({
|
||||
items: ImmutableList(),
|
||||
isLoading: false,
|
||||
next: null,
|
||||
});
|
||||
|
||||
export default function followed_tags(state = initialState, action) {
|
||||
switch(action.type) {
|
||||
case FOLLOWED_HASHTAGS_FETCH_REQUEST:
|
||||
return state.set('isLoading', true);
|
||||
case FOLLOWED_HASHTAGS_FETCH_SUCCESS:
|
||||
return state.withMutations(map => {
|
||||
map.set('items', fromJS(action.followed_tags));
|
||||
map.set('isLoading', false);
|
||||
map.set('next', action.next);
|
||||
});
|
||||
case FOLLOWED_HASHTAGS_FETCH_FAIL:
|
||||
return state.set('isLoading', false);
|
||||
case FOLLOWED_HASHTAGS_EXPAND_REQUEST:
|
||||
return state.set('isLoading', true);
|
||||
case FOLLOWED_HASHTAGS_EXPAND_SUCCESS:
|
||||
return state.withMutations(map => {
|
||||
map.update('items', set => set.concat(fromJS(action.followed_tags)));
|
||||
map.set('isLoading', false);
|
||||
map.set('next', action.next);
|
||||
});
|
||||
case FOLLOWED_HASHTAGS_EXPAND_FAIL:
|
||||
return state.set('isLoading', false);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
|
@ -42,6 +42,7 @@ import picture_in_picture from './picture_in_picture';
|
|||
import accounts_map from './accounts_map';
|
||||
import history from './history';
|
||||
import tags from './tags';
|
||||
import followed_tags from './followed_tags';
|
||||
|
||||
const reducers = {
|
||||
announcements,
|
||||
|
@ -87,6 +88,7 @@ const reducers = {
|
|||
picture_in_picture,
|
||||
history,
|
||||
tags,
|
||||
followed_tags,
|
||||
};
|
||||
|
||||
export default combineReducers(reducers);
|
||||
|
|
|
@ -1588,6 +1588,15 @@ a.sparkline {
|
|||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: $highlight-text-color;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__actions {
|
||||
|
|
|
@ -118,7 +118,7 @@
|
|||
|
||||
&.active {
|
||||
border-color: $highlight-text-color;
|
||||
background: $highlight-text-color;
|
||||
background: $highlight-text-color url("data:image/svg+xml;utf8,<svg width='18' height='18' fill='none' xmlns='http://www.w3.org/2000/svg'><path d='M4.5 8.5L8 12l6-6' stroke='white' stroke-width='1.5'/></svg>") center center no-repeat;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -586,7 +586,6 @@
|
|||
}
|
||||
|
||||
.privacy-dropdown__dropdown {
|
||||
position: absolute;
|
||||
border-radius: 4px;
|
||||
box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4);
|
||||
background: $simple-background-color;
|
||||
|
@ -653,7 +652,6 @@
|
|||
|
||||
.language-dropdown {
|
||||
&__dropdown {
|
||||
position: absolute;
|
||||
background: $simple-background-color;
|
||||
box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4);
|
||||
border-radius: 4px;
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
|
||||
.emoji-picker-dropdown__menu {
|
||||
background: $simple-background-color;
|
||||
position: absolute;
|
||||
position: relative;
|
||||
box-shadow: 4px 4px 6px rgba($base-shadow-color, 0.4);
|
||||
border-radius: 4px;
|
||||
margin-top: 5px;
|
||||
|
|
|
@ -346,9 +346,8 @@
|
|||
}
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
position: absolute;
|
||||
transform-origin: 50% 0;
|
||||
body > [data-popper-placement] {
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.invisible {
|
||||
|
@ -532,6 +531,42 @@
|
|||
}
|
||||
}
|
||||
|
||||
.dropdown-animation {
|
||||
animation: dropdown 300ms cubic-bezier(0.1, 0.7, 0.1, 1);
|
||||
|
||||
@keyframes dropdown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scaleX(0.85) scaleY(0.75);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scaleX(1) scaleY(1);
|
||||
}
|
||||
}
|
||||
|
||||
&.top {
|
||||
transform-origin: bottom;
|
||||
}
|
||||
|
||||
&.right {
|
||||
transform-origin: left;
|
||||
}
|
||||
|
||||
&.bottom {
|
||||
transform-origin: top;
|
||||
}
|
||||
|
||||
&.left {
|
||||
transform-origin: right;
|
||||
}
|
||||
|
||||
.reduce-motion & {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
display: inline-block;
|
||||
}
|
||||
|
@ -600,36 +635,42 @@
|
|||
|
||||
.dropdown-menu__arrow {
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border: 0 solid transparent;
|
||||
|
||||
&.left {
|
||||
right: -5px;
|
||||
margin-top: -5px;
|
||||
border-width: 5px 0 5px 5px;
|
||||
border-left-color: $ui-secondary-color;
|
||||
&::before {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 14px;
|
||||
height: 5px;
|
||||
background-color: $ui-secondary-color;
|
||||
mask-image: url("data:image/svg+xml;utf8,<svg width='14' height='5' xmlns='http://www.w3.org/2000/svg'><path d='M7 0L0 5h14L7 0z' fill='white'/></svg>");
|
||||
}
|
||||
|
||||
&.top {
|
||||
bottom: -5px;
|
||||
margin-left: -7px;
|
||||
border-width: 5px 7px 0;
|
||||
border-top-color: $ui-secondary-color;
|
||||
|
||||
&::before {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
&.right {
|
||||
left: -9px;
|
||||
|
||||
&::before {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
}
|
||||
|
||||
&.bottom {
|
||||
top: -5px;
|
||||
margin-left: -7px;
|
||||
border-width: 0 7px 5px;
|
||||
border-bottom-color: $ui-secondary-color;
|
||||
}
|
||||
|
||||
&.right {
|
||||
left: -5px;
|
||||
margin-top: -5px;
|
||||
border-width: 5px 5px 5px 0;
|
||||
border-right-color: $ui-secondary-color;
|
||||
&.left {
|
||||
right: -9px;
|
||||
|
||||
&::before {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1544,14 +1585,14 @@ button.icon-button.active i.fa-retweet {
|
|||
align-items: center;
|
||||
background: rgba($base-overlay-background, 0.8);
|
||||
display: flex;
|
||||
height: 100%;
|
||||
height: 100vh;
|
||||
justify-content: center;
|
||||
left: 0;
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
visibility: hidden;
|
||||
width: 100%;
|
||||
width: 100vw;
|
||||
z-index: 2000;
|
||||
|
||||
* {
|
||||
|
|
|
@ -37,7 +37,6 @@
|
|||
.modal-root__modal {
|
||||
pointer-events: auto;
|
||||
display: flex;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.media-modal__zoom-button {
|
||||
|
|
|
@ -157,6 +157,15 @@ code {
|
|||
padding: 0.2em 0.4em;
|
||||
background: darken($ui-base-color, 12%);
|
||||
}
|
||||
|
||||
li {
|
||||
list-style: disc;
|
||||
margin-left: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
ul.hint {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
span.hint {
|
||||
|
|
|
@ -12,6 +12,14 @@ html {
|
|||
&.button-alternative-2 {
|
||||
color: $white;
|
||||
}
|
||||
|
||||
&.button-tertiary {
|
||||
color: $highlight-text-color;
|
||||
}
|
||||
}
|
||||
|
||||
.simple_form .button.button-tertiary {
|
||||
color: $highlight-text-color;
|
||||
}
|
||||
|
||||
.status-card__actions button,
|
||||
|
@ -285,22 +293,8 @@ html {
|
|||
.dropdown-menu {
|
||||
background: $white;
|
||||
|
||||
&__arrow {
|
||||
&.left {
|
||||
border-left-color: $white;
|
||||
}
|
||||
|
||||
&.top {
|
||||
border-top-color: $white;
|
||||
}
|
||||
|
||||
&.bottom {
|
||||
border-bottom-color: $white;
|
||||
}
|
||||
|
||||
&.right {
|
||||
border-right-color: $white;
|
||||
}
|
||||
&__arrow::before {
|
||||
background-color: $white;
|
||||
}
|
||||
|
||||
&__item {
|
||||
|
@ -557,25 +551,6 @@ html {
|
|||
}
|
||||
}
|
||||
|
||||
.directory__tag.active > a,
|
||||
.directory__tag.active > div {
|
||||
border-color: $ui-highlight-color;
|
||||
|
||||
&,
|
||||
h4,
|
||||
h4 small,
|
||||
.fa,
|
||||
.trends__item__current {
|
||||
color: $white;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:active,
|
||||
&:focus {
|
||||
background: $ui-highlight-color;
|
||||
}
|
||||
}
|
||||
|
||||
.batch-table {
|
||||
&__toolbar,
|
||||
&__row,
|
||||
|
|
|
@ -117,7 +117,7 @@
|
|||
width: 18px;
|
||||
height: 18px;
|
||||
flex: 0 0 auto;
|
||||
margin-right: 10px;
|
||||
margin-inline-end: 10px;
|
||||
top: -1px;
|
||||
border-radius: 50%;
|
||||
vertical-align: middle;
|
||||
|
@ -212,7 +212,7 @@
|
|||
.button {
|
||||
height: 36px;
|
||||
padding: 0 16px;
|
||||
margin-right: 10px;
|
||||
margin-inline-end: 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
@ -250,7 +250,7 @@
|
|||
line-height: inherit;
|
||||
color: $action-button-color;
|
||||
border-color: $action-button-color;
|
||||
margin-right: 5px;
|
||||
margin-inline-end: 5px;
|
||||
}
|
||||
|
||||
li {
|
||||
|
@ -260,7 +260,7 @@
|
|||
.poll__option {
|
||||
flex: 0 0 auto;
|
||||
width: calc(100% - (23px + 6px));
|
||||
margin-right: 6px;
|
||||
margin-inline-end: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -160,6 +160,18 @@ export function submitCompose(routerHistory) {
|
|||
|
||||
dispatch(submitComposeRequest());
|
||||
|
||||
// If we're editing a post with media attachments, those have not
|
||||
// necessarily been changed on the server. Do it now in the same
|
||||
// API call.
|
||||
let media_attributes;
|
||||
if (statusId !== null) {
|
||||
media_attributes = media.map(item => ({
|
||||
id: item.get('id'),
|
||||
description: item.get('description'),
|
||||
focus: item.get('focus'),
|
||||
}));
|
||||
}
|
||||
|
||||
api(getState).request({
|
||||
url: statusId === null ? '/api/v1/statuses' : `/api/v1/statuses/${statusId}`,
|
||||
method: statusId === null ? 'post' : 'put',
|
||||
|
@ -167,6 +179,7 @@ export function submitCompose(routerHistory) {
|
|||
status,
|
||||
in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null),
|
||||
media_ids: media.map(item => item.get('id')),
|
||||
media_attributes,
|
||||
sensitive: getState().getIn(['compose', 'sensitive']),
|
||||
spoiler_text: getState().getIn(['compose', 'spoiler']) ? getState().getIn(['compose', 'spoiler_text'], '') : '',
|
||||
visibility: getState().getIn(['compose', 'privacy']),
|
||||
|
@ -377,11 +390,31 @@ export function changeUploadCompose(id, params) {
|
|||
return (dispatch, getState) => {
|
||||
dispatch(changeUploadComposeRequest());
|
||||
|
||||
api(getState).put(`/api/v1/media/${id}`, params).then(response => {
|
||||
dispatch(changeUploadComposeSuccess(response.data));
|
||||
}).catch(error => {
|
||||
dispatch(changeUploadComposeFail(id, error));
|
||||
});
|
||||
let media = getState().getIn(['compose', 'media_attachments']).find((item) => item.get('id') === id);
|
||||
|
||||
// Editing already-attached media is deferred to editing the post itself.
|
||||
// For simplicity's sake, fake an API reply.
|
||||
if (media && !media.get('unattached')) {
|
||||
let { description, focus } = params;
|
||||
const data = media.toJS();
|
||||
|
||||
if (description) {
|
||||
data.description = description;
|
||||
}
|
||||
|
||||
if (focus) {
|
||||
focus = focus.split(',');
|
||||
data.meta = { focus: { x: parseFloat(focus[0]), y: parseFloat(focus[1]) } };
|
||||
}
|
||||
|
||||
dispatch(changeUploadComposeSuccess(data, true));
|
||||
} else {
|
||||
api(getState).put(`/api/v1/media/${id}`, params).then(response => {
|
||||
dispatch(changeUploadComposeSuccess(response.data, false));
|
||||
}).catch(error => {
|
||||
dispatch(changeUploadComposeFail(id, error));
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -392,10 +425,11 @@ export function changeUploadComposeRequest() {
|
|||
};
|
||||
}
|
||||
|
||||
export function changeUploadComposeSuccess(media) {
|
||||
export function changeUploadComposeSuccess(media, attached) {
|
||||
return {
|
||||
type: COMPOSE_UPLOAD_CHANGE_SUCCESS,
|
||||
media: media,
|
||||
attached: attached,
|
||||
skipLoading: true,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
export const DROPDOWN_MENU_OPEN = 'DROPDOWN_MENU_OPEN';
|
||||
export const DROPDOWN_MENU_CLOSE = 'DROPDOWN_MENU_CLOSE';
|
||||
|
||||
export function openDropdownMenu(id, placement, keyboard, scroll_key) {
|
||||
return { type: DROPDOWN_MENU_OPEN, id, placement, keyboard, scroll_key };
|
||||
export function openDropdownMenu(id, keyboard, scroll_key) {
|
||||
return { type: DROPDOWN_MENU_OPEN, id, keyboard, scroll_key };
|
||||
}
|
||||
|
||||
export function closeDropdownMenu(id) {
|
||||
|
|
|
@ -1,9 +1,17 @@
|
|||
import api from '../api';
|
||||
import api, { getLinks } from '../api';
|
||||
|
||||
export const HASHTAG_FETCH_REQUEST = 'HASHTAG_FETCH_REQUEST';
|
||||
export const HASHTAG_FETCH_SUCCESS = 'HASHTAG_FETCH_SUCCESS';
|
||||
export const HASHTAG_FETCH_FAIL = 'HASHTAG_FETCH_FAIL';
|
||||
|
||||
export const FOLLOWED_HASHTAGS_FETCH_REQUEST = 'FOLLOWED_HASHTAGS_FETCH_REQUEST';
|
||||
export const FOLLOWED_HASHTAGS_FETCH_SUCCESS = 'FOLLOWED_HASHTAGS_FETCH_SUCCESS';
|
||||
export const FOLLOWED_HASHTAGS_FETCH_FAIL = 'FOLLOWED_HASHTAGS_FETCH_FAIL';
|
||||
|
||||
export const FOLLOWED_HASHTAGS_EXPAND_REQUEST = 'FOLLOWED_HASHTAGS_EXPAND_REQUEST';
|
||||
export const FOLLOWED_HASHTAGS_EXPAND_SUCCESS = 'FOLLOWED_HASHTAGS_EXPAND_SUCCESS';
|
||||
export const FOLLOWED_HASHTAGS_EXPAND_FAIL = 'FOLLOWED_HASHTAGS_EXPAND_FAIL';
|
||||
|
||||
export const HASHTAG_FOLLOW_REQUEST = 'HASHTAG_FOLLOW_REQUEST';
|
||||
export const HASHTAG_FOLLOW_SUCCESS = 'HASHTAG_FOLLOW_SUCCESS';
|
||||
export const HASHTAG_FOLLOW_FAIL = 'HASHTAG_FOLLOW_FAIL';
|
||||
|
@ -37,6 +45,78 @@ export const fetchHashtagFail = error => ({
|
|||
error,
|
||||
});
|
||||
|
||||
export const fetchFollowedHashtags = () => (dispatch, getState) => {
|
||||
dispatch(fetchFollowedHashtagsRequest());
|
||||
|
||||
api(getState).get('/api/v1/followed_tags').then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(fetchFollowedHashtagsSuccess(response.data, next ? next.uri : null));
|
||||
}).catch(err => {
|
||||
dispatch(fetchFollowedHashtagsFail(err));
|
||||
});
|
||||
};
|
||||
|
||||
export function fetchFollowedHashtagsRequest() {
|
||||
return {
|
||||
type: FOLLOWED_HASHTAGS_FETCH_REQUEST,
|
||||
};
|
||||
};
|
||||
|
||||
export function fetchFollowedHashtagsSuccess(followed_tags, next) {
|
||||
return {
|
||||
type: FOLLOWED_HASHTAGS_FETCH_SUCCESS,
|
||||
followed_tags,
|
||||
next,
|
||||
};
|
||||
};
|
||||
|
||||
export function fetchFollowedHashtagsFail(error) {
|
||||
return {
|
||||
type: FOLLOWED_HASHTAGS_FETCH_FAIL,
|
||||
error,
|
||||
};
|
||||
};
|
||||
|
||||
export function expandFollowedHashtags() {
|
||||
return (dispatch, getState) => {
|
||||
const url = getState().getIn(['followed_tags', 'next']);
|
||||
|
||||
if (url === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(expandFollowedHashtagsRequest());
|
||||
|
||||
api(getState).get(url).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(expandFollowedHashtagsSuccess(response.data, next ? next.uri : null));
|
||||
}).catch(error => {
|
||||
dispatch(expandFollowedHashtagsFail(error));
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
export function expandFollowedHashtagsRequest() {
|
||||
return {
|
||||
type: FOLLOWED_HASHTAGS_EXPAND_REQUEST,
|
||||
};
|
||||
};
|
||||
|
||||
export function expandFollowedHashtagsSuccess(followed_tags, next) {
|
||||
return {
|
||||
type: FOLLOWED_HASHTAGS_EXPAND_SUCCESS,
|
||||
followed_tags,
|
||||
next,
|
||||
};
|
||||
};
|
||||
|
||||
export function expandFollowedHashtagsFail(error) {
|
||||
return {
|
||||
type: FOLLOWED_HASHTAGS_EXPAND_FAIL,
|
||||
error,
|
||||
};
|
||||
};
|
||||
|
||||
export const followHashtag = name => (dispatch, getState) => {
|
||||
dispatch(followHashtagRequest(name));
|
||||
|
||||
|
|
|
@ -50,7 +50,7 @@ export default class Trends extends React.PureComponent {
|
|||
<Hashtag
|
||||
key={hashtag.name}
|
||||
name={hashtag.name}
|
||||
to={`/admin/tags/${hashtag.id}`}
|
||||
to={hashtag.id === undefined ? undefined : `/admin/tags/${hashtag.id}`}
|
||||
people={hashtag.history[0].accounts * 1 + hashtag.history[1].accounts * 1}
|
||||
uses={hashtag.history[0].uses * 1 + hashtag.history[1].uses * 1}
|
||||
history={hashtag.history.reverse().map(day => day.uses)}
|
||||
|
|
|
@ -50,6 +50,7 @@ export default class AutosuggestInput extends ImmutablePureComponent {
|
|||
id: PropTypes.string,
|
||||
searchTokens: PropTypes.arrayOf(PropTypes.string),
|
||||
maxLength: PropTypes.number,
|
||||
lang: PropTypes.string,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
|
@ -185,7 +186,7 @@ export default class AutosuggestInput extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
render () {
|
||||
const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, className, id, maxLength } = this.props;
|
||||
const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, className, id, maxLength, lang } = this.props;
|
||||
const { suggestionsHidden } = this.state;
|
||||
|
||||
return (
|
||||
|
@ -210,6 +211,7 @@ export default class AutosuggestInput extends ImmutablePureComponent {
|
|||
id={id}
|
||||
className={className}
|
||||
maxLength={maxLength}
|
||||
lang={lang}
|
||||
/>
|
||||
</label>
|
||||
|
||||
|
|
|
@ -48,6 +48,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
|
|||
onKeyDown: PropTypes.func,
|
||||
onPaste: PropTypes.func.isRequired,
|
||||
autoFocus: PropTypes.bool,
|
||||
lang: PropTypes.string,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
|
@ -192,7 +193,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
render () {
|
||||
const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, children } = this.props;
|
||||
const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, lang, children } = this.props;
|
||||
const { suggestionsHidden } = this.state;
|
||||
|
||||
return [
|
||||
|
@ -216,6 +217,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
|
|||
onPaste={this.onPaste}
|
||||
dir='auto'
|
||||
aria-autocomplete='list'
|
||||
lang={lang}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
|
|
@ -2,9 +2,7 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import IconButton from './icon_button';
|
||||
import Overlay from 'react-overlays/lib/Overlay';
|
||||
import Motion from '../features/ui/util/optional_motion';
|
||||
import spring from 'react-motion/lib/spring';
|
||||
import Overlay from 'react-overlays/Overlay';
|
||||
import { supportsPassiveEvents } from 'detect-passive-events';
|
||||
import classNames from 'classnames';
|
||||
import { CircularProgress } from 'mastodon/components/loading_indicator';
|
||||
|
@ -24,9 +22,6 @@ class DropdownMenu extends React.PureComponent {
|
|||
scrollable: PropTypes.bool,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
style: PropTypes.object,
|
||||
placement: PropTypes.string,
|
||||
arrowOffsetLeft: PropTypes.string,
|
||||
arrowOffsetTop: PropTypes.string,
|
||||
openedViaKeyboard: PropTypes.bool,
|
||||
renderItem: PropTypes.func,
|
||||
renderHeader: PropTypes.func,
|
||||
|
@ -35,11 +30,6 @@ class DropdownMenu extends React.PureComponent {
|
|||
|
||||
static defaultProps = {
|
||||
style: {},
|
||||
placement: 'bottom',
|
||||
};
|
||||
|
||||
state = {
|
||||
mounted: false,
|
||||
};
|
||||
|
||||
handleDocumentClick = e => {
|
||||
|
@ -56,8 +46,6 @@ class DropdownMenu extends React.PureComponent {
|
|||
if (this.focusedItem && this.props.openedViaKeyboard) {
|
||||
this.focusedItem.focus({ preventScroll: true });
|
||||
}
|
||||
|
||||
this.setState({ mounted: true });
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
|
@ -139,40 +127,28 @@ class DropdownMenu extends React.PureComponent {
|
|||
}
|
||||
|
||||
render () {
|
||||
const { items, style, placement, arrowOffsetLeft, arrowOffsetTop, scrollable, renderHeader, loading } = this.props;
|
||||
const { mounted } = this.state;
|
||||
const { items, scrollable, renderHeader, loading } = this.props;
|
||||
|
||||
let renderItem = this.props.renderItem || this.renderItem;
|
||||
|
||||
return (
|
||||
<Motion defaultStyle={{ opacity: 0, scaleX: 0.85, scaleY: 0.75 }} style={{ opacity: spring(1, { damping: 35, stiffness: 400 }), scaleX: spring(1, { damping: 35, stiffness: 400 }), scaleY: spring(1, { damping: 35, stiffness: 400 }) }}>
|
||||
{({ opacity, scaleX, scaleY }) => (
|
||||
// It should not be transformed when mounting because the resulting
|
||||
// size will be used to determine the coordinate of the menu by
|
||||
// react-overlays
|
||||
<div className={`dropdown-menu ${placement}`} style={{ ...style, opacity: opacity, transform: mounted ? `scale(${scaleX}, ${scaleY})` : null }} ref={this.setRef}>
|
||||
<div className={`dropdown-menu__arrow ${placement}`} style={{ left: arrowOffsetLeft, top: arrowOffsetTop }} />
|
||||
<div className={classNames('dropdown-menu__container', { 'dropdown-menu__container--loading': loading })} ref={this.setRef}>
|
||||
{loading && (
|
||||
<CircularProgress size={30} strokeWidth={3.5} />
|
||||
)}
|
||||
|
||||
<div className={classNames('dropdown-menu__container', { 'dropdown-menu__container--loading': loading })}>
|
||||
{loading && (
|
||||
<CircularProgress size={30} strokeWidth={3.5} />
|
||||
)}
|
||||
|
||||
{!loading && renderHeader && (
|
||||
<div className='dropdown-menu__container__header'>
|
||||
{renderHeader(items)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && (
|
||||
<ul className={classNames('dropdown-menu__container__list', { 'dropdown-menu__container__list--scrollable': scrollable })}>
|
||||
{items.map((option, i) => renderItem(option, i, { onClick: this.handleClick, onKeyPress: this.handleItemKeyPress }))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
{!loading && renderHeader && (
|
||||
<div className='dropdown-menu__container__header'>
|
||||
{renderHeader(items)}
|
||||
</div>
|
||||
)}
|
||||
</Motion>
|
||||
|
||||
{!loading && (
|
||||
<ul className={classNames('dropdown-menu__container__list', { 'dropdown-menu__container__list--scrollable': scrollable })}>
|
||||
{items.map((option, i) => renderItem(option, i, { onClick: this.handleClick, onKeyPress: this.handleItemKeyPress }))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -197,7 +173,6 @@ export default class Dropdown extends React.PureComponent {
|
|||
isUserTouching: PropTypes.func,
|
||||
onOpen: PropTypes.func.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
dropdownPlacement: PropTypes.string,
|
||||
openDropdownId: PropTypes.number,
|
||||
openedViaKeyboard: PropTypes.bool,
|
||||
renderItem: PropTypes.func,
|
||||
|
@ -213,13 +188,11 @@ export default class Dropdown extends React.PureComponent {
|
|||
id: id++,
|
||||
};
|
||||
|
||||
handleClick = ({ target, type }) => {
|
||||
handleClick = ({ type }) => {
|
||||
if (this.state.id === this.props.openDropdownId) {
|
||||
this.handleClose();
|
||||
} else {
|
||||
const { top } = target.getBoundingClientRect();
|
||||
const placement = top * 2 < innerHeight ? 'bottom' : 'top';
|
||||
this.props.onOpen(this.state.id, this.handleItemClick, placement, type !== 'click');
|
||||
this.props.onOpen(this.state.id, this.handleItemClick, type !== 'click');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -303,7 +276,6 @@ export default class Dropdown extends React.PureComponent {
|
|||
disabled,
|
||||
loading,
|
||||
scrollable,
|
||||
dropdownPlacement,
|
||||
openDropdownId,
|
||||
openedViaKeyboard,
|
||||
children,
|
||||
|
@ -314,7 +286,6 @@ export default class Dropdown extends React.PureComponent {
|
|||
const open = this.state.id === openDropdownId;
|
||||
|
||||
const button = children ? React.cloneElement(React.Children.only(children), {
|
||||
ref: this.setTargetRef,
|
||||
onClick: this.handleClick,
|
||||
onMouseDown: this.handleMouseDown,
|
||||
onKeyDown: this.handleButtonKeyDown,
|
||||
|
@ -326,7 +297,6 @@ export default class Dropdown extends React.PureComponent {
|
|||
active={open}
|
||||
disabled={disabled}
|
||||
size={size}
|
||||
ref={this.setTargetRef}
|
||||
onClick={this.handleClick}
|
||||
onMouseDown={this.handleMouseDown}
|
||||
onKeyDown={this.handleButtonKeyDown}
|
||||
|
@ -336,19 +306,27 @@ export default class Dropdown extends React.PureComponent {
|
|||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{button}
|
||||
|
||||
<Overlay show={open} placement={dropdownPlacement} target={this.findTarget}>
|
||||
<DropdownMenu
|
||||
items={items}
|
||||
loading={loading}
|
||||
scrollable={scrollable}
|
||||
onClose={this.handleClose}
|
||||
openedViaKeyboard={openedViaKeyboard}
|
||||
renderItem={renderItem}
|
||||
renderHeader={renderHeader}
|
||||
onItemClick={this.handleItemClick}
|
||||
/>
|
||||
<span ref={this.setTargetRef}>
|
||||
{button}
|
||||
</span>
|
||||
<Overlay show={open} offset={[5, 5]} placement={'bottom'} flip target={this.findTarget} popperConfig={{ strategy: 'fixed' }}>
|
||||
{({ props, arrowProps, placement }) => (
|
||||
<div {...props}>
|
||||
<div className={`dropdown-animation dropdown-menu ${placement}`}>
|
||||
<div className={`dropdown-menu__arrow ${placement}`} {...arrowProps} />
|
||||
<DropdownMenu
|
||||
items={items}
|
||||
loading={loading}
|
||||
scrollable={scrollable}
|
||||
onClose={this.handleClose}
|
||||
openedViaKeyboard={openedViaKeyboard}
|
||||
renderItem={renderItem}
|
||||
renderHeader={renderHeader}
|
||||
onItemClick={this.handleItemClick}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Overlay>
|
||||
</React.Fragment>
|
||||
);
|
||||
|
|
|
@ -4,7 +4,6 @@ import { fetchHistory } from 'mastodon/actions/history';
|
|||
import DropdownMenu from 'mastodon/components/dropdown_menu';
|
||||
|
||||
const mapStateToProps = (state, { statusId }) => ({
|
||||
dropdownPlacement: state.getIn(['dropdown_menu', 'placement']),
|
||||
openDropdownId: state.getIn(['dropdown_menu', 'openId']),
|
||||
openedViaKeyboard: state.getIn(['dropdown_menu', 'keyboard']),
|
||||
items: state.getIn(['history', statusId, 'items']),
|
||||
|
@ -13,9 +12,9 @@ const mapStateToProps = (state, { statusId }) => ({
|
|||
|
||||
const mapDispatchToProps = (dispatch, { statusId }) => ({
|
||||
|
||||
onOpen (id, onItemClick, dropdownPlacement, keyboard) {
|
||||
onOpen (id, onItemClick, keyboard) {
|
||||
dispatch(fetchHistory(statusId));
|
||||
dispatch(openDropdownMenu(id, dropdownPlacement, keyboard));
|
||||
dispatch(openDropdownMenu(id, keyboard));
|
||||
},
|
||||
|
||||
onClose (id) {
|
||||
|
|
|
@ -6,13 +6,12 @@ import DropdownMenu from '../components/dropdown_menu';
|
|||
import { isUserTouching } from '../is_mobile';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
dropdownPlacement: state.getIn(['dropdown_menu', 'placement']),
|
||||
openDropdownId: state.getIn(['dropdown_menu', 'openId']),
|
||||
openedViaKeyboard: state.getIn(['dropdown_menu', 'keyboard']),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch, { status, items, scrollKey }) => ({
|
||||
onOpen(id, onItemClick, dropdownPlacement, keyboard) {
|
||||
onOpen(id, onItemClick, keyboard) {
|
||||
if (status) {
|
||||
dispatch(fetchRelationships([status.getIn(['account', 'id'])]));
|
||||
}
|
||||
|
@ -21,7 +20,7 @@ const mapDispatchToProps = (dispatch, { status, items, scrollKey }) => ({
|
|||
status,
|
||||
actions: items,
|
||||
onClick: onItemClick,
|
||||
}) : openDropdownMenu(id, dropdownPlacement, keyboard, scrollKey));
|
||||
}) : openDropdownMenu(id, keyboard, scrollKey));
|
||||
},
|
||||
|
||||
onClose(id) {
|
||||
|
|
|
@ -46,6 +46,7 @@ const messages = defineMessages({
|
|||
follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
|
||||
favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' },
|
||||
lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' },
|
||||
followed_tags: { id: 'navigation_bar.followed_tags', defaultMessage: 'Followed hashtags' },
|
||||
blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' },
|
||||
domain_blocks: { id: 'navigation_bar.domain_blocks', defaultMessage: 'Blocked domains' },
|
||||
mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' },
|
||||
|
@ -193,7 +194,7 @@ class Header extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
if (account.getIn(['relationship', 'requested']) || account.getIn(['relationship', 'following'])) {
|
||||
bellBtn = <IconButton icon='bell-o' size={24} active={account.getIn(['relationship', 'notifying'])} title={intl.formatMessage(account.getIn(['relationship', 'notifying']) ? messages.disableNotifications : messages.enableNotifications, { name: account.get('username') })} onClick={this.props.onNotifyToggle} />;
|
||||
bellBtn = <IconButton icon={account.getIn(['relationship', 'notifying']) ? 'bell' : 'bell-o'} size={24} active={account.getIn(['relationship', 'notifying'])} title={intl.formatMessage(account.getIn(['relationship', 'notifying']) ? messages.disableNotifications : messages.enableNotifications, { name: account.get('username') })} onClick={this.props.onNotifyToggle} />;
|
||||
}
|
||||
|
||||
if (me !== account.get('id')) {
|
||||
|
@ -242,6 +243,7 @@ class Header extends ImmutablePureComponent {
|
|||
menu.push({ text: intl.formatMessage(messages.follow_requests), to: '/follow_requests' });
|
||||
menu.push({ text: intl.formatMessage(messages.favourites), to: '/favourites' });
|
||||
menu.push({ text: intl.formatMessage(messages.lists), to: '/lists' });
|
||||
menu.push({ text: intl.formatMessage(messages.followed_tags), to: '/followed_tags' });
|
||||
menu.push(null);
|
||||
menu.push({ text: intl.formatMessage(messages.mutes), to: '/mutes' });
|
||||
menu.push({ text: intl.formatMessage(messages.blocks), to: '/blocks' });
|
||||
|
|
|
@ -59,7 +59,7 @@ class Audio extends React.PureComponent {
|
|||
duration: null,
|
||||
paused: true,
|
||||
muted: false,
|
||||
volume: 0.5,
|
||||
volume: 1,
|
||||
dragging: false,
|
||||
revealed: this.props.visible !== undefined ? this.props.visible : (displayMedia !== 'hide_all' && !this.props.sensitive || displayMedia === 'show_all'),
|
||||
};
|
||||
|
@ -80,8 +80,8 @@ class Audio extends React.PureComponent {
|
|||
_pack() {
|
||||
return {
|
||||
src: this.props.src,
|
||||
volume: this.audio.volume,
|
||||
muted: this.audio.muted,
|
||||
volume: this.state.volume,
|
||||
muted: this.state.muted,
|
||||
currentTime: this.audio.currentTime,
|
||||
poster: this.props.poster,
|
||||
backgroundColor: this.props.backgroundColor,
|
||||
|
@ -115,7 +115,8 @@ class Audio extends React.PureComponent {
|
|||
this.audio = c;
|
||||
|
||||
if (this.audio) {
|
||||
this.setState({ volume: this.audio.volume, muted: this.audio.muted });
|
||||
this.audio.volume = 1;
|
||||
this.audio.muted = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -202,7 +203,9 @@ class Audio extends React.PureComponent {
|
|||
const muted = !this.state.muted;
|
||||
|
||||
this.setState({ muted }, () => {
|
||||
this.audio.muted = muted;
|
||||
if (this.gainNode) {
|
||||
this.gainNode.gain.value = muted ? 0 : this.state.volume;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -280,7 +283,9 @@ class Audio extends React.PureComponent {
|
|||
|
||||
if(!isNaN(x)) {
|
||||
this.setState({ volume: x }, () => {
|
||||
this.audio.volume = x;
|
||||
if (this.gainNode) {
|
||||
this.gainNode.gain.value = this.state.muted ? 0 : x;
|
||||
}
|
||||
});
|
||||
}
|
||||
}, 15);
|
||||
|
@ -313,20 +318,12 @@ class Audio extends React.PureComponent {
|
|||
}
|
||||
|
||||
handleLoadedData = () => {
|
||||
const { autoPlay, currentTime, volume, muted } = this.props;
|
||||
const { autoPlay, currentTime } = this.props;
|
||||
|
||||
if (currentTime) {
|
||||
this.audio.currentTime = currentTime;
|
||||
}
|
||||
|
||||
if (volume !== undefined) {
|
||||
this.audio.volume = volume;
|
||||
}
|
||||
|
||||
if (muted !== undefined) {
|
||||
this.audio.muted = muted;
|
||||
}
|
||||
|
||||
if (autoPlay) {
|
||||
this.togglePlay();
|
||||
}
|
||||
|
@ -336,11 +333,16 @@ class Audio extends React.PureComponent {
|
|||
const AudioContext = window.AudioContext || window.webkitAudioContext;
|
||||
const context = new AudioContext();
|
||||
const source = context.createMediaElementSource(this.audio);
|
||||
const gainNode = context.createGain();
|
||||
|
||||
gainNode.gain.value = this.state.muted ? 0 : this.state.volume;
|
||||
|
||||
this.visualizer.setAudioContext(context, source);
|
||||
source.connect(context.destination);
|
||||
source.connect(gainNode);
|
||||
gainNode.connect(context.destination);
|
||||
|
||||
this.audioContext = context;
|
||||
this.gainNode = gainNode;
|
||||
}
|
||||
|
||||
handleDownload = () => {
|
||||
|
|
|
@ -11,6 +11,7 @@ const messages = defineMessages({
|
|||
follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
|
||||
favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' },
|
||||
lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' },
|
||||
followed_tags: { id: 'navigation_bar.followed_tags', defaultMessage: 'Followed hashtags' },
|
||||
blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' },
|
||||
domain_blocks: { id: 'navigation_bar.domain_blocks', defaultMessage: 'Hidden domains' },
|
||||
mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' },
|
||||
|
@ -45,6 +46,7 @@ class ActionBar extends React.PureComponent {
|
|||
menu.push({ text: intl.formatMessage(messages.favourites), to: '/favourites' });
|
||||
menu.push({ text: intl.formatMessage(messages.bookmarks), to: '/bookmarks' });
|
||||
menu.push({ text: intl.formatMessage(messages.lists), to: '/lists' });
|
||||
menu.push({ text: intl.formatMessage(messages.followed_tags), to: '/followed_tags' });
|
||||
menu.push(null);
|
||||
menu.push({ text: intl.formatMessage(messages.mutes), to: '/mutes' });
|
||||
menu.push({ text: intl.formatMessage(messages.blocks), to: '/blocks' });
|
||||
|
|
|
@ -65,6 +65,7 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
anyMedia: PropTypes.bool,
|
||||
isInReply: PropTypes.bool,
|
||||
singleColumn: PropTypes.bool,
|
||||
lang: PropTypes.string,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
|
@ -241,6 +242,7 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
searchTokens={[':']}
|
||||
id='cw-spoiler-input'
|
||||
className='spoiler-input__input'
|
||||
lang={this.props.lang}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@ -258,6 +260,7 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
onSuggestionSelected={this.onSuggestionSelected}
|
||||
onPaste={onPaste}
|
||||
autoFocus={autoFocus}
|
||||
lang={this.props.lang}
|
||||
>
|
||||
<EmojiPickerDropdown onPickEmoji={this.handleEmojiPick} />
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import { EmojiPicker as EmojiPickerAsync } from '../../ui/util/async-components';
|
||||
import Overlay from 'react-overlays/lib/Overlay';
|
||||
import Overlay from 'react-overlays/Overlay';
|
||||
import classNames from 'classnames';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { supportsPassiveEvents } from 'detect-passive-events';
|
||||
|
@ -154,9 +154,6 @@ class EmojiPickerMenu extends React.PureComponent {
|
|||
onClose: PropTypes.func.isRequired,
|
||||
onPick: PropTypes.func.isRequired,
|
||||
style: PropTypes.object,
|
||||
placement: PropTypes.string,
|
||||
arrowOffsetLeft: PropTypes.string,
|
||||
arrowOffsetTop: PropTypes.string,
|
||||
intl: PropTypes.object.isRequired,
|
||||
skinTone: PropTypes.number.isRequired,
|
||||
onSkinTone: PropTypes.func.isRequired,
|
||||
|
@ -324,14 +321,13 @@ class EmojiPickerDropdown extends React.PureComponent {
|
|||
state = {
|
||||
active: false,
|
||||
loading: false,
|
||||
placement: null,
|
||||
};
|
||||
|
||||
setRef = (c) => {
|
||||
this.dropdown = c;
|
||||
}
|
||||
|
||||
onShowDropdown = ({ target }) => {
|
||||
onShowDropdown = () => {
|
||||
this.setState({ active: true });
|
||||
|
||||
if (!EmojiPicker) {
|
||||
|
@ -346,9 +342,6 @@ class EmojiPickerDropdown extends React.PureComponent {
|
|||
this.setState({ loading: false, active: false });
|
||||
});
|
||||
}
|
||||
|
||||
const { top } = target.getBoundingClientRect();
|
||||
this.setState({ placement: top * 2 < innerHeight ? 'bottom' : 'top' });
|
||||
}
|
||||
|
||||
onHideDropdown = () => {
|
||||
|
@ -382,7 +375,7 @@ class EmojiPickerDropdown extends React.PureComponent {
|
|||
render () {
|
||||
const { intl, onPickEmoji, onSkinTone, skinTone, frequentlyUsedEmojis, button } = this.props;
|
||||
const title = intl.formatMessage(messages.emoji);
|
||||
const { active, loading, placement } = this.state;
|
||||
const { active, loading } = this.state;
|
||||
|
||||
return (
|
||||
<div className='emoji-picker-dropdown' onKeyDown={this.handleKeyDown}>
|
||||
|
@ -394,16 +387,22 @@ class EmojiPickerDropdown extends React.PureComponent {
|
|||
/>}
|
||||
</div>
|
||||
|
||||
<Overlay show={active} placement={placement} target={this.findTarget}>
|
||||
<EmojiPickerMenu
|
||||
custom_emojis={this.props.custom_emojis}
|
||||
loading={loading}
|
||||
onClose={this.onHideDropdown}
|
||||
onPick={onPickEmoji}
|
||||
onSkinTone={onSkinTone}
|
||||
skinTone={skinTone}
|
||||
frequentlyUsedEmojis={frequentlyUsedEmojis}
|
||||
/>
|
||||
<Overlay show={active} placement={'bottom'} target={this.findTarget} popperConfig={{ strategy: 'fixed' }}>
|
||||
{({ props, placement })=> (
|
||||
<div {...props} style={{ ...props.style, width: 299 }}>
|
||||
<div className={`dropdown-animation ${placement}`}>
|
||||
<EmojiPickerMenu
|
||||
custom_emojis={this.props.custom_emojis}
|
||||
loading={loading}
|
||||
onClose={this.onHideDropdown}
|
||||
onPick={onPickEmoji}
|
||||
onSkinTone={onSkinTone}
|
||||
skinTone={skinTone}
|
||||
frequentlyUsedEmojis={frequentlyUsedEmojis}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Overlay>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -2,9 +2,7 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import { injectIntl, defineMessages } from 'react-intl';
|
||||
import TextIconButton from './text_icon_button';
|
||||
import Overlay from 'react-overlays/lib/Overlay';
|
||||
import Motion from 'mastodon/features/ui/util/optional_motion';
|
||||
import spring from 'react-motion/lib/spring';
|
||||
import Overlay from 'react-overlays/Overlay';
|
||||
import { supportsPassiveEvents } from 'detect-passive-events';
|
||||
import classNames from 'classnames';
|
||||
import { languages as preloadedLanguages } from 'mastodon/initial_state';
|
||||
|
@ -22,10 +20,8 @@ const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
|
|||
class LanguageDropdownMenu extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
style: PropTypes.object,
|
||||
value: PropTypes.string.isRequired,
|
||||
frequentlyUsedLanguages: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
placement: PropTypes.string.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
languages: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.string)),
|
||||
|
@ -37,7 +33,6 @@ class LanguageDropdownMenu extends React.PureComponent {
|
|||
};
|
||||
|
||||
state = {
|
||||
mounted: false,
|
||||
searchValue: '',
|
||||
};
|
||||
|
||||
|
@ -50,7 +45,6 @@ class LanguageDropdownMenu extends React.PureComponent {
|
|||
componentDidMount () {
|
||||
document.addEventListener('click', this.handleDocumentClick, false);
|
||||
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
||||
this.setState({ mounted: true });
|
||||
|
||||
// Because of https://github.com/react-bootstrap/react-bootstrap/issues/2614 we need
|
||||
// to wait for a frame before focusing
|
||||
|
@ -222,29 +216,22 @@ class LanguageDropdownMenu extends React.PureComponent {
|
|||
}
|
||||
|
||||
render () {
|
||||
const { style, placement, intl } = this.props;
|
||||
const { mounted, searchValue } = this.state;
|
||||
const { intl } = this.props;
|
||||
const { searchValue } = this.state;
|
||||
const isSearching = searchValue !== '';
|
||||
const results = this.search();
|
||||
|
||||
return (
|
||||
<Motion defaultStyle={{ opacity: 0, scaleX: 0.85, scaleY: 0.75 }} style={{ opacity: spring(1, { damping: 35, stiffness: 400 }), scaleX: spring(1, { damping: 35, stiffness: 400 }), scaleY: spring(1, { damping: 35, stiffness: 400 }) }}>
|
||||
{({ opacity, scaleX, scaleY }) => (
|
||||
// It should not be transformed when mounting because the resulting
|
||||
// size will be used to determine the coordinate of the menu by
|
||||
// react-overlays
|
||||
<div className={`language-dropdown__dropdown ${placement}`} style={{ ...style, opacity: opacity, transform: mounted ? `scale(${scaleX}, ${scaleY})` : null }} ref={this.setRef}>
|
||||
<div className='emoji-mart-search'>
|
||||
<input type='search' value={searchValue} onChange={this.handleSearchChange} onKeyDown={this.handleSearchKeyDown} placeholder={intl.formatMessage(messages.search)} />
|
||||
<button type='button' className='emoji-mart-search-icon' disabled={!isSearching} aria-label={intl.formatMessage(messages.clear)} onClick={this.handleClear}>{!isSearching ? loupeIcon : deleteIcon}</button>
|
||||
</div>
|
||||
<div ref={this.setRef}>
|
||||
<div className='emoji-mart-search'>
|
||||
<input type='search' value={searchValue} onChange={this.handleSearchChange} onKeyDown={this.handleSearchKeyDown} placeholder={intl.formatMessage(messages.search)} />
|
||||
<button type='button' className='emoji-mart-search-icon' disabled={!isSearching} aria-label={intl.formatMessage(messages.clear)} onClick={this.handleClear}>{!isSearching ? loupeIcon : deleteIcon}</button>
|
||||
</div>
|
||||
|
||||
<div className='language-dropdown__dropdown__results emoji-mart-scroll' role='listbox' ref={this.setListRef}>
|
||||
{results.map(this.renderItem)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Motion>
|
||||
<div className='language-dropdown__dropdown__results emoji-mart-scroll' role='listbox' ref={this.setListRef}>
|
||||
{results.map(this.renderItem)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -266,14 +253,11 @@ class LanguageDropdown extends React.PureComponent {
|
|||
placement: 'bottom',
|
||||
};
|
||||
|
||||
handleToggle = ({ target }) => {
|
||||
const { top } = target.getBoundingClientRect();
|
||||
|
||||
handleToggle = () => {
|
||||
if (this.state.open && this.activeElement) {
|
||||
this.activeElement.focus({ preventScroll: true });
|
||||
}
|
||||
|
||||
this.setState({ placement: top * 2 < innerHeight ? 'bottom' : 'top' });
|
||||
this.setState({ open: !this.state.open });
|
||||
}
|
||||
|
||||
|
@ -293,13 +277,25 @@ class LanguageDropdown extends React.PureComponent {
|
|||
onChange(value);
|
||||
}
|
||||
|
||||
setTargetRef = c => {
|
||||
this.target = c;
|
||||
}
|
||||
|
||||
findTarget = () => {
|
||||
return this.target;
|
||||
}
|
||||
|
||||
handleOverlayEnter = (state) => {
|
||||
this.setState({ placement: state.placement });
|
||||
}
|
||||
|
||||
render () {
|
||||
const { value, intl, frequentlyUsedLanguages } = this.props;
|
||||
const { open, placement } = this.state;
|
||||
|
||||
return (
|
||||
<div className={classNames('privacy-dropdown', { active: open })}>
|
||||
<div className='privacy-dropdown__value'>
|
||||
<div className={classNames('privacy-dropdown', placement, { active: open })}>
|
||||
<div className='privacy-dropdown__value' ref={this.setTargetRef} >
|
||||
<TextIconButton
|
||||
className='privacy-dropdown__value-icon'
|
||||
label={value && value.toUpperCase()}
|
||||
|
@ -309,15 +305,20 @@ class LanguageDropdown extends React.PureComponent {
|
|||
/>
|
||||
</div>
|
||||
|
||||
<Overlay show={open} placement={placement} target={this}>
|
||||
<LanguageDropdownMenu
|
||||
value={value}
|
||||
frequentlyUsedLanguages={frequentlyUsedLanguages}
|
||||
onClose={this.handleClose}
|
||||
onChange={this.handleChange}
|
||||
placement={placement}
|
||||
intl={intl}
|
||||
/>
|
||||
<Overlay show={open} placement={'bottom'} flip target={this.findTarget} popperConfig={{ strategy: 'fixed', onFirstUpdate: this.handleOverlayEnter }}>
|
||||
{({ props, placement }) => (
|
||||
<div {...props}>
|
||||
<div className={`dropdown-animation language-dropdown__dropdown ${placement}`} >
|
||||
<LanguageDropdownMenu
|
||||
value={value}
|
||||
frequentlyUsedLanguages={frequentlyUsedLanguages}
|
||||
onClose={this.handleClose}
|
||||
onChange={this.handleChange}
|
||||
intl={intl}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Overlay>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -2,9 +2,7 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import { injectIntl, defineMessages } from 'react-intl';
|
||||
import IconButton from '../../../components/icon_button';
|
||||
import Overlay from 'react-overlays/lib/Overlay';
|
||||
import Motion from '../../ui/util/optional_motion';
|
||||
import spring from 'react-motion/lib/spring';
|
||||
import Overlay from 'react-overlays/Overlay';
|
||||
import { supportsPassiveEvents } from 'detect-passive-events';
|
||||
import classNames from 'classnames';
|
||||
import Icon from 'mastodon/components/icon';
|
||||
|
@ -29,15 +27,10 @@ class PrivacyDropdownMenu extends React.PureComponent {
|
|||
style: PropTypes.object,
|
||||
items: PropTypes.array.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
placement: PropTypes.string.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
mounted: false,
|
||||
};
|
||||
|
||||
handleDocumentClick = e => {
|
||||
if (this.node && !this.node.contains(e.target)) {
|
||||
this.props.onClose();
|
||||
|
@ -101,7 +94,6 @@ class PrivacyDropdownMenu extends React.PureComponent {
|
|||
document.addEventListener('click', this.handleDocumentClick, false);
|
||||
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
||||
if (this.focusedItem) this.focusedItem.focus({ preventScroll: true });
|
||||
this.setState({ mounted: true });
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
|
@ -118,31 +110,23 @@ class PrivacyDropdownMenu extends React.PureComponent {
|
|||
}
|
||||
|
||||
render () {
|
||||
const { mounted } = this.state;
|
||||
const { style, items, placement, value } = this.props;
|
||||
const { style, items, value } = this.props;
|
||||
|
||||
return (
|
||||
<Motion defaultStyle={{ opacity: 0, scaleX: 0.85, scaleY: 0.75 }} style={{ opacity: spring(1, { damping: 35, stiffness: 400 }), scaleX: spring(1, { damping: 35, stiffness: 400 }), scaleY: spring(1, { damping: 35, stiffness: 400 }) }}>
|
||||
{({ opacity, scaleX, scaleY }) => (
|
||||
// It should not be transformed when mounting because the resulting
|
||||
// size will be used to determine the coordinate of the menu by
|
||||
// react-overlays
|
||||
<div className={`privacy-dropdown__dropdown ${placement}`} style={{ ...style, opacity: opacity, transform: mounted ? `scale(${scaleX}, ${scaleY})` : null }} role='listbox' ref={this.setRef}>
|
||||
{items.map(item => (
|
||||
<div role='option' tabIndex='0' key={item.value} data-index={item.value} onKeyDown={this.handleKeyDown} onClick={this.handleClick} className={classNames('privacy-dropdown__option', { active: item.value === value })} aria-selected={item.value === value} ref={item.value === value ? this.setFocusRef : null}>
|
||||
<div className='privacy-dropdown__option__icon'>
|
||||
<Icon id={item.icon} fixedWidth />
|
||||
</div>
|
||||
<div style={{ ...style }} role='listbox' ref={this.setRef}>
|
||||
{items.map(item => (
|
||||
<div role='option' tabIndex='0' key={item.value} data-index={item.value} onKeyDown={this.handleKeyDown} onClick={this.handleClick} className={classNames('privacy-dropdown__option', { active: item.value === value })} aria-selected={item.value === value} ref={item.value === value ? this.setFocusRef : null}>
|
||||
<div className='privacy-dropdown__option__icon'>
|
||||
<Icon id={item.icon} fixedWidth />
|
||||
</div>
|
||||
|
||||
<div className='privacy-dropdown__option__content'>
|
||||
<strong>{item.text}</strong>
|
||||
{item.meta}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div className='privacy-dropdown__option__content'>
|
||||
<strong>{item.text}</strong>
|
||||
{item.meta}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Motion>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -168,7 +152,7 @@ class PrivacyDropdown extends React.PureComponent {
|
|||
placement: 'bottom',
|
||||
};
|
||||
|
||||
handleToggle = ({ target }) => {
|
||||
handleToggle = () => {
|
||||
if (this.props.isUserTouching && this.props.isUserTouching()) {
|
||||
if (this.state.open) {
|
||||
this.props.onModalClose();
|
||||
|
@ -179,11 +163,9 @@ class PrivacyDropdown extends React.PureComponent {
|
|||
});
|
||||
}
|
||||
} else {
|
||||
const { top } = target.getBoundingClientRect();
|
||||
if (this.state.open && this.activeElement) {
|
||||
this.activeElement.focus({ preventScroll: true });
|
||||
}
|
||||
this.setState({ placement: top * 2 < innerHeight ? 'bottom' : 'top' });
|
||||
this.setState({ open: !this.state.open });
|
||||
}
|
||||
}
|
||||
|
@ -247,6 +229,18 @@ class PrivacyDropdown extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
setTargetRef = c => {
|
||||
this.target = c;
|
||||
}
|
||||
|
||||
findTarget = () => {
|
||||
return this.target;
|
||||
}
|
||||
|
||||
handleOverlayEnter = (state) => {
|
||||
this.setState({ placement: state.placement });
|
||||
}
|
||||
|
||||
render () {
|
||||
const { value, container, disabled, intl } = this.props;
|
||||
const { open, placement } = this.state;
|
||||
|
@ -255,7 +249,7 @@ class PrivacyDropdown extends React.PureComponent {
|
|||
|
||||
return (
|
||||
<div className={classNames('privacy-dropdown', placement, { active: open })} onKeyDown={this.handleKeyDown}>
|
||||
<div className={classNames('privacy-dropdown__value', { active: this.options.indexOf(valueOption) === (placement === 'bottom' ? 0 : (this.options.length - 1)) })}>
|
||||
<div className={classNames('privacy-dropdown__value', { active: this.options.indexOf(valueOption) === (placement === 'bottom' ? 0 : (this.options.length - 1)) })} ref={this.setTargetRef}>
|
||||
<IconButton
|
||||
className='privacy-dropdown__value-icon'
|
||||
icon={valueOption.icon}
|
||||
|
@ -272,14 +266,19 @@ class PrivacyDropdown extends React.PureComponent {
|
|||
/>
|
||||
</div>
|
||||
|
||||
<Overlay show={open} placement={placement} target={this} container={container}>
|
||||
<PrivacyDropdownMenu
|
||||
items={this.options}
|
||||
value={value}
|
||||
onClose={this.handleClose}
|
||||
onChange={this.handleChange}
|
||||
placement={placement}
|
||||
/>
|
||||
<Overlay show={open} placement={'bottom'} flip target={this.findTarget} container={container} popperConfig={{ strategy: 'fixed', onFirstUpdate: this.handleOverlayEnter }}>
|
||||
{({ props, placement }) => (
|
||||
<div {...props}>
|
||||
<div className={`dropdown-animation privacy-dropdown__dropdown ${placement}`}>
|
||||
<PrivacyDropdownMenu
|
||||
items={this.options}
|
||||
value={value}
|
||||
onClose={this.handleClose}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Overlay>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import Overlay from 'react-overlays/lib/Overlay';
|
||||
import Motion from '../../ui/util/optional_motion';
|
||||
import spring from 'react-motion/lib/spring';
|
||||
import Overlay from 'react-overlays/Overlay';
|
||||
import { searchEnabled } from '../../../initial_state';
|
||||
import Icon from 'mastodon/components/icon';
|
||||
|
||||
|
@ -14,31 +12,20 @@ const messages = defineMessages({
|
|||
|
||||
class SearchPopout extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
style: PropTypes.object,
|
||||
};
|
||||
|
||||
render () {
|
||||
const { style } = this.props;
|
||||
const extraInformation = searchEnabled ? <FormattedMessage id='search_popout.tips.full_text' defaultMessage='Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.' /> : <FormattedMessage id='search_popout.tips.text' defaultMessage='Simple text returns matching display names, usernames and hashtags' />;
|
||||
return (
|
||||
<div style={{ ...style, position: 'absolute', width: 285, zIndex: 2 }}>
|
||||
<Motion defaultStyle={{ opacity: 0, scaleX: 0.85, scaleY: 0.75 }} style={{ opacity: spring(1, { damping: 35, stiffness: 400 }), scaleX: spring(1, { damping: 35, stiffness: 400 }), scaleY: spring(1, { damping: 35, stiffness: 400 }) }}>
|
||||
{({ opacity, scaleX, scaleY }) => (
|
||||
<div className='search-popout' style={{ opacity: opacity, transform: `scale(${scaleX}, ${scaleY})` }}>
|
||||
<h4><FormattedMessage id='search_popout.search_format' defaultMessage='Advanced search format' /></h4>
|
||||
<div className='search-popout'>
|
||||
<h4><FormattedMessage id='search_popout.search_format' defaultMessage='Advanced search format' /></h4>
|
||||
|
||||
<ul>
|
||||
<li><em>#example</em> <FormattedMessage id='search_popout.tips.hashtag' defaultMessage='hashtag' /></li>
|
||||
<li><em>@username@domain</em> <FormattedMessage id='search_popout.tips.user' defaultMessage='user' /></li>
|
||||
<li><em>URL</em> <FormattedMessage id='search_popout.tips.user' defaultMessage='user' /></li>
|
||||
<li><em>URL</em> <FormattedMessage id='search_popout.tips.status' defaultMessage='status' /></li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li><em>#example</em> <FormattedMessage id='search_popout.tips.hashtag' defaultMessage='hashtag' /></li>
|
||||
<li><em>@username@domain</em> <FormattedMessage id='search_popout.tips.user' defaultMessage='user' /></li>
|
||||
<li><em>URL</em> <FormattedMessage id='search_popout.tips.user' defaultMessage='user' /></li>
|
||||
<li><em>URL</em> <FormattedMessage id='search_popout.tips.status' defaultMessage='status' /></li>
|
||||
</ul>
|
||||
|
||||
{extraInformation}
|
||||
</div>
|
||||
)}
|
||||
</Motion>
|
||||
{extraInformation}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -115,6 +102,10 @@ class Search extends React.PureComponent {
|
|||
this.setState({ expanded: false });
|
||||
}
|
||||
|
||||
findTarget = () => {
|
||||
return this.searchForm;
|
||||
}
|
||||
|
||||
render () {
|
||||
const { intl, value, submitted } = this.props;
|
||||
const { expanded } = this.state;
|
||||
|
@ -140,8 +131,14 @@ class Search extends React.PureComponent {
|
|||
<Icon id='search' className={hasValue ? '' : 'active'} />
|
||||
<Icon id='times-circle' className={hasValue ? 'active' : ''} aria-label={intl.formatMessage(messages.placeholder)} />
|
||||
</div>
|
||||
<Overlay show={expanded && !hasValue} placement='bottom' target={this} container={this}>
|
||||
<SearchPopout />
|
||||
<Overlay show={expanded && !hasValue} placement='bottom' target={this.findTarget} popperConfig={{ strategy: 'fixed' }}>
|
||||
{({ props, placement }) => (
|
||||
<div {...props} style={{ ...props.style, width: 285, zIndex: 2 }}>
|
||||
<div className={`dropdown-animation ${placement}`}>
|
||||
<SearchPopout />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Overlay>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -43,10 +43,10 @@ export default class Upload extends ImmutablePureComponent {
|
|||
<div className='compose-form__upload-thumbnail' style={{ transform: `scale(${scale})`, backgroundImage: `url(${media.get('preview_url')})`, backgroundPosition: `${x}% ${y}%` }}>
|
||||
<div className='compose-form__upload__actions'>
|
||||
<button type='button' className='icon-button' onClick={this.handleUndoClick}><Icon id='times' /> <FormattedMessage id='upload_form.undo' defaultMessage='Delete' /></button>
|
||||
{!!media.get('unattached') && (<button type='button' className='icon-button' onClick={this.handleFocalPointClick}><Icon id='pencil' /> <FormattedMessage id='upload_form.edit' defaultMessage='Edit' /></button>)}
|
||||
<button type='button' className='icon-button' onClick={this.handleFocalPointClick}><Icon id='pencil' /> <FormattedMessage id='upload_form.edit' defaultMessage='Edit' /></button>
|
||||
</div>
|
||||
|
||||
{(media.get('description') || '').length === 0 && !!media.get('unattached') && (
|
||||
{(media.get('description') || '').length === 0 && (
|
||||
<div className='compose-form__upload__warning'>
|
||||
<button type='button' className='icon-button' onClick={this.handleFocalPointClick}><Icon id='info-circle' /> <FormattedMessage id='upload_form.description_missing' defaultMessage='No description added' /></button>
|
||||
</div>
|
||||
|
|
|
@ -26,6 +26,7 @@ const mapStateToProps = state => ({
|
|||
isUploading: state.getIn(['compose', 'is_uploading']),
|
||||
anyMedia: state.getIn(['compose', 'media_attachments']).size > 0,
|
||||
isInReply: state.getIn(['compose', 'in_reply_to']) !== null,
|
||||
lang: state.getIn(['compose', 'language']),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
|
|
89
app/javascript/mastodon/features/followed_tags/index.js
Normal file
89
app/javascript/mastodon/features/followed_tags/index.js
Normal file
|
@ -0,0 +1,89 @@
|
|||
import { debounce } from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import { connect } from 'react-redux';
|
||||
import ColumnHeader from 'mastodon/components/column_header';
|
||||
import ScrollableList from 'mastodon/components/scrollable_list';
|
||||
import Column from 'mastodon/features/ui/components/column';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import Hashtag from 'mastodon/components/hashtag';
|
||||
import { expandFollowedHashtags, fetchFollowedHashtags } from 'mastodon/actions/tags';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'followed_tags', defaultMessage: 'Followed hashtags' },
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
hashtags: state.getIn(['followed_tags', 'items']),
|
||||
isLoading: state.getIn(['followed_tags', 'isLoading'], true),
|
||||
hasMore: !!state.getIn(['followed_tags', 'next']),
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
@injectIntl
|
||||
class FollowedTags extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
hashtags: ImmutablePropTypes.list,
|
||||
isLoading: PropTypes.bool,
|
||||
hasMore: PropTypes.bool,
|
||||
multiColumn: PropTypes.bool,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.props.dispatch(fetchFollowedHashtags());
|
||||
};
|
||||
|
||||
handleLoadMore = debounce(() => {
|
||||
this.props.dispatch(expandFollowedHashtags());
|
||||
}, 300, { leading: true });
|
||||
|
||||
render () {
|
||||
const { intl, hashtags, isLoading, hasMore, multiColumn } = this.props;
|
||||
|
||||
const emptyMessage = <FormattedMessage id='empty_column.followed_tags' defaultMessage='You have not followed any hashtags yet. When you do, they will show up here.' />;
|
||||
|
||||
return (
|
||||
<Column bindToDocument={!multiColumn}>
|
||||
<ColumnHeader
|
||||
icon='hashtag'
|
||||
title={intl.formatMessage(messages.heading)}
|
||||
showBackButton
|
||||
multiColumn={multiColumn}
|
||||
/>
|
||||
|
||||
<ScrollableList
|
||||
scrollKey='followed_tags'
|
||||
emptyMessage={emptyMessage}
|
||||
hasMore={hasMore}
|
||||
isLoading={isLoading}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
bindToDocument={!multiColumn}
|
||||
>
|
||||
{hashtags.map((hashtag) => (
|
||||
<Hashtag
|
||||
key={hashtag.get('name')}
|
||||
name={hashtag.get('name')}
|
||||
to={`/tags/${hashtag.get('name')}`}
|
||||
withGraph={false}
|
||||
// Taken from ImmutableHashtag. Should maybe refactor ImmutableHashtag to accept more options?
|
||||
people={hashtag.getIn(['history', 0, 'accounts']) * 1 + hashtag.getIn(['history', 1, 'accounts']) * 1}
|
||||
history={hashtag.get('history').reverse().map((day) => day.get('uses')).toArray()}
|
||||
/>
|
||||
))}
|
||||
</ScrollableList>
|
||||
|
||||
<Helmet>
|
||||
<meta name='robots' content='noindex' />
|
||||
</Helmet>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -246,7 +246,11 @@ class Notification extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
renderStatus (notification, link) {
|
||||
const { intl, unread } = this.props;
|
||||
const { intl, unread, status } = this.props;
|
||||
|
||||
if (!status) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<HotKeys handlers={this.getHandlers()}>
|
||||
|
@ -264,6 +268,7 @@ class Notification extends ImmutablePureComponent {
|
|||
<StatusContainer
|
||||
id={notification.get('status')}
|
||||
account={notification.get('account')}
|
||||
contextType='notifications'
|
||||
muted
|
||||
withDismiss
|
||||
hidden={this.props.hidden}
|
||||
|
@ -278,7 +283,11 @@ class Notification extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
renderUpdate (notification, link) {
|
||||
const { intl, unread } = this.props;
|
||||
const { intl, unread, status } = this.props;
|
||||
|
||||
if (!status) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<HotKeys handlers={this.getHandlers()}>
|
||||
|
@ -296,6 +305,7 @@ class Notification extends ImmutablePureComponent {
|
|||
<StatusContainer
|
||||
id={notification.get('status')}
|
||||
account={notification.get('account')}
|
||||
contextType='notifications'
|
||||
muted
|
||||
withDismiss
|
||||
hidden={this.props.hidden}
|
||||
|
@ -310,10 +320,14 @@ class Notification extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
renderPoll (notification, account) {
|
||||
const { intl, unread } = this.props;
|
||||
const { intl, unread, status } = this.props;
|
||||
const ownPoll = me === account.get('id');
|
||||
const message = ownPoll ? intl.formatMessage(messages.ownPoll) : intl.formatMessage(messages.poll);
|
||||
|
||||
if (!status) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<HotKeys handlers={this.getHandlers()}>
|
||||
<div className={classNames('notification notification-poll focusable', { unread })} tabIndex='0' aria-label={notificationForScreenReader(intl, message, notification.get('created_at'))}>
|
||||
|
@ -334,6 +348,7 @@ class Notification extends ImmutablePureComponent {
|
|||
<StatusContainer
|
||||
id={notification.get('status')}
|
||||
account={account}
|
||||
contextType='notifications'
|
||||
muted
|
||||
withDismiss
|
||||
hidden={this.props.hidden}
|
||||
|
|
|
@ -24,7 +24,7 @@ const makeMapStateToProps = () => {
|
|||
const notification = getNotification(state, props.notification, props.accountId);
|
||||
return {
|
||||
notification: notification,
|
||||
status: notification.get('status') ? getStatus(state, { id: notification.get('status') }) : null,
|
||||
status: notification.get('status') ? getStatus(state, { id: notification.get('status'), contextType: 'notifications' }) : null,
|
||||
report: notification.get('report') ? getReport(state, notification.get('report'), notification.getIn(['report', 'target_account', 'id'])) : null,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -320,7 +320,7 @@ class FocalPointModal extends ImmutablePureComponent {
|
|||
<React.Fragment>
|
||||
<label className='setting-text-label' htmlFor='upload-modal__thumbnail'><FormattedMessage id='upload_form.thumbnail' defaultMessage='Change thumbnail' /></label>
|
||||
|
||||
<Button disabled={isUploadingThumbnail} text={intl.formatMessage(messages.chooseImage)} onClick={this.handleFileInputClick} />
|
||||
<Button disabled={isUploadingThumbnail || !media.get('unattached')} text={intl.formatMessage(messages.chooseImage)} onClick={this.handleFileInputClick} />
|
||||
|
||||
<label>
|
||||
<span style={{ display: 'none' }}>{intl.formatMessage(messages.chooseImage)}</span>
|
||||
|
|
|
@ -52,6 +52,8 @@ class LinkFooter extends React.PureComponent {
|
|||
const canInvite = signedIn && ((permissions & PERMISSION_INVITE_USERS) === PERMISSION_INVITE_USERS);
|
||||
const canProfileDirectory = profileDirectory;
|
||||
|
||||
const DividingCircle = <span aria-hidden>{' · '}</span>;
|
||||
|
||||
return (
|
||||
<div className='link-footer'>
|
||||
<p>
|
||||
|
@ -60,17 +62,17 @@ class LinkFooter extends React.PureComponent {
|
|||
<Link key='about' to='/about'><FormattedMessage id='footer.about' defaultMessage='About' /></Link>
|
||||
{canInvite && (
|
||||
<>
|
||||
{' · '}
|
||||
{DividingCircle}
|
||||
<a key='invites' href='/invites' target='_blank'><FormattedMessage id='footer.invite' defaultMessage='Invite people' /></a>
|
||||
</>
|
||||
)}
|
||||
{canProfileDirectory && (
|
||||
<>
|
||||
{' · '}
|
||||
{DividingCircle}
|
||||
<Link key='directory' to='/directory'><FormattedMessage id='footer.directory' defaultMessage='Profiles directory' /></Link>
|
||||
</>
|
||||
)}
|
||||
{' · '}
|
||||
{DividingCircle}
|
||||
<Link key='privacy-policy' to='/privacy-policy'><FormattedMessage id='footer.privacy_policy' defaultMessage='Privacy policy' /></Link>
|
||||
</p>
|
||||
|
||||
|
@ -78,13 +80,13 @@ class LinkFooter extends React.PureComponent {
|
|||
<strong>Mastodon</strong>:
|
||||
{' '}
|
||||
<a href='https://joinmastodon.org' target='_blank'><FormattedMessage id='footer.about' defaultMessage='About' /></a>
|
||||
{' · '}
|
||||
{DividingCircle}
|
||||
<a href='https://joinmastodon.org/apps' target='_blank'><FormattedMessage id='footer.get_app' defaultMessage='Get the app' /></a>
|
||||
{' · '}
|
||||
{DividingCircle}
|
||||
<Link to='/keyboard-shortcuts'><FormattedMessage id='footer.keyboard_shortcuts' defaultMessage='Keyboard shortcuts' /></Link>
|
||||
{' · '}
|
||||
{DividingCircle}
|
||||
<a href={source_url} rel='noopener noreferrer' target='_blank'><FormattedMessage id='footer.source_code' defaultMessage='View source code' /></a>
|
||||
{' · '}
|
||||
{DividingCircle}
|
||||
v{version}
|
||||
</p>
|
||||
</div>
|
||||
|
|
|
@ -30,7 +30,7 @@ const SignInBanner = () => {
|
|||
|
||||
return (
|
||||
<div className='sign-in-banner'>
|
||||
<p><FormattedMessage id='sign_in_banner.text' defaultMessage='Sign in to follow profiles or hashtags, favourite, share and reply to posts, or interact from your account on a different server.' /></p>
|
||||
<p><FormattedMessage id='sign_in_banner.text' defaultMessage='Sign in to follow profiles or hashtags, favourite, share and reply to posts. You can also interact from your account on a different server.' /></p>
|
||||
<a href='/auth/sign_in' className='button button--block'><FormattedMessage id='sign_in_banner.sign_in' defaultMessage='Sign in' /></a>
|
||||
{signupButton}
|
||||
</div>
|
||||
|
|
|
@ -46,6 +46,7 @@ export default class VideoModal extends ImmutablePureComponent {
|
|||
autoPlay={options.autoPlay}
|
||||
volume={options.defaultVolume}
|
||||
onCloseVideo={onClose}
|
||||
autoFocus
|
||||
detailed
|
||||
alt={media.get('description')}
|
||||
/>
|
||||
|
|
|
@ -42,6 +42,7 @@ import {
|
|||
FollowRequests,
|
||||
FavouritedStatuses,
|
||||
BookmarkedStatuses,
|
||||
FollowedTags,
|
||||
ListTimeline,
|
||||
Blocks,
|
||||
DomainBlocks,
|
||||
|
@ -54,7 +55,7 @@ import {
|
|||
About,
|
||||
PrivacyPolicy,
|
||||
} from './util/async-components';
|
||||
import initialState, { me, owner, singleUserMode, showTrends } from '../../initial_state';
|
||||
import initialState, { me, owner, singleUserMode, showTrends, trendsAsLanding } from '../../initial_state';
|
||||
import { closeOnboarding, INTRODUCTION_VERSION } from 'mastodon/actions/onboarding';
|
||||
import Header from './components/header';
|
||||
|
||||
|
@ -163,7 +164,7 @@ class SwitchingColumnsArea extends React.PureComponent {
|
|||
}
|
||||
} else if (singleUserMode && owner && initialState?.accounts[owner]) {
|
||||
redirect = <Redirect from='/' to={`/@${initialState.accounts[owner].username}`} exact />;
|
||||
} else if (showTrends) {
|
||||
} else if (showTrends && trendsAsLanding) {
|
||||
redirect = <Redirect from='/' to='/explore' exact />;
|
||||
} else {
|
||||
redirect = <Redirect from='/' to='/about' exact />;
|
||||
|
@ -216,6 +217,7 @@ class SwitchingColumnsArea extends React.PureComponent {
|
|||
<WrappedRoute path='/follow_requests' component={FollowRequests} content={children} />
|
||||
<WrappedRoute path='/blocks' component={Blocks} content={children} />
|
||||
<WrappedRoute path='/domain_blocks' component={DomainBlocks} content={children} />
|
||||
<WrappedRoute path='/followed_tags' component={FollowedTags} content={children} />
|
||||
<WrappedRoute path='/mutes' component={Mutes} content={children} />
|
||||
<WrappedRoute path='/lists' component={Lists} content={children} />
|
||||
|
||||
|
|
|
@ -90,6 +90,10 @@ export function FavouritedStatuses () {
|
|||
return import(/* webpackChunkName: "features/favourited_statuses" */'../../favourited_statuses');
|
||||
}
|
||||
|
||||
export function FollowedTags () {
|
||||
return import(/* webpackChunkName: "features/followed_tags" */'../../followed_tags');
|
||||
}
|
||||
|
||||
export function BookmarkedStatuses () {
|
||||
return import(/* webpackChunkName: "features/bookmarked_statuses" */'../../bookmarked_statuses');
|
||||
}
|
||||
|
|
|
@ -122,6 +122,7 @@ class Video extends React.PureComponent {
|
|||
volume: PropTypes.number,
|
||||
muted: PropTypes.bool,
|
||||
componentIndex: PropTypes.number,
|
||||
autoFocus: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
|
@ -523,7 +524,7 @@ class Video extends React.PureComponent {
|
|||
}
|
||||
|
||||
render () {
|
||||
const { preview, src, inline, onOpenVideo, onCloseVideo, intl, alt, detailed, sensitive, editable, blurhash } = this.props;
|
||||
const { preview, src, inline, onOpenVideo, onCloseVideo, intl, alt, detailed, sensitive, editable, blurhash, autoFocus } = this.props;
|
||||
const { containerWidth, currentTime, duration, volume, buffer, dragging, paused, fullscreen, hovered, muted, revealed } = this.state;
|
||||
const progress = Math.min((currentTime / duration) * 100, 100);
|
||||
const playerStyle = {};
|
||||
|
@ -617,7 +618,7 @@ class Video extends React.PureComponent {
|
|||
|
||||
<div className='video-player__buttons-bar'>
|
||||
<div className='video-player__buttons left'>
|
||||
<button type='button' title={intl.formatMessage(paused ? messages.play : messages.pause)} aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} className='player-button' onClick={this.togglePlay} autoFocus={detailed}><Icon id={paused ? 'play' : 'pause'} fixedWidth /></button>
|
||||
<button type='button' title={intl.formatMessage(paused ? messages.play : messages.pause)} aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} className='player-button' onClick={this.togglePlay} autoFocus={autoFocus}><Icon id={paused ? 'play' : 'pause'} fixedWidth /></button>
|
||||
<button type='button' title={intl.formatMessage(muted ? messages.unmute : messages.mute)} aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} className='player-button' onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button>
|
||||
|
||||
<div className={classNames('video-player__volume', { active: this.state.hovered })} onMouseDown={this.handleVolumeMouseDown} ref={this.setVolumeRef}>
|
||||
|
|
|
@ -75,6 +75,7 @@
|
|||
* @property {boolean} timeline_preview
|
||||
* @property {string} title
|
||||
* @property {boolean} trends
|
||||
* @property {boolean} trends_as_landing_page
|
||||
* @property {boolean} unfollow_modal
|
||||
* @property {boolean} use_blurhash
|
||||
* @property {boolean=} use_pending_items
|
||||
|
@ -126,6 +127,7 @@ export const singleUserMode = getMeta('single_user_mode');
|
|||
export const source_url = getMeta('source_url');
|
||||
export const timelinePreview = getMeta('timeline_preview');
|
||||
export const title = getMeta('title');
|
||||
export const trendsAsLanding = getMeta('trends_as_landing_page');
|
||||
export const unfollowModal = getMeta('unfollow_modal');
|
||||
export const useBlurhash = getMeta('use_blurhash');
|
||||
export const usePendingItems = getMeta('use_pending_items');
|
||||
|
|
|
@ -128,7 +128,7 @@
|
|||
"compose.language.search": "Soek tale...",
|
||||
"compose_form.direct_message_warning_learn_more": "Leer meer",
|
||||
"compose_form.encryption_warning": "Plasings op Mastodon is nie van punt tot punt versleutel nie. Moet geen sensitiewe inligting op Mastodon deel nie.",
|
||||
"compose_form.hashtag_warning": "Hierdie plasing is ongelys en sal dus onder geen hutsetiket verskyn nie. Slegs openbare plasings is vindbaar wanneer daar na hutsetikette gesoek word.",
|
||||
"compose_form.hashtag_warning": "This post won't be listed under any hashtag as it is unlisted. Only public posts can be searched by hashtag.",
|
||||
"compose_form.lock_disclaimer": "Jou rekening is nie {locked} nie. Enigiemand kan jou volg en sien wat jy vir jou volgers plaas.",
|
||||
"compose_form.lock_disclaimer.lock": "gesluit",
|
||||
"compose_form.placeholder": "Wat wil jy deel?",
|
||||
|
@ -221,6 +221,7 @@
|
|||
"empty_column.favourites": "Hierdie plasing het nog nie goedkeurings ontvang nie. As iemand dit as gunsteling merk, sien jy dit hier.",
|
||||
"empty_column.follow_recommendations": "Looks like no suggestions could be generated for you. You can try using search to look for people you might know or explore trending hashtags.",
|
||||
"empty_column.follow_requests": "Jy het nog geen volgversoeke nie. Wanneer jy een ontvang, sal dit hier vertoon.",
|
||||
"empty_column.followed_tags": "You have not followed any hashtags yet. When you do, they will show up here.",
|
||||
"empty_column.hashtag": "Daar is nog niks vir hierdie hutsetiket nie.",
|
||||
"empty_column.home": "Your home timeline is empty! Follow more people to fill it up. {suggestions}",
|
||||
"empty_column.home.suggestions": "See some suggestions",
|
||||
|
@ -263,6 +264,7 @@
|
|||
"follow_request.authorize": "Authorize",
|
||||
"follow_request.reject": "Reject",
|
||||
"follow_requests.unlocked_explanation": "Even though your account is not locked, the {domain} staff thought you might want to review follow requests from these accounts manually.",
|
||||
"followed_tags": "Followed hashtags",
|
||||
"footer.about": "Oor",
|
||||
"footer.directory": "Profielgids",
|
||||
"footer.get_app": "Kry die app",
|
||||
|
@ -379,6 +381,7 @@
|
|||
"navigation_bar.favourites": "Gunstelinge",
|
||||
"navigation_bar.filters": "Muted words",
|
||||
"navigation_bar.follow_requests": "Follow requests",
|
||||
"navigation_bar.followed_tags": "Followed hashtags",
|
||||
"navigation_bar.follows_and_followers": "Follows and followers",
|
||||
"navigation_bar.lists": "Lyste",
|
||||
"navigation_bar.logout": "Teken uit",
|
||||
|
@ -540,8 +543,9 @@
|
|||
"server_banner.server_stats": "Server stats:",
|
||||
"sign_in_banner.create_account": "Create account",
|
||||
"sign_in_banner.sign_in": "Sign in",
|
||||
"sign_in_banner.text": "Sign in to follow profiles or hashtags, favourite, share and reply to posts, or interact from your account on a different server.",
|
||||
"sign_in_banner.text": "Sign in to follow profiles or hashtags, favourite, share and reply to posts. You can also interact from your account on a different server.",
|
||||
"status.admin_account": "Open moderation interface for @{name}",
|
||||
"status.admin_domain": "Open moderation interface for {domain}",
|
||||
"status.admin_status": "Open hierdie plasing as moderator",
|
||||
"status.block": "Block @{name}",
|
||||
"status.bookmark": "Bookmark",
|
||||
|
@ -558,7 +562,7 @@
|
|||
"status.favourite": "Favourite",
|
||||
"status.filter": "Filter this post",
|
||||
"status.filtered": "Filtered",
|
||||
"status.hide": "Hide toot",
|
||||
"status.hide": "Hide post",
|
||||
"status.history.created": "{name} created {date}",
|
||||
"status.history.edited": "{name} edited {date}",
|
||||
"status.load_more": "Load more",
|
||||
|
|
|
@ -54,7 +54,7 @@
|
|||
"account.posts_with_replies": "Publicacions y respuestas",
|
||||
"account.report": "Denunciar a @{name}",
|
||||
"account.requested": "Esperando l'aprebación",
|
||||
"account.requested_follow": "{name} has requested to follow you",
|
||||
"account.requested_follow": "{name} ha demandau seguir-te",
|
||||
"account.share": "Compartir lo perfil de @{name}",
|
||||
"account.show_reblogs": "Amostrar retutz de @{name}",
|
||||
"account.statuses_counter": "{count, plural, one {{counter} Publicación} other {{counter} Publicaciones}}",
|
||||
|
@ -128,7 +128,7 @@
|
|||
"compose.language.search": "Buscar idiomas...",
|
||||
"compose_form.direct_message_warning_learn_more": "Aprender mas",
|
||||
"compose_form.encryption_warning": "Las publicacions en Mastodon no son zifradas de cabo a cabo. No comparta garra información sensible en Mastodon.",
|
||||
"compose_form.hashtag_warning": "Esta publicación no s'amostrará baixo garra hashtag perque no ye listada. Nomás las publicacions publicas se pueden buscar per hashtag.",
|
||||
"compose_form.hashtag_warning": "Este tut no s'amostrará en cada etiqueta, ya que no ye publico. Nomás los tuts publicos se pueden buscar per etiqueta.",
|
||||
"compose_form.lock_disclaimer": "La tuya cuenta no ye {locked}. Totz pueden seguir-te pa veyer las tuyas publicacions nomás pa seguidores.",
|
||||
"compose_form.lock_disclaimer.lock": "blocau",
|
||||
"compose_form.placeholder": "En qué yes pensando?",
|
||||
|
@ -221,6 +221,7 @@
|
|||
"empty_column.favourites": "Dengún ha marcau esta publicación como favorita. Quan belún lo faiga, amaneixerá aquí.",
|
||||
"empty_column.follow_recommendations": "Pareixe que no s'ha puesto chenerar garra sucherencia pa tu. Puetz prebar a buscar a chent que talment conoixcas u explorar los hashtags que son en tendencia.",
|
||||
"empty_column.follow_requests": "No tiens garra petición de seguidor. Quan recibas una, s'amostrará aquí.",
|
||||
"empty_column.followed_tags": "You have not followed any hashtags yet. When you do, they will show up here.",
|
||||
"empty_column.hashtag": "No i hai cosa en este hashtag encara.",
|
||||
"empty_column.home": "La tuya linia temporal ye vueda! Sigue a mas personas pa replenar-la. {suggestions}",
|
||||
"empty_column.home.suggestions": "Veyer qualques sucherencias",
|
||||
|
@ -236,11 +237,11 @@
|
|||
"errors.unexpected_crash.copy_stacktrace": "Copiar lo seguimiento de pila en o portafuellas",
|
||||
"errors.unexpected_crash.report_issue": "Informar d'un problema/error",
|
||||
"explore.search_results": "Resultaus de busqueda",
|
||||
"explore.suggested_follows": "For you",
|
||||
"explore.suggested_follows": "Pa tu",
|
||||
"explore.title": "Explorar",
|
||||
"explore.trending_links": "News",
|
||||
"explore.trending_statuses": "Posts",
|
||||
"explore.trending_tags": "Hashtags",
|
||||
"explore.trending_links": "Noticias",
|
||||
"explore.trending_statuses": "Publicacions",
|
||||
"explore.trending_tags": "Etiquetas",
|
||||
"filter_modal.added.context_mismatch_explanation": "Esta categoría de filtro no s'aplica a lo contexto en o qual ha accediu a esta publlicación. Si quiers que la publicación sía filtrada tamién en este contexto, habrás d'editar lo filtro.",
|
||||
"filter_modal.added.context_mismatch_title": "Lo contexto no coincide!",
|
||||
"filter_modal.added.expired_explanation": "Esta categoría de filtro ha caducau, amenesterá cambiar la calendata de caducidat pa que s'aplique.",
|
||||
|
@ -263,6 +264,7 @@
|
|||
"follow_request.authorize": "Autorizar",
|
||||
"follow_request.reject": "Refusar",
|
||||
"follow_requests.unlocked_explanation": "Tot y que la tuya cuenta no ye privada, lo personal de {domain} ha pensau que talment habrías de revisar manualment las solicitutz de seguimiento d'estas cuentas.",
|
||||
"followed_tags": "Followed hashtags",
|
||||
"footer.about": "Sobre",
|
||||
"footer.directory": "Directorio de perfils",
|
||||
"footer.get_app": "Obtener l'aplicación",
|
||||
|
@ -379,6 +381,7 @@
|
|||
"navigation_bar.favourites": "Favoritos",
|
||||
"navigation_bar.filters": "Parolas silenciadas",
|
||||
"navigation_bar.follow_requests": "Solicitutz pa seguir-te",
|
||||
"navigation_bar.followed_tags": "Followed hashtags",
|
||||
"navigation_bar.follows_and_followers": "Seguindo y seguidores",
|
||||
"navigation_bar.lists": "Listas",
|
||||
"navigation_bar.logout": "Zarrar sesión",
|
||||
|
@ -540,8 +543,9 @@
|
|||
"server_banner.server_stats": "Estatisticas d'o servidor:",
|
||||
"sign_in_banner.create_account": "Creyar cuenta",
|
||||
"sign_in_banner.sign_in": "Iniciar sesión",
|
||||
"sign_in_banner.text": "Inicia sesión en este servidor pa seguir perfils u etiquetas, alzar, compartir y responder a mensaches. Tamién puetz interactuar dende unatra cuenta en un servidor diferent.",
|
||||
"sign_in_banner.text": "Sign in to follow profiles or hashtags, favourite, share and reply to posts. You can also interact from your account on a different server.",
|
||||
"status.admin_account": "Ubrir interficie de moderación pa @{name}",
|
||||
"status.admin_domain": "Ubrir interficie de moderación pa {domain}",
|
||||
"status.admin_status": "Ubrir este estau en a interficie de moderación",
|
||||
"status.block": "Blocar a @{name}",
|
||||
"status.bookmark": "Anyadir marcador",
|
||||
|
@ -558,7 +562,7 @@
|
|||
"status.favourite": "Favorito",
|
||||
"status.filter": "Filtrar esta publicación",
|
||||
"status.filtered": "Filtrau",
|
||||
"status.hide": "Amagar publicación",
|
||||
"status.hide": "Amagar la publicación",
|
||||
"status.history.created": "{name} creyó {date}",
|
||||
"status.history.edited": "{name} editó {date}",
|
||||
"status.load_more": "Cargar mas",
|
||||
|
|
|
@ -128,7 +128,7 @@
|
|||
"compose.language.search": "البحث عن لغة…",
|
||||
"compose_form.direct_message_warning_learn_more": "تَعَلَّم المَزيد",
|
||||
"compose_form.encryption_warning": "إنّ المنشورات على ماستدون ليست مشفرة من النهاية إلى النهاية. لا تشارك أي معلومات حساسة عبر ماستدون.",
|
||||
"compose_form.hashtag_warning": "لن يُدرَج هذا المنشور تحت أي وسم بما أنَّه غير مُدرَج. فقط المنشورات العامة يُمكن البحث عنها بواسطة الوسم.",
|
||||
"compose_form.hashtag_warning": "This post won't be listed under any hashtag as it is unlisted. Only public posts can be searched by hashtag.",
|
||||
"compose_form.lock_disclaimer": "حسابُك غير {locked}. يُمكن لأي شخص مُتابعتك لرؤية (منشورات المتابعين فقط).",
|
||||
"compose_form.lock_disclaimer.lock": "مُقفَل",
|
||||
"compose_form.placeholder": "فِيمَ تُفكِّر؟",
|
||||
|
@ -221,6 +221,7 @@
|
|||
"empty_column.favourites": "لم يقم أي أحد بالإعجاب بهذا المنشور بعد. عندما يقوم أحدهم بذلك سوف يظهر هنا.",
|
||||
"empty_column.follow_recommendations": "يبدو أنه لا يمكن إنشاء أي اقتراحات لك. يمكنك البحث عن أشخاص قد تعرفهم أو استكشاف الوسوم الرائجة.",
|
||||
"empty_column.follow_requests": "ليس عندك أي طلب للمتابعة بعد. سوف تظهر طلباتك هنا إن قمت بتلقي البعض منها.",
|
||||
"empty_column.followed_tags": "You have not followed any hashtags yet. When you do, they will show up here.",
|
||||
"empty_column.hashtag": "ليس هناك بعدُ أي محتوى ذو علاقة بهذا الوسم.",
|
||||
"empty_column.home": "إنّ الخيط الزمني لصفحتك الرئيسية فارغ. قم بزيارة {public} أو استخدم حقل البحث لكي تكتشف مستخدمين آخرين.",
|
||||
"empty_column.home.suggestions": "شاهد بعض الاقتراحات",
|
||||
|
@ -263,6 +264,7 @@
|
|||
"follow_request.authorize": "ترخيص",
|
||||
"follow_request.reject": "رفض",
|
||||
"follow_requests.unlocked_explanation": "على الرغم من أن حسابك غير مقفل، فإن موظفين الـ{domain} ظنوا أنك قد ترغب في مراجعة طلبات المتابعة من هذه الحسابات يدوياً.",
|
||||
"followed_tags": "الوسوم المتابَعة",
|
||||
"footer.about": "حَول",
|
||||
"footer.directory": "دليل الصفحات التعريفية",
|
||||
"footer.get_app": "احصل على التطبيق",
|
||||
|
@ -379,6 +381,7 @@
|
|||
"navigation_bar.favourites": "المفضلة",
|
||||
"navigation_bar.filters": "الكلمات المكتومة",
|
||||
"navigation_bar.follow_requests": "طلبات المتابعة",
|
||||
"navigation_bar.followed_tags": "الوسوم المتابَعة",
|
||||
"navigation_bar.follows_and_followers": "المتابِعين والمتابَعون",
|
||||
"navigation_bar.lists": "القوائم",
|
||||
"navigation_bar.logout": "خروج",
|
||||
|
@ -540,8 +543,9 @@
|
|||
"server_banner.server_stats": "إحصائيات الخادم:",
|
||||
"sign_in_banner.create_account": "أنشئ حسابًا",
|
||||
"sign_in_banner.sign_in": "تسجيل الدخول",
|
||||
"sign_in_banner.text": "قم بالولوج بحسابك لمتابعة الصفحات الشخصية أو الوسوم، أو لإضافة الرسائل إلى المفضلة ومشاركتها والرد عليها أو التفاعل بواسطة حسابك المتواجد على خادم مختلف.",
|
||||
"sign_in_banner.text": "Sign in to follow profiles or hashtags, favourite, share and reply to posts. You can also interact from your account on a different server.",
|
||||
"status.admin_account": "افتح الواجهة الإدارية لـ @{name}",
|
||||
"status.admin_domain": "فتح واجهة الإشراف لـ {domain}",
|
||||
"status.admin_status": "افتح هذا المنشور على واجهة الإشراف",
|
||||
"status.block": "احجب @{name}",
|
||||
"status.bookmark": "أضفه إلى الفواصل المرجعية",
|
||||
|
@ -558,7 +562,7 @@
|
|||
"status.favourite": "أضف إلى المفضلة",
|
||||
"status.filter": "تصفية هذه الرسالة",
|
||||
"status.filtered": "مُصفّى",
|
||||
"status.hide": "اخف التبويق",
|
||||
"status.hide": "إخفاء المنشور",
|
||||
"status.history.created": "أنشأه {name} {date}",
|
||||
"status.history.edited": "عدله {name} {date}",
|
||||
"status.load_more": "حمّل المزيد",
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
"about.disclaimer": "Mastodon ye software gratuito ya de códigu llibre, ya una marca rexistrada de Mastodon gGmbH.",
|
||||
"about.domain_blocks.no_reason_available": "El motivu nun ta disponible",
|
||||
"about.domain_blocks.preamble": "Polo xeneral, Mastodon permítete ver el conteníu ya interactuar colos perfiles d'otros sirvidores nel fediversu. Estes son les esceiciones que se ficieron nesti sirvidor.",
|
||||
"about.domain_blocks.silenced.explanation": "Polo xeneral, nun ves los perfiles ya el conteníu d'esti sirvidor sacante que los busques o decidas siguilos.",
|
||||
"about.domain_blocks.silenced.explanation": "Polo xeneral, nun ves los perfiles ya'l conteníu d'esti sirvidor sacante que los busques o decidas siguilos.",
|
||||
"about.domain_blocks.silenced.title": "Llendóse",
|
||||
"about.domain_blocks.suspended.explanation": "Nun se procesa, atroxa nin intercambia nengún datu d'esti sirvidor, lo que fai que cualesquier interaición o comunicación colos sos perfiles seya imposible.",
|
||||
"about.domain_blocks.suspended.title": "Suspendióse",
|
||||
|
@ -54,7 +54,7 @@
|
|||
"account.posts_with_replies": "Artículos ya rempuestes",
|
||||
"account.report": "Report @{name}",
|
||||
"account.requested": "Awaiting approval. Click to cancel follow request",
|
||||
"account.requested_follow": "{name} has requested to follow you",
|
||||
"account.requested_follow": "{name} solicitó siguite",
|
||||
"account.share": "Share @{name}'s profile",
|
||||
"account.show_reblogs": "Amosar los artículos compartíos de @{name}",
|
||||
"account.statuses_counter": "{count, plural, one {{counter} artículu} other {{counter} artículos}}",
|
||||
|
@ -221,6 +221,7 @@
|
|||
"empty_column.favourites": "No one has favourited this post yet. When someone does, they will show up here.",
|
||||
"empty_column.follow_recommendations": "Paez que nun se puen xenerar suxerencies pa ti. Pues tentar d'usar la busca p'atopar perfiles que pues conocer o esplorar les etiquetes en tendencia.",
|
||||
"empty_column.follow_requests": "Entá nun tienes nenguna solicitú de siguimientu. Cuando recibas una, apaez equí.",
|
||||
"empty_column.followed_tags": "You have not followed any hashtags yet. When you do, they will show up here.",
|
||||
"empty_column.hashtag": "Entá nun hai nada con esta etiqueta.",
|
||||
"empty_column.home": "Your home timeline is empty! Follow more people to fill it up. {suggestions}",
|
||||
"empty_column.home.suggestions": "See some suggestions",
|
||||
|
@ -263,6 +264,7 @@
|
|||
"follow_request.authorize": "Autorizar",
|
||||
"follow_request.reject": "Refugar",
|
||||
"follow_requests.unlocked_explanation": "Magar que la to cuenta nun seya privada, el personal del dominiu «{domain}» pensó qu'a lo meyor quies revisar manualmente les solicitúes de siguimientu d'estes cuentes.",
|
||||
"followed_tags": "Followed hashtags",
|
||||
"footer.about": "Tocante a",
|
||||
"footer.directory": "Direutoriu de perfiles",
|
||||
"footer.get_app": "Consiguir l'aplicación",
|
||||
|
@ -379,6 +381,7 @@
|
|||
"navigation_bar.favourites": "Favoritos",
|
||||
"navigation_bar.filters": "Pallabres desactivaes",
|
||||
"navigation_bar.follow_requests": "Solicitúes de siguimientu",
|
||||
"navigation_bar.followed_tags": "Followed hashtags",
|
||||
"navigation_bar.follows_and_followers": "Follows and followers",
|
||||
"navigation_bar.lists": "Llistes",
|
||||
"navigation_bar.logout": "Zarrar la sesión",
|
||||
|
@ -389,7 +392,7 @@
|
|||
"navigation_bar.public_timeline": "Llinia de tiempu federada",
|
||||
"navigation_bar.search": "Search",
|
||||
"navigation_bar.security": "Seguranza",
|
||||
"not_signed_in_indicator.not_signed_in": "You need to sign in to access this resource.",
|
||||
"not_signed_in_indicator.not_signed_in": "Tienes d'aniciar la sesión p'acceder a esti recursu.",
|
||||
"notification.admin.report": "{name} informó de: {target}",
|
||||
"notification.admin.sign_up": "{name} rexistróse",
|
||||
"notification.favourite": "{name} marcó'l to artículu como favoritu",
|
||||
|
@ -540,8 +543,9 @@
|
|||
"server_banner.server_stats": "Estadístiques del sirvidor:",
|
||||
"sign_in_banner.create_account": "Crear una cuenta",
|
||||
"sign_in_banner.sign_in": "Sign in",
|
||||
"sign_in_banner.text": "Anicia la sesión pa siguir a perfiles o etiquetes, marcar como favoritu, compartir o responder a artículos, o interactuar cola to cuenta nun sirvidor diferente.",
|
||||
"sign_in_banner.text": "Sign in to follow profiles or hashtags, favourite, share and reply to posts. You can also interact from your account on a different server.",
|
||||
"status.admin_account": "Open moderation interface for @{name}",
|
||||
"status.admin_domain": "Open moderation interface for {domain}",
|
||||
"status.admin_status": "Open this status in the moderation interface",
|
||||
"status.block": "Block @{name}",
|
||||
"status.bookmark": "Meter en Marcadores",
|
||||
|
@ -558,7 +562,7 @@
|
|||
"status.favourite": "Favourite",
|
||||
"status.filter": "Filter this post",
|
||||
"status.filtered": "Filtered",
|
||||
"status.hide": "Hide toot",
|
||||
"status.hide": "Anubrir l'artículu",
|
||||
"status.history.created": "{name} creó {date}",
|
||||
"status.history.edited": "{name} editó {date}",
|
||||
"status.load_more": "Cargar más",
|
||||
|
|
|
@ -128,7 +128,7 @@
|
|||
"compose.language.search": "Шукаць мовы...",
|
||||
"compose_form.direct_message_warning_learn_more": "Даведацца больш",
|
||||
"compose_form.encryption_warning": "Допісы ў Mastodon не абаронены скразным шыфраваннем. Не дзяліцеся ніякай канфідэнцыяльнай інфармацыяй в Mastodon.",
|
||||
"compose_form.hashtag_warning": "Гэты допіс не будзе паказаны пад аніякім хэштэгам, так як ён мае тып \"Не паказваць у стужках\". Толькі публічныя допісы могуць быць знойдзены па хэштэгу.",
|
||||
"compose_form.hashtag_warning": "Гэты допіс не будзе паказаны пад аніякім хэштэгам, бо ён не публічны. Толькі публічныя допісы можна знайсці па хэштэгу.",
|
||||
"compose_form.lock_disclaimer": "Ваш уліковы запіс не {locked}. Усе могуць падпісацца на вас, каб бачыць допісы толькі для падпісчыкаў.",
|
||||
"compose_form.lock_disclaimer.lock": "закрыты",
|
||||
"compose_form.placeholder": "Што здарылася?",
|
||||
|
@ -221,6 +221,7 @@
|
|||
"empty_column.favourites": "Ніхто яшчэ не ўпадабаў гэты допіс. Калі гэта адбудзецца, вы ўбачыце гэтых людзей тут.",
|
||||
"empty_column.follow_recommendations": "Здаецца, прапаноў для вас няма. Вы можаце паспрабаваць выкарыстаць пошук, каб знайсці людзей, якіх вы можаце ведаць, ці даследаваць папулярныя хэштэгі.",
|
||||
"empty_column.follow_requests": "У вас яшчэ няма запытаў на падпіскуі. Калі вы атрымаеце запыт, ён з'явяцца тут.",
|
||||
"empty_column.followed_tags": "You have not followed any hashtags yet. When you do, they will show up here.",
|
||||
"empty_column.hashtag": "Па гэтаму хэштэгу пакуль што нічога няма.",
|
||||
"empty_column.home": "Галоўная стужка пустая! Падпішыцеся на іншых людзей, каб запоўніць яе. {suggestions}",
|
||||
"empty_column.home.suggestions": "Глядзіце прапановы",
|
||||
|
@ -263,6 +264,7 @@
|
|||
"follow_request.authorize": "Аўтарызацыя",
|
||||
"follow_request.reject": "Адхіліць",
|
||||
"follow_requests.unlocked_explanation": "Ваш акаўнт не схаваны, аднак прадстаўнікі {domain} палічылі, што вы можаце захацець праглядзець запыты на падпіску з гэтых профіляў уручную.",
|
||||
"followed_tags": "Followed hashtags",
|
||||
"footer.about": "Пра нас",
|
||||
"footer.directory": "Дырэкторыя профіляў",
|
||||
"footer.get_app": "Спампаваць праграму",
|
||||
|
@ -379,6 +381,7 @@
|
|||
"navigation_bar.favourites": "Упадабаныя",
|
||||
"navigation_bar.filters": "Ігнараваныя словы",
|
||||
"navigation_bar.follow_requests": "Запыты на падпіску",
|
||||
"navigation_bar.followed_tags": "Followed hashtags",
|
||||
"navigation_bar.follows_and_followers": "Падпіскі і падпісчыкі",
|
||||
"navigation_bar.lists": "Спісы",
|
||||
"navigation_bar.logout": "Выйсці",
|
||||
|
@ -540,8 +543,9 @@
|
|||
"server_banner.server_stats": "Статыстыка сервера:",
|
||||
"sign_in_banner.create_account": "Стварыць уліковы запіс",
|
||||
"sign_in_banner.sign_in": "Увайсці",
|
||||
"sign_in_banner.text": "Увайдзіце, каб падпісацца на людзей і тэгі, каб адказваць на допісы, дзяліцца імі і падабаць іх, альбо кантактаваць з вашага ўліковага запісу на іншым серверы.",
|
||||
"sign_in_banner.text": "Sign in to follow profiles or hashtags, favourite, share and reply to posts. You can also interact from your account on a different server.",
|
||||
"status.admin_account": "Адкрыць інтэрфейс мадэратара для @{name}",
|
||||
"status.admin_domain": "Адкрыць інтэрфейс мадэратара для {domain}",
|
||||
"status.admin_status": "Адкрыць гэты допіс у інтэрфейсе мадэрацыі",
|
||||
"status.block": "Заблакаваць @{name}",
|
||||
"status.bookmark": "Дадаць закладку",
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"about.blocks": "Модерирани сървъри",
|
||||
"about.contact": "За контакти:",
|
||||
"about.disclaimer": "Mastodon е безплатен софтуер с отворен изходен код и търговска марка Mastodon gGmbH.",
|
||||
"about.disclaimer": "Mastodon е безплатен софтуер с отворен изходен код и търговска марка на Mastodon gGmbH.",
|
||||
"about.domain_blocks.no_reason_available": "Няма налична причина",
|
||||
"about.domain_blocks.preamble": "Mastodon обикновено позволява да разглеждате съдържание и да взаимодействате с други потребители от всякакви сървъри във Федивърс. Има изключения, направени конкретно за този сървър.",
|
||||
"about.domain_blocks.silenced.explanation": "Обикновено няма да виждате профили и съдържание, освен ако изрично не го потърсите или се включете в него, следвайки го.",
|
||||
|
@ -38,7 +38,7 @@
|
|||
"account.follows.empty": "Потребителят още никого не следва.",
|
||||
"account.follows_you": "Следва ви",
|
||||
"account.go_to_profile": "Към профила",
|
||||
"account.hide_reblogs": "Скриване на споделяния от @{name}",
|
||||
"account.hide_reblogs": "Скриване на подсилвания от @{name}",
|
||||
"account.joined_short": "Дата на присъединяване",
|
||||
"account.languages": "Промяна на езиците, за които сте абонирани",
|
||||
"account.link_verified_on": "Собствеността върху тази връзка е проверена на {date}",
|
||||
|
@ -49,14 +49,14 @@
|
|||
"account.mute": "Заглушаване на @{name}",
|
||||
"account.mute_notifications": "Заглушаване на известия от @{name}",
|
||||
"account.muted": "Заглушено",
|
||||
"account.open_original_page": "Отваряне на оригиналната страница",
|
||||
"account.open_original_page": "Отваряне на първообразната страница",
|
||||
"account.posts": "Публикации",
|
||||
"account.posts_with_replies": "Публ. и отговори",
|
||||
"account.report": "Докладване на @{name}",
|
||||
"account.requested": "Чака се одобрение. Щракнете за отмяна на заявката за последване",
|
||||
"account.requested_follow": "{name} поиска да ви последва",
|
||||
"account.share": "Споделяне на профила на @{name}",
|
||||
"account.show_reblogs": "Показване на споделяния от @{name}",
|
||||
"account.show_reblogs": "Показване на подсилвания от @{name}",
|
||||
"account.statuses_counter": "{count, plural, one {{counter} публикация} other {{counter} публикации}}",
|
||||
"account.unblock": "Отблокиране на @{name}",
|
||||
"account.unblock_domain": "Отблокиране на домейн {domain}",
|
||||
|
@ -93,7 +93,7 @@
|
|||
"bundle_modal_error.close": "Затваряне",
|
||||
"bundle_modal_error.message": "Нещо се обърка, зареждайки компонента.",
|
||||
"bundle_modal_error.retry": "Нов опит",
|
||||
"closed_registrations.other_server_instructions": "Поради това че Mastodon е децентрализиран, можеш да създадеш акаунт на друг сървър, от който можеш да комуникираш с този.",
|
||||
"closed_registrations.other_server_instructions": "Oткак e децентрализиранa Mastodon, може да създадете акаунт на друг сървър и още може да взаимодействате с този.",
|
||||
"closed_registrations_modal.description": "Създаването на акаунт в {domain} сега не е възможно, но обърнете внимание, че нямате нужда от акаунт конкретно на {domain}, за да ползвате Mastodon.",
|
||||
"closed_registrations_modal.find_another_server": "Намиране на друг сървър",
|
||||
"closed_registrations_modal.preamble": "Mastodon е децентрализиран, така че няма значение къде създавате акаунта си, ще може да последвате и взаимодействате с всеки на този сървър. Може дори да стартирате свой собствен сървър!",
|
||||
|
@ -128,14 +128,14 @@
|
|||
"compose.language.search": "Търсене на езици...",
|
||||
"compose_form.direct_message_warning_learn_more": "Още информация",
|
||||
"compose_form.encryption_warning": "Публикациите в Mastodon не са криптирани от край до край. Не споделяйте никаква чувствителна информация там.",
|
||||
"compose_form.hashtag_warning": "Тази публикация няма да бъде изброена под нито един хаштаг, тъй като е скрита. Само публични публикации могат да се търсят по хаштаг.",
|
||||
"compose_form.lock_disclaimer": "Вашият акаунт не е {locked}. Всеки може да ви последва, за да прегледа вашите публикации само за последователи.",
|
||||
"compose_form.hashtag_warning": "Тази публикация няма да се вписва под никакъв хаштаг, тъй като не е обществена. Само публични публикации могат да се търсят по хаштаг.",
|
||||
"compose_form.lock_disclaimer": "Вашият акаунт не е в положение {locked}. Всеки може да ви последва, за да разглежда публикациите ви само за последователи.",
|
||||
"compose_form.lock_disclaimer.lock": "заключено",
|
||||
"compose_form.placeholder": "Какво мислите?",
|
||||
"compose_form.poll.add_option": "Добавяне на избор",
|
||||
"compose_form.poll.duration": "Времетраене на анкетата",
|
||||
"compose_form.poll.option_placeholder": "Избор {number}",
|
||||
"compose_form.poll.remove_option": "Премахване на избора",
|
||||
"compose_form.poll.remove_option": "Премахване на този избор",
|
||||
"compose_form.poll.switch_to_multiple": "Промяна на анкетата, за да се позволят множество възможни избора",
|
||||
"compose_form.poll.switch_to_single": "Промяна на анкетата, за да се позволи един възможен избор",
|
||||
"compose_form.publish": "Публикуване",
|
||||
|
@ -168,7 +168,7 @@
|
|||
"confirmations.mute.explanation": "Това ще скрие публикациите от тях и публикации, които ги споменават, но все още ще им позволява да виждат публикациите ви и да ви следват.",
|
||||
"confirmations.mute.message": "Наистина ли искате да заглушите {name}?",
|
||||
"confirmations.redraft.confirm": "Изтриване и преработване",
|
||||
"confirmations.redraft.message": "Сигурни ли сте, че искате да изтриете тази публикация и да я върнете в чернова? Ще загубите споделянията и маркиранията като любима, и отговорите към оригинала ще останат висящи.",
|
||||
"confirmations.redraft.message": "Сигурни ли сте, че искате да изтриете тази публикация и да я върнете в чернова? Ще загубите подсилванията и означаванията като любима, и отговорите към оригинала ще останат висящи.",
|
||||
"confirmations.reply.confirm": "Отговор",
|
||||
"confirmations.reply.message": "Отговарянето сега ще замени съобщението, което в момента съставяте. Сигурни ли сте, че искате да продължите?",
|
||||
"confirmations.unfollow.confirm": "Без следване",
|
||||
|
@ -213,27 +213,28 @@
|
|||
"empty_column.account_unavailable": "Профилът не е наличен",
|
||||
"empty_column.blocks": "Още не сте блокирали никакви потребители.",
|
||||
"empty_column.bookmarked_statuses": "Още не сте отметнали публикации. Отметвайки някоя, то тя ще се покаже тук.",
|
||||
"empty_column.community": "Локалният инфопоток е празен. Публикувайте нещо, за да започнете!",
|
||||
"empty_column.community": "Местната часова ос е празна. Напишете нещо обществено, за да завъртите нещата!",
|
||||
"empty_column.direct": "Все още нямате лични съобщения. Когато изпратите или получите някое, то ще се покаже тук.",
|
||||
"empty_column.domain_blocks": "Още няма блокирани домейни.",
|
||||
"empty_column.explore_statuses": "Няма нищо популярно в момента. Проверете пак по-късно!",
|
||||
"empty_column.explore_statuses": "Няма нищо налагащо се в момента. Проверете пак по-късно!",
|
||||
"empty_column.favourited_statuses": "Още нямате любими публикации. Поставяйки някоя в любими, то тя ще се покаже тук.",
|
||||
"empty_column.favourites": "Още никой не е поставил публикацията в любими. Когато някой го направи, този човек ще се покаже тук.",
|
||||
"empty_column.follow_recommendations": "Изглежда, че няма генерирани предложения за вас. Можете да опитате да търсите за хора, които знаете или да разгледате популярните тагове.",
|
||||
"empty_column.follow_requests": "Все още нямате заявки за последване. Когато получите такава, тя ще се покаже тук.",
|
||||
"empty_column.follow_recommendations": "Изглежда, че няма предложения, които може да се породят за вас. Може да опитате да потърсите хора, които познавате или да разгледате налагащи се хаштагове.",
|
||||
"empty_column.follow_requests": "Още нямате заявки за последване. Получавайки такава, то тя ще се покаже тук.",
|
||||
"empty_column.followed_tags": "Още не сте последвали никакви хаштагове. Последваните хаштагове ще се покажат тук.",
|
||||
"empty_column.hashtag": "Още няма нищо в този хаштаг.",
|
||||
"empty_column.home": "Вашата начална часова ос е празна! Последвайте повече хора, за да я запълните. {suggestions}",
|
||||
"empty_column.home.suggestions": "Преглед на някои предложения",
|
||||
"empty_column.list": "Още няма нищо в този списък. Когато членовете на списъка публикуват нови публикации, то те ще се появят тук.",
|
||||
"empty_column.list": "Все още списъкът е празен. Членуващите на списъка, публикуващи нови публикации, ще се появят тук.",
|
||||
"empty_column.lists": "Все още нямате списъци. Когато създадете такъв, той ще се покаже тук.",
|
||||
"empty_column.mutes": "Още не сте заглушавали потребители.",
|
||||
"empty_column.notifications": "Все още нямате известия. Взаимодействайте с другите, за да започнете разговора.",
|
||||
"empty_column.public": "Тук няма нищо! Публикувайте нещо или последвайте потребители от други сървъри, за да го напълните",
|
||||
"error.unexpected_crash.explanation": "Поради грешка в нашия код или проблем със съвместимостта на браузъра, тази страница не може да се покаже правилно.",
|
||||
"error.unexpected_crash.explanation_addons": "Тази страница не може да се покаже правилно. Тази грешка вероятно е причинена от добавка на браузъра или инструменти за автоматичен превод.",
|
||||
"error.unexpected_crash.next_steps": "Опитайте да опресните страницата. Ако това не помогне, все още можете да използвате Mastodon чрез различен браузър или приложение.",
|
||||
"error.unexpected_crash.explanation_addons": "Страницата не можа да се покаже правилно. Тази грешка вероятно е причинена от добавка на браузъра или от средства за автоматичен превод.",
|
||||
"error.unexpected_crash.next_steps": "Опитайте опресняване на страницата. Ако това не помогне, още може да използвате Mastodon през различен браузър или приложение.",
|
||||
"error.unexpected_crash.next_steps_addons": "Опитайте се да ги изключите и да опресните страницата. Ако това не помогне, то още може да използвате Mastodon чрез различен браузър или приложение.",
|
||||
"errors.unexpected_crash.copy_stacktrace": "Копиране на stacktrace-а в клипборда",
|
||||
"errors.unexpected_crash.copy_stacktrace": "Копиране на трасето на стека в буферната памет",
|
||||
"errors.unexpected_crash.report_issue": "Сигнал за проблем",
|
||||
"explore.search_results": "Резултати от търсенето",
|
||||
"explore.suggested_follows": "За вас",
|
||||
|
@ -250,7 +251,7 @@
|
|||
"filter_modal.added.settings_link": "страница с настройки",
|
||||
"filter_modal.added.short_explanation": "Тази публикация е добавена към следната категория на филтъра: {title}.",
|
||||
"filter_modal.added.title": "Филтърът е добавен!",
|
||||
"filter_modal.select_filter.context_mismatch": "не е приложимо за този контекст",
|
||||
"filter_modal.select_filter.context_mismatch": "неприложимо за контекста",
|
||||
"filter_modal.select_filter.expired": "изтекло",
|
||||
"filter_modal.select_filter.prompt_new": "Нова категория: {name}",
|
||||
"filter_modal.select_filter.search": "Търсене или създаване",
|
||||
|
@ -259,10 +260,11 @@
|
|||
"filter_modal.title.status": "Филтриране на публ.",
|
||||
"follow_recommendations.done": "Готово",
|
||||
"follow_recommendations.heading": "Следвайте хора, от които харесвате да виждате публикации! Ето някои предложения.",
|
||||
"follow_recommendations.lead": "Съобщения от хора, които следвате, ще се показват в хронологичен ред във вашия основен инфопоток. Не се страхувайте, че ще сгрешите, по всяко време много лесно можете да спрете да ги следвате!",
|
||||
"follow_recommendations.lead": "Публикациите от последваните, ще се показват в хронологичен ред в началния ви инфоканал. Не се страхувайте, че ще сгрешите, по всяко време много лесно може да спрете да ги следвате!",
|
||||
"follow_request.authorize": "Упълномощаване",
|
||||
"follow_request.reject": "Отхвърляне",
|
||||
"follow_requests.unlocked_explanation": "Въпреки че акаунтът ви не е заключен, служителите на {domain} помислиха, че може да искате да преглеждате ръчно заявките за последване на тези профили.",
|
||||
"followed_tags": "Последвани хештагове",
|
||||
"footer.about": "Относно",
|
||||
"footer.directory": "Директория на профилите",
|
||||
"footer.get_app": "Вземане на приложението",
|
||||
|
@ -284,35 +286,35 @@
|
|||
"hashtag.follow": "Следване на хаштаг",
|
||||
"hashtag.unfollow": "Спиране на следване на хаштаг",
|
||||
"home.column_settings.basic": "Основно",
|
||||
"home.column_settings.show_reblogs": "Показване на споделяния",
|
||||
"home.column_settings.show_reblogs": "Показване на подсилванията",
|
||||
"home.column_settings.show_replies": "Показване на отговорите",
|
||||
"home.hide_announcements": "Скриване на оповестяванията",
|
||||
"home.show_announcements": "Показване на оповестяванията",
|
||||
"interaction_modal.description.favourite": "Ако имате профил в Mastodon, можете да маркирате публикация като любима, за да уведомите автора, че я оценявате, и да я запазите за по-късно.",
|
||||
"interaction_modal.description.follow": "Ако имате регистрация в Mastodon, то може да последвате {name}, за да виждате публикациите от този профил в началния си инфопоток.",
|
||||
"interaction_modal.description.reblog": "Ако имате профил в Mastodon, можете да споделите тази публикация със своите последователи.",
|
||||
"interaction_modal.description.reply": "Ако имате профил в Mastodon, можете да добавите отговор към тази публикация.",
|
||||
"interaction_modal.description.favourite": "С акаунт в Mastodon може да направите тази публикация като любима, за да известите автора, че я цените, и да я запазите за по-късно.",
|
||||
"interaction_modal.description.follow": "С акаунт в Mastodon може да последвате {name}, за да получавате публикациите от този акаунт в началния си инфоканал.",
|
||||
"interaction_modal.description.reblog": "С акаунт в Mastodon може да подсилите тази публикация, за да я споделите с последователите си.",
|
||||
"interaction_modal.description.reply": "С акаунт в Mastodon може да добавите отговор към тази публикация.",
|
||||
"interaction_modal.on_another_server": "На различен сървър",
|
||||
"interaction_modal.on_this_server": "На този сървър",
|
||||
"interaction_modal.other_server_instructions": "Копипейстнете този URL адрес в полето за търсене на любимото си приложение Mastodon или мрежови интерфейс на своя Mastodon сървър.",
|
||||
"interaction_modal.preamble": "Откак Mastodon е децентрализиран, може да употребявате съществуващ акаунт, разположен на друг сървър на Mastodon или съвместима платформа, ако нямате акаунт на този сървър.",
|
||||
"interaction_modal.title.favourite": "Любими публикации на {name}",
|
||||
"interaction_modal.title.follow": "Последване на {name}",
|
||||
"interaction_modal.title.reblog": "Споделете публикацията от {name}",
|
||||
"interaction_modal.title.reblog": "Подсилване на публикацията на {name}",
|
||||
"interaction_modal.title.reply": "Отговаряне на публикацията на {name}",
|
||||
"intervals.full.days": "{number, plural, one {# ден} other {# дни}}",
|
||||
"intervals.full.hours": "{number, plural, one {# час} other {# часа}}",
|
||||
"intervals.full.minutes": "{number, plural, one {# минута} other {# минути}}",
|
||||
"keyboard_shortcuts.back": "Навигиране назад",
|
||||
"keyboard_shortcuts.blocked": "Отваряне на списъка с блокирани потребители",
|
||||
"keyboard_shortcuts.boost": "за споделяне",
|
||||
"keyboard_shortcuts.boost": "Подсилване на публикация",
|
||||
"keyboard_shortcuts.column": "Съсредоточение на колона",
|
||||
"keyboard_shortcuts.compose": "Фокус на текстовото пространство за композиране",
|
||||
"keyboard_shortcuts.compose": "Фокус на текстовата зона за съставяне",
|
||||
"keyboard_shortcuts.description": "Опис",
|
||||
"keyboard_shortcuts.direct": "за отваряне на колоната с лични съобщения",
|
||||
"keyboard_shortcuts.down": "Преместване надолу в списъка",
|
||||
"keyboard_shortcuts.enter": "Отваряне на публикация",
|
||||
"keyboard_shortcuts.favourite": "Любима публикация",
|
||||
"keyboard_shortcuts.favourite": "Към любими публикации",
|
||||
"keyboard_shortcuts.favourites": "Отваряне на списъка с любими",
|
||||
"keyboard_shortcuts.federated": "Отваряне на федерирания инфопоток",
|
||||
"keyboard_shortcuts.heading": "Клавишни съчетания",
|
||||
|
@ -320,7 +322,7 @@
|
|||
"keyboard_shortcuts.hotkey": "Бърз клавиш",
|
||||
"keyboard_shortcuts.legend": "Показване на тази легенда",
|
||||
"keyboard_shortcuts.local": "Отваряне на местна часова ос",
|
||||
"keyboard_shortcuts.mention": "Споменаване на автор",
|
||||
"keyboard_shortcuts.mention": "Споменаване на автора",
|
||||
"keyboard_shortcuts.muted": "Отваряне на списъка със заглушени потребители",
|
||||
"keyboard_shortcuts.my_profile": "Отваряне на профила ви",
|
||||
"keyboard_shortcuts.notifications": "Отваряне на колоната с известия",
|
||||
|
@ -330,12 +332,12 @@
|
|||
"keyboard_shortcuts.reply": "Отговаряне на публикация",
|
||||
"keyboard_shortcuts.requests": "Отваряне на списъка със заявки за последване",
|
||||
"keyboard_shortcuts.search": "Фокус на лентата за търсене",
|
||||
"keyboard_shortcuts.spoilers": "за показване/скриване на ПС полето",
|
||||
"keyboard_shortcuts.spoilers": "Показване/скриване на полето за предупреждение на съдържание",
|
||||
"keyboard_shortcuts.start": "Отваряне на колоната \"първи стъпки\"",
|
||||
"keyboard_shortcuts.toggle_hidden": "за показване/скриване на текст зад ПС",
|
||||
"keyboard_shortcuts.toggle_hidden": "Показване/скриване на текст зад предупреждение на съдържание",
|
||||
"keyboard_shortcuts.toggle_sensitivity": "Показване/скриване на мултимедията",
|
||||
"keyboard_shortcuts.toot": "Начало на нова публикация",
|
||||
"keyboard_shortcuts.unfocus": "Разфокусиране на текстовото поле за композиране/търсене",
|
||||
"keyboard_shortcuts.unfocus": "Разфокусиране на текстовото поле за съставяне/търсене",
|
||||
"keyboard_shortcuts.up": "Преместване нагоре в списъка",
|
||||
"lightbox.close": "Затваряне",
|
||||
"lightbox.compress": "Свиване на полето за преглед на образи",
|
||||
|
@ -366,7 +368,7 @@
|
|||
"mute_modal.duration": "Времетраене",
|
||||
"mute_modal.hide_notifications": "Скривате ли известията от този потребител?",
|
||||
"mute_modal.indefinite": "Неопределено",
|
||||
"navigation_bar.about": "За тази инстанция",
|
||||
"navigation_bar.about": "Относно",
|
||||
"navigation_bar.blocks": "Блокирани потребители",
|
||||
"navigation_bar.bookmarks": "Отметки",
|
||||
"navigation_bar.community_timeline": "Локален инфопоток",
|
||||
|
@ -379,6 +381,7 @@
|
|||
"navigation_bar.favourites": "Любими",
|
||||
"navigation_bar.filters": "Заглушени думи",
|
||||
"navigation_bar.follow_requests": "Заявки за последване",
|
||||
"navigation_bar.followed_tags": "Последвани хештагове",
|
||||
"navigation_bar.follows_and_followers": "Последвания и последователи",
|
||||
"navigation_bar.lists": "Списъци",
|
||||
"navigation_bar.logout": "Излизане",
|
||||
|
@ -389,7 +392,7 @@
|
|||
"navigation_bar.public_timeline": "Федеративна хронология",
|
||||
"navigation_bar.search": "Търсене",
|
||||
"navigation_bar.security": "Сигурност",
|
||||
"not_signed_in_indicator.not_signed_in": "Трябва да влезете за достъп до този ресурс.",
|
||||
"not_signed_in_indicator.not_signed_in": "Трябва ви вход за достъп до ресурса.",
|
||||
"notification.admin.report": "{name} докладва {target}",
|
||||
"notification.admin.sign_up": "{name} се регистрира",
|
||||
"notification.favourite": "{name} сложи в любими ваша публикация",
|
||||
|
@ -398,7 +401,7 @@
|
|||
"notification.mention": "{name} ви спомена",
|
||||
"notification.own_poll": "Анкетата ви приключи",
|
||||
"notification.poll": "Анкета, в която гласувахте, приключи",
|
||||
"notification.reblog": "{name} сподели вашата публикация",
|
||||
"notification.reblog": "{name} подсили ваша публикация",
|
||||
"notification.status": "{name} току-що публикува",
|
||||
"notification.update": "{name} промени публикация",
|
||||
"notifications.clear": "Изчистване на известията",
|
||||
|
@ -415,7 +418,7 @@
|
|||
"notifications.column_settings.mention": "Споменавания:",
|
||||
"notifications.column_settings.poll": "Резултати от анкета:",
|
||||
"notifications.column_settings.push": "Изскачащи известия",
|
||||
"notifications.column_settings.reblog": "Споделяния:",
|
||||
"notifications.column_settings.reblog": "Подсилвания:",
|
||||
"notifications.column_settings.show": "Показване в колоната",
|
||||
"notifications.column_settings.sound": "Пускане на звук",
|
||||
"notifications.column_settings.status": "Нови публикации:",
|
||||
|
@ -423,7 +426,7 @@
|
|||
"notifications.column_settings.unread_notifications.highlight": "Изтъкване на непрочетените известия",
|
||||
"notifications.column_settings.update": "Промени:",
|
||||
"notifications.filter.all": "Всичко",
|
||||
"notifications.filter.boosts": "Споделяния",
|
||||
"notifications.filter.boosts": "Подсилвания",
|
||||
"notifications.filter.favourites": "Любими",
|
||||
"notifications.filter.follows": "Последвания",
|
||||
"notifications.filter.mentions": "Споменавания",
|
||||
|
@ -433,7 +436,7 @@
|
|||
"notifications.group": "{count} известия",
|
||||
"notifications.mark_as_read": "Отбелязване на всички известия като прочетени",
|
||||
"notifications.permission_denied": "Известията на работния плот не са налични поради предварително отказана заявка за разрешение в браузъра",
|
||||
"notifications.permission_denied_alert": "Известията на работния плот не могат да бъдат активирани, тъй като разрешението на браузъра е отказвано преди",
|
||||
"notifications.permission_denied_alert": "Известията на работния плот не могат да се включат, тъй като разрешението на браузъра е отказвано преди",
|
||||
"notifications.permission_required": "Известията на работния плот ги няма, щото няма дадено нужното позволение.",
|
||||
"notifications_permission_banner.enable": "Включване на известията на работния плот",
|
||||
"notifications_permission_banner.how_to_control": "За да получавате известия, когато Mastodon не е отворен, включете известията на работния плот. Може да управлявате точно кои видове взаимодействия пораждат известия на работния плот чрез бутона {icon} по-горе, след като бъдат включени.",
|
||||
|
@ -486,23 +489,23 @@
|
|||
"report.close": "Готово",
|
||||
"report.comment.title": "Има ли нещо друго, което смятате, че трябва да знаем?",
|
||||
"report.forward": "Препращане до {target}",
|
||||
"report.forward_hint": "Акаунтът е от друг сървър. Ще изпратите ли анонимно копие на доклада и там?",
|
||||
"report.forward_hint": "Акаунтът е от друг сървър. Ще изпратите ли безимено копие на доклада и там?",
|
||||
"report.mute": "Заглушаване",
|
||||
"report.mute_explanation": "Няма да виждате публикациите на това лице. То още може да ви следва и да вижда публикациите ви и няма да знае, че е заглушено.",
|
||||
"report.next": "Напред",
|
||||
"report.placeholder": "Допълнителни коментари",
|
||||
"report.reasons.dislike": "Не ми харесва",
|
||||
"report.reasons.dislike_description": "Не е нещо, които искам да виждам",
|
||||
"report.reasons.dislike_description": "Не е нещо, което искате да виждате",
|
||||
"report.reasons.other": "Нещо друго е",
|
||||
"report.reasons.other_description": "Проблемът не попада в нито една от другите категории",
|
||||
"report.reasons.spam": "Спам е",
|
||||
"report.reasons.spam_description": "Зловредни връзки, фалшиви взаимодействия, или повтарящи се отговори",
|
||||
"report.reasons.spam_description": "Зловредни връзки, фалшиви ангажименти, или повтарящи се отговори",
|
||||
"report.reasons.violation": "Нарушава правилата на сървъра",
|
||||
"report.reasons.violation_description": "Знаете, че нарушава особени правила",
|
||||
"report.rules.subtitle": "Изберете всичко, което да се прилага",
|
||||
"report.rules.title": "Кои правила са нарушени?",
|
||||
"report.statuses.subtitle": "Изберете всичко, което да се прилага",
|
||||
"report.statuses.title": "Има ли някакви публикации, подкрепящи този доклад?",
|
||||
"report.statuses.title": "Има ли някакви публикации, подкрепящи доклада?",
|
||||
"report.submit": "Подаване",
|
||||
"report.target": "Докладване на {target}",
|
||||
"report.thanks.take_action": "Ето възможностите ви за управление какво виждате в Mastodon:",
|
||||
|
@ -511,7 +514,7 @@
|
|||
"report.thanks.title_actionable": "Благодарности за докладването, ще го прегледаме.",
|
||||
"report.unfollow": "Стоп на следването на @{name}",
|
||||
"report.unfollow_explanation": "Последвали сте този акаунт. За да не виждате повече публикациите му в началния си инфопоток, спрете да го следвате.",
|
||||
"report_notification.attached_statuses": "прикачено {count, plural, one {{count} публикация} other {{count} публикации}}",
|
||||
"report_notification.attached_statuses": "{count, plural, one {прикаченa {count} публикация} other {прикачени {count} публикации}}",
|
||||
"report_notification.categories.other": "Друго",
|
||||
"report_notification.categories.spam": "Спам",
|
||||
"report_notification.categories.violation": "Нарушение на правилото",
|
||||
|
@ -540,13 +543,14 @@
|
|||
"server_banner.server_stats": "Статистика на сървъра:",
|
||||
"sign_in_banner.create_account": "Създаване на акаунт",
|
||||
"sign_in_banner.sign_in": "Вход",
|
||||
"sign_in_banner.text": "Влезте, за да последвате профили или хаштагове, любимо, споделяне и отговаряне на публикации или взаимодействие от акаунта ви на друг сървър.",
|
||||
"sign_in_banner.text": "Влезте, за да последвате профили или хаштагове, отбелязвате като любими, споделяте и отговаряте на публикации. Можете също така да взаимодействате от акаунта ви на друг сървър.",
|
||||
"status.admin_account": "Отваряне на интерфейс за модериране за @{name}",
|
||||
"status.admin_status": "Отваряне на тази публикация в интерфейс на модериране",
|
||||
"status.admin_domain": "Отваряне на модериращия интерфейс за {domain}",
|
||||
"status.admin_status": "Отваряне на публикацията в модериращия интерфейс",
|
||||
"status.block": "Блокиране на @{name}",
|
||||
"status.bookmark": "Отмятане",
|
||||
"status.cancel_reblog_private": "Отсподеляне",
|
||||
"status.cannot_reblog": "Тази публикация не може да бъде споделена",
|
||||
"status.cancel_reblog_private": "Край на подсилването",
|
||||
"status.cannot_reblog": "Публикация не може да се подсили",
|
||||
"status.copy": "Копиране на връзката към публикация",
|
||||
"status.delete": "Изтриване",
|
||||
"status.detailed_status": "Подробен изглед на разговора",
|
||||
|
@ -571,17 +575,17 @@
|
|||
"status.pin": "Закачане в профила",
|
||||
"status.pinned": "Закачена публикация",
|
||||
"status.read_more": "Още за четене",
|
||||
"status.reblog": "Споделяне",
|
||||
"status.reblog_private": "Споделяне с оригинална видимост",
|
||||
"status.reblogged_by": "{name} сподели",
|
||||
"status.reblogs.empty": "Все още никой не е споделил тази публикация. Когато някой го направи, ще се покаже тук.",
|
||||
"status.reblog": "Подсилване",
|
||||
"status.reblog_private": "Подсилване с оригиналната видимост",
|
||||
"status.reblogged_by": "{name} подсили",
|
||||
"status.reblogs.empty": "Още никого не е подсилвал публикацията. Подсилващият ще се покаже тук.",
|
||||
"status.redraft": "Изтриване и преработване",
|
||||
"status.remove_bookmark": "Премахване на отметката",
|
||||
"status.replied_to": "В отговор до {name}",
|
||||
"status.reply": "Отговор",
|
||||
"status.replyAll": "Отговор на тема",
|
||||
"status.replyAll": "Отговор на нишка",
|
||||
"status.report": "Докладване на @{name}",
|
||||
"status.sensitive_warning": "Чувствително съдържание",
|
||||
"status.sensitive_warning": "Деликатно съдържание",
|
||||
"status.share": "Споделяне",
|
||||
"status.show_filter_reason": "Покажи въпреки това",
|
||||
"status.show_less": "Показване на по-малко",
|
||||
|
@ -599,7 +603,7 @@
|
|||
"subscribed_languages.target": "Смяна на езика за {target}",
|
||||
"suggestions.dismiss": "Отхвърляне на предложение",
|
||||
"suggestions.header": "Може да имате интерес от…",
|
||||
"tabs_bar.federated_timeline": "Федерална",
|
||||
"tabs_bar.federated_timeline": "Федеративен",
|
||||
"tabs_bar.home": "Начало",
|
||||
"tabs_bar.local_timeline": "Местни",
|
||||
"tabs_bar.notifications": "Известия",
|
||||
|
@ -620,22 +624,22 @@
|
|||
"units.short.thousand": "{count}хил",
|
||||
"upload_area.title": "Влачене и пускане за качване",
|
||||
"upload_button.label": "Добавете файл с образ, видео или звук",
|
||||
"upload_error.limit": "Превишено ограничение за качване на файлове.",
|
||||
"upload_error.limit": "Превишено ограничението за качване на файлове.",
|
||||
"upload_error.poll": "Качването на файлове не е позволено с анкети.",
|
||||
"upload_form.audio_description": "Опишете за хора със загубен слух",
|
||||
"upload_form.description": "Опишете за хора със зрително увреждане",
|
||||
"upload_form.audio_description": "Опишете за хора, които са глухи или трудно чуват",
|
||||
"upload_form.description": "Опишете за хора, които са слепи или имат слабо зрение",
|
||||
"upload_form.description_missing": "Няма добавен опис",
|
||||
"upload_form.edit": "Редактиране",
|
||||
"upload_form.thumbnail": "Промяна на миниобраза",
|
||||
"upload_form.undo": "Изтриване",
|
||||
"upload_form.video_description": "Опишете за хора със загубен слух или зрително увреждане",
|
||||
"upload_form.video_description": "Опишете за хора, които са глухи или трудно чуват, слепи или имат слабо зрение",
|
||||
"upload_modal.analyzing_picture": "Снимков анализ…",
|
||||
"upload_modal.apply": "Прилагане",
|
||||
"upload_modal.applying": "Прилагане…",
|
||||
"upload_modal.choose_image": "Избор на образ",
|
||||
"upload_modal.description_placeholder": "Ах, чудна българска земьо, полюшвай цъфтящи жита",
|
||||
"upload_modal.detect_text": "Откриване на текст от картина",
|
||||
"upload_modal.edit_media": "Редакция на мултимедия",
|
||||
"upload_modal.edit_media": "Промяна на мултимедия",
|
||||
"upload_modal.hint": "Щракнете или плъзнете кръга на визуализацията, за да изберете фокусна точка, която винаги ще бъде видима на всички миниатюри.",
|
||||
"upload_modal.preparing_ocr": "Подготовка за оптично разпознаване на знаци…",
|
||||
"upload_modal.preview_label": "Нагледно ({ratio})",
|
||||
|
|
|
@ -128,7 +128,7 @@
|
|||
"compose.language.search": "Search languages...",
|
||||
"compose_form.direct_message_warning_learn_more": "আরো জানুন",
|
||||
"compose_form.encryption_warning": "Posts on Mastodon are not end-to-end encrypted. Do not share any dangerous information over Mastodon.",
|
||||
"compose_form.hashtag_warning": "কোনো হ্যাশট্যাগের ভেতরে এই টুটটি থাকবেনা কারণ এটি তালিকাবহির্ভূত। শুধুমাত্র প্রকাশ্য ঠোটগুলো হ্যাশট্যাগের ভেতরে খুঁজে পাওয়া যাবে।",
|
||||
"compose_form.hashtag_warning": "This post won't be listed under any hashtag as it is unlisted. Only public posts can be searched by hashtag.",
|
||||
"compose_form.lock_disclaimer": "আপনার নিবন্ধনে তালা দেওয়া নেই, যে কেও আপনাকে অনুসরণ করতে পারবে এবং অনুশারকদের জন্য লেখা দেখতে পারবে।",
|
||||
"compose_form.lock_disclaimer.lock": "তালা দেওয়া",
|
||||
"compose_form.placeholder": "আপনি কি ভাবছেন ?",
|
||||
|
@ -221,6 +221,7 @@
|
|||
"empty_column.favourites": "কেও এখনো এটাকে পছন্দের টুট হিসেবে চিহ্নিত করেনি। যদি করে, তখন তাদের এখানে পাওয়া যাবে।",
|
||||
"empty_column.follow_recommendations": "Looks like no suggestions could be generated for you. You can try using search to look for people you might know or explore trending hashtags.",
|
||||
"empty_column.follow_requests": "তোমার এখনো কোনো অনুসরণের আবেদন পাওনি। যদি কেউ পাঠায়, এখানে পাওয়া যাবে।",
|
||||
"empty_column.followed_tags": "You have not followed any hashtags yet. When you do, they will show up here.",
|
||||
"empty_column.hashtag": "এই হেসটাগে এখনো কিছু নেই।",
|
||||
"empty_column.home": "আপনার বাড়ির সময়রেখা এখনো খালি! {public} এ ঘুরে আসুন অথবা অনুসন্ধান বেবহার করে শুরু করতে পারেন এবং অন্য ব্যবহারকারীদের সাথে সাক্ষাৎ করতে পারেন।",
|
||||
"empty_column.home.suggestions": "See some suggestions",
|
||||
|
@ -263,6 +264,7 @@
|
|||
"follow_request.authorize": "অনুমতি দিন",
|
||||
"follow_request.reject": "প্রত্যাখ্যান করুন",
|
||||
"follow_requests.unlocked_explanation": "আপনার অ্যাকাউন্টটি লক না থাকলেও, {domain} কর্মীরা ভেবেছিলেন যে আপনি এই অ্যাকাউন্টগুলি থেকে ম্যানুয়ালি অনুসরণের অনুরোধগুলি পর্যালোচনা করতে চাইতে পারেন।",
|
||||
"followed_tags": "Followed hashtags",
|
||||
"footer.about": "About",
|
||||
"footer.directory": "Profiles directory",
|
||||
"footer.get_app": "Get the app",
|
||||
|
@ -379,6 +381,7 @@
|
|||
"navigation_bar.favourites": "পছন্দের",
|
||||
"navigation_bar.filters": "বন্ধ করা শব্দ",
|
||||
"navigation_bar.follow_requests": "অনুসরণের অনুরোধগুলি",
|
||||
"navigation_bar.followed_tags": "Followed hashtags",
|
||||
"navigation_bar.follows_and_followers": "অনুসরণ এবং অনুসরণকারী",
|
||||
"navigation_bar.lists": "তালিকাগুলো",
|
||||
"navigation_bar.logout": "বাইরে যান",
|
||||
|
@ -540,8 +543,9 @@
|
|||
"server_banner.server_stats": "Server stats:",
|
||||
"sign_in_banner.create_account": "Create account",
|
||||
"sign_in_banner.sign_in": "Sign in",
|
||||
"sign_in_banner.text": "Sign in to follow profiles or hashtags, favourite, share and reply to posts, or interact from your account on a different server.",
|
||||
"sign_in_banner.text": "Sign in to follow profiles or hashtags, favourite, share and reply to posts. You can also interact from your account on a different server.",
|
||||
"status.admin_account": "@{name} র জন্য পরিচালনার ইন্টারফেসে ঢুকুন",
|
||||
"status.admin_domain": "Open moderation interface for {domain}",
|
||||
"status.admin_status": "যায় লেখাটি পরিচালনার ইন্টারফেসে খুলুন",
|
||||
"status.block": "@{name} কে ব্লক করুন",
|
||||
"status.bookmark": "বুকমার্ক",
|
||||
|
@ -558,7 +562,7 @@
|
|||
"status.favourite": "পছন্দের করতে",
|
||||
"status.filter": "Filter this post",
|
||||
"status.filtered": "ছাঁকনিদিত",
|
||||
"status.hide": "Hide toot",
|
||||
"status.hide": "Hide post",
|
||||
"status.history.created": "{name} created {date}",
|
||||
"status.history.edited": "{name} edited {date}",
|
||||
"status.load_more": "আরো দেখুন",
|
||||
|
|
|
@ -128,7 +128,7 @@
|
|||
"compose.language.search": "Klask yezhoù...",
|
||||
"compose_form.direct_message_warning_learn_more": "Gouzout hiroc'h",
|
||||
"compose_form.encryption_warning": "Toudoù war Mastodon na vezont ket sifret penn-da-benn. Na rannit ket titouroù kizidik dre Mastodon.",
|
||||
"compose_form.hashtag_warning": "Ne vo ket listennet an toud-mañ dindan gerioù-klik ebet dre m'eo anlistennet. N'eus nemet an toudoù foran a c'hall bezañ klasket dre c'her-klik.",
|
||||
"compose_form.hashtag_warning": "This post won't be listed under any hashtag as it is unlisted. Only public posts can be searched by hashtag.",
|
||||
"compose_form.lock_disclaimer": "N'eo ket {locked} ho kont. An holl a c'hal ho heuliañ evit gwelet ho toudoù prevez.",
|
||||
"compose_form.lock_disclaimer.lock": "prennet",
|
||||
"compose_form.placeholder": "Petra emaoc'h o soñjal e-barzh ?",
|
||||
|
@ -221,6 +221,7 @@
|
|||
"empty_column.favourites": "Den ebet n'eus ouzhpennet an toud-mañ en e reoù muiañ-karet c'hoazh. Pa vo graet gant unan bennak e teuio war wel amañ.",
|
||||
"empty_column.follow_recommendations": "War a seblant ne c'hall ket bezañ savet erbedadenn ebet evidoc'h. Gallout a rit implijout un enklask evit kavout tud a anavezfec'h pe furchal ar gerioù-klik diouzh ar c'hiz.",
|
||||
"empty_column.follow_requests": "N'ho peus reked heuliañ ebet c'hoazh. Pa vo resevet unan e teuio war wel amañ.",
|
||||
"empty_column.followed_tags": "You have not followed any hashtags yet. When you do, they will show up here.",
|
||||
"empty_column.hashtag": "N'eus netra er ger-klik-mañ c'hoazh.",
|
||||
"empty_column.home": "Goullo eo ho red-amzer degemer! Kit da weladenniñ {public} pe implijit ar c'hlask evit kregiñ ganti ha kejañ gant implijer·ien·ezed all.",
|
||||
"empty_column.home.suggestions": "Gwellout damvenegoù",
|
||||
|
@ -263,6 +264,7 @@
|
|||
"follow_request.authorize": "Aotren",
|
||||
"follow_request.reject": "Nac'hañ",
|
||||
"follow_requests.unlocked_explanation": "Daoust ma n'eo ket ho kont prennet, skipailh {domain} a soñj e fellfe deoc'h gwiriekaat pedadennoù heuliañ deus ar c'hontoù-se diwar-zorn.",
|
||||
"followed_tags": "Followed hashtags",
|
||||
"footer.about": "Diwar-benn",
|
||||
"footer.directory": "Kavlec'h ar profiloù",
|
||||
"footer.get_app": "Pellgargañ an arload",
|
||||
|
@ -379,6 +381,7 @@
|
|||
"navigation_bar.favourites": "Ar re vuiañ-karet",
|
||||
"navigation_bar.filters": "Gerioù kuzhet",
|
||||
"navigation_bar.follow_requests": "Pedadoù heuliañ",
|
||||
"navigation_bar.followed_tags": "Followed hashtags",
|
||||
"navigation_bar.follows_and_followers": "Heuliadennoù ha heulier·ezed·ien",
|
||||
"navigation_bar.lists": "Listennoù",
|
||||
"navigation_bar.logout": "Digennaskañ",
|
||||
|
@ -540,8 +543,9 @@
|
|||
"server_banner.server_stats": "Stadegoù ar servijer :",
|
||||
"sign_in_banner.create_account": "Krouiñ ur gont",
|
||||
"sign_in_banner.sign_in": "Kevreañ",
|
||||
"sign_in_banner.text": "Sign in to follow profiles or hashtags, favourite, share and reply to posts, or interact from your account on a different server.",
|
||||
"sign_in_banner.text": "Sign in to follow profiles or hashtags, favourite, share and reply to posts. You can also interact from your account on a different server.",
|
||||
"status.admin_account": "Digeriñ etrefas evezherezh evit @{name}",
|
||||
"status.admin_domain": "Open moderation interface for {domain}",
|
||||
"status.admin_status": "Digeriñ an toud e-barzh an etrefas evezherezh",
|
||||
"status.block": "Berzañ @{name}",
|
||||
"status.bookmark": "Ouzhpennañ d'ar sinedoù",
|
||||
|
@ -558,7 +562,7 @@
|
|||
"status.favourite": "Muiañ-karet",
|
||||
"status.filter": "Silañ ar c'hannad-mañ",
|
||||
"status.filtered": "Silet",
|
||||
"status.hide": "Kuzhat ar c'hannad",
|
||||
"status.hide": "Hide post",
|
||||
"status.history.created": "Krouet gant {name} {date}",
|
||||
"status.history.edited": "Kemmet gant {name} {date}",
|
||||
"status.load_more": "Kargañ muioc'h",
|
||||
|
|
|
@ -221,6 +221,7 @@
|
|||
"empty_column.favourites": "No one has favourited this post yet. When someone does, they will show up here.",
|
||||
"empty_column.follow_recommendations": "Looks like no suggestions could be generated for you. You can try using search to look for people you might know or explore trending hashtags.",
|
||||
"empty_column.follow_requests": "You don't have any follow requests yet. When you receive one, it will show up here.",
|
||||
"empty_column.followed_tags": "You have not followed any hashtags yet. When you do, they will show up here.",
|
||||
"empty_column.hashtag": "There is nothing in this hashtag yet.",
|
||||
"empty_column.home": "Your home timeline is empty! Follow more people to fill it up. {suggestions}",
|
||||
"empty_column.home.suggestions": "See some suggestions",
|
||||
|
@ -263,6 +264,7 @@
|
|||
"follow_request.authorize": "Authorize",
|
||||
"follow_request.reject": "Reject",
|
||||
"follow_requests.unlocked_explanation": "Even though your account is not locked, the {domain} staff thought you might want to review follow requests from these accounts manually.",
|
||||
"followed_tags": "Followed hashtags",
|
||||
"footer.about": "About",
|
||||
"footer.directory": "Profiles directory",
|
||||
"footer.get_app": "Get the app",
|
||||
|
@ -379,6 +381,7 @@
|
|||
"navigation_bar.favourites": "Favourites",
|
||||
"navigation_bar.filters": "Muted words",
|
||||
"navigation_bar.follow_requests": "Follow requests",
|
||||
"navigation_bar.followed_tags": "Followed hashtags",
|
||||
"navigation_bar.follows_and_followers": "Follows and followers",
|
||||
"navigation_bar.lists": "Lists",
|
||||
"navigation_bar.logout": "Logout",
|
||||
|
@ -540,8 +543,9 @@
|
|||
"server_banner.server_stats": "Server stats:",
|
||||
"sign_in_banner.create_account": "Create account",
|
||||
"sign_in_banner.sign_in": "Sign in",
|
||||
"sign_in_banner.text": "Sign in to follow profiles or hashtags, favourite, share and reply to posts, or interact from your account on a different server.",
|
||||
"sign_in_banner.text": "Sign in to follow profiles or hashtags, favourite, share and reply to posts. You can also interact from your account on a different server.",
|
||||
"status.admin_account": "Open moderation interface for @{name}",
|
||||
"status.admin_domain": "Open moderation interface for {domain}",
|
||||
"status.admin_status": "Open this status in the moderation interface",
|
||||
"status.block": "Block @{name}",
|
||||
"status.bookmark": "Bookmark",
|
||||
|
@ -558,7 +562,7 @@
|
|||
"status.favourite": "Favourite",
|
||||
"status.filter": "Filter this post",
|
||||
"status.filtered": "Filtered",
|
||||
"status.hide": "Hide toot",
|
||||
"status.hide": "Hide post",
|
||||
"status.history.created": "{name} created {date}",
|
||||
"status.history.edited": "{name} edited {date}",
|
||||
"status.load_more": "Load more",
|
||||
|
|
|
@ -21,13 +21,13 @@
|
|||
"account.browse_more_on_origin_server": "Navega més en el perfil original",
|
||||
"account.cancel_follow_request": "Retira la sol·licitud de seguiment",
|
||||
"account.direct": "Missatge directe a @{name}",
|
||||
"account.disable_notifications": "No em notifiquis les publicacions de @{name}",
|
||||
"account.disable_notifications": "Deixa de notificar-me els tuts de @{name}",
|
||||
"account.domain_blocked": "Domini blocat",
|
||||
"account.edit_profile": "Edita el perfil",
|
||||
"account.enable_notifications": "Notifica'm les publicacions de @{name}",
|
||||
"account.enable_notifications": "Notifica'm els tuts de @{name}",
|
||||
"account.endorse": "Recomana en el perfil",
|
||||
"account.featured_tags.last_status_at": "Darrera publicació el {date}",
|
||||
"account.featured_tags.last_status_never": "No hi ha publicacions",
|
||||
"account.featured_tags.last_status_never": "No hi ha tuts",
|
||||
"account.featured_tags.title": "etiquetes destacades de {name}",
|
||||
"account.follow": "Segueix",
|
||||
"account.followers": "Seguidors",
|
||||
|
@ -57,7 +57,7 @@
|
|||
"account.requested_follow": "{name} ha demanat de seguir-te",
|
||||
"account.share": "Comparteix el perfil de @{name}",
|
||||
"account.show_reblogs": "Mostra els impulsos de @{name}",
|
||||
"account.statuses_counter": "{count, plural, one {{counter} Publicació} other {{counter} Publicacions}}",
|
||||
"account.statuses_counter": "{count, plural, one {{counter} Tut} other {{counter} Tuts}}",
|
||||
"account.unblock": "Desbloca @{name}",
|
||||
"account.unblock_domain": "Desbloca el domini {domain}",
|
||||
"account.unblock_short": "Desbloca",
|
||||
|
@ -128,8 +128,8 @@
|
|||
"compose.language.search": "Cerca idiomes...",
|
||||
"compose_form.direct_message_warning_learn_more": "Més informació",
|
||||
"compose_form.encryption_warning": "Els tuts a Mastodon no estant xifrats punt a punt. No comparteixis informació sensible mitjançant Mastodon.",
|
||||
"compose_form.hashtag_warning": "Aquest tut no es mostrarà en cap etiqueta, ja que no està llistat. Només els tuts públics es poden cercar per etiqueta.",
|
||||
"compose_form.lock_disclaimer": "El teu compte no està {locked}. Tothom pot seguir-te i veure les publicacions de només per a seguidors.",
|
||||
"compose_form.hashtag_warning": "Aquest tut no es mostrarà en cap etiqueta, ja que no és públic. Només els tuts públics es poden cercar per etiqueta.",
|
||||
"compose_form.lock_disclaimer": "El teu compte no està {locked}. Tothom pot seguir-te i veure els tuts de només per a seguidors.",
|
||||
"compose_form.lock_disclaimer.lock": "blocat",
|
||||
"compose_form.placeholder": "Què et passa pel cap?",
|
||||
"compose_form.poll.add_option": "Afegeix una opció",
|
||||
|
@ -165,7 +165,7 @@
|
|||
"confirmations.logout.confirm": "Tanca la sessió",
|
||||
"confirmations.logout.message": "Segur que vols tancar la sessió?",
|
||||
"confirmations.mute.confirm": "Silencia",
|
||||
"confirmations.mute.explanation": "Això amagarà les seves publicacions i les que els mencionen, però encara els permetrà veure les teves i seguir-te.",
|
||||
"confirmations.mute.explanation": "Això amagarà els tuts d'ells i els d'els que els mencionin, però encara els permetrà veure els teus tuts i seguir-te.",
|
||||
"confirmations.mute.message": "Segur que vols silenciar {name}?",
|
||||
"confirmations.redraft.confirm": "Elimina i reescriu-la",
|
||||
"confirmations.redraft.message": "Segur que vols eliminar aquesta publicació i tornar-la a escriure? Es perdran tots els impulsos i els favorits, i les respostes a la publicació original quedaran aïllades.",
|
||||
|
@ -185,7 +185,7 @@
|
|||
"directory.recently_active": "Actius recentment",
|
||||
"disabled_account_banner.account_settings": "Paràmetres del compte",
|
||||
"disabled_account_banner.text": "El teu compte {disabledAccount} està desactivat.",
|
||||
"dismissable_banner.community_timeline": "Aquestes són les publicacions més recents d'usuaris amb el compte a {domain}.",
|
||||
"dismissable_banner.community_timeline": "Aquests són els tuts públics més recents d'usuaris amb els seus comptes a {domain}.",
|
||||
"dismissable_banner.dismiss": "Ometre",
|
||||
"dismissable_banner.explore_links": "Gent d'aquest i d'altres servidors de la xarxa descentralitzada estan comentant ara mateix aquestes notícies.",
|
||||
"dismissable_banner.explore_statuses": "Aquests tuts d'aquest i altres servidors de la xarxa descentralitzada estan guanyant l'atenció ara mateix en aquest servidor.",
|
||||
|
@ -221,6 +221,7 @@
|
|||
"empty_column.favourites": "Encara no ha marcat ningú aquesta publicació com a preferida. Quan ho faci algú apareixerà aquí.",
|
||||
"empty_column.follow_recommendations": "Sembla que no s'han pogut generar suggeriments per a tu. Pots provar d'usar la cerca per a trobar persones que vulguis conèixer o explorar les etiquetes en tendència.",
|
||||
"empty_column.follow_requests": "Encara no tens cap petició de seguiment. Quan en rebis una, apareixerà aquí.",
|
||||
"empty_column.followed_tags": "Encara no segueixes cap etiqueta. Quan ho facis, es mostraran aquí.",
|
||||
"empty_column.hashtag": "Encara no hi ha res en aquesta etiqueta.",
|
||||
"empty_column.home": "La teva línia de temps és buida! Segueix més gent per a emplenar-la. {suggestions}",
|
||||
"empty_column.home.suggestions": "Mostra alguns suggeriments",
|
||||
|
@ -259,10 +260,11 @@
|
|||
"filter_modal.title.status": "Filtra un tut",
|
||||
"follow_recommendations.done": "Fet",
|
||||
"follow_recommendations.heading": "Segueix a la gent de la que t'agradaria veure els seus tuts! Aquí hi ha algunes recomanacions.",
|
||||
"follow_recommendations.lead": "Les publicacions dels usuaris que segueixes es mostraran en ordre cronològic en la teva línia de temps d'Inici. No tinguis por de cometre errors, pots deixar de seguir-los en qualsevol moment!",
|
||||
"follow_recommendations.lead": "Els tuts dels usuaris que segueixes es mostraran en ordre cronològic en la teva línia de temps Inici. No tinguis por en cometre errors, pots fàcilment deixar de seguir-los en qualsevol moment!",
|
||||
"follow_request.authorize": "Autoritza",
|
||||
"follow_request.reject": "Rebutja",
|
||||
"follow_requests.unlocked_explanation": "Tot i que el teu compte no està blocat, el personal de {domain} ha pensat que és possible que vulguis revisar manualment les sol·licituds de seguiment d’aquests comptes.",
|
||||
"followed_tags": "Etiquetes seguides",
|
||||
"footer.about": "Quant a",
|
||||
"footer.directory": "Directori de perfils",
|
||||
"footer.get_app": "Aconsegueix l'app",
|
||||
|
@ -289,7 +291,7 @@
|
|||
"home.hide_announcements": "Amaga els anuncis",
|
||||
"home.show_announcements": "Mostra els anuncis",
|
||||
"interaction_modal.description.favourite": "Amb un compte a Mastodon pots afavorir aquesta publicació, que l'autor sàpiga que t'ha agradat i desar-la per a més endavant.",
|
||||
"interaction_modal.description.follow": "Amb un compte a Mastodon, pots seguir a {name} per a rebre les seves publicacions en la teva línia de temps d'Inici.",
|
||||
"interaction_modal.description.follow": "Amb un compte a Mastodon, pots seguir a {name} per a rebre els seus tuts en la teva línia de temps d'Inici.",
|
||||
"interaction_modal.description.reblog": "Amb un compte a Mastodon, pots impulsar aquesta publicació per a compartir-la amb els teus seguidors.",
|
||||
"interaction_modal.description.reply": "Amb un compte a Mastodon, pots respondre aquest tut.",
|
||||
"interaction_modal.on_another_server": "A un altre servidor",
|
||||
|
@ -379,6 +381,7 @@
|
|||
"navigation_bar.favourites": "Preferits",
|
||||
"navigation_bar.filters": "Paraules silenciades",
|
||||
"navigation_bar.follow_requests": "Sol·licituds de seguiment",
|
||||
"navigation_bar.followed_tags": "Etiquetes seguides",
|
||||
"navigation_bar.follows_and_followers": "Seguint i seguidors",
|
||||
"navigation_bar.lists": "Llistes",
|
||||
"navigation_bar.logout": "Tanca la sessió",
|
||||
|
@ -418,7 +421,7 @@
|
|||
"notifications.column_settings.reblog": "Impulsos:",
|
||||
"notifications.column_settings.show": "Mostra a la columna",
|
||||
"notifications.column_settings.sound": "Reprodueix so",
|
||||
"notifications.column_settings.status": "Noves publicacions:",
|
||||
"notifications.column_settings.status": "Nous tuts:",
|
||||
"notifications.column_settings.unread_notifications.category": "Notificacions no llegides",
|
||||
"notifications.column_settings.unread_notifications.highlight": "Destaca les notificacions no llegides",
|
||||
"notifications.column_settings.update": "Edicions:",
|
||||
|
@ -475,7 +478,7 @@
|
|||
"relative_time.today": "avui",
|
||||
"reply_indicator.cancel": "Cancel·la",
|
||||
"report.block": "Bloca",
|
||||
"report.block_explanation": "No veuràs les seves publicacions. Ell no podran veure les teves ni seguir-te. Podran saber que estan blocats.",
|
||||
"report.block_explanation": "No veuràs els seus tuts. Ells no podran veure els teus tuts ni et podran seguir. Podran saber que estan blocats.",
|
||||
"report.categories.other": "Altres",
|
||||
"report.categories.spam": "Brossa",
|
||||
"report.categories.violation": "El contingut viola una o més regles del servidor",
|
||||
|
@ -488,7 +491,7 @@
|
|||
"report.forward": "Reenvia a {target}",
|
||||
"report.forward_hint": "El compte és d'un altre servidor. Vols enviar-hi també una còpia anònima de l'informe?",
|
||||
"report.mute": "Silencia",
|
||||
"report.mute_explanation": "No veuràs les seves publicacions. Encara pot seguir-te i veure les teves publicacions, però no sabrà que ha estat silenciat.",
|
||||
"report.mute_explanation": "No veuràs els seus tuts. Encara poden seguir-te i veure els teus tuts, però no sabran que han estat silenciats.",
|
||||
"report.next": "Següent",
|
||||
"report.placeholder": "Comentaris addicionals",
|
||||
"report.reasons.dislike": "No m'agrada",
|
||||
|
@ -510,7 +513,7 @@
|
|||
"report.thanks.title": "No ho vols veure?",
|
||||
"report.thanks.title_actionable": "Gràcies per informar, ho investigarem.",
|
||||
"report.unfollow": "Deixa de seguir @{name}",
|
||||
"report.unfollow_explanation": "Segueixes aquest compte. Per no veure les seves publicacions a la teva línia de temps d'Inici deixa de seguir-lo.",
|
||||
"report.unfollow_explanation": "Estàs seguint aquest compte. Per no veure els seus tuts a la teva línia de temps d'Inici, deixa de seguir-lo.",
|
||||
"report_notification.attached_statuses": "{count, plural, one {{count} tut} other {{count} tuts}} adjunts",
|
||||
"report_notification.categories.other": "Altres",
|
||||
"report_notification.categories.spam": "Brossa",
|
||||
|
@ -540,8 +543,9 @@
|
|||
"server_banner.server_stats": "Estadístiques del servidor:",
|
||||
"sign_in_banner.create_account": "Registra'm",
|
||||
"sign_in_banner.sign_in": "Inicia sessió",
|
||||
"sign_in_banner.text": "Inicia la sessió per seguir perfils o etiquetes, afavorir, compartir i respondre a publicacions o interactuar des del teu compte en un servidor diferent.",
|
||||
"sign_in_banner.text": "Inicia la sessió per a seguir perfils o etiquetes, afavorir, compartir i respondre publicacions. També pots interactuar des del teu compte a un servidor diferent.",
|
||||
"status.admin_account": "Obre la interfície de moderació per a @{name}",
|
||||
"status.admin_domain": "Obre la interfície de moderació per a @{domain}",
|
||||
"status.admin_status": "Obrir aquest tut a la interfície de moderació",
|
||||
"status.block": "Bloca @{name}",
|
||||
"status.bookmark": "Marca",
|
||||
|
|
|
@ -128,7 +128,7 @@
|
|||
"compose.language.search": "گەڕان بە زمانەکان...",
|
||||
"compose_form.direct_message_warning_learn_more": "زیاتر فێربه",
|
||||
"compose_form.encryption_warning": "Posts on Mastodon are not end-to-end encrypted. Do not share any dangerous information over Mastodon.",
|
||||
"compose_form.hashtag_warning": "ئەم توتە لە ژێر هیچ هاشتاگییەک دا ناکرێت وەک ئەوەی لە لیستەکەدا نەریزراوە. تەنها توتی گشتی دەتوانرێت بە هاشتاگی بگەڕێت.",
|
||||
"compose_form.hashtag_warning": "This post won't be listed under any hashtag as it is unlisted. Only public posts can be searched by hashtag.",
|
||||
"compose_form.lock_disclaimer": "هەژمێرەکەی لە حاڵەتی {locked}. هەر کەسێک دەتوانێت شوێنت بکەوێت بۆ پیشاندانی بابەتەکانی تەنها دوایخۆی.",
|
||||
"compose_form.lock_disclaimer.lock": "قفڵ دراوە",
|
||||
"compose_form.placeholder": "چی لە مێشکتدایە?",
|
||||
|
@ -221,6 +221,7 @@
|
|||
"empty_column.favourites": "کەس ئەم توتەی دڵخواز نەکردووە،کاتێک کەسێک وا بکات، لێرە دەرئەکەون.",
|
||||
"empty_column.follow_recommendations": "پێدەچێت هیچ پێشنیارێک بۆ تۆ دروست نەکرێت. دەتوانیت هەوڵبدەیت گەڕان بەکاربهێنیت بۆ گەڕان بەدوای ئەو کەسانەی کە ڕەنگە بیانناسیت یان بەدوای هاشتاگە ڕەوتەکاندا بگەڕێیت.",
|
||||
"empty_column.follow_requests": "تۆ هێشتا هیچ داواکارییەکی بەدواداچووت نیە. کاتێک یەکێکت بۆ هات، لێرە دەرئەکەویت.",
|
||||
"empty_column.followed_tags": "You have not followed any hashtags yet. When you do, they will show up here.",
|
||||
"empty_column.hashtag": "هێشتا هیچ شتێک لەم هاشتاگەدا نییە.",
|
||||
"empty_column.home": "تایم لاینی ماڵەوەت بەتاڵە! سەردانی {public} بکە یان گەڕان بەکاربێنە بۆ دەستپێکردن و بینینی بەکارهێنەرانی تر.",
|
||||
"empty_column.home.suggestions": "چەند پێشنیارێک ببینە",
|
||||
|
@ -263,6 +264,7 @@
|
|||
"follow_request.authorize": "دهسهڵاتپێدراو",
|
||||
"follow_request.reject": "ڕەتکردنەوە",
|
||||
"follow_requests.unlocked_explanation": "هەرچەندە هەژمارەکەت داخراو نییە، ستافی {domain} وا بیریان کردەوە کە لەوانەیە بتانەوێت پێداچوونەوە بە داواکاریەکانی ئەم هەژمارەدا بکەن بە دەستی.",
|
||||
"followed_tags": "Followed hashtags",
|
||||
"footer.about": "About",
|
||||
"footer.directory": "Profiles directory",
|
||||
"footer.get_app": "Get the app",
|
||||
|
@ -379,6 +381,7 @@
|
|||
"navigation_bar.favourites": "دڵخوازەکان",
|
||||
"navigation_bar.filters": "وشە کپەکان",
|
||||
"navigation_bar.follow_requests": "بەدواداچوی داواکاریەکان بکە",
|
||||
"navigation_bar.followed_tags": "Followed hashtags",
|
||||
"navigation_bar.follows_and_followers": "شوێنکەوتوو و شوێنکەوتوان",
|
||||
"navigation_bar.lists": "لیستەکان",
|
||||
"navigation_bar.logout": "دەرچوون",
|
||||
|
@ -540,8 +543,9 @@
|
|||
"server_banner.server_stats": "دۆخی ڕاژەکار:",
|
||||
"sign_in_banner.create_account": "هەژمار دروستبکە",
|
||||
"sign_in_banner.sign_in": "بچۆ ژوورەوە",
|
||||
"sign_in_banner.text": "چوونەژوورەوە بۆ فۆڵۆوکردنی پڕۆفایلی یان هاشتاگەکان، دڵخوازکردن، هاوبەشکردن و وەڵامدانەوەی پۆستەکان، یان کارلێککردن لە ئەکاونتەکەتەوە لەسەر سێرڤەرێکی جیاواز.",
|
||||
"sign_in_banner.text": "Sign in to follow profiles or hashtags, favourite, share and reply to posts. You can also interact from your account on a different server.",
|
||||
"status.admin_account": "کردنەوەی میانڕەوی بەڕێوەبەر بۆ @{name}",
|
||||
"status.admin_domain": "Open moderation interface for {domain}",
|
||||
"status.admin_status": "ئەم توتە بکەوە لە ناو ڕووکاری بەڕیوەبەر",
|
||||
"status.block": "@{name} ئاستەنگ بکە",
|
||||
"status.bookmark": "نیشانه",
|
||||
|
@ -558,7 +562,7 @@
|
|||
"status.favourite": "دڵخواز",
|
||||
"status.filter": "ئەم پۆستە فلتەر بکە",
|
||||
"status.filtered": "پاڵاوتن",
|
||||
"status.hide": "شاردنەوەی توت",
|
||||
"status.hide": "Hide post",
|
||||
"status.history.created": "{name} دروستکراوە لە{date}",
|
||||
"status.history.edited": "{name} دروستکاریکراوە لە{date}",
|
||||
"status.load_more": "زیاتر بار بکە",
|
||||
|
|
|
@ -128,7 +128,7 @@
|
|||
"compose.language.search": "Search languages...",
|
||||
"compose_form.direct_message_warning_learn_more": "Amparà di più",
|
||||
"compose_form.encryption_warning": "Posts on Mastodon are not end-to-end encrypted. Do not share any dangerous information over Mastodon.",
|
||||
"compose_form.hashtag_warning": "Stu statutu ùn hè \"Micca listatu\" è ùn sarà micca listatu indè e circate da hashtag. Per esse vistu in quesse, u statutu deve esse \"Pubblicu\".",
|
||||
"compose_form.hashtag_warning": "This post won't be listed under any hashtag as it is unlisted. Only public posts can be searched by hashtag.",
|
||||
"compose_form.lock_disclaimer": "U vostru contu ùn hè micca {locked}. Tuttu u mondu pò seguitavi è vede i vostri statuti privati.",
|
||||
"compose_form.lock_disclaimer.lock": "privatu",
|
||||
"compose_form.placeholder": "À chè pensate?",
|
||||
|
@ -221,6 +221,7 @@
|
|||
"empty_column.favourites": "Nisunu hà aghjuntu stu statutu à i so favuriti. Quandu qualch'unu farà quessa, u so contu sarà mustratu quì.",
|
||||
"empty_column.follow_recommendations": "Si pare ch'ùn s'hè micca pussutu generà e ricumandazione per voi. Pudete sempre pruvà d'utilizà a ricerca per truvà ghjente chì cunnuscete, o splurà l'hashtag in tindenza.",
|
||||
"empty_column.follow_requests": "Ùn avete manc'una dumanda d'abbunamentu. Quandu averete una, sarà mustrata quì.",
|
||||
"empty_column.followed_tags": "You have not followed any hashtags yet. When you do, they will show up here.",
|
||||
"empty_column.hashtag": "Ùn c'hè ancu nunda quì.",
|
||||
"empty_column.home": "A vostr'accolta hè viota! Pudete andà nant'à {public} o pruvà a ricerca per truvà parsone da siguità.",
|
||||
"empty_column.home.suggestions": "Vede qualchì ricumandazione",
|
||||
|
@ -263,6 +264,7 @@
|
|||
"follow_request.authorize": "Auturizà",
|
||||
"follow_request.reject": "Righjittà",
|
||||
"follow_requests.unlocked_explanation": "U vostru contu ùn hè micca privatu, ma a squadra d'amministrazione di {domain} pensa chì e dumande d'abbunamentu di questi conti anu bisognu d'esse verificate manualmente.",
|
||||
"followed_tags": "Followed hashtags",
|
||||
"footer.about": "About",
|
||||
"footer.directory": "Profiles directory",
|
||||
"footer.get_app": "Get the app",
|
||||
|
@ -379,6 +381,7 @@
|
|||
"navigation_bar.favourites": "Favuriti",
|
||||
"navigation_bar.filters": "Parolle silenzate",
|
||||
"navigation_bar.follow_requests": "Dumande d'abbunamentu",
|
||||
"navigation_bar.followed_tags": "Followed hashtags",
|
||||
"navigation_bar.follows_and_followers": "Abbunati è abbunamenti",
|
||||
"navigation_bar.lists": "Liste",
|
||||
"navigation_bar.logout": "Scunnettassi",
|
||||
|
@ -540,8 +543,9 @@
|
|||
"server_banner.server_stats": "Server stats:",
|
||||
"sign_in_banner.create_account": "Create account",
|
||||
"sign_in_banner.sign_in": "Sign in",
|
||||
"sign_in_banner.text": "Sign in to follow profiles or hashtags, favourite, share and reply to posts, or interact from your account on a different server.",
|
||||
"sign_in_banner.text": "Sign in to follow profiles or hashtags, favourite, share and reply to posts. You can also interact from your account on a different server.",
|
||||
"status.admin_account": "Apre l'interfaccia di muderazione per @{name}",
|
||||
"status.admin_domain": "Open moderation interface for {domain}",
|
||||
"status.admin_status": "Apre stu statutu in l'interfaccia di muderazione",
|
||||
"status.block": "Bluccà @{name}",
|
||||
"status.bookmark": "Segnalibru",
|
||||
|
@ -558,7 +562,7 @@
|
|||
"status.favourite": "Aghjunghje à i favuriti",
|
||||
"status.filter": "Filter this post",
|
||||
"status.filtered": "Filtratu",
|
||||
"status.hide": "Hide toot",
|
||||
"status.hide": "Hide post",
|
||||
"status.history.created": "{name} created {date}",
|
||||
"status.history.edited": "{name} edited {date}",
|
||||
"status.load_more": "Vede di più",
|
||||
|
|
|
@ -128,7 +128,7 @@
|
|||
"compose.language.search": "Prohledat jazyky...",
|
||||
"compose_form.direct_message_warning_learn_more": "Zjistit více",
|
||||
"compose_form.encryption_warning": "Příspěvky na Mastodonu nejsou end-to-end šifrovány. Nesdílejte přes Mastodon žádné citlivé informace.",
|
||||
"compose_form.hashtag_warning": "Tento příspěvek nebude zobrazen pod žádným hashtagem, neboť je neveřejný. Pouze veřejné příspěvky mohou být vyhledány podle hashtagu.",
|
||||
"compose_form.hashtag_warning": "Tento příspěvek nebude zobrazen pod žádným hashtagem, protože není veřejný. Podle hashtagu lze vyhledávat jen veřejné příspěvky.",
|
||||
"compose_form.lock_disclaimer": "Váš účet není {locked}. Kdokoliv vás může sledovat a vidět vaše příspěvky učené pouze pro sledující.",
|
||||
"compose_form.lock_disclaimer.lock": "zamčený",
|
||||
"compose_form.placeholder": "Co se vám honí hlavou?",
|
||||
|
@ -221,6 +221,7 @@
|
|||
"empty_column.favourites": "Tento příspěvek si zatím nikdo neoblíbil. Až to někdo udělá, zobrazí se zde.",
|
||||
"empty_column.follow_recommendations": "Zdá se, že pro vás nelze vygenerovat žádné návrhy. Můžete zkusit přes vyhledávání nalézt lidi, které znáte, nebo prozkoumat populární hashtagy.",
|
||||
"empty_column.follow_requests": "Zatím nemáte žádné žádosti o sledování. Až nějakou obdržíte, zobrazí se zde.",
|
||||
"empty_column.followed_tags": "You have not followed any hashtags yet. When you do, they will show up here.",
|
||||
"empty_column.hashtag": "Pod tímto hashtagem zde zatím nic není.",
|
||||
"empty_column.home": "Vaše domovská časová osa je prázdná! Naplňte ji sledováním dalších lidí. {suggestions}",
|
||||
"empty_column.home.suggestions": "Prohlédněte si návrhy",
|
||||
|
@ -263,6 +264,7 @@
|
|||
"follow_request.authorize": "Autorizovat",
|
||||
"follow_request.reject": "Zamítnout",
|
||||
"follow_requests.unlocked_explanation": "Přestože váš účet není zamčený, administrátor {domain} usoudil, že byste mohli chtít tyto žádosti o sledování zkontrolovat ručně.",
|
||||
"followed_tags": "Followed hashtags",
|
||||
"footer.about": "O aplikaci",
|
||||
"footer.directory": "Adresář profilů",
|
||||
"footer.get_app": "Získat aplikaci",
|
||||
|
@ -379,6 +381,7 @@
|
|||
"navigation_bar.favourites": "Oblíbené",
|
||||
"navigation_bar.filters": "Skrytá slova",
|
||||
"navigation_bar.follow_requests": "Žádosti o sledování",
|
||||
"navigation_bar.followed_tags": "Followed hashtags",
|
||||
"navigation_bar.follows_and_followers": "Sledovaní a sledující",
|
||||
"navigation_bar.lists": "Seznamy",
|
||||
"navigation_bar.logout": "Odhlásit se",
|
||||
|
@ -540,8 +543,9 @@
|
|||
"server_banner.server_stats": "Statistiky serveru:",
|
||||
"sign_in_banner.create_account": "Vytvořit účet",
|
||||
"sign_in_banner.sign_in": "Přihlásit se",
|
||||
"sign_in_banner.text": "Přihlaste se pro sledování profilů nebo hashtagů, oblíbení, sdílení a odpovědí na příspěvky nebo interakci z vašeho účtu na jiném serveru.",
|
||||
"sign_in_banner.text": "Sign in to follow profiles or hashtags, favourite, share and reply to posts. You can also interact from your account on a different server.",
|
||||
"status.admin_account": "Otevřít moderátorské rozhraní pro @{name}",
|
||||
"status.admin_domain": "Otevřít moderátorské rozhraní pro {domain}",
|
||||
"status.admin_status": "Otevřít tento příspěvek v moderátorském rozhraní",
|
||||
"status.block": "Blokovat @{name}",
|
||||
"status.bookmark": "Přidat do záložek",
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue