diff options
312 files changed, 5236 insertions, 1393 deletions
diff --git a/.eslintrc.js b/.eslintrc.js index 56e3d0530..177496d3a 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -41,6 +41,11 @@ module.exports = { 'node_modules', '\\.(css|scss|json)$', ], + 'import/resolver': { + node: { + paths: ['app/javascript'], + }, + }, }, rules: { diff --git a/AUTHORS.md b/AUTHORS.md index 1bcf455b1..3171214e0 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -9,18 +9,18 @@ and provided thanks to the work of the following contributors: * [akihikodaki](https://github.com/akihikodaki) * [ThibG](https://github.com/ThibG) * [mjankowski](https://github.com/mjankowski) +* [dependabot[bot]](https://github.com/apps/dependabot) * [unarist](https://github.com/unarist) * [m4sk1n](https://github.com/m4sk1n) -* [dependabot[bot]](https://github.com/apps/dependabot) * [yiskah](https://github.com/yiskah) * [nolanlawson](https://github.com/nolanlawson) -* [sorin-davidoi](https://github.com/sorin-davidoi) * [ysksn](https://github.com/ysksn) +* [sorin-davidoi](https://github.com/sorin-davidoi) * [abcang](https://github.com/abcang) * [lynlynlynx](https://github.com/lynlynlynx) -* [alpaca-tc](https://github.com/alpaca-tc) * [mayaeh](https://github.com/mayaeh) * [renatolond](https://github.com/renatolond) +* [alpaca-tc](https://github.com/alpaca-tc) * [nclm](https://github.com/nclm) * [ineffyble](https://github.com/ineffyble) * [jeroenpraat](https://github.com/jeroenpraat) @@ -28,9 +28,9 @@ and provided thanks to the work of the following contributors: * [Quent-in](https://github.com/Quent-in) * [JantsoP](https://github.com/JantsoP) * [mabkenar](https://github.com/mabkenar) +* [Kjwon15](https://github.com/Kjwon15) * [nullkal](https://github.com/nullkal) * [yookoala](https://github.com/yookoala) -* [Kjwon15](https://github.com/Kjwon15) * [shuheiktgw](https://github.com/shuheiktgw) * [ashfurrow](https://github.com/ashfurrow) * [Quenty31](https://github.com/Quenty31) @@ -48,16 +48,16 @@ and provided thanks to the work of the following contributors: * [rkarabut](https://github.com/rkarabut) * [yukimochi](https://github.com/yukimochi) * [Artoria2e5](https://github.com/Artoria2e5) +* [nightpool](https://github.com/nightpool) * [marrus-sh](https://github.com/marrus-sh) * [krainboltgreene](https://github.com/krainboltgreene) -* [patf](https://github.com/patf) +* [pfigel](https://github.com/pfigel) * [Aldarone](https://github.com/Aldarone) * [BoFFire](https://github.com/BoFFire) * [clworld](https://github.com/clworld) * [dracos](https://github.com/dracos) * [SerCom_KC](mailto:sercom-kc@users.noreply.github.com) * [Sylvhem](https://github.com/Sylvhem) -* [nightpool](https://github.com/nightpool) * [MasterGroosha](https://github.com/MasterGroosha) * [JeanGauthier](https://github.com/JeanGauthier) * [kschaper](https://github.com/kschaper) @@ -77,11 +77,14 @@ and provided thanks to the work of the following contributors: * [johnsudaar](https://github.com/johnsudaar) * [trebmuh](https://github.com/trebmuh) * [Rakib Hasan](mailto:rmhasan@gmail.com) +* [ashleyhull-versent](https://github.com/ashleyhull-versent) * [lindwurm](https://github.com/lindwurm) * [victorhck](mailto:victorhck@geeko.site) * [voidsatisfaction](https://github.com/voidsatisfaction) +* [rinsuki](https://github.com/rinsuki) * [hikari-no-yume](https://github.com/hikari-no-yume) * [angristan](https://github.com/angristan) +* [hinaloe](https://github.com/hinaloe) * [seefood](https://github.com/seefood) * [jackjennings](https://github.com/jackjennings) * [spla](mailto:spla@mastodont.cat) @@ -92,20 +95,20 @@ and provided thanks to the work of the following contributors: * [dunn](https://github.com/dunn) * [xqus](https://github.com/xqus) * [hugogameiro](https://github.com/hugogameiro) +* [ariasuni](https://github.com/ariasuni) * [pfm-eyesightjp](https://github.com/pfm-eyesightjp) * [fakenine](https://github.com/fakenine) * [tsuwatch](https://github.com/tsuwatch) * [victorhck](https://github.com/victorhck) -* [ashleyhull-versent](https://github.com/ashleyhull-versent) * [kedamaDQ](https://github.com/kedamaDQ) * [puckipedia](https://github.com/puckipedia) * [fvh-P](https://github.com/fvh-P) * [contraexemplo](https://github.com/contraexemplo) +* [Aditoo17](https://github.com/Aditoo17) * [kazu9su](https://github.com/kazu9su) * [Komic](https://github.com/Komic) * [lmorchard](https://github.com/lmorchard) * [diomed](https://github.com/diomed) -* [ariasuni](https://github.com/ariasuni) * [Neetshin](mailto:neetshin@neetsh.in) * [rainyday](https://github.com/rainyday) * [ProgVal](https://github.com/ProgVal) @@ -114,7 +117,8 @@ and provided thanks to the work of the following contributors: * [goofy-bz](mailto:goofy@babelzilla.org) * [kadiix](https://github.com/kadiix) * [kodacs](https://github.com/kodacs) -* [rtucker](https://github.com/rtucker) +* [trwnh](https://github.com/trwnh) +* [JMendyk](https://github.com/JMendyk) * [KScl](https://github.com/KScl) * [sterdev](https://github.com/sterdev) * [TheKinrar](https://github.com/TheKinrar) @@ -125,16 +129,16 @@ and provided thanks to the work of the following contributors: * [fhemberger](https://github.com/fhemberger) * [greysteil](https://github.com/greysteil) * [hensmith](https://github.com/hensmith) -* [hinaloe](https://github.com/hinaloe) * [d6rkaiz](https://github.com/d6rkaiz) * [Reverite](https://github.com/Reverite) -* [JMendyk](https://github.com/JMendyk) * [JohnD28](https://github.com/JohnD28) * [znz](https://github.com/znz) * [Naouak](https://github.com/Naouak) * [pawelngei](https://github.com/pawelngei) +* [rtucker](https://github.com/rtucker) * [reneklacan](https://github.com/reneklacan) * [ekiru](https://github.com/ekiru) +* [noellabo](https://github.com/noellabo) * [tcitworld](https://github.com/tcitworld) * [geta6](https://github.com/geta6) * [happycoloredbanana](https://github.com/happycoloredbanana) @@ -144,9 +148,9 @@ and provided thanks to the work of the following contributors: * [noraworld](https://github.com/noraworld) * [theboss](https://github.com/theboss) * [178inaba](https://github.com/178inaba) -* [Aditoo17](https://github.com/Aditoo17) * [alyssais](https://github.com/alyssais) -* [kodnaplakal](https://github.com/kodnaplakal) +* [hiphref](https://github.com/hiphref) +* [BenLubar](https://github.com/BenLubar) * [stalker314314](https://github.com/stalker314314) * [huertanix](https://github.com/huertanix) * [genesixx](https://github.com/genesixx) @@ -157,6 +161,7 @@ and provided thanks to the work of the following contributors: * [kmichl](https://github.com/kmichl) * [Kurtis Rainbolt-Greene](mailto:me@kurtisrainboltgreene.name) * [saper](https://github.com/saper) +* [marek-lach](https://github.com/marek-lach) * [nevillepark](https://github.com/nevillepark) * [ornithocoder](https://github.com/ornithocoder) * [pierreozoux](https://github.com/pierreozoux) @@ -164,7 +169,6 @@ and provided thanks to the work of the following contributors: * [Ram Lmn](mailto:ramlmn@users.noreply.github.com) * [harukasan](https://github.com/harukasan) * [stamak](https://github.com/stamak) -* [noellabo](https://github.com/noellabo) * [Technowix](mailto:technowix@users.noreply.github.com) * [Eychics](https://github.com/Eychics) * [Thor Harald Johansen](mailto:thj@thj.no) @@ -179,21 +183,20 @@ and provided thanks to the work of the following contributors: * [hoodie](mailto:hoodiekitten@outlook.com) * [luzi82](https://github.com/luzi82) * [duxovni](https://github.com/duxovni) -* [trwnh](https://github.com/trwnh) +* [tmm576](https://github.com/tmm576) * [unsmell](https://github.com/unsmell) * [valerauko](https://github.com/valerauko) * [chriswmartin](https://github.com/chriswmartin) * [vahnj](https://github.com/vahnj) * [ikuradon](https://github.com/ikuradon) * [AndreLewin](https://github.com/AndreLewin) -* [rinsuki](https://github.com/rinsuki) * [0xflotus](https://github.com/0xflotus) * [redtachyons](https://github.com/redtachyons) * [thurloat](https://github.com/thurloat) * [aaribaud](https://github.com/aaribaud) +* [pointlessone](https://github.com/pointlessone) * [Andrew](mailto:andrewlchronister@gmail.com) * [estuans](https://github.com/estuans) -* [BenLubar](https://github.com/BenLubar) * [dissolve](https://github.com/dissolve) * [PurpleBooth](https://github.com/PurpleBooth) * [bradurani](https://github.com/bradurani) @@ -216,6 +219,7 @@ and provided thanks to the work of the following contributors: * [ErikXXon](https://github.com/ErikXXon) * [ian-kelling](https://github.com/ian-kelling) * [immae](https://github.com/immae) +* [J0WI](https://github.com/J0WI) * [foozmeat](https://github.com/foozmeat) * [jasonrhodes](https://github.com/jasonrhodes) * [Jason Snell](mailto:jason@newrelic.com) @@ -230,6 +234,7 @@ and provided thanks to the work of the following contributors: * [Lorenz Diener](mailto:halcyon@icosahedron.website) * [alimony](https://github.com/alimony) * [mig5](https://github.com/mig5) +* [moritzheiber](https://github.com/moritzheiber) * [ndarville](https://github.com/ndarville) * [Abzol](https://github.com/Abzol) * [pwoolcoc](https://github.com/pwoolcoc) @@ -238,6 +243,7 @@ and provided thanks to the work of the following contributors: * [ignisf](https://github.com/ignisf) * [raymestalez](https://github.com/raymestalez) * [remram44](https://github.com/remram44) +* [sts10](https://github.com/sts10) * [sascha-sl](https://github.com/sascha-sl) * [u1-liquid](https://github.com/u1-liquid) * [sim6](https://github.com/sim6) @@ -288,6 +294,7 @@ and provided thanks to the work of the following contributors: * [857b](https://github.com/857b) * [insom](https://github.com/insom) * [tachyons](https://github.com/tachyons) +* [acid-chicken](https://github.com/acid-chicken) * [Esteth](https://github.com/Esteth) * [unascribed](https://github.com/unascribed) * [Aguay-val](https://github.com/Aguay-val) @@ -297,7 +304,6 @@ and provided thanks to the work of the following contributors: * [unleashed](https://github.com/unleashed) * [alxrcs](https://github.com/alxrcs) * [console-cowboy](https://github.com/console-cowboy) -* [pointlessone](https://github.com/pointlessone) * [Alkarex](https://github.com/Alkarex) * [a2](https://github.com/a2) * [0xa](https://github.com/0xa) @@ -329,6 +335,7 @@ and provided thanks to the work of the following contributors: * [Motoma](https://github.com/Motoma) * [chriswk](https://github.com/chriswk) * [csu](https://github.com/csu) +* [clarcharr](https://github.com/clarcharr) * [kklleemm](https://github.com/kklleemm) * [colindean](https://github.com/colindean) * [dachinat](https://github.com/dachinat) @@ -356,6 +363,7 @@ and provided thanks to the work of the following contributors: * [espenronnevik](https://github.com/espenronnevik) * [Finariel](https://github.com/Finariel) * [siuying](https://github.com/siuying) +* [zoc](https://github.com/zoc) * [fwenzel](https://github.com/fwenzel) * [GenbuHase](https://github.com/GenbuHase) * [hattori6789](https://github.com/hattori6789) @@ -416,6 +424,7 @@ and provided thanks to the work of the following contributors: * [martymcguire](https://github.com/martymcguire) * [marvinkopf](https://github.com/marvinkopf) * [otsune](https://github.com/otsune) +* [mbugowski](https://github.com/mbugowski) * [Mathias B](mailto:10813340+mathias-b@users.noreply.github.com) * [matt-auckland](https://github.com/matt-auckland) * [webroo](https://github.com/webroo) @@ -434,7 +443,6 @@ and provided thanks to the work of the following contributors: * [premist](https://github.com/premist) * [Mnkai](https://github.com/Mnkai) * [mitchhentges](https://github.com/mitchhentges) -* [moritzheiber](https://github.com/moritzheiber) * [mouse-reeve](https://github.com/mouse-reeve) * [Mozinet-fr](https://github.com/Mozinet-fr) * [lae](https://github.com/lae) @@ -458,17 +466,17 @@ and provided thanks to the work of the following contributors: * [Pangoraw](https://github.com/Pangoraw) * [peterkeen](https://github.com/peterkeen) * [pgate](https://github.com/pgate) -* [retokromer](https://github.com/retokromer) -* [rfwatson](https://github.com/rfwatson) -* [rfreebern](https://github.com/rfreebern) +* [Reto Kromer](mailto:retokromer@users.noreply.github.com) +* [Rey Tucker](mailto:git@reytucker.us) +* [Rob Watson](mailto:rfwatson@users.noreply.github.com) +* [Ryan Freebern](mailto:ryan@freebern.org) * [Ryan Wade](mailto:ryan.wade@protonmail.com) -* [sylph01](https://github.com/sylph01) -* [S-H-GAMELINKS](https://github.com/S-H-GAMELINKS) -* [staticsafe](https://github.com/staticsafe) -* [snwh](https://github.com/snwh) -* [sts10](https://github.com/sts10) -* [skoji](https://github.com/skoji) -* [ScienJus](https://github.com/ScienJus) +* [Ryo Kajiwara](mailto:kfe-fecn6.prussian@s01.info) +* [S.H](mailto:gamelinks007@gmail.com) +* [Sadiq Saif](mailto:staticsafe@users.noreply.github.com) +* [Sam Hewitt](mailto:hewittsamuel@gmail.com) +* [Satoshi KOJIMA](mailto:skoji@mac.com) +* [ScienJus](mailto:i@scienjus.com) * [Scott Larkin](mailto:scott@codeclimate.com) * [Sebastian Hübner](mailto:imolein@users.noreply.github.com) * [Sebastian Morr](mailto:sebastian@morr.cc) @@ -483,6 +491,7 @@ and provided thanks to the work of the following contributors: * [Sir-Boops](mailto:admin@boops.me) * [Soshi Kato](mailto:mail@sossii.com) * [Spanky](mailto:2788886+spankyworks@users.noreply.github.com) +* [Stanislas](mailto:angristan@pm.me) * [StefOfficiel](mailto:pichard.stephane@free.fr) * [Steven Tappert](mailto:admin@dark-it.net) * [Svetlozar Todorov](mailto:svetlik@users.noreply.github.com) @@ -532,6 +541,7 @@ and provided thanks to the work of the following contributors: * [fsubal](mailto:fsubal@users.noreply.github.com) * [fusshi-](mailto:dikky1218@users.noreply.github.com) * [gentaro](mailto:gentaroooo@gmail.com) +* [gol-cha](mailto:info@mevo.xyz) * [hakoai](mailto:hk--76@qa2.so-net.ne.jp) * [haosbvnker](mailto:github@chaosbunker.com) * [isati](mailto:phil@juchnowi.cz) @@ -549,12 +559,12 @@ and provided thanks to the work of the following contributors: * [luzpaz](mailto:luzpaz@users.noreply.github.com) * [maxypy](mailto:maxime@mpigou.fr) * [mhe](mailto:mail@marcus-herrmann.com) +* [mike castleman](mailto:m@mlcastle.net) * [mimikun](mailto:dzdzble_effort_311@outlook.jp) * [mshrtkch](mailto:mshrtkch@users.noreply.github.com) * [muan](mailto:muan@github.com) * [namelessGonbai](mailto:43787036+namelessgonbai@users.noreply.github.com) * [neetshin](mailto:neetshin@neetsh.in) -* [nightpool](mailto:nightpool@users.noreply.github.com) * [rch850](mailto:rich850@gmail.com) * [roikale](mailto:roikale@users.noreply.github.com) * [rysiekpl](mailto:rysiek@hackerspace.pl) diff --git a/CHANGELOG.md b/CHANGELOG.md index cfb6b15f5..f3356d542 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,47 @@ Changelog All notable changes to this project will be documented in this file. +## [2.7.2] - 2019-02-17 +### Added + +- Add support for IPv6 in e-mail validation ([zoc](https://github.com/tootsuite/mastodon/pull/10009)) +- Add record of IP address used for signing up ([ThibG](https://github.com/tootsuite/mastodon/pull/10026)) +- Add tight rate-limit for API deletions (30 per 30 minutes) ([Gargron](https://github.com/tootsuite/mastodon/pull/10042)) +- Add support for embedded `Announce` objects attributed to the same actor ([ThibG](https://github.com/tootsuite/mastodon/pull/9998), [Gargron](https://github.com/tootsuite/mastodon/pull/10065)) +- Add spam filter for `Create` and `Announce` activities ([Gargron](https://github.com/tootsuite/mastodon/pull/10005), [Gargron](https://github.com/tootsuite/mastodon/pull/10041), [Gargron](https://github.com/tootsuite/mastodon/pull/10062)) +- Add `registrations` attribute to `GET /api/v1/instance` ([Gargron](https://github.com/tootsuite/mastodon/pull/10060)) +- Add `vapid_key` to `POST /api/v1/apps` and `GET /api/v1/apps/verify_credentials` ([Gargron](https://github.com/tootsuite/mastodon/pull/10058)) + +### Fixed + +- Fix link color and add link underlines in high-contrast theme ([Gargron](https://github.com/tootsuite/mastodon/pull/9949), [Gargron](https://github.com/tootsuite/mastodon/pull/10028)) +- Fix unicode characters in URLs not being linkified ([JMendyk](https://github.com/tootsuite/mastodon/pull/8447), [hinaloe](https://github.com/tootsuite/mastodon/pull/9991)) +- Fix URLs linkifier grabbing ending quotation as part of the link ([Gargron](https://github.com/tootsuite/mastodon/pull/9997)) +- Fix authorized applications page design ([rinsuki](https://github.com/tootsuite/mastodon/pull/9969)) +- Fix custom emojis not showing up in share page emoji picker ([rinsuki](https://github.com/tootsuite/mastodon/pull/9970)) +- Fix too liberal application of whitespace in toots ([trwnh](https://github.com/tootsuite/mastodon/pull/9968)) +- Fix misleading e-mail hint being displayed in admin view ([ThibG](https://github.com/tootsuite/mastodon/pull/9973)) +- Fix tombstones not being cleared out ([abcang](https://github.com/tootsuite/mastodon/pull/9978)) +- Fix some timeline jumps ([ThibG](https://github.com/tootsuite/mastodon/pull/9982), [ThibG](https://github.com/tootsuite/mastodon/pull/10001), [rinsuki](https://github.com/tootsuite/mastodon/pull/10046)) +- Fix content warning input taking keyboard focus even when hidden ([hinaloe](https://github.com/tootsuite/mastodon/pull/10017)) +- Fix hashtags select styling in default and high-contrast themes ([Gargron](https://github.com/tootsuite/mastodon/pull/10029)) +- Fix style regressions on landing page ([Gargron](https://github.com/tootsuite/mastodon/pull/10030)) +- Fix hashtag column not subscribing to stream on mount ([Gargron](https://github.com/tootsuite/mastodon/pull/10040)) +- Fix relay enabling/disabling not resetting inbox availability status ([Gargron](https://github.com/tootsuite/mastodon/pull/10048)) +- Fix mutes, blocks, domain blocks and follow requests not paginating ([Gargron](https://github.com/tootsuite/mastodon/pull/10057)) +- Fix crash on public hashtag pages when streaming fails ([ThibG](https://github.com/tootsuite/mastodon/pull/10061)) + +### Changed + +- Change icon for unlisted visibility level ([clarcharr](https://github.com/tootsuite/mastodon/pull/9952)) +- Change queue of actor deletes from push to pull for non-follower recipients ([ThibG](https://github.com/tootsuite/mastodon/pull/10016)) +- Change robots.txt to exclude media proxy URLs ([nightpool](https://github.com/tootsuite/mastodon/pull/10038)) +- Change upload description input to allow line breaks ([BenLubar](https://github.com/tootsuite/mastodon/pull/10036)) +- Change `dist/mastodon-streaming.service` to recommend running node without intermediary npm command ([nolanlawson](https://github.com/tootsuite/mastodon/pull/10032)) +- Change conversations to always show names of other participants ([Gargron](https://github.com/tootsuite/mastodon/pull/10047)) +- Change buttons on timeline preview to open the interaction dialog ([Gargron](https://github.com/tootsuite/mastodon/pull/10054)) +- Change error graphic to hover-to-play ([Gargron](https://github.com/tootsuite/mastodon/pull/10055)) + ## [2.7.1] - 2019-01-28 ### Fixed diff --git a/Dockerfile b/Dockerfile index aecf5a385..5ca5d0915 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,19 +24,18 @@ COPY --from=node /usr/local/lib/node_modules /usr/local/lib/node_modules COPY --from=node /usr/local/bin/npm /usr/local/bin/npm COPY --from=node /opt/yarn-* /opt/yarn -RUN apk -U upgrade \ - && apk add -t build-dependencies \ +RUN apk add --no-cache -t build-dependencies \ build-base \ icu-dev \ libidn-dev \ - libressl \ + openssl \ libtool \ libxml2-dev \ libxslt-dev \ postgresql-dev \ protobuf-dev \ python \ - && apk add \ + && apk add --no-cache \ ca-certificates \ ffmpeg \ file \ @@ -64,7 +63,7 @@ RUN apk -U upgrade \ && make install \ && libtool --finish /usr/local/lib \ && cd /mastodon \ - && rm -rf /tmp/* /var/cache/apk/* + && rm -rf /tmp/* COPY Gemfile Gemfile.lock package.json yarn.lock .yarnclean /mastodon/ COPY stack-fix.c /lib diff --git a/Gemfile b/Gemfile index 51595a758..b83284823 100644 --- a/Gemfile +++ b/Gemfile @@ -24,7 +24,7 @@ gem 'streamio-ffmpeg', '~> 3.0' gem 'active_model_serializers', '~> 0.10' gem 'addressable', '~> 2.6' -gem 'bootsnap', '~> 1.3', require: false +gem 'bootsnap', '~> 1.4', require: false gem 'browser' gem 'charlock_holmes', '~> 0.7.6' gem 'iso-639' @@ -108,15 +108,15 @@ group :production, :test do end group :test do - gem 'capybara', '~> 3.12' + gem 'capybara', '~> 3.13' gem 'climate_control', '~> 0.2' gem 'faker', '~> 1.9' - gem 'microformats', '~> 4.0' + gem 'microformats', '~> 4.1' gem 'rails-controller-testing', '~> 1.0' gem 'rspec-sidekiq', '~> 3.0' gem 'simplecov', '~> 0.16', require: false gem 'webmock', '~> 3.5' - gem 'parallel_tests', '~> 2.27' + gem 'parallel_tests', '~> 2.28' end group :development do @@ -128,7 +128,7 @@ group :development do gem 'letter_opener', '~> 1.7' gem 'letter_opener_web', '~> 1.3' gem 'memory_profiler' - gem 'rubocop', '~> 0.63', require: false + gem 'rubocop', '~> 0.64', require: false gem 'brakeman', '~> 4.4', require: false gem 'bundler-audit', '~> 0.6', require: false gem 'scss_lint', '~> 0.57', require: false diff --git a/Gemfile.lock b/Gemfile.lock index ce961afa8..40f4149c7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -38,7 +38,7 @@ GEM erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.0.3) - active_model_serializers (0.10.8) + active_model_serializers (0.10.9) actionpack (>= 4.1, < 6) activemodel (>= 4.1, < 6) case_transform (>= 0.2) @@ -92,13 +92,13 @@ GEM aws-sigv4 (1.0.3) bcrypt (3.1.12) benchmark-ips (2.7.2) - better_errors (2.5.0) + better_errors (2.5.1) coderay (>= 1.0.0) erubi (>= 1.0.0) rack (>= 0.9.0) binding_of_caller (0.8.0) debug_inspector (>= 0.0.1) - bootsnap (1.3.2) + bootsnap (1.4.0) msgpack (~> 1.0) brakeman (4.4.0) browser (2.5.3) @@ -126,7 +126,7 @@ GEM sshkit (~> 1.3) capistrano-yarn (2.0.2) capistrano (~> 3.0) - capybara (3.12.0) + capybara (3.13.2) addressable mini_mime (>= 0.1.3) nokogiri (~> 1.8) @@ -205,7 +205,7 @@ GEM tzinfo excon (0.62.0) fabrication (2.20.1) - faker (1.9.1) + faker (1.9.3) i18n (>= 0.7) faraday (0.15.0) multipart-post (>= 1.2, < 3) @@ -268,10 +268,10 @@ GEM domain_name (~> 0.5) http-form_data (2.1.1) http_accept_language (2.1.1) - httplog (1.2.0) + httplog (1.2.1) rack (>= 1.0) rainbow (>= 2.0.0) - i18n (1.5.2) + i18n (1.5.3) concurrent-ruby (~> 1.0) i18n-tasks (0.9.28) activesupport (>= 4.0.2) @@ -337,9 +337,9 @@ GEM redis (>= 3.0.5) memory_profiler (0.9.12) method_source (0.9.2) - microformats (4.0.7) - json - nokogiri + microformats (4.1.0) + json (~> 2.1) + nokogiri (~> 1.8, >= 1.8.3) mime-types (3.2.2) mime-types-data (~> 3.2015) mime-types-data (3.2018.0812) @@ -347,7 +347,7 @@ GEM mini_mime (1.0.1) mini_portile2 (2.4.0) minitest (5.11.3) - msgpack (1.2.4) + msgpack (1.2.6) multi_json (1.13.1) multipart-post (2.0.0) necromancer (0.4.0) @@ -392,7 +392,7 @@ GEM av (~> 0.9.0) paperclip (>= 2.5.2) parallel (1.13.0) - parallel_tests (2.27.1) + parallel_tests (2.28.0) parallel parser (2.6.0.0) ast (~> 2.4.0) @@ -402,7 +402,7 @@ GEM pg (1.1.4) pghero (2.2.0) activerecord - pkg-config (1.3.2) + pkg-config (1.3.3) powerpack (0.1.2) premailer (1.11.1) addressable @@ -457,7 +457,7 @@ GEM nokogiri (>= 1.6) rails-html-sanitizer (1.0.4) loofah (~> 2.2, >= 2.2.2) - rails-i18n (5.1.2) + rails-i18n (5.1.3) i18n (>= 0.7, < 2) railties (>= 5.0, < 6) rails-settings-cached (0.6.6) @@ -527,7 +527,7 @@ GEM rspec-core (~> 3.0, >= 3.0.0) sidekiq (>= 2.4.0) rspec-support (3.8.0) - rubocop (0.63.1) + rubocop (0.64.0) jaro_winkler (~> 1.5.1) parallel (~> 1.10) parser (>= 2.5, != 2.5.1.1) @@ -565,7 +565,7 @@ GEM rufus-scheduler (~> 3.2) sidekiq (>= 3) tilt (>= 1.4.0) - sidekiq-unique-jobs (6.0.8) + sidekiq-unique-jobs (6.0.9) concurrent-ruby (~> 1.0, >= 1.0.5) sidekiq (>= 4.0, < 6.0) thor (~> 0) @@ -662,7 +662,7 @@ DEPENDENCIES aws-sdk-s3 (~> 1.30) better_errors (~> 2.5) binding_of_caller (~> 0.7) - bootsnap (~> 1.3) + bootsnap (~> 1.4) brakeman (~> 4.4) browser bullet (~> 5.9) @@ -671,7 +671,7 @@ DEPENDENCIES capistrano-rails (~> 1.4) capistrano-rbenv (~> 2.1) capistrano-yarn (~> 2.0) - capybara (~> 3.12) + capybara (~> 3.13) charlock_holmes (~> 0.7.6) chewy (~> 5.0) cld3 (~> 3.2.3) @@ -712,7 +712,7 @@ DEPENDENCIES makara (~> 0.4) mario-redis-lock (~> 1.2) memory_profiler - microformats (~> 4.0) + microformats (~> 4.1) mime-types (~> 3.2) net-ldap (~> 0.10) nokogiri (~> 1.10) @@ -725,7 +725,7 @@ DEPENDENCIES ox (~> 2.10) paperclip (~> 6.0) paperclip-av-transcoder (~> 0.6) - parallel_tests (~> 2.27) + parallel_tests (~> 2.28) pg (~> 1.1) pghero (~> 2.2) pkg-config (~> 1.3) @@ -749,7 +749,7 @@ DEPENDENCIES rqrcode (~> 0.10) rspec-rails (~> 3.8) rspec-sidekiq (~> 3.0) - rubocop (~> 0.63) + rubocop (~> 0.64) sanitize (~> 5.0) scss_lint (~> 0.57) sidekiq (~> 5.2) diff --git a/app/chewy/statuses_index.rb b/app/chewy/statuses_index.rb index d3104172c..eafc1818b 100644 --- a/app/chewy/statuses_index.rb +++ b/app/chewy/statuses_index.rb @@ -31,7 +31,7 @@ class StatusesIndex < Chewy::Index }, } - define_type ::Status.unscoped.without_reblogs do + define_type ::Status.unscoped.without_reblogs.includes(:media_attachments) do crutch :mentions do |collection| data = ::Mention.where(status_id: collection.map(&:id)).pluck(:status_id, :account_id) data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) } @@ -50,7 +50,7 @@ class StatusesIndex < Chewy::Index root date_detection: false do field :account_id, type: 'long' - field :text, type: 'text', value: ->(status) { [status.spoiler_text, Formatter.instance.plaintext(status)].join("\n\n") } do + field :text, type: 'text', value: ->(status) { [status.spoiler_text, Formatter.instance.plaintext(status)].concat(status.media_attachments.map(&:description)).join("\n\n") } do field :stemmed, type: 'text', analyzer: 'content' end diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index 3a4382850..442e99089 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -53,11 +53,12 @@ class AccountsController < ApplicationController private def show_pinned_statuses? - [replies_requested?, media_requested?, params[:max_id].present?, params[:min_id].present?].none? + [replies_requested?, media_requested?, tag_requested?, params[:max_id].present?, params[:min_id].present?].none? end def filtered_statuses default_statuses.tap do |statuses| + statuses.merge!(hashtag_scope) if tag_requested? statuses.merge!(only_media_scope) if media_requested? statuses.merge!(no_replies_scope) unless replies_requested? end @@ -79,12 +80,15 @@ class AccountsController < ApplicationController Status.without_replies end + def hashtag_scope + Status.tagged_with(Tag.find_by(name: params[:tag].downcase)&.id) + end + def set_account @account = Account.find_local!(params[:username]) end def older_url - ::Rails.logger.info("older: max_id #{@statuses.last.id}, url #{pagination_url(max_id: @statuses.last.id)}") pagination_url(max_id: @statuses.last.id) end @@ -93,7 +97,9 @@ class AccountsController < ApplicationController end def pagination_url(max_id: nil, min_id: nil) - if media_requested? + if tag_requested? + short_account_tag_url(@account, params[:tag], max_id: max_id, min_id: min_id) + elsif media_requested? short_account_media_url(@account, max_id: max_id, min_id: min_id) elsif replies_requested? short_account_with_replies_url(@account, max_id: max_id, min_id: min_id) @@ -110,6 +116,10 @@ class AccountsController < ApplicationController request.path.ends_with?('/with_replies') end + def tag_requested? + request.path.ends_with?(Addressable::URI.parse("/tagged/#{params[:tag]}").normalize) + end + def filtered_status_page(params) if params[:min_id].present? filtered_statuses.paginate_by_min_id(PAGE_SIZE, params[:min_id]).reverse diff --git a/app/controllers/admin/instances_controller.rb b/app/controllers/admin/instances_controller.rb index 431ce6f4d..6dd659a30 100644 --- a/app/controllers/admin/instances_controller.rb +++ b/app/controllers/admin/instances_controller.rb @@ -38,7 +38,7 @@ module Admin end def filter_params - params.permit(:limited) + params.permit(:limited, :by_domain) end end end diff --git a/app/controllers/api/v1/accounts/statuses_controller.rb b/app/controllers/api/v1/accounts/statuses_controller.rb index 6c2a5c141..6fdc827cb 100644 --- a/app/controllers/api/v1/accounts/statuses_controller.rb +++ b/app/controllers/api/v1/accounts/statuses_controller.rb @@ -33,6 +33,7 @@ class Api::V1::Accounts::StatusesController < Api::BaseController statuses.merge!(only_media_scope) if truthy_param?(:only_media) statuses.merge!(no_replies_scope) if truthy_param?(:exclude_replies) statuses.merge!(no_reblogs_scope) if truthy_param?(:exclude_reblogs) + statuses.merge!(hashtag_scope) if params[:tagged].present? statuses end @@ -67,6 +68,10 @@ class Api::V1::Accounts::StatusesController < Api::BaseController Status.without_reblogs end + def hashtag_scope + Status.tagged_with(Tag.find_by(name: params[:tagged])&.id) + end + def pagination_params(core_params) params.slice(:limit, :only_media, :exclude_replies).permit(:limit, :only_media, :exclude_replies).merge(core_params) end diff --git a/app/controllers/api/v1/apps/credentials_controller.rb b/app/controllers/api/v1/apps/credentials_controller.rb index e469c7d21..8b63d0490 100644 --- a/app/controllers/api/v1/apps/credentials_controller.rb +++ b/app/controllers/api/v1/apps/credentials_controller.rb @@ -6,6 +6,6 @@ class Api::V1::Apps::CredentialsController < Api::BaseController respond_to :json def show - render json: doorkeeper_token.application, serializer: REST::StatusSerializer::ApplicationSerializer + render json: doorkeeper_token.application, serializer: REST::ApplicationSerializer, fields: %i(name website vapid_key) end end diff --git a/app/controllers/auth/registrations_controller.rb b/app/controllers/auth/registrations_controller.rb index 11436d7c5..efe29b53f 100644 --- a/app/controllers/auth/registrations_controller.rb +++ b/app/controllers/auth/registrations_controller.rb @@ -29,6 +29,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController resource.invite_code = params[:invite_code] if resource.invite_code.blank? resource.agreement = true + resource.current_sign_in_ip = request.remote_ip if resource.current_sign_in_ip.nil? resource.build_account if resource.account.nil? end diff --git a/app/controllers/oauth/authorized_applications_controller.rb b/app/controllers/oauth/authorized_applications_controller.rb index 1e420b3e7..4e45445df 100644 --- a/app/controllers/oauth/authorized_applications_controller.rb +++ b/app/controllers/oauth/authorized_applications_controller.rb @@ -6,6 +6,7 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio before_action :store_current_location before_action :authenticate_resource_owner! before_action :set_pack + before_action :set_body_classes include Localized @@ -16,6 +17,10 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio private + def set_body_classes + @body_classes = 'admin' + end + def store_current_location store_location_for(:user, request.url) end diff --git a/app/controllers/settings/featured_tags_controller.rb b/app/controllers/settings/featured_tags_controller.rb new file mode 100644 index 000000000..3a3241425 --- /dev/null +++ b/app/controllers/settings/featured_tags_controller.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +class Settings::FeaturedTagsController < Settings::BaseController + layout 'admin' + + before_action :authenticate_user! + before_action :set_featured_tags, only: :index + before_action :set_featured_tag, except: [:index, :create] + before_action :set_most_used_tags, only: :index + + def index + @featured_tag = FeaturedTag.new + end + + def create + @featured_tag = current_account.featured_tags.new(featured_tag_params) + @featured_tag.reset_data + + if @featured_tag.save + redirect_to settings_featured_tags_path + else + set_featured_tags + set_most_used_tags + + render :index + end + end + + def destroy + @featured_tag.destroy! + redirect_to settings_featured_tags_path + end + + private + + def set_featured_tag + @featured_tag = current_account.featured_tags.find(params[:id]) + end + + def set_featured_tags + @featured_tags = current_account.featured_tags.order(statuses_count: :desc).reject(&:new_record?) + end + + def set_most_used_tags + @most_used_tags = Tag.most_used(current_account).where.not(id: @featured_tags.map(&:id)).limit(10) + end + + def featured_tag_params + params.require(:featured_tag).permit(:name) + end +end diff --git a/app/controllers/settings/profiles_controller.rb b/app/controllers/settings/profiles_controller.rb index 1a0b73d16..76d599f08 100644 --- a/app/controllers/settings/profiles_controller.rb +++ b/app/controllers/settings/profiles_controller.rb @@ -29,6 +29,6 @@ class Settings::ProfilesController < Settings::BaseController end def set_account - @account = current_user.account + @account = current_account end end diff --git a/app/controllers/settings/sessions_controller.rb b/app/controllers/settings/sessions_controller.rb index 780ea64b4..d74db6000 100644 --- a/app/controllers/settings/sessions_controller.rb +++ b/app/controllers/settings/sessions_controller.rb @@ -2,6 +2,7 @@ # Intentionally does not inherit from BaseController class Settings::SessionsController < ApplicationController + before_action :authenticate_user! before_action :set_session, only: :destroy def destroy diff --git a/app/helpers/admin/filter_helper.rb b/app/helpers/admin/filter_helper.rb index 97beb587f..275b5f2fe 100644 --- a/app/helpers/admin/filter_helper.rb +++ b/app/helpers/admin/filter_helper.rb @@ -6,7 +6,7 @@ module Admin::FilterHelper INVITE_FILTER = %i(available expired).freeze CUSTOM_EMOJI_FILTERS = %i(local remote by_domain shortcode).freeze TAGS_FILTERS = %i(hidden).freeze - INSTANCES_FILTERS = %i(limited).freeze + INSTANCES_FILTERS = %i(limited by_domain).freeze FILTERS = ACCOUNT_FILTERS + REPORT_FILTERS + INVITE_FILTER + CUSTOM_EMOJI_FILTERS + TAGS_FILTERS + INSTANCES_FILTERS diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb index 0e957e946..e868b45c0 100644 --- a/app/helpers/settings_helper.rb +++ b/app/helpers/settings_helper.rb @@ -4,7 +4,7 @@ module SettingsHelper HUMAN_LOCALES = { en: 'English', ar: 'العربية', - ast: 'l\'asturianu', + ast: 'Asturianu', bg: 'Български', ca: 'Català', co: 'Corsu', @@ -30,23 +30,26 @@ module SettingsHelper ja: '日本語', ka: 'ქართული', ko: '한국어', + lv: 'Latviešu', ml: 'മലയാളം', + ms: 'Bahasa Melayu', nl: 'Nederlands', no: 'Norsk', oc: 'Occitan', - pl: 'Polszczyzna', + pl: 'Polski', pt: 'Português', 'pt-BR': 'Português do Brasil', - ro: 'Limba română', + ro: 'Română', ru: 'Русский', sk: 'Slovenčina', sl: 'Slovenščina', + sq: 'Shqip', sr: 'Српски', 'sr-Latn': 'Srpski (latinica)', sv: 'Svenska', ta: 'தமிழ்', te: 'తెలుగు', - th: 'ภาษาไทย', + th: 'ไทย', tr: 'Türkçe', uk: 'Українська', zh: '中文', diff --git a/app/helpers/stream_entries_helper.rb b/app/helpers/stream_entries_helper.rb index 792a91162..e2a303a77 100644 --- a/app/helpers/stream_entries_helper.rb +++ b/app/helpers/stream_entries_helper.rb @@ -176,7 +176,7 @@ module StreamEntriesHelper when 'public' fa_icon 'globe fw' when 'unlisted' - fa_icon 'unlock-alt fw' + fa_icon 'unlock fw' when 'private' fa_icon 'lock fw' when 'direct' diff --git a/app/javascript/core/theme.yml b/app/javascript/core/theme.yml index f48ab40c0..32787bcab 100644 --- a/app/javascript/core/theme.yml +++ b/app/javascript/core/theme.yml @@ -13,7 +13,7 @@ pack: mailer: filename: mailer.js stylesheet: true - modal: + modal: public.js public: public.js settings: settings.js share: diff --git a/app/javascript/flavours/glitch/actions/alerts.js b/app/javascript/flavours/glitch/actions/alerts.js index f37fdeeb6..50cd48a9e 100644 --- a/app/javascript/flavours/glitch/actions/alerts.js +++ b/app/javascript/flavours/glitch/actions/alerts.js @@ -1,3 +1,10 @@ +import { defineMessages } from 'react-intl'; + +const messages = defineMessages({ + unexpectedTitle: { id: 'alert.unexpected.title', defaultMessage: 'Oops!' }, + unexpectedMessage: { id: 'alert.unexpected.message', defaultMessage: 'An unexpected error occurred.' }, +}); + export const ALERT_SHOW = 'ALERT_SHOW'; export const ALERT_DISMISS = 'ALERT_DISMISS'; export const ALERT_CLEAR = 'ALERT_CLEAR'; @@ -15,10 +22,28 @@ export function clearAlert() { }; }; -export function showAlert(title, message) { +export function showAlert(title = messages.unexpectedTitle, message = messages.unexpectedMessage) { return { type: ALERT_SHOW, title, message, }; }; + +export function showAlertForError(error) { + if (error.response) { + const { data, status, statusText } = error.response; + + let message = statusText; + let title = `${status}`; + + if (data.error) { + message = data.error; + } + + return showAlert(title, message); + } else { + console.error(error); + return showAlert(); + } +} diff --git a/app/javascript/flavours/glitch/actions/compose.js b/app/javascript/flavours/glitch/actions/compose.js index 71d3fb1b2..0dd1766bc 100644 --- a/app/javascript/flavours/glitch/actions/compose.js +++ b/app/javascript/flavours/glitch/actions/compose.js @@ -1,5 +1,5 @@ import api from 'flavours/glitch/util/api'; -import { CancelToken } from 'axios'; +import { CancelToken, isCancel } from 'axios'; import { throttle } from 'lodash'; import { search as emojiSearch } from 'flavours/glitch/util/emoji/emoji_mart_search_light'; import { useEmoji } from './emojis'; @@ -8,6 +8,9 @@ import { recoverHashtags } from 'flavours/glitch/util/hashtag'; import resizeImage from 'flavours/glitch/util/resize_image'; import { updateTimeline } from './timelines'; +import { showAlertForError } from './alerts'; +import { showAlert } from './alerts'; +import { defineMessages } from 'react-intl'; let cancelFetchComposeSuggestionsAccounts; @@ -52,6 +55,10 @@ export const COMPOSE_UPLOAD_CHANGE_FAIL = 'COMPOSE_UPLOAD_UPDATE_FAIL'; export const COMPOSE_DOODLE_SET = 'COMPOSE_DOODLE_SET'; +const messages = defineMessages({ + uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' }, +}); + export function changeCompose(text) { return { type: COMPOSE_CHANGE, @@ -134,7 +141,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')), - sensitive: getState().getIn(['compose', 'sensitive']) || spoilerText.length > 0, + sensitive: getState().getIn(['compose', 'sensitive']) || (spoilerText.length > 0 && media.size !== 0), spoiler_text: spoilerText, visibility: getState().getIn(['compose', 'privacy']), }, { @@ -207,20 +214,32 @@ export function doodleSet(options) { export function uploadCompose(files) { return function (dispatch, getState) { - if (getState().getIn(['compose', 'media_attachments']).size > 3) { + const uploadLimit = 4; + const media = getState().getIn(['compose', 'media_attachments']); + const total = Array.from(files).reduce((a, v) => a + v.size, 0); + const progress = new Array(files.length).fill(0); + + if (files.length + media.size > uploadLimit) { + dispatch(showAlert(undefined, messages.uploadErrorLimit)); return; } - dispatch(uploadComposeRequest()); - resizeImage(files[0]).then(file => { - const data = new FormData(); - data.append('file', file); + for (const [i, f] of Array.from(files).entries()) { + if (media.size + i > 3) break; - return api(getState).post('/api/v1/media', data, { - onUploadProgress: ({ loaded, total }) => dispatch(uploadComposeProgress(loaded, total)), - }).then(({ data }) => dispatch(uploadComposeSuccess(data))); - }).catch(error => dispatch(uploadComposeFail(error))); + resizeImage(f).then(file => { + const data = new FormData(); + data.append('file', file); + + return api(getState).post('/api/v1/media', data, { + onUploadProgress: function({ loaded }){ + progress[i] = loaded; + dispatch(uploadComposeProgress(progress.reduce((a, v) => a + v, 0), total)); + }, + }).then(({ data }) => dispatch(uploadComposeSuccess(data))); + }).catch(error => dispatch(uploadComposeFail(error))); + }; }; }; @@ -320,6 +339,10 @@ const fetchComposeSuggestionsAccounts = throttle((dispatch, getState, token) => }, }).then(response => { dispatch(readyComposeSuggestionsAccounts(token, response.data)); + }).catch(error => { + if (!isCancel(error)) { + dispatch(showAlertForError(error)); + } }); }, 200, { leading: true, trailing: true }); diff --git a/app/javascript/flavours/glitch/actions/lists.js b/app/javascript/flavours/glitch/actions/lists.js index 7d94ee950..f29ca1e01 100644 --- a/app/javascript/flavours/glitch/actions/lists.js +++ b/app/javascript/flavours/glitch/actions/lists.js @@ -1,4 +1,5 @@ import api from 'flavours/glitch/util/api'; +import { showAlertForError } from './alerts'; export const LIST_FETCH_REQUEST = 'LIST_FETCH_REQUEST'; export const LIST_FETCH_SUCCESS = 'LIST_FETCH_SUCCESS'; @@ -239,7 +240,8 @@ export const fetchListSuggestions = q => (dispatch, getState) => { }; api(getState).get('/api/v1/accounts/search', { params }) - .then(({ data }) => dispatch(fetchListSuggestionsReady(q, data))); + .then(({ data }) => dispatch(fetchListSuggestionsReady(q, data))) + .catch(error => dispatch(showAlertForError(error))); }; export const fetchListSuggestionsReady = (query, accounts) => ({ diff --git a/app/javascript/flavours/glitch/actions/push_notifications/registerer.js b/app/javascript/flavours/glitch/actions/push_notifications/registerer.js index 91f442415..8fdb239f7 100644 --- a/app/javascript/flavours/glitch/actions/push_notifications/registerer.js +++ b/app/javascript/flavours/glitch/actions/push_notifications/registerer.js @@ -109,14 +109,11 @@ export function register () { pushNotificationsSetting.remove(me); } - try { - getRegistration() - .then(getPushSubscription) - .then(unsubscribe); - } catch (e) { - - } - }); + return getRegistration() + .then(getPushSubscription) + .then(unsubscribe); + }) + .catch(console.warn); } else { console.warn('Your browser does not support Web Push Notifications.'); } @@ -137,6 +134,6 @@ export function saveSettings() { if (me) { pushNotificationsSetting.set(me, data); } - }); + }).catch(console.warn); }; } diff --git a/app/javascript/flavours/glitch/actions/settings.js b/app/javascript/flavours/glitch/actions/settings.js index 87b2ae76d..fb0bcc09c 100644 --- a/app/javascript/flavours/glitch/actions/settings.js +++ b/app/javascript/flavours/glitch/actions/settings.js @@ -1,5 +1,6 @@ import api from 'flavours/glitch/util/api'; import { debounce } from 'lodash'; +import { showAlertForError } from './alerts'; export const SETTING_CHANGE = 'SETTING_CHANGE'; export const SETTING_SAVE = 'SETTING_SAVE'; @@ -23,7 +24,9 @@ const debouncedSave = debounce((dispatch, getState) => { const data = getState().get('settings').filter((_, path) => path !== 'saved').toJS(); - api(getState).put('/api/web/settings', { data }).then(() => dispatch({ type: SETTING_SAVE })); + api(getState).put('/api/web/settings', { data }) + .then(() => dispatch({ type: SETTING_SAVE })) + .catch(error => dispatch(showAlertForError(error))); }, 5000, { trailing: true }); export function saveSettings() { diff --git a/app/javascript/flavours/glitch/components/account.js b/app/javascript/flavours/glitch/components/account.js index 072c601e0..4fcafc509 100644 --- a/app/javascript/flavours/glitch/components/account.js +++ b/app/javascript/flavours/glitch/components/account.js @@ -85,7 +85,7 @@ export default class Account extends ImmutablePureComponent { if (requested) { buttons = <IconButton disabled icon='hourglass' title={intl.formatMessage(messages.requested)} />; } else if (blocking) { - buttons = <IconButton active icon='unlock-alt' title={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.handleBlock} />; + buttons = <IconButton active icon='unlock' title={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.handleBlock} />; } else if (muting) { let hidingNotificationsButton; if (account.getIn(['relationship', 'muting_notifications'])) { diff --git a/app/javascript/flavours/glitch/components/domain.js b/app/javascript/flavours/glitch/components/domain.js index f657cb8d2..74174f83d 100644 --- a/app/javascript/flavours/glitch/components/domain.js +++ b/app/javascript/flavours/glitch/components/domain.js @@ -32,7 +32,7 @@ export default class Account extends ImmutablePureComponent { </span> <div className='domain__buttons'> - <IconButton active icon='unlock-alt' title={intl.formatMessage(messages.unblockDomain, { domain })} onClick={this.handleDomainUnblock} /> + <IconButton active icon='unlock' title={intl.formatMessage(messages.unblockDomain, { domain })} onClick={this.handleDomainUnblock} /> </div> </div> </div> diff --git a/app/javascript/flavours/glitch/components/intersection_observer_article.js b/app/javascript/flavours/glitch/components/intersection_observer_article.js index 6eeca5598..900c98638 100644 --- a/app/javascript/flavours/glitch/components/intersection_observer_article.js +++ b/app/javascript/flavours/glitch/components/intersection_observer_article.js @@ -63,7 +63,7 @@ export default class IntersectionObserverArticle extends ImmutablePureComponent } updateStateAfterIntersection = (prevState) => { - if (prevState.isIntersecting && !this.entry.isIntersecting) { + if (prevState.isIntersecting !== false && !this.entry.isIntersecting) { scheduleIdleTask(this.hideIfNotIntersecting); } return { @@ -103,24 +103,23 @@ export default class IntersectionObserverArticle extends ImmutablePureComponent const { children, id, index, listLength, cachedHeight } = this.props; const { isIntersecting, isHidden } = this.state; + const style = {}; + if (!isIntersecting && (isHidden || cachedHeight)) { - return ( - <article - ref={this.handleRef} - aria-posinset={index + 1} - aria-setsize={listLength} - style={{ height: `${this.height || cachedHeight}px`, opacity: 0, overflow: 'hidden' }} - data-id={id} - tabIndex='0' - > - {children && React.cloneElement(children, { hidden: true })} - </article> - ); + style.height = `${this.height || cachedHeight || 150}px`; + style.opacity = 0; + style.overflow = 'hidden'; } return ( - <article ref={this.handleRef} aria-posinset={index + 1} aria-setsize={listLength} data-id={id} tabIndex='0'> - {children && React.cloneElement(children, { hidden: false })} + <article + ref={this.handleRef} + aria-posinset={index + 1} + aria-setsize={listLength} + data-id={id} + tabIndex='0' + style={style}> + {children && React.cloneElement(children, { hidden: !isIntersecting && (isHidden || cachedHeight) })} </article> ); } diff --git a/app/javascript/flavours/glitch/components/media_gallery.js b/app/javascript/flavours/glitch/components/media_gallery.js index d0226bbbb..1fa25ee4e 100644 --- a/app/javascript/flavours/glitch/components/media_gallery.js +++ b/app/javascript/flavours/glitch/components/media_gallery.js @@ -224,6 +224,8 @@ export default class MediaGallery extends React.PureComponent { size: PropTypes.object, onOpenMedia: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, + defaultWidth: PropTypes.number, + cacheWidth: PropTypes.func, }; static defaultProps = { @@ -232,6 +234,7 @@ export default class MediaGallery extends React.PureComponent { state = { visible: this.props.revealed === undefined ? (displayMedia !== 'hide_all' && !this.props.sensitive || displayMedia === 'show_all') : this.props.revealed, + width: this.props.defaultWidth, }; componentWillReceiveProps (nextProps) { @@ -259,6 +262,7 @@ export default class MediaGallery extends React.PureComponent { handleRef = (node) => { this.node = node; if (node && node.offsetWidth && node.offsetWidth != this.state.width) { + if (this.props.cacheWidth) this.props.cacheWidth(node.offsetWidth); this.setState({ width: node.offsetWidth, }); @@ -271,10 +275,12 @@ export default class MediaGallery extends React.PureComponent { } render () { - const { media, intl, sensitive, letterbox, fullwidth } = this.props; - const { width, visible } = this.state; + const { media, intl, sensitive, letterbox, fullwidth, defaultWidth } = this.props; + const { visible } = this.state; const size = media.take(4).size; + const width = this.state.width || defaultWidth; + let children; const style = {}; diff --git a/app/javascript/flavours/glitch/components/scrollable_list.js b/app/javascript/flavours/glitch/components/scrollable_list.js index 7cd0774a9..462185bbc 100644 --- a/app/javascript/flavours/glitch/components/scrollable_list.js +++ b/app/javascript/flavours/glitch/components/scrollable_list.js @@ -40,6 +40,7 @@ export default class ScrollableList extends PureComponent { state = { fullscreen: null, + cachedMediaWidth: 300, }; intersectionObserverWrapper = new IntersectionObserverWrapper(); @@ -128,7 +129,7 @@ export default class ScrollableList extends PureComponent { } getScrollPosition = () => { - if (this.node && this.node.scrollTop > 0) { + if (this.node && (this.node.scrollTop > 0 || this.mouseMovedRecently)) { return {height: this.node.scrollHeight, top: this.node.scrollTop}; } else { return null; @@ -141,6 +142,10 @@ export default class ScrollableList extends PureComponent { this.setScrollTop(newScrollTop); } + cacheMediaWidth = (width) => { + if (width && this.state.cachedMediaWidth != width) this.setState({ cachedMediaWidth: width }); + } + getSnapshotBeforeUpdate (prevProps, prevState) { const someItemInserted = React.Children.count(prevProps.children) > 0 && React.Children.count(prevProps.children) < React.Children.count(this.props.children) && @@ -252,7 +257,12 @@ export default class ScrollableList extends PureComponent { intersectionObserverWrapper={this.intersectionObserverWrapper} saveHeightKey={trackScroll ? `${this.context.router.route.location.key}:${scrollKey}` : null} > - {React.cloneElement(child, {getScrollPosition: this.getScrollPosition, updateScrollBottom: this.updateScrollBottom})} + {React.cloneElement(child, { + getScrollPosition: this.getScrollPosition, + updateScrollBottom: this.updateScrollBottom, + cachedMediaWidth: this.state.cachedMediaWidth, + cacheMediaWidth: this.cacheMediaWidth, + })} </IntersectionObserverArticleContainer> ))} diff --git a/app/javascript/flavours/glitch/components/status.js b/app/javascript/flavours/glitch/components/status.js index 9ff53485e..349f9c6cc 100644 --- a/app/javascript/flavours/glitch/components/status.js +++ b/app/javascript/flavours/glitch/components/status.js @@ -72,6 +72,8 @@ export default class Status extends ImmutablePureComponent { updateScrollBottom: PropTypes.func, expanded: PropTypes.bool, intl: PropTypes.object.isRequired, + cacheMediaWidth: PropTypes.func, + cachedMediaWidth: PropTypes.number, }; state = { @@ -214,6 +216,8 @@ export default class Status extends ImmutablePureComponent { // Hack to fix timeline jumps on second rendering when auto-collapsing this.setState({ autoCollapsed: true }); } + + this.didShowCard = !this.props.muted && !this.props.hidden && this.props.status && this.props.status.get('card') && this.props.settings.get('inline_preview_cards'); } getSnapshotBeforeUpdate (prevProps, prevState) { @@ -226,8 +230,10 @@ export default class Status extends ImmutablePureComponent { // Hack to fix timeline jumps on second rendering when auto-collapsing componentDidUpdate (prevProps, prevState, snapshot) { - if (this.state.autoCollapsed) { - this.setState({ autoCollapsed: false }); + const doShowCard = !this.props.muted && !this.props.hidden && this.props.status && this.props.status.get('card') && this.props.settings.get('inline_preview_cards'); + if (this.state.autoCollapsed || (doShowCard && !this.didShowCard)) { + if (doShowCard) this.didShowCard = true; + if (this.state.autoCollapsed) this.setState({ autoCollapsed: false }); if (snapshot !== null && this.props.updateScrollBottom) { if (this.node.offsetTop < snapshot.top) { this.props.updateScrollBottom(snapshot.height - snapshot.top); @@ -236,6 +242,15 @@ export default class Status extends ImmutablePureComponent { } } + componentWillUnmount() { + if (this.node && this.props.getScrollPosition) { + const position = this.props.getScrollPosition(); + if (position !== null && this.node.offsetTop < position.top) { + requestAnimationFrame(() => { this.props.updateScrollBottom(position.height - position.top); }); + } + } + } + // `setCollapsed()` sets the value of `isCollapsed` in our state, that is, // whether the toot is collapsed or not. @@ -384,15 +399,7 @@ export default class Status extends ImmutablePureComponent { if (hidden) { return ( - <div - ref={this.handleRef} - data-id={status.get('id')} - style={{ - height: `${this.height}px`, - opacity: 0, - overflow: 'hidden', - }} - > + <div ref={this.handleRef}> {status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])} {' '} {status.get('content')} @@ -408,7 +415,7 @@ export default class Status extends ImmutablePureComponent { return ( <HotKeys handlers={minHandlers}> - <div className='status__wrapper status__wrapper--filtered focusable' tabIndex='0'> + <div className='status__wrapper status__wrapper--filtered focusable' tabIndex='0' ref={this.handleRef}> <FormattedMessage id='status.filtered' defaultMessage='Filtered' /> </div> </HotKeys> @@ -453,6 +460,8 @@ export default class Status extends ImmutablePureComponent { fullwidth={settings.getIn(['media', 'fullwidth'])} preventPlayback={isCollapsed || !isExpanded} onOpenVideo={this.handleOpenVideo} + width={this.props.cachedMediaWidth} + cacheWidth={this.props.cacheMediaWidth} />)} </Bundle> ); @@ -468,6 +477,8 @@ export default class Status extends ImmutablePureComponent { fullwidth={settings.getIn(['media', 'fullwidth'])} hidden={isCollapsed || !isExpanded} onOpenMedia={this.props.onOpenMedia} + cacheWidth={this.props.cacheMediaWidth} + defaultWidth={this.props.cachedMediaWidth} /> )} </Bundle> @@ -484,6 +495,8 @@ export default class Status extends ImmutablePureComponent { onOpenMedia={this.props.onOpenMedia} card={status.get('card')} compact + cacheWidth={this.props.cacheMediaWidth} + defaultWidth={this.props.cachedMediaWidth} /> ); mediaIcon = 'link'; diff --git a/app/javascript/flavours/glitch/components/status_action_bar.js b/app/javascript/flavours/glitch/components/status_action_bar.js index 16abcab4e..1d3130604 100644 --- a/app/javascript/flavours/glitch/components/status_action_bar.js +++ b/app/javascript/flavours/glitch/components/status_action_bar.js @@ -34,6 +34,7 @@ const messages = defineMessages({ embed: { id: 'status.embed', defaultMessage: 'Embed' }, admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' }, admin_status: { id: 'status.admin_status', defaultMessage: 'Open this status in the moderation interface' }, + copy: { id: 'status.copy', defaultMessage: 'Copy link to status' }, }); const obfuscatedCount = count => { @@ -82,7 +83,11 @@ export default class StatusActionBar extends ImmutablePureComponent { ] handleReplyClick = () => { - this.props.onReply(this.props.status, this.context.router.history); + if (me) { + this.props.onReply(this.props.status, this.context.router.history); + } else { + this._openInteractionDialog('reply'); + } } handleShareClick = () => { @@ -93,17 +98,29 @@ export default class StatusActionBar extends ImmutablePureComponent { } handleFavouriteClick = (e) => { - this.props.onFavourite(this.props.status, e); + if (me) { + this.props.onFavourite(this.props.status, e); + } else { + this._openInteractionDialog('favourite'); + } } handleBookmarkClick = (e) => { this.props.onBookmark(this.props.status, e); } - handleReblogClick = (e) => { - this.props.onReblog(this.props.status, e); + handleReblogClick = e => { + if (me) { + this.props.onReblog(this.props.status, e); + } else { + this._openInteractionDialog('reblog'); + } } + _openInteractionDialog = type => { + window.open(`/interact/${this.props.status.get('id')}?type=${type}`, 'mastodon-intent', 'width=445,height=600,resizable=no,menubar=no,status=no,scrollbars=yes'); + } + handleDeleteClick = () => { this.props.onDelete(this.props.status, this.context.router.history); } @@ -148,13 +165,32 @@ export default class StatusActionBar extends ImmutablePureComponent { this.props.onMuteConversation(this.props.status); } + handleCopy = () => { + const url = this.props.status.get('url'); + const textarea = document.createElement('textarea'); + + textarea.textContent = url; + textarea.style.position = 'fixed'; + + document.body.appendChild(textarea); + + try { + textarea.select(); + document.execCommand('copy'); + } catch (e) { + + } finally { + document.body.removeChild(textarea); + } + } + render () { const { status, intl, withDismiss, showReplyCount } = this.props; const mutingConversation = status.get('muted'); const anonymousAccess = !me; const publicStatus = ['public', 'unlisted'].includes(status.get('visibility')); - const reblogDisabled = anonymousAccess || (status.get('visibility') === 'direct' || (status.get('visibility') === 'private' && me !== status.getIn(['account', 'id']))); + const reblogDisabled = status.get('visibility') === 'direct' || (status.get('visibility') === 'private' && me !== status.getIn(['account', 'id'])); const reblogMessage = status.get('visibility') === 'private' ? messages.reblog_private : messages.reblog; let menu = []; @@ -165,6 +201,7 @@ export default class StatusActionBar extends ImmutablePureComponent { menu.push({ text: intl.formatMessage(messages.open), action: this.handleOpen }); if (publicStatus) { + menu.push({ text: intl.formatMessage(messages.copy), action: this.handleCopy }); menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed }); } @@ -189,6 +226,7 @@ export default class StatusActionBar extends ImmutablePureComponent { menu.push({ text: intl.formatMessage(messages.mute, { name: status.getIn(['account', 'username']) }), action: this.handleMuteClick }); menu.push({ text: intl.formatMessage(messages.block, { name: status.getIn(['account', 'username']) }), action: this.handleBlockClick }); menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport }); + if (isStaff && (accountAdminLink || statusAdminLink)) { menu.push(null); if (accountAdminLink !== undefined) { @@ -221,7 +259,6 @@ export default class StatusActionBar extends ImmutablePureComponent { let replyButton = ( <IconButton className='status__action-bar-button' - disabled={anonymousAccess} title={replyTitle} icon={replyIcon} onClick={this.handleReplyClick} @@ -240,7 +277,7 @@ export default class StatusActionBar extends ImmutablePureComponent { <div className='status__action-bar'> {replyButton} <IconButton className='status__action-bar-button' disabled={reblogDisabled} active={status.get('reblogged')} pressed={status.get('reblogged')} title={reblogDisabled ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(reblogMessage)} icon={reblogIcon} onClick={this.handleReblogClick} /> - <IconButton className='status__action-bar-button star-icon' disabled={anonymousAccess} animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} /> + <IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} /> {shareButton} <IconButton className='status__action-bar-button bookmark-icon' disabled={anonymousAccess} active={status.get('bookmarked')} pressed={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} /> diff --git a/app/javascript/flavours/glitch/components/status_content.js b/app/javascript/flavours/glitch/components/status_content.js index 6542df65b..c60d63f9a 100644 --- a/app/javascript/flavours/glitch/components/status_content.js +++ b/app/javascript/flavours/glitch/components/status_content.js @@ -17,6 +17,7 @@ export default class StatusContent extends React.PureComponent { mediaIcon: PropTypes.string, parseClick: PropTypes.func, disabled: PropTypes.bool, + onUpdate: PropTypes.func, }; state = { @@ -62,6 +63,7 @@ export default class StatusContent extends React.PureComponent { componentDidUpdate () { this._updateStatusLinks(); + if (this.props.onUpdate) this.props.onUpdate(); } onLinkClick = (e) => { diff --git a/app/javascript/flavours/glitch/components/status_header.js b/app/javascript/flavours/glitch/components/status_header.js index 65458e3f0..f9321904c 100644 --- a/app/javascript/flavours/glitch/components/status_header.js +++ b/app/javascript/flavours/glitch/components/status_header.js @@ -19,7 +19,7 @@ export default class StatusHeader extends React.PureComponent { // Handles clicks on account name/image handleAccountClick = (e) => { const { status, parseClick } = this.props; - parseClick(e, `/accounts/${+status.getIn(['account', 'id'])}`); + parseClick(e, `/accounts/${status.getIn(['account', 'id'])}`); } // Rendering. diff --git a/app/javascript/flavours/glitch/components/status_prepend.js b/app/javascript/flavours/glitch/components/status_prepend.js index f4ef83135..4e329f546 100644 --- a/app/javascript/flavours/glitch/components/status_prepend.js +++ b/app/javascript/flavours/glitch/components/status_prepend.js @@ -15,7 +15,7 @@ export default class StatusPrepend extends React.PureComponent { handleClick = (e) => { const { account, parseClick } = this.props; - parseClick(e, `/accounts/${+account.get('id')}`); + parseClick(e, `/accounts/${account.get('id')}`); } Message = () => { diff --git a/app/javascript/flavours/glitch/components/status_visibility_icon.js b/app/javascript/flavours/glitch/components/status_visibility_icon.js index 017b69cbb..5e7b8ed00 100644 --- a/app/javascript/flavours/glitch/components/status_visibility_icon.js +++ b/app/javascript/flavours/glitch/components/status_visibility_icon.js @@ -25,7 +25,7 @@ export default class VisibilityIcon extends ImmutablePureComponent { const visibilityClass = { public: 'globe', - unlisted: 'unlock-alt', + unlisted: 'unlock', private: 'lock', direct: 'envelope', }[visibility]; diff --git a/app/javascript/flavours/glitch/containers/compose_container.js b/app/javascript/flavours/glitch/containers/compose_container.js index 60f6a9c9f..74c411b7c 100644 --- a/app/javascript/flavours/glitch/containers/compose_container.js +++ b/app/javascript/flavours/glitch/containers/compose_container.js @@ -7,6 +7,7 @@ import { IntlProvider, addLocaleData } from 'react-intl'; import { getLocale } from 'mastodon/locales'; import Compose from 'flavours/glitch/features/standalone/compose'; import initialState from 'flavours/glitch/util/initial_state'; +import { fetchCustomEmojis } from 'flavours/glitch/actions/custom_emojis'; const { localeData, messages } = getLocale(); addLocaleData(localeData); @@ -17,6 +18,8 @@ if (initialState) { store.dispatch(hydrateStore(initialState)); } +store.dispatch(fetchCustomEmojis()); + export default class TimelineContainer extends React.PureComponent { static propTypes = { diff --git a/app/javascript/flavours/glitch/containers/status_container.js b/app/javascript/flavours/glitch/containers/status_container.js index f28dce609..f783878b0 100644 --- a/app/javascript/flavours/glitch/containers/status_container.js +++ b/app/javascript/flavours/glitch/containers/status_container.js @@ -25,6 +25,7 @@ import { openModal } from 'flavours/glitch/actions/modal'; import { changeLocalSetting } from 'flavours/glitch/actions/local_settings'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { boostModal, favouriteModal, deleteModal } from 'flavours/glitch/util/initial_state'; +import { showAlertForError } from '../actions/alerts'; const messages = defineMessages({ deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' }, @@ -134,7 +135,10 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ }, onEmbed (status) { - dispatch(openModal('EMBED', { url: status.get('url') })); + dispatch(openModal('EMBED', { + url: status.get('url'), + onError: error => dispatch(showAlertForError(error)), + })); }, onDelete (status, history, withRedraft = false) { diff --git a/app/javascript/flavours/glitch/features/account/components/header.js b/app/javascript/flavours/glitch/features/account/components/header.js index dc5b1447b..96696c2a5 100644 --- a/app/javascript/flavours/glitch/features/account/components/header.js +++ b/app/javascript/flavours/glitch/features/account/components/header.js @@ -88,7 +88,7 @@ export default class Header extends ImmutablePureComponent { } else if (account.getIn(['relationship', 'blocking'])) { actionBtn = ( <div className='account--action-button'> - <IconButton size={26} icon='unlock-alt' title={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.props.onBlock} /> + <IconButton size={26} icon='unlock' title={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.props.onBlock} /> </div> ); } diff --git a/app/javascript/flavours/glitch/features/composer/options/index.js b/app/javascript/flavours/glitch/features/composer/options/index.js index 05cbe24c9..5b4a7444c 100644 --- a/app/javascript/flavours/glitch/features/composer/options/index.js +++ b/app/javascript/flavours/glitch/features/composer/options/index.js @@ -197,7 +197,7 @@ export default class ComposerOptions extends React.PureComponent { text: <FormattedMessage {...messages.public_short} />, }, unlisted: { - icon: 'unlock-alt', + icon: 'unlock', meta: <FormattedMessage {...messages.unlisted_long} />, name: 'unlisted', text: <FormattedMessage {...messages.unlisted_short} />, @@ -214,6 +214,7 @@ export default class ComposerOptions extends React.PureComponent { onChange={handleChangeFiles} ref={handleRefFileElement} type='file' + multiple {...hiddenComponent} /> <Dropdown diff --git a/app/javascript/flavours/glitch/features/composer/publisher/index.js b/app/javascript/flavours/glitch/features/composer/publisher/index.js index 5ded26f80..dc9c8f8eb 100644 --- a/app/javascript/flavours/glitch/features/composer/publisher/index.js +++ b/app/javascript/flavours/glitch/features/composer/publisher/index.js @@ -58,7 +58,7 @@ export default function ComposerPublisher ({ <Icon icon={{ public: 'globe', - unlisted: 'unlock-alt', + unlisted: 'unlock', private: 'lock', direct: 'envelope', }[sideArm]} @@ -82,7 +82,7 @@ export default function ComposerPublisher ({ direct: 'envelope', private: 'lock', public: 'globe', - unlisted: 'unlock-alt', + unlisted: 'unlock', }[privacy]} /> {' '} diff --git a/app/javascript/flavours/glitch/features/composer/spoiler/index.js b/app/javascript/flavours/glitch/features/composer/spoiler/index.js index 1c3c962f0..e2f9c7021 100644 --- a/app/javascript/flavours/glitch/features/composer/spoiler/index.js +++ b/app/javascript/flavours/glitch/features/composer/spoiler/index.js @@ -87,6 +87,7 @@ export default class ComposerSpoiler extends React.PureComponent { type='text' value={text} ref={handleRefSpoilerText} + disabled={hidden} /> </label> </div> diff --git a/app/javascript/flavours/glitch/features/composer/upload_form/item/index.js b/app/javascript/flavours/glitch/features/composer/upload_form/item/index.js index 93fa4e39e..4f5f66f04 100644 --- a/app/javascript/flavours/glitch/features/composer/upload_form/item/index.js +++ b/app/javascript/flavours/glitch/features/composer/upload_form/item/index.js @@ -169,13 +169,12 @@ export default class ComposerUploadFormItem extends React.PureComponent { </div> <label> <span style={{ display: 'none' }}><FormattedMessage {...messages.description} /></span> - <input + <textarea maxLength={420} onBlur={handleBlur} onChange={handleChange} onFocus={handleFocus} placeholder={intl.formatMessage(messages.description)} - type='text' value={description} /> </label> diff --git a/app/javascript/flavours/glitch/features/getting_started/index.js b/app/javascript/flavours/glitch/features/getting_started/index.js index 126350813..d0c72c087 100644 --- a/app/javascript/flavours/glitch/features/getting_started/index.js +++ b/app/javascript/flavours/glitch/features/getting_started/index.js @@ -166,7 +166,7 @@ export default class GettingStarted extends ImmutablePureComponent { <div className='getting-started__footer'> <ul> {invitesEnabled && <li><a href='/invites' target='_blank'><FormattedMessage id='getting_started.invite' defaultMessage='Invite people' /></a> · </li>} - <li><a href='/about/more' target='_blank'><FormattedMessage id='navigation_bar.info' defaultMessage='About this instance' /></a> · </li> + <li><a href='/about/more' target='_blank'><FormattedMessage id='navigation_bar.info' defaultMessage='About this server' /></a> · </li> <li><a href='https://joinmastodon.org/apps' target='_blank'><FormattedMessage id='navigation_bar.apps' defaultMessage='Mobile apps' /></a> · </li> <li><a href='/terms' target='_blank'><FormattedMessage id='getting_started.terms' defaultMessage='Terms of service' /></a> · </li> <li><a href='https://docs.joinmastodon.org' target='_blank'><FormattedMessage id='getting_started.documentation' defaultMessage='Documentation' /></a></li> diff --git a/app/javascript/flavours/glitch/features/hashtag_timeline/components/column_settings.js b/app/javascript/flavours/glitch/features/hashtag_timeline/components/column_settings.js index 82936c838..dc0ffee85 100644 --- a/app/javascript/flavours/glitch/features/hashtag_timeline/components/column_settings.js +++ b/app/javascript/flavours/glitch/features/hashtag_timeline/components/column_settings.js @@ -1,10 +1,15 @@ import React from 'react'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import { injectIntl, FormattedMessage } from 'react-intl'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import Toggle from 'react-toggle'; import AsyncSelect from 'react-select/lib/Async'; +const messages = defineMessages({ + placeholder: { id: 'hashtag.column_settings.select.placeholder', defaultMessage: 'Enter hashtags…' }, + noOptions: { id: 'hashtag.column_settings.select.no_options_message', defaultMessage: 'No suggestions found' }, +}); + @injectIntl export default class ColumnSettings extends React.PureComponent { @@ -25,6 +30,7 @@ export default class ColumnSettings extends React.PureComponent { tags (mode) { let tags = this.props.settings.getIn(['tags', mode]) || []; + if (tags.toJSON) { return tags.toJSON(); } else { @@ -32,33 +38,36 @@ export default class ColumnSettings extends React.PureComponent { } }; - onSelect = (mode) => { - return (value) => { - this.props.onChange(['tags', mode], value); - }; - }; + onSelect = mode => value => this.props.onChange(['tags', mode], value); onToggle = () => { if (this.state.open && this.hasTags()) { this.props.onChange('tags', {}); } + this.setState({ open: !this.state.open }); }; + noOptionsMessage = () => this.props.intl.formatMessage(messages.noOptions); + modeSelect (mode) { return ( - <div className='column-settings__section'> - {this.modeLabel(mode)} + <div className='column-settings__row'> + <span className='column-settings__section'> + {this.modeLabel(mode)} + </span> + <AsyncSelect isMulti autoFocus value={this.tags(mode)} - settings={this.props.settings} - settingPath={['tags', mode]} onChange={this.onSelect(mode)} loadOptions={this.props.onLoad} - classNamePrefix='column-settings__hashtag-select' + className='column-select__container' + classNamePrefix='column-select' name='tags' + placeholder={this.props.intl.formatMessage(messages.placeholder)} + noOptionsMessage={this.noOptionsMessage} /> </div> ); @@ -66,11 +75,15 @@ export default class ColumnSettings extends React.PureComponent { modeLabel (mode) { switch(mode) { - case 'any': return <FormattedMessage id='hashtag.column_settings.tag_mode.any' defaultMessage='Any of these' />; - case 'all': return <FormattedMessage id='hashtag.column_settings.tag_mode.all' defaultMessage='All of these' />; - case 'none': return <FormattedMessage id='hashtag.column_settings.tag_mode.none' defaultMessage='None of these' />; + case 'any': + return <FormattedMessage id='hashtag.column_settings.tag_mode.any' defaultMessage='Any of these' />; + case 'all': + return <FormattedMessage id='hashtag.column_settings.tag_mode.all' defaultMessage='All of these' />; + case 'none': + return <FormattedMessage id='hashtag.column_settings.tag_mode.none' defaultMessage='None of these' />; + default: + return ''; } - return ''; }; render () { @@ -78,23 +91,21 @@ export default class ColumnSettings extends React.PureComponent { <div> <div className='column-settings__row'> <div className='setting-toggle'> - <Toggle - id='hashtag.column_settings.tag_toggle' - onChange={this.onToggle} - checked={this.state.open} - /> + <Toggle id='hashtag.column_settings.tag_toggle' onChange={this.onToggle} checked={this.state.open} /> + <span className='setting-toggle__label'> <FormattedMessage id='hashtag.column_settings.tag_toggle' defaultMessage='Include additional tags in this column' /> </span> </div> </div> - {this.state.open && + + {this.state.open && ( <div className='column-settings__hashtags'> {this.modeSelect('any')} {this.modeSelect('all')} {this.modeSelect('none')} </div> - } + )} </div> ); } diff --git a/app/javascript/flavours/glitch/features/hashtag_timeline/index.js b/app/javascript/flavours/glitch/features/hashtag_timeline/index.js index d04e9cafa..21efaceea 100644 --- a/app/javascript/flavours/glitch/features/hashtag_timeline/index.js +++ b/app/javascript/flavours/glitch/features/hashtag_timeline/index.js @@ -40,15 +40,19 @@ export default class HashtagTimeline extends React.PureComponent { title = () => { let title = [this.props.params.id]; + if (this.additionalFor('any')) { - title.push(' ', <FormattedMessage id='hashtag.column_header.tag_mode.any' values={{ additional: this.additionalFor('any') }} defaultMessage='or {additional}' />); + title.push(' ', <FormattedMessage key='any' id='hashtag.column_header.tag_mode.any' values={{ additional: this.additionalFor('any') }} defaultMessage='or {additional}' />); } + if (this.additionalFor('all')) { - title.push(' ', <FormattedMessage id='hashtag.column_header.tag_mode.all' values={{ additional: this.additionalFor('all') }} defaultMessage='and {additional}' />); + title.push(' ', <FormattedMessage key='all' id='hashtag.column_header.tag_mode.all' values={{ additional: this.additionalFor('all') }} defaultMessage='and {additional}' />); } + if (this.additionalFor('none')) { - title.push(' ', <FormattedMessage id='hashtag.column_header.tag_mode.none' values={{ additional: this.additionalFor('none') }} defaultMessage='without {additional}' />); + title.push(' ', <FormattedMessage key='none' id='hashtag.column_header.tag_mode.none' values={{ additional: this.additionalFor('none') }} defaultMessage='without {additional}' />); } + return title; } @@ -76,9 +80,10 @@ export default class HashtagTimeline extends React.PureComponent { let all = (tags.all || []).map(tag => tag.value); let none = (tags.none || []).map(tag => tag.value); - [id, ...any].map((tag) => { - this.disconnects.push(dispatch(connectHashtagStream(id, tag, (status) => { + [id, ...any].map(tag => { + this.disconnects.push(dispatch(connectHashtagStream(id, tag, status => { let tags = status.tags.map(tag => tag.name); + return all.filter(tag => tags.includes(tag)).length === all.length && none.filter(tag => tags.includes(tag)).length === 0; }))); @@ -94,12 +99,14 @@ export default class HashtagTimeline extends React.PureComponent { const { dispatch } = this.props; const { id, tags } = this.props.params; + this._subscribe(dispatch, id, tags); dispatch(expandHashtagTimeline(id, { tags })); } componentWillReceiveProps (nextProps) { const { dispatch, params } = this.props; const { id, tags } = nextProps.params; + if (id !== params.id || !isEqual(tags, params.tags)) { this._unsubscribe(); this._subscribe(dispatch, id, tags); diff --git a/app/javascript/flavours/glitch/features/local_settings/navigation/index.js b/app/javascript/flavours/glitch/features/local_settings/navigation/index.js index ce10e3f51..c583c4863 100644 --- a/app/javascript/flavours/glitch/features/local_settings/navigation/index.js +++ b/app/javascript/flavours/glitch/features/local_settings/navigation/index.js @@ -11,7 +11,7 @@ import { preferencesLink } from 'flavours/glitch/util/backend_links'; const messages = defineMessages({ general: { id: 'settings.general', defaultMessage: 'General' }, - compose: { id: 'settings.compose_box_opts', defaultMessage: 'Compose box options' }, + compose: { id: 'settings.compose_box_opts', defaultMessage: 'Compose box' }, content_warnings: { id: 'settings.content_warnings', defaultMessage: 'Content Warnings' }, collapsed: { id: 'settings.collapsed_statuses', defaultMessage: 'Collapsed toots' }, media: { id: 'settings.media', defaultMessage: 'Media' }, diff --git a/app/javascript/flavours/glitch/features/local_settings/page/index.js b/app/javascript/flavours/glitch/features/local_settings/page/index.js index 16c64ced6..4535d9849 100644 --- a/app/javascript/flavours/glitch/features/local_settings/page/index.js +++ b/app/javascript/flavours/glitch/features/local_settings/page/index.js @@ -108,7 +108,7 @@ export default class LocalSettingsPage extends React.PureComponent { ), ({ intl, onChange, settings }) => ( <div className='glitch local-settings__page compose_box_opts'> - <h1><FormattedMessage id='settings.compose_box_opts' defaultMessage='Compose box options' /></h1> + <h1><FormattedMessage id='settings.compose_box_opts' defaultMessage='Compose box' /></h1> <LocalSettingsPageItem settings={settings} item={['always_show_spoilers_field']} diff --git a/app/javascript/flavours/glitch/features/notifications/components/notification.js b/app/javascript/flavours/glitch/features/notifications/components/notification.js index 21c55accc..daafe3507 100644 --- a/app/javascript/flavours/glitch/features/notifications/components/notification.js +++ b/app/javascript/flavours/glitch/features/notifications/components/notification.js @@ -18,6 +18,9 @@ export default class Notification extends ImmutablePureComponent { onMention: PropTypes.func.isRequired, getScrollPosition: PropTypes.func, updateScrollBottom: PropTypes.func, + cacheMediaWidth: PropTypes.func, + cachedMediaWidth: PropTypes.number, + onUnmount: PropTypes.func, }; render () { @@ -57,6 +60,9 @@ export default class Notification extends ImmutablePureComponent { contextType='notifications' getScrollPosition={getScrollPosition} updateScrollBottom={updateScrollBottom} + cachedMediaWidth={this.props.cachedMediaWidth} + cacheMediaWidth={this.props.cacheMediaWidth} + onUnmount={this.props.onUnmount} withDismiss /> ); @@ -75,6 +81,9 @@ export default class Notification extends ImmutablePureComponent { onMention={onMention} getScrollPosition={getScrollPosition} updateScrollBottom={updateScrollBottom} + cachedMediaWidth={this.props.cachedMediaWidth} + cacheMediaWidth={this.props.cacheMediaWidth} + onUnmount={this.props.onUnmount} withDismiss /> ); @@ -93,6 +102,9 @@ export default class Notification extends ImmutablePureComponent { onMention={onMention} getScrollPosition={getScrollPosition} updateScrollBottom={updateScrollBottom} + cachedMediaWidth={this.props.cachedMediaWidth} + cacheMediaWidth={this.props.cacheMediaWidth} + onUnmount={this.props.onUnmount} withDismiss /> ); diff --git a/app/javascript/flavours/glitch/features/public_timeline/index.js b/app/javascript/flavours/glitch/features/public_timeline/index.js index 477d3b8c7..7fe472202 100644 --- a/app/javascript/flavours/glitch/features/public_timeline/index.js +++ b/app/javascript/flavours/glitch/features/public_timeline/index.js @@ -127,7 +127,7 @@ export default class PublicTimeline extends React.PureComponent { onLoadMore={this.handleLoadMore} trackScroll={!pinned} scrollKey={`public_timeline-${columnId}`} - emptyMessage={<FormattedMessage id='empty_column.public' defaultMessage='There is nothing here! Write something publicly, or manually follow users from other instances to fill it up' />} + emptyMessage={<FormattedMessage id='empty_column.public' defaultMessage='There is nothing here! Write something publicly, or manually follow users from other servers to fill it up' />} /> </Column> ); diff --git a/app/javascript/flavours/glitch/features/status/components/action_bar.js b/app/javascript/flavours/glitch/features/status/components/action_bar.js index be82bca5b..66cc10d78 100644 --- a/app/javascript/flavours/glitch/features/status/components/action_bar.js +++ b/app/javascript/flavours/glitch/features/status/components/action_bar.js @@ -29,6 +29,7 @@ const messages = defineMessages({ embed: { id: 'status.embed', defaultMessage: 'Embed' }, admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' }, admin_status: { id: 'status.admin_status', defaultMessage: 'Open this status in the moderation interface' }, + copy: { id: 'status.copy', defaultMessage: 'Copy link to status' }, }); @injectIntl @@ -119,6 +120,25 @@ export default class ActionBar extends React.PureComponent { this.props.onEmbed(this.props.status); } + handleCopy = () => { + const url = this.props.status.get('url'); + const textarea = document.createElement('textarea'); + + textarea.textContent = url; + textarea.style.position = 'fixed'; + + document.body.appendChild(textarea); + + try { + textarea.select(); + document.execCommand('copy'); + } catch (e) { + + } finally { + document.body.removeChild(textarea); + } + } + render () { const { status, intl } = this.props; @@ -128,6 +148,7 @@ export default class ActionBar extends React.PureComponent { let menu = []; if (publicStatus) { + menu.push({ text: intl.formatMessage(messages.copy), action: this.handleCopy }); menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed }); menu.push(null); } diff --git a/app/javascript/flavours/glitch/features/status/components/card.js b/app/javascript/flavours/glitch/features/status/components/card.js index 1e1604d5c..e405a5ef0 100644 --- a/app/javascript/flavours/glitch/features/status/components/card.js +++ b/app/javascript/flavours/glitch/features/status/components/card.js @@ -60,6 +60,8 @@ export default class Card extends React.PureComponent { maxDescription: PropTypes.number, onOpenMedia: PropTypes.func.isRequired, compact: PropTypes.bool, + defaultWidth: PropTypes.number, + cacheWidth: PropTypes.func, }; static defaultProps = { @@ -68,7 +70,7 @@ export default class Card extends React.PureComponent { }; state = { - width: 280, + width: this.props.defaultWidth || 280, embedded: false, }; @@ -111,6 +113,7 @@ export default class Card extends React.PureComponent { setRef = c => { if (c) { + if (this.props.cacheWidth) this.props.cacheWidth(c.offsetWidth); this.setState({ width: c.offsetWidth }); } } @@ -133,7 +136,7 @@ export default class Card extends React.PureComponent { } render () { - const { card, maxDescription, compact } = this.props; + const { card, maxDescription, compact, defaultWidth } = this.props; const { width, embedded } = this.state; if (card === null) { diff --git a/app/javascript/flavours/glitch/features/status/components/detailed_status.js b/app/javascript/flavours/glitch/features/status/components/detailed_status.js index 02f02efea..120ae6817 100644 --- a/app/javascript/flavours/glitch/features/status/components/detailed_status.js +++ b/app/javascript/flavours/glitch/features/status/components/detailed_status.js @@ -26,7 +26,7 @@ export default class DetailedStatus extends ImmutablePureComponent { settings: ImmutablePropTypes.map.isRequired, onOpenMedia: PropTypes.func.isRequired, onOpenVideo: PropTypes.func.isRequired, - onToggleHidden: PropTypes.func.isRequired, + onToggleHidden: PropTypes.func, expanded: PropTypes.bool, measureHeight: PropTypes.bool, onHeightChange: PropTypes.func, @@ -79,6 +79,10 @@ export default class DetailedStatus extends ImmutablePureComponent { this._measureHeight(prevState.height !== this.state.height); } + handleChildUpdate = () => { + this._measureHeight(); + } + handleModalLink = e => { e.preventDefault(); @@ -94,7 +98,7 @@ export default class DetailedStatus extends ImmutablePureComponent { } render () { - const status = this.props.status.get('reblog') ? this.props.status.get('reblog') : this.props.status; + const status = (this.props.status && this.props.status.get('reblog')) ? this.props.status.get('reblog') : this.props.status; const { expanded, onToggleHidden, settings } = this.props; const outerStyle = { boxSizing: 'border-box' }; const { compact } = this.props; @@ -218,6 +222,7 @@ export default class DetailedStatus extends ImmutablePureComponent { collapsed={false} onExpandedToggle={onToggleHidden} parseClick={this.parseClick} + onUpdate={this.handleChildUpdate} /> <div className='detailed-status__meta'> diff --git a/app/javascript/flavours/glitch/features/status/containers/detailed_status_container.js b/app/javascript/flavours/glitch/features/status/containers/detailed_status_container.js index e41b1dc88..6d3909ea7 100644 --- a/app/javascript/flavours/glitch/features/status/containers/detailed_status_container.js +++ b/app/javascript/flavours/glitch/features/status/containers/detailed_status_container.js @@ -160,14 +160,6 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ } }, - onToggleHidden (status) { - if (status.get('hidden')) { - dispatch(revealStatus(status.get('id'))); - } else { - dispatch(hideStatus(status.get('id'))); - } - }, - }); export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(DetailedStatus)); diff --git a/app/javascript/flavours/glitch/features/ui/components/doodle_modal.js b/app/javascript/flavours/glitch/features/ui/components/doodle_modal.js index 9c74451b3..72f7f30b9 100644 --- a/app/javascript/flavours/glitch/features/ui/components/doodle_modal.js +++ b/app/javascript/flavours/glitch/features/ui/components/doodle_modal.js @@ -110,7 +110,7 @@ function dataURLtoFile(dataurl, filename) { } return new File([u8arr], filename, { type: mime }); } - +/** Doodle canvas size options */ const DOODLE_SIZES = { normal: [500, 500, 'Square 500'], tootbanner: [702, 330, 'Tootbanner'], @@ -520,7 +520,7 @@ export default class DoodleModal extends ImmutablePureComponent { let newSize = e.target.value; if (newSize === this.oldSize) return; - if (this.undos.length > 1 && !confirm('Change size? This will erase your drawing!')) { + if (this.undos.length > 1 && !confirm('Change canvas size? This will erase your current drawing!')) { return; } @@ -528,7 +528,7 @@ export default class DoodleModal extends ImmutablePureComponent { }; handleClearBtn = () => { - if (this.undos.length > 1 && !confirm('Clear screen? This will erase your drawing!')) { + if (this.undos.length > 1 && !confirm('Clear canvas? This will erase your current drawing!')) { return; } diff --git a/app/javascript/flavours/glitch/features/ui/components/embed_modal.js b/app/javascript/flavours/glitch/features/ui/components/embed_modal.js index bf29b0da5..b1643df1c 100644 --- a/app/javascript/flavours/glitch/features/ui/components/embed_modal.js +++ b/app/javascript/flavours/glitch/features/ui/components/embed_modal.js @@ -10,6 +10,7 @@ export default class EmbedModal extends ImmutablePureComponent { static propTypes = { url: PropTypes.string.isRequired, onClose: PropTypes.func.isRequired, + onError: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, } @@ -35,6 +36,8 @@ export default class EmbedModal extends ImmutablePureComponent { iframeDocument.body.style.margin = 0; this.iframe.width = iframeDocument.body.scrollWidth; this.iframe.height = iframeDocument.body.scrollHeight; + }).catch(error => { + this.props.onError(error); }); } diff --git a/app/javascript/flavours/glitch/features/ui/components/report_modal.js b/app/javascript/flavours/glitch/features/ui/components/report_modal.js index a139394ac..8be1d5856 100644 --- a/app/javascript/flavours/glitch/features/ui/components/report_modal.js +++ b/app/javascript/flavours/glitch/features/ui/components/report_modal.js @@ -97,7 +97,7 @@ export default class ReportModal extends ImmutablePureComponent { <div className='report-modal__container'> <div className='report-modal__comment'> - <p><FormattedMessage id='report.hint' defaultMessage='The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:' /></p> + <p><FormattedMessage id='report.hint' defaultMessage='The report will be sent to your server moderators. You can provide an explanation of why you are reporting this account below:' /></p> <textarea className='setting-text light' diff --git a/app/javascript/flavours/glitch/features/ui/containers/notifications_container.js b/app/javascript/flavours/glitch/features/ui/containers/notifications_container.js index 88d482bcf..283aa2373 100644 --- a/app/javascript/flavours/glitch/features/ui/containers/notifications_container.js +++ b/app/javascript/flavours/glitch/features/ui/containers/notifications_container.js @@ -1,11 +1,22 @@ +import { injectIntl } from 'react-intl'; import { connect } from 'react-redux'; import { NotificationStack } from 'react-notification'; import { dismissAlert } from 'flavours/glitch/actions/alerts'; import { getAlerts } from 'flavours/glitch/selectors'; -const mapStateToProps = state => ({ - notifications: getAlerts(state), -}); +const mapStateToProps = (state, { intl }) => { + const notifications = getAlerts(state); + + notifications.forEach(notification => ['title', 'message'].forEach(key => { + const value = notification[key]; + + if (typeof value === 'object') { + notification[key] = intl.formatMessage(value); + } + })); + + return { notifications }; +}; const mapDispatchToProps = (dispatch) => { return { @@ -15,4 +26,4 @@ const mapDispatchToProps = (dispatch) => { }; }; -export default connect(mapStateToProps, mapDispatchToProps)(NotificationStack); +export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(NotificationStack)); diff --git a/app/javascript/flavours/glitch/features/ui/index.js b/app/javascript/flavours/glitch/features/ui/index.js index 602d93832..a19b3abf1 100644 --- a/app/javascript/flavours/glitch/features/ui/index.js +++ b/app/javascript/flavours/glitch/features/ui/index.js @@ -186,7 +186,7 @@ export default class UI extends React.Component { this.setState({ draggingOver: false }); this.dragTargets = []; - if (e.dataTransfer && e.dataTransfer.files.length === 1) { + if (e.dataTransfer && e.dataTransfer.files.length >= 1) { this.props.dispatch(uploadCompose(e.dataTransfer.files)); } } diff --git a/app/javascript/flavours/glitch/features/video/index.js b/app/javascript/flavours/glitch/features/video/index.js index d8e8791a7..cf66536c4 100644 --- a/app/javascript/flavours/glitch/features/video/index.js +++ b/app/javascript/flavours/glitch/features/video/index.js @@ -103,6 +103,7 @@ export default class Video extends React.PureComponent { inline: PropTypes.bool, preventPlayback: PropTypes.bool, intl: PropTypes.object.isRequired, + cacheWidth: PropTypes.func, }; state = { @@ -111,7 +112,7 @@ export default class Video extends React.PureComponent { volume: 0.5, paused: true, dragging: false, - containerWidth: false, + containerWidth: this.props.width, fullscreen: false, hovered: false, muted: false, @@ -131,6 +132,7 @@ export default class Video extends React.PureComponent { this.player = c; if (c && c.offsetWidth && c.offsetWidth != this.state.containerWidth) { + if (this.props.cacheWidth) this.props.cacheWidth(this.player.offsetWidth); this.setState({ containerWidth: c.offsetWidth, }); @@ -275,6 +277,7 @@ export default class Video extends React.PureComponent { componentDidUpdate (prevProps) { if (this.player && this.player.offsetWidth && this.player.offsetWidth != this.state.containerWidth && !this.state.fullscreen) { + if (this.props.cacheWidth) this.props.cacheWidth(this.player.offsetWidth); this.setState({ containerWidth: this.player.offsetWidth, }); @@ -363,7 +366,6 @@ export default class Video extends React.PureComponent { width = containerWidth; height = containerWidth / (16/9); - playerStyle.width = width; playerStyle.height = height; } else if (inline) { return (<div className={computedClass} ref={this.setPlayerRef} tabindex={0}></div>); diff --git a/app/javascript/flavours/glitch/middleware/errors.js b/app/javascript/flavours/glitch/middleware/errors.js index f3dfc8b06..212c1f4ad 100644 --- a/app/javascript/flavours/glitch/middleware/errors.js +++ b/app/javascript/flavours/glitch/middleware/errors.js @@ -1,4 +1,4 @@ -import { showAlert } from 'flavours/glitch/actions/alerts'; +import { showAlertForError } from 'flavours/glitch/actions/alerts'; const defaultFailSuffix = 'FAIL'; @@ -8,21 +8,7 @@ export default function errorsMiddleware() { const isFail = new RegExp(`${defaultFailSuffix}$`, 'g'); if (action.type.match(isFail)) { - if (action.error.response) { - const { data, status, statusText } = action.error.response; - - let message = statusText; - let title = `${status}`; - - if (data.error) { - message = data.error; - } - - dispatch(showAlert(title, message)); - } else { - console.error(action.error); - dispatch(showAlert('Oops!', 'An unexpected error occurred.')); - } + dispatch(showAlertForError(action.error)); } } diff --git a/app/javascript/flavours/glitch/packs/error.js b/app/javascript/flavours/glitch/packs/error.js new file mode 100644 index 000000000..81c86c3ab --- /dev/null +++ b/app/javascript/flavours/glitch/packs/error.js @@ -0,0 +1,13 @@ +import ready from 'flavours/glitch/util/ready'; + +ready(() => { + const image = document.querySelector('img'); + + image.addEventListener('mouseenter', () => { + image.src = '/oops.gif'; + }); + + image.addEventListener('mouseleave', () => { + image.src = '/oops.png'; + }); +}); diff --git a/app/javascript/flavours/glitch/styles/_mixins.scss b/app/javascript/flavours/glitch/styles/_mixins.scss index c46d7260d..586802185 100644 --- a/app/javascript/flavours/glitch/styles/_mixins.scss +++ b/app/javascript/flavours/glitch/styles/_mixins.scss @@ -82,3 +82,34 @@ font-size: 16px; } } + +@mixin search-popout() { + background: $simple-background-color; + border-radius: 4px; + padding: 10px 14px; + padding-bottom: 14px; + margin-top: 10px; + color: $light-text-color; + box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4); + + h4 { + text-transform: uppercase; + color: $light-text-color; + font-size: 13px; + font-weight: 500; + margin-bottom: 10px; + } + + li { + padding: 4px 0; + } + + ul { + margin-bottom: 10px; + } + + em { + font-weight: 500; + color: $inverted-text-color; + } +} diff --git a/app/javascript/flavours/glitch/styles/about.scss b/app/javascript/flavours/glitch/styles/about.scss index 302de020b..329482458 100644 --- a/app/javascript/flavours/glitch/styles/about.scss +++ b/app/javascript/flavours/glitch/styles/about.scss @@ -49,15 +49,9 @@ $small-breakpoint: 960px; } } + strong, em { - display: inline; - margin: 0; - padding: 0; font-weight: 700; - background: transparent; - font-family: inherit; - font-size: inherit; - line-height: inherit; color: lighten($darker-text-color, 10%); } @@ -798,7 +792,7 @@ $small-breakpoint: 960px; width: 100%; display: flex; flex-direction: row-reverse; - flex-wrap: wrap; + flex-wrap: nowrap; justify-content: space-between; align-items: center; } @@ -848,14 +842,7 @@ $small-breakpoint: 960px; } strong { - display: inline; - margin: 0; - padding: 0; - font-weight: 700; - background: transparent; - font-family: inherit; - font-size: inherit; - line-height: inherit; + font-weight: 500; color: lighten($darker-text-color, 10%); } diff --git a/app/javascript/flavours/glitch/styles/accounts.scss b/app/javascript/flavours/glitch/styles/accounts.scss index 9c6518bea..d2ae83b2e 100644 --- a/app/javascript/flavours/glitch/styles/accounts.scss +++ b/app/javascript/flavours/glitch/styles/accounts.scss @@ -290,3 +290,7 @@ border-bottom: 0; } } + +.directory__tag .trends__item__current { + width: auto; +} diff --git a/app/javascript/flavours/glitch/styles/admin.scss b/app/javascript/flavours/glitch/styles/admin.scss index 4e969601b..4dbbaa1e8 100644 --- a/app/javascript/flavours/glitch/styles/admin.scss +++ b/app/javascript/flavours/glitch/styles/admin.scss @@ -153,10 +153,15 @@ $content-width: 840px; font-weight: 500; } - .directory__tag a { + .directory__tag > a, + .directory__tag > div { box-shadow: none; } + .directory__tag .table-action-link .fa { + color: inherit; + } + .directory__tag h4 { font-size: 18px; font-weight: 700; diff --git a/app/javascript/flavours/glitch/styles/components/accounts.scss b/app/javascript/flavours/glitch/styles/components/accounts.scss index ce6cc8b29..0b7b58bb0 100644 --- a/app/javascript/flavours/glitch/styles/components/accounts.scss +++ b/app/javascript/flavours/glitch/styles/components/accounts.scss @@ -339,14 +339,41 @@ display: block; font-weight: 500; margin-bottom: 10px; +} + +.column-settings__hashtags { + .column-settings__row { + margin-bottom: 15px; + } - .column-settings__hashtag-select { + .column-select { &__control { @include search-input(); } + &__placeholder { + color: $dark-text-color; + padding-left: 2px; + font-size: 12px; + } + + &__value-container { + padding-left: 6px; + } + &__multi-value { background: lighten($ui-base-color, 8%); + + &__remove { + cursor: pointer; + + &:hover, + &:active, + &:focus { + background: lighten($ui-base-color, 12%); + color: lighten($darker-text-color, 4%); + } + } } &__multi-value__label, @@ -354,9 +381,42 @@ color: $darker-text-color; } - &__indicator-separator, + &__clear-indicator, &__dropdown-indicator { - display: none; + cursor: pointer; + transition: none; + color: $dark-text-color; + + &:hover, + &:active, + &:focus { + color: lighten($dark-text-color, 4%); + } + } + + &__indicator-separator { + background-color: lighten($ui-base-color, 8%); + } + + &__menu { + @include search-popout(); + padding: 0; + background: $ui-secondary-color; + } + + &__menu-list { + padding: 6px; + } + + &__option { + color: $inverted-text-color; + border-radius: 4px; + font-size: 14px; + + &--is-focused, + &--is-selected { + background: darken($ui-secondary-color, 10%); + } } } } diff --git a/app/javascript/flavours/glitch/styles/components/composer.scss b/app/javascript/flavours/glitch/styles/components/composer.scss index 2267b798c..fa24cabf2 100644 --- a/app/javascript/flavours/glitch/styles/components/composer.scss +++ b/app/javascript/flavours/glitch/styles/components/composer.scss @@ -262,7 +262,7 @@ background-repeat: no-repeat; overflow: hidden; - input { + textarea { display: block; position: absolute; box-sizing: border-box; @@ -294,7 +294,7 @@ &.active { & > div { - input { opacity: 1 } + textarea { opacity: 1 } } } } diff --git a/app/javascript/flavours/glitch/styles/components/drawer.scss b/app/javascript/flavours/glitch/styles/components/drawer.scss index 2821deec7..f4931c36c 100644 --- a/app/javascript/flavours/glitch/styles/components/drawer.scss +++ b/app/javascript/flavours/glitch/styles/components/drawer.scss @@ -157,29 +157,7 @@ } .drawer--search--popout { - box-sizing: border-box; - margin-top: 10px; - border-radius: 4px; - padding: 10px 14px 14px 14px; - box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4); - color: $light-text-color; - background: $simple-background-color; - - h4 { - margin-bottom: 10px; - color: $light-text-color; - font-size: 13px; - font-weight: 500; - text-transform: uppercase; - } - - ul { margin-bottom: 10px } - li { padding: 4px 0 } - - em { - color: $inverted-text-color; - font-weight: 500; - } + @include search-popout(); } .drawer--account { diff --git a/app/javascript/flavours/glitch/styles/contrast/diff.scss b/app/javascript/flavours/glitch/styles/contrast/diff.scss index eee9ecc3e..8429103b8 100644 --- a/app/javascript/flavours/glitch/styles/contrast/diff.scss +++ b/app/javascript/flavours/glitch/styles/contrast/diff.scss @@ -12,3 +12,58 @@ } } } + +.rich-formatting a, +.rich-formatting p a, +.rich-formatting li a, +.landing-page__short-description p a, +.status__content a, +.reply-indicator__content a { + color: lighten($ui-highlight-color, 12%); + text-decoration: underline; + + &.mention { + text-decoration: none; + } + + &.mention span { + text-decoration: underline; + + &:hover, + &:focus, + &:active { + text-decoration: none; + } + } + + &:hover, + &:focus, + &:active { + text-decoration: none; + } + + &.status__content__spoiler-link { + color: $secondary-text-color; + text-decoration: none; + } +} + +.status__content__read-more-button { + text-decoration: underline; + + &:hover, + &:focus, + &:active { + text-decoration: none; + } +} + +.getting-started__footer a { + text-decoration: underline; + + &:hover, + &:focus, + &:active { + text-decoration: none; + } +} diff --git a/app/javascript/flavours/glitch/styles/widgets.scss b/app/javascript/flavours/glitch/styles/widgets.scss index c97337e4e..1eaf30c5b 100644 --- a/app/javascript/flavours/glitch/styles/widgets.scss +++ b/app/javascript/flavours/glitch/styles/widgets.scss @@ -269,7 +269,8 @@ box-sizing: border-box; margin-bottom: 10px; - a { + & > a, + & > div { display: flex; align-items: center; justify-content: space-between; @@ -279,7 +280,9 @@ text-decoration: none; color: inherit; box-shadow: 0 0 15px rgba($base-shadow-color, 0.2); + } + & > a { &:hover, &:active, &:focus { @@ -287,7 +290,7 @@ } } - &.active a { + &.active > a { background: $ui-highlight-color; cursor: default; } diff --git a/app/javascript/flavours/glitch/theme.yml b/app/javascript/flavours/glitch/theme.yml index 0c8342c44..d8f313381 100644 --- a/app/javascript/flavours/glitch/theme.yml +++ b/app/javascript/flavours/glitch/theme.yml @@ -7,7 +7,7 @@ pack: filename: packs/common.js stylesheet: true embed: packs/public.js - error: + error: packs/error.js home: filename: packs/home.js preload: diff --git a/app/javascript/flavours/glitch/util/hashtag.js b/app/javascript/flavours/glitch/util/hashtag.js index d5ea57662..9b663487f 100644 --- a/app/javascript/flavours/glitch/util/hashtag.js +++ b/app/javascript/flavours/glitch/util/hashtag.js @@ -2,7 +2,7 @@ export function recoverHashtags (recognizedTags, text) { return recognizedTags.map(tag => { const re = new RegExp(`(?:^|[^\/\)\w])#(${tag.name})`, 'i'); const matched_hashtag = text.match(re); - return matched_hashtag ? matched_hashtag[1] : tag; + return matched_hashtag ? matched_hashtag[1] : null; } - ); + ).filter(x => x !== null); } diff --git a/app/javascript/flavours/glitch/util/resize_image.js b/app/javascript/flavours/glitch/util/resize_image.js index d1608094f..bbdbc865e 100644 --- a/app/javascript/flavours/glitch/util/resize_image.js +++ b/app/javascript/flavours/glitch/util/resize_image.js @@ -31,7 +31,7 @@ const loadImage = inputFile => new Promise((resolve, reject) => { }); const getOrientation = (img, type = 'image/png') => new Promise(resolve => { - if (type !== 'image/jpeg') { + if (!['image/jpeg', 'image/webp'].includes(type)) { resolve(1); return; } diff --git a/app/javascript/flavours/vanilla/theme.yml b/app/javascript/flavours/vanilla/theme.yml index bd9fb1dab..a215b2625 100644 --- a/app/javascript/flavours/vanilla/theme.yml +++ b/app/javascript/flavours/vanilla/theme.yml @@ -7,7 +7,7 @@ pack: filename: common.js stylesheet: true embed: public.js - error: + error: error.js home: filename: application.js preload: diff --git a/app/javascript/mastodon/actions/alerts.js b/app/javascript/mastodon/actions/alerts.js index 3f5d7ef46..50cd48a9e 100644 --- a/app/javascript/mastodon/actions/alerts.js +++ b/app/javascript/mastodon/actions/alerts.js @@ -22,7 +22,7 @@ export function clearAlert() { }; }; -export function showAlert(title, message) { +export function showAlert(title = messages.unexpectedTitle, message = messages.unexpectedMessage) { return { type: ALERT_SHOW, title, @@ -44,6 +44,6 @@ export function showAlertForError(error) { return showAlert(title, message); } else { console.error(error); - return showAlert(messages.unexpectedTitle, messages.unexpectedMessage); + return showAlert(); } } diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js index a4352faab..0be2a5cd4 100644 --- a/app/javascript/mastodon/actions/compose.js +++ b/app/javascript/mastodon/actions/compose.js @@ -8,6 +8,8 @@ import resizeImage from '../utils/resize_image'; import { importFetchedAccounts } from './importer'; import { updateTimeline } from './timelines'; import { showAlertForError } from './alerts'; +import { showAlert } from './alerts'; +import { defineMessages } from 'react-intl'; let cancelFetchComposeSuggestionsAccounts; @@ -49,6 +51,10 @@ export const COMPOSE_UPLOAD_CHANGE_REQUEST = 'COMPOSE_UPLOAD_UPDATE_REQUEST' export const COMPOSE_UPLOAD_CHANGE_SUCCESS = 'COMPOSE_UPLOAD_UPDATE_SUCCESS'; export const COMPOSE_UPLOAD_CHANGE_FAIL = 'COMPOSE_UPLOAD_UPDATE_FAIL'; +const messages = defineMessages({ + uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' }, +}); + export function changeCompose(text) { return { type: COMPOSE_CHANGE, @@ -184,20 +190,32 @@ export function submitComposeFail(error) { export function uploadCompose(files) { return function (dispatch, getState) { - if (getState().getIn(['compose', 'media_attachments']).size > 3) { + const uploadLimit = 4; + const media = getState().getIn(['compose', 'media_attachments']); + const total = Array.from(files).reduce((a, v) => a + v.size, 0); + const progress = new Array(files.length).fill(0); + + if (files.length + media.size > uploadLimit) { + dispatch(showAlert(undefined, messages.uploadErrorLimit)); return; } - dispatch(uploadComposeRequest()); - resizeImage(files[0]).then(file => { - const data = new FormData(); - data.append('file', file); - - return api(getState).post('/api/v1/media', data, { - onUploadProgress: ({ loaded, total }) => dispatch(uploadComposeProgress(loaded, total)), - }).then(({ data }) => dispatch(uploadComposeSuccess(data))); - }).catch(error => dispatch(uploadComposeFail(error))); + for (const [i, f] of Array.from(files).entries()) { + if (media.size + i > 3) break; + + resizeImage(f).then(file => { + const data = new FormData(); + data.append('file', file); + + return api(getState).post('/api/v1/media', data, { + onUploadProgress: function({ loaded }){ + progress[i] = loaded; + dispatch(uploadComposeProgress(progress.reduce((a, v) => a + v, 0), total)); + }, + }).then(({ data }) => dispatch(uploadComposeSuccess(data))); + }).catch(error => dispatch(uploadComposeFail(error))); + }; }; }; diff --git a/app/javascript/mastodon/components/account.js b/app/javascript/mastodon/components/account.js index 206030c00..2705a6001 100644 --- a/app/javascript/mastodon/components/account.js +++ b/app/javascript/mastodon/components/account.js @@ -88,7 +88,7 @@ class Account extends ImmutablePureComponent { if (requested) { buttons = <IconButton disabled icon='hourglass' title={intl.formatMessage(messages.requested)} />; } else if (blocking) { - buttons = <IconButton active icon='unlock-alt' title={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.handleBlock} />; + buttons = <IconButton active icon='unlock' title={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.handleBlock} />; } else if (muting) { let hidingNotificationsButton; if (account.getIn(['relationship', 'muting_notifications'])) { diff --git a/app/javascript/mastodon/components/attachment_list.js b/app/javascript/mastodon/components/attachment_list.js index 8e5bb0e0b..5dfa1464c 100644 --- a/app/javascript/mastodon/components/attachment_list.js +++ b/app/javascript/mastodon/components/attachment_list.js @@ -2,6 +2,7 @@ import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; import ImmutablePureComponent from 'react-immutable-pure-component'; +import Icon from 'mastodon/components/icon'; const filename = url => url.split('/').pop().split('#')[0].split('?')[0]; @@ -24,7 +25,7 @@ export default class AttachmentList extends ImmutablePureComponent { return ( <li key={attachment.get('id')}> - <a href={displayUrl} target='_blank' rel='noopener'><i className='fa fa-link' /> {filename(displayUrl)}</a> + <a href={displayUrl} target='_blank' rel='noopener'><Icon id='link' /> {filename(displayUrl)}</a> </li> ); })} @@ -36,7 +37,7 @@ export default class AttachmentList extends ImmutablePureComponent { return ( <div className='attachment-list'> <div className='attachment-list__icon'> - <i className='fa fa-link' /> + <Icon id='link' /> </div> <ul className='attachment-list__list'> diff --git a/app/javascript/mastodon/components/column_back_button.js b/app/javascript/mastodon/components/column_back_button.js index 8a60c4192..f41045787 100644 --- a/app/javascript/mastodon/components/column_back_button.js +++ b/app/javascript/mastodon/components/column_back_button.js @@ -1,6 +1,7 @@ import React from 'react'; import { FormattedMessage } from 'react-intl'; import PropTypes from 'prop-types'; +import Icon from 'mastodon/components/icon'; export default class ColumnBackButton extends React.PureComponent { @@ -19,7 +20,7 @@ export default class ColumnBackButton extends React.PureComponent { render () { return ( <button onClick={this.handleClick} className='column-back-button'> - <i className='fa fa-fw fa-chevron-left column-back-button__icon' /> + <Icon id='chevron-left' className='column-back-button__icon' fixedWidth /> <FormattedMessage id='column_back_button.label' defaultMessage='Back' /> </button> ); diff --git a/app/javascript/mastodon/components/column_back_button_slim.js b/app/javascript/mastodon/components/column_back_button_slim.js index 964c100be..cc8bfb151 100644 --- a/app/javascript/mastodon/components/column_back_button_slim.js +++ b/app/javascript/mastodon/components/column_back_button_slim.js @@ -1,6 +1,7 @@ import React from 'react'; import { FormattedMessage } from 'react-intl'; import ColumnBackButton from './column_back_button'; +import Icon from 'mastodon/components/icon'; export default class ColumnBackButtonSlim extends ColumnBackButton { @@ -8,7 +9,7 @@ export default class ColumnBackButtonSlim extends ColumnBackButton { return ( <div className='column-back-button--slim'> <div role='button' tabIndex='0' onClick={this.handleClick} className='column-back-button column-back-button--slim-button'> - <i className='fa fa-fw fa-chevron-left column-back-button__icon' /> + <Icon id='chevron-left' className='column-back-button__icon' fixedWidth /> <FormattedMessage id='column_back_button.label' defaultMessage='Back' /> </div> </div> diff --git a/app/javascript/mastodon/components/column_header.js b/app/javascript/mastodon/components/column_header.js index f68e4155e..f33c689e7 100644 --- a/app/javascript/mastodon/components/column_header.js +++ b/app/javascript/mastodon/components/column_header.js @@ -2,6 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import { FormattedMessage, injectIntl, defineMessages } from 'react-intl'; +import Icon from 'mastodon/components/icon'; const messages = defineMessages({ show: { id: 'column_header.show_settings', defaultMessage: 'Show settings' }, @@ -109,22 +110,22 @@ class ColumnHeader extends React.PureComponent { } if (multiColumn && pinned) { - pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={this.handlePin}><i className='fa fa fa-times' /> <FormattedMessage id='column_header.unpin' defaultMessage='Unpin' /></button>; + pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={this.handlePin}><Icon id='times' /> <FormattedMessage id='column_header.unpin' defaultMessage='Unpin' /></button>; moveButtons = ( <div key='move-buttons' className='column-header__setting-arrows'> - <button title={formatMessage(messages.moveLeft)} aria-label={formatMessage(messages.moveLeft)} className='text-btn column-header__setting-btn' onClick={this.handleMoveLeft}><i className='fa fa-chevron-left' /></button> - <button title={formatMessage(messages.moveRight)} aria-label={formatMessage(messages.moveRight)} className='text-btn column-header__setting-btn' onClick={this.handleMoveRight}><i className='fa fa-chevron-right' /></button> + <button title={formatMessage(messages.moveLeft)} aria-label={formatMessage(messages.moveLeft)} className='text-btn column-header__setting-btn' onClick={this.handleMoveLeft}><Icon id='chevron-left' /></button> + <button title={formatMessage(messages.moveRight)} aria-label={formatMessage(messages.moveRight)} className='text-btn column-header__setting-btn' onClick={this.handleMoveRight}><Icon id='chevron-right' /></button> </div> ); } else if (multiColumn) { - pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={this.handlePin}><i className='fa fa fa-plus' /> <FormattedMessage id='column_header.pin' defaultMessage='Pin' /></button>; + pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={this.handlePin}><Icon id='plus' /> <FormattedMessage id='column_header.pin' defaultMessage='Pin' /></button>; } if (!pinned && (multiColumn || showBackButton)) { backButton = ( <button onClick={this.handleBackClick} className='column-header__back-button'> - <i className='fa fa-fw fa-chevron-left column-back-button__icon' /> + <Icon id='chevron-left' className='column-back-button__icon' fixedWidth /> <FormattedMessage id='column_back_button.label' defaultMessage='Back' /> </button> ); @@ -140,7 +141,7 @@ class ColumnHeader extends React.PureComponent { } if (children || multiColumn) { - collapseButton = <button className={collapsibleButtonClassName} title={formatMessage(collapsed ? messages.show : messages.hide)} aria-label={formatMessage(collapsed ? messages.show : messages.hide)} aria-pressed={collapsed ? 'false' : 'true'} onClick={this.handleToggleClick}><i className='fa fa-sliders' /></button>; + collapseButton = <button className={collapsibleButtonClassName} title={formatMessage(collapsed ? messages.show : messages.hide)} aria-label={formatMessage(collapsed ? messages.show : messages.hide)} aria-pressed={collapsed ? 'false' : 'true'} onClick={this.handleToggleClick}><Icon id='sliders' /></button>; } const hasTitle = icon && title; @@ -150,7 +151,7 @@ class ColumnHeader extends React.PureComponent { <h1 className={buttonClassName}> {hasTitle && ( <button onClick={this.handleTitleClick}> - <i className={`fa fa-fw fa-${icon} column-header__icon`} /> + <Icon id={icon} fixedWidth className='column-header__icon' /> {title} </button> )} diff --git a/app/javascript/mastodon/components/display_name.js b/app/javascript/mastodon/components/display_name.js index acddf77c5..6b9dd6f81 100644 --- a/app/javascript/mastodon/components/display_name.js +++ b/app/javascript/mastodon/components/display_name.js @@ -11,26 +11,36 @@ export default class DisplayName extends React.PureComponent { }; render () { - const { account, others, localDomain } = this.props; - const displayNameHtml = { __html: account.get('display_name_html') }; + const { others, localDomain } = this.props; - let suffix; + let displayName, suffix, account; if (others && others.size > 1) { - suffix = `+${others.size}`; + displayName = others.take(2).map(a => <bdi key={a.get('id')}><strong className='display-name__html' dangerouslySetInnerHTML={{ __html: a.get('display_name_html') }} /></bdi>).reduce((prev, cur) => [prev, ', ', cur]); + + if (others.size - 2 > 0) { + suffix = `+${others.size - 2}`; + } } else { + if (others && others.size > 0) { + account = others.first(); + } else { + account = this.props.account; + } + let acct = account.get('acct'); if (acct.indexOf('@') === -1 && localDomain) { acct = `${acct}@${localDomain}`; } - suffix = <span className='display-name__account'>@{acct}</span>; + displayName = <bdi><strong className='display-name__html' dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }} /></bdi>; + suffix = <span className='display-name__account'>@{acct}</span>; } return ( <span className='display-name'> - <bdi><strong className='display-name__html' dangerouslySetInnerHTML={displayNameHtml} /></bdi> {suffix} + {displayName} {suffix} </span> ); } diff --git a/app/javascript/mastodon/components/domain.js b/app/javascript/mastodon/components/domain.js index 24f80e788..85729ca94 100644 --- a/app/javascript/mastodon/components/domain.js +++ b/app/javascript/mastodon/components/domain.js @@ -32,7 +32,7 @@ class Account extends ImmutablePureComponent { </span> <div className='domain__buttons'> - <IconButton active icon='unlock-alt' title={intl.formatMessage(messages.unblockDomain, { domain })} onClick={this.handleDomainUnblock} /> + <IconButton active icon='unlock' title={intl.formatMessage(messages.unblockDomain, { domain })} onClick={this.handleDomainUnblock} /> </div> </div> </div> diff --git a/app/javascript/mastodon/components/icon.js b/app/javascript/mastodon/components/icon.js new file mode 100644 index 000000000..d8a17722f --- /dev/null +++ b/app/javascript/mastodon/components/icon.js @@ -0,0 +1,21 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; + +export default class Icon extends React.PureComponent { + + static propTypes = { + id: PropTypes.string.isRequired, + className: PropTypes.string, + fixedWidth: PropTypes.bool, + }; + + render () { + const { id, className, fixedWidth, ...other } = this.props; + + return ( + <i role='img' className={classNames('fa', `fa-${id}`, className, { 'fa-fw': fixedWidth })} {...other} /> + ); + } + +} diff --git a/app/javascript/mastodon/components/icon_button.js b/app/javascript/mastodon/components/icon_button.js index b96e48fd0..fbb42f78f 100644 --- a/app/javascript/mastodon/components/icon_button.js +++ b/app/javascript/mastodon/components/icon_button.js @@ -3,6 +3,7 @@ import Motion from '../features/ui/util/optional_motion'; import spring from 'react-motion/lib/spring'; import PropTypes from 'prop-types'; import classNames from 'classnames'; +import Icon from 'mastodon/components/icon'; export default class IconButton extends React.PureComponent { @@ -86,7 +87,7 @@ export default class IconButton extends React.PureComponent { style={style} tabIndex={tabIndex} > - <i className={`fa fa-fw fa-${icon}`} aria-hidden='true' /> + <Icon id={icon} fixedWidth aria-hidden='true' /> </button> ); } @@ -104,7 +105,7 @@ export default class IconButton extends React.PureComponent { style={style} tabIndex={tabIndex} > - <i style={{ transform: `rotate(${rotate}deg)` }} className={`fa fa-fw fa-${icon}`} aria-hidden='true' /> + <Icon id={icon} style={{ transform: `rotate(${rotate}deg)` }} fixedWidth aria-hidden='true' /> </button> )} </Motion> diff --git a/app/javascript/mastodon/components/intersection_observer_article.js b/app/javascript/mastodon/components/intersection_observer_article.js index de2203a4b..e453730ba 100644 --- a/app/javascript/mastodon/components/intersection_observer_article.js +++ b/app/javascript/mastodon/components/intersection_observer_article.js @@ -65,7 +65,7 @@ export default class IntersectionObserverArticle extends React.Component { } updateStateAfterIntersection = (prevState) => { - if (prevState.isIntersecting && !this.entry.isIntersecting) { + if (prevState.isIntersecting !== false && !this.entry.isIntersecting) { scheduleIdleTask(this.hideIfNotIntersecting); } return { diff --git a/app/javascript/mastodon/components/load_gap.js b/app/javascript/mastodon/components/load_gap.js index ed4d445d0..a44d55d09 100644 --- a/app/javascript/mastodon/components/load_gap.js +++ b/app/javascript/mastodon/components/load_gap.js @@ -1,6 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { injectIntl, defineMessages } from 'react-intl'; +import Icon from 'mastodon/components/icon'; const messages = defineMessages({ load_more: { id: 'status.load_more', defaultMessage: 'Load more' }, @@ -25,7 +26,7 @@ class LoadGap extends React.PureComponent { return ( <button className='load-more load-gap' disabled={disabled} onClick={this.handleClick} aria-label={intl.formatMessage(messages.load_more)}> - <i className='fa fa-ellipsis-h' /> + <Icon id='ellipsis-h' /> </button> ); } diff --git a/app/javascript/mastodon/components/media_gallery.js b/app/javascript/mastodon/components/media_gallery.js index c507920d0..a2bc95255 100644 --- a/app/javascript/mastodon/components/media_gallery.js +++ b/app/javascript/mastodon/components/media_gallery.js @@ -194,6 +194,8 @@ class MediaGallery extends React.PureComponent { height: PropTypes.number.isRequired, onOpenMedia: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, + defaultWidth: PropTypes.number, + cacheWidth: PropTypes.func, }; static defaultProps = { @@ -202,6 +204,7 @@ class MediaGallery extends React.PureComponent { state = { visible: displayMedia !== 'hide_all' && !this.props.sensitive || displayMedia === 'show_all', + width: this.props.defaultWidth, }; componentWillReceiveProps (nextProps) { @@ -221,6 +224,7 @@ class MediaGallery extends React.PureComponent { handleRef = (node) => { if (node /*&& this.isStandaloneEligible()*/) { // offsetWidth triggers a layout, so only calculate when we need to + if (this.props.cacheWidth) this.props.cacheWidth(node.offsetWidth); this.setState({ width: node.offsetWidth, }); @@ -233,8 +237,10 @@ class MediaGallery extends React.PureComponent { } render () { - const { media, intl, sensitive, height } = this.props; - const { width, visible } = this.state; + const { media, intl, sensitive, height, defaultWidth } = this.props; + const { visible } = this.state; + + const width = this.state.width || defaultWidth; let children; diff --git a/app/javascript/mastodon/components/scrollable_list.js b/app/javascript/mastodon/components/scrollable_list.js index fec06e263..0376cf85a 100644 --- a/app/javascript/mastodon/components/scrollable_list.js +++ b/app/javascript/mastodon/components/scrollable_list.js @@ -40,6 +40,7 @@ export default class ScrollableList extends PureComponent { state = { fullscreen: null, + cachedMediaWidth: 250, // Default media/card width using default Mastodon theme }; intersectionObserverWrapper = new IntersectionObserverWrapper(); @@ -130,6 +131,20 @@ export default class ScrollableList extends PureComponent { this.handleScroll(); } + getScrollPosition = () => { + if (this.node && (this.node.scrollTop > 0 || this.mouseMovedRecently)) { + return { height: this.node.scrollHeight, top: this.node.scrollTop }; + } else { + return null; + } + } + + updateScrollBottom = (snapshot) => { + const newScrollTop = this.node.scrollHeight - snapshot; + + this.setScrollTop(newScrollTop); + } + getSnapshotBeforeUpdate (prevProps) { const someItemInserted = React.Children.count(prevProps.children) > 0 && React.Children.count(prevProps.children) < React.Children.count(this.props.children) && @@ -150,6 +165,12 @@ export default class ScrollableList extends PureComponent { } } + cacheMediaWidth = (width) => { + if (width && this.state.cachedMediaWidth !== width) { + this.setState({ cachedMediaWidth: width }); + } + } + componentWillUnmount () { this.clearMouseIdleTimer(); this.detachScrollListener(); @@ -239,7 +260,12 @@ export default class ScrollableList extends PureComponent { intersectionObserverWrapper={this.intersectionObserverWrapper} saveHeightKey={trackScroll ? `${this.context.router.route.location.key}:${scrollKey}` : null} > - {child} + {React.cloneElement(child, { + getScrollPosition: this.getScrollPosition, + updateScrollBottom: this.updateScrollBottom, + cachedMediaWidth: this.state.cachedMediaWidth, + cacheMediaWidth: this.cacheMediaWidth, + })} </IntersectionObserverArticleContainer> ))} diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js index 20d838500..6270d3c92 100644 --- a/app/javascript/mastodon/components/status.js +++ b/app/javascript/mastodon/components/status.js @@ -15,6 +15,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; import { MediaGallery, Video } from '../features/ui/util/async-components'; import { HotKeys } from 'react-hotkeys'; import classNames from 'classnames'; +import Icon from 'mastodon/components/icon'; // We use the component (and not the container) since we do not want // to use the progress bar to show download progress @@ -68,6 +69,10 @@ class Status extends ImmutablePureComponent { onMoveUp: PropTypes.func, onMoveDown: PropTypes.func, showThread: PropTypes.bool, + getScrollPosition: PropTypes.func, + updateScrollBottom: PropTypes.func, + cacheMediaWidth: PropTypes.func, + cachedMediaWidth: PropTypes.number, }; // Avoid checking props that are functions (and whose equality will always @@ -79,6 +84,43 @@ class Status extends ImmutablePureComponent { 'hidden', ]; + // Track height changes we know about to compensate scrolling + componentDidMount () { + this.didShowCard = !this.props.muted && !this.props.hidden && this.props.status && this.props.status.get('card'); + } + + getSnapshotBeforeUpdate () { + if (this.props.getScrollPosition) { + return this.props.getScrollPosition(); + } else { + return null; + } + } + + // Compensate height changes + componentDidUpdate (prevProps, prevState, snapshot) { + const doShowCard = !this.props.muted && !this.props.hidden && this.props.status && this.props.status.get('card'); + if (doShowCard && !this.didShowCard) { + this.didShowCard = true; + if (snapshot !== null && this.props.updateScrollBottom) { + if (this.node && this.node.offsetTop < snapshot.top) { + this.props.updateScrollBottom(snapshot.height - snapshot.top); + } + } + } + } + + componentWillUnmount() { + if (this.node && this.props.getScrollPosition) { + const position = this.props.getScrollPosition(); + if (position !== null && this.node.offsetTop < position.top) { + requestAnimationFrame(() => { + this.props.updateScrollBottom(position.height - position.top); + }); + } + } + } + handleClick = () => { if (this.props.onClick) { this.props.onClick(); @@ -165,6 +207,10 @@ class Status extends ImmutablePureComponent { } } + handleRef = c => { + this.node = c; + } + render () { let media = null; let statusAvatar, prepend, rebloggedByText; @@ -179,7 +225,7 @@ class Status extends ImmutablePureComponent { if (hidden) { return ( - <div> + <div ref={this.handleRef}> {status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])} {status.get('content')} </div> @@ -194,7 +240,7 @@ class Status extends ImmutablePureComponent { return ( <HotKeys handlers={minHandlers}> - <div className='status__wrapper status__wrapper--filtered focusable' tabIndex='0'> + <div className='status__wrapper status__wrapper--filtered focusable' tabIndex='0' ref={this.handleRef}> <FormattedMessage id='status.filtered' defaultMessage='Filtered' /> </div> </HotKeys> @@ -204,7 +250,7 @@ class Status extends ImmutablePureComponent { if (featured) { prepend = ( <div className='status__prepend'> - <div className='status__prepend-icon-wrapper'><i className='fa fa-fw fa-thumb-tack status__prepend-icon' /></div> + <div className='status__prepend-icon-wrapper'><Icon id='thumb-tack' className='status__prepend-icon' fixedWidth /></div> <FormattedMessage id='status.pinned' defaultMessage='Pinned toot' /> </div> ); @@ -213,7 +259,7 @@ class Status extends ImmutablePureComponent { prepend = ( <div className='status__prepend'> - <div className='status__prepend-icon-wrapper'><i className='fa fa-fw fa-retweet status__prepend-icon' /></div> + <div className='status__prepend-icon-wrapper'><Icon id='retweet' className='status__prepend-icon' fixedWidth /></div> <FormattedMessage id='status.reblogged_by' defaultMessage='{name} boosted' values={{ name: <a onClick={this.handleAccountClick} data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} className='status__display-name muted'><bdi><strong dangerouslySetInnerHTML={display_name_html} /></bdi></a> }} /> </div> ); @@ -242,11 +288,12 @@ class Status extends ImmutablePureComponent { preview={video.get('preview_url')} src={video.get('url')} alt={video.get('description')} - width={239} + width={this.props.cachedMediaWidth} height={110} inline sensitive={status.get('sensitive')} onOpenVideo={this.handleOpenVideo} + cacheWidth={this.props.cacheMediaWidth} /> )} </Bundle> @@ -254,7 +301,16 @@ class Status extends ImmutablePureComponent { } else { media = ( <Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery}> - {Component => <Component media={status.get('media_attachments')} sensitive={status.get('sensitive')} height={110} onOpenMedia={this.props.onOpenMedia} />} + {Component => ( + <Component + media={status.get('media_attachments')} + sensitive={status.get('sensitive')} + height={110} + onOpenMedia={this.props.onOpenMedia} + cacheWidth={this.props.cacheMediaWidth} + defaultWidth={this.props.cachedMediaWidth} + /> + )} </Bundle> ); } @@ -264,11 +320,13 @@ class Status extends ImmutablePureComponent { onOpenMedia={this.props.onOpenMedia} card={status.get('card')} compact + cacheWidth={this.props.cacheMediaWidth} + defaultWidth={this.props.cachedMediaWidth} /> ); } - if (otherAccounts) { + if (otherAccounts && otherAccounts.size > 0) { statusAvatar = <AvatarComposite accounts={otherAccounts} size={48} />; } else if (account === undefined || account === null) { statusAvatar = <Avatar account={status.get('account')} size={48} />; @@ -290,7 +348,7 @@ class Status extends ImmutablePureComponent { return ( <HotKeys handlers={handlers}> - <div className={classNames('status__wrapper', `status__wrapper-${status.get('visibility')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id'), read: unread === false, focusable: !this.props.muted })} tabIndex={this.props.muted ? null : 0} data-featured={featured ? 'true' : null} aria-label={textForScreenReader(intl, status, rebloggedByText, !status.get('hidden'))}> + <div className={classNames('status__wrapper', `status__wrapper-${status.get('visibility')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id'), read: unread === false, focusable: !this.props.muted })} tabIndex={this.props.muted ? null : 0} data-featured={featured ? 'true' : null} aria-label={textForScreenReader(intl, status, rebloggedByText, !status.get('hidden'))} ref={this.handleRef}> {prepend} <div className={classNames('status', `status-${status.get('visibility')}`, { 'status-reply': !!status.get('in_reply_to_id'), muted: this.props.muted, read: unread === false })} data-id={status.get('id')}> diff --git a/app/javascript/mastodon/components/status_action_bar.js b/app/javascript/mastodon/components/status_action_bar.js index 0995a1490..53d17d418 100644 --- a/app/javascript/mastodon/components/status_action_bar.js +++ b/app/javascript/mastodon/components/status_action_bar.js @@ -32,6 +32,7 @@ const messages = defineMessages({ embed: { id: 'status.embed', defaultMessage: 'Embed' }, admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' }, admin_status: { id: 'status.admin_status', defaultMessage: 'Open this status in the moderation interface' }, + copy: { id: 'status.copy', defaultMessage: 'Copy link to status' }, }); const obfuscatedCount = count => { @@ -77,7 +78,11 @@ class StatusActionBar extends ImmutablePureComponent { ] handleReplyClick = () => { - this.props.onReply(this.props.status, this.context.router.history); + if (me) { + this.props.onReply(this.props.status, this.context.router.history); + } else { + this._openInteractionDialog('reply'); + } } handleShareClick = () => { @@ -90,11 +95,23 @@ class StatusActionBar extends ImmutablePureComponent { } handleFavouriteClick = () => { - this.props.onFavourite(this.props.status); + if (me) { + this.props.onFavourite(this.props.status); + } else { + this._openInteractionDialog('favourite'); + } + } + + handleReblogClick = e => { + if (me) { + this.props.onReblog(this.props.status, e); + } else { + this._openInteractionDialog('reblog'); + } } - handleReblogClick = (e) => { - this.props.onReblog(this.props.status, e); + _openInteractionDialog = type => { + window.open(`/interact/${this.props.status.get('id')}?type=${type}`, 'mastodon-intent', 'width=445,height=600,resizable=no,menubar=no,status=no,scrollbars=yes'); } handleDeleteClick = () => { @@ -141,6 +158,25 @@ class StatusActionBar extends ImmutablePureComponent { this.props.onMuteConversation(this.props.status); } + handleCopy = () => { + const url = this.props.status.get('url'); + const textarea = document.createElement('textarea'); + + textarea.textContent = url; + textarea.style.position = 'fixed'; + + document.body.appendChild(textarea); + + try { + textarea.select(); + document.execCommand('copy'); + } catch (e) { + + } finally { + document.body.removeChild(textarea); + } + } + render () { const { status, intl, withDismiss } = this.props; @@ -156,6 +192,7 @@ class StatusActionBar extends ImmutablePureComponent { menu.push({ text: intl.formatMessage(messages.open), action: this.handleOpen }); if (publicStatus) { + menu.push({ text: intl.formatMessage(messages.copy), action: this.handleCopy }); menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed }); } @@ -184,6 +221,7 @@ class StatusActionBar extends ImmutablePureComponent { menu.push({ text: intl.formatMessage(messages.mute, { name: status.getIn(['account', 'username']) }), action: this.handleMuteClick }); menu.push({ text: intl.formatMessage(messages.block, { name: status.getIn(['account', 'username']) }), action: this.handleBlockClick }); menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport }); + if (isStaff) { menu.push(null); menu.push({ text: intl.formatMessage(messages.admin_account, { name: status.getIn(['account', 'username']) }), href: `/admin/accounts/${status.getIn(['account', 'id'])}` }); @@ -211,9 +249,9 @@ class StatusActionBar extends ImmutablePureComponent { return ( <div className='status__action-bar'> - <div className='status__action-bar__counter'><IconButton className='status__action-bar-button' disabled={anonymousAccess} title={replyTitle} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} /><span className='status__action-bar__counter__label' >{obfuscatedCount(status.get('replies_count'))}</span></div> - <IconButton className='status__action-bar-button' disabled={anonymousAccess || !publicStatus} active={status.get('reblogged')} pressed={status.get('reblogged')} title={!publicStatus ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} /> - <IconButton className='status__action-bar-button star-icon' disabled={anonymousAccess} animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} /> + <div className='status__action-bar__counter'><IconButton className='status__action-bar-button' title={replyTitle} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} /><span className='status__action-bar__counter__label' >{obfuscatedCount(status.get('replies_count'))}</span></div> + <IconButton className='status__action-bar-button' disabled={!publicStatus} active={status.get('reblogged')} pressed={status.get('reblogged')} title={!publicStatus ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} /> + <IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} /> {shareButton} <div className='status__action-bar-dropdown'> diff --git a/app/javascript/mastodon/components/status_content.js b/app/javascript/mastodon/components/status_content.js index 5e3365618..70713047d 100644 --- a/app/javascript/mastodon/components/status_content.js +++ b/app/javascript/mastodon/components/status_content.js @@ -5,6 +5,7 @@ import { isRtl } from '../rtl'; import { FormattedMessage } from 'react-intl'; import Permalink from './permalink'; import classnames from 'classnames'; +import Icon from 'mastodon/components/icon'; const MAX_HEIGHT = 642; // 20px * 32 (+ 2px padding at the top) @@ -160,7 +161,7 @@ export default class StatusContent extends React.PureComponent { const readMoreButton = ( <button className='status__content__read-more-button' onClick={this.props.onClick} key='read-more'> - <FormattedMessage id='status.read_more' defaultMessage='Read more' /><i className='fa fa-fw fa-angle-right' /> + <FormattedMessage id='status.read_more' defaultMessage='Read more' /><Icon id='angle-right' fixedWidth /> </button> ); diff --git a/app/javascript/mastodon/containers/compose_container.js b/app/javascript/mastodon/containers/compose_container.js index 5ee1d2f14..7bc7bbaa4 100644 --- a/app/javascript/mastodon/containers/compose_container.js +++ b/app/javascript/mastodon/containers/compose_container.js @@ -7,6 +7,7 @@ import { IntlProvider, addLocaleData } from 'react-intl'; import { getLocale } from '../locales'; import Compose from '../features/standalone/compose'; import initialState from '../initial_state'; +import { fetchCustomEmojis } from '../actions/custom_emojis'; const { localeData, messages } = getLocale(); addLocaleData(localeData); @@ -17,6 +18,8 @@ if (initialState) { store.dispatch(hydrateStore(initialState)); } +store.dispatch(fetchCustomEmojis()); + export default class TimelineContainer extends React.PureComponent { static propTypes = { diff --git a/app/javascript/mastodon/features/account/components/header.js b/app/javascript/mastodon/features/account/components/header.js index 2ab25cde4..7fe6d6a4f 100644 --- a/app/javascript/mastodon/features/account/components/header.js +++ b/app/javascript/mastodon/features/account/components/header.js @@ -8,6 +8,7 @@ import spring from 'react-motion/lib/spring'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { autoPlayGif, me } from '../../../initial_state'; import classNames from 'classnames'; +import Icon from 'mastodon/components/icon'; const messages = defineMessages({ unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, @@ -132,7 +133,7 @@ class Header extends ImmutablePureComponent { } else if (account.getIn(['relationship', 'blocking'])) { actionBtn = ( <div className='account--action-button'> - <IconButton size={26} icon='unlock-alt' title={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.props.onBlock} /> + <IconButton size={26} icon='unlock' title={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.props.onBlock} /> </div> ); } @@ -149,7 +150,7 @@ class Header extends ImmutablePureComponent { } if (account.get('locked')) { - lockedIcon = <i className='fa fa-lock' title={intl.formatMessage(messages.account_locked)} />; + lockedIcon = <Icon id='lock' title={intl.formatMessage(messages.account_locked)} />; } const content = { __html: account.get('note_emojified') }; @@ -176,7 +177,7 @@ class Header extends ImmutablePureComponent { <dt dangerouslySetInnerHTML={{ __html: pair.get('name_emojified') }} title={pair.get('name')} /> <dd className={pair.get('verified_at') && 'verified'} title={pair.get('value_plain')}> - {pair.get('verified_at') && <span title={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(pair.get('verified_at'), dateFormatOptions) })}><i className='fa fa-check verified__mark' /></span>} <span dangerouslySetInnerHTML={{ __html: pair.get('value_emojified') }} /> + {pair.get('verified_at') && <span title={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(pair.get('verified_at'), dateFormatOptions) })}><Icon id='check' className='verified__mark' /></span>} <span dangerouslySetInnerHTML={{ __html: pair.get('value_emojified') }} /> </dd> </dl> ))} diff --git a/app/javascript/mastodon/features/account_gallery/components/media_item.js b/app/javascript/mastodon/features/account_gallery/components/media_item.js index 7c330c430..80ac9d9ec 100644 --- a/app/javascript/mastodon/features/account_gallery/components/media_item.js +++ b/app/javascript/mastodon/features/account_gallery/components/media_item.js @@ -3,6 +3,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; import Permalink from '../../../components/permalink'; import { displayMedia } from '../../../initial_state'; +import Icon from 'mastodon/components/icon'; export default class MediaItem extends ImmutablePureComponent { @@ -45,7 +46,7 @@ export default class MediaItem extends ImmutablePureComponent { } else { icon = ( <span className='account-gallery__item__icons'> - <i className='fa fa-eye-slash' /> + <Icon id='eye-slash' /> </span> ); } diff --git a/app/javascript/mastodon/features/account_timeline/components/moved_note.js b/app/javascript/mastodon/features/account_timeline/components/moved_note.js index 280389bba..3e090bb5f 100644 --- a/app/javascript/mastodon/features/account_timeline/components/moved_note.js +++ b/app/javascript/mastodon/features/account_timeline/components/moved_note.js @@ -5,6 +5,7 @@ import { FormattedMessage } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; import AvatarOverlay from '../../../components/avatar_overlay'; import DisplayName from '../../../components/display_name'; +import Icon from 'mastodon/components/icon'; export default class MovedNote extends ImmutablePureComponent { @@ -33,7 +34,7 @@ export default class MovedNote extends ImmutablePureComponent { return ( <div className='account__moved-note'> <div className='account__moved-note__message'> - <div className='account__moved-note__icon-wrapper'><i className='fa fa-fw fa-suitcase account__moved-note__icon' /></div> + <div className='account__moved-note__icon-wrapper'><Icon id='suitcase' className='account__moved-note__icon' fixedWidth /></div> <FormattedMessage id='account.moved_to' defaultMessage='{name} has moved to:' values={{ name: <bdi><strong dangerouslySetInnerHTML={displayNameHtml} /></bdi> }} /> </div> diff --git a/app/javascript/mastodon/features/blocks/index.js b/app/javascript/mastodon/features/blocks/index.js index ca7ce6f8e..96a219c94 100644 --- a/app/javascript/mastodon/features/blocks/index.js +++ b/app/javascript/mastodon/features/blocks/index.js @@ -18,6 +18,7 @@ const messages = defineMessages({ const mapStateToProps = state => ({ accountIds: state.getIn(['user_lists', 'blocks', 'items']), + hasMore: !!state.getIn(['user_lists', 'blocks', 'next']), }); export default @connect(mapStateToProps) @@ -29,6 +30,7 @@ class Blocks extends ImmutablePureComponent { dispatch: PropTypes.func.isRequired, shouldUpdateScroll: PropTypes.func, accountIds: ImmutablePropTypes.list, + hasMore: PropTypes.bool, intl: PropTypes.object.isRequired, }; @@ -41,7 +43,7 @@ class Blocks extends ImmutablePureComponent { }, 300, { leading: true }); render () { - const { intl, accountIds, shouldUpdateScroll } = this.props; + const { intl, accountIds, shouldUpdateScroll, hasMore } = this.props; if (!accountIds) { return ( @@ -59,6 +61,7 @@ class Blocks extends ImmutablePureComponent { <ScrollableList scrollKey='blocks' onLoadMore={this.handleLoadMore} + hasMore={hasMore} shouldUpdateScroll={shouldUpdateScroll} emptyMessage={emptyMessage} > diff --git a/app/javascript/mastodon/features/compose/components/compose_form.js b/app/javascript/mastodon/features/compose/components/compose_form.js index d6add9b0d..8909b39fd 100644 --- a/app/javascript/mastodon/features/compose/components/compose_form.js +++ b/app/javascript/mastodon/features/compose/components/compose_form.js @@ -17,6 +17,7 @@ import { isMobile } from '../../../is_mobile'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { length } from 'stringz'; import { countableText } from '../util/counter'; +import Icon from 'mastodon/components/icon'; import { maxChars } from '../../../initial_state'; const allowedAroundShortCode = '><\u0085\u0020\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029\u0009\u000a\u000b\u000c\u000d'; @@ -166,7 +167,7 @@ class ComposeForm extends ImmutablePureComponent { let publishText = ''; if (this.props.privacy === 'private' || this.props.privacy === 'direct') { - publishText = <span className='compose-form__publish-private'><i className='fa fa-lock' /> {intl.formatMessage(messages.publish)}</span>; + publishText = <span className='compose-form__publish-private'><Icon id='lock' /> {intl.formatMessage(messages.publish)}</span>; } else { publishText = this.props.privacy !== 'unlisted' ? intl.formatMessage(messages.publishLoud, { publish: intl.formatMessage(messages.publish) }) : intl.formatMessage(messages.publish); } @@ -180,7 +181,7 @@ class ComposeForm extends ImmutablePureComponent { <div className={`spoiler-input ${this.props.spoiler ? 'spoiler-input--visible' : ''}`}> <label> <span style={{ display: 'none' }}>{intl.formatMessage(messages.spoiler_placeholder)}</span> - <input placeholder={intl.formatMessage(messages.spoiler_placeholder)} value={this.props.spoiler_text} onChange={this.handleChangeSpoilerText} onKeyDown={this.handleKeyDown} type='text' className='spoiler-input__input' id='cw-spoiler-input' ref={this.setSpoilerText} /> + <input placeholder={intl.formatMessage(messages.spoiler_placeholder)} value={this.props.spoiler_text} onChange={this.handleChangeSpoilerText} onKeyDown={this.handleKeyDown} tabIndex={this.props.spoiler ? 0 : -1} type='text' className='spoiler-input__input' id='cw-spoiler-input' ref={this.setSpoilerText} /> </label> </div> diff --git a/app/javascript/mastodon/features/compose/components/privacy_dropdown.js b/app/javascript/mastodon/features/compose/components/privacy_dropdown.js index 5698765d9..3288f81f8 100644 --- a/app/javascript/mastodon/features/compose/components/privacy_dropdown.js +++ b/app/javascript/mastodon/features/compose/components/privacy_dropdown.js @@ -7,6 +7,7 @@ import Motion from '../../ui/util/optional_motion'; import spring from 'react-motion/lib/spring'; import detectPassiveEvents from 'detect-passive-events'; import classNames from 'classnames'; +import Icon from 'mastodon/components/icon'; const messages = defineMessages({ public_short: { id: 'privacy.public.short', defaultMessage: 'Public' }, @@ -132,7 +133,7 @@ class PrivacyDropdownMenu extends React.PureComponent { {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'> - <i className={`fa fa-fw fa-${item.icon}`} /> + <Icon id={item.icon} fixedWidth /> </div> <div className='privacy-dropdown__option__content'> @@ -214,7 +215,7 @@ class PrivacyDropdown extends React.PureComponent { this.options = [ { icon: 'globe', value: 'public', text: formatMessage(messages.public_short), meta: formatMessage(messages.public_long) }, - { icon: 'unlock-alt', value: 'unlisted', text: formatMessage(messages.unlisted_short), meta: formatMessage(messages.unlisted_long) }, + { icon: 'unlock', value: 'unlisted', text: formatMessage(messages.unlisted_short), meta: formatMessage(messages.unlisted_long) }, { icon: 'lock', value: 'private', text: formatMessage(messages.private_short), meta: formatMessage(messages.private_long) }, { icon: 'envelope', value: 'direct', text: formatMessage(messages.direct_short), meta: formatMessage(messages.direct_long) }, ]; diff --git a/app/javascript/mastodon/features/compose/components/search.js b/app/javascript/mastodon/features/compose/components/search.js index d203d8780..1ab197ac5 100644 --- a/app/javascript/mastodon/features/compose/components/search.js +++ b/app/javascript/mastodon/features/compose/components/search.js @@ -5,6 +5,7 @@ import Overlay from 'react-overlays/lib/Overlay'; import Motion from '../../ui/util/optional_motion'; import spring from 'react-motion/lib/spring'; import { searchEnabled } from '../../../initial_state'; +import Icon from 'mastodon/components/icon'; const messages = defineMessages({ placeholder: { id: 'search.placeholder', defaultMessage: 'Search' }, @@ -116,8 +117,8 @@ class Search extends React.PureComponent { </label> <div role='button' tabIndex='0' className='search__icon' onClick={this.handleClear}> - <i className={`fa fa-search ${hasValue ? '' : 'active'}`} /> - <i aria-label={intl.formatMessage(messages.placeholder)} className={`fa fa-times-circle ${hasValue ? 'active' : ''}`} /> + <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}> diff --git a/app/javascript/mastodon/features/compose/components/search_results.js b/app/javascript/mastodon/features/compose/components/search_results.js index f0ddf6d71..e0966f52c 100644 --- a/app/javascript/mastodon/features/compose/components/search_results.js +++ b/app/javascript/mastodon/features/compose/components/search_results.js @@ -6,6 +6,7 @@ import AccountContainer from '../../../containers/account_container'; import StatusContainer from '../../../containers/status_container'; import ImmutablePureComponent from 'react-immutable-pure-component'; import Hashtag from '../../../components/hashtag'; +import Icon from 'mastodon/components/icon'; const messages = defineMessages({ dismissSuggestion: { id: 'suggestions.dismiss', defaultMessage: 'Dismiss suggestion' }, @@ -34,7 +35,7 @@ class SearchResults extends ImmutablePureComponent { <div className='search-results'> <div className='trends'> <div className='trends__header'> - <i className='fa fa-user-plus fa-fw' /> + <Icon id='user-plus' fixedWidth /> <FormattedMessage id='suggestions.header' defaultMessage='You might be interested in…' /> </div> @@ -59,7 +60,7 @@ class SearchResults extends ImmutablePureComponent { count += results.get('accounts').size; accounts = ( <div className='search-results__section'> - <h5><i className='fa fa-fw fa-users' /><FormattedMessage id='search_results.accounts' defaultMessage='People' /></h5> + <h5><Icon id='users' fixedWidth /><FormattedMessage id='search_results.accounts' defaultMessage='People' /></h5> {results.get('accounts').map(accountId => <AccountContainer key={accountId} id={accountId} />)} </div> @@ -70,7 +71,7 @@ class SearchResults extends ImmutablePureComponent { count += results.get('statuses').size; statuses = ( <div className='search-results__section'> - <h5><i className='fa fa-fw fa-quote-right' /><FormattedMessage id='search_results.statuses' defaultMessage='Toots' /></h5> + <h5><Icon id='quote-right' fixedWidth /><FormattedMessage id='search_results.statuses' defaultMessage='Toots' /></h5> {results.get('statuses').map(statusId => <StatusContainer key={statusId} id={statusId} />)} </div> @@ -81,7 +82,7 @@ class SearchResults extends ImmutablePureComponent { count += results.get('hashtags').size; hashtags = ( <div className='search-results__section'> - <h5><i className='fa fa-fw fa-hashtag' /><FormattedMessage id='search_results.hashtags' defaultMessage='Hashtags' /></h5> + <h5><Icon id='hashtag' fixedWidth /><FormattedMessage id='search_results.hashtags' defaultMessage='Hashtags' /></h5> {results.get('hashtags').map(hashtag => <Hashtag key={hashtag.get('name')} hashtag={hashtag} />)} </div> @@ -91,7 +92,7 @@ class SearchResults extends ImmutablePureComponent { return ( <div className='search-results'> <div className='search-results__header'> - <i className='fa fa-search fa-fw' /> + <Icon id='search' fixedWidth /> <FormattedMessage id='search_results.total' defaultMessage='{count, number} {count, plural, one {result} other {results}}' values={{ count }} /> </div> diff --git a/app/javascript/mastodon/features/compose/components/upload.js b/app/javascript/mastodon/features/compose/components/upload.js index a1e99dcbb..629cbc36a 100644 --- a/app/javascript/mastodon/features/compose/components/upload.js +++ b/app/javascript/mastodon/features/compose/components/upload.js @@ -6,6 +6,7 @@ import spring from 'react-motion/lib/spring'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import classNames from 'classnames'; +import Icon from 'mastodon/components/icon'; const messages = defineMessages({ description: { id: 'upload_form.description', defaultMessage: 'Describe for the visually impaired' }, @@ -99,17 +100,16 @@ 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={classNames('compose-form__upload__actions', { active })}> - <button className='icon-button' onClick={this.handleUndoClick}><i className='fa fa-times' /> <FormattedMessage id='upload_form.undo' defaultMessage='Delete' /></button> - {media.get('type') === 'image' && <button className='icon-button' onClick={this.handleFocalPointClick}><i className='fa fa-crosshairs' /> <FormattedMessage id='upload_form.focus' defaultMessage='Crop' /></button>} + <button className='icon-button' onClick={this.handleUndoClick}><Icon id='times' /> <FormattedMessage id='upload_form.undo' defaultMessage='Delete' /></button> + {media.get('type') === 'image' && <button className='icon-button' onClick={this.handleFocalPointClick}><Icon id='crosshairs' /> <FormattedMessage id='upload_form.focus' defaultMessage='Crop' /></button>} </div> <div className={classNames('compose-form__upload-description', { active })}> <label> <span style={{ display: 'none' }}>{intl.formatMessage(messages.description)}</span> - <input + <textarea placeholder={intl.formatMessage(messages.description)} - type='text' value={description} maxLength={420} onFocus={this.handleInputFocus} diff --git a/app/javascript/mastodon/features/compose/components/upload_button.js b/app/javascript/mastodon/features/compose/components/upload_button.js index b6fe770ea..db55ad70b 100644 --- a/app/javascript/mastodon/features/compose/components/upload_button.js +++ b/app/javascript/mastodon/features/compose/components/upload_button.js @@ -63,7 +63,7 @@ class UploadButton extends ImmutablePureComponent { key={resetFileKey} ref={this.setRef} type='file' - multiple={false} + multiple accept={acceptContentTypes.toArray().join(',')} onChange={this.handleChange} disabled={disabled} diff --git a/app/javascript/mastodon/features/compose/components/upload_progress.js b/app/javascript/mastodon/features/compose/components/upload_progress.js index d5e6f19cd..cbe58f573 100644 --- a/app/javascript/mastodon/features/compose/components/upload_progress.js +++ b/app/javascript/mastodon/features/compose/components/upload_progress.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import Motion from '../../ui/util/optional_motion'; import spring from 'react-motion/lib/spring'; import { FormattedMessage } from 'react-intl'; +import Icon from 'mastodon/components/icon'; export default class UploadProgress extends React.PureComponent { @@ -21,7 +22,7 @@ export default class UploadProgress extends React.PureComponent { return ( <div className='upload-progress'> <div className='upload-progress__icon'> - <i className='fa fa-upload' /> + <Icon id='upload' /> </div> <div className='upload-progress__message'> diff --git a/app/javascript/mastodon/features/compose/index.js b/app/javascript/mastodon/features/compose/index.js index d76cd76e6..fff329106 100644 --- a/app/javascript/mastodon/features/compose/index.js +++ b/app/javascript/mastodon/features/compose/index.js @@ -14,6 +14,7 @@ import SearchResultsContainer from './containers/search_results_container'; import { changeComposing } from '../../actions/compose'; import elephantUIPlane from '../../../images/elephant_ui_plane.svg'; import { mascot } from '../../initial_state'; +import Icon from 'mastodon/components/icon'; const messages = defineMessages({ start: { id: 'getting_started.heading', defaultMessage: 'Getting started' }, @@ -77,21 +78,21 @@ class Compose extends React.PureComponent { const { columns } = this.props; header = ( <nav className='drawer__header'> - <Link to='/getting-started' className='drawer__tab' title={intl.formatMessage(messages.start)} aria-label={intl.formatMessage(messages.start)}><i role='img' className='fa fa-fw fa-bars' /></Link> + <Link to='/getting-started' className='drawer__tab' title={intl.formatMessage(messages.start)} aria-label={intl.formatMessage(messages.start)}><Icon id='bars' fixedWidth /></Link> {!columns.some(column => column.get('id') === 'HOME') && ( - <Link to='/timelines/home' className='drawer__tab' title={intl.formatMessage(messages.home_timeline)} aria-label={intl.formatMessage(messages.home_timeline)}><i role='img' className='fa fa-fw fa-home' /></Link> + <Link to='/timelines/home' className='drawer__tab' title={intl.formatMessage(messages.home_timeline)} aria-label={intl.formatMessage(messages.home_timeline)}><Icon id='home' fixedWidth /></Link> )} {!columns.some(column => column.get('id') === 'NOTIFICATIONS') && ( - <Link to='/notifications' className='drawer__tab' title={intl.formatMessage(messages.notifications)} aria-label={intl.formatMessage(messages.notifications)}><i role='img' className='fa fa-fw fa-bell' /></Link> + <Link to='/notifications' className='drawer__tab' title={intl.formatMessage(messages.notifications)} aria-label={intl.formatMessage(messages.notifications)}><Icon id='bell' fixedWidth /></Link> )} {!columns.some(column => column.get('id') === 'COMMUNITY') && ( - <Link to='/timelines/public/local' className='drawer__tab' title={intl.formatMessage(messages.community)} aria-label={intl.formatMessage(messages.community)}><i role='img' className='fa fa-fw fa-users' /></Link> + <Link to='/timelines/public/local' className='drawer__tab' title={intl.formatMessage(messages.community)} aria-label={intl.formatMessage(messages.community)}><Icon id='users' fixedWidth /></Link> )} {!columns.some(column => column.get('id') === 'PUBLIC') && ( - <Link to='/timelines/public' className='drawer__tab' title={intl.formatMessage(messages.public)} aria-label={intl.formatMessage(messages.public)}><i role='img' className='fa fa-fw fa-globe' /></Link> + <Link to='/timelines/public' className='drawer__tab' title={intl.formatMessage(messages.public)} aria-label={intl.formatMessage(messages.public)}><Icon id='globe' fixedWidth /></Link> )} - <a href='/settings/preferences' className='drawer__tab' title={intl.formatMessage(messages.preferences)} aria-label={intl.formatMessage(messages.preferences)}><i role='img' className='fa fa-fw fa-cog' /></a> - <a href='/auth/sign_out' className='drawer__tab' data-method='delete' title={intl.formatMessage(messages.logout)} aria-label={intl.formatMessage(messages.logout)}><i role='img' className='fa fa-fw fa-sign-out' /></a> + <a href='/settings/preferences' className='drawer__tab' title={intl.formatMessage(messages.preferences)} aria-label={intl.formatMessage(messages.preferences)}><Icon id='cog' fixedWidth /></a> + <a href='/auth/sign_out' className='drawer__tab' data-method='delete' title={intl.formatMessage(messages.logout)} aria-label={intl.formatMessage(messages.logout)}><Icon id='sign-out' fixedWidth /></a> </nav> ); } diff --git a/app/javascript/mastodon/features/domain_blocks/index.js b/app/javascript/mastodon/features/domain_blocks/index.js index 5c1bd1161..7c075f5a5 100644 --- a/app/javascript/mastodon/features/domain_blocks/index.js +++ b/app/javascript/mastodon/features/domain_blocks/index.js @@ -19,6 +19,7 @@ const messages = defineMessages({ const mapStateToProps = state => ({ domains: state.getIn(['domain_lists', 'blocks', 'items']), + hasMore: !!state.getIn(['domain_lists', 'blocks', 'next']), }); export default @connect(mapStateToProps) @@ -29,6 +30,7 @@ class Blocks extends ImmutablePureComponent { params: PropTypes.object.isRequired, dispatch: PropTypes.func.isRequired, shouldUpdateScroll: PropTypes.func, + hasMore: PropTypes.bool, domains: ImmutablePropTypes.orderedSet, intl: PropTypes.object.isRequired, }; @@ -42,7 +44,7 @@ class Blocks extends ImmutablePureComponent { }, 300, { leading: true }); render () { - const { intl, domains, shouldUpdateScroll } = this.props; + const { intl, domains, shouldUpdateScroll, hasMore } = this.props; if (!domains) { return ( @@ -60,6 +62,7 @@ class Blocks extends ImmutablePureComponent { <ScrollableList scrollKey='domain_blocks' onLoadMore={this.handleLoadMore} + hasMore={hasMore} shouldUpdateScroll={shouldUpdateScroll} emptyMessage={emptyMessage} > diff --git a/app/javascript/mastodon/features/follow_requests/index.js b/app/javascript/mastodon/features/follow_requests/index.js index 56ae8764b..3871e0e5d 100644 --- a/app/javascript/mastodon/features/follow_requests/index.js +++ b/app/javascript/mastodon/features/follow_requests/index.js @@ -18,6 +18,7 @@ const messages = defineMessages({ const mapStateToProps = state => ({ accountIds: state.getIn(['user_lists', 'follow_requests', 'items']), + hasMore: !!state.getIn(['user_lists', 'follow_requests', 'next']), }); export default @connect(mapStateToProps) @@ -28,6 +29,7 @@ class FollowRequests extends ImmutablePureComponent { params: PropTypes.object.isRequired, dispatch: PropTypes.func.isRequired, shouldUpdateScroll: PropTypes.func, + hasMore: PropTypes.bool, accountIds: ImmutablePropTypes.list, intl: PropTypes.object.isRequired, }; @@ -41,7 +43,7 @@ class FollowRequests extends ImmutablePureComponent { }, 300, { leading: true }); render () { - const { intl, shouldUpdateScroll, accountIds } = this.props; + const { intl, shouldUpdateScroll, accountIds, hasMore } = this.props; if (!accountIds) { return ( @@ -59,6 +61,7 @@ class FollowRequests extends ImmutablePureComponent { <ScrollableList scrollKey='follow_requests' onLoadMore={this.handleLoadMore} + hasMore={hasMore} shouldUpdateScroll={shouldUpdateScroll} emptyMessage={emptyMessage} > diff --git a/app/javascript/mastodon/features/getting_started/index.js b/app/javascript/mastodon/features/getting_started/index.js index 709a3aa96..e1f84de27 100644 --- a/app/javascript/mastodon/features/getting_started/index.js +++ b/app/javascript/mastodon/features/getting_started/index.js @@ -12,6 +12,7 @@ import { fetchFollowRequests } from '../../actions/accounts'; import { List as ImmutableList } from 'immutable'; import { Link } from 'react-router-dom'; import NavigationBar from '../compose/components/navigation_bar'; +import Icon from 'mastodon/components/icon'; const messages = defineMessages({ home_timeline: { id: 'tabs_bar.home', defaultMessage: 'Home' }, @@ -140,7 +141,7 @@ class GettingStarted extends ImmutablePureComponent { {multiColumn && <div className='column-header__wrapper'> <h1 className='column-header'> <button> - <i className='fa fa-bars fa-fw column-header__icon' /> + <Icon id='bars' className='column-header__icon' fixedWidth /> <FormattedMessage id='getting_started.heading' defaultMessage='Getting started' /> </button> </h1> @@ -159,7 +160,7 @@ class GettingStarted extends ImmutablePureComponent { {invitesEnabled && <li><a href='/invites' target='_blank'><FormattedMessage id='getting_started.invite' defaultMessage='Invite people' /></a> · </li>} {multiColumn && <li><Link to='/keyboard-shortcuts'><FormattedMessage id='navigation_bar.keyboard_shortcuts' defaultMessage='Hotkeys' /></Link> · </li>} <li><a href='/auth/edit'><FormattedMessage id='getting_started.security' defaultMessage='Security' /></a> · </li> - <li><a href='/about/more' target='_blank'><FormattedMessage id='navigation_bar.info' defaultMessage='About this instance' /></a> · </li> + <li><a href='/about/more' target='_blank'><FormattedMessage id='navigation_bar.info' defaultMessage='About this server' /></a> · </li> <li><a href='https://joinmastodon.org/apps' target='_blank'><FormattedMessage id='navigation_bar.apps' defaultMessage='Mobile apps' /></a> · </li> <li><a href='/terms' target='_blank'><FormattedMessage id='getting_started.terms' defaultMessage='Terms of service' /></a> · </li> <li><a href='/settings/applications' target='_blank'><FormattedMessage id='getting_started.developers' defaultMessage='Developers' /></a> · </li> diff --git a/app/javascript/mastodon/features/hashtag_timeline/components/column_settings.js b/app/javascript/mastodon/features/hashtag_timeline/components/column_settings.js index 9c9f62d82..cdc138c8b 100644 --- a/app/javascript/mastodon/features/hashtag_timeline/components/column_settings.js +++ b/app/javascript/mastodon/features/hashtag_timeline/components/column_settings.js @@ -1,10 +1,15 @@ import React from 'react'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import { injectIntl, FormattedMessage } from 'react-intl'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import Toggle from 'react-toggle'; import AsyncSelect from 'react-select/lib/Async'; +const messages = defineMessages({ + placeholder: { id: 'hashtag.column_settings.select.placeholder', defaultMessage: 'Enter hashtags…' }, + noOptions: { id: 'hashtag.column_settings.select.no_options_message', defaultMessage: 'No suggestions found' }, +}); + export default @injectIntl class ColumnSettings extends React.PureComponent { @@ -25,6 +30,7 @@ class ColumnSettings extends React.PureComponent { tags (mode) { let tags = this.props.settings.getIn(['tags', mode]) || []; + if (tags.toJSON) { return tags.toJSON(); } else { @@ -32,33 +38,36 @@ class ColumnSettings extends React.PureComponent { } }; - onSelect = (mode) => { - return (value) => { - this.props.onChange(['tags', mode], value); - }; - }; + onSelect = mode => value => this.props.onChange(['tags', mode], value); onToggle = () => { if (this.state.open && this.hasTags()) { this.props.onChange('tags', {}); } + this.setState({ open: !this.state.open }); }; + noOptionsMessage = () => this.props.intl.formatMessage(messages.noOptions); + modeSelect (mode) { return ( - <div className='column-settings__section'> - {this.modeLabel(mode)} + <div className='column-settings__row'> + <span className='column-settings__section'> + {this.modeLabel(mode)} + </span> + <AsyncSelect isMulti autoFocus value={this.tags(mode)} - settings={this.props.settings} - settingPath={['tags', mode]} onChange={this.onSelect(mode)} loadOptions={this.props.onLoad} - classNamePrefix='column-settings__hashtag-select' + className='column-select__container' + classNamePrefix='column-select' name='tags' + placeholder={this.props.intl.formatMessage(messages.placeholder)} + noOptionsMessage={this.noOptionsMessage} /> </div> ); @@ -66,11 +75,15 @@ class ColumnSettings extends React.PureComponent { modeLabel (mode) { switch(mode) { - case 'any': return <FormattedMessage id='hashtag.column_settings.tag_mode.any' defaultMessage='Any of these' />; - case 'all': return <FormattedMessage id='hashtag.column_settings.tag_mode.all' defaultMessage='All of these' />; - case 'none': return <FormattedMessage id='hashtag.column_settings.tag_mode.none' defaultMessage='None of these' />; + case 'any': + return <FormattedMessage id='hashtag.column_settings.tag_mode.any' defaultMessage='Any of these' />; + case 'all': + return <FormattedMessage id='hashtag.column_settings.tag_mode.all' defaultMessage='All of these' />; + case 'none': + return <FormattedMessage id='hashtag.column_settings.tag_mode.none' defaultMessage='None of these' />; + default: + return ''; } - return ''; }; render () { @@ -78,23 +91,21 @@ class ColumnSettings extends React.PureComponent { <div> <div className='column-settings__row'> <div className='setting-toggle'> - <Toggle - id='hashtag.column_settings.tag_toggle' - onChange={this.onToggle} - checked={this.state.open} - /> + <Toggle id='hashtag.column_settings.tag_toggle' onChange={this.onToggle} checked={this.state.open} /> + <span className='setting-toggle__label'> <FormattedMessage id='hashtag.column_settings.tag_toggle' defaultMessage='Include additional tags in this column' /> </span> </div> </div> - {this.state.open && + + {this.state.open && ( <div className='column-settings__hashtags'> {this.modeSelect('any')} {this.modeSelect('all')} {this.modeSelect('none')} </div> - } + )} </div> ); } diff --git a/app/javascript/mastodon/features/hashtag_timeline/index.js b/app/javascript/mastodon/features/hashtag_timeline/index.js index c2e026d13..0d3c97a64 100644 --- a/app/javascript/mastodon/features/hashtag_timeline/index.js +++ b/app/javascript/mastodon/features/hashtag_timeline/index.js @@ -41,15 +41,19 @@ class HashtagTimeline extends React.PureComponent { title = () => { let title = [this.props.params.id]; + if (this.additionalFor('any')) { - title.push(' ', <FormattedMessage id='hashtag.column_header.tag_mode.any' values={{ additional: this.additionalFor('any') }} defaultMessage='or {additional}' />); + title.push(' ', <FormattedMessage key='any' id='hashtag.column_header.tag_mode.any' values={{ additional: this.additionalFor('any') }} defaultMessage='or {additional}' />); } + if (this.additionalFor('all')) { - title.push(' ', <FormattedMessage id='hashtag.column_header.tag_mode.all' values={{ additional: this.additionalFor('all') }} defaultMessage='and {additional}' />); + title.push(' ', <FormattedMessage key='all' id='hashtag.column_header.tag_mode.all' values={{ additional: this.additionalFor('all') }} defaultMessage='and {additional}' />); } + if (this.additionalFor('none')) { - title.push(' ', <FormattedMessage id='hashtag.column_header.tag_mode.none' values={{ additional: this.additionalFor('none') }} defaultMessage='without {additional}' />); + title.push(' ', <FormattedMessage key='none' id='hashtag.column_header.tag_mode.none' values={{ additional: this.additionalFor('none') }} defaultMessage='without {additional}' />); } + return title; } @@ -77,9 +81,10 @@ class HashtagTimeline extends React.PureComponent { let all = (tags.all || []).map(tag => tag.value); let none = (tags.none || []).map(tag => tag.value); - [id, ...any].map((tag) => { - this.disconnects.push(dispatch(connectHashtagStream(id, tag, (status) => { + [id, ...any].map(tag => { + this.disconnects.push(dispatch(connectHashtagStream(id, tag, status => { let tags = status.tags.map(tag => tag.name); + return all.filter(tag => tags.includes(tag)).length === all.length && none.filter(tag => tags.includes(tag)).length === 0; }))); @@ -95,12 +100,14 @@ class HashtagTimeline extends React.PureComponent { const { dispatch } = this.props; const { id, tags } = this.props.params; + this._subscribe(dispatch, id, tags); dispatch(expandHashtagTimeline(id, { tags })); } componentWillReceiveProps (nextProps) { const { dispatch, params } = this.props; const { id, tags } = nextProps.params; + if (id !== params.id || !isEqual(tags, params.tags)) { this._unsubscribe(); this._subscribe(dispatch, id, tags); diff --git a/app/javascript/mastodon/features/introduction/index.js b/app/javascript/mastodon/features/introduction/index.js index e712b2f7d..754477bb9 100644 --- a/app/javascript/mastodon/features/introduction/index.js +++ b/app/javascript/mastodon/features/introduction/index.js @@ -89,7 +89,7 @@ const FrameInteractions = ({ onNext }) => ( </div> <div className='introduction__action'> - <button className='button' onClick={onNext}><FormattedMessage id='introduction.interactions.action' defaultMessage='Finish tutorial!' /></button> + <button className='button' onClick={onNext}><FormattedMessage id='introduction.interactions.action' defaultMessage='Finish toot-orial!' /></button> </div> </div> ); diff --git a/app/javascript/mastodon/features/list_adder/components/list.js b/app/javascript/mastodon/features/list_adder/components/list.js index cb8eb7d7a..60c8958a7 100644 --- a/app/javascript/mastodon/features/list_adder/components/list.js +++ b/app/javascript/mastodon/features/list_adder/components/list.js @@ -6,6 +6,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import IconButton from '../../../components/icon_button'; import { defineMessages, injectIntl } from 'react-intl'; import { removeFromListAdder, addToListAdder } from '../../../actions/lists'; +import Icon from 'mastodon/components/icon'; const messages = defineMessages({ remove: { id: 'lists.account.remove', defaultMessage: 'Remove from list' }, @@ -53,7 +54,7 @@ class List extends ImmutablePureComponent { <div className='list'> <div className='list__wrapper'> <div className='list__display-name'> - <i className='fa fa-fw fa-list-ul column-link__icon' /> + <Icon id='list-ul' className='column-link__icon' fixedWidth /> {list.get('title')} </div> diff --git a/app/javascript/mastodon/features/list_editor/components/edit_list_form.js b/app/javascript/mastodon/features/list_editor/components/edit_list_form.js new file mode 100644 index 000000000..3dc59c12e --- /dev/null +++ b/app/javascript/mastodon/features/list_editor/components/edit_list_form.js @@ -0,0 +1,70 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import { changeListEditorTitle, submitListEditor } from '../../../actions/lists'; +import IconButton from '../../../components/icon_button'; +import { defineMessages, injectIntl } from 'react-intl'; + +const messages = defineMessages({ + title: { id: 'lists.edit.submit', defaultMessage: 'Change title' }, +}); + +const mapStateToProps = state => ({ + value: state.getIn(['listEditor', 'title']), + disabled: !state.getIn(['listEditor', 'isChanged']), +}); + +const mapDispatchToProps = dispatch => ({ + onChange: value => dispatch(changeListEditorTitle(value)), + onSubmit: () => dispatch(submitListEditor(false)), +}); + +export default @connect(mapStateToProps, mapDispatchToProps) +@injectIntl +class ListForm extends React.PureComponent { + + static propTypes = { + value: PropTypes.string.isRequired, + disabled: PropTypes.bool, + intl: PropTypes.object.isRequired, + onChange: PropTypes.func.isRequired, + onSubmit: PropTypes.func.isRequired, + }; + + handleChange = e => { + this.props.onChange(e.target.value); + } + + handleSubmit = e => { + e.preventDefault(); + this.props.onSubmit(); + } + + handleClick = () => { + this.props.onSubmit(); + } + + render () { + const { value, disabled, intl } = this.props; + + const title = intl.formatMessage(messages.title); + + return ( + <form className='column-inline-form' onSubmit={this.handleSubmit}> + <input + className='setting-text' + value={value} + onChange={this.handleChange} + /> + + <IconButton + disabled={disabled} + icon='check' + title={title} + onClick={this.handleClick} + /> + </form> + ); + } + +} diff --git a/app/javascript/mastodon/features/list_editor/components/search.js b/app/javascript/mastodon/features/list_editor/components/search.js index f7617fe58..e3f069bb8 100644 --- a/app/javascript/mastodon/features/list_editor/components/search.js +++ b/app/javascript/mastodon/features/list_editor/components/search.js @@ -4,6 +4,7 @@ import { connect } from 'react-redux'; import { defineMessages, injectIntl } from 'react-intl'; import { fetchListSuggestions, clearListSuggestions, changeListSuggestions } from '../../../actions/lists'; import classNames from 'classnames'; +import Icon from 'mastodon/components/icon'; const messages = defineMessages({ search: { id: 'lists.search', defaultMessage: 'Search among people you follow' }, @@ -65,8 +66,8 @@ class Search extends React.PureComponent { </label> <div role='button' tabIndex='0' className='search__icon' onClick={this.handleClear}> - <i className={classNames('fa fa-search', { active: !hasValue })} /> - <i aria-label={intl.formatMessage(messages.search)} className={classNames('fa fa-times-circle', { active: hasValue })} /> + <Icon id='search' className={classNames({ active: !hasValue })} /> + <Icon id='times-circle' aria-label={intl.formatMessage(messages.search)} className={classNames({ active: hasValue })} /> </div> </div> ); diff --git a/app/javascript/mastodon/features/list_editor/index.js b/app/javascript/mastodon/features/list_editor/index.js index aab0cdd0c..48466604a 100644 --- a/app/javascript/mastodon/features/list_editor/index.js +++ b/app/javascript/mastodon/features/list_editor/index.js @@ -7,11 +7,11 @@ import { injectIntl } from 'react-intl'; import { setupListEditor, clearListSuggestions, resetListEditor } from '../../actions/lists'; import Account from './components/account'; import Search from './components/search'; +import EditListForm from './components/edit_list_form'; import Motion from '../ui/util/optional_motion'; import spring from 'react-motion/lib/spring'; const mapStateToProps = state => ({ - title: state.getIn(['listEditor', 'title']), accountIds: state.getIn(['listEditor', 'accounts', 'items']), searchAccountIds: state.getIn(['listEditor', 'suggestions', 'items']), }); @@ -33,7 +33,6 @@ class ListEditor extends ImmutablePureComponent { onInitialize: PropTypes.func.isRequired, onClear: PropTypes.func.isRequired, onReset: PropTypes.func.isRequired, - title: PropTypes.string.isRequired, accountIds: ImmutablePropTypes.list.isRequired, searchAccountIds: ImmutablePropTypes.list.isRequired, }; @@ -49,12 +48,12 @@ class ListEditor extends ImmutablePureComponent { } render () { - const { title, accountIds, searchAccountIds, onClear } = this.props; + const { accountIds, searchAccountIds, onClear } = this.props; const showSearch = searchAccountIds.size > 0; return ( <div className='modal-root__modal list-editor'> - <h4>{title}</h4> + <EditListForm /> <Search /> diff --git a/app/javascript/mastodon/features/list_timeline/index.js b/app/javascript/mastodon/features/list_timeline/index.js index 5b047ace4..b6722e91a 100644 --- a/app/javascript/mastodon/features/list_timeline/index.js +++ b/app/javascript/mastodon/features/list_timeline/index.js @@ -14,6 +14,7 @@ import { fetchList, deleteList } from '../../actions/lists'; import { openModal } from '../../actions/modal'; import MissingIndicator from '../../components/missing_indicator'; import LoadingIndicator from '../../components/loading_indicator'; +import Icon from 'mastodon/components/icon'; const messages = defineMessages({ deleteMessage: { id: 'confirmations.delete_list.message', defaultMessage: 'Are you sure you want to permanently delete this list?' }, @@ -150,11 +151,11 @@ class ListTimeline extends React.PureComponent { > <div className='column-header__links'> <button className='text-btn column-header__setting-btn' tabIndex='0' onClick={this.handleEditClick}> - <i className='fa fa-pencil' /> <FormattedMessage id='lists.edit' defaultMessage='Edit list' /> + <Icon id='pencil' /> <FormattedMessage id='lists.edit' defaultMessage='Edit list' /> </button> <button className='text-btn column-header__setting-btn' tabIndex='0' onClick={this.handleDeleteClick}> - <i className='fa fa-trash' /> <FormattedMessage id='lists.delete' defaultMessage='Delete list' /> + <Icon id='trash' /> <FormattedMessage id='lists.delete' defaultMessage='Delete list' /> </button> </div> diff --git a/app/javascript/mastodon/features/mutes/index.js b/app/javascript/mastodon/features/mutes/index.js index f979ef72f..4ed29a1ce 100644 --- a/app/javascript/mastodon/features/mutes/index.js +++ b/app/javascript/mastodon/features/mutes/index.js @@ -18,6 +18,7 @@ const messages = defineMessages({ const mapStateToProps = state => ({ accountIds: state.getIn(['user_lists', 'mutes', 'items']), + hasMore: !!state.getIn(['user_lists', 'mutes', 'next']), }); export default @connect(mapStateToProps) @@ -28,6 +29,7 @@ class Mutes extends ImmutablePureComponent { params: PropTypes.object.isRequired, dispatch: PropTypes.func.isRequired, shouldUpdateScroll: PropTypes.func, + hasMore: PropTypes.bool, accountIds: ImmutablePropTypes.list, intl: PropTypes.object.isRequired, }; @@ -41,7 +43,7 @@ class Mutes extends ImmutablePureComponent { }, 300, { leading: true }); render () { - const { intl, shouldUpdateScroll, accountIds } = this.props; + const { intl, shouldUpdateScroll, hasMore, accountIds } = this.props; if (!accountIds) { return ( @@ -59,6 +61,7 @@ class Mutes extends ImmutablePureComponent { <ScrollableList scrollKey='mutes' onLoadMore={this.handleLoadMore} + hasMore={hasMore} shouldUpdateScroll={shouldUpdateScroll} emptyMessage={emptyMessage} > diff --git a/app/javascript/mastodon/features/notifications/components/clear_column_button.js b/app/javascript/mastodon/features/notifications/components/clear_column_button.js index e0bf4c82d..b82fd092f 100644 --- a/app/javascript/mastodon/features/notifications/components/clear_column_button.js +++ b/app/javascript/mastodon/features/notifications/components/clear_column_button.js @@ -1,6 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { FormattedMessage } from 'react-intl'; +import Icon from 'mastodon/components/icon'; export default class ClearColumnButton extends React.PureComponent { @@ -10,7 +11,7 @@ export default class ClearColumnButton extends React.PureComponent { render () { return ( - <button className='text-btn column-header__setting-btn' tabIndex='0' onClick={this.props.onClick}><i className='fa fa-eraser' /> <FormattedMessage id='notifications.clear' defaultMessage='Clear notifications' /></button> + <button className='text-btn column-header__setting-btn' tabIndex='0' onClick={this.props.onClick}><Icon id='eraser' /> <FormattedMessage id='notifications.clear' defaultMessage='Clear notifications' /></button> ); } diff --git a/app/javascript/mastodon/features/notifications/components/filter_bar.js b/app/javascript/mastodon/features/notifications/components/filter_bar.js index f95a2c9de..6ae8b7491 100644 --- a/app/javascript/mastodon/features/notifications/components/filter_bar.js +++ b/app/javascript/mastodon/features/notifications/components/filter_bar.js @@ -1,6 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import Icon from 'mastodon/components/icon'; const tooltips = defineMessages({ mentions: { id: 'notifications.filter.mentions', defaultMessage: 'Mentions' }, @@ -62,28 +63,28 @@ class FilterBar extends React.PureComponent { onClick={this.onClick('mention')} title={intl.formatMessage(tooltips.mentions)} > - <i className='fa fa-fw fa-at' /> + <Icon id='at' fixedWidth /> </button> <button className={selectedFilter === 'favourite' ? 'active' : ''} onClick={this.onClick('favourite')} title={intl.formatMessage(tooltips.favourites)} > - <i className='fa fa-fw fa-star' /> + <Icon id='star' fixedWidth /> </button> <button className={selectedFilter === 'reblog' ? 'active' : ''} onClick={this.onClick('reblog')} title={intl.formatMessage(tooltips.boosts)} > - <i className='fa fa-fw fa-retweet' /> + <Icon id='retweet' fixedWidth /> </button> <button className={selectedFilter === 'follow' ? 'active' : ''} onClick={this.onClick('follow')} title={intl.formatMessage(tooltips.follows)} > - <i className='fa fa-fw fa-user-plus' /> + <Icon id='user-plus' fixedWidth /> </button> </div> ); diff --git a/app/javascript/mastodon/features/notifications/components/notification.js b/app/javascript/mastodon/features/notifications/components/notification.js index 97efff69c..9669b6e7d 100644 --- a/app/javascript/mastodon/features/notifications/components/notification.js +++ b/app/javascript/mastodon/features/notifications/components/notification.js @@ -7,6 +7,7 @@ import { injectIntl, FormattedMessage } from 'react-intl'; import Permalink from '../../../components/permalink'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { HotKeys } from 'react-hotkeys'; +import Icon from 'mastodon/components/icon'; const notificationForScreenReader = (intl, message, timestamp) => { const output = [message]; @@ -34,6 +35,10 @@ class Notification extends ImmutablePureComponent { onToggleHidden: PropTypes.func.isRequired, status: PropTypes.option, intl: PropTypes.object.isRequired, + getScrollPosition: PropTypes.func, + updateScrollBottom: PropTypes.func, + cacheMediaWidth: PropTypes.func, + cachedMediaWidth: PropTypes.number, }; handleMoveUp = () => { @@ -105,7 +110,7 @@ class Notification extends ImmutablePureComponent { <div className='notification notification-follow focusable' tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage({ id: 'notification.follow', defaultMessage: '{name} followed you' }, { name: account.get('acct') }), notification.get('created_at'))}> <div className='notification__message'> <div className='notification__favourite-icon-wrapper'> - <i className='fa fa-fw fa-user-plus' /> + <Icon id='user-plus' fixedWidth /> </div> <span title={notification.get('created_at')}> @@ -128,6 +133,10 @@ class Notification extends ImmutablePureComponent { onMoveDown={this.handleMoveDown} onMoveUp={this.handleMoveUp} contextType='notifications' + getScrollPosition={this.props.getScrollPosition} + updateScrollBottom={this.props.updateScrollBottom} + cachedMediaWidth={this.props.cachedMediaWidth} + cacheMediaWidth={this.props.cacheMediaWidth} /> ); } @@ -140,7 +149,7 @@ class Notification extends ImmutablePureComponent { <div className='notification notification-favourite focusable' tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage({ id: 'notification.favourite', defaultMessage: '{name} favourited your status' }, { name: notification.getIn(['account', 'acct']) }), notification.get('created_at'))}> <div className='notification__message'> <div className='notification__favourite-icon-wrapper'> - <i className='fa fa-fw fa-star star-icon' /> + <Icon id='star' className='star-icon' fixedWidth /> </div> <span title={notification.get('created_at')}> @@ -148,7 +157,17 @@ class Notification extends ImmutablePureComponent { </span> </div> - <StatusContainer id={notification.get('status')} account={notification.get('account')} muted withDismiss hidden={!!this.props.hidden} /> + <StatusContainer + id={notification.get('status')} + account={notification.get('account')} + muted + withDismiss + hidden={!!this.props.hidden} + getScrollPosition={this.props.getScrollPosition} + updateScrollBottom={this.props.updateScrollBottom} + cachedMediaWidth={this.props.cachedMediaWidth} + cacheMediaWidth={this.props.cacheMediaWidth} + /> </div> </HotKeys> ); @@ -162,7 +181,7 @@ class Notification extends ImmutablePureComponent { <div className='notification notification-reblog focusable' tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage({ id: 'notification.reblog', defaultMessage: '{name} boosted your status' }, { name: notification.getIn(['account', 'acct']) }), notification.get('created_at'))}> <div className='notification__message'> <div className='notification__favourite-icon-wrapper'> - <i className='fa fa-fw fa-retweet' /> + <Icon id='retweet' fixedWidth /> </div> <span title={notification.get('created_at')}> @@ -170,7 +189,17 @@ class Notification extends ImmutablePureComponent { </span> </div> - <StatusContainer id={notification.get('status')} account={notification.get('account')} muted withDismiss hidden={this.props.hidden} /> + <StatusContainer + id={notification.get('status')} + account={notification.get('account')} + muted + withDismiss + hidden={this.props.hidden} + getScrollPosition={this.props.getScrollPosition} + updateScrollBottom={this.props.updateScrollBottom} + cachedMediaWidth={this.props.cachedMediaWidth} + cacheMediaWidth={this.props.cacheMediaWidth} + /> </div> </HotKeys> ); diff --git a/app/javascript/mastodon/features/public_timeline/index.js b/app/javascript/mastodon/features/public_timeline/index.js index d640033eb..2b7d9c56f 100644 --- a/app/javascript/mastodon/features/public_timeline/index.js +++ b/app/javascript/mastodon/features/public_timeline/index.js @@ -124,7 +124,7 @@ class PublicTimeline extends React.PureComponent { onLoadMore={this.handleLoadMore} trackScroll={!pinned} scrollKey={`public_timeline-${columnId}`} - emptyMessage={<FormattedMessage id='empty_column.public' defaultMessage='There is nothing here! Write something publicly, or manually follow users from other instances to fill it up' />} + emptyMessage={<FormattedMessage id='empty_column.public' defaultMessage='There is nothing here! Write something publicly, or manually follow users from other servers to fill it up' />} shouldUpdateScroll={shouldUpdateScroll} /> </Column> diff --git a/app/javascript/mastodon/features/status/components/action_bar.js b/app/javascript/mastodon/features/status/components/action_bar.js index d3b725283..73be1fc5f 100644 --- a/app/javascript/mastodon/features/status/components/action_bar.js +++ b/app/javascript/mastodon/features/status/components/action_bar.js @@ -28,6 +28,7 @@ const messages = defineMessages({ embed: { id: 'status.embed', defaultMessage: 'Embed' }, admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' }, admin_status: { id: 'status.admin_status', defaultMessage: 'Open this status in the moderation interface' }, + copy: { id: 'status.copy', defaultMessage: 'Copy link to status' }, }); export default @injectIntl @@ -113,6 +114,25 @@ class ActionBar extends React.PureComponent { this.props.onEmbed(this.props.status); } + handleCopy = () => { + const url = this.props.status.get('url'); + const textarea = document.createElement('textarea'); + + textarea.textContent = url; + textarea.style.position = 'fixed'; + + document.body.appendChild(textarea); + + try { + textarea.select(); + document.execCommand('copy'); + } catch (e) { + + } finally { + document.body.removeChild(textarea); + } + } + render () { const { status, intl } = this.props; @@ -122,6 +142,7 @@ class ActionBar extends React.PureComponent { let menu = []; if (publicStatus) { + menu.push({ text: intl.formatMessage(messages.copy), action: this.handleCopy }); menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed }); menu.push(null); } diff --git a/app/javascript/mastodon/features/status/components/card.js b/app/javascript/mastodon/features/status/components/card.js index 8491299ef..0eff54411 100644 --- a/app/javascript/mastodon/features/status/components/card.js +++ b/app/javascript/mastodon/features/status/components/card.js @@ -4,6 +4,7 @@ import Immutable from 'immutable'; import ImmutablePropTypes from 'react-immutable-proptypes'; import punycode from 'punycode'; import classnames from 'classnames'; +import Icon from 'mastodon/components/icon'; const IDNA_PREFIX = 'xn--'; @@ -60,6 +61,8 @@ export default class Card extends React.PureComponent { maxDescription: PropTypes.number, onOpenMedia: PropTypes.func.isRequired, compact: PropTypes.bool, + defaultWidth: PropTypes.number, + cacheWidth: PropTypes.func, }; static defaultProps = { @@ -68,7 +71,7 @@ export default class Card extends React.PureComponent { }; state = { - width: 280, + width: this.props.defaultWidth || 280, embedded: false, }; @@ -111,6 +114,7 @@ export default class Card extends React.PureComponent { setRef = c => { if (c) { + if (this.props.cacheWidth) this.props.cacheWidth(c.offsetWidth); this.setState({ width: c.offsetWidth }); } } @@ -175,8 +179,8 @@ export default class Card extends React.PureComponent { <div className='status-card__actions'> <div> - <button onClick={this.handleEmbedClick}><i className={`fa fa-${iconVariant}`} /></button> - {horizontal && <a href={card.get('url')} target='_blank' rel='noopener'><i className='fa fa-external-link' /></a>} + <button onClick={this.handleEmbedClick}><Icon id={iconVariant} /></button> + {horizontal && <a href={card.get('url')} target='_blank' rel='noopener'><Icon id='external-link' /></a>} </div> </div> </div> @@ -198,7 +202,7 @@ export default class Card extends React.PureComponent { } else { embed = ( <div className='status-card__image'> - <i className='fa fa-file-text' /> + <Icon id='file-text' /> </div> ); } diff --git a/app/javascript/mastodon/features/status/components/detailed_status.js b/app/javascript/mastodon/features/status/components/detailed_status.js index 0630387d2..49bc43a7b 100644 --- a/app/javascript/mastodon/features/status/components/detailed_status.js +++ b/app/javascript/mastodon/features/status/components/detailed_status.js @@ -13,6 +13,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; import Video from '../../video'; import scheduleIdleTask from '../../ui/util/schedule_idle_task'; import classNames from 'classnames'; +import Icon from 'mastodon/components/icon'; export default class DetailedStatus extends ImmutablePureComponent { @@ -86,7 +87,7 @@ export default class DetailedStatus extends ImmutablePureComponent { } render () { - const status = this.props.status.get('reblog') ? this.props.status.get('reblog') : this.props.status; + const status = (this.props.status && this.props.status.get('reblog')) ? this.props.status.get('reblog') : this.props.status; const outerStyle = { boxSizing: 'border-box' }; const { compact } = this.props; @@ -148,11 +149,11 @@ export default class DetailedStatus extends ImmutablePureComponent { } if (status.get('visibility') === 'private') { - reblogLink = <i className={`fa fa-${reblogIcon}`} />; + reblogLink = <Icon id={reblogIcon} />; } else if (this.context.router) { reblogLink = ( <Link to={`/statuses/${status.get('id')}/reblogs`} className='detailed-status__link'> - <i className={`fa fa-${reblogIcon}`} /> + <Icon id={reblogIcon} /> <span className='detailed-status__reblogs'> <FormattedNumber value={status.get('reblogs_count')} /> </span> @@ -161,7 +162,7 @@ export default class DetailedStatus extends ImmutablePureComponent { } else { reblogLink = ( <a href={`/interact/${status.get('id')}?type=reblog`} className='detailed-status__link' onClick={this.handleModalLink}> - <i className={`fa fa-${reblogIcon}`} /> + <Icon id={reblogIcon} /> <span className='detailed-status__reblogs'> <FormattedNumber value={status.get('reblogs_count')} /> </span> @@ -172,7 +173,7 @@ export default class DetailedStatus extends ImmutablePureComponent { if (this.context.router) { favouriteLink = ( <Link to={`/statuses/${status.get('id')}/favourites`} className='detailed-status__link'> - <i className='fa fa-star' /> + <Icon id='star' /> <span className='detailed-status__favorites'> <FormattedNumber value={status.get('favourites_count')} /> </span> @@ -181,7 +182,7 @@ export default class DetailedStatus extends ImmutablePureComponent { } else { favouriteLink = ( <a href={`/interact/${status.get('id')}?type=favourite`} className='detailed-status__link' onClick={this.handleModalLink}> - <i className='fa fa-star' /> + <Icon id='star' /> <span className='detailed-status__favorites'> <FormattedNumber value={status.get('favourites_count')} /> </span> diff --git a/app/javascript/mastodon/features/status/index.js b/app/javascript/mastodon/features/status/index.js index d48b682eb..3b5febcac 100644 --- a/app/javascript/mastodon/features/status/index.js +++ b/app/javascript/mastodon/features/status/index.js @@ -44,6 +44,7 @@ import { HotKeys } from 'react-hotkeys'; import { boostModal, deleteModal } from '../../initial_state'; import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../ui/util/fullscreen'; import { textForScreenReader } from '../../components/status'; +import Icon from 'mastodon/components/icon'; const messages = defineMessages({ deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' }, @@ -425,7 +426,7 @@ class Status extends ImmutablePureComponent { <ColumnHeader showBackButton extraButton={( - <button className='column-header__button' title={intl.formatMessage(status.get('hidden') ? messages.revealAll : messages.hideAll)} aria-label={intl.formatMessage(status.get('hidden') ? messages.revealAll : messages.hideAll)} onClick={this.handleToggleAll} aria-pressed={status.get('hidden') ? 'false' : 'true'}><i className={`fa fa-${status.get('hidden') ? 'eye-slash' : 'eye'}`} /></button> + <button className='column-header__button' title={intl.formatMessage(status.get('hidden') ? messages.revealAll : messages.hideAll)} aria-label={intl.formatMessage(status.get('hidden') ? messages.revealAll : messages.hideAll)} onClick={this.handleToggleAll} aria-pressed={status.get('hidden') ? 'false' : 'true'}><Icon id={status.get('hidden') ? 'eye-slash' : 'eye'} /></button> )} /> diff --git a/app/javascript/mastodon/features/ui/components/boost_modal.js b/app/javascript/mastodon/features/ui/components/boost_modal.js index b128e67d2..920e93d40 100644 --- a/app/javascript/mastodon/features/ui/components/boost_modal.js +++ b/app/javascript/mastodon/features/ui/components/boost_modal.js @@ -8,6 +8,7 @@ import Avatar from '../../../components/avatar'; import RelativeTimestamp from '../../../components/relative_timestamp'; import DisplayName from '../../../components/display_name'; import ImmutablePureComponent from 'react-immutable-pure-component'; +import Icon from 'mastodon/components/icon'; const messages = defineMessages({ reblog: { id: 'status.reblog', defaultMessage: 'Boost' }, @@ -74,7 +75,7 @@ class BoostModal extends ImmutablePureComponent { </div> <div className='boost-modal__action-bar'> - <div><FormattedMessage id='boost_modal.combo' defaultMessage='You can press {combo} to skip this next time' values={{ combo: <span>Shift + <i className='fa fa-retweet' /></span> }} /></div> + <div><FormattedMessage id='boost_modal.combo' defaultMessage='You can press {combo} to skip this next time' values={{ combo: <span>Shift + <Icon id='retweet' /></span> }} /></div> <Button text={intl.formatMessage(messages.reblog)} onClick={this.handleReblog} ref={this.setRef} /> </div> </div> diff --git a/app/javascript/mastodon/features/ui/components/column_header.js b/app/javascript/mastodon/features/ui/components/column_header.js index e8bdd8054..b1a36e173 100644 --- a/app/javascript/mastodon/features/ui/components/column_header.js +++ b/app/javascript/mastodon/features/ui/components/column_header.js @@ -1,6 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; +import Icon from 'mastodon/components/icon'; export default class ColumnHeader extends React.PureComponent { @@ -21,7 +22,7 @@ export default class ColumnHeader extends React.PureComponent { let iconElement = ''; if (icon) { - iconElement = <i className={`fa fa-fw fa-${icon} column-header__icon`} />; + iconElement = <Icon id={icon} fixedWidth className='column-header__icon' />; } return ( diff --git a/app/javascript/mastodon/features/ui/components/column_link.js b/app/javascript/mastodon/features/ui/components/column_link.js index 25c2d1cf8..0a25f1ea2 100644 --- a/app/javascript/mastodon/features/ui/components/column_link.js +++ b/app/javascript/mastodon/features/ui/components/column_link.js @@ -1,6 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Link } from 'react-router-dom'; +import Icon from 'mastodon/components/icon'; const ColumnLink = ({ icon, text, to, href, method, badge }) => { const badgeElement = typeof badge !== 'undefined' ? <span className='column-link__badge'>{badge}</span> : null; @@ -8,7 +9,7 @@ const ColumnLink = ({ icon, text, to, href, method, badge }) => { if (href) { return ( <a href={href} className='column-link' data-method={method}> - <i className={`fa fa-fw fa-${icon} column-link__icon`} /> + <Icon id={icon} fixedWidth className='column-link__icon' /> {text} {badgeElement} </a> @@ -16,7 +17,7 @@ const ColumnLink = ({ icon, text, to, href, method, badge }) => { } else { return ( <Link to={to} className='column-link'> - <i className={`fa fa-fw fa-${icon} column-link__icon`} /> + <Icon id={icon} fixedWidth className='column-link__icon' /> {text} {badgeElement} </Link> diff --git a/app/javascript/mastodon/features/ui/components/columns_area.js b/app/javascript/mastodon/features/ui/components/columns_area.js index b7e350cbc..63feeac45 100644 --- a/app/javascript/mastodon/features/ui/components/columns_area.js +++ b/app/javascript/mastodon/features/ui/components/columns_area.js @@ -13,6 +13,7 @@ import ColumnLoading from './column_loading'; import DrawerLoading from './drawer_loading'; import BundleColumnError from './bundle_column_error'; import { Compose, Notifications, HomeTimeline, CommunityTimeline, PublicTimeline, HashtagTimeline, DirectTimeline, FavouritedStatuses, ListTimeline } from '../../ui/util/async-components'; +import Icon from 'mastodon/components/icon'; import detectPassiveEvents from 'detect-passive-events'; import { scrollRight } from '../../../scroll'; @@ -160,7 +161,7 @@ class ColumnsArea extends ImmutablePureComponent { this.pendingIndex = null; if (singleColumn) { - const floatingActionButton = shouldHideFAB(this.context.router.history.location.pathname) ? null : <Link key='floating-action-button' to='/statuses/new' className='floating-action-button' aria-label={intl.formatMessage(messages.publish)}><i className='fa fa-pencil' /></Link>; + const floatingActionButton = shouldHideFAB(this.context.router.history.location.pathname) ? null : <Link key='floating-action-button' to='/statuses/new' className='floating-action-button' aria-label={intl.formatMessage(messages.publish)}><Icon id='pencil' /></Link>; return columnIndex !== -1 ? [ <ReactSwipeableViews key='content' index={columnIndex} onChangeIndex={this.handleSwipe} onTransitionEnd={this.handleAnimationEnd} animateTransitions={shouldAnimate} springConfig={{ duration: '400ms', delay: '0s', easeFunction: 'ease' }} style={{ height: '100%' }}> diff --git a/app/javascript/mastodon/features/ui/components/media_modal.js b/app/javascript/mastodon/features/ui/components/media_modal.js index d29a4a6a7..2120746da 100644 --- a/app/javascript/mastodon/features/ui/components/media_modal.js +++ b/app/javascript/mastodon/features/ui/components/media_modal.js @@ -9,6 +9,7 @@ import { defineMessages, injectIntl } from 'react-intl'; import IconButton from '../../../components/icon_button'; import ImmutablePureComponent from 'react-immutable-pure-component'; import ImageLoader from './image_loader'; +import Icon from 'mastodon/components/icon'; const messages = defineMessages({ close: { id: 'lightbox.close', defaultMessage: 'Close' }, @@ -108,8 +109,8 @@ class MediaModal extends ImmutablePureComponent { const index = this.getIndex(); let pagination = []; - const leftNav = media.size > 1 && <button tabIndex='0' className='media-modal__nav media-modal__nav--left' onClick={this.handlePrevClick} aria-label={intl.formatMessage(messages.previous)}><i className='fa fa-fw fa-chevron-left' /></button>; - const rightNav = media.size > 1 && <button tabIndex='0' className='media-modal__nav media-modal__nav--right' onClick={this.handleNextClick} aria-label={intl.formatMessage(messages.next)}><i className='fa fa-fw fa-chevron-right' /></button>; + const leftNav = media.size > 1 && <button tabIndex='0' className='media-modal__nav media-modal__nav--left' onClick={this.handlePrevClick} aria-label={intl.formatMessage(messages.previous)}><Icon id='chevron-left' fixedWidth /></button>; + const rightNav = media.size > 1 && <button tabIndex='0' className='media-modal__nav media-modal__nav--right' onClick={this.handleNextClick} aria-label={intl.formatMessage(messages.next)}><Icon id='chevron-right' fixedWidth /></button>; if (media.size > 1) { pagination = media.map((item, i) => { diff --git a/app/javascript/mastodon/features/ui/components/report_modal.js b/app/javascript/mastodon/features/ui/components/report_modal.js index bc6b18664..2e41f784d 100644 --- a/app/javascript/mastodon/features/ui/components/report_modal.js +++ b/app/javascript/mastodon/features/ui/components/report_modal.js @@ -97,7 +97,7 @@ class ReportModal extends ImmutablePureComponent { <div className='report-modal__container'> <div className='report-modal__comment'> - <p><FormattedMessage id='report.hint' defaultMessage='The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:' /></p> + <p><FormattedMessage id='report.hint' defaultMessage='The report will be sent to your server moderators. You can provide an explanation of why you are reporting this account below:' /></p> <textarea className='setting-text light' diff --git a/app/javascript/mastodon/features/ui/components/tabs_bar.js b/app/javascript/mastodon/features/ui/components/tabs_bar.js index 16236ea51..1b2bb7781 100644 --- a/app/javascript/mastodon/features/ui/components/tabs_bar.js +++ b/app/javascript/mastodon/features/ui/components/tabs_bar.js @@ -4,16 +4,17 @@ import { NavLink, withRouter } from 'react-router-dom'; import { FormattedMessage, injectIntl } from 'react-intl'; import { debounce } from 'lodash'; import { isUserTouching } from '../../../is_mobile'; +import Icon from 'mastodon/components/icon'; export const links = [ - <NavLink className='tabs-bar__link primary' to='/timelines/home' data-preview-title-id='column.home' data-preview-icon='home' ><i className='fa fa-fw fa-home' /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></NavLink>, - <NavLink className='tabs-bar__link primary' to='/notifications' data-preview-title-id='column.notifications' data-preview-icon='bell' ><i className='fa fa-fw fa-bell' /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></NavLink>, + <NavLink className='tabs-bar__link primary' to='/timelines/home' data-preview-title-id='column.home' data-preview-icon='home' ><Icon id='home' fixedWidth /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></NavLink>, + <NavLink className='tabs-bar__link primary' to='/notifications' data-preview-title-id='column.notifications' data-preview-icon='bell' ><Icon id='bell' fixedWidth /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></NavLink>, - <NavLink className='tabs-bar__link secondary' to='/timelines/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><i className='fa fa-fw fa-users' /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></NavLink>, - <NavLink className='tabs-bar__link secondary' exact to='/timelines/public' data-preview-title-id='column.public' data-preview-icon='globe' ><i className='fa fa-fw fa-globe' /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink>, - <NavLink className='tabs-bar__link primary' to='/search' data-preview-title-id='tabs_bar.search' data-preview-icon='bell' ><i className='fa fa-fw fa-search' /><FormattedMessage id='tabs_bar.search' defaultMessage='Search' /></NavLink>, + <NavLink className='tabs-bar__link secondary' to='/timelines/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><Icon id='users' fixedWidth /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></NavLink>, + <NavLink className='tabs-bar__link secondary' exact to='/timelines/public' data-preview-title-id='column.public' data-preview-icon='globe' ><Icon id='globe' fixedWidth /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink>, + <NavLink className='tabs-bar__link primary' to='/search' data-preview-title-id='tabs_bar.search' data-preview-icon='bell' ><Icon id='search' fixedWidth /><FormattedMessage id='tabs_bar.search' defaultMessage='Search' /></NavLink>, - <NavLink className='tabs-bar__link primary' style={{ flexGrow: '0', flexBasis: '30px' }} to='/getting-started' data-preview-title-id='getting_started.heading' data-preview-icon='bars' ><i className='fa fa-fw fa-bars' /></NavLink>, + <NavLink className='tabs-bar__link primary' style={{ flexGrow: '0', flexBasis: '30px' }} to='/getting-started' data-preview-title-id='getting_started.heading' data-preview-icon='bars' ><Icon id='bars' fixedWidth /></NavLink>, ]; export function getIndex (path) { diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js index f01c2bf24..93e45678f 100644 --- a/app/javascript/mastodon/features/ui/index.js +++ b/app/javascript/mastodon/features/ui/index.js @@ -263,7 +263,7 @@ class UI extends React.PureComponent { this.setState({ draggingOver: false }); this.dragTargets = []; - if (e.dataTransfer && e.dataTransfer.files.length === 1) { + if (e.dataTransfer && e.dataTransfer.files.length >= 1) { this.props.dispatch(uploadCompose(e.dataTransfer.files)); } } diff --git a/app/javascript/mastodon/features/video/index.js b/app/javascript/mastodon/features/video/index.js index 0d0c24d71..55dd249e1 100644 --- a/app/javascript/mastodon/features/video/index.js +++ b/app/javascript/mastodon/features/video/index.js @@ -6,6 +6,7 @@ import { throttle } from 'lodash'; import classNames from 'classnames'; import { isFullscreen, requestFullscreen, exitFullscreen } from '../ui/util/fullscreen'; import { displayMedia } from '../../initial_state'; +import Icon from 'mastodon/components/icon'; const messages = defineMessages({ play: { id: 'video.play', defaultMessage: 'Play' }, @@ -99,6 +100,7 @@ class Video extends React.PureComponent { onCloseVideo: PropTypes.func, detailed: PropTypes.bool, inline: PropTypes.bool, + cacheWidth: PropTypes.func, intl: PropTypes.object.isRequired, }; @@ -108,7 +110,7 @@ class Video extends React.PureComponent { volume: 0.5, paused: true, dragging: false, - containerWidth: false, + containerWidth: this.props.width, fullscreen: false, hovered: false, muted: false, @@ -128,6 +130,7 @@ class Video extends React.PureComponent { this.player = c; if (c) { + if (this.props.cacheWidth) this.props.cacheWidth(this.player.offsetWidth); this.setState({ containerWidth: c.offsetWidth, }); @@ -344,7 +347,6 @@ class Video extends React.PureComponent { width = containerWidth; height = containerWidth / (16/9); - playerStyle.width = width; playerStyle.height = height; } @@ -416,8 +418,8 @@ class Video extends React.PureComponent { <div className='video-player__buttons-bar'> <div className='video-player__buttons left'> - <button type='button' aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} onClick={this.togglePlay}><i className={classNames('fa fa-fw', { 'fa-play': paused, 'fa-pause': !paused })} /></button> - <button type='button' aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} onClick={this.toggleMute}><i className={classNames('fa fa-fw', { 'fa-volume-off': muted, 'fa-volume-up': !muted })} /></button> + <button type='button' aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} onClick={this.togglePlay}><Icon id={paused ? 'play' : 'pause'} fixedWidth /></button> + <button type='button' aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button> <div className='video-player__volume' onMouseDown={this.handleVolumeMouseDown} ref={this.setVolumeRef}> <div className='video-player__volume__current' style={{ width: `${volumeWidth}px` }} /> <span @@ -437,10 +439,10 @@ class Video extends React.PureComponent { </div> <div className='video-player__buttons right'> - {!onCloseVideo && <button type='button' aria-label={intl.formatMessage(messages.hide)} onClick={this.toggleReveal}><i className='fa fa-fw fa-eye' /></button>} - {(!fullscreen && onOpenVideo) && <button type='button' aria-label={intl.formatMessage(messages.expand)} onClick={this.handleOpenVideo}><i className='fa fa-fw fa-expand' /></button>} - {onCloseVideo && <button type='button' aria-label={intl.formatMessage(messages.close)} onClick={this.handleCloseVideo}><i className='fa fa-fw fa-compress' /></button>} - <button type='button' aria-label={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)} onClick={this.toggleFullscreen}><i className={classNames('fa fa-fw', { 'fa-arrows-alt': !fullscreen, 'fa-compress': fullscreen })} /></button> + {!onCloseVideo && <button type='button' aria-label={intl.formatMessage(messages.hide)} onClick={this.toggleReveal}><Icon id='eye' fixedWidth /></button>} + {(!fullscreen && onOpenVideo) && <button type='button' aria-label={intl.formatMessage(messages.expand)} onClick={this.handleOpenVideo}><Icon id='expand' fixedWidth /></button>} + {onCloseVideo && <button type='button' aria-label={intl.formatMessage(messages.close)} onClick={this.handleCloseVideo}><Icon id='compress' fixedWidth /></button>} + <button type='button' aria-label={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)} onClick={this.toggleFullscreen}><Icon id={fullscreen ? 'compress' : 'arrows-alt'} fixedWidth /></button> </div> </div> </div> diff --git a/app/javascript/mastodon/locales/ar.json b/app/javascript/mastodon/locales/ar.json index 1486272e2..78e63e5e6 100644 --- a/app/javascript/mastodon/locales/ar.json +++ b/app/javascript/mastodon/locales/ar.json @@ -297,6 +297,7 @@ "status.block": "Block @{name}", "status.cancel_reblog_private": "إلغاء الترقية", "status.cannot_reblog": "تعذرت ترقية هذا المنشور", + "status.copy": "Copy link to status", "status.delete": "إحذف", "status.detailed_status": "تفاصيل المحادثة", "status.direct": "رسالة خاصة إلى @{name}", @@ -342,6 +343,7 @@ "ui.beforeunload": "سوف تفقد مسودتك إن تركت ماستدون.", "upload_area.title": "إسحب ثم أفلت للرفع", "upload_button.label": "إضافة وسائط (JPEG، PNG، GIF، WebM، MP4، MOV)", + "upload_error.limit": "File upload limit exceeded.", "upload_form.description": "وصف للمعاقين بصريا", "upload_form.focus": "قص", "upload_form.undo": "حذف", diff --git a/app/javascript/mastodon/locales/ast.json b/app/javascript/mastodon/locales/ast.json index a9407e82d..a94e23572 100644 --- a/app/javascript/mastodon/locales/ast.json +++ b/app/javascript/mastodon/locales/ast.json @@ -297,6 +297,7 @@ "status.block": "Bloquiar a @{name}", "status.cancel_reblog_private": "Dexar de compartir", "status.cannot_reblog": "Esti artículu nun pue compartise", + "status.copy": "Copy link to status", "status.delete": "Delete", "status.detailed_status": "Detailed conversation view", "status.direct": "Unviar un mensaxe direutu a @{name}", @@ -342,6 +343,7 @@ "ui.beforeunload": "El borrador va perdese si coles de Mastodon.", "upload_area.title": "Drag & drop to upload", "upload_button.label": "Add media", + "upload_error.limit": "File upload limit exceeded.", "upload_form.description": "Descripción pa discapacitaos visuales", "upload_form.focus": "Crop", "upload_form.undo": "Desaniciar", diff --git a/app/javascript/mastodon/locales/bg.json b/app/javascript/mastodon/locales/bg.json index a812f5cb1..a5ab165b7 100644 --- a/app/javascript/mastodon/locales/bg.json +++ b/app/javascript/mastodon/locales/bg.json @@ -297,6 +297,7 @@ "status.block": "Block @{name}", "status.cancel_reblog_private": "Unboost", "status.cannot_reblog": "This post cannot be boosted", + "status.copy": "Copy link to status", "status.delete": "Изтриване", "status.detailed_status": "Detailed conversation view", "status.direct": "Direct message @{name}", @@ -342,6 +343,7 @@ "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "upload_area.title": "Drag & drop to upload", "upload_button.label": "Добави медия", + "upload_error.limit": "File upload limit exceeded.", "upload_form.description": "Describe for the visually impaired", "upload_form.focus": "Crop", "upload_form.undo": "Отмяна", diff --git a/app/javascript/mastodon/locales/ca.json b/app/javascript/mastodon/locales/ca.json index 07a4f0174..8519590b7 100644 --- a/app/javascript/mastodon/locales/ca.json +++ b/app/javascript/mastodon/locales/ca.json @@ -297,6 +297,7 @@ "status.block": "Block @{name}", "status.cancel_reblog_private": "Desfer l'impuls", "status.cannot_reblog": "Aquesta publicació no pot ser retootejada", + "status.copy": "Copy link to status", "status.delete": "Esborrar", "status.detailed_status": "Visualització detallada de la conversa", "status.direct": "Missatge directe @{name}", @@ -342,6 +343,7 @@ "ui.beforeunload": "El vostre esborrany es perdrà si sortiu de Mastodon.", "upload_area.title": "Arrossega i deixa anar per carregar", "upload_button.label": "Afegir multimèdia (JPEG, PNG, GIF, WebM, MP4, MOV)", + "upload_error.limit": "File upload limit exceeded.", "upload_form.description": "Descriure els problemes visuals", "upload_form.focus": "Modificar la previsualització", "upload_form.undo": "Esborra", diff --git a/app/javascript/mastodon/locales/co.json b/app/javascript/mastodon/locales/co.json index 496f13e7d..b4d8f9781 100644 --- a/app/javascript/mastodon/locales/co.json +++ b/app/javascript/mastodon/locales/co.json @@ -297,6 +297,7 @@ "status.block": "Bluccà @{name}", "status.cancel_reblog_private": "Ùn sparte più", "status.cannot_reblog": "Stu statutu ùn pò micca esse spartutu", + "status.copy": "Copy link to status", "status.delete": "Toglie", "status.detailed_status": "Vista in ditagliu di a cunversazione", "status.direct": "Mandà un missaghju @{name}", @@ -342,6 +343,7 @@ "ui.beforeunload": "A bruttacopia sarà persa s'ellu hè chjosu Mastodon.", "upload_area.title": "Drag & drop per caricà un fugliale", "upload_button.label": "Aghjunghje un media (JPEG, PNG, GIF, WebM, MP4, MOV)", + "upload_error.limit": "File upload limit exceeded.", "upload_form.description": "Discrive per i malvistosi", "upload_form.focus": "Cambià a vista", "upload_form.undo": "Sguassà", diff --git a/app/javascript/mastodon/locales/cs.json b/app/javascript/mastodon/locales/cs.json index e11e0be93..508b5debe 100644 --- a/app/javascript/mastodon/locales/cs.json +++ b/app/javascript/mastodon/locales/cs.json @@ -10,7 +10,7 @@ "account.edit_profile": "Upravit profil", "account.endorse": "Představit na profilu", "account.follow": "Sledovat", - "account.followers": "Sledovatelé", + "account.followers": "Sledující", "account.followers.empty": "Tohoto uživatele ještě nikdo nesleduje.", "account.follows": "Sledovaní", "account.follows.empty": "Tento uživatel ještě nikoho nesleduje.", @@ -70,8 +70,8 @@ "compose_form.direct_message_warning": "Tento toot bude odeslán pouze zmíněným uživatelům.", "compose_form.direct_message_warning_learn_more": "Zjistit více", "compose_form.hashtag_warning": "Tento toot nebude zobrazen pod žádným hashtagem, neboť je neuvedený. Pouze veřejné tooty mohou být vyhledány podle hashtagu.", - "compose_form.lock_disclaimer": "Váš účet není {locked}. Kdokoliv vás může sledovat a vidět vaše příspěvky pouze pro sledovatele.", - "compose_form.lock_disclaimer.lock": "zamčený", + "compose_form.lock_disclaimer": "Váš účet není {locked}. Kdokoliv vás může sledovat a vidět vaše příspěvky pouze pro sledující.", + "compose_form.lock_disclaimer.lock": "uzamčen", "compose_form.placeholder": "Co máte na mysli?", "compose_form.publish": "Tootnout", "compose_form.publish_loud": "{publish}!", @@ -88,7 +88,7 @@ "confirmations.delete_list.confirm": "Smazat", "confirmations.delete_list.message": "Jste si jistý/á, že chcete tento seznam navždy vymazat?", "confirmations.domain_block.confirm": "Skrýt celou doménu", - "confirmations.domain_block.message": "Jste si opravdu, opravdu jistý/á, že chcete blokovat celou {domain}? Ve většině případů stačí zablokovat nebo ignorovat pár konkrétních uživatelů, což se doporučuje. Z této domény neuvidíte obsah v žádné veřejné časové ose ani v oznámeních. Vaši sledovatelé z této domény budou odstraněni.", + "confirmations.domain_block.message": "Jste si opravdu, opravdu jistý/á, že chcete blokovat celou doménu {domain}? Ve většině případů stačí zablokovat nebo ignorovat pár konkrétních uživatelů, což se doporučuje. Z této domény neuvidíte obsah v žádné veřejné časové ose ani v oznámeních. Vaši sledující z této domény budou odstraněni.", "confirmations.mute.confirm": "Ignorovat", "confirmations.mute.message": "Jste si jistý/á, že chcete ignorovat uživatele {name}?", "confirmations.redraft.confirm": "Vymazat a přepsat", @@ -128,7 +128,7 @@ "empty_column.lists": "Ještě nemáte žádný seznam. Pokud nějaký vytvoříte, zobrazí se zde.", "empty_column.mutes": "Ještě neignorujete žádné uživatele.", "empty_column.notifications": "Ještě nemáte žádná oznámení. Začněte konverzaci komunikováním s ostatními.", - "empty_column.public": "Tady nic není! Napište něco veřejně, nebo manuálně začněte sledovat uživatele z jiných instancí, aby tu něco přibylo", + "empty_column.public": "Tady nic není! Napište něco veřejně, nebo začněte ručně sledovat uživatele z jiných serverů, aby tu něco přibylo", "follow_request.authorize": "Autorizovat", "follow_request.reject": "Odmítnout", "getting_started.developers": "Vývojáři", @@ -160,7 +160,7 @@ "introduction.interactions.favourite.headline": "Oblíbení", "introduction.interactions.favourite.text": "Oblíbením si můžete uložit toot na později a dát jeho autorovi vědět, že se vám líbí.", "introduction.interactions.reblog.headline": "Boost", - "introduction.interactions.reblog.text": "Boostnutím můžete sdílet tooty jiných lidí s vašimi sledovately.", + "introduction.interactions.reblog.text": "Boostnutím můžete sdílet tooty jiných lidí s vašimi sledujícími.", "introduction.interactions.reply.headline": "Odpověď", "introduction.interactions.reply.text": "Můžete odpovídat na tooty jiných lidí i vaše vlastní, což je propojí do konverzace.", "introduction.welcome.action": "Jdeme na to!", @@ -224,10 +224,10 @@ "navigation_bar.favourites": "Oblíbené", "navigation_bar.filters": "Skrytá slova", "navigation_bar.follow_requests": "Požadavky o sledování", - "navigation_bar.info": "O této instanci", + "navigation_bar.info": "O tomto serveru", "navigation_bar.keyboard_shortcuts": "Klávesové zkratky", "navigation_bar.lists": "Seznamy", - "navigation_bar.logout": "Odhlásit se", + "navigation_bar.logout": "Odhlásit", "navigation_bar.mutes": "Ignorovaní uživatelé", "navigation_bar.personal": "Osobní", "navigation_bar.pins": "Připnuté tooty", @@ -245,7 +245,7 @@ "notifications.column_settings.filter_bar.advanced": "Zobrazit všechny kategorie", "notifications.column_settings.filter_bar.category": "Panel rychlého filtrování", "notifications.column_settings.filter_bar.show": "Zobrazit", - "notifications.column_settings.follow": "Noví sledovatelé:", + "notifications.column_settings.follow": "Noví sledující:", "notifications.column_settings.mention": "Zmínky:", "notifications.column_settings.push": "Push oznámení", "notifications.column_settings.reblog": "Boosty:", @@ -260,8 +260,8 @@ "privacy.change": "Změnit soukromí příspěvku", "privacy.direct.long": "Odeslat pouze zmíněným uživatelům", "privacy.direct.short": "Přímý", - "privacy.private.long": "Odeslat pouze sledovatelům", - "privacy.private.short": "Pouze pro sledovatele", + "privacy.private.long": "Odeslat pouze sledujícím", + "privacy.private.short": "Pouze pro sledující", "privacy.public.long": "Odeslat na veřejné časové osy", "privacy.public.short": "Veřejný", "privacy.unlisted.long": "Neodeslat na veřejné časové osy", @@ -276,7 +276,7 @@ "reply_indicator.cancel": "Zrušit", "report.forward": "Přeposlat na {target}", "report.forward_hint": "Tento účet je z jiného serveru. Chcete na něj také poslat anonymizovanou kopii?", - "report.hint": "Toto nahlášení bude zasláno moderátorům vaší instance. Níže můžete uvést, proč tento účet nahlašujete:", + "report.hint": "Toto nahlášení bude zasláno moderátorům vašeho serveru. Níže můžete uvést, proč tento účet nahlašujete:", "report.placeholder": "Dodatečné komentáře", "report.submit": "Odeslat", "report.target": "Nahlášení uživatele {target}", @@ -297,6 +297,7 @@ "status.block": "Zablokovat uživatele @{name}", "status.cancel_reblog_private": "Zrušit boost", "status.cannot_reblog": "Tento příspěvek nemůže být boostnutý", + "status.copy": "Copy link to status", "status.delete": "Smazat", "status.detailed_status": "Detailní zobrazení konverzace", "status.direct": "Poslat přímou zprávu uživateli @{name}", @@ -342,6 +343,7 @@ "ui.beforeunload": "Váš koncept se ztratí, pokud Mastodon opustíte.", "upload_area.title": "Přetažením nahrajete", "upload_button.label": "Přidat média (JPEG, PNG, GIF, WebM, MP4, MOV)", + "upload_error.limit": "File upload limit exceeded.", "upload_form.description": "Popis pro zrakově postižené", "upload_form.focus": "Změnit náhled", "upload_form.undo": "Smazat", diff --git a/app/javascript/mastodon/locales/cy.json b/app/javascript/mastodon/locales/cy.json index f35b96244..df08b907c 100644 --- a/app/javascript/mastodon/locales/cy.json +++ b/app/javascript/mastodon/locales/cy.json @@ -1,5 +1,5 @@ { - "account.add_or_remove_from_list": "Add or Remove from lists", + "account.add_or_remove_from_list": "Ychwanegu neu Dileu o'r rhestrau", "account.badges.bot": "Bot", "account.block": "Blocio @{name}", "account.block_domain": "Cuddio popeth rhag {domain}", @@ -17,7 +17,7 @@ "account.follows_you": "Yn eich dilyn chi", "account.hide_reblogs": "Cuddio bwstiau o @{name}", "account.link_verified_on": "Gwiriwyd perchnogaeth y ddolen yma ar {date}", - "account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.", + "account.locked_info": "Mae'r statws preifatrwydd cyfrif hwn wedi'i osod i gloi. Mae'r perchennog yn adolygu'r sawl sy'n gallu eu dilyn.", "account.media": "Cyfryngau", "account.mention": "Crybwyll @{name}", "account.moved_to": "Mae @{name} wedi symud i:", @@ -132,7 +132,7 @@ "follow_request.authorize": "Caniatau", "follow_request.reject": "Gwrthod", "getting_started.developers": "Datblygwyr", - "getting_started.directory": "Profile directory", + "getting_started.directory": "Cyfeiriadur proffil", "getting_started.documentation": "Dogfennaeth", "getting_started.heading": "Dechrau", "getting_started.invite": "Gwahodd pobl", @@ -156,15 +156,15 @@ "introduction.federation.home.text": "Posts from people you follow will appear in your home feed. You can follow anyone on any server!", "introduction.federation.local.headline": "Local", "introduction.federation.local.text": "Public posts from people on the same server as you will appear in the local timeline.", - "introduction.interactions.action": "Finish tutorial!", + "introduction.interactions.action": "Gorffen tiwtorial!", "introduction.interactions.favourite.headline": "Ffefryn", "introduction.interactions.favourite.text": "You can save a toot for later, and let the author know that you liked it, by favouriting it.", "introduction.interactions.reblog.headline": "Hwb", "introduction.interactions.reblog.text": "You can share other people's toots with your followers by boosting them.", "introduction.interactions.reply.headline": "Ateb", "introduction.interactions.reply.text": "You can reply to other people's and your own toots, which will chain them together in a conversation.", - "introduction.welcome.action": "Let's go!", - "introduction.welcome.headline": "First steps", + "introduction.welcome.action": "Awn ni!", + "introduction.welcome.headline": "Camau cyntaf", "introduction.welcome.text": "Welcome to the fediverse! In a few moments, you'll be able to broadcast messages and talk to your friends across a wide variety of servers. But this server, {domain}, is special—it hosts your profile, so remember its name.", "keyboard_shortcuts.back": "i lywio nôl", "keyboard_shortcuts.blocked": "i agor rhestr defnyddwyr a flociwyd", @@ -242,8 +242,8 @@ "notifications.clear_confirmation": "Ydych chi'n sicr eich bod am glirio'ch holl hysbysiadau am byth?", "notifications.column_settings.alert": "Hysbysiadau bwrdd gwaith", "notifications.column_settings.favourite": "Ffefrynnau:", - "notifications.column_settings.filter_bar.advanced": "Display all categories", - "notifications.column_settings.filter_bar.category": "Quick filter bar", + "notifications.column_settings.filter_bar.advanced": "Dangos pob categori", + "notifications.column_settings.filter_bar.category": "Bar hidlo", "notifications.column_settings.filter_bar.show": "Dangos", "notifications.column_settings.follow": "Dilynwyr newydd:", "notifications.column_settings.mention": "Crybwylliadau:", @@ -255,7 +255,7 @@ "notifications.filter.boosts": "Hybiadau", "notifications.filter.favourites": "Ffefrynnau", "notifications.filter.follows": "Yn dilyn", - "notifications.filter.mentions": "Mentions", + "notifications.filter.mentions": "Crybwylliadau", "notifications.group": "{count} o hysbysiadau", "privacy.change": "Addasu preifatrwdd y tŵt", "privacy.direct.long": "Cyhoeddi i'r defnyddwyr sy'n cael eu crybwyll yn unig", @@ -290,13 +290,14 @@ "search_results.accounts": "Pobl", "search_results.hashtags": "Hanshnodau", "search_results.statuses": "Tŵtiau", - "search_results.total": "{count, number} {count, plural, one {result} other {results}}", + "search_results.total": "{count, number} {count, plural, one {result} arall {results}}", "standalone.public_title": "Golwg tu fewn...", "status.admin_account": "Open moderation interface for @{name}", "status.admin_status": "Open this tŵt in the moderation interface", "status.block": "Blocio @{name}", "status.cancel_reblog_private": "Dadfŵstio", "status.cannot_reblog": "Ni ellir sbarduno'r tŵt hwn", + "status.copy": "Copy link to status", "status.delete": "Dileu", "status.detailed_status": "Golwg manwl o'r sgwrs", "status.direct": "Neges breifat @{name}", @@ -331,8 +332,8 @@ "status.show_thread": "Dangos edefyn", "status.unmute_conversation": "Dad-dawelu sgwrs", "status.unpin": "Dadbinio o'r proffil", - "suggestions.dismiss": "Dismiss suggestion", - "suggestions.header": "You might be interested in…", + "suggestions.dismiss": "Diswyddo", + "suggestions.header": "Efallai y bydd gennych ddiddordeb mewn…", "tabs_bar.federated_timeline": "Ffederasiwn", "tabs_bar.home": "Hafan", "tabs_bar.local_timeline": "Lleol", @@ -342,6 +343,7 @@ "ui.beforeunload": "Mi fyddwch yn colli eich drafft os gadewch Mastodon.", "upload_area.title": "Llusgwch & gollwing i uwchlwytho", "upload_button.label": "Ychwanegwch gyfryngau (JPEG, PNG, GIF, WebM, MP4, MOV)", + "upload_error.limit": "File upload limit exceeded.", "upload_form.description": "Disgrifio i'r rheini a nam ar ei golwg", "upload_form.focus": "Newid rhagolwg", "upload_form.undo": "Dileu", diff --git a/app/javascript/mastodon/locales/da.json b/app/javascript/mastodon/locales/da.json index 60315211a..f418d26f7 100644 --- a/app/javascript/mastodon/locales/da.json +++ b/app/javascript/mastodon/locales/da.json @@ -297,6 +297,7 @@ "status.block": "Bloker @{name}", "status.cancel_reblog_private": "Fremhæv ikke længere", "status.cannot_reblog": "Denne post kan ikke fremhæves", + "status.copy": "Copy link to status", "status.delete": "Slet", "status.detailed_status": "Detaljeret visning af samtale", "status.direct": "Send direkte besked til @{name}", @@ -342,6 +343,7 @@ "ui.beforeunload": "Din kladde vil gå tabt hvis du forlader Mastodon.", "upload_area.title": "Træk og slip for at uploade", "upload_button.label": "Tilføj medie (JPEG, PNG, GIF, WebM, MP4, MOV)", + "upload_error.limit": "File upload limit exceeded.", "upload_form.description": "Beskriv for de svagtseende", "upload_form.focus": "Beskær", "upload_form.undo": "Slet", diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json index 3a55f26a7..e7cfd48b7 100644 --- a/app/javascript/mastodon/locales/de.json +++ b/app/javascript/mastodon/locales/de.json @@ -150,20 +150,20 @@ "home.column_settings.show_reblogs": "Geteilte Beiträge anzeigen", "home.column_settings.show_replies": "Antworten anzeigen", "introduction.federation.action": "Weiter", - "introduction.federation.federated.headline": "Federated", - "introduction.federation.federated.text": "Öffentliche Beiträge von anderen Servern im Fediverse werden in der föderierten Zeitleiste erscheinen.", + "introduction.federation.federated.headline": "Föderiert", + "introduction.federation.federated.text": "Öffentliche Beiträge von anderen Servern im Fediverse erscheinen in der föderierten Zeitleiste.", "introduction.federation.home.headline": "Home", - "introduction.federation.home.text": "Beiträge von Leuten, denen du folgst werden in deiner Startseite erscheinen. Du kannst jedem auf irgendeinen Server folgen!", - "introduction.federation.local.headline": "Local", - "introduction.federation.local.text": "Öffentliche Beiträge von Leuten auf demselben Server wie du werden in der lokalen Zeitleiste erscheinen.", + "introduction.federation.home.text": "Beiträge von Leuten, denen du folgst, erscheinen in deiner Start-Zeitleiste. Du kannst Menschen auf beliebigen Servern folgen!", + "introduction.federation.local.headline": "Lokal", + "introduction.federation.local.text": "Öffentliche Beiträge von Leuten auf demselben Server wie du erscheinen in der lokalen Zeitleiste.", "introduction.interactions.action": "Tutorial beenden!", "introduction.interactions.favourite.headline": "Favorisieren", - "introduction.interactions.favourite.text": "Du kannst einen Beitrag für später speichern und dem Autor wissen lassen, dass du ihn magst, indem du ihn favorisierst.", + "introduction.interactions.favourite.text": "Du kannst Beitrage für später speichern und ihre AutorInnen wissen lassen, dass sie dir gefallen haben, indem du sie favorisierst.", "introduction.interactions.reblog.headline": "Teilen", - "introduction.interactions.reblog.text": "Du kannst Beiträge von anderen Leuten an deine Follower teilen.", + "introduction.interactions.reblog.text": "Du kannst Beiträge anderer mit deinen Followern teilen, indem du sie boostest.", "introduction.interactions.reply.headline": "Antworten", - "introduction.interactions.reply.text": "Du kannst auf die Beiträge von anderen Leuten antworten und die Beiträge werden dann in eine Konversation zusammengebunden.", - "introduction.welcome.action": "Lasst uns loslegen!", + "introduction.interactions.reply.text": "Du kannst auf die Beiträge anderer antworten und die Beiträge werden dann in einer Unterhaltung zusammengefasst.", + "introduction.welcome.action": "Lass uns loslegen!", "introduction.welcome.headline": "Erste Schritte", "introduction.welcome.text": "Willkommen im Fediverse! In wenigen Momenten wirst du in der Lage sein Nachrichten zu versenden und mit deinen Freunden über Server hinweg in Kontakt zu treten. Aber dieser Server, {domain}, ist sehr speziell — er hostet dein Profil, also merke dir den Namen.", "keyboard_shortcuts.back": "zurück navigieren", @@ -297,6 +297,7 @@ "status.block": "Blockiere @{name}", "status.cancel_reblog_private": "Nicht mehr teilen", "status.cannot_reblog": "Dieser Beitrag kann nicht geteilt werden", + "status.copy": "Copy link to status", "status.delete": "Löschen", "status.detailed_status": "Detaillierte Ansicht der Konversation", "status.direct": "Direktnachricht @{name}", @@ -342,6 +343,7 @@ "ui.beforeunload": "Dein Entwurf geht verloren, wenn du Mastodon verlässt.", "upload_area.title": "Zum Hochladen hereinziehen", "upload_button.label": "Mediendatei hinzufügen (JPEG, PNG, GIF, WebM, MP4, MOV)", + "upload_error.limit": "File upload limit exceeded.", "upload_form.description": "Für Menschen mit Sehbehinderung beschreiben", "upload_form.focus": "Thumbnail bearbeiten", "upload_form.undo": "Löschen", diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json index 60f481076..6db3061aa 100644 --- a/app/javascript/mastodon/locales/defaultMessages.json +++ b/app/javascript/mastodon/locales/defaultMessages.json @@ -15,6 +15,15 @@ { "descriptors": [ { + "defaultMessage": "File upload limit exceeded.", + "id": "upload_error.limit" + } + ], + "path": "app/javascript/mastodon/actions/compose.json" + }, + { + "descriptors": [ + { "defaultMessage": "{name} mentioned you", "id": "notification.mention" }, @@ -310,6 +319,10 @@ { "defaultMessage": "Open this status in the moderation interface", "id": "status.admin_status" + }, + { + "defaultMessage": "Copy link to status", + "id": "status.copy" } ], "path": "app/javascript/mastodon/components/status_action_bar.json" @@ -1275,7 +1288,7 @@ "id": "getting_started.security" }, { - "defaultMessage": "About this instance", + "defaultMessage": "About this server", "id": "navigation_bar.info" }, { @@ -1448,7 +1461,7 @@ "id": "introduction.interactions.favourite.text" }, { - "defaultMessage": "Finish tutorial!", + "defaultMessage": "Finish toot-orial!", "id": "introduction.interactions.action" } ], @@ -1828,7 +1841,7 @@ "id": "column.public" }, { - "defaultMessage": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up", + "defaultMessage": "There is nothing here! Write something publicly, or manually follow users from other servers to fill it up", "id": "empty_column.public" } ], @@ -1946,6 +1959,10 @@ { "defaultMessage": "Open this status in the moderation interface", "id": "status.admin_status" + }, + { + "defaultMessage": "Copy link to status", + "id": "status.copy" } ], "path": "app/javascript/mastodon/features/status/components/action_bar.json" @@ -2188,7 +2205,7 @@ "id": "report.target" }, { - "defaultMessage": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:", + "defaultMessage": "The report will be sent to your server moderators. You can provide an explanation of why you are reporting this account below:", "id": "report.hint" }, { diff --git a/app/javascript/mastodon/locales/el.json b/app/javascript/mastodon/locales/el.json index 7b4852271..165f90f0c 100644 --- a/app/javascript/mastodon/locales/el.json +++ b/app/javascript/mastodon/locales/el.json @@ -7,7 +7,7 @@ "account.direct": "Προσωπικό μήνυμα προς @{name}", "account.disclaimer_full": "Οι παρακάτω πληροφορίες μπορει να μην αντανακλούν το προφίλ του χρήστη επαρκως.", "account.domain_blocked": "Κρυμμένος τομέας", - "account.edit_profile": "Επεξεργάσου το προφίλ", + "account.edit_profile": "Επεξεργασία προφίλ", "account.endorse": "Προβολή στο προφίλ", "account.follow": "Ακολούθησε", "account.followers": "Ακόλουθοι", @@ -145,16 +145,16 @@ "hashtag.column_settings.tag_mode.all": "Όλα αυτα", "hashtag.column_settings.tag_mode.any": "Οποιοδήποτε από αυτά", "hashtag.column_settings.tag_mode.none": "Κανένα από αυτά", - "hashtag.column_settings.tag_toggle": "Include additional tags in this column", + "hashtag.column_settings.tag_toggle": "Προσθήκη επιπλέον ταμπελών για την κολώνα", "home.column_settings.basic": "Βασικά", "home.column_settings.show_reblogs": "Εμφάνιση προωθήσεων", "home.column_settings.show_replies": "Εμφάνιση απαντήσεων", "introduction.federation.action": "Επόμενο", - "introduction.federation.federated.headline": "Federated", + "introduction.federation.federated.headline": "Ομοσπονδιακή", "introduction.federation.federated.text": "Οι δημόσιες αναρτήσεις από άλλους κόμβους του fediverse θα εμφανίζονται στην ομοσπονδιακή ροή.", - "introduction.federation.home.headline": "Home", + "introduction.federation.home.headline": "Αρχική", "introduction.federation.home.text": "Οι αναρτήσεις όσων ακολουθείς θα εμφανίζονται στην αρχική ροή. Μπορείς να ακολουθήσεις όποιον θέλεις σε οποιονδήποτε κόμβο!", - "introduction.federation.local.headline": "Local", + "introduction.federation.local.headline": "Τοπική", "introduction.federation.local.text": "Οι δημόσιες αναρτήσεις από άτομα στον ίδιο κόμβο με εσένα θα εμφανίζονται στην τοπική ροή.", "introduction.interactions.action": "Τέλος μαθήματος!", "introduction.interactions.favourite.headline": "Αγαπημένο", @@ -165,7 +165,7 @@ "introduction.interactions.reply.text": "Μπορείς να απαντήσεις στα τουτ άλλων αλλά ακόμα και στα δικά σου, δένοντας τα όλα μαζί σε μια συζήτηση.", "introduction.welcome.action": "Ας ξεκινήσουμε!", "introduction.welcome.headline": "Πρώτα βήματα", - "introduction.welcome.text": "Καλώς ήρθες στο fediverse! Σε πολύ λίγο θα μπορείς να στέλνεις δημοσιεύσεις και να μιλάς με τους φίλους σου σε πολλούς, διαφορετικούς κόμβους. Ο κόμβος {domain} όμως είναι ξεχωριστός — φιλοξενεί τον λογαριασμό σου, για αυτό μα θυμάσαι το όνομά του.", + "introduction.welcome.text": "Καλώς ήρθες στο fediverse! Σε πολύ λίγο θα μπορείς να στέλνεις δημοσιεύσεις και να μιλάς με τους φίλους σου σε πολλούς, διαφορετικούς κόμβους. Ο κόμβος {domain} όμως είναι ξεχωριστός — φιλοξενεί τον λογαριασμό σου, για αυτό να θυμάσαι το όνομά του.", "keyboard_shortcuts.back": "επιστροφή", "keyboard_shortcuts.blocked": "άνοιγμα λίστας αποκλεισμένων χρηστών", "keyboard_shortcuts.boost": "προώθηση", @@ -292,11 +292,12 @@ "search_results.statuses": "Τουτ", "search_results.total": "{count, number} {count, plural, ένα {result} υπόλοιπα {results}}", "standalone.public_title": "Μια πρώτη γεύση...", - "status.admin_account": "Open moderation interface for @{name}", - "status.admin_status": "Open this status in the moderation interface", + "status.admin_account": "Άνοιγμα λειτουργίας διαμεσολάβησης για τον/την @{name}", + "status.admin_status": "Άνοιγμα αυτής της δημοσίευσης στη λειτουργία διαμεσολάβησης", "status.block": "Αποκλεισμός @{name}", "status.cancel_reblog_private": "Ακύρωσε την προώθηση", "status.cannot_reblog": "Αυτή η δημοσίευση δεν μπορεί να προωθηθεί", + "status.copy": "Copy link to status", "status.delete": "Διαγραφή", "status.detailed_status": "Προβολή λεπτομερειών συζήτησης", "status.direct": "Προσωπικό μήνυμα προς @{name}", @@ -342,6 +343,7 @@ "ui.beforeunload": "Το προσχέδιό σου θα χαθεί αν φύγεις από το Mastodon.", "upload_area.title": "Drag & drop για να ανεβάσεις", "upload_button.label": "Πρόσθεσε πολυμέσα (JPEG, PNG, GIF, WebM, MP4, MOV)", + "upload_error.limit": "File upload limit exceeded.", "upload_form.description": "Περιέγραψε για όσους & όσες έχουν προβλήματα όρασης", "upload_form.focus": "Αλλαγή προεπισκόπησης", "upload_form.undo": "Διαγραφή", diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 3bb157aeb..8bc69b424 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -132,7 +132,7 @@ "empty_column.lists": "You don't have any lists yet. When you create one, it will show up here.", "empty_column.mutes": "You haven't muted any users yet.", "empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.", - "empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up", + "empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other servers to fill it up", "follow_request.authorize": "Authorize", "follow_request.reject": "Reject", "getting_started.developers": "Developers", @@ -228,7 +228,7 @@ "navigation_bar.favourites": "Favourites", "navigation_bar.filters": "Muted words", "navigation_bar.follow_requests": "Follow requests", - "navigation_bar.info": "About this instance", + "navigation_bar.info": "About this server", "navigation_bar.keyboard_shortcuts": "Hotkeys", "navigation_bar.lists": "Lists", "navigation_bar.misc": "Misc", @@ -281,7 +281,7 @@ "reply_indicator.cancel": "Cancel", "report.forward": "Forward to {target}", "report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?", - "report.hint": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:", + "report.hint": "The report will be sent to your server moderators. You can provide an explanation of why you are reporting this account below:", "report.placeholder": "Additional comments", "report.submit": "Submit", "report.target": "Reporting {target}", @@ -302,6 +302,7 @@ "status.block": "Block @{name}", "status.cancel_reblog_private": "Unboost", "status.cannot_reblog": "This post cannot be boosted", + "status.copy": "Copy link to status", "status.delete": "Delete", "status.detailed_status": "Detailed conversation view", "status.direct": "Direct message @{name}", @@ -347,6 +348,7 @@ "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "upload_area.title": "Drag & drop to upload", "upload_button.label": "Add media (JPEG, PNG, GIF, WebM, MP4, MOV)", + "upload_error.limit": "File upload limit exceeded.", "upload_form.description": "Describe for the visually impaired", "upload_form.focus": "Change preview", "upload_form.undo": "Delete", diff --git a/app/javascript/mastodon/locales/eo.json b/app/javascript/mastodon/locales/eo.json index 8be964a52..759eed46a 100644 --- a/app/javascript/mastodon/locales/eo.json +++ b/app/javascript/mastodon/locales/eo.json @@ -297,6 +297,7 @@ "status.block": "Bloki @{name}", "status.cancel_reblog_private": "Eksdiskonigi", "status.cannot_reblog": "Ĉi tiu mesaĝo ne diskonigeblas", + "status.copy": "Copy link to status", "status.delete": "Forigi", "status.detailed_status": "Detala konversacia vido", "status.direct": "Rekte mesaĝi @{name}", @@ -342,6 +343,7 @@ "ui.beforeunload": "Via malneto perdiĝos se vi eliras de Mastodon.", "upload_area.title": "Altreni kaj lasi por alŝuti", "upload_button.label": "Aldoni aŭdovidaĵon (JPEG, PNG, GIF, WebM, MP4, MOV)", + "upload_error.limit": "File upload limit exceeded.", "upload_form.description": "Priskribi por misvidantaj homoj", "upload_form.focus": "Stuci", "upload_form.undo": "Forigi", diff --git a/app/javascript/mastodon/locales/es.json b/app/javascript/mastodon/locales/es.json index 4f73dbba2..43bfe039c 100644 --- a/app/javascript/mastodon/locales/es.json +++ b/app/javascript/mastodon/locales/es.json @@ -297,6 +297,7 @@ "status.block": "Block @{name}", "status.cancel_reblog_private": "Des-impulsar", "status.cannot_reblog": "Este toot no puede retootearse", + "status.copy": "Copy link to status", "status.delete": "Borrar", "status.detailed_status": "Vista de conversación detallada", "status.direct": "Mensaje directo a @{name}", @@ -342,6 +343,7 @@ "ui.beforeunload": "Tu borrador se perderá si sales de Mastodon.", "upload_area.title": "Arrastra y suelta para subir", "upload_button.label": "Subir multimedia (JPEG, PNG, GIF, WebM, MP4, MOV)", + "upload_error.limit": "File upload limit exceeded.", "upload_form.description": "Describir para los usuarios con dificultad visual", "upload_form.focus": "Recortar", "upload_form.undo": "Borrar", diff --git a/app/javascript/mastodon/locales/eu.json b/app/javascript/mastodon/locales/eu.json index 0602fbf9e..747348e71 100644 --- a/app/javascript/mastodon/locales/eu.json +++ b/app/javascript/mastodon/locales/eu.json @@ -292,11 +292,12 @@ "search_results.statuses": "Toot-ak", "search_results.total": "{count, number} {count, plural, one {result} other {results}}", "standalone.public_title": "Begiradatxo bat...", - "status.admin_account": "Open moderation interface for @{name}", - "status.admin_status": "Open this status in the moderation interface", + "status.admin_account": "Ireki @{name} erabiltzailearen moderazio interfazea", + "status.admin_status": "Ireki mezu hau moderazio interfazean", "status.block": "Block @{name}", "status.cancel_reblog_private": "Kendu bultzada", "status.cannot_reblog": "Mezu honi ezin zaio bultzada eman", + "status.copy": "Copy link to status", "status.delete": "Ezabatu", "status.detailed_status": "Elkarrizketaren ikuspegi xehetsua", "status.direct": "Mezu zuzena @{name}(r)i", @@ -342,6 +343,7 @@ "ui.beforeunload": "Zure zirriborroa galduko da Mastodon uzten baduzu.", "upload_area.title": "Arrastatu eta jaregin igotzeko", "upload_button.label": "Gehitu multimedia (JPEG, PNG, GIF, WebM, MP4, MOV)", + "upload_error.limit": "File upload limit exceeded.", "upload_form.description": "Deskribatu ikusmen arazoak dituztenentzat", "upload_form.focus": "Aldatu aurrebista", "upload_form.undo": "Ezabatu", diff --git a/app/javascript/mastodon/locales/fa.json b/app/javascript/mastodon/locales/fa.json index b11d88d87..44c39802a 100644 --- a/app/javascript/mastodon/locales/fa.json +++ b/app/javascript/mastodon/locales/fa.json @@ -297,6 +297,7 @@ "status.block": "مسدودسازی @{name}", "status.cancel_reblog_private": "حذف بازبوق", "status.cannot_reblog": "این نوشته را نمیشود بازبوقید", + "status.copy": "Copy link to status", "status.delete": "پاککردن", "status.detailed_status": "نمایش کامل گفتگو", "status.direct": "پیغام مستقیم به @{name}", @@ -342,6 +343,7 @@ "ui.beforeunload": "اگر از ماستدون خارج شوید پیشنویس شما پاک خواهد شد.", "upload_area.title": "برای بارگذاری به اینجا بکشید", "upload_button.label": "افزودن عکس و ویدیو (JPEG, PNG, GIF, WebM, MP4, MOV)", + "upload_error.limit": "File upload limit exceeded.", "upload_form.description": "نوشتهٔ توضیحی برای کمبینایان و نابینایان", "upload_form.focus": "بریدن لبهها", "upload_form.undo": "حذف", diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json index c8d258672..6e878d7ff 100644 --- a/app/javascript/mastodon/locales/fi.json +++ b/app/javascript/mastodon/locales/fi.json @@ -297,6 +297,7 @@ "status.block": "Estä @{name}", "status.cancel_reblog_private": "Peru buustaus", "status.cannot_reblog": "Tätä julkaisua ei voi buustata", + "status.copy": "Copy link to status", "status.delete": "Poista", "status.detailed_status": "Detailed conversation view", "status.direct": "Viesti käyttäjälle @{name}", @@ -342,6 +343,7 @@ "ui.beforeunload": "Luonnos häviää, jos poistut Mastodonista.", "upload_area.title": "Lataa raahaamalla ja pudottamalla tähän", "upload_button.label": "Lisää mediaa", + "upload_error.limit": "File upload limit exceeded.", "upload_form.description": "Anna kuvaus näkörajoitteisia varten", "upload_form.focus": "Rajaa", "upload_form.undo": "Peru", diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json index 762896887..24182c00b 100644 --- a/app/javascript/mastodon/locales/fr.json +++ b/app/javascript/mastodon/locales/fr.json @@ -297,6 +297,7 @@ "status.block": "Block @{name}", "status.cancel_reblog_private": "Dé-booster", "status.cannot_reblog": "Cette publication ne peut être boostée", + "status.copy": "Copy link to status", "status.delete": "Effacer", "status.detailed_status": "Vue détaillée de la conversation", "status.direct": "Envoyer un message direct à @{name}", @@ -342,6 +343,7 @@ "ui.beforeunload": "Votre brouillon sera perdu si vous quittez Mastodon.", "upload_area.title": "Glissez et déposez pour envoyer", "upload_button.label": "Joindre un média (JPEG, PNG, GIF, WebM, MP4, MOV)", + "upload_error.limit": "File upload limit exceeded.", "upload_form.description": "Décrire pour les malvoyant·e·s", "upload_form.focus": "Modifier l’aperçu", "upload_form.undo": "Supprimer", diff --git a/app/javascript/mastodon/locales/gl.json b/app/javascript/mastodon/locales/gl.json index c1ece163d..45bc2d36c 100644 --- a/app/javascript/mastodon/locales/gl.json +++ b/app/javascript/mastodon/locales/gl.json @@ -297,6 +297,7 @@ "status.block": "Block @{name}", "status.cancel_reblog_private": "Non promover", "status.cannot_reblog": "Esta mensaxe non pode ser promovida", + "status.copy": "Copy link to status", "status.delete": "Eliminar", "status.detailed_status": "Vista detallada da conversa", "status.direct": "Mensaxe directa @{name}", @@ -342,6 +343,7 @@ "ui.beforeunload": "O borrador perderase se sae de Mastodon.", "upload_area.title": "Arrastre e solte para subir", "upload_button.label": "Engadir medios (JPEG, PNG, GIF, WebM, MP4, MOV)", + "upload_error.limit": "File upload limit exceeded.", "upload_form.description": "Describa para deficientes visuais", "upload_form.focus": "Cambiar vista previa", "upload_form.undo": "Eliminar", diff --git a/app/javascript/mastodon/locales/he.json b/app/javascript/mastodon/locales/he.json index e27e7f09e..13278382f 100644 --- a/app/javascript/mastodon/locales/he.json +++ b/app/javascript/mastodon/locales/he.json @@ -297,6 +297,7 @@ "status.block": "Block @{name}", "status.cancel_reblog_private": "Unboost", "status.cannot_reblog": "לא ניתן להדהד הודעה זו", + "status.copy": "Copy link to status", "status.delete": "מחיקה", "status.detailed_status": "Detailed conversation view", "status.direct": "Direct message @{name}", @@ -342,6 +343,7 @@ "ui.beforeunload": "הטיוטא תאבד אם תעזבו את מסטודון.", "upload_area.title": "ניתן להעלות על ידי Drag & drop", "upload_button.label": "הוספת מדיה", + "upload_error.limit": "File upload limit exceeded.", "upload_form.description": "תיאור לכבדי ראיה", "upload_form.focus": "Crop", "upload_form.undo": "ביטול", diff --git a/app/javascript/mastodon/locales/hr.json b/app/javascript/mastodon/locales/hr.json index 71dd5319e..a4aafd8fb 100644 --- a/app/javascript/mastodon/locales/hr.json +++ b/app/javascript/mastodon/locales/hr.json @@ -297,6 +297,7 @@ "status.block": "Block @{name}", "status.cancel_reblog_private": "Unboost", "status.cannot_reblog": "Ovaj post ne može biti boostan", + "status.copy": "Copy link to status", "status.delete": "Obriši", "status.detailed_status": "Detailed conversation view", "status.direct": "Direct message @{name}", @@ -342,6 +343,7 @@ "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "upload_area.title": "Povuci i spusti kako bi uploadao", "upload_button.label": "Dodaj media", + "upload_error.limit": "File upload limit exceeded.", "upload_form.description": "Describe for the visually impaired", "upload_form.focus": "Crop", "upload_form.undo": "Poništi", diff --git a/app/javascript/mastodon/locales/hu.json b/app/javascript/mastodon/locales/hu.json index c2842aea7..41734b58d 100644 --- a/app/javascript/mastodon/locales/hu.json +++ b/app/javascript/mastodon/locales/hu.json @@ -297,6 +297,7 @@ "status.block": "Block @{name}", "status.cancel_reblog_private": "Unboost", "status.cannot_reblog": "Ezen státusz nem rebloggolható", + "status.copy": "Copy link to status", "status.delete": "Törlés", "status.detailed_status": "Detailed conversation view", "status.direct": "Direct message @{name}", @@ -342,6 +343,7 @@ "ui.beforeunload": "A piszkozata el fog vesztődni ha elhagyja Mastodon-t.", "upload_area.title": "Húzza ide a feltöltéshez", "upload_button.label": "Média hozzáadása", + "upload_error.limit": "File upload limit exceeded.", "upload_form.description": "Describe for the visually impaired", "upload_form.focus": "Crop", "upload_form.undo": "Mégsem", diff --git a/app/javascript/mastodon/locales/hy.json b/app/javascript/mastodon/locales/hy.json index 691994887..2169bb990 100644 --- a/app/javascript/mastodon/locales/hy.json +++ b/app/javascript/mastodon/locales/hy.json @@ -297,6 +297,7 @@ "status.block": "Արգելափակել @{name}֊ին", "status.cancel_reblog_private": "Unboost", "status.cannot_reblog": "Այս թութը չի կարող տարածվել", + "status.copy": "Copy link to status", "status.delete": "Ջնջել", "status.detailed_status": "Detailed conversation view", "status.direct": "Direct message @{name}", @@ -342,6 +343,7 @@ "ui.beforeunload": "Քո սեւագիրը կկորի, եթե լքես Մաստոդոնը։", "upload_area.title": "Քաշիր ու նետիր՝ վերբեռնելու համար", "upload_button.label": "Ավելացնել մեդիա", + "upload_error.limit": "File upload limit exceeded.", "upload_form.description": "Նկարագրություն ավելացրու տեսողական խնդիրներ ունեցողների համար", "upload_form.focus": "Crop", "upload_form.undo": "Հետարկել", diff --git a/app/javascript/mastodon/locales/id.json b/app/javascript/mastodon/locales/id.json index eed61af70..c8cd2b3d2 100644 --- a/app/javascript/mastodon/locales/id.json +++ b/app/javascript/mastodon/locales/id.json @@ -297,6 +297,7 @@ "status.block": "Block @{name}", "status.cancel_reblog_private": "Unboost", "status.cannot_reblog": "This post cannot be boosted", + "status.copy": "Copy link to status", "status.delete": "Hapus", "status.detailed_status": "Detailed conversation view", "status.direct": "Direct message @{name}", @@ -342,6 +343,7 @@ "ui.beforeunload": "Naskah anda akan hilang jika anda keluar dari Mastodon.", "upload_area.title": "Seret & lepaskan untuk mengunggah", "upload_button.label": "Tambahkan media", + "upload_error.limit": "File upload limit exceeded.", "upload_form.description": "Deskripsikan untuk mereka yang tidak bisa melihat dengan jelas", "upload_form.focus": "Potong", "upload_form.undo": "Undo", diff --git a/app/javascript/mastodon/locales/io.json b/app/javascript/mastodon/locales/io.json index b26fa6c4a..17f74cbab 100644 --- a/app/javascript/mastodon/locales/io.json +++ b/app/javascript/mastodon/locales/io.json @@ -297,6 +297,7 @@ "status.block": "Block @{name}", "status.cancel_reblog_private": "Unboost", "status.cannot_reblog": "This post cannot be boosted", + "status.copy": "Copy link to status", "status.delete": "Efacar", "status.detailed_status": "Detailed conversation view", "status.direct": "Direct message @{name}", @@ -342,6 +343,7 @@ "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "upload_area.title": "Tranar faligar por kargar", "upload_button.label": "Adjuntar kontenajo", + "upload_error.limit": "File upload limit exceeded.", "upload_form.description": "Describe for the visually impaired", "upload_form.focus": "Crop", "upload_form.undo": "Desfacar", diff --git a/app/javascript/mastodon/locales/it.json b/app/javascript/mastodon/locales/it.json index 1f52d3724..423efdbc3 100644 --- a/app/javascript/mastodon/locales/it.json +++ b/app/javascript/mastodon/locales/it.json @@ -292,11 +292,12 @@ "search_results.statuses": "Toot", "search_results.total": "{count} {count, plural, one {risultato} other {risultati}}", "standalone.public_title": "Un'occhiata all'interno...", - "status.admin_account": "Open moderation interface for @{name}", - "status.admin_status": "Open this status in the moderation interface", + "status.admin_account": "Apri interfaccia di moderazione per @{name}", + "status.admin_status": "Apri questo status nell'interfaccia di moderazione", "status.block": "Block @{name}", "status.cancel_reblog_private": "Annulla condivisione", "status.cannot_reblog": "Questo post non può essere condiviso", + "status.copy": "Copy link to status", "status.delete": "Elimina", "status.detailed_status": "Vista conversazione dettagliata", "status.direct": "Messaggio diretto @{name}", @@ -342,6 +343,7 @@ "ui.beforeunload": "La bozza andrà persa se esci da Mastodon.", "upload_area.title": "Trascina per caricare", "upload_button.label": "Aggiungi file multimediale", + "upload_error.limit": "File upload limit exceeded.", "upload_form.description": "Descrizione per utenti con disabilità visive", "upload_form.focus": "Modifica anteprima", "upload_form.undo": "Cancella", diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json index e0cba7764..35ccfe739 100644 --- a/app/javascript/mastodon/locales/ja.json +++ b/app/javascript/mastodon/locales/ja.json @@ -132,7 +132,7 @@ "empty_column.lists": "まだリストがありません。リストを作るとここに表示されます。", "empty_column.mutes": "まだ誰もミュートしていません。", "empty_column.notifications": "まだ通知がありません。他の人とふれ合って会話を始めましょう。", - "empty_column.public": "ここにはまだ何もありません! 公開で何かを投稿したり、他のインスタンスのユーザーをフォローしたりしていっぱいにしましょう", + "empty_column.public": "ここにはまだ何もありません! 公開で何かを投稿したり、他のサーバーのユーザーをフォローしたりしていっぱいにしましょう", "follow_request.authorize": "許可", "follow_request.reject": "拒否", "getting_started.developers": "開発", @@ -228,7 +228,7 @@ "navigation_bar.favourites": "お気に入り", "navigation_bar.filters": "フィルター設定", "navigation_bar.follow_requests": "フォローリクエスト", - "navigation_bar.info": "このインスタンスについて", + "navigation_bar.info": "このサーバーについて", "navigation_bar.keyboard_shortcuts": "ホットキー", "navigation_bar.lists": "リスト", "navigation_bar.logout": "ログアウト", @@ -280,8 +280,8 @@ "relative_time.seconds": "{number}秒前", "reply_indicator.cancel": "キャンセル", "report.forward": "{target} に転送する", - "report.forward_hint": "このアカウントは別のインスタンスに所属しています。通報内容を匿名で転送しますか?", - "report.hint": "通報内容はあなたのインスタンスのモデレーターへ送信されます。通報理由を入力してください。:", + "report.forward_hint": "このアカウントは別のサーバーに所属しています。通報内容を匿名で転送しますか?", + "report.hint": "通報内容はあなたのサーバーのモデレーターへ送信されます。通報理由を入力してください。:", "report.placeholder": "追加コメント", "report.submit": "通報する", "report.target": "{target}さんを通報する", @@ -302,6 +302,7 @@ "status.block": "@{name}さんをブロック", "status.cancel_reblog_private": "ブースト解除", "status.cannot_reblog": "この投稿はブーストできません", + "status.copy": "Copy link to status", "status.delete": "削除", "status.detailed_status": "詳細な会話ビュー", "status.direct": "@{name}さんにダイレクトメッセージ", @@ -347,6 +348,7 @@ "ui.beforeunload": "Mastodonから離れると送信前の投稿は失われます。", "upload_area.title": "ドラッグ&ドロップでアップロード", "upload_button.label": "メディアを追加 (JPEG, PNG, GIF, WebM, MP4, MOV)", + "upload_error.limit": "アップロードできる上限を超えています。", "upload_form.description": "視覚障害者のための説明", "upload_form.focus": "焦点", "upload_form.undo": "削除", diff --git a/app/javascript/mastodon/locales/ka.json b/app/javascript/mastodon/locales/ka.json index 93a11027a..e2e7306fd 100644 --- a/app/javascript/mastodon/locales/ka.json +++ b/app/javascript/mastodon/locales/ka.json @@ -297,6 +297,7 @@ "status.block": "დაბლოკე @{name}", "status.cancel_reblog_private": "ბუსტის მოშორება", "status.cannot_reblog": "ეს პოსტი ვერ დაიბუსტება", + "status.copy": "Copy link to status", "status.delete": "წაშლა", "status.detailed_status": "Detailed conversation view", "status.direct": "პირდაპირი წერილი @{name}-ს", @@ -342,6 +343,7 @@ "ui.beforeunload": "თქვენი დრაფტი გაუქმდება თუ დატოვებთ მასტოდონს.", "upload_area.title": "გადმოწიეთ და ჩააგდეთ ასატვირთათ", "upload_button.label": "მედიის დამატება", + "upload_error.limit": "File upload limit exceeded.", "upload_form.description": "აღწერილობა ვიზუალურად უფასურისთვის", "upload_form.focus": "კროპი", "upload_form.undo": "გაუქმება", diff --git a/app/javascript/mastodon/locales/ko.json b/app/javascript/mastodon/locales/ko.json index ceb474a3e..6d607068e 100644 --- a/app/javascript/mastodon/locales/ko.json +++ b/app/javascript/mastodon/locales/ko.json @@ -297,6 +297,7 @@ "status.block": "@{name} 차단", "status.cancel_reblog_private": "부스트 취소", "status.cannot_reblog": "이 포스트는 부스트 할 수 없습니다", + "status.copy": "Copy link to status", "status.delete": "삭제", "status.detailed_status": "대화 자세히 보기", "status.direct": "@{name}에게 다이렉트 메시지", @@ -342,6 +343,7 @@ "ui.beforeunload": "지금 나가면 저장되지 않은 항목을 잃게 됩니다.", "upload_area.title": "드래그 & 드롭으로 업로드", "upload_button.label": "미디어 추가 (JPEG, PNG, GIF, WebM, MP4, MOV)", + "upload_error.limit": "File upload limit exceeded.", "upload_form.description": "시각장애인을 위한 설명", "upload_form.focus": "미리보기 변경", "upload_form.undo": "삭제", diff --git a/app/javascript/mastodon/locales/lv.json b/app/javascript/mastodon/locales/lv.json index 0d510d011..dbea1d6dc 100644 --- a/app/javascript/mastodon/locales/lv.json +++ b/app/javascript/mastodon/locales/lv.json @@ -1,136 +1,136 @@ { - "account.add_or_remove_from_list": "Add or Remove from lists", - "account.badges.bot": "Bot", - "account.block": "Block @{name}", - "account.block_domain": "Hide everything from {domain}", - "account.blocked": "Blocked", - "account.direct": "Direct message @{name}", - "account.disclaimer_full": "Information below may reflect the user's profile incompletely.", - "account.domain_blocked": "Domain hidden", - "account.edit_profile": "Edit profile", - "account.endorse": "Feature on profile", - "account.follow": "Follow", - "account.followers": "Followers", - "account.followers.empty": "No one follows this user yet.", - "account.follows": "Follows", - "account.follows.empty": "This user doesn't follow anyone yet.", - "account.follows_you": "Follows you", - "account.hide_reblogs": "Hide boosts from @{name}", - "account.link_verified_on": "Ownership of this link was checked on {date}", - "account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.", - "account.media": "Media", - "account.mention": "Mention @{name}", - "account.moved_to": "{name} has moved to:", - "account.mute": "Mute @{name}", - "account.mute_notifications": "Mute notifications from @{name}", - "account.muted": "Muted", - "account.posts": "Toots", - "account.posts_with_replies": "Toots and replies", - "account.report": "Report @{name}", - "account.requested": "Awaiting approval. Click to cancel follow request", - "account.share": "Share @{name}'s profile", - "account.show_reblogs": "Show boosts from @{name}", - "account.unblock": "Unblock @{name}", - "account.unblock_domain": "Unhide {domain}", - "account.unendorse": "Don't feature on profile", - "account.unfollow": "Unfollow", - "account.unmute": "Unmute @{name}", - "account.unmute_notifications": "Unmute notifications from @{name}", - "account.view_full_profile": "View full profile", - "alert.unexpected.message": "An unexpected error occurred.", - "alert.unexpected.title": "Oops!", - "boost_modal.combo": "You can press {combo} to skip this next time", - "bundle_column_error.body": "Something went wrong while loading this component.", - "bundle_column_error.retry": "Try again", - "bundle_column_error.title": "Network error", - "bundle_modal_error.close": "Close", - "bundle_modal_error.message": "Something went wrong while loading this component.", - "bundle_modal_error.retry": "Try again", - "column.blocks": "Blocked users", - "column.community": "Local timeline", - "column.direct": "Direct messages", - "column.domain_blocks": "Hidden domains", - "column.favourites": "Favourites", - "column.follow_requests": "Follow requests", - "column.home": "Home", - "column.lists": "Lists", - "column.mutes": "Muted users", - "column.notifications": "Notifications", - "column.pins": "Pinned toot", - "column.public": "Federated timeline", - "column_back_button.label": "Back", - "column_header.hide_settings": "Hide settings", - "column_header.moveLeft_settings": "Move column to the left", - "column_header.moveRight_settings": "Move column to the right", - "column_header.pin": "Pin", - "column_header.show_settings": "Show settings", - "column_header.unpin": "Unpin", - "column_subheading.settings": "Settings", - "community.column_settings.media_only": "Media Only", - "compose_form.direct_message_warning": "This toot will only be sent to all the mentioned users.", - "compose_form.direct_message_warning_learn_more": "Learn more", - "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.", - "compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.", - "compose_form.lock_disclaimer.lock": "locked", - "compose_form.placeholder": "What is on your mind?", - "compose_form.publish": "Toot", + "account.add_or_remove_from_list": "Pievienot vai noņemt no saraksta", + "account.badges.bot": "Bots", + "account.block": "Bloķēt @{name}", + "account.block_domain": "Slēpt visu no {domain}", + "account.blocked": "Bloķēts", + "account.direct": "Privātā ziņa @{name}", + "account.disclaimer_full": "Informācija zemāk var nepilnīgi atspoguļot lietotāja profilu.", + "account.domain_blocked": "Domēns ir paslēpts", + "account.edit_profile": "Labot profilu", + "account.endorse": "Izcelts profilā", + "account.follow": "Sekot", + "account.followers": "Sekotāji", + "account.followers.empty": "Šim lietotājam nav sekotāju.", + "account.follows": "Seko", + "account.follows.empty": "Šis lietotājs pagaidām nevienam neseko.", + "account.follows_you": "Seko tev", + "account.hide_reblogs": "Paslēpt paceltos ierakstus no lietotāja @{name}", + "account.link_verified_on": "Šīs saites piederība ir pārbaudīta {date}", + "account.locked_info": "Šī konta privātuma status ir iestatīts slēgts. Īpašnieks izskatīs un izvēlēsies kas viņam drīkst sekot.", + "account.media": "Mēdiji", + "account.mention": "Piemin @{name}", + "account.moved_to": "{name} ir pārvācies uz:", + "account.mute": "Apklusināt @{name}", + "account.mute_notifications": "Nerādīt paziņojumus no @{name}", + "account.muted": "Apklusināts", + "account.posts": "Ieraksti", + "account.posts_with_replies": "Ieraksti un atbildes", + "account.report": "Ziņot par lietotāju @{name}", + "account.requested": "Gaidām apstiprinājumu. Nospied lai atceltu sekošanas pieparasījumu", + "account.share": "Dalīties ar lietotāja @{name}'s profilu", + "account.show_reblogs": "Parādīt lietotāja @{name} paceltos ierakstus", + "account.unblock": "Atbloķēt lietotāju @{name}", + "account.unblock_domain": "Atbloķēt domēnu {domain}", + "account.unendorse": "Neizcelt profilā", + "account.unfollow": "Nesekot", + "account.unmute": "Noņemt apklusinājumu no lietotāja @{name}", + "account.unmute_notifications": "Rādīt paziņojumus no lietotāja @{name}", + "account.view_full_profile": "Apskatīt pilnu profilu", + "alert.unexpected.message": "Negaidīta kļūda.", + "alert.unexpected.title": "Ups!", + "boost_modal.combo": "Nospied {combo} lai izlaistu šo nākamreiz", + "bundle_column_error.body": "Kaut kas nogāja greizi ielādējot šo komponenti.", + "bundle_column_error.retry": "Mēģini vēlreiz", + "bundle_column_error.title": "Tīkla kļūda", + "bundle_modal_error.close": "Aizvērt", + "bundle_modal_error.message": "Kaut kas nogāja greizi ielādējot šo komponenti.", + "bundle_modal_error.retry": "Mēģini vēlreiz", + "column.blocks": "Bloķētie lietotāji", + "column.community": "Lokālā laika līnija", + "column.direct": "Privātās ziņas", + "column.domain_blocks": "Paslēptie domēni", + "column.favourites": "Favorīti", + "column.follow_requests": "Sekotāju pieprasījumi", + "column.home": "Sākums", + "column.lists": "Saraksti", + "column.mutes": "Apklusinātie lietotāji", + "column.notifications": "Paziņojumi", + "column.pins": "Piespraustie ziņojumi", + "column.public": "Federatīvā laika līnija", + "column_back_button.label": "Atpakaļ", + "column_header.hide_settings": "Paslēpt iestatījumus", + "column_header.moveLeft_settings": "Pārvietot kolonu pa kreisi", + "column_header.moveRight_settings": "Pārvietot kolonu pa labi", + "column_header.pin": "Piespraust", + "column_header.show_settings": "Rādīt iestatījumus", + "column_header.unpin": "Atspraust", + "column_subheading.settings": "Iestatījumi", + "community.column_settings.media_only": "Tikai mēdiji", + "compose_form.direct_message_warning": "Šis ziņojums tiks nosūtīts tikai pieminētajiem lietotājiem.", + "compose_form.direct_message_warning_learn_more": "Papildus informācija", + "compose_form.hashtag_warning": "Ziņojumu nebūs iespējams atrast zem haštagiem jo tas nav publisks. Tikai publiskos ziņojumus ir iespējams meklēt pēc tiem.", + "compose_form.lock_disclaimer": "Tavs konts nav {locked}. Ikviens var Tev sekot lai apskatītu tikai sekotājiem paredzētos ziņojumus.", + "compose_form.lock_disclaimer.lock": "slēgts", + "compose_form.placeholder": "Ko vēlies publicēt?", + "compose_form.publish": "Publicēt", "compose_form.publish_loud": "{publish}!", - "compose_form.sensitive.marked": "Media is marked as sensitive", - "compose_form.sensitive.unmarked": "Media is not marked as sensitive", - "compose_form.spoiler.marked": "Text is hidden behind warning", - "compose_form.spoiler.unmarked": "Text is not hidden", - "compose_form.spoiler_placeholder": "Write your warning here", - "confirmation_modal.cancel": "Cancel", - "confirmations.block.confirm": "Block", - "confirmations.block.message": "Are you sure you want to block {name}?", - "confirmations.delete.confirm": "Delete", - "confirmations.delete.message": "Are you sure you want to delete this status?", - "confirmations.delete_list.confirm": "Delete", - "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?", - "confirmations.domain_block.confirm": "Hide entire domain", - "confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.", - "confirmations.mute.confirm": "Mute", - "confirmations.mute.message": "Are you sure you want to mute {name}?", - "confirmations.redraft.confirm": "Delete & redraft", - "confirmations.redraft.message": "Are you sure you want to delete this status and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.", - "confirmations.reply.confirm": "Reply", - "confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?", - "confirmations.unfollow.confirm": "Unfollow", - "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?", - "embed.instructions": "Embed this status on your website by copying the code below.", - "embed.preview": "Here is what it will look like:", - "emoji_button.activity": "Activity", - "emoji_button.custom": "Custom", - "emoji_button.flags": "Flags", - "emoji_button.food": "Food & Drink", - "emoji_button.label": "Insert emoji", - "emoji_button.nature": "Nature", - "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻", - "emoji_button.objects": "Objects", - "emoji_button.people": "People", - "emoji_button.recent": "Frequently used", - "emoji_button.search": "Search...", - "emoji_button.search_results": "Search results", - "emoji_button.symbols": "Symbols", - "emoji_button.travel": "Travel & Places", - "empty_column.account_timeline": "No toots here!", - "empty_column.blocks": "You haven't blocked any users yet.", - "empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!", - "empty_column.direct": "You don't have any direct messages yet. When you send or receive one, it will show up here.", - "empty_column.domain_blocks": "There are no hidden domains yet.", - "empty_column.favourited_statuses": "You don't have any favourite toots yet. When you favourite one, it will show up here.", - "empty_column.favourites": "No one has favourited this toot yet. When someone does, they will show up here.", - "empty_column.follow_requests": "You don't have any follow requests yet. When you receive one, it will show up here.", - "empty_column.hashtag": "There is nothing in this hashtag yet.", - "empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.", - "empty_column.home.public_timeline": "the public timeline", - "empty_column.list": "There is nothing in this list yet. When members of this list post new statuses, they will appear here.", - "empty_column.lists": "You don't have any lists yet. When you create one, it will show up here.", - "empty_column.mutes": "You haven't muted any users yet.", - "empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.", - "empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up", - "follow_request.authorize": "Authorize", - "follow_request.reject": "Reject", + "compose_form.sensitive.marked": "Mēdijs ir atzīmēts kā sensitīvs", + "compose_form.sensitive.unmarked": "Mēdijs nav atzīmēts kā sensitīvs", + "compose_form.spoiler.marked": "Teksts ir paslēpts aiz brīdinājuma", + "compose_form.spoiler.unmarked": "Teksts nav paslēpts", + "compose_form.spoiler_placeholder": "Ieraksti Savu brīdinājuma tekstu šeit", + "confirmation_modal.cancel": "Atcelt", + "confirmations.block.confirm": "Bloķēt", + "confirmations.block.message": "Vai tiešām vēlies bloķēt lietotāju {name}?", + "confirmations.delete.confirm": "Dzēst", + "confirmations.delete.message": "Vai tiešām vēlies dzēst šo ierakstu?", + "confirmations.delete_list.confirm": "Dzēst", + "confirmations.delete_list.message": "Vai tiešam vēlies neatgriezeniski dzēst šo sarakstu?", + "confirmations.domain_block.confirm": "Paslēpt visu domēnu", + "confirmations.domain_block.message": "Vai tu tiešām, tiešam vēlies bloķēt visu domēnu {domain}? Lielākajā daļā gadījumu pietiek ja nobloķē vai apklusini kādu. Tu neredzēsi saturu vai paziņojumus no šī domēna nevienā laika līnijā. Tavi sekotāji no šī domēna tiks noņemti.", + "confirmations.mute.confirm": "Apklusināt", + "confirmations.mute.message": "Vai Tu tiešām velies apklusināt {name}?", + "confirmations.redraft.confirm": "Dzēst un pārrakstīt", + "confirmations.redraft.message": "Vai tiešām vēlies dzēst un pārrakstīt šo ierakstu? Favorīti un paceltie ieraksti tiks dzēsti, kā arī atbildes tiks atsaistītas no šī ieraksta.", + "confirmations.reply.confirm": "Atbildēt", + "confirmations.reply.message": "Atbildot tagad tava ziņa ko šobrīd raksti tiks pārrakstīta. Vai tiešām vēlies turpināt?", + "confirmations.unfollow.confirm": "Nesekot", + "confirmations.unfollow.message": "Vai tiešam vairs nevēlies sekot lietotājam {name}?", + "embed.instructions": "Iegul šo ziņojumu savā mājaslapā kopējot kodu zemāk.", + "embed.preview": "Tas izskatīsies šādi:", + "emoji_button.activity": "Aktivitāte", + "emoji_button.custom": "Pielāgots", + "emoji_button.flags": "Karogi", + "emoji_button.food": "Ēdieni un dzērieni", + "emoji_button.label": "Ielikt emoji smaidiņu", + "emoji_button.nature": "Daba", + "emoji_button.not_found": "Nekādu emodžīšu!! (╯°□°)╯︵ ┻━┻", + "emoji_button.objects": "Objekti", + "emoji_button.people": "Cilvēki", + "emoji_button.recent": "Biežāk lietotie", + "emoji_button.search": "Meklēt...", + "emoji_button.search_results": "Meklēšanas rezultāti", + "emoji_button.symbols": "Simboli", + "emoji_button.travel": "Ceļošana & Vietas", + "empty_column.account_timeline": "Šeit ziņojumu nav!", + "empty_column.blocks": "Tu neesi vēl nevienu bloķējis.", + "empty_column.community": "Lokālā laika līnija ir tukša. :/ Ieraksti kaut ko lai sākas rosība!", + "empty_column.direct": "Tev nav privāto ziņu. Tiklīdz saņemsi tās šeit parādīsies.", + "empty_column.domain_blocks": "Slēpto domēnu vēl nav.", + "empty_column.favourited_statuses": "Tev vēl nav iemīļoto ziņojumu. Kad Tev tādu būs tie šeit parādīsies.", + "empty_column.favourites": "Neviens šo ziņojumu nav pievienojis favorītiem. Kad tādu būs tie šeit parādīsies.", + "empty_column.follow_requests": "Šobrīd neviens nav pieteicies tev sekot. Kad kāds pieteiksies tas parādīsies šeit.", + "empty_column.hashtag": "Ar šo haštagu nekas nav atrodams.", + "empty_column.home": "Tava laika līnija ir tukša! Apmeklē federatīvo laika līniju vai uzmeklē kādu meklētājā lai satiktu citus.", + "empty_column.home.public_timeline": "publiskā laika līnija", + "empty_column.list": "Šis saraksts ir tukšs. Kad šī saraksta dalībnieki atjaunos statusu tas parādīsies šeit.", + "empty_column.lists": "Tev nav neviena saraksta. Kad tādu būs tie parādīsies šeit.", + "empty_column.mutes": "Tu neesi nevienu apklusinājis.", + "empty_column.notifications": "Tev nav paziņojumu. Iesaisties sarunās ar citiem.", + "empty_column.public": "Šeit nekā nav, tukšums! Ieraksti kaut ko publiski, vai uzmeklē un seko kādam no citas instances", + "follow_request.authorize": "Autorizēt", + "follow_request.reject": "Noraidīt", "getting_started.developers": "Developers", "getting_started.directory": "Profile directory", "getting_started.documentation": "Documentation", @@ -297,6 +297,7 @@ "status.block": "Block @{name}", "status.cancel_reblog_private": "Unboost", "status.cannot_reblog": "This post cannot be boosted", + "status.copy": "Copy link to status", "status.delete": "Delete", "status.detailed_status": "Detailed conversation view", "status.direct": "Direct message @{name}", @@ -342,6 +343,7 @@ "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "upload_area.title": "Drag & drop to upload", "upload_button.label": "Add media (JPEG, PNG, GIF, WebM, MP4, MOV)", + "upload_error.limit": "File upload limit exceeded.", "upload_form.description": "Describe for the visually impaired", "upload_form.focus": "Crop", "upload_form.undo": "Delete", diff --git a/app/javascript/mastodon/locales/ms.json b/app/javascript/mastodon/locales/ms.json index 0d510d011..602a59ca0 100644 --- a/app/javascript/mastodon/locales/ms.json +++ b/app/javascript/mastodon/locales/ms.json @@ -297,6 +297,7 @@ "status.block": "Block @{name}", "status.cancel_reblog_private": "Unboost", "status.cannot_reblog": "This post cannot be boosted", + "status.copy": "Copy link to status", "status.delete": "Delete", "status.detailed_status": "Detailed conversation view", "status.direct": "Direct message @{name}", @@ -342,6 +343,7 @@ "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "upload_area.title": "Drag & drop to upload", "upload_button.label": "Add media (JPEG, PNG, GIF, WebM, MP4, MOV)", + "upload_error.limit": "File upload limit exceeded.", "upload_form.description": "Describe for the visually impaired", "upload_form.focus": "Crop", "upload_form.undo": "Delete", diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json index 871142195..92f94ddc0 100644 --- a/app/javascript/mastodon/locales/nl.json +++ b/app/javascript/mastodon/locales/nl.json @@ -297,6 +297,7 @@ "status.block": "Blokkeer @{name}", "status.cancel_reblog_private": "Niet langer boosten", "status.cannot_reblog": "Deze toot kan niet geboost worden", + "status.copy": "Copy link to status", "status.delete": "Verwijderen", "status.detailed_status": "Uitgebreide gespreksweergave", "status.direct": "Directe toot @{name}", @@ -342,6 +343,7 @@ "ui.beforeunload": "Je concept zal verloren gaan als je Mastodon verlaat.", "upload_area.title": "Hierin slepen om te uploaden", "upload_button.label": "Media toevoegen (JPEG, PNG, GIF, WebM, MP4, MOV)", + "upload_error.limit": "File upload limit exceeded.", "upload_form.description": "Omschrijf dit voor mensen met een visuele beperking", "upload_form.focus": "Voorvertoning aanpassen", "upload_form.undo": "Verwijderen", diff --git a/app/javascript/mastodon/locales/no.json b/app/javascript/mastodon/locales/no.json index fa08e8d73..f6910f0e6 100644 --- a/app/javascript/mastodon/locales/no.json +++ b/app/javascript/mastodon/locales/no.json @@ -297,6 +297,7 @@ "status.block": "Block @{name}", "status.cancel_reblog_private": "Unboost", "status.cannot_reblog": "Denne posten kan ikke fremheves", + "status.copy": "Copy link to status", "status.delete": "Slett", "status.detailed_status": "Detailed conversation view", "status.direct": "Direct message @{name}", @@ -342,6 +343,7 @@ "ui.beforeunload": "Din kladd vil bli forkastet om du forlater Mastodon.", "upload_area.title": "Dra og slipp for å laste opp", "upload_button.label": "Legg til media", + "upload_error.limit": "File upload limit exceeded.", "upload_form.description": "Beskriv for synshemmede", "upload_form.focus": "Crop", "upload_form.undo": "Angre", diff --git a/app/javascript/mastodon/locales/oc.json b/app/javascript/mastodon/locales/oc.json index c28e9f5b8..8250d59bd 100644 --- a/app/javascript/mastodon/locales/oc.json +++ b/app/javascript/mastodon/locales/oc.json @@ -251,7 +251,7 @@ "notifications.column_settings.reblog": "Partatges :", "notifications.column_settings.show": "Mostrar dins la colomna", "notifications.column_settings.sound": "Emetre un son", - "notifications.filter.all": "Totes", + "notifications.filter.all": "Totas", "notifications.filter.boosts": "Partages", "notifications.filter.favourites": "Favorits", "notifications.filter.follows": "Seguiments", @@ -297,6 +297,7 @@ "status.block": "Blocar @{name}", "status.cancel_reblog_private": "Quitar de partejar", "status.cannot_reblog": "Aqueste estatut pòt pas èsser partejat", + "status.copy": "Copy link to status", "status.delete": "Escafar", "status.detailed_status": "Vista detalhada de la convèrsa", "status.direct": "Messatge per @{name}", @@ -342,6 +343,7 @@ "ui.beforeunload": "Vòstre brolhon serà perdut se quitatz Mastodon.", "upload_area.title": "Lisatz e depausatz per mandar", "upload_button.label": "Ajustar un mèdia (JPEG, PNG, GIF, WebM, MP4, MOV)", + "upload_error.limit": "File upload limit exceeded.", "upload_form.description": "Descripcion pels mal vesents", "upload_form.focus": "Modificar l’apercebut", "upload_form.undo": "Suprimir", diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json index 9f78c430f..4dd887e3c 100644 --- a/app/javascript/mastodon/locales/pl.json +++ b/app/javascript/mastodon/locales/pl.json @@ -302,6 +302,7 @@ "status.block": "Zablokuj @{name}", "status.cancel_reblog_private": "Cofnij podbicie", "status.cannot_reblog": "Ten wpis nie może zostać podbity", + "status.copy": "Copy link to status", "status.delete": "Usuń", "status.detailed_status": "Szczegółowy widok konwersacji", "status.direct": "Wyślij wiadomość bezpośrednią do @{name}", @@ -347,6 +348,7 @@ "ui.beforeunload": "Utracisz tworzony wpis, jeżeli opuścisz Mastodona.", "upload_area.title": "Przeciągnij i upuść aby wysłać", "upload_button.label": "Dodaj zawartość multimedialną (JPEG, PNG, GIF, WebM, MP4, MOV)", + "upload_error.limit": "Przekroczono limit plików do wysłania.", "upload_form.description": "Wprowadź opis dla niewidomych i niedowidzących", "upload_form.focus": "Dopasuj podgląd", "upload_form.undo": "Usuń", diff --git a/app/javascript/mastodon/locales/pt-BR.json b/app/javascript/mastodon/locales/pt-BR.json index 392e7f485..bc3682921 100644 --- a/app/javascript/mastodon/locales/pt-BR.json +++ b/app/javascript/mastodon/locales/pt-BR.json @@ -297,6 +297,7 @@ "status.block": "Block @{name}", "status.cancel_reblog_private": "Desfazer compartilhamento", "status.cannot_reblog": "Esta postagem não pode ser compartilhada", + "status.copy": "Copy link to status", "status.delete": "Excluir", "status.detailed_status": "Visão detalhada da conversa", "status.direct": "Enviar mensagem direta a @{name}", @@ -342,6 +343,7 @@ "ui.beforeunload": "Seu rascunho será perdido se você sair do Mastodon.", "upload_area.title": "Arraste e solte para enviar", "upload_button.label": "Adicionar mídia (JPEG, PNG, GIF, WebM, MP4, MOV)", + "upload_error.limit": "File upload limit exceeded.", "upload_form.description": "Descreva a imagem para deficientes visuais", "upload_form.focus": "Ajustar foco", "upload_form.undo": "Remover", diff --git a/app/javascript/mastodon/locales/pt.json b/app/javascript/mastodon/locales/pt.json index d4126704a..edcfd2fa2 100644 --- a/app/javascript/mastodon/locales/pt.json +++ b/app/javascript/mastodon/locales/pt.json @@ -297,6 +297,7 @@ "status.block": "Block @{name}", "status.cancel_reblog_private": "Unboost", "status.cannot_reblog": "Este post não pode ser partilhado", + "status.copy": "Copy link to status", "status.delete": "Eliminar", "status.detailed_status": "Detailed conversation view", "status.direct": "Direct message @{name}", @@ -342,6 +343,7 @@ "ui.beforeunload": "O teu rascunho vai ser perdido se abandonares o Mastodon.", "upload_area.title": "Arraste e solte para enviar", "upload_button.label": "Adicionar media", + "upload_error.limit": "File upload limit exceeded.", "upload_form.description": "Descrição da imagem para pessoas com dificuldades visuais", "upload_form.focus": "Crop", "upload_form.undo": "Anular", diff --git a/app/javascript/mastodon/locales/ro.json b/app/javascript/mastodon/locales/ro.json index a1a514f49..3be3214ee 100644 --- a/app/javascript/mastodon/locales/ro.json +++ b/app/javascript/mastodon/locales/ro.json @@ -297,6 +297,7 @@ "status.block": "Blochează @{name}", "status.cancel_reblog_private": "Nedistribuit", "status.cannot_reblog": "Această postare nu poate fi redistribuită", + "status.copy": "Copy link to status", "status.delete": "Șterge", "status.detailed_status": "Conversația detailată", "status.direct": "Mesaj direct @{name}", @@ -342,6 +343,7 @@ "ui.beforeunload": "Postarea se va pierde dacă părăsești pagina.", "upload_area.title": "Trage și eliberează pentru a încărca", "upload_button.label": "Adaugă media (JPEG, PNG, GIF, WebM, MP4, MOV)", + "upload_error.limit": "File upload limit exceeded.", "upload_form.description": "Adaugă o descriere pentru persoanele cu deficiențe de vedere", "upload_form.focus": "Schimbă previzualizarea", "upload_form.undo": "Șterge", diff --git a/app/javascript/mastodon/locales/ru.json b/app/javascript/mastodon/locales/ru.json index cb6010898..db9e732c0 100644 --- a/app/javascript/mastodon/locales/ru.json +++ b/app/javascript/mastodon/locales/ru.json @@ -297,6 +297,7 @@ "status.block": "Заблокировать @{name}", "status.cancel_reblog_private": "Не продвигать", "status.cannot_reblog": "Этот статус не может быть продвинут", + "status.copy": "Copy link to status", "status.delete": "Удалить", "status.detailed_status": "Подробный просмотр обсуждения", "status.direct": "Написать @{name}", @@ -342,6 +343,7 @@ "ui.beforeunload": "Ваш черновик будет утерян, если вы покинете Mastodon.", "upload_area.title": "Перетащите сюда, чтобы загрузить", "upload_button.label": "Добавить медиаконтент", + "upload_error.limit": "File upload limit exceeded.", "upload_form.description": "Описать для людей с нарушениями зрения", "upload_form.focus": "Обрезать", "upload_form.undo": "Отменить", diff --git a/app/javascript/mastodon/locales/sk.json b/app/javascript/mastodon/locales/sk.json index a3467785a..8ca913245 100644 --- a/app/javascript/mastodon/locales/sk.json +++ b/app/javascript/mastodon/locales/sk.json @@ -13,7 +13,7 @@ "account.followers": "Sledujúci", "account.followers.empty": "Tohto užívateľa ešte nikto nenásleduje.", "account.follows": "Následuje", - "account.follows.empty": "Tento užívateľ tu ešte nikoho nenásleduje.", + "account.follows.empty": "Tento užívateľ ešte nikoho nenásleduje.", "account.follows_you": "Následuje ťa", "account.hide_reblogs": "Skryť povýšenia od @{name}", "account.link_verified_on": "Vlastníctvo tohto odkazu bolo skontrolované {date}", @@ -35,7 +35,7 @@ "account.unendorse": "Nezobrazuj na profile", "account.unfollow": "Prestaň následovať", "account.unmute": "Prestaň ignorovať @{name}", - "account.unmute_notifications": "Odtĺm oboznámenia od @{name}", + "account.unmute_notifications": "Zrušiť stlmenie oznámení od @{name}", "account.view_full_profile": "Pozri celý profil", "alert.unexpected.message": "Vyskytla sa nečakaná chyba.", "alert.unexpected.title": "Oops!", @@ -60,9 +60,9 @@ "column.public": "Federovaná časová os", "column_back_button.label": "Späť", "column_header.hide_settings": "Skryť nastavenia", - "column_header.moveLeft_settings": "Presunúť stĺpec doľava", - "column_header.moveRight_settings": "Presunúť stĺpec doprava", - "column_header.pin": "Pripnúť", + "column_header.moveLeft_settings": "Presuň stĺpec doľava", + "column_header.moveRight_settings": "Presuň stĺpec doprava", + "column_header.pin": "Pripni", "column_header.show_settings": "Ukáž nastavenia", "column_header.unpin": "Odopnúť", "column_subheading.settings": "Nastavenia", @@ -79,14 +79,14 @@ "compose_form.sensitive.unmarked": "Médiálny obsah nieje označený ako chúlostivý", "compose_form.spoiler.marked": "Text je ukrytý za varovaním", "compose_form.spoiler.unmarked": "Text nieje ukrytý", - "compose_form.spoiler_placeholder": "Sem napíšte vaše varovanie", - "confirmation_modal.cancel": "Zrušiť", - "confirmations.block.confirm": "Blokovať", - "confirmations.block.message": "Si si istý, že chcete blokovať {name}?", - "confirmations.delete.confirm": "Zmazať", - "confirmations.delete.message": "Si si naozaj istá/ý, že chceš vymazať túto správu?", - "confirmations.delete_list.confirm": "Vymazať", - "confirmations.delete_list.message": "Si si istý/á, že chceš navždy vymazať tento zoznam?", + "compose_form.spoiler_placeholder": "Sem napíš tvoje varovanie", + "confirmation_modal.cancel": "Zruš", + "confirmations.block.confirm": "Blokuj", + "confirmations.block.message": "Si si istý/á, že chceš blokovať {name}?", + "confirmations.delete.confirm": "Vymaž", + "confirmations.delete.message": "Si si istý/á, že chceš vymazať túto správu?", + "confirmations.delete_list.confirm": "Vymaž", + "confirmations.delete_list.message": "Si si istý/á, že chceš natrvalo vymazať tento zoznam?", "confirmations.domain_block.confirm": "Skryť celú doménu", "confirmations.domain_block.message": "Si si naozaj istý, že chceš blokovať celú {domain}? Vo väčšine prípadov stačí blokovať alebo ignorovať pár konkrétnych užívateľov, čo sa doporučuje. Neuvidíš obsah z tejto domény v žiadnej verejnej časovej osi, ani v oznámeniach. Tvoji následovníci pochádzajúci z tejto domény budú odstránení.", "confirmations.mute.confirm": "Ignoruj", @@ -162,7 +162,7 @@ "introduction.interactions.reblog.headline": "Povýš", "introduction.interactions.reblog.text": "Môžeš zdieľať príspevky iných ľudí s vašimi následovateľmi tým, že ich povýšiš.", "introduction.interactions.reply.headline": "Odpovedz", - "introduction.interactions.reply.text": "Odpovedať môžeš na príspevky iných ľudí, aj na svoje vlastné, čím sa sspolu prepoja do konverzácie.", + "introduction.interactions.reply.text": "Odpovedať môžeš na príspevky iných ľudí, aj na svoje vlastné, čím sa spolu prepoja do konverzácie.", "introduction.welcome.action": "Poďme do toho!", "introduction.welcome.headline": "Prvé kroky", "introduction.welcome.text": "Vitaj vo fediverse! Za malú chvíľu budeš môcť posielať správy a rozpovedať sa so svojími priateľmi cez širokú škálu rôznorodých serverov. Ale tento server, {domain}, je špeciálny v tom, že ukladá tvoj profil, takže si jeho názov zapametaj.", @@ -212,7 +212,7 @@ "media_gallery.toggle_visible": "Zapnúť/Vypnúť viditeľnosť", "missing_indicator.label": "Nenájdené", "missing_indicator.sublabel": "Tento zdroj sa ešte nepodarilo nájsť", - "mute_modal.hide_notifications": "Skryť oboznámenia od tohoto užívateľa?", + "mute_modal.hide_notifications": "Skryť oznámenia od tohto používateľa?", "navigation_bar.apps": "Mobilné aplikácie", "navigation_bar.blocks": "Blokovaní užívatelia", "navigation_bar.community_timeline": "Lokálna časová os", @@ -220,11 +220,11 @@ "navigation_bar.direct": "Súkromné správy", "navigation_bar.discover": "Objavuj", "navigation_bar.domain_blocks": "Skryté domény", - "navigation_bar.edit_profile": "Uprav profil", + "navigation_bar.edit_profile": "Upraviť profil", "navigation_bar.favourites": "Obľúbené", "navigation_bar.filters": "Utĺmené slová", "navigation_bar.follow_requests": "Žiadosti o sledovanie", - "navigation_bar.info": "O tomto Mastodon serveri", + "navigation_bar.info": "O tomto serveri", "navigation_bar.keyboard_shortcuts": "Klávesové skratky", "navigation_bar.lists": "Zoznamy", "navigation_bar.logout": "Odhlás sa", @@ -255,10 +255,10 @@ "notifications.filter.boosts": "Vyzdvihnutia", "notifications.filter.favourites": "Obľúbené", "notifications.filter.follows": "Sledovania", - "notifications.filter.mentions": "Spomenutia", - "notifications.group": "{count} oznámenia", - "privacy.change": "Zmeňiť viditeľnosť statusu", - "privacy.direct.long": "Poslať priamo iba spomenutým používateľom", + "notifications.filter.mentions": "Iba spomenutia", + "notifications.group": "{count} oboznámení", + "privacy.change": "Uprav súkromie príspevku", + "privacy.direct.long": "Pošli iba spomenutým používateľom", "privacy.direct.short": "Súkromne", "privacy.private.long": "Poslať iba následovateľom", "privacy.private.short": "Iba pre sledujúcich", @@ -297,6 +297,7 @@ "status.block": "Blokovať @{name}", "status.cancel_reblog_private": "Nezdieľaj", "status.cannot_reblog": "Tento príspevok nemôže byť re-tootnutý", + "status.copy": "Copy link to status", "status.delete": "Zmazať", "status.detailed_status": "Podrobný náhľad celej konverzácie", "status.direct": "Súkromná správa @{name}", @@ -342,6 +343,7 @@ "ui.beforeunload": "Čo máš rozpísané sa stratí, ak opustíš Mastodon.", "upload_area.title": "Pretiahni a pusť pre nahratie", "upload_button.label": "Pridať médiálny súbor (JPEG, PNG, GIF, WebM, MP4, MOV)", + "upload_error.limit": "File upload limit exceeded.", "upload_form.description": "Opis pre slabo vidiacich", "upload_form.focus": "Pozmeň náhľad", "upload_form.undo": "Vymaž", diff --git a/app/javascript/mastodon/locales/sl.json b/app/javascript/mastodon/locales/sl.json index cabad737d..8e5e3deb9 100644 --- a/app/javascript/mastodon/locales/sl.json +++ b/app/javascript/mastodon/locales/sl.json @@ -1,5 +1,5 @@ { - "account.add_or_remove_from_list": "Add or Remove from lists", + "account.add_or_remove_from_list": "Dodaj ali odstrani iz seznama", "account.badges.bot": "Robot", "account.block": "Blokiraj @{name}", "account.block_domain": "Skrij vse iz {domain}", @@ -297,6 +297,7 @@ "status.block": "Block @{name}", "status.cancel_reblog_private": "Unboost", "status.cannot_reblog": "This post cannot be boosted", + "status.copy": "Copy link to status", "status.delete": "Delete", "status.detailed_status": "Detailed conversation view", "status.direct": "Direct message @{name}", @@ -342,6 +343,7 @@ "ui.beforeunload": "Vaš osnutek bo izgubljen, če zapustite Mastodona.", "upload_area.title": "Povlecite in spustite za pošiljanje", "upload_button.label": "Dodaj medij", + "upload_error.limit": "File upload limit exceeded.", "upload_form.description": "Opišite za slabovidne", "upload_form.focus": "Obreži", "upload_form.undo": "Izbriši", diff --git a/app/javascript/mastodon/locales/sq.json b/app/javascript/mastodon/locales/sq.json new file mode 100644 index 000000000..3dd7fb443 --- /dev/null +++ b/app/javascript/mastodon/locales/sq.json @@ -0,0 +1,360 @@ +{ + "account.add_or_remove_from_list": "Add or Remove from lists", + "account.badges.bot": "Bot", + "account.block": "Block @{name}", + "account.block_domain": "Hide everything from {domain}", + "account.blocked": "Blocked", + "account.direct": "Direct message @{name}", + "account.disclaimer_full": "Information below may reflect the user's profile incompletely.", + "account.domain_blocked": "Domain hidden", + "account.edit_profile": "Edit profile", + "account.endorse": "Feature on profile", + "account.follow": "Follow", + "account.followers": "Followers", + "account.followers.empty": "No one follows this user yet.", + "account.follows": "Follows", + "account.follows.empty": "This user doesn't follow anyone yet.", + "account.follows_you": "Follows you", + "account.hide_reblogs": "Hide boosts from @{name}", + "account.link_verified_on": "Ownership of this link was checked on {date}", + "account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.", + "account.media": "Media", + "account.mention": "Mention @{name}", + "account.moved_to": "{name} has moved to:", + "account.mute": "Mute @{name}", + "account.mute_notifications": "Mute notifications from @{name}", + "account.muted": "Muted", + "account.posts": "Toots", + "account.posts_with_replies": "Toots and replies", + "account.report": "Report @{name}", + "account.requested": "Awaiting approval. Click to cancel follow request", + "account.share": "Share @{name}'s profile", + "account.show_reblogs": "Show boosts from @{name}", + "account.unblock": "Unblock @{name}", + "account.unblock_domain": "Unhide {domain}", + "account.unendorse": "Don't feature on profile", + "account.unfollow": "Unfollow", + "account.unmute": "Unmute @{name}", + "account.unmute_notifications": "Unmute notifications from @{name}", + "account.view_full_profile": "View full profile", + "alert.unexpected.message": "An unexpected error occurred.", + "alert.unexpected.title": "Oops!", + "boost_modal.combo": "You can press {combo} to skip this next time", + "bundle_column_error.body": "Something went wrong while loading this component.", + "bundle_column_error.retry": "Try again", + "bundle_column_error.title": "Network error", + "bundle_modal_error.close": "Close", + "bundle_modal_error.message": "Something went wrong while loading this component.", + "bundle_modal_error.retry": "Try again", + "column.blocks": "Blocked users", + "column.community": "Local timeline", + "column.direct": "Direct messages", + "column.domain_blocks": "Hidden domains", + "column.favourites": "Favourites", + "column.follow_requests": "Follow requests", + "column.home": "Home", + "column.lists": "Lists", + "column.mutes": "Muted users", + "column.notifications": "Notifications", + "column.pins": "Pinned toot", + "column.public": "Federated timeline", + "column_back_button.label": "Back", + "column_header.hide_settings": "Hide settings", + "column_header.moveLeft_settings": "Move column to the left", + "column_header.moveRight_settings": "Move column to the right", + "column_header.pin": "Pin", + "column_header.show_settings": "Show settings", + "column_header.unpin": "Unpin", + "column_subheading.settings": "Settings", + "community.column_settings.media_only": "Media Only", + "compose_form.direct_message_warning": "This toot will only be sent to all the mentioned users.", + "compose_form.direct_message_warning_learn_more": "Learn more", + "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.", + "compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.", + "compose_form.lock_disclaimer.lock": "locked", + "compose_form.placeholder": "What is on your mind?", + "compose_form.publish": "Toot", + "compose_form.publish_loud": "{publish}!", + "compose_form.sensitive.marked": "Media is marked as sensitive", + "compose_form.sensitive.unmarked": "Media is not marked as sensitive", + "compose_form.spoiler.marked": "Text is hidden behind warning", + "compose_form.spoiler.unmarked": "Text is not hidden", + "compose_form.spoiler_placeholder": "Write your warning here", + "confirmation_modal.cancel": "Cancel", + "confirmations.block.confirm": "Block", + "confirmations.block.message": "Are you sure you want to block {name}?", + "confirmations.delete.confirm": "Delete", + "confirmations.delete.message": "Are you sure you want to delete this status?", + "confirmations.delete_list.confirm": "Delete", + "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?", + "confirmations.domain_block.confirm": "Hide entire domain", + "confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.", + "confirmations.mute.confirm": "Mute", + "confirmations.mute.message": "Are you sure you want to mute {name}?", + "confirmations.redraft.confirm": "Delete & redraft", + "confirmations.redraft.message": "Are you sure you want to delete this status and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.", + "confirmations.reply.confirm": "Reply", + "confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?", + "confirmations.unfollow.confirm": "Unfollow", + "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?", + "embed.instructions": "Embed this status on your website by copying the code below.", + "embed.preview": "Here is what it will look like:", + "emoji_button.activity": "Activity", + "emoji_button.custom": "Custom", + "emoji_button.flags": "Flags", + "emoji_button.food": "Food & Drink", + "emoji_button.label": "Insert emoji", + "emoji_button.nature": "Nature", + "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻", + "emoji_button.objects": "Objects", + "emoji_button.people": "People", + "emoji_button.recent": "Frequently used", + "emoji_button.search": "Search...", + "emoji_button.search_results": "Search results", + "emoji_button.symbols": "Symbols", + "emoji_button.travel": "Travel & Places", + "empty_column.account_timeline": "No toots here!", + "empty_column.blocks": "You haven't blocked any users yet.", + "empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!", + "empty_column.direct": "You don't have any direct messages yet. When you send or receive one, it will show up here.", + "empty_column.domain_blocks": "There are no hidden domains yet.", + "empty_column.favourited_statuses": "You don't have any favourite toots yet. When you favourite one, it will show up here.", + "empty_column.favourites": "No one has favourited this toot yet. When someone does, they will show up here.", + "empty_column.follow_requests": "You don't have any follow requests yet. When you receive one, it will show up here.", + "empty_column.hashtag": "There is nothing in this hashtag yet.", + "empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.", + "empty_column.home.public_timeline": "the public timeline", + "empty_column.list": "There is nothing in this list yet. When members of this list post new statuses, they will appear here.", + "empty_column.lists": "You don't have any lists yet. When you create one, it will show up here.", + "empty_column.mutes": "You haven't muted any users yet.", + "empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.", + "empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other servers to fill it up", + "follow_request.authorize": "Authorize", + "follow_request.reject": "Reject", + "getting_started.developers": "Developers", + "getting_started.directory": "Profile directory", + "getting_started.documentation": "Documentation", + "getting_started.heading": "Getting started", + "getting_started.invite": "Invite people", + "getting_started.open_source_notice": "Mastodon is open source software. You can contribute or report issues on GitHub at {github}.", + "getting_started.security": "Security", + "getting_started.terms": "Terms of service", + "hashtag.column_header.tag_mode.all": "and {additional}", + "hashtag.column_header.tag_mode.any": "or {additional}", + "hashtag.column_header.tag_mode.none": "without {additional}", + "hashtag.column_settings.tag_mode.all": "All of these", + "hashtag.column_settings.tag_mode.any": "Any of these", + "hashtag.column_settings.tag_mode.none": "None of these", + "hashtag.column_settings.tag_toggle": "Include additional tags in this column", + "home.column_settings.basic": "Basic", + "home.column_settings.show_reblogs": "Show boosts", + "home.column_settings.show_replies": "Show replies", + "introduction.federation.action": "Next", + "introduction.federation.federated.headline": "Federated", + "introduction.federation.federated.text": "Public posts from other servers of the fediverse will appear in the federated timeline.", + "introduction.federation.home.headline": "Home", + "introduction.federation.home.text": "Posts from people you follow will appear in your home feed. You can follow anyone on any server!", + "introduction.federation.local.headline": "Local", + "introduction.federation.local.text": "Public posts from people on the same server as you will appear in the local timeline.", + "introduction.interactions.action": "Finish toot-orial!", + "introduction.interactions.favourite.headline": "Favourite", + "introduction.interactions.favourite.text": "You can save a toot for later, and let the author know that you liked it, by favouriting it.", + "introduction.interactions.reblog.headline": "Boost", + "introduction.interactions.reblog.text": "You can share other people's toots with your followers by boosting them.", + "introduction.interactions.reply.headline": "Reply", + "introduction.interactions.reply.text": "You can reply to other people's and your own toots, which will chain them together in a conversation.", + "introduction.welcome.action": "Let's go!", + "introduction.welcome.headline": "First steps", + "introduction.welcome.text": "Welcome to the fediverse! In a few moments, you'll be able to broadcast messages and talk to your friends across a wide variety of servers. But this server, {domain}, is special—it hosts your profile, so remember its name.", + "keyboard_shortcuts.back": "to navigate back", + "keyboard_shortcuts.blocked": "to open blocked users list", + "keyboard_shortcuts.boost": "to boost", + "keyboard_shortcuts.column": "to focus a status in one of the columns", + "keyboard_shortcuts.compose": "to focus the compose textarea", + "keyboard_shortcuts.description": "Description", + "keyboard_shortcuts.direct": "to open direct messages column", + "keyboard_shortcuts.down": "to move down in the list", + "keyboard_shortcuts.enter": "to open status", + "keyboard_shortcuts.favourite": "to favourite", + "keyboard_shortcuts.favourites": "to open favourites list", + "keyboard_shortcuts.federated": "to open federated timeline", + "keyboard_shortcuts.heading": "Keyboard Shortcuts", + "keyboard_shortcuts.home": "to open home timeline", + "keyboard_shortcuts.hotkey": "Hotkey", + "keyboard_shortcuts.legend": "to display this legend", + "keyboard_shortcuts.local": "to open local timeline", + "keyboard_shortcuts.mention": "to mention author", + "keyboard_shortcuts.muted": "to open muted users list", + "keyboard_shortcuts.my_profile": "to open your profile", + "keyboard_shortcuts.notifications": "to open notifications column", + "keyboard_shortcuts.pinned": "to open pinned toots list", + "keyboard_shortcuts.profile": "to open author's profile", + "keyboard_shortcuts.reply": "to reply", + "keyboard_shortcuts.requests": "to open follow requests list", + "keyboard_shortcuts.search": "to focus search", + "keyboard_shortcuts.start": "to open \"get started\" column", + "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW", + "keyboard_shortcuts.toot": "to start a brand new toot", + "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search", + "keyboard_shortcuts.up": "to move up in the list", + "lightbox.close": "Close", + "lightbox.next": "Next", + "lightbox.previous": "Previous", + "lists.account.add": "Add to list", + "lists.account.remove": "Remove from list", + "lists.delete": "Delete list", + "lists.edit": "Edit list", + "lists.new.create": "Add list", + "lists.new.title_placeholder": "New list title", + "lists.search": "Search among people you follow", + "lists.subheading": "Your lists", + "loading_indicator.label": "Loading...", + "media_gallery.toggle_visible": "Toggle visibility", + "missing_indicator.label": "Not found", + "missing_indicator.sublabel": "This resource could not be found", + "mute_modal.hide_notifications": "Hide notifications from this user?", + "navigation_bar.apps": "Mobile apps", + "navigation_bar.blocks": "Blocked users", + "navigation_bar.community_timeline": "Local timeline", + "navigation_bar.compose": "Compose new toot", + "navigation_bar.direct": "Direct messages", + "navigation_bar.discover": "Discover", + "navigation_bar.domain_blocks": "Hidden domains", + "navigation_bar.edit_profile": "Edit profile", + "navigation_bar.favourites": "Favourites", + "navigation_bar.filters": "Muted words", + "navigation_bar.follow_requests": "Follow requests", + "navigation_bar.info": "About this server", + "navigation_bar.keyboard_shortcuts": "Hotkeys", + "navigation_bar.lists": "Lists", + "navigation_bar.logout": "Logout", + "navigation_bar.mutes": "Muted users", + "navigation_bar.personal": "Personal", + "navigation_bar.pins": "Pinned toots", + "navigation_bar.preferences": "Preferences", + "navigation_bar.public_timeline": "Federated timeline", + "navigation_bar.security": "Security", + "notification.favourite": "{name} favourited your status", + "notification.follow": "{name} followed you", + "notification.mention": "{name} mentioned you", + "notification.reblog": "{name} boosted your status", + "notifications.clear": "Clear notifications", + "notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?", + "notifications.column_settings.alert": "Desktop notifications", + "notifications.column_settings.favourite": "Favourites:", + "notifications.column_settings.filter_bar.advanced": "Display all categories", + "notifications.column_settings.filter_bar.category": "Quick filter bar", + "notifications.column_settings.filter_bar.show": "Show", + "notifications.column_settings.follow": "New followers:", + "notifications.column_settings.mention": "Mentions:", + "notifications.column_settings.push": "Push notifications", + "notifications.column_settings.reblog": "Boosts:", + "notifications.column_settings.show": "Show in column", + "notifications.column_settings.sound": "Play sound", + "notifications.filter.all": "All", + "notifications.filter.boosts": "Boosts", + "notifications.filter.favourites": "Favourites", + "notifications.filter.follows": "Follows", + "notifications.filter.mentions": "Mentions", + "notifications.group": "{count} notifications", + "privacy.change": "Adjust status privacy", + "privacy.direct.long": "Post to mentioned users only", + "privacy.direct.short": "Direct", + "privacy.private.long": "Post to followers only", + "privacy.private.short": "Followers-only", + "privacy.public.long": "Post to public timelines", + "privacy.public.short": "Public", + "privacy.unlisted.long": "Do not show in public timelines", + "privacy.unlisted.short": "Unlisted", + "regeneration_indicator.label": "Loading…", + "regeneration_indicator.sublabel": "Your home feed is being prepared!", + "relative_time.days": "{number}d", + "relative_time.hours": "{number}h", + "relative_time.just_now": "now", + "relative_time.minutes": "{number}m", + "relative_time.seconds": "{number}s", + "reply_indicator.cancel": "Cancel", + "report.forward": "Forward to {target}", + "report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?", + "report.hint": "The report will be sent to your server moderators. You can provide an explanation of why you are reporting this account below:", + "report.placeholder": "Additional comments", + "report.submit": "Submit", + "report.target": "Report {target}", + "search.placeholder": "Search", + "search_popout.search_format": "Advanced search format", + "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.", + "search_popout.tips.hashtag": "hashtag", + "search_popout.tips.status": "status", + "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags", + "search_popout.tips.user": "user", + "search_results.accounts": "People", + "search_results.hashtags": "Hashtags", + "search_results.statuses": "Toots", + "search_results.total": "{count, number} {count, plural, one {result} other {results}}", + "standalone.public_title": "A look inside...", + "status.admin_account": "Open moderation interface for @{name}", + "status.admin_status": "Open this status in the moderation interface", + "status.block": "Block @{name}", + "status.cancel_reblog_private": "Unboost", + "status.cannot_reblog": "This post cannot be boosted", + "status.copy": "Copy link to status", + "status.delete": "Delete", + "status.detailed_status": "Detailed conversation view", + "status.direct": "Direct message @{name}", + "status.embed": "Embed", + "status.favourite": "Favourite", + "status.filtered": "Filtered", + "status.load_more": "Load more", + "status.media_hidden": "Media hidden", + "status.mention": "Mention @{name}", + "status.more": "More", + "status.mute": "Mute @{name}", + "status.mute_conversation": "Mute conversation", + "status.open": "Expand this status", + "status.pin": "Pin on profile", + "status.pinned": "Pinned toot", + "status.read_more": "Read more", + "status.reblog": "Boost", + "status.reblog_private": "Boost to original audience", + "status.reblogged_by": "{name} boosted", + "status.reblogs.empty": "No one has boosted this toot yet. When someone does, they will show up here.", + "status.redraft": "Delete & re-draft", + "status.reply": "Reply", + "status.replyAll": "Reply to thread", + "status.report": "Report @{name}", + "status.sensitive_toggle": "Click to view", + "status.sensitive_warning": "Sensitive content", + "status.share": "Share", + "status.show_less": "Show less", + "status.show_less_all": "Show less for all", + "status.show_more": "Show more", + "status.show_more_all": "Show more for all", + "status.show_thread": "Show thread", + "status.unmute_conversation": "Unmute conversation", + "status.unpin": "Unpin from profile", + "suggestions.dismiss": "Dismiss suggestion", + "suggestions.header": "You might be interested in…", + "tabs_bar.federated_timeline": "Federated", + "tabs_bar.home": "Home", + "tabs_bar.local_timeline": "Local", + "tabs_bar.notifications": "Notifications", + "tabs_bar.search": "Search", + "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", + "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", + "upload_area.title": "Drag & drop to upload", + "upload_button.label": "Add media (JPEG, PNG, GIF, WebM, MP4, MOV)", + "upload_error.limit": "File upload limit exceeded.", + "upload_form.description": "Describe for the visually impaired", + "upload_form.focus": "Crop", + "upload_form.undo": "Delete", + "upload_progress.label": "Uploading...", + "video.close": "Close video", + "video.exit_fullscreen": "Exit full screen", + "video.expand": "Expand video", + "video.fullscreen": "Full screen", + "video.hide": "Hide video", + "video.mute": "Mute sound", + "video.pause": "Pause", + "video.play": "Play", + "video.unmute": "Unmute sound" +} diff --git a/app/javascript/mastodon/locales/sr-Latn.json b/app/javascript/mastodon/locales/sr-Latn.json index c8513dbe1..484e2690d 100644 --- a/app/javascript/mastodon/locales/sr-Latn.json +++ b/app/javascript/mastodon/locales/sr-Latn.json @@ -297,6 +297,7 @@ "status.block": "Block @{name}", "status.cancel_reblog_private": "Unboost", "status.cannot_reblog": "Ovaj status ne može da se podrži", + "status.copy": "Copy link to status", "status.delete": "Obriši", "status.detailed_status": "Detailed conversation view", "status.direct": "Direct message @{name}", @@ -342,6 +343,7 @@ "ui.beforeunload": "Ako napustite Mastodont, izgubićete napisani nacrt.", "upload_area.title": "Prevucite ovde da otpremite", "upload_button.label": "Dodaj multimediju", + "upload_error.limit": "File upload limit exceeded.", "upload_form.description": "Opiši za slabovide osobe", "upload_form.focus": "Crop", "upload_form.undo": "Opozovi", diff --git a/app/javascript/mastodon/locales/sr.json b/app/javascript/mastodon/locales/sr.json index 6e0ac6eca..284ceab47 100644 --- a/app/javascript/mastodon/locales/sr.json +++ b/app/javascript/mastodon/locales/sr.json @@ -297,6 +297,7 @@ "status.block": "Блокирај @{name}", "status.cancel_reblog_private": "Уклони подршку", "status.cannot_reblog": "Овај статус не може да се подржи", + "status.copy": "Copy link to status", "status.delete": "Обриши", "status.detailed_status": "Детаљни преглед разговора", "status.direct": "Директна порука @{name}", @@ -342,6 +343,7 @@ "ui.beforeunload": "Ако напустите Мастодонт, изгубићете написани нацрт.", "upload_area.title": "Превуците овде да отпремите", "upload_button.label": "Додај мултимедију (JPEG, PNG, GIF, WebM, MP4, MOV)", + "upload_error.limit": "File upload limit exceeded.", "upload_form.description": "Опишите за особе са оштећеним видом", "upload_form.focus": "Подесите", "upload_form.undo": "Обриши", diff --git a/app/javascript/mastodon/locales/sv.json b/app/javascript/mastodon/locales/sv.json index 47ce8497a..9acf2428e 100644 --- a/app/javascript/mastodon/locales/sv.json +++ b/app/javascript/mastodon/locales/sv.json @@ -297,6 +297,7 @@ "status.block": "Block @{name}", "status.cancel_reblog_private": "Ta bort knuff", "status.cannot_reblog": "Detta inlägg kan inte knuffas", + "status.copy": "Copy link to status", "status.delete": "Ta bort", "status.detailed_status": "Detailed conversation view", "status.direct": "Direktmeddela @{name}", @@ -342,6 +343,7 @@ "ui.beforeunload": "Ditt utkast kommer att förloras om du lämnar Mastodon.", "upload_area.title": "Dra & släpp för att ladda upp", "upload_button.label": "Lägg till media", + "upload_error.limit": "File upload limit exceeded.", "upload_form.description": "Beskriv för synskadade", "upload_form.focus": "Beskär", "upload_form.undo": "Ta bort", diff --git a/app/javascript/mastodon/locales/ta.json b/app/javascript/mastodon/locales/ta.json index 0d510d011..602a59ca0 100644 --- a/app/javascript/mastodon/locales/ta.json +++ b/app/javascript/mastodon/locales/ta.json @@ -297,6 +297,7 @@ "status.block": "Block @{name}", "status.cancel_reblog_private": "Unboost", "status.cannot_reblog": "This post cannot be boosted", + "status.copy": "Copy link to status", "status.delete": "Delete", "status.detailed_status": "Detailed conversation view", "status.direct": "Direct message @{name}", @@ -342,6 +343,7 @@ "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "upload_area.title": "Drag & drop to upload", "upload_button.label": "Add media (JPEG, PNG, GIF, WebM, MP4, MOV)", + "upload_error.limit": "File upload limit exceeded.", "upload_form.description": "Describe for the visually impaired", "upload_form.focus": "Crop", "upload_form.undo": "Delete", diff --git a/app/javascript/mastodon/locales/te.json b/app/javascript/mastodon/locales/te.json index 7306ec001..b118c189f 100644 --- a/app/javascript/mastodon/locales/te.json +++ b/app/javascript/mastodon/locales/te.json @@ -132,7 +132,7 @@ "follow_request.authorize": "అనుమతించు", "follow_request.reject": "తిరస్కరించు", "getting_started.developers": "డెవలపర్లు", - "getting_started.directory": "ప్రొఫైల్ డైరెక్టరీProfile directory", + "getting_started.directory": "ప్రొఫైల్ డైరెక్టరీ", "getting_started.documentation": "డాక్యుమెంటేషన్", "getting_started.heading": "మొదలుపెడదాం", "getting_started.invite": "వ్యక్తులను ఆహ్వానించండి", @@ -292,11 +292,12 @@ "search_results.statuses": "టూట్లు", "search_results.total": "{count, number} {count, plural, one {result} other {results}}", "standalone.public_title": "లోపలికి ఒక చూపు...", - "status.admin_account": "Open moderation interface for @{name}", - "status.admin_status": "Open this status in the moderation interface", + "status.admin_account": "@{name} కొరకు సమన్వయ వినిమయసీమను తెరువు", + "status.admin_status": "సమన్వయ వినిమయసీమలో ఈ స్టేటస్ ను తెరవండి", "status.block": "@{name} ను బ్లాక్ చేయి", "status.cancel_reblog_private": "బూస్ట్ను తొలగించు", "status.cannot_reblog": "ఈ పోస్ట్ను బూస్ట్ చేయడం సాధ్యం కాదు", + "status.copy": "Copy link to status", "status.delete": "తొలగించు", "status.detailed_status": "వివరణాత్మక సంభాషణ వీక్షణ", "status.direct": "@{name}కు నేరుగా సందేశం పంపు", @@ -342,6 +343,7 @@ "ui.beforeunload": "మీరు మాస్టొడొన్ను వదిలివేస్తే మీ డ్రాఫ్ట్లు పోతాయి.", "upload_area.title": "అప్లోడ్ చేయడానికి డ్రాగ్ & డ్రాప్ చేయండి", "upload_button.label": "మీడియాను జోడించండి (JPEG, PNG, GIF, WebM, MP4, MOV)", + "upload_error.limit": "File upload limit exceeded.", "upload_form.description": "దృష్టి లోపమున్న వారి కోసం వివరించండి", "upload_form.focus": "ప్రివ్యూను మార్చు", "upload_form.undo": "తొలగించు", diff --git a/app/javascript/mastodon/locales/th.json b/app/javascript/mastodon/locales/th.json index 2683284f4..2b0fc40e2 100644 --- a/app/javascript/mastodon/locales/th.json +++ b/app/javascript/mastodon/locales/th.json @@ -297,6 +297,7 @@ "status.block": "Block @{name}", "status.cancel_reblog_private": "Unboost", "status.cannot_reblog": "This post cannot be boosted", + "status.copy": "Copy link to status", "status.delete": "Delete", "status.detailed_status": "Detailed conversation view", "status.direct": "Direct message @{name}", @@ -342,6 +343,7 @@ "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "upload_area.title": "Drag & drop to upload", "upload_button.label": "Add media", + "upload_error.limit": "File upload limit exceeded.", "upload_form.description": "Describe for the visually impaired", "upload_form.focus": "Crop", "upload_form.undo": "Undo", diff --git a/app/javascript/mastodon/locales/tr.json b/app/javascript/mastodon/locales/tr.json index 5d8fc229e..30dc06d8c 100644 --- a/app/javascript/mastodon/locales/tr.json +++ b/app/javascript/mastodon/locales/tr.json @@ -297,6 +297,7 @@ "status.block": "Block @{name}", "status.cancel_reblog_private": "Unboost", "status.cannot_reblog": "Bu gönderi boost edilemez", + "status.copy": "Copy link to status", "status.delete": "Sil", "status.detailed_status": "Detailed conversation view", "status.direct": "Direct message @{name}", @@ -342,6 +343,7 @@ "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "upload_area.title": "Upload için sürükle bırak yapınız", "upload_button.label": "Görsel ekle", + "upload_error.limit": "File upload limit exceeded.", "upload_form.description": "Describe for the visually impaired", "upload_form.focus": "Crop", "upload_form.undo": "Geri al", diff --git a/app/javascript/mastodon/locales/uk.json b/app/javascript/mastodon/locales/uk.json index 606dda89f..5dfda4933 100644 --- a/app/javascript/mastodon/locales/uk.json +++ b/app/javascript/mastodon/locales/uk.json @@ -297,6 +297,7 @@ "status.block": "Block @{name}", "status.cancel_reblog_private": "Unboost", "status.cannot_reblog": "Цей допис не може бути передмухнутий", + "status.copy": "Copy link to status", "status.delete": "Видалити", "status.detailed_status": "Detailed conversation view", "status.direct": "Direct message @{name}", @@ -342,6 +343,7 @@ "ui.beforeunload": "Вашу чернетку буде втрачено, якщо ви покинете Mastodon.", "upload_area.title": "Перетягніть сюди, щоб завантажити", "upload_button.label": "Додати медіаконтент", + "upload_error.limit": "File upload limit exceeded.", "upload_form.description": "Опишіть для людей з вадами зору", "upload_form.focus": "Обрізати", "upload_form.undo": "Видалити", diff --git a/app/javascript/mastodon/locales/whitelist_sq.json b/app/javascript/mastodon/locales/whitelist_sq.json new file mode 100644 index 000000000..0d4f101c7 --- /dev/null +++ b/app/javascript/mastodon/locales/whitelist_sq.json @@ -0,0 +1,2 @@ +[ +] diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json index dfa261d6e..0cfa3f712 100644 --- a/app/javascript/mastodon/locales/zh-CN.json +++ b/app/javascript/mastodon/locales/zh-CN.json @@ -297,6 +297,7 @@ "status.block": "屏蔽 @{name}", "status.cancel_reblog_private": "取消转嘟", "status.cannot_reblog": "无法转嘟这条嘟文", + "status.copy": "Copy link to status", "status.delete": "删除", "status.detailed_status": "Detailed conversation view", "status.direct": "发送私信给 @{name}", @@ -342,6 +343,7 @@ "ui.beforeunload": "如果你现在离开 Mastodon,你的草稿内容将会被丢弃。", "upload_area.title": "将文件拖放到此处开始上传", "upload_button.label": "上传媒体文件", + "upload_error.limit": "File upload limit exceeded.", "upload_form.description": "为视觉障碍人士添加文字说明", "upload_form.focus": "剪裁", "upload_form.undo": "取消上传", diff --git a/app/javascript/mastodon/locales/zh-HK.json b/app/javascript/mastodon/locales/zh-HK.json index e57aa6d96..999ca3216 100644 --- a/app/javascript/mastodon/locales/zh-HK.json +++ b/app/javascript/mastodon/locales/zh-HK.json @@ -297,6 +297,7 @@ "status.block": "封鎖 @{name}", "status.cancel_reblog_private": "取消轉推", "status.cannot_reblog": "這篇文章無法被轉推", + "status.copy": "Copy link to status", "status.delete": "刪除", "status.detailed_status": "Detailed conversation view", "status.direct": "私訊 @{name}", @@ -342,6 +343,7 @@ "ui.beforeunload": "如果你現在離開 Mastodon,你的草稿內容將會被丟棄。", "upload_area.title": "將檔案拖放至此上載", "upload_button.label": "上載媒體檔案", + "upload_error.limit": "File upload limit exceeded.", "upload_form.description": "為視覺障礙人士添加文字說明", "upload_form.focus": "裁切", "upload_form.undo": "刪除", diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json index 0cbe5da5a..26328e12e 100644 --- a/app/javascript/mastodon/locales/zh-TW.json +++ b/app/javascript/mastodon/locales/zh-TW.json @@ -297,6 +297,7 @@ "status.block": "封鎖 @{name}", "status.cancel_reblog_private": "取消轉嘟", "status.cannot_reblog": "這篇嘟文無法被轉嘟", + "status.copy": "Copy link to status", "status.delete": "刪除", "status.detailed_status": "對話的詳細內容", "status.direct": "發送私訊給 @{name}", @@ -342,6 +343,7 @@ "ui.beforeunload": "如果離開 Mastodon,你的草稿將會不見。", "upload_area.title": "拖放來上傳", "upload_button.label": "上傳媒體檔案 (JPEG, PNG, GIF, WebM, MP4, MOV)", + "upload_error.limit": "File upload limit exceeded.", "upload_form.description": "為視障人士增加文字說明", "upload_form.focus": "裁切", "upload_form.undo": "刪除", diff --git a/app/javascript/mastodon/reducers/list_editor.js b/app/javascript/mastodon/reducers/list_editor.js index 02a0dabb1..91e524dd5 100644 --- a/app/javascript/mastodon/reducers/list_editor.js +++ b/app/javascript/mastodon/reducers/list_editor.js @@ -22,6 +22,7 @@ import { const initialState = ImmutableMap({ listId: null, isSubmitting: false, + isChanged: false, title: '', accounts: ImmutableMap({ @@ -47,10 +48,16 @@ export default function listEditorReducer(state = initialState, action) { map.set('isSubmitting', false); }); case LIST_EDITOR_TITLE_CHANGE: - return state.set('title', action.value); + return state.withMutations(map => { + map.set('title', action.value); + map.set('isChanged', true); + }); case LIST_CREATE_REQUEST: case LIST_UPDATE_REQUEST: - return state.set('isSubmitting', true); + return state.withMutations(map => { + map.set('isSubmitting', true); + map.set('isChanged', false); + }); case LIST_CREATE_FAIL: case LIST_UPDATE_FAIL: return state.set('isSubmitting', false); diff --git a/app/javascript/mastodon/utils/resize_image.js b/app/javascript/mastodon/utils/resize_image.js index d1608094f..bbdbc865e 100644 --- a/app/javascript/mastodon/utils/resize_image.js +++ b/app/javascript/mastodon/utils/resize_image.js @@ -31,7 +31,7 @@ const loadImage = inputFile => new Promise((resolve, reject) => { }); const getOrientation = (img, type = 'image/png') => new Promise(resolve => { - if (type !== 'image/jpeg') { + if (!['image/jpeg', 'image/webp'].includes(type)) { resolve(1); return; } diff --git a/app/javascript/packs/error.js b/app/javascript/packs/error.js new file mode 100644 index 000000000..685c89065 --- /dev/null +++ b/app/javascript/packs/error.js @@ -0,0 +1,13 @@ +import ready from '../mastodon/ready'; + +ready(() => { + const image = document.querySelector('img'); + + image.addEventListener('mouseenter', () => { + image.src = '/oops.gif'; + }); + + image.addEventListener('mouseleave', () => { + image.src = '/oops.png'; + }); +}); diff --git a/app/javascript/styles/contrast/diff.scss b/app/javascript/styles/contrast/diff.scss index eee9ecc3e..8429103b8 100644 --- a/app/javascript/styles/contrast/diff.scss +++ b/app/javascript/styles/contrast/diff.scss @@ -12,3 +12,58 @@ } } } + +.rich-formatting a, +.rich-formatting p a, +.rich-formatting li a, +.landing-page__short-description p a, +.status__content a, +.reply-indicator__content a { + color: lighten($ui-highlight-color, 12%); + text-decoration: underline; + + &.mention { + text-decoration: none; + } + + &.mention span { + text-decoration: underline; + + &:hover, + &:focus, + &:active { + text-decoration: none; + } + } + + &:hover, + &:focus, + &:active { + text-decoration: none; + } + + &.status__content__spoiler-link { + color: $secondary-text-color; + text-decoration: none; + } +} + +.status__content__read-more-button { + text-decoration: underline; + + &:hover, + &:focus, + &:active { + text-decoration: none; + } +} + +.getting-started__footer a { + text-decoration: underline; + + &:hover, + &:focus, + &:active { + text-decoration: none; + } +} diff --git a/app/javascript/styles/mastodon-light/diff.scss b/app/javascript/styles/mastodon-light/diff.scss index 78bc2dbb6..de03cf1a6 100644 --- a/app/javascript/styles/mastodon-light/diff.scss +++ b/app/javascript/styles/mastodon-light/diff.scss @@ -352,6 +352,8 @@ .moved-account-widget, .memoriam-widget, .activity-stream, -.nothing-here { +.nothing-here, +.directory__tag > a, +.directory__tag > div { box-shadow: none; } diff --git a/app/javascript/styles/mastodon/_mixins.scss b/app/javascript/styles/mastodon/_mixins.scss index d5bafe6b6..08806599e 100644 --- a/app/javascript/styles/mastodon/_mixins.scss +++ b/app/javascript/styles/mastodon/_mixins.scss @@ -41,3 +41,34 @@ font-size: 16px; } } + +@mixin search-popout() { + background: $simple-background-color; + border-radius: 4px; + padding: 10px 14px; + padding-bottom: 14px; + margin-top: 10px; + color: $light-text-color; + box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4); + + h4 { + text-transform: uppercase; + color: $light-text-color; + font-size: 13px; + font-weight: 500; + margin-bottom: 10px; + } + + li { + padding: 4px 0; + } + + ul { + margin-bottom: 10px; + } + + em { + font-weight: 500; + color: $inverted-text-color; + } +} diff --git a/app/javascript/styles/mastodon/about.scss b/app/javascript/styles/mastodon/about.scss index b6c92a09e..b078d4d24 100644 --- a/app/javascript/styles/mastodon/about.scss +++ b/app/javascript/styles/mastodon/about.scss @@ -49,15 +49,9 @@ $small-breakpoint: 960px; } } + strong, em { - display: inline; - margin: 0; - padding: 0; font-weight: 700; - background: transparent; - font-family: inherit; - font-size: inherit; - line-height: inherit; color: lighten($darker-text-color, 10%); } @@ -796,7 +790,7 @@ $small-breakpoint: 960px; width: 100%; display: flex; flex-direction: row-reverse; - flex-wrap: wrap; + flex-wrap: nowrap; justify-content: space-between; align-items: center; } @@ -846,14 +840,7 @@ $small-breakpoint: 960px; } strong { - display: inline; - margin: 0; - padding: 0; - font-weight: 700; - background: transparent; - font-family: inherit; - font-size: inherit; - line-height: inherit; + font-weight: 500; color: lighten($darker-text-color, 10%); } diff --git a/app/javascript/styles/mastodon/accounts.scss b/app/javascript/styles/mastodon/accounts.scss index 63a5c61b8..f4f458cf4 100644 --- a/app/javascript/styles/mastodon/accounts.scss +++ b/app/javascript/styles/mastodon/accounts.scss @@ -288,3 +288,7 @@ border-bottom: 0; } } + +.directory__tag .trends__item__current { + width: auto; +} diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss index 4e969601b..4dbbaa1e8 100644 --- a/app/javascript/styles/mastodon/admin.scss +++ b/app/javascript/styles/mastodon/admin.scss @@ -153,10 +153,15 @@ $content-width: 840px; font-weight: 500; } - .directory__tag a { + .directory__tag > a, + .directory__tag > div { box-shadow: none; } + .directory__tag .table-action-link .fa { + color: inherit; + } + .directory__tag h4 { font-size: 18px; font-weight: 700; diff --git a/app/javascript/styles/mastodon/basics.scss b/app/javascript/styles/mastodon/basics.scss index 746def625..4411ca0b4 100644 --- a/app/javascript/styles/mastodon/basics.scss +++ b/app/javascript/styles/mastodon/basics.scss @@ -100,12 +100,14 @@ body { vertical-align: middle; margin: 20px; - img { - display: block; - max-width: 470px; - width: 100%; - height: auto; - margin-top: -120px; + &__illustration { + img { + display: block; + max-width: 470px; + width: 100%; + height: auto; + margin-top: -120px; + } } h1 { diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 8c1115e76..11823a45b 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -476,7 +476,7 @@ opacity: 0; transition: opacity .1s ease; - input { + textarea { background: transparent; color: $secondary-text-color; border: 0; @@ -638,7 +638,6 @@ font-weight: 400; overflow: hidden; text-overflow: ellipsis; - white-space: pre-wrap; padding-top: 2px; color: $primary-text-color; @@ -662,6 +661,7 @@ p { margin-bottom: 20px; + white-space: pre-wrap; &:last-child { margin-bottom: 0; @@ -3056,14 +3056,41 @@ a.status-card.compact:hover { display: block; font-weight: 500; margin-bottom: 10px; +} + +.column-settings__hashtags { + .column-settings__row { + margin-bottom: 15px; + } - .column-settings__hashtag-select { + .column-select { &__control { @include search-input(); } + &__placeholder { + color: $dark-text-color; + padding-left: 2px; + font-size: 12px; + } + + &__value-container { + padding-left: 6px; + } + &__multi-value { background: lighten($ui-base-color, 8%); + + &__remove { + cursor: pointer; + + &:hover, + &:active, + &:focus { + background: lighten($ui-base-color, 12%); + color: lighten($darker-text-color, 4%); + } + } } &__multi-value__label, @@ -3071,9 +3098,42 @@ a.status-card.compact:hover { color: $darker-text-color; } - &__indicator-separator, + &__clear-indicator, &__dropdown-indicator { - display: none; + cursor: pointer; + transition: none; + color: $dark-text-color; + + &:hover, + &:active, + &:focus { + color: lighten($dark-text-color, 4%); + } + } + + &__indicator-separator { + background-color: lighten($ui-base-color, 8%); + } + + &__menu { + @include search-popout(); + padding: 0; + background: $ui-secondary-color; + } + + &__menu-list { + padding: 6px; + } + + &__option { + color: $inverted-text-color; + border-radius: 4px; + font-size: 14px; + + &--is-focused, + &--is-selected { + background: darken($ui-secondary-color, 10%); + } } } } @@ -4867,34 +4927,7 @@ a.status-card.compact:hover { } .search-popout { - background: $simple-background-color; - border-radius: 4px; - padding: 10px 14px; - padding-bottom: 14px; - margin-top: 10px; - color: $light-text-color; - box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4); - - h4 { - text-transform: uppercase; - color: $light-text-color; - font-size: 13px; - font-weight: 500; - margin-bottom: 10px; - } - - li { - padding: 4px 0; - } - - ul { - margin-bottom: 10px; - } - - em { - font-weight: 500; - color: $inverted-text-color; - } + @include search-popout(); } noscript { @@ -5130,7 +5163,7 @@ noscript { .icon-button { flex: 0 0 auto; - margin-left: 5px; + margin: 0 5px; } } diff --git a/app/javascript/styles/mastodon/widgets.scss b/app/javascript/styles/mastodon/widgets.scss index c97337e4e..1eaf30c5b 100644 --- a/app/javascript/styles/mastodon/widgets.scss +++ b/app/javascript/styles/mastodon/widgets.scss @@ -269,7 +269,8 @@ box-sizing: border-box; margin-bottom: 10px; - a { + & > a, + & > div { display: flex; align-items: center; justify-content: space-between; @@ -279,7 +280,9 @@ text-decoration: none; color: inherit; box-shadow: 0 0 15px rgba($base-shadow-color, 0.2); + } + & > a { &:hover, &:active, &:focus { @@ -287,7 +290,7 @@ } } - &.active a { + &.active > a { background: $ui-highlight-color; cursor: default; } diff --git a/app/lib/activity_tracker.rb b/app/lib/activity_tracker.rb index 5b4972674..ae3c11b6a 100644 --- a/app/lib/activity_tracker.rb +++ b/app/lib/activity_tracker.rb @@ -4,6 +4,8 @@ class ActivityTracker EXPIRE_AFTER = 90.days.seconds class << self + include Redisable + def increment(prefix) key = [prefix, current_week].join(':') @@ -20,10 +22,6 @@ class ActivityTracker private - def redis - Redis.current - end - def current_week Time.zone.today.cweek end diff --git a/app/lib/activitypub/activity.rb b/app/lib/activitypub/activity.rb index 87318fb1c..11fa3363a 100644 --- a/app/lib/activitypub/activity.rb +++ b/app/lib/activitypub/activity.rb @@ -2,6 +2,10 @@ class ActivityPub::Activity include JsonLdHelper + include Redisable + + SUPPORTED_TYPES = %w(Note).freeze + CONVERTED_TYPES = %w(Image Video Article Page).freeze def initialize(json, account, **options) @json = json @@ -70,8 +74,16 @@ class ActivityPub::Activity @object_uri ||= value_or_id(@object) end - def redis - Redis.current + def unsupported_object_type? + @object.is_a?(String) || !(supported_object_type? || converted_object_type?) + end + + def supported_object_type? + equals_or_includes_any?(@object['type'], SUPPORTED_TYPES) + end + + def converted_object_type? + equals_or_includes_any?(@object['type'], CONVERTED_TYPES) end def distribute(status) @@ -123,6 +135,24 @@ class ActivityPub::Activity redis.setex("delete_upon_arrival:#{@account.id}:#{uri}", 6.hours.seconds, uri) end + def status_from_object + # If the status is already known, return it + status = status_from_uri(object_uri) + + return status unless status.nil? + + # If the boosted toot is embedded and it is a self-boost, handle it like a Create + unless unsupported_object_type? + actor_id = value_or_id(first_of_value(@object['attributedTo'])) || @account.uri + + if actor_id == @account.uri + return ActivityPub::Activity.factory({ 'type' => 'Create', 'actor' => actor_id, 'object' => @object }, @account).perform + end + end + + fetch_remote_original_status + end + def fetch_remote_original_status if object_uri.start_with?('http') return if ActivityPub::TagManager.instance.local_uri?(object_uri) @@ -137,4 +167,21 @@ class ActivityPub::Activity ensure redis.del(key) end + + def fetch? + !@options[:delivery] + end + + def followed_by_local_accounts? + @account.passive_relationships.exists? + end + + def requested_through_relay? + @options[:relayed_through_account] && Relay.find_by(inbox_url: @options[:relayed_through_account].inbox_url)&.enabled? + end + + def reject_payload! + Rails.logger.info("Rejected #{@json['type']} activity #{@json['id']} from #{@account.uri}#{@options[:relayed_through_account] && "via #{@options[:relayed_through_account].uri}"}") + nil + end end diff --git a/app/lib/activitypub/activity/announce.rb b/app/lib/activitypub/activity/announce.rb index 34d1b7cbd..9f8ffd9fb 100644 --- a/app/lib/activitypub/activity/announce.rb +++ b/app/lib/activitypub/activity/announce.rb @@ -2,10 +2,11 @@ class ActivityPub::Activity::Announce < ActivityPub::Activity def perform - original_status = status_from_uri(object_uri) - original_status ||= fetch_remote_original_status + return reject_payload! if delete_arrived_first?(@json['id']) || !related_to_local_activity? - return if original_status.nil? || delete_arrived_first?(@json['id']) || !announceable?(original_status) + original_status = status_from_object + + return reject_payload! if original_status.nil? || !announceable?(original_status) status = Status.find_by(account: @account, reblog: original_status) @@ -41,4 +42,12 @@ class ActivityPub::Activity::Announce < ActivityPub::Activity def announceable?(status) status.account_id == @account.id || status.public_visibility? || status.unlisted_visibility? end + + def related_to_local_activity? + followed_by_local_accounts? || requested_through_relay? || reblog_of_local_status? + end + + def reblog_of_local_status? + status_from_uri(object_uri)&.account&.local? + end end diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index b49657d4b..d7bd65c80 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -1,12 +1,8 @@ # frozen_string_literal: true class ActivityPub::Activity::Create < ActivityPub::Activity - SUPPORTED_TYPES = %w(Note).freeze - CONVERTED_TYPES = %w(Image Video Article Page).freeze - def perform - return if unsupported_object_type? || invalid_origin?(@object['id']) - return if Tombstone.exists?(uri: @object['id']) + return reject_payload! if unsupported_object_type? || invalid_origin?(@object['id']) || Tombstone.exists?(uri: @object['id']) || !related_to_local_activity? RedisLock.acquire(lock_options) do |lock| if lock.acquired? @@ -318,22 +314,10 @@ class ActivityPub::Activity::Create < ActivityPub::Activity @object['nameMap'].is_a?(Hash) && !@object['nameMap'].empty? end - def unsupported_object_type? - @object.is_a?(String) || !(supported_object_type? || converted_object_type?) - end - def unsupported_media_type?(mime_type) mime_type.present? && !(MediaAttachment::IMAGE_MIME_TYPES + MediaAttachment::VIDEO_MIME_TYPES).include?(mime_type) end - def supported_object_type? - equals_or_includes_any?(@object['type'], SUPPORTED_TYPES) - end - - def converted_object_type? - equals_or_includes_any?(@object['type'], CONVERTED_TYPES) - end - def skip_download? return @skip_download if defined?(@skip_download) @skip_download ||= DomainBlock.find_by(domain: @account.domain)&.reject_media? @@ -352,6 +336,25 @@ class ActivityPub::Activity::Create < ActivityPub::Activity !replied_to_status.nil? && replied_to_status.account.local? end + def related_to_local_activity? + fetch? || followed_by_local_accounts? || requested_through_relay? || + responds_to_followed_account? || addresses_local_accounts? + end + + def responds_to_followed_account? + !replied_to_status.nil? && (replied_to_status.account.local? || replied_to_status.account.passive_relationships.exists?) + end + + def addresses_local_accounts? + return true if @options[:delivered_to_account_id] + + local_usernames = (as_array(@object['to']) + as_array(@object['cc'])).uniq.select { |uri| ActivityPub::TagManager.instance.local_uri?(uri) }.map { |uri| ActivityPub::TagManager.instance.uri_to_local_id(uri, :username) } + + return false if local_usernames.empty? + + Account.local.where(username: local_usernames).exists? + end + def forward_for_reply return unless @json['signature'].present? && reply_to_local? ActivityPub::RawDistributionWorker.perform_async(Oj.dump(@json), replied_to_status.account_id, [@account.preferred_inbox_url]) diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index a1b186f1c..4bc75dae8 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -4,6 +4,7 @@ require 'singleton' class FeedManager include Singleton + include Redisable MAX_ITEMS = 400 @@ -35,7 +36,7 @@ class FeedManager def unpush_from_home(account, status) return false unless remove_from_feed(:home, account.id, status) - Redis.current.publish("timeline:#{account.id}", Oj.dump(event: :delete, payload: status.id.to_s)) + redis.publish("timeline:#{account.id}", Oj.dump(event: :delete, payload: status.id.to_s)) true end @@ -54,7 +55,7 @@ class FeedManager def unpush_from_list(list, status) return false unless remove_from_feed(:list, list.id, status) - Redis.current.publish("timeline:list:#{list.id}", Oj.dump(event: :delete, payload: status.id.to_s)) + redis.publish("timeline:list:#{list.id}", Oj.dump(event: :delete, payload: status.id.to_s)) true end @@ -143,10 +144,6 @@ class FeedManager private - def redis - Redis.current - end - def push_update_required?(timeline_id) redis.exists("subscribed:#{timeline_id}") end diff --git a/app/lib/formatter.rb b/app/lib/formatter.rb index 05fd9eeb1..0653214f5 100644 --- a/app/lib/formatter.rb +++ b/app/lib/formatter.rb @@ -99,7 +99,7 @@ class Formatter end def encode_and_link_urls(html, accounts = nil, options = {}) - entities = Extractor.extract_entities_with_indices(html, extract_url_without_protocol: false) + entities = utf8_friendly_extractor(html, extract_url_without_protocol: false) if accounts.is_a?(Hash) options = accounts @@ -199,6 +199,53 @@ class Formatter result.flatten.join end + UNICODE_ESCAPE_BLACKLIST_RE = /\p{Z}|\p{P}/ + + def utf8_friendly_extractor(text, options = {}) + old_to_new_index = [0] + + escaped = text.chars.map do |c| + output = begin + if c.ord.to_s(16).length > 2 && UNICODE_ESCAPE_BLACKLIST_RE.match(c).nil? + CGI.escape(c) + else + c + end + end + + old_to_new_index << old_to_new_index.last + output.length + + output + end.join + + # Note: I couldn't obtain list_slug with @user/list-name format + # for mention so this requires additional check + special = Extractor.extract_urls_with_indices(escaped, options).map do |extract| + # exactly one of :url, :hashtag, :screen_name, :cashtag keys is present + key = (extract.keys & [:url, :hashtag, :screen_name, :cashtag]).first + + new_indices = [ + old_to_new_index.find_index(extract[:indices].first), + old_to_new_index.find_index(extract[:indices].last), + ] + + has_prefix_char = [:hashtag, :screen_name, :cashtag].include?(key) + value_indices = [ + new_indices.first + (has_prefix_char ? 1 : 0), # account for #, @ or $ + new_indices.last - 1, + ] + + next extract.merge( + :indices => new_indices, + key => text[value_indices.first..value_indices.last] + ) + end + + standard = Extractor.extract_entities_with_indices(text, options) + + Extractor.remove_overlapping_entities(special + standard) + end + def link_to_url(entity, options = {}) url = Addressable::URI.parse(entity[:url]) html_attrs = { target: '_blank', rel: 'nofollow noopener' } diff --git a/app/lib/ostatus/activity/base.rb b/app/lib/ostatus/activity/base.rb index c5933f3ad..db70f1998 100644 --- a/app/lib/ostatus/activity/base.rb +++ b/app/lib/ostatus/activity/base.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class OStatus::Activity::Base + include Redisable + def initialize(xml, account = nil, **options) @xml = xml @account = account @@ -66,8 +68,4 @@ class OStatus::Activity::Base Status.find_by(uri: uri) end end - - def redis - Redis.current - end end diff --git a/app/lib/potential_friendship_tracker.rb b/app/lib/potential_friendship_tracker.rb index 017a9748d..188aa4a27 100644 --- a/app/lib/potential_friendship_tracker.rb +++ b/app/lib/potential_friendship_tracker.rb @@ -11,6 +11,8 @@ class PotentialFriendshipTracker }.freeze class << self + include Redisable + def record(account_id, target_account_id, action) return if account_id == target_account_id @@ -31,11 +33,5 @@ class PotentialFriendshipTracker return [] if account_ids.empty? Account.searchable.where(id: account_ids) end - - private - - def redis - Redis.current - end end end diff --git a/app/models/account_conversation.rb b/app/models/account_conversation.rb index cc6b39279..0c03747e2 100644 --- a/app/models/account_conversation.rb +++ b/app/models/account_conversation.rb @@ -30,7 +30,8 @@ class AccountConversation < ApplicationRecord if participant_account_ids.empty? [account] else - Account.where(id: participant_account_ids) + participants = Account.where(id: participant_account_ids) + participants.empty? ? [account] : participants end end diff --git a/app/models/account_domain_block.rb b/app/models/account_domain_block.rb index e352000c3..7c0d60379 100644 --- a/app/models/account_domain_block.rb +++ b/app/models/account_domain_block.rb @@ -12,6 +12,7 @@ class AccountDomainBlock < ApplicationRecord include Paginable + include DomainNormalizable belongs_to :account validates :domain, presence: true, uniqueness: { scope: :account_id } diff --git a/app/models/concerns/account_associations.rb b/app/models/concerns/account_associations.rb index 4e730451a..3ab8a0daa 100644 --- a/app/models/concerns/account_associations.rb +++ b/app/models/concerns/account_associations.rb @@ -56,5 +56,6 @@ module AccountAssociations # Hashtags has_and_belongs_to_many :tags + has_many :featured_tags, -> { includes(:tag) }, dependent: :destroy, inverse_of: :account end end diff --git a/app/models/concerns/account_avatar.rb b/app/models/concerns/account_avatar.rb index 2d5ebfca3..5fff3ef5d 100644 --- a/app/models/concerns/account_avatar.rb +++ b/app/models/concerns/account_avatar.rb @@ -3,7 +3,7 @@ module AccountAvatar extend ActiveSupport::Concern - IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif'].freeze + IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'].freeze LIMIT = 2.megabytes class_methods do diff --git a/app/models/concerns/account_header.rb b/app/models/concerns/account_header.rb index 067e166eb..a748fdff7 100644 --- a/app/models/concerns/account_header.rb +++ b/app/models/concerns/account_header.rb @@ -3,7 +3,7 @@ module AccountHeader extend ActiveSupport::Concern - IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif'].freeze + IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'].freeze LIMIT = 2.megabytes MAX_PIXELS = 750_000 # 1500x500px diff --git a/app/models/concerns/domain_normalizable.rb b/app/models/concerns/domain_normalizable.rb index dff3e5414..fb84058fc 100644 --- a/app/models/concerns/domain_normalizable.rb +++ b/app/models/concerns/domain_normalizable.rb @@ -10,6 +10,6 @@ module DomainNormalizable private def normalize_domain - self.domain = TagManager.instance.normalize_domain(domain) + self.domain = TagManager.instance.normalize_domain(domain&.strip) end end diff --git a/app/models/concerns/redisable.rb b/app/models/concerns/redisable.rb new file mode 100644 index 000000000..c6cf97359 --- /dev/null +++ b/app/models/concerns/redisable.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Redisable + extend ActiveSupport::Concern + + private + + def redis + Redis.current + end +end diff --git a/app/models/domain_block.rb b/app/models/domain_block.rb index 1064ea7c8..069cda367 100644 --- a/app/models/domain_block.rb +++ b/app/models/domain_block.rb @@ -24,6 +24,8 @@ class DomainBlock < ApplicationRecord has_many :accounts, foreign_key: :domain, primary_key: :domain delegate :count, to: :accounts, prefix: true + scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) } + def self.blocked?(domain) where(domain: domain, severity: :suspend).exists? end diff --git a/app/models/export.rb b/app/models/export.rb index a2520e9c2..fc4bb6964 100644 --- a/app/models/export.rb +++ b/app/models/export.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require 'csv' class Export diff --git a/app/models/featured_tag.rb b/app/models/featured_tag.rb new file mode 100644 index 000000000..b5a10ad2d --- /dev/null +++ b/app/models/featured_tag.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true +# == Schema Information +# +# Table name: featured_tags +# +# id :bigint(8) not null, primary key +# account_id :bigint(8) +# tag_id :bigint(8) +# statuses_count :bigint(8) default(0), not null +# last_status_at :datetime +# created_at :datetime not null +# updated_at :datetime not null +# + +class FeaturedTag < ApplicationRecord + belongs_to :account, inverse_of: :featured_tags, required: true + belongs_to :tag, inverse_of: :featured_tags, required: true + + delegate :name, to: :tag, allow_nil: true + + validates :name, presence: true + validate :validate_featured_tags_limit, on: :create + + def name=(str) + self.tag = Tag.find_or_initialize_by(name: str.delete('#').mb_chars.downcase.to_s) + end + + def increment(timestamp) + update(statuses_count: statuses_count + 1, last_status_at: timestamp) + end + + def decrement(deleted_status_id) + update(statuses_count: [0, statuses_count - 1].max, last_status_at: account.statuses.where(visibility: %i(public unlisted)).tagged_with(tag).where.not(id: deleted_status_id).select(:created_at).first&.created_at) + end + + def reset_data + self.statuses_count = account.statuses.where(visibility: %i(public unlisted)).tagged_with(tag).count + self.last_status_at = account.statuses.where(visibility: %i(public unlisted)).tagged_with(tag).select(:created_at).first&.created_at + end + + private + + def validate_featured_tags_limit + errors.add(:base, I18n.t('featured_tags.errors.limit')) if account.featured_tags.count >= 10 + end +end diff --git a/app/models/feed.rb b/app/models/feed.rb index 5bce88f25..0e8943ff8 100644 --- a/app/models/feed.rb +++ b/app/models/feed.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class Feed + include Redisable + def initialize(type, id) @type = type @id = id @@ -27,8 +29,4 @@ class Feed def key FeedManager.instance.key(@type, @id) end - - def redis - Redis.current - end end diff --git a/app/models/import.rb b/app/models/import.rb index 55e970b0d..a7a0d8065 100644 --- a/app/models/import.rb +++ b/app/models/import.rb @@ -13,20 +13,30 @@ # data_file_size :integer # data_updated_at :datetime # account_id :bigint(8) not null +# overwrite :boolean default(FALSE), not null # class Import < ApplicationRecord - FILE_TYPES = ['text/plain', 'text/csv'].freeze + FILE_TYPES = %w(text/plain text/csv).freeze + MODES = %i(merge overwrite).freeze self.inheritance_column = false belongs_to :account - enum type: [:following, :blocking, :muting] + enum type: [:following, :blocking, :muting, :domain_blocking] validates :type, presence: true has_attached_file :data validates_attachment_content_type :data, content_type: FILE_TYPES validates_attachment_presence :data + + def mode + overwrite? ? :overwrite : :merge + end + + def mode=(str) + self.overwrite = str.to_sym == :overwrite + end end diff --git a/app/models/instance_filter.rb b/app/models/instance_filter.rb index 3483d8cd6..848fff53e 100644 --- a/app/models/instance_filter.rb +++ b/app/models/instance_filter.rb @@ -9,9 +9,13 @@ class InstanceFilter def results if params[:limited].present? - DomainBlock.order(id: :desc) + scope = DomainBlock + scope = scope.matches_domain(params[:by_domain]) if params[:by_domain].present? + scope.order(id: :desc) else - Account.remote.by_domain_accounts + scope = Account.remote + scope = scope.matches_domain(params[:by_domain]) if params[:by_domain].present? + scope.by_domain_accounts end end end diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb index 601b14223..81397a18e 100644 --- a/app/models/media_attachment.rb +++ b/app/models/media_attachment.rb @@ -25,11 +25,11 @@ class MediaAttachment < ApplicationRecord enum type: [:image, :gifv, :video, :audio, :unknown] - IMAGE_FILE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif'].freeze + IMAGE_FILE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.webp'].freeze VIDEO_FILE_EXTENSIONS = ['.webm', '.mp4', '.m4v', '.mov'].freeze AUDIO_FILE_EXTENSIONS = ['.mp3', '.m4a', '.wav', '.ogg'].freeze - IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif'].freeze + IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'].freeze VIDEO_MIME_TYPES = ['video/webm', 'video/mp4', 'video/quicktime'].freeze VIDEO_CONVERTIBLE_MIME_TYPES = ['video/webm', 'video/quicktime'].freeze AUDIO_MIME_TYPES = ['audio/mpeg', 'audio/mp4', 'audio/vnd.wav', 'audio/wav', 'audio/x-wav', 'audio/x-wave', 'audio/ogg',].freeze @@ -105,8 +105,8 @@ class MediaAttachment < ApplicationRecord convert_options: { all: '-quality 90 -strip' } validates_attachment_content_type :file, content_type: IMAGE_MIME_TYPES + VIDEO_MIME_TYPES + AUDIO_MIME_TYPES - validates_attachment_size :file, less_than: IMAGE_LIMIT, unless: :video? - validates_attachment_size :file, less_than: VIDEO_LIMIT, if: :video? + validates_attachment_size :file, less_than: IMAGE_LIMIT, unless: :video_or_gifv? + validates_attachment_size :file, less_than: VIDEO_LIMIT, if: :video_or_gifv? remotable_attachment :file, VIDEO_LIMIT include Attachmentable @@ -129,6 +129,10 @@ class MediaAttachment < ApplicationRecord file.blank? && remote_url.present? end + def video_or_gifv? + video? || gifv? + end + def to_param shortcode end diff --git a/app/models/preview_card.rb b/app/models/preview_card.rb index a792b352b..f26ea0c74 100644 --- a/app/models/preview_card.rb +++ b/app/models/preview_card.rb @@ -25,7 +25,7 @@ # class PreviewCard < ApplicationRecord - IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif'].freeze + IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'].freeze LIMIT = 1.megabytes self.inheritance_column = false diff --git a/app/models/relay.rb b/app/models/relay.rb index 7478c110d..6934a5c62 100644 --- a/app/models/relay.rb +++ b/app/models/relay.rb @@ -29,6 +29,7 @@ class Relay < ApplicationRecord payload = Oj.dump(follow_activity(activity_id)) update!(state: :pending, follow_activity_id: activity_id) + DeliveryFailureTracker.new(inbox_url).track_success! ActivityPub::DeliveryWorker.perform_async(payload, some_local_account.id, inbox_url) end @@ -37,6 +38,7 @@ class Relay < ApplicationRecord payload = Oj.dump(unfollow_activity(activity_id)) update!(state: :idle, follow_activity_id: nil) + DeliveryFailureTracker.new(inbox_url).track_success! ActivityPub::DeliveryWorker.perform_async(payload, some_local_account.id, inbox_url) end diff --git a/app/models/tag.rb b/app/models/tag.rb index 99830ae92..4373e967b 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -14,6 +14,7 @@ class Tag < ApplicationRecord has_and_belongs_to_many :accounts has_and_belongs_to_many :sample_accounts, -> { searchable.discoverable.popular.limit(3) }, class_name: 'Account' + has_many :featured_tags, dependent: :destroy, inverse_of: :tag has_one :account_tag_stat, dependent: :destroy HASHTAG_NAME_RE = '[[:word:]_]*[[:alpha:]_·][[:word:]_]*' @@ -23,6 +24,7 @@ class Tag < ApplicationRecord scope :discoverable, -> { joins(:account_tag_stat).where(AccountTagStat.arel_table[:accounts_count].gt(0)).where(account_tag_stats: { hidden: false }).order(Arel.sql('account_tag_stats.accounts_count desc')) } scope :hidden, -> { where(account_tag_stats: { hidden: true }) } + scope :most_used, ->(account) { joins(:statuses).where(statuses: { account: account }).group(:id).order(Arel.sql('count(*) desc')) } delegate :accounts_count, :accounts_count=, diff --git a/app/models/trending_tags.rb b/app/models/trending_tags.rb index 3a8be2164..148535c21 100644 --- a/app/models/trending_tags.rb +++ b/app/models/trending_tags.rb @@ -7,6 +7,8 @@ class TrendingTags THRESHOLD = 5 class << self + include Redisable + def record_use!(tag, account, at_time = Time.now.utc) return if disallowed_hashtags.include?(tag.name) || account.silenced? || account.bot? @@ -59,9 +61,5 @@ class TrendingTags @disallowed_hashtags = @disallowed_hashtags.split(' ') if @disallowed_hashtags.is_a? String @disallowed_hashtags = @disallowed_hashtags.map(&:downcase) end - - def redis - Redis.current - end end end diff --git a/app/serializers/activitypub/activity_serializer.rb b/app/serializers/activitypub/activity_serializer.rb index 50c4f6a04..b51e8c544 100644 --- a/app/serializers/activitypub/activity_serializer.rb +++ b/app/serializers/activitypub/activity_serializer.rb @@ -3,8 +3,8 @@ class ActivityPub::ActivitySerializer < ActiveModel::Serializer attributes :id, :type, :actor, :published, :to, :cc - has_one :proper, key: :object, serializer: ActivityPub::NoteSerializer, unless: :announce? - attribute :proper_uri, key: :object, if: :announce? + has_one :proper, key: :object, serializer: ActivityPub::NoteSerializer, unless: :owned_announce? + attribute :proper_uri, key: :object, if: :owned_announce? attribute :atom_uri, if: :announce? def id @@ -42,4 +42,8 @@ class ActivityPub::ActivitySerializer < ActiveModel::Serializer def announce? object.reblog? end + + def owned_announce? + announce? && object.account == object.proper.account && object.proper.private_visibility? + end end diff --git a/app/serializers/manifest_serializer.rb b/app/serializers/manifest_serializer.rb index 859ef0d14..cc8b9a4d4 100644 --- a/app/serializers/manifest_serializer.rb +++ b/app/serializers/manifest_serializer.rb @@ -52,6 +52,14 @@ class ManifestSerializer < ActiveModel::Serializer end def share_target - { url_template: 'share?title={title}&text={text}&url={url}' } + { + url_template: 'share?title={title}&text={text}&url={url}', + action: 'share', + params: { + title: 'title', + text: 'text', + url: 'url', + }, + } end end diff --git a/app/serializers/rest/application_serializer.rb b/app/serializers/rest/application_serializer.rb index a9316cd4b..ab68219ad 100644 --- a/app/serializers/rest/application_serializer.rb +++ b/app/serializers/rest/application_serializer.rb @@ -2,7 +2,7 @@ class REST::ApplicationSerializer < ActiveModel::Serializer attributes :id, :name, :website, :redirect_uri, - :client_id, :client_secret + :client_id, :client_secret, :vapid_key def id object.id.to_s @@ -19,4 +19,8 @@ class REST::ApplicationSerializer < ActiveModel::Serializer def website object.website.presence end + + def vapid_key + Rails.configuration.x.vapid_public_key + end end diff --git a/app/serializers/rest/instance_serializer.rb b/app/serializers/rest/instance_serializer.rb index cab05e60a..41ed1995d 100644 --- a/app/serializers/rest/instance_serializer.rb +++ b/app/serializers/rest/instance_serializer.rb @@ -5,7 +5,7 @@ class REST::InstanceSerializer < ActiveModel::Serializer attributes :uri, :title, :description, :email, :version, :urls, :stats, :thumbnail, :max_toot_chars, - :languages + :languages, :registrations has_one :contact_account, serializer: REST::AccountSerializer @@ -55,6 +55,10 @@ class REST::InstanceSerializer < ActiveModel::Serializer [I18n.default_locale] end + def registrations + Setting.open_registrations && !Rails.configuration.x.single_user_mode + end + private def instance_presenter diff --git a/app/services/activitypub/process_account_service.rb b/app/services/activitypub/process_account_service.rb index 487456f3a..5e3308428 100644 --- a/app/services/activitypub/process_account_service.rb +++ b/app/services/activitypub/process_account_service.rb @@ -212,7 +212,7 @@ class ActivityPub::ProcessAccountService < BaseService end def clear_tombstones! - Tombstone.delete_all(account_id: @account.id) + Tombstone.where(account_id: @account.id).delete_all end def protocol_changed? diff --git a/app/services/activitypub/process_collection_service.rb b/app/services/activitypub/process_collection_service.rb index 5c54aad89..881df478b 100644 --- a/app/services/activitypub/process_collection_service.rb +++ b/app/services/activitypub/process_collection_service.rb @@ -44,6 +44,7 @@ class ActivityPub::ProcessCollectionService < BaseService end def verify_account! + @options[:relayed_through_account] = @account @account = ActivityPub::LinkedDataSignature.new(@json).verify_account! rescue JSON::LD::JsonLdError => e Rails.logger.debug "Could not verify LD-Signature for #{value_or_id(@json['actor'])}: #{e.message}" diff --git a/app/services/batched_remove_status_service.rb b/app/services/batched_remove_status_service.rb index 61c408926..d78f506c6 100644 --- a/app/services/batched_remove_status_service.rb +++ b/app/services/batched_remove_status_service.rb @@ -2,6 +2,7 @@ class BatchedRemoveStatusService < BaseService include StreamEntryRenderer + include Redisable # Delete given statuses and reblogs of them # Dispatch PuSH updates of the deleted statuses, but only local ones @@ -120,10 +121,6 @@ class BatchedRemoveStatusService < BaseService end end - def redis - Redis.current - end - def build_xml(stream_entry) return @activity_xml[stream_entry.id] if @activity_xml.key?(stream_entry.id) diff --git a/app/services/follow_service.rb b/app/services/follow_service.rb index 9d36a1449..92d8c864a 100644 --- a/app/services/follow_service.rb +++ b/app/services/follow_service.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class FollowService < BaseService + include Redisable + # Follow a remote user, notify remote user about the follow # @param [Account] source_account From which to follow # @param [String, Account] uri User URI to follow in the form of username@domain (or account record) @@ -67,10 +69,6 @@ class FollowService < BaseService follow end - def redis - Redis.current - end - def build_follow_request_xml(follow_request) OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.follow_request_salmon(follow_request)) end diff --git a/app/services/import_service.rb b/app/services/import_service.rb new file mode 100644 index 000000000..3f558626e --- /dev/null +++ b/app/services/import_service.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: true + +require 'csv' + +class ImportService < BaseService + ROWS_PROCESSING_LIMIT = 20_000 + + def call(import) + @import = import + @account = @import.account + @data = CSV.new(import_data).reject(&:blank?) + + case @import.type + when 'following' + import_follows! + when 'blocking' + import_blocks! + when 'muting' + import_mutes! + when 'domain_blocking' + import_domain_blocks! + end + end + + private + + def import_follows! + import_relationships!('follow', 'unfollow', @account.following, follow_limit) + end + + def import_blocks! + import_relationships!('block', 'unblock', @account.blocking, ROWS_PROCESSING_LIMIT) + end + + def import_mutes! + import_relationships!('mute', 'unmute', @account.muting, ROWS_PROCESSING_LIMIT) + end + + def import_domain_blocks! + items = @data.take(ROWS_PROCESSING_LIMIT).map { |row| row.first.strip } + + if @import.overwrite? + presence_hash = items.each_with_object({}) { |id, mapping| mapping[id] = true } + + @account.domain_blocks.find_each do |domain_block| + if presence_hash[domain_block.domain] + items.delete(domain_block.domain) + else + @account.unblock_domain!(domain_block.domain) + end + end + end + + items.each do |domain| + @account.block_domain!(domain) + end + + AfterAccountDomainBlockWorker.push_bulk(items) do |domain| + [@account.id, domain] + end + end + + def import_relationships!(action, undo_action, overwrite_scope, limit) + items = @data.take(limit).map { |row| row.first.strip } + + if @import.overwrite? + presence_hash = items.each_with_object({}) { |id, mapping| mapping[id] = true } + + overwrite_scope.find_each do |target_account| + if presence_hash[target_account.acct] + items.delete(target_account.acct) + else + Import::RelationshipWorker.perform_async(@account.id, target_account.acct, undo_action) + end + end + end + + Import::RelationshipWorker.push_bulk(items) do |acct| + [@account.id, acct, action] + end + end + + def import_data + Paperclip.io_adapters.for(@import.data).read + end + + def follow_limit + FollowLimitValidator.limit_for_account(@account) + end +end diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb index 5d431c42a..cfb266fbb 100644 --- a/app/services/post_status_service.rb +++ b/app/services/post_status_service.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class PostStatusService < BaseService + include Redisable + MIN_SCHEDULE_OFFSET = 5.minutes.freeze # Post a text status update, fetch and notify remote users mentioned @@ -115,10 +117,6 @@ class PostStatusService < BaseService ProcessHashtagsService.new end - def redis - Redis.current - end - def scheduled? @scheduled_at.present? end diff --git a/app/services/process_hashtags_service.rb b/app/services/process_hashtags_service.rb index cf7471c98..d5ec076a8 100644 --- a/app/services/process_hashtags_service.rb +++ b/app/services/process_hashtags_service.rb @@ -2,12 +2,22 @@ class ProcessHashtagsService < BaseService def call(status, tags = []) - tags = Extractor.extract_hashtags(status.text) if status.local? + tags = Extractor.extract_hashtags(status.text) if status.local? + records = [] tags.map { |str| str.mb_chars.downcase }.uniq(&:to_s).each do |name| tag = Tag.where(name: name).first_or_create(name: name) + status.tags << tag + records << tag + TrendingTags.record_use!(tag, status.account, status.created_at) if status.public_visibility? end + + return unless status.public_visibility? || status.unlisted_visibility? + + status.account.featured_tags.where(tag_id: records.map(&:id)).each do |featured_tag| + featured_tag.increment(status.created_at) + end end end diff --git a/app/services/remove_status_service.rb b/app/services/remove_status_service.rb index 4bee86c8a..99c8e6cbb 100644 --- a/app/services/remove_status_service.rb +++ b/app/services/remove_status_service.rb @@ -2,6 +2,7 @@ class RemoveStatusService < BaseService include StreamEntryRenderer + include Redisable def call(status, **options) @payload = Oj.dump(event: :delete, payload: status.id.to_s) @@ -56,7 +57,7 @@ class RemoveStatusService < BaseService def remove_from_affected @mentions.map(&:account).select(&:local?).each do |account| - Redis.current.publish("timeline:#{account.id}", @payload) + redis.publish("timeline:#{account.id}", @payload) end end @@ -131,26 +132,30 @@ class RemoveStatusService < BaseService end def remove_from_hashtags + @account.featured_tags.where(tag_id: @status.tags.pluck(:id)).each do |featured_tag| + featured_tag.decrement(@status.id) + end + return unless @status.public_visibility? @tags.each do |hashtag| - Redis.current.publish("timeline:hashtag:#{hashtag}", @payload) - Redis.current.publish("timeline:hashtag:#{hashtag}:local", @payload) if @status.local? + redis.publish("timeline:hashtag:#{hashtag}", @payload) + redis.publish("timeline:hashtag:#{hashtag}:local", @payload) if @status.local? end end def remove_from_public return unless @status.public_visibility? - Redis.current.publish('timeline:public', @payload) - Redis.current.publish('timeline:public:local', @payload) if @status.local? + redis.publish('timeline:public', @payload) + redis.publish('timeline:public:local', @payload) if @status.local? end def remove_from_media return unless @status.public_visibility? - Redis.current.publish('timeline:public:media', @payload) - Redis.current.publish('timeline:public:local:media', @payload) if @status.local? + redis.publish('timeline:public:media', @payload) + redis.publish('timeline:public:local:media', @payload) if @status.local? end def remove_from_direct @@ -159,8 +164,4 @@ class RemoveStatusService < BaseService end Redis.current.publish("timeline:direct:#{@account.id}", @payload) if @account.local? end - - def redis - Redis.current - end end diff --git a/app/services/suspend_account_service.rb b/app/services/suspend_account_service.rb index 1bc2314de..fc3bc03a5 100644 --- a/app/services/suspend_account_service.rb +++ b/app/services/suspend_account_service.rb @@ -102,6 +102,10 @@ class SuspendAccountService < BaseService ActivityPub::DeliveryWorker.push_bulk(delivery_inboxes) do |inbox_url| [delete_actor_json, @account.id, inbox_url] end + + ActivityPub::LowPriorityDeliveryWorker.push_bulk(low_priority_delivery_inboxes) do |inbox_url| + [delete_actor_json, @account.id, inbox_url] + end end def delete_actor_json @@ -117,7 +121,11 @@ class SuspendAccountService < BaseService end def delivery_inboxes - Account.inboxes + Relay.enabled.pluck(:inbox_url) + @delivery_inboxes ||= @account.followers.inboxes + Relay.enabled.pluck(:inbox_url) + end + + def low_priority_delivery_inboxes + Account.inboxes - delivery_inboxes end def associations_for_destruction diff --git a/app/validators/email_mx_validator.rb b/app/validators/email_mx_validator.rb index 5b4c684b2..96fbedcfc 100644 --- a/app/validators/email_mx_validator.rb +++ b/app/validators/email_mx_validator.rb @@ -24,6 +24,7 @@ class EmailMxValidator < ActiveModel::Validator ([domain] + hostnames).uniq.each do |hostname| ips.concat(dns.getresources(hostname, Resolv::DNS::Resource::IN::A).to_a.map { |e| e.address.to_s }) + ips.concat(dns.getresources(hostname, Resolv::DNS::Resource::IN::AAAA).to_a.map { |e| e.address.to_s }) end end diff --git a/app/views/accounts/show.html.haml b/app/views/accounts/show.html.haml index 0ee9dd7de..0da69728f 100644 --- a/app/views/accounts/show.html.haml +++ b/app/views/accounts/show.html.haml @@ -63,4 +63,17 @@ - @endorsed_accounts.each do |account| = account_link_to account + - @account.featured_tags.order(statuses_count: :desc).each do |featured_tag| + .directory__tag{ class: params[:tag] == featured_tag.name ? 'active' : nil } + = link_to short_account_tag_path(@account, featured_tag.tag) do + %h4 + = fa_icon 'hashtag' + = featured_tag.name + %small + - if featured_tag.last_status_at.nil? + = t('accounts.nothing_here') + - else + %time{ datetime: featured_tag.last_status_at.iso8601, title: l(featured_tag.last_status_at) }= l featured_tag.last_status_at + .trends__item__current= number_to_human featured_tag.statuses_count, strip_insignificant_zeros: true + = render 'application/sidebar' diff --git a/app/views/admin/accounts/index.html.haml b/app/views/admin/accounts/index.html.haml index 91fddadf8..345f74f90 100644 --- a/app/views/admin/accounts/index.html.haml +++ b/app/views/admin/accounts/index.html.haml @@ -26,8 +26,9 @@ = hidden_field_tag key, params[key] - %i(username by_domain display_name email ip).each do |key| - .input.string.optional - = text_field_tag key, params[key], class: 'string optional', placeholder: I18n.t("admin.accounts.#{key}") + - unless key == :by_domain && params[:remote].blank? + .input.string.optional + = text_field_tag key, params[key], class: 'string optional', placeholder: I18n.t("admin.accounts.#{key}") .actions %button= t('admin.accounts.search') diff --git a/app/views/admin/accounts/show.html.haml b/app/views/admin/accounts/show.html.haml index 280a834ba..7ac73bd07 100644 --- a/app/views/admin/accounts/show.html.haml +++ b/app/views/admin/accounts/show.html.haml @@ -166,6 +166,12 @@ - else = link_to t('admin.accounts.perform_full_suspension'), new_admin_account_action_path(@account.id, type: 'suspend'), class: 'button button--destructive' if can?(:suspend, @account) + - unless @account.local? + - if DomainBlock.where(domain: @account.domain).exists? + = link_to t('admin.domain_blocks.undo'), admin_instance_path(@account.domain), class: 'button' + - else + = link_to t('admin.domain_blocks.add_new'), new_admin_domain_block_path(_domain: @account.domain), class: 'button button--destructive' + %hr.spacer/ - unless @warnings.empty? diff --git a/app/views/admin/change_emails/show.html.haml b/app/views/admin/change_emails/show.html.haml index 6febef9b1..6ff0d785e 100644 --- a/app/views/admin/change_emails/show.html.haml +++ b/app/views/admin/change_emails/show.html.haml @@ -3,7 +3,7 @@ = simple_form_for @user, url: admin_account_change_email_path(@account.id) do |f| .fields-group - = f.input :email, wrapper: :with_label, disabled: true, label: t('admin.accounts.change_email.current_email') + = f.input :email, wrapper: :with_label, hint: false, disabled: true, label: t('admin.accounts.change_email.current_email') .fields-group = f.input :unconfirmed_email, wrapper: :with_label, label: t('admin.accounts.change_email.new_email') diff --git a/app/views/admin/instances/index.html.haml b/app/views/admin/instances/index.html.haml index ce35b5db4..235927140 100644 --- a/app/views/admin/instances/index.html.haml +++ b/app/views/admin/instances/index.html.haml @@ -11,6 +11,20 @@ %div{ style: 'flex: 1 1 auto; text-align: right' } = link_to t('admin.domain_blocks.add_new'), new_admin_domain_block_path, class: 'button' += form_tag admin_instances_url, method: 'GET', class: 'simple_form' do + .fields-group + - Admin::FilterHelper::INSTANCES_FILTERS.each do |key| + - if params[key].present? + = hidden_field_tag key, params[key] + + - %i(by_domain).each do |key| + .input.string.optional + = text_field_tag key, params[key], class: 'string optional', placeholder: I18n.t("admin.instances.#{key}") + + .actions + %button= t('admin.accounts.search') + = link_to t('admin.accounts.reset'), admin_instances_path, class: 'button negative' + %hr.spacer/ - @instances.each do |instance| diff --git a/app/views/layouts/error.html.haml b/app/views/layouts/error.html.haml index d662d85e2..f8315afb5 100644 --- a/app/views/layouts/error.html.haml +++ b/app/views/layouts/error.html.haml @@ -5,10 +5,12 @@ %meta{ charset: 'utf-8' }/ %title= safe_join([yield(:page_title), Setting.default_settings['site_title']], ' - ') %meta{ content: 'width=device-width,initial-scale=1', name: 'viewport' }/ + = javascript_pack_tag "locales", integrity: true, crossorigin: 'anonymous' = render partial: 'layouts/theme', object: (@core || { pack: 'common' }) - = render partial: 'layouts/theme', object: (@theme || { pack: 'common', flavour: 'glitch', skin: 'default' }) + = render partial: 'layouts/theme', object: (@theme || { pack: 'error', flavour: 'glitch', common: { pack: 'common', flavour: 'glitch', skin: 'default' } }) %body.error .dialog - %img{ alt: Setting.default_settings['site_title'], src: current_user&.setting_auto_play_gif ? '/oops.gif' : '/oops.png' }/ - %div + .dialog__illustration + %img{ alt: Setting.default_settings['site_title'], src: '/oops.png' }/ + .dialog__message %h1= yield :content diff --git a/app/views/settings/featured_tags/index.html.haml b/app/views/settings/featured_tags/index.html.haml new file mode 100644 index 000000000..5f69517f3 --- /dev/null +++ b/app/views/settings/featured_tags/index.html.haml @@ -0,0 +1,27 @@ +- content_for :page_title do + = t('settings.featured_tags') + += simple_form_for @featured_tag, url: settings_featured_tags_path do |f| + = render 'shared/error_messages', object: @featured_tag + + .fields-group + = f.input :name, wrapper: :with_block_label, hint: safe_join([t('simple_form.hints.featured_tag.name'), safe_join(@most_used_tags.map { |tag| link_to("##{tag.name}", settings_featured_tags_path(featured_tag: { name: tag.name }), method: :post) }, ', ')], ' ') + + .actions + = f.button :button, t('featured_tags.add_new'), type: :submit + +%hr.spacer/ + +- @featured_tags.each do |featured_tag| + .directory__tag{ class: params[:tag] == featured_tag.name ? 'active' : nil } + %div + %h4 + = fa_icon 'hashtag' + = featured_tag.name + %small + - if featured_tag.last_status_at.nil? + = t('accounts.nothing_here') + - else + %time{ datetime: featured_tag.last_status_at.iso8601, title: l(featured_tag.last_status_at) }= l featured_tag.last_status_at + = table_link_to 'trash', t('filters.index.delete'), settings_featured_tag_path(featured_tag), method: :delete, data: { confirm: t('admin.accounts.are_you_sure') } + .trends__item__current= number_to_human featured_tag.statuses_count, strip_insignificant_zeros: true diff --git a/app/views/settings/imports/show.html.haml b/app/views/settings/imports/show.html.haml index 4512fc714..7bb4beb01 100644 --- a/app/views/settings/imports/show.html.haml +++ b/app/views/settings/imports/show.html.haml @@ -5,8 +5,11 @@ .field-group = f.input :type, collection: Import.types.keys, wrapper: :with_block_label, include_blank: false, label_method: lambda { |type| I18n.t("imports.types.#{type}") }, hint: t('imports.preface') - .field-group - = f.input :data, wrapper: :with_block_label, hint: t('simple_form.hints.imports.data') + .fields-row + .fields-group.fields-row__column.fields-row__column-6 + = f.input :data, wrapper: :with_block_label, hint: t('simple_form.hints.imports.data') + .fields-group.fields-row__column.fields-row__column-6 + = f.input :mode, as: :radio_buttons, collection: Import::MODES, label_method: lambda { |mode| safe_join([I18n.t("imports.modes.#{mode}"), content_tag(:span, I18n.t("imports.modes.#{mode}_long"), class: 'hint')]) }, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li' .actions = f.button :button, t('imports.upload'), type: :submit diff --git a/app/workers/activitypub/low_priority_delivery_worker.rb b/app/workers/activitypub/low_priority_delivery_worker.rb new file mode 100644 index 000000000..a141b8f78 --- /dev/null +++ b/app/workers/activitypub/low_priority_delivery_worker.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class ActivityPub::LowPriorityDeliveryWorker < ActivityPub::DeliveryWorker + sidekiq_options queue: 'pull', retry: 8, dead: false +end diff --git a/app/workers/activitypub/processing_worker.rb b/app/workers/activitypub/processing_worker.rb index a8a3ebf0f..a3abe72cf 100644 --- a/app/workers/activitypub/processing_worker.rb +++ b/app/workers/activitypub/processing_worker.rb @@ -6,6 +6,6 @@ class ActivityPub::ProcessingWorker sidekiq_options backtrace: true def perform(account_id, body, delivered_to_account_id = nil) - ActivityPub::ProcessCollectionService.new.call(body, Account.find(account_id), override_timestamps: true, delivered_to_account_id: delivered_to_account_id) + ActivityPub::ProcessCollectionService.new.call(body, Account.find(account_id), override_timestamps: true, delivered_to_account_id: delivered_to_account_id, delivery: true) end end diff --git a/app/workers/import/relationship_worker.rb b/app/workers/import/relationship_worker.rb index 1dd8bf8fb..e9db20a46 100644 --- a/app/workers/import/relationship_worker.rb +++ b/app/workers/import/relationship_worker.rb @@ -13,11 +13,17 @@ class Import::RelationshipWorker case relationship when 'follow' - FollowService.new.call(from_account, target_account.acct) + FollowService.new.call(from_account, target_account) + when 'unfollow' + UnfollowService.new.call(from_account, target_account) when 'block' BlockService.new.call(from_account, target_account) + when 'unblock' + UnblockService.new.call(from_account, target_account) when 'mute' MuteService.new.call(from_account, target_account) + when 'unmute' + UnmuteService.new.call(from_account, target_account) end rescue ActiveRecord::RecordNotFound true diff --git a/app/workers/import_worker.rb b/app/workers/import_worker.rb index aeb221cf6..dfa71b29e 100644 --- a/app/workers/import_worker.rb +++ b/app/workers/import_worker.rb @@ -1,44 +1,14 @@ # frozen_string_literal: true -require 'csv' - class ImportWorker include Sidekiq::Worker sidekiq_options queue: 'pull', retry: false - attr_reader :import - def perform(import_id) - @import = Import.find(import_id) - - Import::RelationshipWorker.push_bulk(import_rows) do |row| - [@import.account_id, row.first, relationship_type] - end - - @import.destroy - end - - private - - def import_contents - Paperclip.io_adapters.for(@import.data).read - end - - def relationship_type - case @import.type - when 'following' - 'follow' - when 'blocking' - 'block' - when 'muting' - 'mute' - end - end - - def import_rows - rows = CSV.new(import_contents).reject(&:blank?) - rows = rows.take(FollowLimitValidator.limit_for_account(@import.account)) if @import.type == 'following' - rows + import = Import.find(import_id) + ImportService.new.call(import) + ensure + import&.destroy end end diff --git a/app/workers/scheduler/feed_cleanup_scheduler.rb b/app/workers/scheduler/feed_cleanup_scheduler.rb index cd2273418..bf5e20757 100644 --- a/app/workers/scheduler/feed_cleanup_scheduler.rb +++ b/app/workers/scheduler/feed_cleanup_scheduler.rb @@ -2,6 +2,7 @@ class Scheduler::FeedCleanupScheduler include Sidekiq::Worker + include Redisable sidekiq_options unique: :until_executed, retry: 0 @@ -57,8 +58,4 @@ class Scheduler::FeedCleanupScheduler def feed_manager FeedManager.instance end - - def redis - Redis.current - end end diff --git a/config/application.rb b/config/application.rb index 06ff8ed36..69ed1627a 100644 --- a/config/application.rb +++ b/config/application.rb @@ -65,6 +65,8 @@ module Mastodon :ja, :ka, :ko, + :lv, + :ms, :nl, :no, :oc, @@ -75,6 +77,7 @@ module Mastodon :ru, :sk, :sl, + :sq, :sr, :'sr-Latn', :sv, diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml index e9564692f..b2a621e85 100644 --- a/config/i18n-tasks.yml +++ b/config/i18n-tasks.yml @@ -35,11 +35,8 @@ ignore_missing: - 'activemodel.errors.*' - 'activerecord.attributes.*' - 'activerecord.errors.*' - - '{devise,pagination,doorkeeper}.*' + - '{pagination,doorkeeper}.*' - '{date,datetime,time,number}.*' - - 'simple_form.{yes,no}' - - 'simple_form.{placeholders,hints,labels}.*' - - 'simple_form.{error_notification,required}.:' - 'errors.messages.*' - 'activerecord.errors.models.doorkeeper/*' - 'sessions.{browsers,platforms}.*' diff --git a/config/initializers/rack_attack.rb b/config/initializers/rack_attack.rb index 35302e37b..28201cc64 100644 --- a/config/initializers/rack_attack.rb +++ b/config/initializers/rack_attack.rb @@ -46,14 +46,14 @@ class Rack::Attack end throttle('throttle_authenticated_api', limit: 300, period: 5.minutes) do |req| - req.api_request? && req.authenticated_user_id + req.authenticated_user_id if req.api_request? end throttle('throttle_unauthenticated_api', limit: 7_500, period: 5.minutes) do |req| req.ip if req.api_request? end - throttle('throttle_media', limit: 30, period: 30.minutes) do |req| + throttle('throttle_api_media', limit: 30, period: 30.minutes) do |req| req.authenticated_user_id if req.post? && req.path.start_with?('/api/v1/media') end @@ -61,6 +61,13 @@ class Rack::Attack req.ip if req.post? && req.path == '/api/v1/accounts' end + API_DELETE_REBLOG_REGEX = /\A\/api\/v1\/statuses\/[\d]+\/unreblog/.freeze + API_DELETE_STATUS_REGEX = /\A\/api\/v1\/statuses\/[\d]+/.freeze + + throttle('throttle_api_delete', limit: 30, period: 30.minutes) do |req| + req.authenticated_user_id if (req.post? && req.path =~ API_DELETE_REBLOG_REGEX) || (req.delete? && req.path =~ API_DELETE_STATUS_REGEX) + end + throttle('protected_paths', limit: 25, period: 5.minutes) do |req| req.ip if req.post? && req.path =~ PROTECTED_PATHS_REGEX end diff --git a/config/initializers/twitter_regex.rb b/config/initializers/twitter_regex.rb index 0e8f5bfeb..0ddbbee98 100644 --- a/config/initializers/twitter_regex.rb +++ b/config/initializers/twitter_regex.rb @@ -1,7 +1,7 @@ module Twitter class Regex - REGEXEN[:valid_general_url_path_chars] = /[^\p{White_Space}\(\)\?]/iou - REGEXEN[:valid_url_path_ending_chars] = /[^\p{White_Space}\(\)\?!\*';:=\,\.\$%\[\]~&\|@]|(?:#{REGEXEN[:valid_url_balanced_parens]})/iou + REGEXEN[:valid_general_url_path_chars] = /[^\p{White_Space}<>\(\)\?]/iou + REGEXEN[:valid_url_path_ending_chars] = /[^\p{White_Space}\(\)\?!\*"'「」<>;:=\,\.\$%\[\]~&\|@]|(?:#{REGEXEN[:valid_url_balanced_parens]})/iou REGEXEN[:valid_url_balanced_parens] = / \( (?: diff --git a/config/locales/activerecord.sq.yml b/config/locales/activerecord.sq.yml new file mode 100644 index 000000000..e52345978 --- /dev/null +++ b/config/locales/activerecord.sq.yml @@ -0,0 +1,13 @@ +--- +sq: + activerecord: + errors: + models: + account: + attributes: + username: + invalid: vetëm shkronja, numra dhe nënvija + status: + attributes: + reblog: + taken: e gjendjes ekziston tashmë diff --git a/config/locales/ar.yml b/config/locales/ar.yml index 08f5578ba..ec8b15cba 100644 --- a/config/locales/ar.yml +++ b/config/locales/ar.yml @@ -195,6 +195,7 @@ ar: assigned_to_self_report: قام %{name} بتعيين التقرير %{target} لأنفسهم change_email_user: غيّر %{name} عنوان البريد الإلكتروني للمستخدم %{target} confirm_user: "%{name} قد قام بتأكيد عنوان البريد الإلكتروني لـ %{target}" + create_account_warning: قام %{name} بإرسال تحذير إلى %{target} create_custom_emoji: "%{name} قام برفع إيموجي جديد %{target}" create_domain_block: "%{name} قام بحجب نطاق %{target}" create_email_domain_block: "%{name} قد قام بحظر نطاق البريد الإلكتروني %{target}" diff --git a/config/locales/co.yml b/config/locales/co.yml index a7264e754..5f961b466 100644 --- a/config/locales/co.yml +++ b/config/locales/co.yml @@ -553,8 +553,11 @@ co: warning_title: Dispunibilità di i cuntenuti sparsi directories: directory: Annuariu di i prufili + enabled: Site inscrittu·a indè l'annuariu. + enabled_but_waiting: Avete sceltu d'esse inscrittu·a indè l'annuariu, mà ùn avete micca ancu u numeru minimale d'abbunati (%{min_followers}) per esse listatu·a. explanation: Scopre utilizatori à partesi di i so centri d'interessu explore_mastodon: Scopre à %{title} + how_to_enable: Ùn site micca ancu inscrittu·a indè l'annuariu. Pudete inscrive vi quì sottu. Utilizate qualchi hashtag indè a vostra biugrafia per esse listatu·a indè tag specifichi! people: one: "%{count} persona" other: "%{count} persone" diff --git a/config/locales/cs.yml b/config/locales/cs.yml index 424ec3dab..c75d0b643 100644 --- a/config/locales/cs.yml +++ b/config/locales/cs.yml @@ -3,11 +3,11 @@ cs: about: about_hashtag_html: Tohle jsou veřejné tooty označené hashtagem <strong>#%{hashtag}</strong>. Pokud máte účet kdekoliv na fediverse, můžete s nimi interagovat. about_mastodon_html: Mastodon je sociální síť založená na otevřených webových protokolech a svobodném, otevřeném softwaru. Je decentralizovaná jako e-mail. - about_this: O této instanci - administered_by: 'Instanci spravuje:' + about_this: O tomto serveru + administered_by: 'Server spravuje:' api: API apps: Mobilní aplikace - closed_registrations: Registrace na této instanci jsou momentálně uzavřené. Ale pozor! Můžete si najít jinou instanci, vytvořit si na ní účet a získat z ní přístup do naprosto stejné sítě. + closed_registrations: Registrace na tomto serveru jsou momentálně uzavřené. Ale pozor! Můžete si najít jiný server, vytvořit si na něm účet a získat z něj přístup do naprosto stejné sítě. contact: Kontakt contact_missing: Nenastaveno contact_unavailable: Neuvedeno @@ -25,9 +25,9 @@ cs: within_reach_body: Několik aplikací pro iOS, Android a jiné platformy vám díky jednoduchému API ekosystému dovolují držet krok s vašimi přáteli, ať už jste kdekoliv. within_reach_title: Vždy v dosahu generic_description: "%{domain} je jedním ze serverů v síti" - hosted_on: Instance Mastodon na adrese %{domain} + hosted_on: Server Mastodon na adrese %{domain} learn_more: Zjistit více - other_instances: Seznam instancí + other_instances: Seznam serverů privacy_policy: Zásady soukromí source_code: Zdrojový kód status_count_after: @@ -46,9 +46,9 @@ cs: choices_html: 'Volby uživatele %{name}:' follow: Sledovat followers: - few: Sledovatelé - one: Sledovatel - other: Sledovatelů + few: Sledující + one: Sledující + other: Sledujících following: Sledovaných joined: Připojil/a se v %{date} last_active: naposledy aktivní @@ -109,8 +109,8 @@ cs: enable: Povolit enabled: Povoleno feed_url: URL proudu - followers: Sledovatelé - followers_url: URL sledovatelů + followers: Sledující + followers_url: URL sledujících follows: Sledovaní header: Hlavička inbox_url: URL přijatých zpráv @@ -157,7 +157,7 @@ cs: roles: admin: Administrátor moderator: Moderátor - staff: Personál + staff: Člen personálu user: Uživatel salmon_url: URL Salmon search: Hledat @@ -305,7 +305,7 @@ cs: title: Černá listina e-mailů followers: back_to_account: Zpět na účet - title: Sledovatelé uživatele %{acct} + title: Sledující uživatele %{acct} instances: delivery_available: Doručení je k dispozici known_accounts: @@ -318,9 +318,9 @@ cs: title: Moderace title: Federace total_blocked_by_us: Blokované námi - total_followed_by_them: Sledované jím + total_followed_by_them: Sledované jimi total_followed_by_us: Sledované námi - total_reported: Nahlášení o něm + total_reported: Nahlášení o nich total_storage: Mediální přílohy invites: deactivate_all: Deaktivovat vše @@ -392,14 +392,14 @@ cs: desc_html: Pozměnit vzhled pomocí šablony CSS načtené na každé stránce title: Vlastní CSS hero: - desc_html: Zobrazuje se na hlavní stránce. Doporučuje se rozlišení alespoň 600x100 px. Pokud toto není nastaveno, bude zobrazena miniatura instance + desc_html: Zobrazuje se na hlavní stránce. Doporučuje se rozlišení alespoň 600x100 px. Pokud toto není nastaveno, bude zobrazena miniatura serveru title: Hlavní obrázek mascot: desc_html: Zobrazuje se na hlavní stránce. Doporučuje se rozlišení alespoň 293x205 px. Pokud toto není nastaveno, bude zobrazen výchozí maskot title: Obrázek maskota peers_api_enabled: - desc_html: Domény, na které tato instance narazila ve fediverse - title: Zveřejnit seznam objevených instancí + desc_html: Domény, na které tento server narazil ve fediverse + title: Zveřejnit seznam objevených serverů preview_sensitive_media: desc_html: Náhledy odkazů na jiných stránkách budou zobrazeny i pokud jsou media označena jako citlivá title: Zobrazovat v náhledech OpenGraph i citlivá média @@ -427,20 +427,20 @@ cs: title: Zobrazit odznak personálu site_description: desc_html: Úvodní odstavec na hlavní straně. Popište, díky čemu je tento server Mastodon zvláštní, a cokoliv jiného, co je důležité. Můžete zde používat HTML značky, hlavně <code><a></code> a <code><em></code>. - title: Popis instance + title: Popis serveru site_description_extended: - desc_html: Dobré místo pro vaše pravidla, pokyny a jiné věci, které vaši instanci odlišují od ostatních. Lze použít HTML značky + desc_html: Dobré místo pro vaše pravidla, pokyny a jiné věci, které váš server odlišují od ostatních. Lze použít HTML značky title: Vlastní doplňující informace site_short_description: - desc_html: Zobrazen v postranním panelu a meta značkách. Popište, co je Mastodon a díky čemu je tento server zvláštní v jediném odstavci. Je-li tohle prázdné, zobrazí se popis instance. - title: Krátký popis instance + desc_html: Zobrazen v postranním panelu a meta značkách. Popište, co je Mastodon a díky čemu je tento server zvláštní v jediném odstavci. Je-li tohle prázdné, zobrazí se popis serveru. + title: Krátký popis serveru site_terms: desc_html: Můžete si napsat vlastní zásady soukromí, podmínky používání či jiné legality. Můžete použít HTML značky title: Vlastní podmínky používání - site_title: Název instance + site_title: Název serveru thumbnail: desc_html: Používáno pro náhledy přes OpenGraph a API. Doporučuje se rozlišení 1200x630px - title: Miniatura instance + title: Miniatura serveru timeline_preview: desc_html: Zobrazit na hlavní straně veřejnou časovou osu title: Náhled časové osy @@ -501,7 +501,7 @@ cs: warning: Buďte s těmito daty velmi opatrní. Nikdy je s nikým nesdílejte! your_token: Váš přístupový token auth: - agreement_html: Kliknutím na tlačítko „Registrovat“ souhlasíte s následováním <a href="%{rules_path}">pravidel této instance</a> a <a href="%{terms_path}">našich podmínek používání</a>. + agreement_html: Kliknutím na tlačítko „Registrovat“ souhlasíte s následováním <a href="%{rules_path}">pravidel tohoto serveru</a> a <a href="%{terms_path}">našich podmínek používání</a>. change_password: Heslo confirm_email: Potvrdit e-mail delete_account: Odstranit účet @@ -555,12 +555,12 @@ cs: description_html: Tímto <strong>trvale a nenávratně</strong> odstraníte obsah z vašeho účtu a deaktivujete ho. Vaše uživatelské jméno zůstane rezervované pro zabránění budoucím napodobováním. proceed: Odstranit účet success_msg: Váš účet byl úspěšně odstraněn - warning_html: Pouze vymazání obsahu z této konkrétní instance je zaručeno. Obsah, který byl široce sdílen, po sobě pravděpodobně zanechá stopy. U offline serverů a serverů, které vaše aktualizace již neodebírají, nebudou databáze aktualizovány. + warning_html: Pouze vymazání obsahu z tohoto konkrétního serveru je zaručeno. Obsah, který byl široce sdílen, po sobě pravděpodobně zanechá stopy. U offline serverů a serverů, které vaše aktualizace již neodebírají, nebudou databáze aktualizovány. warning_title: Dostupnost rozšířeného obsahu directories: directory: Adresář profilů enabled: Aktuálně jste v adresáři uveden/a. - enabled_but_waiting: Přihlásil/a jste se k uvedení v adresáři, ale ještě nemáte minimální počet sledovatelů (%{min_followers}) pro uvedení. + enabled_but_waiting: Přihlásil/a jste se k uvedení v adresáři, ale ještě nemáte minimální počet sledujících (%{min_followers}) pro uvedení. explanation: Objevujte uživatele podle jejich zájmů explore_mastodon: Prozkoumejte %{title} how_to_enable: Aktuálně nejste přihlášen/a do adresáře. Přihlásit se můžete níže. Použijte ve svém popisu profilu hashtagy, abyste mohl/a být uveden/a pod konkrétními hashtagy! @@ -613,17 +613,17 @@ cs: title: Přidat nový filtr followers: domain: Doména - explanation_html: Chcete-li zaručit soukromí vašich příspěvků, musíte mít na vědomí, kdo vás sleduje. <strong>Vaše soukromé příspěvky jsou doručeny na všechny instance, kde máte sledovatele</strong>. Nejspíš si je budete chtít zkontrolovat a odstranit sledovatele na instancích, jejichž personálu či softwaru nedůvěřujete s respektováním vašeho soukromí. - followers_count: Počet sledovatelů + explanation_html: Chcete-li zaručit soukromí vašich příspěvků, musíte mít na vědomí, kdo vás sleduje. <strong>Vaše soukromé příspěvky jsou doručeny na všechny servery, kde máte sledující</strong>. Nejspíš si je budete chtít zkontrolovat a odstranit sledující na serverech, jejichž provozovatelům či softwaru nedůvěřujete s respektováním vašeho soukromí. + followers_count: Počet sledujících lock_link: Zamkněte svůj účet - purge: Odstranit ze sledovatelů + purge: Odstranit ze sledujících success: - few: V průběhu blokování sledovatelů ze %{count} domén... - one: V průběhu blokování sledovatelů z jedné domény... - other: V průběhu blokování sledovatelů z %{count} domén... + few: V průběhu blokování sledujících ze %{count} domén... + one: V průběhu blokování sledujících z jedné domény... + other: V průběhu blokování sledujících z %{count} domén... true_privacy_html: Berte prosím na vědomí, že <strong>skutečného soukromí se dá dosáhnout pouze za pomoci end-to-end šifrování</strong>. - unlocked_warning_html: Kdokoliv vás může sledovat a okamžitě vidět vaše soukromé příspěvky. %{lock_link}, abyste mohl/a zkontrolovat a odmítnout sledovatele. - unlocked_warning_title: Váš účet není zamknutý + unlocked_warning_html: Kdokoliv vás může sledovat a okamžitě vidět vaše soukromé příspěvky. %{lock_link}, abyste mohl/a kontrolovat a odmítat sledující. + unlocked_warning_title: Váš účet není uzamčen footer: developers: Vývojáři more: Více… @@ -637,7 +637,7 @@ cs: one: Něco ještě není úplně v pořádku! Prosím zkontrolujte chybu níže other: Něco ještě není úplně v pořádku! Prosím zkontrolujte %{count} chyb níže imports: - preface: Můžete importovat data, která jste exportoval/a z jiné instance, jako například seznam lidí, které sledujete či blokujete. + preface: Můžete importovat data, která jste exportoval/a z jiného serveru, jako například seznam lidí, které sledujete či blokujete. success: Vaše data byla úspěšně nahrána a nyní budou zpracována v daný čas types: blocking: Seznam blokovaných @@ -663,7 +663,7 @@ cs: one: 1 použití other: "%{count} použití" max_uses_prompt: Bez limitu - prompt: Vygenerujte a sdílejte s ostatními odkazy a umožněte jim přístup na tuto instanci + prompt: Vygenerujte a sdílejte s ostatními odkazy a umožněte jim přístup na tento server table: expires_at: Vyprší uses: Použití @@ -688,9 +688,9 @@ cs: body: Zde najdete stručný souhrn zpráv, které jste zmeškal/a od vaší poslední návštěvy %{since} mention: "%{name} vás zmínil/a v:" new_followers_summary: - few: Navíc jste získal/a %{count} nové sledovatele, zatímco jste byl/a pryč! Skvělé! - one: Navíc jste získal/a jednoho nového sledovatele, zatímco jste byl/a pryč! Hurá! - other: Navíc jste získal/a %{count} nových sledovatelů, zatímco jste byl/a pryč! Úžasné! + few: Navíc jste získal/a %{count} nové sledující, zatímco jste byl/a pryč! Skvělé! + one: Navíc jste získal/a jednoho nového sledujícího, zatímco jste byl/a pryč! Hurá! + other: Navíc jste získal/a %{count} nových sledujících, zatímco jste byl/a pryč! Úžasné! subject: few: "%{count} nová oznámení od vaší poslední návštěvy \U0001F418" one: "1 nové oznámení od vaší poslední návštěvy \U0001F418" @@ -703,11 +703,11 @@ cs: follow: body: "%{name} vás nyní sleduje!" subject: "%{name} vás nyní sleduje" - title: Nový sledovatel + title: Nový sledující follow_request: action: Spravovat požadavky o sledování body: "%{name} požádal/a o povolení vás sledovat" - subject: 'Čekající sledovatel: %{name}' + subject: 'Čekající sledující: %{name}' title: Nový požadavek o sledování mention: action: Odpovědět @@ -812,7 +812,7 @@ cs: development: Vývoj edit_profile: Upravit profil export: Export dat - followers: Autorizovaní sledovatelé + followers: Autorizovaní sledující import: Import migrate: Přesunutí účtu notifications: Oznámení @@ -849,8 +849,8 @@ cs: sign_in_to_participate: Chcete-li se účastnit této konverzace, přihlaste se title: "%{name}: „%{quote}“" visibilities: - private: Pouze pro sledovatele - private_long: Zobrazit pouze sledovatelům + private: Pouze pro sledující + private_long: Zobrazit pouze sledujícím public: Veřejné public_long: Všichni mohou vidět unlisted: Neuvedené @@ -866,8 +866,8 @@ cs: <ul> <li><em>Základní informace o účtu</em>: Pokud se na tomto serveru zaregistrujete, můžeme vás požádat o zadání uživatelského jména, e-mailové adresy a hesla. Můžete také zadat dodatečné profilové informace, jako například zobrazované jméno a krátký životopis, a nahrát si profilovou fotografii a hlavičkový obrázek. Uživatelské i zobrazované jméno, životopis, profilová fotografie a hlavičkový obrázek jsou vždy uvedeny veřejně.</li> - <li><em>Příspěvky, sledovatelé a další veřejné informace</em>: Seznam lidí, které sledujete, je uveden veřejně, totéž platí i pro vaše sledovatele. Když sem nahrajete zprávu, bude uloženo datum a čas, společně s aplikací, ze které jste zprávu odeslali. Zprávy mohou obsahovat mediální přílohy, jako jsou obrázky a videa. Veřejné a neuvedené příspěvky jsou dostupné veřejně. Pokud na vašem profilu uvedete příspěvek, je to také veřejně dostupná informace. Vaše příspěvky jsou doručeny vašim sledovatelům, což v některých případech znamená, že budou doručeny na různé servery, na kterých budou ukládány kopie. Pokud příspěvky smažete, bude tohle taktéž doručeno vašim sledovatelům. Akce znovusdílení nebo oblíbení jiného příspěvku je vždy veřejná.</li> - <li><em>Příspěvky přímé a pouze pro sledovatele</em>: Všechny příspěvky jsou uloženy a zpracovány na serveru. Příspěvky pouze pro sledovatele jsou doručeny vašim sledovatelům a uživatelům v nich zmíněným a přímé příspěvky jsou doručeny pouze uživatelům v nich zmíněným. V některých případech tohle znamená, že budou doručeny na různé servery, na kterých budou ukládány kopie. Snažíme se omezit přístup k těmto příspěvkům pouze na autorizované uživatele, ovšem jiné servery tak nemusejí učinit. Proto je důležité posoudit servery, ke kterým vaši sledovatelé patří. V nastavení si můžete zapnout volbu pro manuální schvalování či odmítnutí nových sledovatelů. <em>Prosím mějte na paměti, že operátoři tohoto serveru a kteréhokoliv přijímacího serveru mohou tyto zprávy vidět</em> a příjemci mohou vytvořit jejich snímek, zkopírovat je, nebo je jinak sdílet. <em>Nesdílejte přes Mastodon jakékoliv nebezpečné informace.</em></li> + <li><em>Příspěvky, sledující a další veřejné informace</em>: Seznam lidí, které sledujete, je uveden veřejně, totéž platí i pro vaše sledující. Když sem nahrajete zprávu, bude uloženo datum a čas, společně s aplikací, ze které jste zprávu odeslali. Zprávy mohou obsahovat mediální přílohy, jako jsou obrázky a videa. Veřejné a neuvedené příspěvky jsou dostupné veřejně. Pokud na vašem profilu uvedete příspěvek, je to také veřejně dostupná informace. Vaše příspěvky jsou doručeny vašim sledujícím, což v některých případech znamená, že budou doručeny na různé servery, na kterých budou ukládány kopie. Pokud příspěvky smažete, bude tohle taktéž doručeno vašim sledujícím. Akce znovusdílení nebo oblíbení jiného příspěvku je vždy veřejná.</li> + <li><em>Příspěvky přímé a pouze pro sledující</em>: Všechny příspěvky jsou uloženy a zpracovány na serveru. Příspěvky pouze pro sledující jsou doručeny vašim sledujícím a uživatelům v nich zmíněným a přímé příspěvky jsou doručeny pouze uživatelům v nich zmíněným. V některých případech tohle znamená, že budou doručeny na různé servery, na kterých budou ukládány kopie. Snažíme se omezit přístup k těmto příspěvkům pouze na autorizované uživatele, ovšem jiné servery tak nemusejí učinit. Proto je důležité posoudit servery, ke kterým vaši sledující patří. V nastavení si můžete zapnout volbu pro manuální schvalování či odmítnutí nových sledujících. <em>Prosím mějte na paměti, že operátoři tohoto serveru a kteréhokoliv přijímacího serveru mohou tyto zprávy vidět</em> a příjemci mohou vytvořit jejich snímek, zkopírovat je, nebo je jinak sdílet. <em>Nesdílejte přes Mastodon jakékoliv nebezpečné informace.</em></li> <li><em>IP adresy a další metadata</em>: Když se přihlásíte, zaznamenáváme IP adresu, ze které se přihlašujete, jakožto i název vašeho webového prohlížeče. Všechny vaše webové relace jsou v nastavení přístupné k vašemu posouzení a odvolání. Nejpozdější IP adresa použita je uložena maximálně do 12 měsíců. Můžeme také uchovávat serverové záznamy, které obsahují IP adresy každého požadavku odeslaného na náš server.</li> </ul> @@ -908,7 +908,7 @@ cs: <h3 id="cookies">Používáme cookies?</h3> - <p>Ano. Cookies jsou malé soubory, které stránka nebo její poskytovatel uloží na pevný disk vašeho počítače (pokud to dovolíte). Tyto cookies umožňují stránce rozpoznat váš prohlížeč a, pokud máte registrovaný účet, přidružit ho s vaším registrovaným účtem.</p> + <p>Ano. Cookies jsou malé soubory, které stránka nebo její poskytovatel uloží na pevný disk vašeho počítače (pokud to dovolíte). Tyto cookies umožňují stránce rozpoznat váš prohlížeč, a pokud máte registrovaný účet, přidružit ho s vaším registrovaným účtem.</p> <p>Používáme cookies pro pochopení a ukládání vašich předvoleb pro budoucí návštěvy.</p> @@ -918,15 +918,15 @@ cs: <p>Vaše osobně identifikovatelné informace neprodáváme, neobchodujeme s nimi, ani je nijak nepřenášíme vnějším stranám. Do tohoto se nepočítají důvěryhodné třetí strany, které nám pomáhají provozovat naši stránku, podnikat, nebo vás obsluhovat, pokud tyto strany souhlasí se zachováním důvěrnosti těchto informací. Můžeme také uvolnit vaše informace, pokud věříme, že je to nutné pro soulad se zákonem, prosazování našich zásad, nebo ochranu práv, majetku, či bezpečnost nás či ostatních.</p> - <p>Váš veřejný obsah může být stažen jinými servery na síti. Vaše příspěvky veřejné a pouze pro sledovatele budou doručeny na servery vašich sledovatelů a přímé zprávy budou doručeny na servery příjemců, pokud jsou tito sledovatelé nebo příjemci zaregistrováni na jiném serveru, než je tento.</p> + <p>Váš veřejný obsah může být stažen jinými servery na síti. Vaše příspěvky veřejné a pouze pro sledující budou doručeny na servery vašich sledujících a přímé zprávy budou doručeny na servery příjemců, pokud jsou tito sledující nebo příjemci zaregistrováni na jiném serveru, než je tento.</p> - <p>Když autorizujete aplikaci, aby používala váš účet, může, v závislosti na rozsahu oprávnění, které jí udělíte, přistupovat k vašim veřejným profilovým informacím, seznamu lidí, které sledujete, vašim sledovatelům, vašim seznamům, všem vašim příspěvkům a příspěvkům, které jste si oblíbili. Aplikace nikdy nemohou získat vaši e-mailovou adresu či heslo.</p> + <p>Když autorizujete aplikaci, aby používala váš účet, může, v závislosti na rozsahu oprávnění, které jí udělíte, přistupovat k vašim veřejným profilovým informacím, seznamu lidí, které sledujete, vašim sledujícím, vašim seznamům, všem vašim příspěvkům a příspěvkům, které jste si oblíbili. Aplikace nikdy nemohou získat vaši e-mailovou adresu či heslo.</p> <hr class="spacer" /> <h3 id="children">Používání stránky dětmi</h3> - <p>Pokud se tento server nachází v EU nebo EHP: Naše stránka, produkty a služby jsou všechny směřovány na lidi, kterým je alespoň 16 let. Pokud je vám méně než 16, dle požadavků nařízení GDPR (<a href="https://cs.wikipedia.org/wiki/Obecn%C3%A9_na%C5%99%C3%ADzen%C3%AD_o_ochran%C4%9B_osobn%C3%ADch_%C3%BAdaj%C5%AF">Obecné nařízení o ochraně sobních údajů</a>) tuto stránku nepoužívejte.</p> + <p>Pokud se tento server nachází v EU nebo EHP: Naše stránka, produkty a služby jsou všechny směřovány na lidi, kterým je alespoň 16 let. Pokud je vám méně než 16, dle požadavků nařízení GDPR (<a href="https://cs.wikipedia.org/wiki/Obecn%C3%A9_na%C5%99%C3%ADzen%C3%AD_o_ochran%C4%9B_osobn%C3%ADch_%C3%BAdaj%C5%AF">Obecné nařízení o ochraně osobních údajů</a>) tuto stránku nepoužívejte.</p> <p>Pokud se tento server nachází v USA: Naše stránka, produkty a služby jsou všechny směřovány na lidi, kterým je alespoň 13 let. Pokud je vám méně než 13, dle požadavků zákona COPPA (<a href="https://cs.wikipedia.org/wiki/Children%27s_online_privacy_protection_act">Children's Online Privacy Protection Act</a>) tuto stránku nepoužívejte.</p> @@ -975,7 +975,7 @@ cs: explanation: disable: Zatímco je váš účet zmražen, zůstávají data vašeho účtu nedotčená, ale nemůžete vykonávat žádné akce, dokud nebude odemčen. silence: Zatímco je váš účet omezen, mohou vaše tooty na tomto serveru vidět pouze lidé, kteří váš již sledují, a můžete být vyloučen/a z různých veřejných výpisů. Ostatní vás však pořád mohou manuálně sledovat. - suspend: Váš účet byl pozastaven a všechny vaše tooty a vaše nahrané mediální soubory byly nenávratně odstraněny z tohoto serveru a serverů, na kterých jste měl/a sledovatele. + suspend: Váš účet byl pozastaven a všechny vaše tooty a vaše nahrané mediální soubory byly nenávratně odstraněny z tohoto serveru a serverů, na kterých jste měl/a sledující. review_server_policies: Posoudit politiku serveru subject: disable: Váš účet %{acct} byl zmražen @@ -989,12 +989,12 @@ cs: suspend: Účet pozastaven welcome: edit_profile_action: Nastavit profil - edit_profile_step: Můžete si přizpůsobit svůj profil nahráním avataru a obrázku na hlavičce, změnou zobrazovaného jména a dalších. Chcete-li posoudit nové sledovatele předtím, než vás mohou sledovat, můžete svůj účet uzamknout. + edit_profile_step: Můžete si přizpůsobit svůj profil nahráním avataru a obrázku na hlavičce, změnou zobrazovaného jména a dalších. Chcete-li posoudit nové sledující předtím, než vás mohou sledovat, můžete svůj účet uzamknout. explanation: Zde je pár tipů na začátek final_action: Začněte přispívat - final_step: 'Začněte přispívat! I když nemáte sledovatele, mohou vaše zprávy vidět jiní lidé, například na místní časové ose a mezi hashtagy. Můžete se ostatním představit pomocí hashtagu #introductions.' + final_step: 'Začněte přispívat! I když nemáte sledující, mohou vaše zprávy vidět jiní lidé, například na místní časové ose a mezi hashtagy. Můžete se ostatním představit pomocí hashtagu #introductions.' full_handle: Vaše celá adresa profilu - full_handle_hint: Tohle je, co byste řekl/a svým přátelům, aby vám mohli posílat zprávy nebo vás sledovat z jiné instance. + full_handle_hint: Tohle je, co byste řekl/a svým přátelům, aby vám mohli posílat zprávy nebo vás sledovat z jiného serveru. review_preferences_action: Změnit nastavení review_preferences_step: Nezapomeňte si nastavit své volby, například jaké e-maily chcete přijímat či jak soukromé mají být vaše příspěvky ve výchozím stavu. Nemáte-li epilepsii, můžete si nastavit automatické přehrávání obrázků GIF. subject: Vítejte na Mastodonu diff --git a/config/locales/cy.yml b/config/locales/cy.yml index 53b474c16..f225cc086 100644 --- a/config/locales/cy.yml +++ b/config/locales/cy.yml @@ -60,6 +60,7 @@ cy: zero: Dilynwyr following: Yn dilyn joined: Ymunodd %{date} + last_active: diweddaraf link_verified_on: Gwiriwyd perchnogaeth y ddolen yma ar %{date} media: Cyfryngau moved_html: 'Mae %{name} wedi symud i %{new_profile_link}:' @@ -87,6 +88,7 @@ cy: admin: account_actions: action: Cyflawni gweithred + title: Perfformio cymedroli ar %{acct} account_moderation_notes: create: Gadael nodyn created_msg: Crewyd nodyn cymedroli yn llwyddiannus! @@ -155,6 +157,7 @@ cy: push_subscription_expires: Tanysgrifiad PuSH yn dod i ben redownload: Adnewyddu proffil remove_avatar: Dileu afatar + remove_header: Dileu pennawd resend_confirmation: already_confirmed: Mae'r defnyddiwr hwn wedi ei gadarnhau yn barod send: Ailanfonwch e-bost cadarnhad @@ -172,8 +175,8 @@ cy: search: Chwilio shared_inbox_url: URL Mewnflwch wedi ei rannu show: - created_reports: Adroddiadau a grewyd gan y cyfri hwn - targeted_reports: Adroddiadau am y cyfri hwn + created_reports: Adroddiadau a wnaed + targeted_reports: Adroddwyd gan eraill silence: Tawelu silenced: Tawelwyd statuses: Statysau @@ -192,6 +195,7 @@ cy: assigned_to_self_report: Aseiniodd %{name} adroddiad %{target} i'w hunan change_email_user: Newidodd %{name} gyfeiriad e-bost y defnyddiwr %{target} confirm_user: Cadarnhaodd %{name} gyfeiriad e-bost y defnyddiwr %{target} + create_account_warning: Anfonwyd rhybudd i %{target} gan %{name} create_custom_emoji: Uwchlwythodd %{name} emoji newydd %{target} create_domain_block: Blociodd %{name} y parth %{target} create_email_domain_block: Cosbrestrwyd parth e-bost %{target} gan %{name} @@ -250,6 +254,7 @@ cy: config: Cyfluniad feature_deletions: Dileadau cyfrif feature_invites: Dolenni gwahodd + feature_profile_directory: Cyfeiriadur proffil feature_registrations: Cofrestriadau feature_relay: Relái ffederasiwn features: Nodweddion @@ -267,7 +272,7 @@ cy: week_users_active: gweithredol yr wythnos hon week_users_new: defnyddwyr yr wythnos hon domain_blocks: - add_new: Ychwanegu + add_new: Ychwanegu bloc parth newydd created_msg: Mae'r bloc parth nawr yn cael ei brosesu destroyed_msg: Mae'r bloc parth wedi ei ddadwneud domain: Parth @@ -284,14 +289,25 @@ cy: reject_media_hint: Dileu dogfennau cyfryngau wedi eu cadw yn lleol ac yn gwrthod i lawrlwytho unrhyw rai yn y dyfodol. Amherthnasol i ataliadau reject_reports: Gwrthod adroddiadau reject_reports_hint: Anwybyddu'r holl adroddiadau sy'n dod o'r parth hwn. Amherthnasol i ataliadau + rejecting_media: Yn gwrthod ffeiliau cyfryngau + rejecting_reports: Yn gwrthod adroddiadau + severity: + silence: tawelu + suspend: ataliedig show: - affected_accounts: "%{count} o gyfrifoedd yn y bas data wedi eu hefeithio" + affected_accounts: + few: Effeithiwyd ar %{count} gyfrifon + many: Effeithiwyd ar %{count} gyfrifon + one: Effeithiwyd ar un cyfrif + other: Effeithiwyd ar %{count} gyfrifon + two: Effeithiwyd ar %{count} gyfrifon + zero: Effeithiwyd ar %{count} gyfrifon retroactive: silence: Dad-dawelu pob cyfri presennol o'r parth hwn suspend: Dad-atal pob cyfrif o'r parth hwn sy'n bodoli title: Dadwneud blocio parth ar gyfer %{domain} undo: Dadwneud - undo: Dadwneud + undo: Dadwneud bloc parth email_domain_blocks: add_new: Ychwanegu created_msg: Llwyddwyd i ychwanegu parth e-bost i'r gosbrestr @@ -302,11 +318,28 @@ cy: create: Ychwanegu parth title: Cofnod newydd yng nghosbrestr e-byst title: Cosbrestr e-bost + followers: + back_to_account: Nôl i'r gyfrif + title: Dilynwyr %{acct} instances: + delivery_available: Mae'r cyflenwad ar gael + known_accounts: + few: "%{count} cyfrifon hysbys" + many: "%{count} cyfrifon hysbys" + one: "%{count} cyfrif hysbys" + other: "%{count} cyfrifon hysbys" + two: "%{count} cyfrifon hysbys" + zero: "%{count} cyfrifon hysbys" moderation: all: Pob limited: Gyfyngedig + title: Cymedroli title: Ffederasiwn + total_blocked_by_us: Wedi'i bloc gan ni + total_followed_by_them: Yn dilyn ganynt + total_followed_by_us: Yn dilyn ganom ni + total_reported: Adroddiadau amdanynt + total_storage: Atodiadau cyfryngau invites: deactivate_all: Diffodd pob un filter: @@ -388,6 +421,9 @@ cy: preview_sensitive_media: desc_html: Bydd rhagolygon ar wefannau eraill yn dangos ciplun hyd yn oed os oes na gyfryngau wedi eu marcio'n sensitif title: Dangos cyfryngau sensitif mewn rhagolygon OpenGraph + profile_directory: + desc_html: Caniatáu i ddefnyddwyr gael eu gweld + title: Galluogi cyfeiriadur proffil registrations: closed_message: desc_html: I'w arddangos ar y dudalen flaen wedi i gofrestru cau. Mae modd defnyddio tagiau HTML @@ -450,9 +486,14 @@ cy: tags: accounts: Cyfrifon hidden: Cudd + hide: Cuddio o gyfeiriadur name: Hashnod + title: Hashnodau + unhide: Dangoswch yn y cyfeiriadur + visible: Gweladwy title: Gweinyddiaeth warning_presets: + add_new: Ychwanegu newydd delete: Dileu edit: Golygu admin_mailer: @@ -532,6 +573,20 @@ cy: success_msg: Llwyddwyd i ddileu eich cyfrif warning_html: Dim ond dileu cynnwys o'r achos hwn ellid bod yn sicr ei fod wedi ei ddileu. Mae cynnwys sydd wedi ei rannu'n eang yn debygol o adael olion. Ni fydd gweinyddwyr all-lein a gweinyddwyr sydd wedi dad-danysgrifio o'ch diwedderiadau ddim yn diweddaru eu cronfeydd data. warning_title: Argaeledd cynnwys wedi'i rannu + directories: + directory: Cyfeiriadur proffil + enabled: Rydych chi wedi'ch rhestru yn y cyfeiriadur ar hyn o bryd. + enabled_but_waiting: Rydych wedi dewis i chi gael eich rhestru yn y cyfeiriadur, ond nid oes gennych y nifer lleiaf o ddilynwyr (%{min_followers}) i'w rhestru eto. + explanation: Darganfod defnyddwyr yn seiliedig ar eu diddordebau + explore_mastodon: Archwilio %{title} + how_to_enable: Ar hyn o bryd nid ydych chi wedi dewis y cyfeiriadur. Gallwch ddewis i mewn isod. Defnyddiwch hashnodau yn eich bio-destun i'w restru dan hashnodau penodol! + people: + few: "%{count} personau" + many: "%{count} personau" + one: "%{count} person" + other: "%{count} personau" + two: "%{count} personau" + zero: "%{count} personau" errors: '403': Nid oes gennych ganiatad i weld y dudalen hon. '404': Nid yw'r dudalen yr oeddech yn chwilio amdani'n bodoli. @@ -554,7 +609,9 @@ cy: size: Maint blocks: Yr ydych yn blocio csv: CSV + domain_blocks: Blociau parth follows: Yr ydych yn dilyn + lists: Rhestrau mutes: Yr ydych yn tawelu storage: Storio cyfryngau filters: @@ -579,7 +636,13 @@ cy: followers_count: Nifer y dilynwyr lock_link: Cloi eich cyfrif purge: Dileu o dilynwyr - success: Yn y broses o ysgafn-flocio dilynwyr o %{count} parth... + success: + few: Yn y broses o ysgafn-flocio defnyddwyr o %{count} parth... + many: Yn y broses o ysgafn-flocio defnyddwyr o %{count} parth... + one: Yn y broses o ysgafn-flocio dilynwyr o un parth... + other: Yn y broses o ysgafn-flocio defnyddwyr o %{count} parth... + two: Yn y broses o ysgafn-flocio defnyddwyr o %{count} parth... + zero: Yn y broses o ysgafn-flocio defnyddwyr o %{count} parth... true_privacy_html: Cofiwch <strong>mai ond amgryptio pen-i-ben all sicrhau gwir breifatrwydd</strong>. unlocked_warning_html: Gall unrhywun eich dilyn yn syth i weld eich tŵtiau preifat. %{lock_link} i gael adolygu a gwrthod dilynwyr. unlocked_warning_title: Nid yw eich cyfrif wedi ei gloi @@ -591,7 +654,13 @@ cy: changes_saved_msg: Llwyddwyd i gadw y newidiadau! copy: Copïo save_changes: Cadw newidiadau - validation_errors: Mae rhywbeth o'i le o hyd! Edrychwch ar y %{count} gwall isod os gwelwch yn dda + validation_errors: + few: Mae rhywbeth o'i le o hyd! Edrychwch ar y %{count} gwall isod os gwelwch yn dda + many: Mae rhywbeth o'i le o hyd! Edrychwch ar y %{count} gwall isod os gwelwch yn dda + one: Mae rhywbeth o'i le o hyd! Edrychwch ar y gwall isod os gwelwch yn dda + other: Mae rhywbeth o'i le o hyd! Edrychwch ar y %{count} gwall isod os gwelwch yn dda + two: Mae rhywbeth o'i le o hyd! Edrychwch ar y %{count} gwall isod os gwelwch yn dda + zero: Mae rhywbeth o'i le o hyd! Edrychwch ar y %{count} gwall isod os gwelwch yn dda imports: preface: Mae modd mewnforio data yr ydych wedi allforio o achos arall, megis rhestr o bobl yr ydych yn ei ddilyn neu yn blocio. success: Uwchlwythwyd eich data yn llwyddiannus ac fe fydd yn cael ei brosesu mewn da bryd @@ -614,7 +683,13 @@ cy: expires_in_prompt: Byth generate: Cynhyrchu invited_by: 'Cawsoch eich gwahodd gan:' - max_uses: "%{count} defnydd" + max_uses: + few: "%{count} defnydd" + many: "%{count} defnydd" + one: 1 iws + other: "%{count} defnydd" + two: "%{count} defnydd" + zero: "%{count} defnydd" max_uses_prompt: Dim terfyn prompt: Cynhyrchwch a rhannwch ddolenni gyda eraill i ganiatau mynediad i'r achos hwn table: @@ -640,8 +715,20 @@ cy: action: Gweld holl hysbysiadau body: Dyma grynodeb byr o'r holl negeseuon golloch chi ers eich ymweliad diwethaf ar %{since} mention: 'Soniodd %{name} amdanoch chi:' - new_followers_summary: Hefyd, rydych wedi ennill %{count} dilynwr newydd tra eich bod i ffwrdd! Hwrê! - subject: "%{count} hysbysiad newydd ers eich ymweliad diwethaf \U0001F418" + new_followers_summary: + few: Hefyd, rydych wedi ennill %{count} dilynwr newydd tra eich bod i ffwrdd! Hwrê! + many: Hefyd, rydych wedi ennill %{count} dilynwr newydd tra eich bod i ffwrdd! Hwrê! + one: Yr ydych wedi ennill dilynwr newydd tra eich bod i ffwrdd! Hwrê! + other: Hefyd, rydych wedi ennill %{count} dilynwr newydd tra eich bod i ffwrdd! Hwrê! + two: Hefyd, rydych wedi ennill %{count} dilynwr newydd tra eich bod i ffwrdd! Hwrê! + zero: Hefyd, rydych wedi ennill %{count} dilynwr newydd tra eich bod i ffwrdd! Hwrê! + subject: + few: "%{count} hysbysiad newydd ers eich ymweliad diwethaf" + many: "%{count} hysbysiad newydd ers eich ymweliad diwethaf" + one: 1 hysbysiad newydd ers eich ymweliad diwethaf + other: "%{count} hysbysiad newydd ers eich ymweliad diwethaf" + two: "%{count} hysbysiad newydd ers eich ymweliad diwethaf" + zero: "%{count} hysbysiad newydd ers eich ymweliad diwethaf" title: Yn eich absenoldeb... favourite: body: 'Cafodd eich statws ei hoffi gan %{name}:' @@ -755,7 +842,13 @@ cy: statuses: attached: description: 'Ynghlwm: %{attached}' - image: "%{count} o luniau" + image: + few: "%{count} o luniau" + many: "%{count} o luniau" + one: "%{count} llun" + other: "%{count} o luniau" + two: "%{count} o luniau" + zero: "%{count} o luniau" video: few: "%{count} fideo" many: "%{count} fideo" @@ -765,7 +858,13 @@ cy: zero: "%{count} fideo" boosted_from_html: Wedi ei fŵstio %{acct_link} content_warning: 'Rhybudd cynnwys: %{warning}' - disallowed_hashtags: 'yn cynnwys yr hashnod gwaharddedig: %{tags}' + disallowed_hashtags: + few: 'yn cynnwys yr hashnod gwaharddedig: %{tags}' + many: 'yn cynnwys yr hashnod gwaharddedig: %{tags}' + one: 'yn cynnwys hashnod gwaharddedig: %{tags}' + other: 'yn cynnwys yr hashnod gwaharddedig: %{tags}' + two: 'yn cynnwys yr hashnod gwaharddedig: %{tags}' + zero: 'yn cynnwys yr hashnod gwaharddedig: %{tags}' language_detection: Canfod iaith yn awtomataidd open_in_web: Agor yn y wê over_character_limit: wedi mynd heibio'r uchafswm nodyn o %{max} @@ -901,6 +1000,14 @@ cy: subject: Mae eich archif yn barod i'w lawrlwytho title: Allfudo archif warning: + explanation: + disable: Er bod eich cyfrif wedi'i rewi, mae eich data cyfrif yn parhau i fod yn gyfan, ond ni allwch chi berfformio unrhyw gamau nes ei ddatgloi. + review_server_policies: Adolygu polisïau'r gweinydd + subject: + disable: Mae'ch cyfrif %{acct} wedi'i rewi + none: Rhybudd am %{acct} + silence: Mae'ch cyfrif %{acct} wedi bod yn gyfyngedig + suspend: Mae'ch cyfrif %{acct} wedi'i atal title: disable: Cyfrif wedi'i rewi none: Rhybudd @@ -912,7 +1019,7 @@ cy: explanation: Dyma ambell nodyn i'ch helpu i ddechrau final_action: Dechrau postio final_step: 'Dechrau postio! Hyd yn oed heb ddilynwyr mae''n bosib i eraill weld eich negeseuon cyhoeddus, er enghraifft at y ffrwd leol ac mewn hashnodau. Mae''n bosib yr hoffech hi gyflwyno''ch hun ar yr hashnod #introductions.' - full_handle: Eich enw Mastodon llawn + full_handle: Eich enw llawn full_handle_hint: Dyma'r hyn y bysech yn dweud wrth eich ffrindiau er mwyn iddyn nhw gael anfon neges atoch o achos arall. review_preferences_action: Newid dewisiadau review_preferences_step: Gwnewch yn siŵr i chi osod eich dewisiadau, megis pa e-byst hoffech eu derbyn, neu ba lefel preifatrwydd hoffech eich tŵtiau ragosod i. Os nad oes gennych salwch symud, gallwch ddewis i ganiatau chwarae GIFs yn awtomatig. diff --git a/config/locales/devise.cs.yml b/config/locales/devise.cs.yml index 850ff1fe8..83534cccd 100644 --- a/config/locales/devise.cs.yml +++ b/config/locales/devise.cs.yml @@ -20,17 +20,17 @@ cs: action: Potvrdit e-mailovou adresu action_with_app: Potvrdit a navrátit se do %{app} explanation: S touto e-mailovou adresou jste si vytvořil/a účet na %{host}. K jeho aktivaci vám zbývá jedno kliknutí. Pokud jste to nebyl/a vy, ignorujte této e-mail. - extra_html: Prosím podívejte se také na <a href="%{terms_path}">pravidla této instance</a> a <a href="%{policy_path}">naše podmínky používání</a>. + extra_html: Prosím podívejte se také na <a href="%{terms_path}">pravidla tohoto serveru</a> a <a href="%{policy_path}">naše podmínky používání</a>. subject: 'Mastodon: Potvrzovací instrukce pro %{instance}' title: Potvrďte e-mailovou adresu email_changed: explanation: 'E-mailová adresa vašeho účtu byla změněna na:' - extra: Pokud jste si e-mail nezměnil/a, je pravděpodobné, že někdo jiný získal přístup k vašemu účtu. Prosím změňte si okamžitě heslo, nebo, pokud se nemůžete na účet přihlásit, kontaktujte administrátora instance. + extra: Pokud jste si e-mail nezměnil/a, je pravděpodobné, že někdo jiný získal přístup k vašemu účtu. Prosím změňte si okamžitě heslo, nebo, pokud se nemůžete na účet přihlásit, kontaktujte administrátora serveru. subject: 'Mastodon: E-mail byl změněn' title: Nová e-mailová adresa password_change: explanation: Heslo k vašemu účtu bylo změněno. - extra: Pokud jste si heslo nezměnil/a, je pravděpodobné, že někdo jiný získal přístup k vašemu účtu. Prosím změňte si okamžitě heslo, nebo, pokud se nemůžete na účet přihlásit, kontaktujte administrátora instance. + extra: Pokud jste si heslo nezměnil/a, je pravděpodobné, že někdo jiný získal přístup k vašemu účtu. Prosím změňte si okamžitě heslo, nebo, pokud se nemůžete na účet přihlásit, kontaktujte administrátora serveru. subject: 'Mastodon: Heslo bylo změněno' title: Heslo bylo změněno reconfirmation_instructions: @@ -60,8 +60,8 @@ cs: signed_up: Vítejte! Registroval/a jste se úspěšně. signed_up_but_inactive: Registroval/a jste se úspěšně. Nemohli jsme vás však přihlásit, protože váš účet ještě není aktivován. signed_up_but_locked: Registroval/a jste se úspěšně. Nemohli jsme vás však přihlásit, protože váš účet je uzamčen. - signed_up_but_unconfirmed: Na vaši e-mailovou adresu byla poslána zpráva s potvrzovacím odkazem. Pro aktivaci účtu přejděte na danou adresu. Prosím zkontrolujte si složku spam, jestli jste tento e-mail neobdržel/a. - update_needs_confirmation: Váš účet byl úspěšně aktualizován, ale je potřeba ověřit vaši novou e-mailovou adresu. Prosím zkontrolujte si e-mail a klikněte na odkaz pro potvrzení vaši nové e-mailové adresy. Prosím zkontrolujte si složku spam, jestli jste tento e-mail neobdržel/a. + signed_up_but_unconfirmed: Na vaši e-mailovou adresu byla poslána zpráva s potvrzovacím odkazem. Pro aktivaci účtu přejděte na danou adresu. Pokud jste tento e-mail neobdržel/a, prosím zkontrolujte si složku spam. + update_needs_confirmation: Váš účet byl úspěšně aktualizován, ale je potřeba ověřit vaši novou e-mailovou adresu. Prosím zkontrolujte si e-mail a klikněte na odkaz pro potvrzení vaši nové e-mailové adresy. Pokud jste tento e-mail neobdržel/a, prosím zkontrolujte si složku spam. updated: Váš účet byl úspěšně aktualizován. sessions: already_signed_out: Odhlášení proběhlo úspěšně. diff --git a/config/locales/devise.cy.yml b/config/locales/devise.cy.yml index 7d96e57f9..8ea7e1b4f 100644 --- a/config/locales/devise.cy.yml +++ b/config/locales/devise.cy.yml @@ -18,6 +18,7 @@ cy: mailer: confirmation_instructions: action: Gwiriwch eich cyfeiriad e-bost + action_with_app: Cadarnhau a dychwelyd i %{app} explanation: Yr ydych wedi creu cyfrif ar %{host} gyda'r cyfrif e-bost hwn. Dim ond un clic sydd angen i'w wneud yn weithredol. Os nad chi oedd hyn, anwybyddwch yr e-bost hwn os gwelwch yn dda. extra_html: Gwnewch yn siŵr i edrych ar <a href="%{terms_path}">reolau'r achos</a> a <a href="%{policy_path}">ein telerau gwasanaeth</a>. subject: 'Mastodon: Canllawiau cadarnhau i %{instance}' @@ -77,4 +78,10 @@ cy: expired: wedi dod i ben, gwnewch gais am un newydd os gwelwch yn dda not_found: heb ei ganfod not_locked: heb ei gloi - not_saved: 'Gwaharddwyd yr %{resource} rhag cael ei arbed oherwydd %{count} gwall:' + not_saved: + few: 'Gwaharddwyd yr %{resource} rhag cael ei arbed oherwydd %{count} gwall:' + many: 'Gwaharddwyd yr %{resource} rhag cael ei arbed oherwydd %{count} gwall:' + one: 'Gwaharddwyd yr %{resource} rhag cael ei arbed oherwydd 1 gwall:' + other: 'Gwaharddwyd yr %{resource} rhag cael ei arbed oherwydd %{count} gwall:' + two: 'Gwaharddwyd yr %{resource} rhag cael ei arbed oherwydd %{count} gwall:' + zero: 'Gwaharddwyd yr %{resource} rhag cael ei arbed oherwydd %{count} gwall:' diff --git a/config/locales/devise.en.yml b/config/locales/devise.en.yml index bd0642b25..726c0504e 100644 --- a/config/locales/devise.en.yml +++ b/config/locales/devise.en.yml @@ -20,17 +20,17 @@ en: action: Verify email address action_with_app: Confirm and return to %{app} explanation: You have created an account on %{host} with this email address. You are one click away from activating it. If this wasn't you, please ignore this email. - extra_html: Please also check out <a href="%{terms_path}">the rules of the instance</a> and <a href="%{policy_path}">our terms of service</a>. + extra_html: Please also check out <a href="%{terms_path}">the rules of the server</a> and <a href="%{policy_path}">our terms of service</a>. subject: 'Mastodon: Confirmation instructions for %{instance}' title: Verify email address email_changed: explanation: 'The email address for your account is being changed to:' - extra: If you did not change your email, it is likely that someone has gained access to your account. Please change your password immediately or contact the instance admin if you're locked out of your account. + extra: If you did not change your email, it is likely that someone has gained access to your account. Please change your password immediately or contact the server admin if you're locked out of your account. subject: 'Mastodon: Email changed' title: New email address password_change: explanation: The password for your account has been changed. - extra: If you did not change your password, it is likely that someone has gained access to your account. Please change your password immediately or contact the instance admin if you're locked out of your account. + extra: If you did not change your password, it is likely that someone has gained access to your account. Please change your password immediately or contact the server admin if you're locked out of your account. subject: 'Mastodon: Password changed' title: Password changed reconfirmation_instructions: diff --git a/config/locales/devise.ja.yml b/config/locales/devise.ja.yml index cae76d493..bd2bb71bb 100644 --- a/config/locales/devise.ja.yml +++ b/config/locales/devise.ja.yml @@ -20,17 +20,17 @@ ja: action: メールアドレスの確認 action_with_app: 確認し %{app} に戻る explanation: このメールアドレスで%{host}にアカウントを作成しました。有効にするまであと一歩です。もし心当たりがない場合、申し訳ありませんがこのメールを無視してください。 - extra_html: また <a href="%{terms_path}">インスタンスのルール</a> と <a href="%{policy_path}">利用規約</a> もお読みください。 + extra_html: また <a href="%{terms_path}">サーバーのルール</a> と <a href="%{policy_path}">利用規約</a> もお読みください。 subject: 'Mastodon: メールアドレスの確認 %{instance}' title: メールアドレスの確認 email_changed: explanation: 'アカウントのメールアドレスは以下のように変更されます:' - extra: メールアドレスの変更を行っていない場合、他の誰かがあなたのアカウントにアクセスした可能性があります。すぐにパスワードを変更するか、アカウントがロックされている場合はインスタンス管理者に連絡してください。 + extra: メールアドレスの変更を行っていない場合、他の誰かがあなたのアカウントにアクセスした可能性があります。すぐにパスワードを変更するか、アカウントがロックされている場合はサーバー管理者に連絡してください。 subject: 'Mastodon: メールアドレスの変更' title: 新しいメールアドレス password_change: explanation: パスワードが変更されました。 - extra: パスワードの変更を行っていない場合、他の誰かがあなたのアカウントにアクセスした可能性があります。すぐにパスワードを変更するか、アカウントがロックされている場合はインスタンス管理者に連絡してください。 + extra: パスワードの変更を行っていない場合、他の誰かがあなたのアカウントにアクセスした可能性があります。すぐにパスワードを変更するか、アカウントがロックされている場合はサーバー管理者に連絡してください。 subject: 'Mastodon: パスワードが変更されました' title: パスワードの変更 reconfirmation_instructions: diff --git a/config/locales/devise.sk.yml b/config/locales/devise.sk.yml index 16cd9262e..e054efb30 100644 --- a/config/locales/devise.sk.yml +++ b/config/locales/devise.sk.yml @@ -2,9 +2,9 @@ sk: devise: confirmations: - confirmed: Váš účet bol úspešne overený. - send_instructions: O niekoľko minút obdržíte email s inštrukciami ako potvrdiť váš účet. - send_paranoid_instructions: Ak sa váš email nachádza v našej databáze, obdržíte email s inštrukciami ako potvrdiť váš účet. + confirmed: Tvoja emailová adresa bola úspešne overená. + send_instructions: O niekoľko minút obdržíš email s inštrukciami ako potvrdiť svoj účet. Prosím, skontroluj si aj zložku spam, ak sa k tebe toto potvrdenie nedostalo. + send_paranoid_instructions: Ak sa tvoja emailová adresa nachádza v našej databázi, o niekoľko minút obdržíš email s pokynmi ako potvrdiť svoj účet. Prosím, skontroluj aj zložku spam, ak sa k tebe toto potvrdenie nedostalo. failure: already_authenticated: Už si prihlásený/á. inactive: Tvoj účet ešte nebol potvrdený. @@ -51,15 +51,15 @@ sk: success: Úspešné overenie z účtu %{kind}. passwords: no_token: Túto stránku nemôžete navštíviť pokiaľ neprichádzate z emailu s inštrukciami na obnovu hesla. Pokiaľ prichádzate z tohto emailu, prosím uistite sa že ste použili celú URL z emailu. - send_instructions: Ak zadaný email existuje v našej databázi, tak o niekoľko minút obdržíte email s inštrukciami ako nastaviť nové heslo. - send_paranoid_instructions: Ak zadaný email existuje v našej databázi, zachvíľu obdržíte odkaz na obnovu hesla na svoj email. Skontrolujte aj spam ak tento email nevidíte. + send_instructions: Pokiaľ sa tvoja emailová adresa nachádza v databázi, tak o niekoľko minút obdržíš email s inštrukciami ako nastaviť nové heslo. Ak máš pocit, že si email neobdržal/a, prosím skontroluj aj svoju spam zložku. + send_paranoid_instructions: Ak sa tvoja emailová adresa nachádza v databázi, za chvíľu obdržíš odkaz pre obnovu hesla na svoj email. Skontroluj ale prosím aj svoj spam, ak tento email nevidíš. updated: Tvoje heslo bolo úspešne zmenené. Teraz si prihlásený/á. updated_not_active: Tvoje heslo bolo úspešne zmenené. registrations: destroyed: Dovidenia! Tvoj účet bol úspešne zrušený. Dúfame ale, že ťa tu opäť niekedy uvidíme. signed_up: Vitaj! Tvoja registrácia bola úspešná. - signed_up_but_inactive: Registrácia bola úspešná. Avšak, účet ešte nebol aktivovaný, takže ťa nemôžeme prihlásiť. - signed_up_but_locked: Prihlasovanie úspešné. Avšak tvoj účet je zamknutý, takže ťa nieje možné prihlásiť. + signed_up_but_inactive: Registrácia bola úspešná. Avšak, účet ešte nebol aktivovaný, takže ťa nemožno prihlásiť. + signed_up_but_locked: Registroval/a si sa úspešné. Avšak, tvoj účet je zamknutý, takže ťa nemožno prihlásiť. signed_up_but_unconfirmed: Správa s odkazom na potvrdenie registrácie bola odoslaná na tvoj email. Pre aktváciu účtu, klikni prosím na daný odkaz. Takisto ale skontroluj aj svoju spam zložku, pokiaľ sa ti zdá, že si tento email nedostal/a. update_needs_confirmation: Účet bol úspešne zmenený ale ešte potrebujeme overiť tvoju novú emailovú adresu. Pre overenie prosím klikni na link v správe ktorú si dostal/a na email. Takisto ale skontroluj aj svoju spam zložku, ak sa ti zdá, že si tento email nedostal/a. updated: Tvoj účet bol úspešne aktualizovaný. diff --git a/config/locales/devise.sq.yml b/config/locales/devise.sq.yml new file mode 100644 index 000000000..906bece81 --- /dev/null +++ b/config/locales/devise.sq.yml @@ -0,0 +1,83 @@ +--- +sq: + devise: + confirmations: + confirmed: Adresa juaj email u ripohua me sukses. + send_instructions: Brenda pak minutash, do të merrni një email me udhëzime se si të ripohoni adresën tuaj email. Ju lutemi, kontrolloni dosjen e mesazheve të padëshiruar, nëse nuk e morët këtë email. + send_paranoid_instructions: Nëse adresa juaj email gjendet në bazën tonë të të dhënave, brenda pak minutash, do të merrni një email me udhëzime se si të ripohoni adresën tuaj email. Ju lutemi, kontrolloni dosjen e mesazheve të padëshiruar, nëse nuk e morët këtë email. + failure: + already_authenticated: Jeni tashmë i futur. + inactive: Llogaria juaj s’është aktivizuar ende. + invalid: "%{authentication_keys} ose fjalëkalim i pavlefshëm." + last_attempt: Mund të provoni edhe një herë, përpara se llogaria juaj të kyçet. + locked: Llogaria juaj është e kyçur. + not_found_in_database: "%{authentication_keys} ose fjalëkalim i pavlefshëm." + timeout: Sesioni juaj ka skaduar. Ju lutemi, që të vazhdohet, ribëni hyrjen. + unauthenticated: Përpara se të vazhdohet më tej, lypset të bëni hyrjen ose të regjistroheni. + unconfirmed: Përpara se të vazhdohet, lypset të ripohoni adresën tuaj email. + mailer: + confirmation_instructions: + action: Verifikoni adresë email + action_with_app: Ripohojeni dhe kthehuni te %{app} + explanation: Keni krijuar një llogari te %{host}, me këtë adresë email. Jeni një klikim larg aktivizimit të saj. Nëse s’jeni ju, shpërfilleni këtë email. + extra_html: Ju lutemi, shihni edhe <a href="%{terms_path}">rregullat e instancës</a> dhe <a href="%{policy_path}">kushtet tona të shërbimit</a>. + subject: 'Mastodon: Udhëzime ripohimi për %{instance}' + title: Verifikoni adresë email + email_changed: + explanation: 'Adresa email për llogarinë tuaj po ndryshohet në:' + extra: Nëse email-in tuaj nuk e ndryshuat ju, gjasat janë që dikush tjetër ka arritur të hyjë në llogarinë tuaj. Ju lutemi, ndryshoni menjëherë fjalëkalimin tuaj ose lidhuni me përgjegjësin e instancës, nëse jeni kyçur jashtë llogarisë tuaj. + subject: 'Mastodon: Email-i u ndryshua' + title: Adresë email e re + password_change: + explanation: Fjalëkalimi për llogarinë tuaj u ndryshua. + extra: Nëse fjalëkalimin tuaj nuk e ndryshuat ju, gjasat janë që dikush tjetër ka arritur të hyjë në llogarinë tuaj. Ju lutemi, ndryshoni menjëherë fjalëkalimin tuaj ose lidhuni me përgjegjësin e instancës, nëse jeni kyçur jashtë llogarisë tuaj. + subject: 'Mastodon: Fjalëkalimi u ndryshua' + title: Fjalëkalimi u ndryshua + reconfirmation_instructions: + explanation: Që të ndryshohet email-i juaj, ripohoni adresën e re. + extra: Nëse ky ndryshim s’qe filluar prej jush, ju lutemi, shpërfilleni këtë email. Adresa email për llogarinë Mastodon s’do të ndryshojë, para se të hyni në lidhjen më sipër. + subject: 'Mastodon: Ripohoni email-in për %{instance}' + title: Verifikoni adresë email + reset_password_instructions: + action: Ndryshoni fjalëkalimin + explanation: Kërkuat një fjalëkalim të ri për këtë llogari. + extra: Nëse këtë s’e kërkuat ju, ju lutemi, shpërfilleni këtë email. Fjalëkalimi juaj s’do të ndryshohet pa hyrë në lidhjen më sipër dhe krijimin e një të riu. + subject: 'Mastodon: Udhëzime ricaktimi fjalëkalimi' + title: Ricaktim fjalëkalimi + unlock_instructions: + subject: 'Mastodon: Udhëzime shkyçjeje' + omniauth_callbacks: + failure: S’u bë dot mirëfilltësimi juaj nga %{kind}, sepse "%{reason}". + success: Mirëfilltësimi nga llogaria %{kind} u bë me sukses. + passwords: + no_token: S’mund të hyni në këtë faqe paardhur nga një email ricaktimi fjalëkalimi. Nëse vini nga një email ricaktimi fjalëkalimi, ju lutemi, sigurohuni se përdorët URL-në e plotë dhënë për ju. + send_instructions: Nëse adresa juaj email gjendet në bazën tonë të të dhënave, brenda pak minutash, te adresa juaj email do të merrni një lidhje rimarrjeje fjalëkalimi. Ju lutemi, kontrolloni dosjen e mesazheve të padëshiruar, nëse nuk e morët këtë email. + send_paranoid_instructions: Nëse adresa juaj email gjendet në bazën tonë të të dhënave, brenda pak minutash, te adresa juaj email do të merrni një lidhje rimarrjeje fjalëkalimi. Ju lutemi, kontrolloni dosjen e mesazheve të padëshiruar, nëse nuk e morët këtë email. + updated: Fjalëkalimi juaj u ndryshua me sukses. Tani jeni i futur. + updated_not_active: Fjalëkalimi juaj u ndryshua me sukses. + registrations: + destroyed: Shëndet! Llogaria juaj u fshi me sukses. Shpresojmë t’ju rishohim së shpejti. + signed_up: Mirë se vini! U regjistruat me sukses. + signed_up_but_inactive: U regjistruat me sukses. Megjithatë, s’u bë dot hyrja juaj, ngaqë llogaria juaj s’është aktivizuar ende. + signed_up_but_locked: U regjistruat me sukses. Megjithatë, s’u bë dot hyrja juaj, ngaqë llogaria juaj është kyçur. + signed_up_but_unconfirmed: Te adresa juaj email u dërgua një mesazh me një lidhje ripohimi. Ju lutemi, që të aktivizoni llogarinë tuaj, ndiqni lidhjen. Ju lutemi, kontrolloni dosjen e mesazheve të padëshiruar, nëse nuk e morët këtë email. + update_needs_confirmation: E përditësuat me sukses llogarinë tuaj, por na duhet të verifikojmë adresën tuaj të re email. Ju lutemi, që të ripohoni adresën tuaj të re email, kontrolloni email-in tuaj dhe ndiqni lidhjen. Ju lutemi, kontrolloni dosjen e mesazheve të padëshiruar, nëse nuk e morët këtë email. + updated: Llogaria juaj u përditësua me sukses. + sessions: + already_signed_out: Dolët me sukses. + signed_in: Hytë me sukses. + signed_out: Dolët me sukses. + unlocks: + send_instructions: Brenda pak minutash, do të merrni një email me udhëzime se si të shkyçni llogarinë tuaj. Ju lutemi, kontrolloni dosjen e mesazheve të padëshiruar, nëse nuk e morët këtë email. + send_paranoid_instructions: Nëse llogaria juaj ekziston, brenda pak minutash, do të merrni një email me udhëzime se si të shkyçni llogarinë tuaj. Ju lutemi, kontrolloni dosjen e mesazheve të padëshiruar, nëse nuk e morët këtë email. + unlocked: Llogaria juaj u shkyç me sukses. Ju lutemi, që të vazhdohet, bëni hyrjen. + errors: + messages: + already_confirmed: qe e ripohuar tashmë, ju lutemi, provoni të bëni hyrjen + confirmation_period_expired: lyp të ripohohet brenda %{period}, ju lutemi, kërkoni një të ri + expired: ka skaduar, ju lutemi, kërkoni një të ri + not_found: s’u gjet + not_locked: s’qe kyçur + not_saved: + one: 'Ruajtjen e këtij %{resource} e pengoi 1 gabim:' + other: 'Ruajtjen e këtij %{resource} e penguan %{count} gabime:' diff --git a/config/locales/doorkeeper.cs.yml b/config/locales/doorkeeper.cs.yml index b9e9bc034..03b66a0fa 100644 --- a/config/locales/doorkeeper.cs.yml +++ b/config/locales/doorkeeper.cs.yml @@ -54,7 +54,7 @@ cs: title: 'Aplikace: %{name}' authorizations: buttons: - authorize: Ověřit + authorize: Autorizovat deny: Zamítnout error: title: Vyskytla se chyba diff --git a/config/locales/doorkeeper.sq.yml b/config/locales/doorkeeper.sq.yml new file mode 100644 index 000000000..a1f2121f9 --- /dev/null +++ b/config/locales/doorkeeper.sq.yml @@ -0,0 +1,142 @@ +--- +sq: + activerecord: + attributes: + doorkeeper/application: + name: Emër aplikacioni + redirect_uri: URI Ridrejtimi + scopes: Fushëveprime + website: Sajt aplikacioni + errors: + models: + doorkeeper/application: + attributes: + redirect_uri: + fragment_present: s’mund të përmbajë një fragment. + invalid_uri: duhet të jetë një URI e vlefshme. + relative_uri: duhet të jetë një URI absolute. + secured_uri: duhet të jetë një URI HTTPS/SSL. + doorkeeper: + applications: + buttons: + authorize: Autorizoje + cancel: Anuloje + destroy: Asgjësoje + edit: Përpunoni + submit: Parashtroje + confirmations: + destroy: A jeni i sigurt? + edit: + title: Përpunoni aplikacion + form: + error: Oh! Kontrolloni formularin tuaj për gabime të mundshme + help: + native_redirect_uri: Përdor %{native_redirect_uri} për teste vendore + redirect_uri: Përdorni një URI për rresht + scopes: Ndajini fushëveprimet me hapësira. Që të përdoren fushëveprimet parazgjedhje, lëreni të zbrazët. + index: + application: Aplikacion + callback_url: URL Callback-u + delete: Fshije + name: Emër + new: Aplikacion i ri + scopes: Fushëveprime + show: Shfaqe + title: Aplikacionet tuaja + new: + title: Aplikacion i ri + show: + actions: Veprime + application_id: Kyç klienti + callback_urls: URL-ra Callback + scopes: Fushëveprime + secret: E fshehtë klienti + title: 'Aplikacion: %{name}' + authorizations: + buttons: + authorize: Autorizoje + deny: Mohoje + error: + title: Ndodhi një gabim + new: + able_to: Do të jetë në gjendje të + prompt: "%{client_name} kërkesa hyrjeje aplikacionesh te llogaria juaj" + title: Lypset autorizim + show: + title: Kopjojeni këtë kod autorizimi dhe ngjiteni te aplikacioni. + authorized_applications: + buttons: + revoke: Shfuqizoje + confirmations: + revoke: A jeni i sigurt? + index: + application: Aplikacion + created_at: I autorizuar + date_format: "%d.%m.%Y, %H:%M:%S" + scopes: Fushëveprime + title: Aplikacionet tuaja të autorizuara + errors: + messages: + access_denied: I zoti i burimit ose shërbyesi i autorizimit e hodhi poshtë kërkesën. + credential_flow_not_configured: Rrjedha e Kredencialeve të Fjalëkalimit të të Zotit të Burimit dështoi për shkak se Doorkeeper.configure.resource_owner_from_credentials është i paformësuar. + invalid_client: Mirëfilltësimi i klientit dështoi për shkak klienti të panjohur, mospërfshirjeje mirëfilltësimi klienti, ose metode të pambuluar mirëfilltësimi. + invalid_grant: Autorizimi i dhënë është i pavlefshëm, ka skaduar, është shfuqizuar, nuk përputhet me URI-n e ridrejtimit të përdorur te kërkesa e autorizimit, ose është emetuar për klient tjetër. + invalid_redirect_uri: URI e ridrejtimit s’është e vlefshme. + invalid_request: Kërkesës i mungon një parametër i domosdoshëm, përfshin një vlerë të pambuluar parametri, ose përndryshe është e keqformuar. + invalid_resource_owner: Kredencialet e dhëna për të zotin e burimit s’janë të vlefshme, ose s’gjendet i zoti i burimit + invalid_scope: Fushëveprimi i kërkuar është i pavlefshëm, i panjohur ose i keqformuar. + invalid_token: + expired: Token-i i hyrjeve skadoi + revoked: Token-i i hyrjeve u shfuqizua + unknown: Token-i i hyrjeve është i pavlefshëm + resource_owner_authenticator_not_configured: Gjetja e të Zotit të Burimit dështoi, ngaqë Doorkeeper.configure.resource_owner_authenticator s’është i formësuar. + server_error: Shërbyesi i autorizimit hasi një kusht të papritur, i cili e pengoi të plotësonte kërkesën. + temporarily_unavailable: Shërbyesi i mirëfilltësimeve hëpërhë s’është në gjendje të trajtojë kërkesën, për shkak të një mbingarkese të përkohshme ose ndonjë mirëmbajtjeje të shërbyesit. + unauthorized_client: Klienti s’është i autorizuar të kryejë këtë kërkesë duke përdorur këtë metodë. + unsupported_grant_type: Lloji i autorizimit të dhënë nuk mbulohet nga shërbyesi i autorizimeve. + unsupported_response_type: Shërbyesi i autorizimeve nuk e mbulon këtë lloj përgjigjeje. + flash: + applications: + create: + notice: Aplikacioni u krijua. + destroy: + notice: Aplikacioni u fshi. + update: + notice: Aplikacioni u përditësua. + authorized_applications: + destroy: + notice: Aplikacioni u shfuqizua. + layouts: + admin: + nav: + applications: Aplikacione + oauth2_provider: Furnizues OAuth2 + application: + title: Lypset autorizim OAuth + scopes: + follow: të ndryshojë marrëdhënies llogarish + push: të marrë njoftime push për ju + read: të lexojë krejt të dhënat e llogarisë tuaj + read:accounts: të shohë të dhëna llogarish + read:blocks: të shohë blloqet tuaja + read:favourites: të shohë të parapëlqyerit tuaj + read:filters: të shohë filtrat tuaj + read:follows: të shohë ndjekësit tuaj + read:lists: të shohë listat tuaja + read:mutes: të shohë ç’keni heshtuar + read:notifications: të shohë njoftimet tuaja + read:reports: të shohë raportet tuaja + read:search: të bëjë kërkime në emrin tuaj + read:statuses: të shohë krejt gjendjet + write: të ndryshojë krejt të dhënat e llogarisë tuaj + write:accounts: të ndryshojë profilin tuaj + write:blocks: të bllokojë llogari dhe përkatësi + write:favourites: të parapëlqejë gjendje + write:filters: të krijojë filtra + write:follows: të ndjekë persona + write:lists: të krijojë lista + write:media: të ngarkojë kartela media + write:mutes: të heshtojë persona dhe biseda + write:notifications: të pastrojë njoftimet tuaja + write:reports: të raportojë persona të tjerë + write:statuses: të botojë gjendje diff --git a/config/locales/el.yml b/config/locales/el.yml index c29b8301e..95a72571d 100644 --- a/config/locales/el.yml +++ b/config/locales/el.yml @@ -115,7 +115,7 @@ el: joined: Γράφτηκε location: all: Όλες - local: Τοπικά + local: Τοπική remote: Απομακρυσμένα title: Τοποθεσία login_status: Κατάσταση σύνδεσης @@ -556,7 +556,7 @@ el: enabled: Περιλαμβάνεσαι στον κατάλογο. enabled_but_waiting: Έχεις επιλέξει να εμφανίζεσαι στον κατάλογο μεν, αλλά ακόμα δεν έχεις τον ελάχιστο αριθμό ακόλουθων (%{min_followers}) που απαιτείται για να συμπεριληφθείς. explanation: Βρες χρήστες βάσει των ενδιαφερόντων τους - explore_mastodon: Εξερεύνησε %{title} + explore_mastodon: Εξερεύνησε το %{title} how_to_enable: Δεν έχεις επιλέξει να συμπεριληφθείς στον καταλογο. Μπορείς να επιλέξεις παρακάτω. Χρησιμοποίησε ταμπέλες στο κείμενο του βιογραφικού σου για να εμφανίζεσαι κάτω από συγκεκριμένες ταμπέλες! people: one: "%{count} άτομο" @@ -657,7 +657,7 @@ el: table: expires_at: Λήγει uses: Χρήσεις - title: Προσκάλεσε άτομα + title: Προσκάλεσε κόσμο lists: errors: limit: Έχεις φτάσει το μέγιστο πλήθος επιτρεπτών λιστών diff --git a/config/locales/en.yml b/config/locales/en.yml index 8cc9d7c0b..1121ef3db 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -7,7 +7,7 @@ en: administered_by: 'Administered by:' api: API apps: Mobile apps - closed_registrations: Registrations are currently closed on this instance. However! You can find a different instance to make an account on and get access to the very same network from there. + closed_registrations: Registrations are currently closed on this server. However! You can find a different server to make an account on and get access to the very same network from there. contact: Contact contact_missing: Not set contact_unavailable: N/A @@ -27,7 +27,7 @@ en: generic_description: "%{domain} is one server in the network" hosted_on: Mastodon hosted on %{domain} learn_more: Learn more - other_instances: Instance list + other_instances: Server list privacy_policy: Privacy policy source_code: Source code status_count_after: @@ -302,6 +302,7 @@ en: back_to_account: Back To Account title: "%{acct}'s Followers" instances: + by_domain: Domain delivery_available: Delivery is available known_accounts: one: "%{count} known account" @@ -386,7 +387,7 @@ en: desc_html: Modify the look with CSS loaded on every page title: Custom CSS hero: - desc_html: Displayed on the frontpage. At least 600x100px recommended. When not set, falls back to instance thumbnail + desc_html: Displayed on the frontpage. At least 600x100px recommended. When not set, falls back to server thumbnail title: Hero image hide_followers_count: desc_html: Do not show followers count on user profiles @@ -395,8 +396,8 @@ en: desc_html: Displayed on multiple pages. At least 293×205px recommended. When not set, falls back to default mascot title: Mascot image peers_api_enabled: - desc_html: Domain names this instance has encountered in the fediverse - title: Publish list of discovered instances + desc_html: Domain names this server has encountered in the fediverse + title: Publish list of discovered servers preview_sensitive_media: desc_html: Link previews on other websites will display a thumbnail even if the media is marked as sensitive title: Show sensitive media in OpenGraph previews @@ -424,20 +425,20 @@ en: title: Show staff badge site_description: desc_html: Introductory paragraph on the frontpage. Describe what makes this Mastodon server special and anything else important. You can use HTML tags, in particular <code><a></code> and <code><em></code>. - title: Instance description + title: Server description site_description_extended: - desc_html: A good place for your code of conduct, rules, guidelines and other things that set your instance apart. You can use HTML tags + desc_html: A good place for your code of conduct, rules, guidelines and other things that set your server apart. You can use HTML tags title: Custom extended information site_short_description: - desc_html: Displayed in sidebar and meta tags. Describe what Mastodon is and what makes this server special in a single paragraph. If empty, defaults to instance description. - title: Short instance description + desc_html: Displayed in sidebar and meta tags. Describe what Mastodon is and what makes this server special in a single paragraph. If empty, defaults to server description. + title: Short server description site_terms: desc_html: You can write your own privacy policy, terms of service or other legalese. You can use HTML tags title: Custom terms of service - site_title: Instance name + site_title: Server name thumbnail: desc_html: Used for previews via OpenGraph and API. 1200x630px recommended - title: Instance thumbnail + title: Server thumbnail timeline_preview: desc_html: Display public timeline on landing page title: Timeline preview @@ -498,7 +499,7 @@ en: warning: Be very careful with this data. Never share it with anyone! your_token: Your access token auth: - agreement_html: By clicking "Sign up" below you agree to follow <a href="%{rules_path}">the rules of the instance</a> and <a href="%{terms_path}">our terms of service</a>. + agreement_html: By clicking "Sign up" below you agree to follow <a href="%{rules_path}">the rules of the server</a> and <a href="%{terms_path}">our terms of service</a>. change_password: Password confirm_email: Confirm email delete_account: Delete account @@ -552,7 +553,7 @@ en: description_html: This will <strong>permanently, irreversibly</strong> remove content from your account and deactivate it. Your username will remain reserved to prevent future impersonations. proceed: Delete account success_msg: Your account was successfully deleted - warning_html: Only deletion of content from this particular instance is guaranteed. Content that has been widely shared is likely to leave traces. Offline servers and servers that have unsubscribed from your updates will not update their databases. + warning_html: Only deletion of content from this particular server is guaranteed. Content that has been widely shared is likely to leave traces. Offline servers and servers that have unsubscribed from your updates will not update their databases. warning_title: Disseminated content availability directories: directory: Profile directory @@ -566,8 +567,8 @@ en: other: "%{count} people" errors: '403': You don't have permission to view this page. - '404': The page you were looking for doesn't exist. - '410': The page you were looking for doesn't exist anymore. + '404': The page you are looking for isn't here. + '410': The page you were looking for doesn't exist here anymore. '422': content: Security verification failed. Are you blocking cookies? title: Security verification failed @@ -580,7 +581,7 @@ en: archive_takeout: date: Date download: Download your archive - hint_html: You can request an archive of your <strong>toots and uploaded media</strong>. The exported data will be in ActivityPub format, readable by any compliant software. You can request an archive every 7 days. + hint_html: You can request an archive of your <strong>toots and uploaded media</strong>. The exported data will be in the ActivityPub format, readable by any compliant software. You can request an archive every 7 days. in_progress: Compiling your archive... request: Request your archive size: Size @@ -591,6 +592,10 @@ en: lists: Lists mutes: You mute storage: Media storage + featured_tags: + add_new: Add new + errors: + limit: You have already featured the maximum amount of hashtags filters: contexts: home: Home timeline @@ -609,7 +614,7 @@ en: title: Add new filter followers: domain: Domain - explanation_html: If you want to ensure the privacy of your statuses, you must be aware of who is following you. <strong>Your private statuses are delivered to all instances where you have followers</strong>. You may wish to review them, and remove followers if you do not trust your privacy to be respected by the staff or software of those instances. + explanation_html: If you want to ensure the privacy of your statuses, you must be aware of who is following you. <strong>Your private statuses are delivered to all servers where you have followers</strong>. You may wish to review them, and remove followers if you do not trust your privacy to be respected by the staff or software of those servers. followers_count: Number of followers lock_link: Lock your account purge: Remove from followers @@ -632,10 +637,16 @@ en: one: Something isn't quite right yet! Please review the error below other: Something isn't quite right yet! Please review %{count} errors below imports: - preface: You can import data that you have exported from another instance, such as a list of the people you are following or blocking. + modes: + merge: Merge + merge_long: Keep existing records and add new ones + overwrite: Overwrite + overwrite_long: Replace current records with the new ones + preface: You can import data that you have exported from another server, such as a list of the people you are following or blocking. success: Your data was successfully uploaded and will now be processed in due time types: blocking: Blocking list + domain_blocking: Domain blocking list following: Following list muting: Muting list upload: Upload @@ -657,7 +668,7 @@ en: one: 1 use other: "%{count} uses" max_uses_prompt: No limit - prompt: Generate and share links with others to grant access to this instance + prompt: Generate and share links with others to grant access to this server table: expires_at: Expires uses: Uses @@ -805,6 +816,7 @@ en: development: Development edit_profile: Edit profile export: Data export + featured_tags: Featured hashtags flavours: Flavours followers: Authorized followers import: Import @@ -934,9 +946,9 @@ en: <p>Originally adapted from the <a href="https://github.com/discourse/discourse">Discourse privacy policy</a>.</p> title: "%{instance} Terms of Service and Privacy Policy" themes: - contrast: High contrast - default: Mastodon - mastodon-light: Mastodon (light) + contrast: Mastodon (High contrast) + default: Mastodon (Dark) + mastodon-light: Mastodon (Light) time: formats: default: "%b %d, %Y, %H:%M" @@ -985,7 +997,7 @@ en: final_action: Start posting final_step: 'Start posting! Even without followers your public messages may be seen by others, for example on the local timeline and in hashtags. You may want to introduce yourself on the #introductions hashtag.' full_handle: Your full handle - full_handle_hint: This is what you would tell your friends so they can message or follow you from another instance. + full_handle_hint: This is what you would tell your friends so they can message or follow you from another server. review_preferences_action: Change preferences review_preferences_step: Make sure to set your preferences, such as which emails you'd like to receive, or what privacy level you’d like your posts to default to. If you don’t have motion sickness, you could choose to enable GIF autoplay. subject: Welcome to Mastodon diff --git a/config/locales/eu.yml b/config/locales/eu.yml index 6abf15201..84fbbbaf7 100644 --- a/config/locales/eu.yml +++ b/config/locales/eu.yml @@ -92,6 +92,7 @@ eu: confirm: Berretsi confirmed: Berretsita confirming: Berresten + deleted: Ezabatua demote: Jaitsi mailaz disable: Desgaitu disable_two_factor_authentication: Desgaitu 2FA @@ -109,7 +110,9 @@ eu: follows: Jarraitzen du header: Goiburua inbox_url: Sarrera ontziaren URL-a + invited_by: 'Honek gonbidatua:' ip: IP + joined: Elkartuta location: all: Denak local: Lokala @@ -136,7 +139,7 @@ eu: protocol: Protokoloa public: Publikoa push_subscription_expires: Push harpidetzaren iraugitzea - redownload: Freskatu abatarra + redownload: Freskatu profila remove_avatar: Kendu abatarra remove_header: Kendu goiburua resend_confirmation: @@ -156,8 +159,8 @@ eu: search: Bilatu shared_inbox_url: Partekatutako sarrera ontziaren URL-a show: - created_reports: Kontu honek sortutako txostenak - targeted_reports: Kontu honek egindako salaketak + created_reports: Sortutako txostenak + targeted_reports: Besteen salaketak silence: Isilarazi silenced: Isilarazita statuses: Mezuak @@ -169,6 +172,7 @@ eu: undo_suspension: Desegin kanporatzea unsubscribe: Kendu harpidetza username: Erabiltzaile-izena + warn: Abisatu web: Web action_logs: actions: @@ -252,7 +256,7 @@ eu: week_users_active: aktibo aste honetan week_users_new: erabiltzaile aste honetan domain_blocks: - add_new: Gehitu berria + add_new: Gehitu domeinuaren blokeo berria created_msg: Domeinuaren blokeoa orain prozesatzen ari da destroyed_msg: Domeinuaren blokeoa desegin da domain: Domeinua @@ -264,11 +268,16 @@ eu: noop: Bat ere ez silence: Isilarazi suspend: Kanporatu - title: Domeinu blokeo berria + title: Domeinuaren blokeo berria reject_media: Ukatu multimedia fitxategiak reject_media_hint: Lokalki gordetako multimedia fitxategiak ezabatzen ditu eta etorkizunean fitxategi berriak deskargatzeari uko egingo dio. Ez du garrantzirik kanporaketetan reject_reports: Errefusatu salaketak reject_reports_hint: Ezikusi domeinu honetatik jasotako salaketak. Kanporatzeentzako garrantzirik gabekoa + rejecting_media: errefusatu multimedia fitxategiak + rejecting_reports: txostenak errefusatzen + severity: + silence: isilarazia + suspend: kanporatua show: affected_accounts: one: Datu-baseko kontu bati eragiten dio @@ -278,7 +287,7 @@ eu: suspend: Kendu kanporatzeko agindua domeinu honetako kontu guztiei title: Desegin %{domain} domeinuko blokeoa undo: Desegin - undo: Desegin + undo: Desegin domeinuaren blokeoa email_domain_blocks: add_new: Gehitu berria created_msg: Ongi gehitu da e-mail helbidea domeinuen zerrenda beltzera @@ -293,7 +302,20 @@ eu: back_to_account: Itzuli kontura title: "%{acct} kontuaren jarraitzaileak" instances: - title: Instantzia ezagunak + delivery_available: Bidalketa eskuragarri dago + known_accounts: + one: Kontu ezagun %{count} + other: "%{count} kontu ezagun" + moderation: + all: Denak + limited: Mugatua + title: Moderazioa + title: Federazioa + total_blocked_by_us: Guk blokeatuta + total_followed_by_them: Haiek jarraitua + total_followed_by_us: Guk jarraitua + total_reported: Heiei buruzko txostenak + total_storage: Multimedia eranskinak invites: deactivate_all: Desgaitu guztiak filter: @@ -531,8 +553,11 @@ eu: warning_title: Sakabanatutako edukiaren eskuragarritasuna directories: directory: Profilen direktorioa + enabled: Direktorioan zerrendatuta zaude orain. + enabled_but_waiting: Direktorioan zerrendatuta izatea aukeratu duzu, baina ez duzu oraindik gutxieneko jarraitzaile kopurua (%{min_followers}) zerrendan agertzeko. explanation: Deskubritu erabiltzaileak interesen arabera explore_mastodon: Esploratu %{title} + how_to_enable: Ez duzu aukeratu direktorioan zerrendatua izatea aukeratu. Behean aukeratu dezakezu. Erabili traolak zure biografiaren testuan traola zehatzetan agertzeko! people: one: pertsona %{count} other: "%{count} pertsona" @@ -558,7 +583,9 @@ eu: size: Tamaina blocks: Zuk blokeatutakoak csv: CSV + domain_blocks: Domeinuen blokeoak follows: Zuk jarraitutakoak + lists: Zerrendak mutes: Zuk mututukoak storage: Multimedia biltegiratzea filters: @@ -707,10 +734,25 @@ eu: no_account_html: Ez duzu konturik? <a href='%{sign_up_path}' target='_blank'>Izena eman</a> dezakezu proceed: Ekin jarraitzeari prompt: 'Hau jarraituko duzu:' + reason_html: "<strong>Zergaitik eman behar da urrats hau?</strong><code>%{instance}</code> agian ez da izena eman duzun zerbitzaria, eta zure hasiera-zerbitzarira eraman behar zaitugu aurretik." + remote_interaction: + favourite: + proceed: Bihurtu gogoko + prompt: 'Toot hau gogoko bihurtu nahi duzu:' + reblog: + proceed: Eman bultzada + prompt: 'Toot honi bultzada eman nahi diozu:' + reply: + proceed: Ekin erantzuteari + prompt: 'Toot honi erantzun nahi diozu:' remote_unfollow: error: Errorea title: Izenburua unfollowed: Jarraitzeari utzita + scheduled_statuses: + over_daily_limit: Egun horretarako programatutako toot kopuruaren muga gainditu duzu (%{limit}) + over_total_limit: Programatutako toot kopuruaren muga gainditu duzu (%{limit}) + too_soon: Programatutako data etorkizunean egon behar du sessions: activity: Azken jarduera browser: Nabigatzailea @@ -919,15 +961,18 @@ eu: explanation: disable: Zure kontua izoztuta dagoen bitartean, zure kontua bere horretan dirau, baina ezin duzu ekintzarik burutu desblokeatzen den arte. silence: Zure kontua murriztua dagoen bitartean, jada zu jarraitzen zaituztenak besterik ez dituzte zure Toot-ak ikusiko zerbitzari honetan, eta agian zerrenda publikoetatik kenduko zaizu. Hala ere besteek oraindik zu jarraitu zaitzakete. + suspend: Zure kontua kanporatua izan da, zure toot guztiak eta multimedia fitxategiak behin betiko ezabatu dira zerbitzari honetatik, eta zure jarraitzaileen zerbitzarietatik. review_server_policies: Berrikusi zerbitzariko politikak subject: disable: Zure %{acct} kontua izoztu da none: "%{acct} konturako abisua" silence: Zure %{acct} kontua murriztu da + suspend: Zure %{acct} kontua kanporatua izan da title: disable: Kontu izoztua none: Abisua silence: Kontu murriztua + suspend: Kontu kanporatua welcome: edit_profile_action: Ezarri profila edit_profile_step: Pertsonalizatu profila abatar bat igoz, goiburu bat, zure pantaila-izena aldatuz eta gehiago. Jarraitzaile berriak onartu aurretik gainbegiratu nahi badituzu, kontua giltzaperatu dezakezu. @@ -935,7 +980,7 @@ eu: final_action: Hasi mezuak bidaltzen final_step: 'Hasi argitaratzen! Jarraitzailerik ez baduzu ere zure mezu publikoak besteek ikusi ditzakete, esaterako denbora-lerro lokalean eta traoletan. Zure burua aurkeztu nahi baduzu #aurkezpenak traola erabili zenezake.' full_handle: Zure erabiltzaile-izen osoa - full_handle_hint: Hau da lagunei esango zeniena beste instantzia batetik zu jarraitzeko edo zuri mezuak bidaltzeko. + full_handle_hint: Hau da lagunei esango zeniekeena beste instantzia batetik zu jarraitzeko edo zuri mezuak bidaltzeko. review_preferences_action: Aldatu hobespenak review_preferences_step: Ziurtatu hobespenak ezartzen dituzula, jaso nahi dituzu e-mail mezuak, lehenetsitako pribatutasuna mezu berrietarako. Mareatzen ez bazaitu GIF-ak automatikoki abiatzea ezarri dezakezu ere. subject: Ongi etorri Mastodon-era diff --git a/config/locales/ga.yml b/config/locales/ga.yml new file mode 100644 index 000000000..9f586aa37 --- /dev/null +++ b/config/locales/ga.yml @@ -0,0 +1,4 @@ +--- +ga: + about: + about_hashtag_html: Is toots phoiblí iad seo atá clibáilte le <strong>#%{hashtag}</strong>. Is féidir leat idirghníomhú leo má tá cuntas agat áit ar bith sa fediverse. diff --git a/config/locales/id.yml b/config/locales/id.yml index fabf2746e..d155ab0a7 100644 --- a/config/locales/id.yml +++ b/config/locales/id.yml @@ -4,6 +4,7 @@ id: about_hashtag_html: Ini adalah toot public yang ditandai dengan <strong>#%{hashtag}</strong>. Anda bisa berinteraksi dengan mereka jika anda memiliki akun dimanapun di fediverse. about_mastodon_html: Mastodon adalah sebuah jejaring sosial <em>terbuka, open-source</em. Sebuah alternatif <em>desentralisasi</em> dari platform komersial, menjauhkan anda resiko dari sebuah perusahaan yang memonopoli komunikasi anda. Pilih server yang anda percayai — apapun yang anda pilih, anda tetap dapat berinteraksi dengan semua orang. Semua orang dapat menjalankan server Mastodon sendiri dan berpartisipasi dalam <em>jejaring sosial</em> dengan mudah. about_this: Tentang server ini + administered_by: 'Dikelola oleh:' api: API apps: Aplikasi hp closed_registrations: Pendaftaran untuk server ini sedang ditutup. Tetapi, anda bisa mencari server lain untuk membuat akun dan mendapatkan akses dari jaringan yang sama di sana. @@ -70,52 +71,104 @@ id: admin: account_actions: action: Lakukan aksi + title: Lakukan moderasi pada %{acct} + account_moderation_notes: + create: Beri catatan + created_msg: Catatan moderasi berhasil dibuat! + delete: Hapus + destroyed_msg: Catatan moderasi berhasil dihapus! accounts: are_you_sure: Anda yakin? + avatar: Avatar + by_domain: Domain + change_email: + changed_msg: Email akun ini berhasil diubah! + current_email: Email saat ini + label: Ganti email + new_email: Email baru + submit: Ganti email + title: Ganti email untuk %{username} confirm: Konfirmasi confirmed: Dikonfirmasi confirming: Mengkonfirmasi + deleted: Terhapus + disable: Nonaktifkan + disable_two_factor_authentication: Nonaktifkan 2FA + disabled: Dinonaktifkan display_name: Nama domain: Domain edit: Ubah email: E-mail email_status: Status Email + enable: Aktifkan + enabled: Diaktifkan feed_url: URL Feed followers: Pengikut + followers_url: URL pengikut follows: Mengikut + inbox_url: URL Kotak masuk + invited_by: Diundang oleh + ip: IP + joined: Bergabung location: all: Semua local: Lokal remote: Remote title: Lokasi + login_status: Status login media_attachments: Lampiran media + memorialize: Ubah menjadi memoriam moderation: + active: Aktif all: Semua silenced: Didiamkan suspended: Disuspen title: Moderasi + moderation_notes: Catatan moderasi most_recent_activity: Aktivitas terbaru most_recent_ip: IP terbaru + no_limits_imposed: Tidak ada batasan not_subscribed: Tidak berlangganan + outbox_url: URL Kotak keluar perform_full_suspension: Lakukan suspen penuh profile_url: URL profil + promote: Promosikan + protocol: Protokol public: Publik push_subscription_expires: Langganan PuSH telah kadaluarsa + redownload: Muat ulang profil + remove_avatar: Hapus avatar + remove_header: Hapus header resend_confirmation: already_confirmed: Pengguna ini sudah dikonfirmasi send: Kirim ulang email konfirmasi success: Email konfirmasi berhasil dikirim! + reset: Reset reset_password: Reset kata sandi + resubscribe: Langganan ulang + role: Hak akses + roles: + admin: Administrator + moderator: Moderator + staff: Staf + user: Pengguna salmon_url: URL Salmon + search: Cari show: created_reports: Laporan yang dibuat oleh akun ini targeted_reports: Laporan yang dibuat tentang akun ini silence: Diam + silenced: Didiamkan statuses: Status + subscribe: Langganan + suspended: Disuspen title: Akun + unconfirmed_email: Email belum dikonfirmasi undo_silenced: Undo mendiamkan undo_suspension: Undo suspen + unsubscribe: Berhenti langganan username: Nama pengguna + warn: Beri Peringatan web: Web domain_blocks: add_new: Tambah diff --git a/config/locales/it.yml b/config/locales/it.yml index 339dadaf4..fea39b1fd 100644 --- a/config/locales/it.yml +++ b/config/locales/it.yml @@ -28,7 +28,7 @@ it: hosted_on: Mastodon ospitato su %{domain} learn_more: Scopri altro other_instances: Elenco istanze - privacy_policy: Policy su la Privacy + privacy_policy: Politica della privacy source_code: Codice sorgente status_count_after: one: status diff --git a/config/locales/ja.yml b/config/locales/ja.yml index 2b1b70639..598726c57 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -7,7 +7,7 @@ ja: administered_by: '管理者:' api: API apps: アプリ - closed_registrations: 現在このインスタンスでの新規登録は受け付けていません。しかし、他のインスタンスにアカウントを作成しても全く同じネットワークに参加することができます。 + closed_registrations: 現在このサーバーでの新規登録は受け付けていません。しかし、他のサーバーにアカウントを作成しても全く同じネットワークに参加することができます。 contact: 連絡先 contact_missing: 未設定 contact_unavailable: N/A @@ -24,10 +24,10 @@ ja: real_conversation_title: 本当のコミュニケーションのために within_reach_body: デベロッパーフレンドリーな API により実現された、iOS や Android、その他様々なプラットフォームのためのアプリでどこでも友人とやりとりできます。 within_reach_title: いつでも身近に - generic_description: "%{domain} は、Mastodon インスタンスの一つです" + generic_description: "%{domain} は、Mastodon サーバーの一つです" hosted_on: Mastodon hosted on %{domain} learn_more: もっと詳しく - other_instances: 他のインスタンス + other_instances: 他のサーバー privacy_policy: プライバシーポリシー source_code: ソースコード status_count_after: @@ -310,7 +310,7 @@ ja: all: すべて limited: 制限あり title: モデレーション - title: 既知のインスタンス + title: 既知のサーバー total_blocked_by_us: ブロック合計 total_followed_by_them: 被フォロー合計 total_followed_by_us: フォロー合計 @@ -392,8 +392,8 @@ ja: desc_html: 複数のページに表示されます。サイズは293x205px以上推奨です。未設定の場合、標準のマスコットが使用されます title: マスコットイメージ peers_api_enabled: - desc_html: 連合内でこのインスタンスが遭遇したドメインの名前 - title: 接続しているインスタンスのリストを公開する + desc_html: 連合内でこのサーバーが遭遇したドメインの名前 + title: 接続しているサーバーのリストを公開する preview_sensitive_media: desc_html: 他のウェブサイトにリンクを貼った際、メディアが閲覧注意としてマークされていてもサムネイルが表示されます title: OpenGraphによるプレビューで閲覧注意のメディアも表示する @@ -420,21 +420,21 @@ ja: desc_html: ユーザーページにスタッフのバッジを表示します title: スタッフバッジを表示する site_description: - desc_html: フロントページへの表示に使用される紹介文です。このMastodonインスタンスを特徴付けることやその他重要なことを記述してください。HTMLタグ、特に<code><a></code> と <code><em></code>が使えます。 - title: インスタンスの説明 + desc_html: フロントページへの表示に使用される紹介文です。このMastodonサーバーを特徴付けることやその他重要なことを記述してください。HTMLタグ、特に<code><a></code> と <code><em></code>が使えます。 + title: サーバーの説明 site_description_extended: - desc_html: あなたのインスタンスにおける行動規範やルール、ガイドライン、そのほかの記述をする際に最適な場所です。HTMLタグが使えます + desc_html: あなたのサーバーにおける行動規範やルール、ガイドライン、そのほかの記述をする際に最適な場所です。HTMLタグが使えます title: カスタム詳細説明 site_short_description: - desc_html: サイドバーと meta タグに表示されます。Mastodon とは何か、そしてこのサーバーの特別な何かを1段落で記述してください。空欄の場合、インスタンスの説明が使用されます。 - title: 短いインスタンスの説明 + desc_html: サイドバーと meta タグに表示されます。Mastodon とは何か、そしてこのサーバーの特別な何かを1段落で記述してください。空欄の場合、サーバーの説明が使用されます。 + title: 短いサーバーの説明 site_terms: desc_html: あなたは独自のプライバシーポリシーや利用規約、そのほかの法的根拠を書くことができます。HTMLタグが使えます title: カスタム利用規約 - site_title: インスタンスの名前 + site_title: サーバーの名前 thumbnail: desc_html: OpenGraphとAPIによるプレビューに使用されます。サイズは1200×630px推奨です - title: インスタンスのサムネイル + title: サーバーのサムネイル timeline_preview: desc_html: ランディングページに公開タイムラインを表示します title: タイムラインプレビュー @@ -495,7 +495,7 @@ ja: warning: このデータは気をつけて取り扱ってください。他の人と共有しないでください! your_token: アクセストークン auth: - agreement_html: 登録するをクリックすると <a href="%{rules_path}">インスタンスのルール</a> と <a href="%{terms_path}">プライバシーポリシー</a> に従うことに同意したことになります。 + agreement_html: 登録するをクリックすると <a href="%{rules_path}">サーバーのルール</a> と <a href="%{terms_path}">プライバシーポリシー</a> に従うことに同意したことになります。 change_password: パスワード confirm_email: メールアドレスの確認 delete_account: アカウントの削除 @@ -513,7 +513,7 @@ ja: cas: CAS saml: SAML register: 登録する - register_elsewhere: 他のインスタンスで新規登録 + register_elsewhere: 他のサーバーで新規登録 resend_confirmation: 確認メールを再送する reset_password: パスワードを再発行 security: セキュリティ @@ -549,7 +549,7 @@ ja: description_html: あなたのアカウントに含まれるコンテンツは全て削除され、アカウントは無効化されます。これは恒久的なもので、<strong>取り消すことはできません</strong>。なりすましを防ぐために、同じユーザー名で再度登録することはできなくなります。 proceed: アカウントを削除する success_msg: アカウントは正常に削除されました - warning_html: 削除が保証されるのはこのインスタンス上のコンテンツのみです。他のインスタンス等、外部に広く共有されたコンテンツについては痕跡が残ることがあります。また、現在接続できないサーバーや、あなたの更新を受け取らなくなったサーバーに対しては、削除は反映されません。 + warning_html: 削除が保証されるのはこのサーバー上のコンテンツのみです。他のサーバー等、外部に広く共有されたコンテンツについては痕跡が残ることがあります。また、現在接続できないサーバーや、あなたの更新を受け取らなくなったサーバーに対しては、削除は反映されません。 warning_title: 共有されたコンテンツについて directories: directory: ディレクトリ @@ -588,6 +588,10 @@ ja: lists: リスト mutes: ミュート storage: メディア + featured_tags: + add_new: 追加 + errors: + limit: 注目のハッシュタグの上限に達しました filters: contexts: home: ホームタイムライン @@ -606,7 +610,7 @@ ja: title: 新規フィルターを追加 followers: domain: ドメイン - explanation_html: あなたの投稿のプライバシーを確保したい場合、誰があなたをフォローしているのかを把握している必要があります。 <strong>プライベート投稿は、あなたのフォロワーがいる全てのインスタンスに配信されます</strong>。 フォロワーのインスタンスの管理者やソフトウェアがあなたのプライバシーを尊重してくれるかどうか怪しい場合は、そのフォロワーを削除した方がよいかもしれません。 + explanation_html: あなたの投稿のプライバシーを確保したい場合、誰があなたをフォローしているのかを把握している必要があります。 <strong>プライベート投稿は、あなたのフォロワーがいる全てのサーバーに配信されます</strong>。 フォロワーのサーバーの管理者やソフトウェアがあなたのプライバシーを尊重してくれるかどうか怪しい場合は、そのフォロワーを削除した方がよいかもしれません。 followers_count: フォロワー数 lock_link: 承認制アカウントにする purge: フォロワーから削除する @@ -629,10 +633,16 @@ ja: one: エラーが発生しました! 以下のエラーを確認してください other: エラーが発生しました! 以下の%{count}個のエラーを確認してください imports: - preface: 他のインスタンスでエクスポートされたファイルから、フォロー/ブロックした情報をこのインスタンス上のアカウントにインポートできます。 + modes: + merge: 統合 + merge_long: 現在のレコードを保持したまま新しいものを追加します + overwrite: 上書き + overwrite_long: 現在のレコードを新しいもので置き換えます + preface: 他のサーバーでエクスポートされたファイルから、フォロー/ブロックした情報をこのサーバー上のアカウントにインポートできます。 success: ファイルは正常にアップロードされ、現在処理中です。しばらくしてから確認してください types: blocking: ブロックしたアカウントリスト + domain_blocking: 非表示にしたドメインリスト following: フォロー中のアカウントリスト muting: ミュートしたアカウントリスト upload: アップロード @@ -654,7 +664,7 @@ ja: one: '1' other: "%{count}" max_uses_prompt: 無制限 - prompt: リンクを生成・共有してこのインスタンスへの新規登録を受け付けることができます + prompt: リンクを生成・共有してこのサーバーへの新規登録を受け付けることができます table: expires_at: 有効期限 uses: 使用 @@ -801,8 +811,9 @@ ja: development: 開発 edit_profile: プロフィールを編集 export: データのエクスポート + featured_tags: 注目のハッシュタグ flavours: フレーバー - followers: 信頼済みのインスタンス + followers: 信頼済みのサーバー import: データのインポート migrate: アカウントの引っ越し notifications: 通知 @@ -930,7 +941,8 @@ ja: <p>オリジナルの出典: <a href="https://github.com/discourse/discourse">Discourse privacy policy</a></p> title: "%{instance} 利用規約・プライバシーポリシー" themes: - default: Mastodon + contrast: Mastodon (ハイコントラスト) + default: Mastodon (ダーク) mastodon-light: Mastodon (ライト) time: formats: @@ -980,13 +992,13 @@ ja: final_action: 始めましょう final_step: 'さあ始めましょう! たとえフォロワーがいなくても、あなたの公開した投稿はローカルタイムラインやハッシュタグなどで誰かの目に止まるかもしれません。自己紹介をしたい時は #introductions ハッシュタグを使うといいかもしれません。' full_handle: あなたの正式なユーザー名 - full_handle_hint: これは別のインスタンスからフォローしてもらったりメッセージのやり取りをする際に、友達に伝えるといいでしょう。 + full_handle_hint: これは別のサーバーからフォローしてもらったりメッセージのやり取りをする際に、友達に伝えるといいでしょう。 review_preferences_action: 設定の変更 review_preferences_step: 受け取りたいメールや投稿の公開範囲などの設定を必ず行ってください。不快でないならアニメーション GIF の自動再生を有効にすることもできます。 subject: Mastodon へようこそ - tip_federated_timeline: 連合タイムラインは Mastodon ネットワークの流れを見られるものです。ただしあなたと同じインスタンスの人がフォローしている人だけが含まれるので、それが全てではありません。 - tip_following: 標準では自動でインスタンスの管理者をフォローしています。もっと興味のある人たちを見つけるには、ローカルタイムラインと連合タイムラインを確認してください。 - tip_local_timeline: ローカルタイムラインは %{instance} にいる人々の流れを見られるものです。彼らはあなたと同じインスタンスにいる隣人のようなものです! + tip_federated_timeline: 連合タイムラインは Mastodon ネットワークの流れを見られるものです。ただしあなたと同じサーバーの人がフォローしている人だけが含まれるので、それが全てではありません。 + tip_following: 標準では自動でサーバーの管理者をフォローしています。もっと興味のある人たちを見つけるには、ローカルタイムラインと連合タイムラインを確認してください。 + tip_local_timeline: ローカルタイムラインは %{instance} にいる人々の流れを見られるものです。彼らはあなたと同じサーバーにいる隣人のようなものです! tip_mobile_webapp: もしモバイル端末のブラウザで Mastodon をホーム画面に追加できる場合、プッシュ通知を受け取ることができます。それはまるでネイティブアプリのように動作します! tips: 豆知識 title: ようこそ、%{name} ! diff --git a/config/locales/ko.yml b/config/locales/ko.yml index bf6a8b770..0e95adf7b 100644 --- a/config/locales/ko.yml +++ b/config/locales/ko.yml @@ -93,7 +93,7 @@ ko: confirmed: 확인됨 confirming: 확인 중 deleted: 삭제됨 - demote: 모더레이터 강등 + demote: 강등 disable: 비활성화 disable_two_factor_authentication: 2단계 인증을 비활성화 disabled: 비활성화된 @@ -135,7 +135,7 @@ ko: outbox_url: 발신함 URL perform_full_suspension: 정지시키기 profile_url: 프로필 URL - promote: 모더레이터로 승급 + promote: 승급 protocol: 프로토콜 public: 전체 공개 push_subscription_expires: PuSH 구독 기간 만료 @@ -350,7 +350,7 @@ ko: report: 리포트 action_taken_by: 신고 처리자 are_you_sure: 정말로 실행하시겠습니까? - assign_to_self: 나에게 할당 됨 + assign_to_self: 나에게 할당하기 assigned: 할당 된 모더레이터 comment: none: 없음 diff --git a/config/locales/lt.yml b/config/locales/lt.yml new file mode 100644 index 000000000..52aa868a6 --- /dev/null +++ b/config/locales/lt.yml @@ -0,0 +1,182 @@ +--- +lt: + about: + about_hashtag_html: Čia visiems prieinamas įrankis <strong>#%{hashtag}</strong>. Jūs galite juo naudotis bet kur, jeigu turite paskyra fedi-visatoje. + about_mastodon_html: Mastodon, tai socialinis tinklas pagrįstas atviro kodo programavimu, ir atvirais web protokolais. Visiškai nemokamas. Ši sistema decantrilizuota kaip jūsų elektroninis paštas. + about_this: Apie + administered_by: 'Administruoja:' + api: API + apps: Mobilioji Aplikacija + closed_registrations: Registracija šiuo metu uždaryta prie šito tinklo. Jūs galite rasti kitą būdą susikurti paskyrą ir gauti prieeiga prie to paties tinklo. + contact: Kontaktai + contact_missing: Nenustatyta + contact_unavailable: N/A + documentation: Dokumentacija + extended_description_html: | + <h3>Taisyklės</h3> + <p>Ilgas aprašymas dar nėra sudartyas</p> + features: + humane_approach_body: Mokantis iš kitų socialinių tinklų, bei jų daromu klaidų, Mastodon siekia sukurti etiška dizainą, kovojant su netinkamu socialinių tinklų naudojimu. + humane_approach_title: Humaniškesnis metodas + not_a_product_body: Mastodon nėra komercinis tinklas. Jokių reklamų, privačios informacijos rinkimo. Čia nėra vieno žmogaus, kuris už viską atsako. + not_a_product_title: Tu esi žmogus, o ne produktas + real_conversation_body: Su 500 simbolių limitu, ir galimybe pažymėti savo įkeliama informacija su įspėjamaisiais ženklais, galite išsireikšti kaip tik norite. + real_conversation_title: Sukurtas tikram bendravimui + within_reach_body: Mobiliosios aplikacijos skirtos iOS, Android, ir kitoms platformoms. Draugiškos API ekosistemos dėka, Jūs galite palaikyti pokalbi su draugais bet kur. + within_reach_title: Visada pasiekama + generic_description: "%{domain} yra vienas serveris tinkle" + hosted_on: Mastodon palaikomas naudojantis %{domain} talpinimu + learn_more: Daugiau + privacy_policy: Privatumo Politika + source_code: Šaltinio kodas + status_count_after: + few: statusai + one: statusas + other: statusai + status_count_before: Autorius + terms: Naudojimo sąlygos + user_count_after: + few: vartotojai + one: vartotojas + other: vartotojai + user_count_before: Namai + what_is_mastodon: Kas tai, Mastodon? + accounts: + choices_html: "%{name} pasirinkimai:" + follow: Sekti + followers: + few: Sekėjai + one: Sekėjas + other: Sekėjai + following: Sekami + joined: Prisijungiai %{date} + last_active: paskutinį kartą aktyvus + link_verified_on: Nuorodos nuosavybė paskutinį kartą tikrinta %{date} + media: Medija + moved_html: "%{name} persikėlė į %{new_profile_link}:" + network_hidden: Ši informacija neprieinama + nothing_here: Čia nieko nėra! + people_followed_by: Žmonės, kuriuos %{name} seka + people_who_follow: Žmonės kurie seka %{name} + pin_errors: + following: Privalai sekti žmogų kurį nori pagerbti + posts: + few: Tootai + one: Tootas + other: Tootai + posts_tab_heading: Tootai + posts_with_replies: Tootai ir atsakymai + reserved_username: Vartotojo vardas rezervuotas + roles: + admin: Administratorius + bot: Bot'as + moderator: Moderatorius + unfollow: Nesekti + admin: + account_actions: + action: Veiksmas + title: Moderuoti %{acct} + account_moderation_notes: + create: Palikti žinutę + created_msg: Moderavimo žinutė sėkimngai sukurta! + delete: Ištrinti + destroyed_msg: Moderacijos žinutė sėkmingai ištrinta! + accounts: + are_you_sure: Ar esate įsitikinęs? + avatar: Profilio nuotrauka + by_domain: Domenas + change_email: + changed_msg: Paskyros el paštas sėkmingai pakeistas! + current_email: Dabartinis el paštas + label: Pakeisti el pašto adresą + new_email: Naujas el pašto adresas + submit: Pakeisti el pašto adresą + title: Pakeisti el pašto adresą vartotojui %{username} + confirm: Patvirtinti + confirmed: Patvirtinta + confirming: Tvirtinama + deleted: Ištrinti + demote: Pažeminti + disable: Išjungti + disable_two_factor_authentication: Išjungti 2 faktorių autentifikaciją + disabled: Išjungta + display_name: Matomas vardas + domain: Domenas + edit: Keisti + email: El paštas + email_status: El pašto statusas + enable: Įjungti + enabled: Įjungta + followers: Sekėjai + followers_url: Sekėjų URL + follows: Seka + header: Antraštė + inbox_url: Gautųjų URL + invited_by: Pakvietė + ip: IP + joined: Prisijungė + location: + all: Visi + local: Lokali + title: Lokacija + login_status: Prisijungimo statusas + media_attachments: Prisegti medijos failai + memorialize: Paversti į memorija + moderation: + active: Aktyvus + all: Visi + silenced: Užtildytas + suspended: Užrakintas + title: Moderacija + moderation_notes: Medaracijos žinutės + most_recent_activity: Paskutinioji veikla + most_recent_ip: Paskutinis IP + no_limits_imposed: Be limitu + not_subscribed: Ne prenumeruota + outbox_url: Išsiustųjų URL + perform_full_suspension: Užrakinti + profile_url: Profilio URL + promote: Paaukštinti + protocol: Protokolas + public: Viešas + push_subscription_expires: PuSH prenumeramivas pasibaigė + redownload: Perkrauti profilį + remove_avatar: Panaikinti profilio nuotrauką + remove_header: Panaikinti antraštę + resend_confirmation: + already_confirmed: Šis vartotojas jau patvirtintas + send: Dar kartą išsiųsti patvirtinimo žinutę + success: Patvirtinimo laiškas sėkmingai išsiųstas! + reset: Iš naujo + reset_password: Atkurti slaptažodį + resubscribe: Per prenumeruoti + role: Leidimai + roles: + admin: Administratorius + moderator: Moderatorius + staff: Personalas + user: Vartotojas + salmon_url: Lašišos URL + search: Ieškoti + shared_inbox_url: Bendroji gautųjų URL + show: + created_reports: Parašyti raportai + targeted_reports: Reportuotas kitų + silence: Tyla + silenced: Užtildytas + statuses: Statusai + subscribe: Prenumeruoti + suspended: Užrakintas + title: Vartotojai + unconfirmed_email: Nepatvirtintas el pašto adresas + undo_silenced: Atšaukti užtildymą + undo_suspension: Atšaukti užrakinimą + unsubscribe: Nebeprenumeruoti + username: Slapyvardis + warn: Įspėti + web: Web + action_logs: + actions: + assigned_to_self_report: "%{name} paskyrė reportą %{target} saviems" + change_email_user: "%{name} pakeitė el pašto adresą vartotojui %{target}" + confirm_user: "%{name} patvirtino el pašto adresą vartotojui %{target}" diff --git a/config/locales/pl.yml b/config/locales/pl.yml index 9f457a460..1567ac626 100644 --- a/config/locales/pl.yml +++ b/config/locales/pl.yml @@ -565,8 +565,11 @@ pl: warning_title: Dostępność usuniętej zawartości directories: directory: Katalog profilów + enabled: Jesteś obecnie zapisany(-a) do katalogu + enabled_but_waiting: Jesteś zapisany(-a) do katalogu, ale jeszcze nie śledzi Cię wystarczająca liczba osób (%{min_followers}), aby się tam pojawić. explanation: Poznaj profile na podstawie zainteresowań explore_mastodon: Odkrywaj %{title} + how_to_enable: Nie jesteś obecnie zapisany(-a) do katalogu. Poniżej możesz zapisać się. Użyj hashtagów w swoim opisie, aby zostać wyświetlonym pod określonymi hashtagami! people: few: "%{count} osoby" many: "%{count} osób" @@ -599,6 +602,10 @@ pl: lists: Listy mutes: Wyciszeni storage: Urządzenie przechowujące dane + featured_tags: + add_new: Dodaj nowy + errors: + limit: Już przekroczyłeś(-aś) maksymalną liczbę wyróżnionych hashtagów filters: contexts: home: Strona główna @@ -621,7 +628,11 @@ pl: followers_count: Liczba śledzących lock_link: Zablokuj swoje konto purge: Przestań śledzić - success: W trakcie usuwania śledzących z %{count} domen… + success: + few: W trakcie usuwania śledzących z %{count} domen… + many: W trakcie usuwania śledzących z %{count} domen… + one: W trakcie usuwania śledzących z jednej domeny… + other: W trakcie usuwania śledzących z %{count} domen… true_privacy_html: Pamiętaj, że <strong>rzeczywista prywatność może zostać uzyskana wyłącznie dzięki szyfrowaniu end-to-end</strong>. unlocked_warning_html: Każdy może Cię śledzić, dzięki czemu może zobaczyć Twoje niepubliczne wpisy. %{lock_link} aby móc kontrolować, kto Cię śledzi. unlocked_warning_title: Twoje konto nie jest zablokowane @@ -631,6 +642,7 @@ pl: resources: Zasoby generic: changes_saved_msg: Ustawienia zapisane! + copy: Kopiuj save_changes: Zapisz zmiany use_this: Użyj tego validation_errors: @@ -639,10 +651,16 @@ pl: one: Coś jest wciąż nie tak! Przyjrzyj się poniższemu błędowi other: Coś jest wciąż nie tak! Przejrzyj poniższe błędy (%{count}) imports: + modes: + merge: Połącz + merge_long: Zachowaj obecne wpisy i dodaj nowe + overwrite: Nadpisz + overwrite_long: Zastąp obecne wpisy nowymi preface: Możesz zaimportować pewne dane (np. lista kont, które śledzisz lub blokujesz) do swojego konta na tym serwerze, korzystając z danych wyeksportowanych z innego serwera. success: Twoje dane zostały załadowane i zostaną niebawem przetworzone types: blocking: Lista blokowanych + domain_blocking: Lista zablokowanych domen following: Lista śledzonych muting: Lista wyciszonych upload: Załaduj @@ -751,10 +769,25 @@ pl: no_account_html: Nie masz konta? Możesz <a href='%{sign_up_path}' target='_blank'>zarejestrować się tutaj</a> proceed: Śledź prompt: 'Zamierzasz śledzić:' + reason_html: "<strong>Dlaczego ten krok jest konieczny?</strong> <code>%{instance}</code> może nie być serwerem na którym jesteś zarejestrowany(-a), więc musisz zostać przekierowany(-a) na swój serwer." + remote_interaction: + favourite: + proceed: Przejdź do dodania do ulubionych + prompt: 'Chcesz dodać ten wpis do ulubionych:' + reblog: + proceed: Przejdź do podbicia + prompt: 'Chcesz podbić ten wpis:' + reply: + proceed: Przejdź do dodawania odpowiedzi + prompt: 'Chcesz odpowiedzieć na ten wpis:' remote_unfollow: error: Błąd title: Tytuł - unfollowed: Przestałeś śledzić + unfollowed: Przestałeś(-aś) śledzić + scheduled_statuses: + over_daily_limit: Przekroczyłeś(-aś) limit %{limit} zaplanowanych wpisów na ten dzień + over_total_limit: Przekroczyłeś(-aś) limit %{limit} zaplanowanych wpisów + too_soon: Zaplanowana data musi wypadać w przyszłości sessions: activity: Ostatnia aktywność browser: Przeglądarka @@ -803,6 +836,7 @@ pl: development: Tworzenie aplikacji edit_profile: Edytuj profil export: Eksportowanie danych + featured_tags: Wyróżnione hashtagi flavours: Odmiany followers: Autoryzowani śledzący import: Importowanie danych @@ -827,7 +861,11 @@ pl: other: "%{count} filmów" boosted_from_html: Podbito przez %{acct_link} content_warning: 'Ostrzeżenie o zawartości: %{warning}' - disallowed_hashtags: 'zawiera niedozwolone hashtagi: %{tags}' + disallowed_hashtags: + few: 'zawiera niedozwolone hashtagi: %{tags}' + many: 'zawiera niedozwolone hashtagi: %{tags}' + one: 'zawiera niedozwolony hashtag: %{tags}' + other: 'zawiera niedozwolone hashtagi: %{tags}' language_detection: Automatycznie wykrywaj język open_in_web: Otwórz w przeglądarce over_character_limit: limit %{max} znaków przekroczony diff --git a/config/locales/simple_form.cs.yml b/config/locales/simple_form.cs.yml index 0e255e4dc..99127c2b6 100644 --- a/config/locales/simple_form.cs.yml +++ b/config/locales/simple_form.cs.yml @@ -15,14 +15,14 @@ cs: bot: Tento účet provádí hlavně automatizované akce a nemusí být spravován context: Jedno či více kontextů, ve kterých má být filtr uplatněn digest: Odesíláno pouze po dlouhé době nečinnosti a pouze, pokud jste při své nepřítomnosti obdržel/a osobní zprávy - discoverable_html: <a href="%{path}" target="_blank">Adresář</a> dovoluje lidem najít účty podle zájmů a aktivity. Vyžaduje alespoň %{min_followers} sledovatelů + discoverable_html: <a href="%{path}" target="_blank">Adresář</a> dovoluje lidem najít účty podle zájmů a aktivity. Vyžaduje alespoň %{min_followers} sledujících email: Bude vám poslán potvrzovací e-mail fields: Na profilu můžete mít až 4 položky zobrazené jako tabulka header: PNG, GIF či JPG. Maximálně %{size}. Bude zmenšen na %{dimensions} px inbox_url: Zkopírujte URL z hlavní stránky mostu, který chcete použít irreversible: Filtrované tooty nenávratně zmizí, i pokud bude filtr později odstraněn locale: Jazyk uživatelského rozhraní, e-mailů a oznámení push - locked: Vyžaduje manuální schvalování sledovatelů + locked: Vyžaduje, abyste ručně schvaloval/a sledující password: Použijte alespoň 8 znaků phrase: Shoda bude nalezena bez ohledu na velikost písmen v těle tootu či varování o obsahu scopes: Které API bude aplikace povolena používat. Pokud vyberete rozsah nejvyššího stupně, nebudete je muset vybírat po jednom. @@ -37,7 +37,7 @@ cs: username: Vaše uživatelské jméno bude na %{domain} unikátní whole_word: Je-li klíčové slovo či fráze pouze alfanumerická, bude aplikována pouze, pokud se shoduje s celým slovem imports: - data: Soubor CSV exportován z jiné instance Mastodon + data: Soubor CSV exportovaný z jiného serveru Mastodon sessions: otp: 'Napište dvoufaktorový kód vygenerovaný vaší mobilní aplikací, nebo použijte jeden z vašich záložních kódů:' user: diff --git a/config/locales/simple_form.cy.yml b/config/locales/simple_form.cy.yml index d3f9cb340..24ae49a2a 100644 --- a/config/locales/simple_form.cy.yml +++ b/config/locales/simple_form.cy.yml @@ -38,6 +38,18 @@ cy: fields: name: Label value: Cynnwys + account_warning_preset: + text: Testun rhagosodedig + admin_account_action: + send_email_notification: Hysbysu'r defnyddiwr trwy e-bost + text: Rhybudd wedi'i addasu + type: Gweithredu + types: + disable: Analluogi + none: Gwneud dim + silence: Tawelwch + suspend: Dileu data cyfrif + warning_preset_id: Defnyddiwch ragnod rhag rhybudd defaults: autofollow: Gwahodd i ddilyn eich cyfrif avatar: Afatar @@ -48,6 +60,7 @@ cy: context: Hidlo cyd-destunau current_password: Cyfrinair presennol data: Data + discoverable: Rhestrwch y cyfrif hwn ar y cyfeiriadur display_name: Enw arddangos email: Cyfeiriad e-bost expires_in: Yn dod i ben ar ôl @@ -63,6 +76,7 @@ cy: otp_attempt: Côd dau gam password: Cyfrinair phrase: Allweddair neu ymadrodd + setting_aggregate_reblogs: Grŵp hybiau mewn llinellau amser setting_auto_play_gif: Chwarae GIFs wedi'u hanimeiddio yn awtomatig setting_boost_modal: Dangos deialog cadarnhad cyn bŵstio setting_default_language: Cyhoeddi iaith diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index 674abff63..ad9ae7417 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -37,8 +37,10 @@ en: setting_skin: Reskins the selected Mastodon flavour username: Your username will be unique on %{domain} whole_word: When the keyword or phrase is alphanumeric only, it will only be applied if it matches the whole word + featured_tag: + name: 'You might want to use one of these:' imports: - data: CSV file exported from another Mastodon instance + data: CSV file exported from another Mastodon server sessions: otp: 'Enter the two-factor code generated by your phone app or use one of your recovery codes:' user: @@ -112,6 +114,8 @@ en: username: Username username_or_email: Username or Email whole_word: Whole word + featured_tag: + name: Hashtag interactions: must_be_follower: Block notifications from non-followers must_be_following: Block notifications from people you don't follow diff --git a/config/locales/simple_form.eu.yml b/config/locales/simple_form.eu.yml index 0ffc22b53..2b9c2bb81 100644 --- a/config/locales/simple_form.eu.yml +++ b/config/locales/simple_form.eu.yml @@ -57,6 +57,7 @@ eu: disable: Desaktibatu none: Ez egin ezer silence: Isiltarazi + suspend: Kanporatu eta behin betiko ezabatu kontuko datuak warning_preset_id: Erabili aurre-ezarritako abisu bat defaults: autofollow: Gonbidatu zure kontua jarraitzera diff --git a/config/locales/simple_form.gl.yml b/config/locales/simple_form.gl.yml index d5e0ef574..aebab5247 100644 --- a/config/locales/simple_form.gl.yml +++ b/config/locales/simple_form.gl.yml @@ -41,7 +41,7 @@ gl: sessions: otp: 'Introduza o código de doble-factor xerado no aplicativo do seu móbil ou utilice un dos seus códigos de recuperación:' user: - chosen_languages: Si se marca, só os toots nos idiomas seleccionados serán mostrados en liñas temporais públicas + chosen_languages: Se ten marca, só os toots nos idiomas seleccionados serán mostrados en liñas temporais públicas labels: account: fields: diff --git a/config/locales/simple_form.ja.yml b/config/locales/simple_form.ja.yml index 9419331e4..2cade4301 100644 --- a/config/locales/simple_form.ja.yml +++ b/config/locales/simple_form.ja.yml @@ -33,11 +33,14 @@ ja: setting_display_media_show_all: 閲覧注意としてマークされたメディアも常に表示する setting_hide_network: フォローとフォロワーの情報がプロフィールページで見られないようにします setting_noindex: 公開プロフィールおよび各投稿ページに影響します + setting_show_application: トゥートするのに使用したアプリがトゥートの詳細ビューに表示されるようになります setting_theme: ログインしている全てのデバイスで適用されるデザインです。 username: あなたのユーザー名は %{domain} の中で重複していない必要があります whole_word: キーワードまたはフレーズが英数字のみの場合、単語全体と一致する場合のみ適用されるようになります + featured_tag: + name: 'これらを使うといいかもしれません:' imports: - data: 他の Mastodon インスタンスからエクスポートしたCSVファイルを選択して下さい + data: 他の Mastodon サーバーからエクスポートしたCSVファイルを選択して下さい sessions: otp: '携帯電話のアプリで生成された二段階認証コードを入力するか、リカバリーコードを使用してください:' user: @@ -101,6 +104,7 @@ ja: setting_hide_network: 繋がりを隠す setting_noindex: 検索エンジンによるインデックスを拒否する setting_reduce_motion: アニメーションの動きを減らす + setting_show_application: トゥートの送信に使用したアプリを開示する setting_system_font_ui: システムのデフォルトフォントを使う setting_theme: サイトテーマ setting_unfollow_modal: フォローを解除する前に確認ダイアログを表示する @@ -109,6 +113,8 @@ ja: username: ユーザー名 username_or_email: ユーザー名またはメールアドレス whole_word: 単語全体にマッチ + featured_tag: + name: ハッシュタグ interactions: must_be_follower: フォロワー以外からの通知をブロック must_be_following: フォローしていないユーザーからの通知をブロック diff --git a/config/locales/simple_form.pl.yml b/config/locales/simple_form.pl.yml index 660841e06..f5b5a6ca5 100644 --- a/config/locales/simple_form.pl.yml +++ b/config/locales/simple_form.pl.yml @@ -33,9 +33,12 @@ pl: setting_display_media_show_all: Zawsze pokazuj zawartość multimedialną jako wrażliwą setting_hide_network: Informacje o tym, kto Cię śledzi i kogo śledzisz nie będą widoczne setting_noindex: Wpływa na widoczność strony profilu i Twoich wpisów + setting_show_application: W informacjach o wpisie będzie widoczna informacja o aplikacji, z której został wysłany setting_skin: Zmienia wygląd używanej odmiany Mastodona username: Twoja nazwa użytkownika będzie niepowtarzalna na %{domain} whole_word: Jeśli słowo lub fraza składa się jedynie z liter lub cyfr, filtr będzie zastosowany tylko do pełnych wystąpień + featured_tag: + name: 'Sugerujemy użycie jednego z następujących:' imports: data: Plik CSV wyeksportowany z innej instancji Mastodona sessions: @@ -102,6 +105,7 @@ pl: setting_hide_network: Ukryj swoją sieć setting_noindex: Nie indeksuj mojego profilu w wyszukiwarkach internetowych setting_reduce_motion: Ogranicz ruch w animacjach + setting_show_application: Informuj o aplikacji z której wysłano wpisy setting_skin: Motyw setting_system_font_ui: Używaj domyślnej czcionki systemu setting_unfollow_modal: Pytaj o potwierdzenie przed cofnięciem śledzenia @@ -110,6 +114,8 @@ pl: username: Nazwa użytkownika username_or_email: Nazwa użytkownika lub adres e-mail whole_word: Całe słowo + featured_tag: + name: Hashtag interactions: must_be_follower: Nie wyświetlaj powiadomień od osób, które Cię nie śledzą must_be_following: Nie wyświetlaj powiadomień od osób, których nie śledzisz diff --git a/config/locales/simple_form.sk.yml b/config/locales/simple_form.sk.yml index f2d26cf02..41d045a4a 100644 --- a/config/locales/simple_form.sk.yml +++ b/config/locales/simple_form.sk.yml @@ -14,7 +14,7 @@ sk: avatar: PNG, GIF alebo JPG. Maximálne %{size}. Bude zmenšený na %{dimensions}px bot: Tento účet vykonáva hlavne automatizované akcie, a je pravdepodobne nespravovaný context: Jedno, alebo viac kritérií, v ktorých má byť filtrovanie uplatnené - digest: Odoslané iba v prípade dlhodobej neprítomnosti, a len ak si obdŕžal/a nejaké osobné správy kým si bol/a preč + digest: Odoslané iba v prípade dlhodobej neprítomnosti, a len ak si obdržal/a nejaké osobné správy kým si bol/a preč discoverable_html: Táto <a href="%{path}" target="_blank">databáza</a> umožňuje ľudom nájsť profily podľa záujmu a aktívnosti. Vyžaduje aby mali aspoň %{min_followers} sledovateľov email: Bude ti odoslaný potvrdzujúci email fields: Až štyri položky môžeš mať na svojom profile zobrazené vo forme tabuľky @@ -22,7 +22,7 @@ sk: inbox_url: Skopíruj adresu z hlavnej stránky mostíka, ktorý chceš používať irreversible: Vytriedené príspevky zmiznú nenávratne, aj keď triedenie neskôr zrušíš locale: Jazyk užívateľského rozhrania, emailových, a nástenkových oboznámení - locked: Vyžaduje, aby si manuálne schvaľoval/a následovateľov + locked: Vyžaduje sa manuálne schvaľovanie sledujúcich password: Zadaj aspoň osem znakov phrase: Zhoda sa nájde nezávisle od toho, či je text napísaný, veľkými, alebo malými písmenami, či už v tele, alebo v hlavičke scopes: Ktoré API budú povolené aplikácii pre prístup. Ak vyberieš vrcholný stupeň, nemusíš už potom vyberať po jednom. @@ -69,8 +69,8 @@ sk: context: Triedenie kontextov current_password: Súčasné heslo data: Dáta - discoverable: Zaraď tento účet do databázy - display_name: Meno + discoverable: Zaraď tento účet do databázy profilov + display_name: Zobrazované meno email: Emailová adresa expires_in: Expirovať po fields: Metadáta profilu @@ -99,8 +99,8 @@ sk: setting_expand_spoilers: Stále rozbaľ príspevky označené varovaním o obsahu setting_hide_network: Ukri svoju sieť kontaktov setting_noindex: Nezaraďuj príspevky do indexu pre vyhľadávče - setting_reduce_motion: Redukovať pohyb v animáciách - setting_system_font_ui: Použiť základné systémové písmo + setting_reduce_motion: Mierni pohyb pri animáciách + setting_system_font_ui: Použi základné systémové písmo setting_theme: Vzhľad webu setting_unfollow_modal: Zobrazuj potvrdzovacie okno pred skončením sledovania iného užívateľa severity: Závažnosť @@ -109,8 +109,8 @@ sk: username_or_email: Prezívka, alebo email whole_word: Celé slovo interactions: - must_be_follower: Blokuj oboznámenia ohľadom užívateľov, ktorí ťa nesledujú - must_be_following: Blokuj oboznámenia ohľadom ľudí ktorých nesleduješ + must_be_follower: Blokovať oznámenia od nesledujúcich + must_be_following: Blokovať oznámenia od ľudí, ktorých nesleduješ must_be_following_dm: Blokuj súkromné správy od ľudí ktorých nesleduješ notification_emails: digest: Posielaj súhrnné emaily diff --git a/config/locales/simple_form.sl.yml b/config/locales/simple_form.sl.yml index 890cbac41..781485864 100644 --- a/config/locales/simple_form.sl.yml +++ b/config/locales/simple_form.sl.yml @@ -96,7 +96,7 @@ sl: follow_request: Pošlji e-pošto, ko vam nekdo želi slediti mention: Pošlji e-pošto, ko vas nekdo omeni reblog: Pošlji e-pošto, ko nekdo sune vaše stanje - report: Pošlji e-pošto, ko je oddano novo poročilo + report: Pošlji e-pošto, ko je oddana nova prijava 'no': Ne required: mark: "*" diff --git a/config/locales/simple_form.sq.yml b/config/locales/simple_form.sq.yml new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/config/locales/simple_form.sq.yml @@ -0,0 +1 @@ +{} diff --git a/config/locales/sk.yml b/config/locales/sk.yml index d0f2524f9..4f89e7162 100644 --- a/config/locales/sk.yml +++ b/config/locales/sk.yml @@ -3,7 +3,7 @@ sk: about: about_hashtag_html: Toto sú verejné príspevky otagované pod <strong>#%{hashtag}</strong>. Ak máš účet hocikde v rámci fediversa, môžeš ich využívať. about_mastodon_html: Mastodon je sociálna sieť založená na otvorených webových protokoloch a na slobodnom softvéri. Je decentralizovaná, podobne ako email. - about_this: O tejto instancii + about_this: O tomto serveri administered_by: 'Správcom je:' api: API apps: Aplikácie @@ -64,7 +64,7 @@ sk: posts: few: Príspevkov one: Príspevok - other: Príspevkov + other: Príspevky posts_tab_heading: Príspevky posts_with_replies: Príspevky s odpoveďami reserved_username: Prihlasovacie meno je rezervované @@ -122,11 +122,11 @@ sk: local: Miestne remote: Federované title: Lokácia - login_status: Status prihlásenia + login_status: Stav prihlásenia media_attachments: Prílohy memorialize: Zmeniť na "Navždy budeme spomínať" moderation: - active: Aktívny/a + active: Aktívny all: Všetko silenced: Umlčané suspended: Vylúčený/á @@ -139,7 +139,7 @@ sk: outbox_url: URL poslaných perform_full_suspension: Zablokovať profile_url: URL profilu - promote: Povýšiť + promote: Povýš protocol: Protokol public: Verejná os push_subscription_expires: PuSH odoberanie expiruje @@ -215,8 +215,8 @@ sk: custom_emojis: by_domain: Doména copied_msg: Lokálna kópia emoji úspešne vytvorená - copy: Kopírovať - copy_failed_msg: Nebolo možné vytvoriť lokálnu kópiu tohto emoji + copy: Kopíruj + copy_failed_msg: Nebolo možné vytvoriť miestnu kópiu tohto emoji created_msg: Emoji úspešne vytvorené! delete: Zmazať destroyed_msg: Emojo úspešne zničený! @@ -256,7 +256,7 @@ sk: title: Spravovacie rozhranie total_users: užívateľov celkovo trends: Trendy - week_interactions: Tohto týždňové interakcie + week_interactions: Tohto-týždňové interakcie week_users_active: aktívni tento týždeň week_users_new: užívateľov počas tohto týždňa domain_blocks: @@ -271,7 +271,7 @@ sk: desc_html: "<strong>Stíšenie</strong> urobí všetky príspevky daného účtu neviditeľné pre všetkých ktorí nenásledujú tento účet. <strong>Suspendácia</strong> zmaže všetky príspevky, médiá a profilové informácie. Použi <strong>Žiadne</strong>, ak chceš iba neprijímať súbory médií." noop: Nič silence: Stíšiť - suspend: Suspendovať + suspend: Vylúčiť title: Nové blokovanie domény reject_media: Odmietať súbory s obrázkami alebo videami reject_media_hint: Zmaže lokálne uložené súbory médií a odmietne ich sťahovanie v budúcnosti. Irelevantné pre suspendáciu @@ -398,10 +398,10 @@ sk: desc_html: Zobrazované na viacerých stránkach. Odporúčaná veľkosť aspoň 293×205px. Pokiaľ nieje nahraté, bude zobrazený základný maskot title: Obrázok maskota peers_api_enabled: - desc_html: Domény na ktoré táto instancia už vo fediverse natrafila - title: Zverejniť zoznam objavených instancií + desc_html: Domény, na ktoré tento server už vo fediverse natrafil + title: Zverejni zoznam objavených serverov preview_sensitive_media: - desc_html: Náhľad adresy z iných instancií, bude zobrazený aj vtedy, keď sú dané médiá označené ako senzitívne + desc_html: Náhľad odkazov z iných serverov, bude zobrazený aj vtedy, keď sú médiá označené ako senzitívne title: Ukazuj aj chúlostivé médiá v náhľadoch OpenGraph profile_directory: desc_html: Povoliť užívateľom aby boli nájdení @@ -429,13 +429,13 @@ sk: desc_html: Oboznamujúci paragraf na hlavnej stránke a pri meta tagoch. Opíš, čo robí tento Mastodon server špecifickým, a ďalej hocičo iné, čo považuješ za dôležité. Môžeš použiť HTML kód, hlavne <code>< a ></code>, ale tiež <code><></code>. title: Popis instancie site_description_extended: - desc_html: Toto je vhodné miesto pre vaše pravidlá, oboznámenia a iné veci, ktorými je vaša instancia špecifická. Je možné tu používať HTML kód + desc_html: Toto je vhodné miesto pre tvoje pravidlá o prevádzke, pokyny a iné veci, ktorými je tvoja instancia špecifická. Je možné tu používať HTML kód title: Vlastné doplňujúce informácie site_short_description: desc_html: Zobrazené na bočnom paneli a pri meta tagoch. Popíš čo je Mastodon, a čo robí tento server iným, v jednom paragrafe. Pokiaľ toto necháš prázdne, bude tu zobrazený základný popis instancie. title: Krátky popis instancie site_terms: - desc_html: Môžete si napísať vaše vlastné pravidla o súkromí, prevádzke, alebo aj iné legality. Môžete tu používať HTML kód + desc_html: Môžeš si napísať svoje vlastné pravidla o súkromí, prevádzke, alebo aj iné legality. Môžeš tu používať HTML kód title: Vlastné pravidlá prevádzky site_title: Názov instancie thumbnail: @@ -488,10 +488,10 @@ sk: application_mailer: notification_preferences: Zmeniť e-mailové voľby salutation: "%{name}," - settings: 'Zmeniť e-mailové voľby: %{link}' + settings: 'Zmeň emailové voľby: %{link}' view: 'Zobraziť:' - view_profile: Zobraziť profil - view_status: Zobraziť status + view_profile: Zobraz profil + view_status: Zobraz status applications: created: Aplikácia bola vytvorená úspešne destroyed: Aplikáciu sa podarilo odstrániť @@ -506,13 +506,13 @@ sk: confirm_email: Potvrdiť email delete_account: Vymazať účet delete_account_html: Pokiaľ chceš vymazať svoj účet, môžeš tak <a href="%{path}">urobiť tu</a>. Budeš požiadaný/á o potvrdenie tohto kroku. - didnt_get_confirmation: Neobdŕžal/a si kroky pre potvrdenie? + didnt_get_confirmation: Neobdržal/a si kroky pre potvrdenie? forgot_password: Zabudnuté heslo? invalid_reset_password_token: Token na obnovu hesla vypršal. Prosím vypítaj si nový. login: Prihlás sa logout: Odhlás sa - migrate_account: Presunúť sa na iný účet - migrate_account_html: Pokiaľ si želáš presmerovať tento účet na nejaký iný, môžeš to <a href="%{path}">urobiť tu</a>. + migrate_account: Presúvam sa na iný účet + migrate_account_html: Pokiaľ si želáš presmerovať tento účet na nejaký iný, môžeš si to <a href="%{path}">nastaviť tu</a>. or: alebo or_log_in_with: Alebo prihlásiť z providers: @@ -570,16 +570,16 @@ sk: other: "%{count} ľudia" errors: '403': Nemáš povolenie na zobrazenie tejto stránky. - '404': Stránka ktorú si hľadal/a sa tu nenachádza. - '410': Stránka ktorú si tu hľadal/a už viac neexistuje. + '404': Stránka ktorú hľadáš nieje tu. + '410': Stránka ktorú si tu hľadal/a sa tu už viac nenachádza. '422': content: Bezpečtnostné overenie zlyhalo. Blokuješ cookies? title: Bezpečtnostné overenie zlyhalo '429': Zamlčané '500': - content: Ospravedlňujeme sa. Niečo sa pokazilo na našom konci. + content: Ospravedlňujem sa. Niečo sa pokazilo na našom konci. title: Táto stránka nieje v poriadku - noscript_html: Aby bolo možné používať Mastodon web aplikáciu, prosím povoľte JavaScript. Alebo skúste jednu z <a href="%{apps_path}"> aplikácii </a> dostupných pre vašu platformu. + noscript_html: Aby bolo možné používať Mastodon web aplikáciu, povoľ prosím JavaScript. Alebo skús jednu z <a href="%{apps_path}"> aplikácii </a> dostupných pre vašu platformu. exports: archive_takeout: date: Dátum @@ -598,7 +598,7 @@ sk: filters: contexts: home: Domáca os - notifications: Oboznámenia + notifications: Oznámenia public: Verejné osi thread: Konverzácie edit: @@ -607,7 +607,7 @@ sk: invalid_context: Nebola poskytnutá žiadna, alebo ide o neplatnú súvislosť invalid_irreversible: Nezvratné filtrovanie funguje iba so súvislostiami domovskej osi a oboznámení index: - delete: Vymazať + delete: Vymaž title: Triedenia new: title: Pridaj nové triedenie @@ -615,8 +615,8 @@ sk: domain: Doména explanation_html: Pokiaľ chceš zaručiť súkromie svojích príspevkov, musíš mať na vedomí, kto ťa sleduje. <strong> Tvoje súkromné príspevky sú doručené na každý server z ktorého ťa niekto následuje. </strong> Takže možno by si ich chcel/a skontrolovať, a odstrániť tých následovníkov, čo sú na serveroch ktorím nedôveruješ, že ich moderátori, alebo úpravbuy kódu budú tiež rešpektovať tvoje súkromie. followers_count: Počet následovateľov - lock_link: Zamknite svoj účet - purge: Odstrániť následovateľa + lock_link: Zamkni svoj účet + purge: Odstráň sledovateľa success: few: Počas utišovania sledovateľov z %{count} domén... one: Počas utišovania sledovateľov z jednej domény... @@ -630,7 +630,7 @@ sk: resources: Podklady generic: changes_saved_msg: Zmeny boli úspešne uložené! - copy: Kopírovať + copy: Kopíruj save_changes: Ulož zmeny validation_errors: few: Niečo ešte stále nieje v poriadku! Prosím skontroluj všetky %{count} chyby @@ -741,8 +741,8 @@ sk: web: Web remote_follow: acct: Napíš svoju prezývku@doménu z ktorej chceš následovať - missing_resource: Nemôžeme nájsť potrebnú presmerovaciu adresu k tvojmu účtu - no_account_html: Nemáš ešte účet? Môžeš sa <a href='%{sign_up_path}' target='_blank'>zaregistrovať tu</a> + missing_resource: Nemožno nájsť potrebnú presmerovaciu adresu k tvojmu účtu + no_account_html: Nemáš účet? Môžeš sa <a href='%{sign_up_path}' target='_blank'>zaregistrovať tu</a> proceed: Začni následovať prompt: 'Budeš sledovať:' reason_html: "<strong>Načo je tento krok potrebný?</strong> <code>%{instance}</code> nemusí byť práve tým serverom na ktorom si zaregistrovaný/á, takže je ťa najprv potrebné presmerovať na tvoj domáci server." @@ -814,8 +814,8 @@ sk: export: Exportovať dáta followers: Povolení následovatelia import: Importovať - migrate: Presunúť účet - notifications: Oboznámenia + migrate: Presunutie účtu + notifications: Oznámenia preferences: Voľby settings: Nastavenia two_factor_authentication: Dvoj-faktorové overenie @@ -878,7 +878,15 @@ sk: <hr class="spacer" /> - <h3 id="use"> + <h3 id="use">Načo sú tvoje údaje používané?</h3> + + <p>Hociktorá z informácií, ktoré sú o tebe zozbierané, môže byť použité následujúcimi spôsobmi:</p> + + <ul> + <li>Pre zabezpečenie základného fungovania Mastodonu. You can only interact with other people's content and post your own content when you are logged in. For example, you may follow other people to view their combined posts in your own personalized home timeline.</li> + <li>Pre lepšie moderovanie komunity sa napríklad môže tvoja IP adresa porovnať s ostatnými už známimi adresami, aby bolo možné zistiť, či nedochádza napríklad k obchádzaniu vylúčenia, aleb k iným porušeniam pravidiel.</li> + <li>Emailová adresa ktorú poskytneš môže byť použitá na zasielanie informácií, oboznámení keď ostatní užívatelia interaktujú s tvojím obsahom, alebo na posielanie správ, odpovedí na otázky a iné požiadavky.</li> + </ul> title: Podmienky užívania, a pravidlá súkromia pre %{instance} themes: default: Mastodon @@ -929,7 +937,7 @@ sk: edit_profile_step: Profil si môžeš prispôsobiť nahratím portrétu a hlavičky, môžeš upraviť svoje meno a viac. Pokiaľ chceš preverovať nových následovateľov predtým než ťa budú môcť sledovať, môžeš uzamknúť svoj účet. explanation: Tu nájdeš nejaké tipy do začiatku final_action: Začni prispievať - final_step: 'Začnite písať! Aj bez následovníkov budú vaše verejné správy videné ostatnými, napríklad na lokálnej osi a pod haštagmi. Môžete sa ostatným predstaviť pod haštagom #introductions.' + final_step: 'Začni písať! Aj bez následovateľov budú tvoje verejné príspevky videné ostatnými, napríklad na miestnej osi a pod haštagmi. Ak chceš, môžeš sa ostatným predstaviť pod haštagom #introductions.' full_handle: Adresa tvojho profilu v celom formáte full_handle_hint: Toto je čo musíš dať vedieť svojím priateľom aby ti mohli posielať správy, alebo ťa následovať z inej instancie. review_preferences_action: Zmeniť nastavenia diff --git a/config/locales/sl.yml b/config/locales/sl.yml index 594c58acc..35cba927b 100644 --- a/config/locales/sl.yml +++ b/config/locales/sl.yml @@ -54,6 +54,7 @@ sl: two: Sledilci following: Sledim joined: Se je pridružil na %{date} + last_active: zadnji aktivni link_verified_on: Lastništvo te povezave je bilo preverjeno na %{date} media: Medij moved_html: "%{name} se je prestavil na %{new_profile_link}:" @@ -77,8 +78,11 @@ sl: moderator: Mod unfollow: Prenehaj slediti admin: + account_actions: + action: Izvedi dejanje + title: Izvedi moderirano dejanje %{acct} account_moderation_notes: - create: Pusti sporočilo + create: Pusti opombo created_msg: Uspešno ustvarjena opomba moderiranja! delete: Izbriši destroyed_msg: Moderirana opomba je uspešno uničena! @@ -96,6 +100,7 @@ sl: confirm: Potrdi confirmed: Potrjeno confirming: Potrjujem + deleted: Izbrisano demote: Ponižaj disable: Onemogoči disable_two_factor_authentication: Onemogoči 2FA @@ -111,8 +116,11 @@ sl: followers: Sledilci followers_url: URL sledilci follows: Sledi + header: Glava inbox_url: URl v mapi "Prejeto" + invited_by: Povabljen od ip: IP + joined: Pridružil location: all: Vse local: Lokalno @@ -122,6 +130,7 @@ sl: media_attachments: Medijske priloge memorialize: Spremenite v spomin moderation: + active: Dejaven all: Vse silenced: Utišan suspended: Suspendiran @@ -129,7 +138,265 @@ sl: moderation_notes: Opombe moderiranja most_recent_activity: Zadnja aktivnost most_recent_ip: Zadnji IP + no_limits_imposed: Brez omejitev + not_subscribed: Ni naročeno + outbox_url: URl za pošiljanje + perform_full_suspension: Začasno ustavi + profile_url: URL profila promote: Spodbujanje + protocol: Protokol + public: Javen + push_subscription_expires: Naročnina PuSH preteče + redownload: Osveži profil + remove_avatar: Odstrani podobo + remove_header: Odstrani glavo + resend_confirmation: + already_confirmed: Ta uporabnik je že potrjen + send: Ponovno pošlji potrditveno e-pošto + success: Potrditvena e-pošta je uspešno poslana! + reset: Ponastavi + reset_password: Ponastavi geslo + resubscribe: Ponovno se naroči + role: Dovoljenja + roles: + admin: Skrbnik + moderator: Moderator + staff: Osebje + user: Uporabnik + salmon_url: Salmon URL + search: Poišči + shared_inbox_url: URL mape "Prejeto v skupni rabi" + show: + created_reports: Narejene prijave + targeted_reports: Prijavili drugi + silence: Utišaj + silenced: Utišan + statuses: Stanja + subscribe: Naroči + suspended: Suspendiran + title: Računi + unconfirmed_email: Nepotrjena e-pošta + undo_silenced: Razveljavi utišanje + undo_suspension: Razveljavi suspendiranje + unsubscribe: Odjavi se od naročnine + username: Uporabniško ime + warn: Opozori + web: Splet + action_logs: + actions: + assigned_to_self_report: "%{name} je prijavil %{target} sebi" + change_email_user: "%{name} je spremenil naslov e-pošte uporabnika %{target}" + confirm_user: "%{name} je potrdil naslov e-pošte uporabnika %{target}" + create_account_warning: "%{name} je poslal opozorilo %{target}" + create_custom_emoji: "%{name} je poslal nove emotikone %{target}" + create_domain_block: "%{name} je blokiral domeno %{target}" + create_email_domain_block: "%{name} je dal na črni seznam e-pošto domene %{target}" + demote_user: "%{name} je degradiral uporabnika %{target}" + destroy_custom_emoji: "%{name} je uničil emotikon %{target}" + destroy_domain_block: "%{name} je odblokiral domeno %{target}" + destroy_email_domain_block: "%{name} je dal na beli seznam e-pošto domene %{target}" + destroy_status: "%{name} je odstranil stanje od %{target}" + disable_2fa_user: "%{name} je onemogočil dvofaktorsko zahtevo za uporabnika %{target}" + disable_custom_emoji: "%{name} je onemogočil emotikon %{target}" + disable_user: "%{name} je onemogočil prijavo za uporabnika %{target}" + enable_custom_emoji: "%{name} je omogočil emotikon %{target}" + enable_user: "%{name} je omogočil prijavo za uporabnika %{target}" + memorialize_account: "%{name} je spremenil račun od %{target} v stran spominov" + promote_user: "%{name} je spodbudil uporabnika %{target}" + remove_avatar_user: "%{name} je odstranil podobo od %{target}" + reopen_report: "%{name} je ponovno odprl prijavo %{target}" + reset_password_user: "%{name} je ponastavil geslo od uporabnika %{target}" + resolve_report: "%{name} je razrešil prijavo %{target}" + silence_account: "%{name} je utišal račun od %{target}" + suspend_account: "%{name} je začasno ustavil račun od %{target}" + unassigned_report: "%{name} je nedodeljeno prijavil %{target}" + unsilence_account: "%{name} je preklical utišanje računa od %{target}" + unsuspend_account: "%{name} je aktiviral račun od %{target}" + update_custom_emoji: "%{name} je posodobil emotikone %{target}" + update_status: "%{name} je posodobil stanje od %{target}" + deleted_status: "(izbrisano stanje)" + title: Dnevnik revizije + custom_emojis: + by_domain: Domena + copied_msg: Lokalna kopija emotikona je bila uspešno ustvarjena + copy: Kopiraj + copy_failed_msg: Lokalne kopije emotikona ni bilo mogoče ustvariti + created_msg: Emotikon je uspešno ustvarjen! + delete: Izbriši + destroyed_msg: Emotikon je uspešno uničen! + disable: Onemogoči + disabled_msg: Ta emotikon je uspešno onemogočen + emoji: Emotikon + enable: Omogoči + enabled_msg: Ta emotikon je uspešno omogočen + image_hint: PNG do 50KB + listed: Navedeno + new: + title: Dodaj nove emotikone + overwrite: Prepiši + shortcode: Kratka koda + shortcode_hint: Najmanj 2 znaka, samo alfanumerični znaki in podčrtaji + title: Emotikoni po meri + unlisted: Neuvrščeni + update_failed_msg: Tega emotikona ni bilo mogoče posodobiti + updated_msg: Emotikon je uspešno posodobljen! + upload: Pošlji + dashboard: + backlog: Zaostala opravila + config: Nastavitve + feature_deletions: Brisanje računov + feature_invites: Poveza povabil + feature_profile_directory: Mapa profila + feature_registrations: Registracije + feature_relay: Rele federacije + features: Zmožnosti + hidden_service: Federacija s skritimi storitvami + open_reports: odprte prijave + recent_users: Nedavni uporabniki + search: Iskanje po celotnem besedilu + single_user_mode: Način enega uporabnika + software: Programska oprema + space: Uporaba prostora + title: Nadzorna plošča + total_users: Skupaj uporabnikov + trends: Trendi + week_interactions: interakcije ta teden + week_users_active: aktivni ta teden + week_users_new: uporabniki ta teden + domain_blocks: + add_new: Dodaj nov domenski blok + created_msg: Domenski blok se sedaj obdeluje + destroyed_msg: Domenski blok je bil razveljavljen + domain: Domena + new: + create: Ustvari blok + hint: Domenski blok ne bo preprečil ustvarjanja vnosov računov v zbirko podatkov, ampak bo retroaktivno in samodejno uporabil posebne metode moderiranja na teh računih. + severity: + desc_html: "<strong>Utišaj</strong> bo vse objave računa naredil nevidne vsem, ki jih ne sledijo. <strong>Suspendiraj</strong> bo odstranil vso vsebino, medije in podatke profila računa. Uporabi <strong>nič</strong>, če želite le zavrniti predstavnostne datoteke." + noop: Nič + silence: Utišaj + suspend: Suspendiraj + title: Nov domenski blok + reject_media: Zavrni predstavnostne datoteke + reject_media_hint: Odstrani lokalno shranjene predstavnostne datoteke in zavrača prenašanje le-teh v prihodnosti. Za suspenzije ni pomembno + reject_reports: Zavrnjene prijave + reject_reports_hint: Prezri vse prijave, ki pridejo iz te domene. Za suspenzije ni pomembno + rejecting_media: zavrnitev predstavnostnih datotek + rejecting_reports: zavrnitev prijav + severity: + silence: utišani + suspend: suspendirani + show: + affected_accounts: + few: "%{count} računov v bazi podatkov so prizadeti" + one: En račun v bazi podatkov je prizadet + other: "%{count} računov v bazi podatkov so prizadeti" + two: "%{count} računov v bazi podatkov so prizadeti" + retroactive: + silence: Prekliči utišanje za vse obstoječe račune iz te domene + suspend: Odsuspendiraj vse obstoječe račune iz te domene + title: Razveljavi domenski blok za %{domain} + undo: Razveljavi + undo: Razveljavi domenski blok + email_domain_blocks: + add_new: Dodaj novo + created_msg: Domena e-pošte je bila uspešno dodana na črni seznam + delete: Izbriši + destroyed_msg: Domena e-pošte je bila uspešno izbrisana iz črnega seznama + domain: Domena + new: + create: Dodaj domeno + title: Nov vnos e-pošte na črni seznam + title: Črni seznam e-pošte + followers: + back_to_account: Nazaj na račun + title: Sledilci od %{acct} + instances: + delivery_available: Na voljo je dostava + known_accounts: + few: "%{count} znanih računov" + one: "%{count} znan račun" + other: "%{count} znanih računov" + two: "%{count} znanih računov" + moderation: + all: Vse + limited: Omejeno + title: Moderiranje + title: Federacija + total_blocked_by_us: Blokirano iz naše strani + total_followed_by_them: Oni ti sledijo + total_followed_by_us: Mi ti sledimo + total_reported: Poročila o njih + total_storage: Predstavnostne priloge + invites: + deactivate_all: Onemogoči vse + filter: + all: Vse + available: Razpoložljivo + expired: Potekel + title: Filter + title: Povabila + relays: + add_new: Dodaj nov rele + delete: Izbriši + description_html: "<strong>Rele federacije</strong> je posredniški strežnik, ki si izmenjuje velike količine javnih trobov med strežniki, ki so se naročili in objavili na njem. <strong>Majhnim in srednjim strežnikom lahko pomaga pri odkrivanju vsebine iz sistema fediverse</strong>, kar bi sicer zahtevalo, da lokalni uporabniki ročno sledijo druge osebe na oddaljenih strežnikih." + disable: Onemogoči + disabled: Onemogočeno + enable: Omogoči + enable_hint: Ko je omogočen, se bo vaš strežnik naročil na vse javne trobe iz tega releja in začel pošiljati javne trobe tega strežnika. + enabled: Omogočeno + inbox_url: URL releja + pending: Čakanje na odobritev releja + save_and_enable: Shrani in omogoči + setup: Nastavi povezavo releja + status: Stanje + title: Releji + report_notes: + created_msg: Opomba o prijavi je uspešno ustvarjena! + destroyed_msg: Opomba o prijavi je uspešno izbrisana! + reports: + account: + note: opomba + report: prijava + action_taken_by: Dejanje, ki ga je sprejel + are_you_sure: Ali ste prepričani? + assign_to_self: Dodeli meni + assigned: Dodeljen moderator + comment: + none: Nič + created_at: Prijavljeno + mark_as_resolved: Označi kot rešeno + mark_as_unresolved: Označi kot nerešeno + notes: + create: Dodaj opombo + create_and_resolve: Razreši z opombo + create_and_unresolve: Ponovo odpri z opombo + delete: Izbriši + placeholder: Opišite dejanja, ki ste jih izvedli, ali katere koli druge posodobitve... + reopen: Ponovno odpri prijavo + report: 'Prijavi #%{id}' + reported_account: Prijavljeni račun + reported_by: Prijavljen od + resolved: Razrešeno + resolved_msg: Prijava je uspešno razrešena! + status: Stanje + title: Prijave + unassign: Odstopi + unresolved: Nerešeno + updated_at: Posodobljen + settings: + activity_api_enabled: + desc_html: Številke lokalno objavljenih stanj, aktivnih uporabnikov in novih registracij na tedenskih seznamih + title: Objavi združeno statistiko o dejavnosti uporabnikov + bootstrap_timeline_accounts: + desc_html: Več uporabniških imen ločite z vejico. Deluje samo na lokalnih in odklenjenih računih. Privzeto, ko je prazno, pri vseh lokalnih skrbnikih. + title: Privzeta sledenja za nove uporabnike + contact_information: + email: Poslovna e-pošta + username: Uporabniško ime stika + custom_css: + desc_html: Spremeni videz z naloženim CSS na vsaki strani + title: CSS po meri statuses: pin_errors: ownership: Trob nekoga drugega ne more biti pripet diff --git a/config/locales/sq.yml b/config/locales/sq.yml new file mode 100644 index 000000000..e583b9309 --- /dev/null +++ b/config/locales/sq.yml @@ -0,0 +1,612 @@ +--- +sq: + about: + about_this: Mbi + administered_by: 'Administruar nga:' + api: API + apps: Aplikacione për celular + contact: Kontakt + contact_missing: I parregulluar + contact_unavailable: N/A + documentation: Dokumentim + features: + humane_approach_title: Një trajtim më njerëzor + not_a_product_title: Jeni një person, jo një produkt + real_conversation_title: Ndërtuar për bashkëbisedim të njëmendtë + within_reach_title: Përherë i kapshëm + generic_description: "%{domain} është një shërbyes te rrjeti" + hosted_on: Mastodon i strehuar në %{domain} + learn_more: Mësoni më tepër + other_instances: Listë instancash + privacy_policy: Rregulla privatësie + source_code: Kod burim + status_count_after: + one: gjendje + other: gjendje + status_count_before: Cili krijoi + terms: Kushte shërbimi + user_count_after: + one: përdorues + other: përdorues + what_is_mastodon: Ç’është Mastodon-i? + accounts: + choices_html: 'Zgjedhje të %{name}:' + follow: Ndiqeni + followers: + one: Ndjekës + other: Ndjekës + following: Ndjekje + joined: U bë pjesë më %{date} + last_active: aktiv së fundi + media: Media + nothing_here: S’ka gjë këtu! + people_followed_by: Persona të ndjekur nga %{name} + people_who_follow: Persona që ndjekin %{name} + reserved_username: Emri i përdoruesit është i ruajtur për dikë + roles: + admin: Përgjegjës + bot: Bot + moderator: Mod + unfollow: Resht së ndjekuri + admin: + account_actions: + action: Kryeje veprimin + account_moderation_notes: + create: Lini një shënim + created_msg: Shënimi i moderimit u krijua me sukses! + delete: Fshije + destroyed_msg: Shënimi i moderimit u asgjësua me sukses! + accounts: + are_you_sure: A jeni i sigurt? + avatar: Avatar + by_domain: Përkatësi + change_email: + changed_msg: Email-i i llogarisë u ndryshua me sukses! + current_email: Email-i i tanishëm + label: Ndrysho email-in + new_email: Email i ri + submit: Ndrysho email-in + title: Ndrysho email-in për %{username} + confirm: Ripohojeni + confirmed: U ripohua + confirming: Po ripohohet + deleted: U fshi + demote: Zhgradoje + disable: Çaktivizoje + disable_two_factor_authentication: Çaktivizoni 2FA + disabled: E çaktivizuar + display_name: Emër në ekran + domain: Përkatësi + edit: Përpunojeni + email: Email + email_status: Gjendje email-i + enable: Aktivizoje + enabled: E aktivizuar + feed_url: URL prurjeje + followers: Ndjekës + followers_url: URL Ndjekësish + follows: Ndjekje + header: Krye + inbox_url: URL Mesazhesh të Marrë + invited_by: Ftuar nga + ip: IP + joined: U bë pjesë + location: + all: Krejt + local: Vendore + remote: E largët + title: Vendndodhje + login_status: Gjendje hyrjeje + media_attachments: Bashkëngjitje media + moderation: + active: Aktiv + all: Krejt + suspended: Të pezulluara + title: Moderim + moderation_notes: Shënime moderimesh + most_recent_activity: Veprimtaria më e freskët + most_recent_ip: IP-ja më e freskët + no_limits_imposed: Pa imponim kufijsh + not_subscribed: Jo i pajtuar + outbox_url: URL Mesazhesh të Dërguar + perform_full_suspension: Pezulloje + profile_url: URL profili + protocol: Protokoll + public: Publike + push_subscription_expires: Pajtimi PuSH skadon më + redownload: Rifresko profilin + remove_avatar: Hiqe avatarin + remove_header: Hiqe kryen + resend_confirmation: + already_confirmed: Ky përdorues është i ripohuar tashmë + send: Ridërgo email ripohimi + success: Email-i i ripohimit u dërgua me sukses! + reset_password: Ricaktoni fjalëkalimin + resubscribe: Ripajtohuni + role: Leje + roles: + admin: Përgjegjës + moderator: Moderator + staff: Staf + user: Përdorues + search: Kërkoni + show: + created_reports: Ka bërë raporte + targeted_reports: Raportuar nga të tjerë + silence: Heshtje + statuses: Gjendje + subscribe: Pajtomë + suspended: Të pezulluara + title: Llogari + unconfirmed_email: Email i paripohuar + undo_silenced: Zhbëje heshtjen + undo_suspension: Zhbëje pezullimin + unsubscribe: Shpajtohuni + username: Emër përdoruesi + warn: Sinjalizoje + web: Web + action_logs: + actions: + change_email_user: "%{name} ndryshoi adresën email të përdoruesit %{target}" + confirm_user: "%{name} ripohoi adresën email të përdoruesit %{target}" + create_account_warning: "%{name} dërgoi një sinjalizim për %{target}" + create_custom_emoji: "%{name} ngarkoi emotikon të ri %{target}" + create_domain_block: "%{name} bllokoi përkatësinë %{target}" + create_email_domain_block: "%{name} e shtoi në listë bllokimesh përkatësinë %{target}" + demote_user: "%{name} zhgradoi përdoruesin %{target}" + destroy_custom_emoji: "%{name} asgjësoi emotikonin %{target}" + destroy_domain_block: "%{name} zhbllokoi përkatësinë %{target}" + disable_2fa_user: "%{name} çaktivizoi domosdoshmëritë për dyfaktorësh për përdoruesin %{target}" + disable_custom_emoji: "%{name} çaktivizoi emotikonin %{target}" + disable_user: "%{name} çaktivizoi hyrje për përdoruesin %{target}" + enable_custom_emoji: "%{name} aktivizoi emotikonin %{target}" + enable_user: "%{name} aktivizoi hyrje për përdoruesin %{target}" + promote_user: "%{name} gradoi përdoruesin %{target}" + remove_avatar_user: "%{name} hoqi avatarin e %{target}" + reopen_report: "%{name} rihapi raportin %{target}" + reset_password_user: "%{name} ricaktoi fjalëkalimi për përdoruesin %{target}" + silence_account: "%{name} heshtoi llogarinë e %{target}" + suspend_account: "%{name} pezulloi llogarinë e %{target}" + unsilence_account: "%{name} hoqi heshtimin për llogarinë %{target}" + unsuspend_account: "%{name} hoqi pezullimin për llogarinë e %{target}" + update_custom_emoji: "%{name} përditësoi emotikonin %{target}" + custom_emojis: + by_domain: Përkatësi + copy: Kopjoje + created_msg: Emotikoni u krijua me sukses! + delete: Fshije + destroyed_msg: Emotikoni u asgjësua me sukses! + disable: Çaktivizoje + disabled_msg: Ai emotikon u çaktivizua me sukses + emoji: Emotikon + enable: Aktivizoje + enabled_msg: Ai emotikon u aktivizua me sukses + image_hint: PNG deri 50KB + new: + title: Shtoni emotikon të ri vetjak + overwrite: Mbishkruaje + shortcode: Kod i shkurtër + title: Emotikone vetjake + update_failed_msg: S’u përditësua dot ai emotikon + updated_msg: Emotikoni u përditësua me sukses! + upload: Ngarkoje + dashboard: + config: Formësim + feature_invites: Lidhje ftesash + feature_profile_directory: Drejtori profilesh + feature_registrations: Regjistrime + feature_relay: Rele federimi + features: Veçori + search: Kërko tekstin e plotë + single_user_mode: Mënyrë me përdorues të vetëm + software: Software + space: Përdorim hapësire + title: Pult + total_users: përdorues gjithsej + trends: Tendenca + week_interactions: Ndërveprime këtë javë + week_users_active: aktivë këtë javë + week_users_new: përdorues këtë javë + domain_blocks: + add_new: Shtoni bllokim të ri përkatësie + created_msg: Bllokimi i përkatësisë tani po përpunohet + destroyed_msg: Bllokimi i përkatësisë u hoq + domain: Përkatësi + new: + create: Krijoni bllokim + severity: + noop: Asnjë + silence: Heshtje + suspend: Pezulloje + title: Shtoni Bllokim i ri përkatësie + show: + affected_accounts: Pat ndikim te një llogari në bazën e të dhënave + undo: Zhbëje + undo: Zhbëje bllokimin e përkatësisë + email_domain_blocks: + add_new: Shtoni të ri + delete: Fshije + domain: Përkatësi + new: + create: Shtoni përkatësi + followers: + back_to_account: Mbrapsht Te Llogaria + title: Ndjekës të %{acct} + instances: + moderation: + all: Krejt + limited: E kufizuar + title: Moderim + title: Federim + total_blocked_by_us: Bllokuar nga ne + total_followed_by_them: Ndjekur prej tyre + total_followed_by_us: Ndjekur nga ne + total_reported: Raportime rreth tyre + total_storage: Bashkëngjitje media + invites: + deactivate_all: Çaktivizoji krejt + filter: + all: Krejt + expired: Ka skaduar + title: Filtër + title: Ftesa + relays: + add_new: Shtoni rele të re + delete: Fshije + disable: Çaktivizoje + disabled: E çaktivizuar + enable: Aktivizoje + enabled: E aktivizuar + inbox_url: URL releje + pending: Në pritje të miratimit të relesë + save_and_enable: Ruaje dhe aktivizoje + setup: Ujdisni një lidhje rele + status: Gjendje + title: Rele + report_notes: + created_msg: Shënimi i raportimit u krijua me sukses! + destroyed_msg: Shënimi i raportimit u fshi me sukses! + reports: + account: + note: shënim + report: Raportoje + action_taken_by: 'Veprimi i ndërmarrë nga ' + are_you_sure: A jeni i sigurt? + assign_to_self: Caktojani vetes + comment: + none: Asnjë + created_at: Raportuar më + mark_as_resolved: Vëri shenjë si i zgjidhur + mark_as_unresolved: Vëri shenjë si të pazgjidhur + notes: + create: Shtoni shënim + create_and_resolve: Zgjidhe me shënim + create_and_unresolve: Rihape me shënim + delete: Fshije + reopen: Rihape raportimin + report: 'Raportim #%{id}' + reported_account: Llogari e raportuar + reported_by: Raportuar nga + status: Gjendje + title: Raportime + unresolved: Të pazgjidhur + updated_at: U përditësua më + settings: + custom_css: + title: CSS Vetjake + mascot: + title: Figurë simboli + peers_api_enabled: + title: Boto listë instancash të zbuluara + profile_directory: + desc_html: Lejoju përdoruesve të jenë të zbulueshëm + title: Aktivizo drejtori profilesh + registrations: + open: + title: Hap regjistrim + show_staff_badge: + desc_html: Shfaq një stemë stafi në faqen e një përdoruesi + title: Shfaq stemë stafi + site_description: + title: Përshkrim instance + site_terms: + title: Kushte vetjake shërbimi + site_title: Emër instance + thumbnail: + title: Miniaturë instance + timeline_preview: + title: Paraparje rrjedhe kohore + title: Rregullime sajti + statuses: + back_to_account: Mbrapsht te faqja e llogarisë + batch: + delete: Fshije + nsfw_off: Vëri shenjë si jo rezervat + nsfw_on: Vëri shenjë si rezervat + failed_to_execute: S’u arrit të përmbushej + media: + title: Media + no_media: S’ka media + no_status_selected: S’u ndryshua ndonjë gjendje, ngaqë s’u përzgjodh ndonjë e tillë + title: Gjendje llogarish + with_media: Me media + subscriptions: + callback_url: URL Callback-u + expires_in: Skadon më + topic: Temë + tags: + accounts: Llogari + hidden: Fshehur + hide: Fshihe prej drejtorie + name: Hashtag + title: Hashtage + unhide: Shfaqe në drejtori + visible: E dukshme + title: Administrim + warning_presets: + add_new: Shtoni të ri + delete: Fshije + edit: Përpunoni + admin_mailer: + new_report: + body: "%{reporter} ka raportuar %{target}" + subject: Raport i ri për %{instance} (#%{id}) + application_mailer: + notification_preferences: Ndryshoni parapëlqime email-i + salutation: "%{name}," + settings: 'Ndryshoni parapëlqime email-i: %{link}' + view: 'Parje:' + view_profile: Shihni Profilin + view_status: Shihini gjendjen + applications: + created: Aplikacioni u krijua me sukses + destroyed: Aplikacioni u fshi me sukses + invalid_url: URL-ja e dhënë është e pavlefshme + regenerate_token: Riprodho token hyrjesh + your_token: Token-i juaj për hyrje + auth: + change_password: Fjalëkalim + confirm_email: Ripohoni email-in + delete_account: Fshije llogarinë + didnt_get_confirmation: S’morët udhëzime ripohimi? + forgot_password: Harruat fjalëkalimin tuaj? + login: Hyni + logout: Dalje + migrate_account: Kaloni në një tjetër llogari + migrate_account_html: Nëse doni ta ridrejtoni këtë llogari te një tjetër, këtë mund <a href="%{path}">ta formësoni këtu</a>. + or: ose + or_log_in_with: Ose bëni hyrjen me + providers: + cas: CAS + register: Regjistrohuni + register_elsewhere: Regjistrohuni në një tjetër shërbyes + resend_confirmation: Ridërgo udhëzime ripohimi + reset_password: Ricaktoni fjalëkalimin + security: Siguri + set_new_password: Caktoni fjalëkalim të ri + authorize_follow: + already_following: E ndiqni tashmë këtë llogari + follow: Ndiqeni + follow_request: 'Keni dërguar një kërkesë ndjekjeje te:' + following: 'Sukses! Tani e ndiqni:' + post_follow: + return: Shfaq profilin e përdoruesit + title: Ndiq %{acct} + datetime: + distance_in_words: + about_x_hours: "%{count}o" + about_x_months: "%{count}mj" + about_x_years: "%{count}v" + almost_x_years: "%{count}v" + half_a_minute: Mu tani + less_than_x_minutes: "%{count}m" + less_than_x_seconds: Mu tani + over_x_years: "%{count}v" + x_days: "%{count}d" + x_minutes: "%{count}m" + x_months: "%{count}mj" + x_seconds: "%{count}s" + deletes: + proceed: Fshini llogarinë + directories: + directory: Drejtori profilesh + explore_mastodon: Eksploroni %{title} + people: + one: "%{count} person" + other: "%{count} persona" + errors: + '403': S’keni leje të shihni këtë faqe. + '404': Faqja që po kërkonit, nuk ekziston. + '410': Faqja që po kërkonit, nuk ekziston më. + '422': + title: Verifikimi i sigurisë dështoi + '500': + title: Kjo faqe s’është e saktë + exports: + archive_takeout: + date: Datë + size: Madhësi + csv: CSV + lists: Lista + storage: Depozitim për media + filters: + contexts: + notifications: Njoftime + thread: Biseda + edit: + title: Përpunoni filtër + index: + delete: Fshije + title: Filtra + new: + title: Shtoni filtër të ri + followers: + domain: Përkatësi + followers_count: Numër ndjekësish + purge: Hiqe nga ndjekësit + unlocked_warning_title: Llogaria juaj s’është kyçur + footer: + developers: Zhvillues + more: Më tepër… + resources: Burime + generic: + changes_saved_msg: Ndryshimet u ruajtën me sukses! + copy: Kopjoje + save_changes: Ruaji ndryshimet + imports: + types: + blocking: Listë bllokimesh + invites: + expires_in: + '1800': 30 minuta + '21600': 6 orë + '3600': 1 orë + '43200': 12 orë + '604800': 1 javë + '86400': 1 ditë + expires_in_prompt: Kurrë + generate: Prodhoje + invited_by: 'Qetë ftuar nga:' + max_uses: + one: 1 përdorim + other: "%{count} përdorime" + max_uses_prompt: Pa kufi + table: + expires_at: Skadon më + uses: Përdorime + title: Ftoni njerëz + migrations: + proceed: Ruaje + moderation: + title: Moderim + notification_mailer: + digest: + action: Shihini krejt njoftimet + favourite: + title: E parapëlqyer e re + follow: + title: Ndjekës i ri + mention: + body: 'U përmendët nga %{name} në:' + subject: U përmendët nga %{name} + title: Përmendje e re + number: + human: + decimal_units: + format: "%n%u" + units: + billion: B + million: M + quadrillion: K + thousand: K + trillion: T + pagination: + truncate: "…" + preferences: + languages: Gjuhë + other: Tjetër + publishing: Publikim + web: Web + remote_unfollow: + error: Gabim + title: Titull + sessions: + activity: Veprimtaria e fundit + browser: Shfletues + browsers: + blackberry: Blackberry + chrome: Chrome + edge: Microsoft Edge + electron: Electron + firefox: Firefox + generic: Shfletues i panjohur + ie: Internet Explorer + micro_messenger: MicroMessenger + nokia: Shfletues Nokia S40 Ovi + opera: Opera + otter: Otter + phantom_js: PhantomJS + qq: QQ Browser + safari: Safari + uc_browser: UCBrowser + weibo: Weibo + current_session: Sesioni i tanishëm + description: "%{browser} në %{platform}" + ip: IP + platforms: + adobe_air: Adobe Air + android: Android + blackberry: Blackberry + chrome_os: ChromeOS + firefox_os: Firefox OS + ios: iOS + linux: Linux + mac: Mac + other: platformë e panjohur + windows: Windows + windows_mobile: Windows Mobile + windows_phone: Windows Phone + revoke: Shfuqizoje + title: Sesione + settings: + authorized_apps: Aplikacione të autorizuara + back: Mbrapsht te Mastodon + delete: Fshirje llogarie + development: Zhvillim + edit_profile: Përpunoni profilin + export: Eksportim të dhënash + followers: Ndjekës të autorizuar + import: Importo + migrate: Migrim llogarie + notifications: Njoftime + preferences: Parapëlqime + settings: Rregullime + your_apps: Aplikacionet tuaja + statuses: + attached: + image: + one: "%{count} figurë" + other: "%{count} figura" + video: + one: "%{count} video" + other: "%{count} video" + content_warning: 'Sinjalizim lënde: %{warning}' + show_more: Shfaq më tepër + title: '%{name}: "%{quote}"' + visibilities: + private: Vetëm ndjekësve + private_long: Shfaqua vetëm ndjekësve + public: Publike + public_long: Mund ta shohë kushdo + themes: + contrast: Me kontrast të lartë + default: Mastodon + mastodon-light: Mastodon (light) + time: + formats: + default: "%d %b, %Y, %H:%M" + month: "%b %Y" + two_factor_authentication: + disable: Çaktivizoje + enable: Aktivizoje + enabled_success: Mirëfilltësimi dyfaktorësh u aktivizua me sukses + user_mailer: + backup_ready: + subject: Arkivi juaj është gati për shkarkim + warning: + subject: + disable: Llogaria juaj %{acct} është ngrirë + none: Sinjalizim për %{acct} + silence: Llogaria juaj %{acct} është kufizuar + welcome: + explanation: Ja disa ndihmëza, sa për t’ia filluar + final_action: Filloni të postoni + subject: Mirë se vini te Mastodon-i + tips: Ndihmëza + title: Mirë se vini, %{name}! + users: + invalid_email: Adresa email është e pavlefshme + invalid_otp_token: Kod dyfaktorësh i pavlefshëm + signed_in_as: 'I futur si:' + verification: + verification: Verifikim diff --git a/config/locales/sr.yml b/config/locales/sr.yml index 1d373ed67..331ec3f80 100644 --- a/config/locales/sr.yml +++ b/config/locales/sr.yml @@ -34,14 +34,14 @@ sr: few: статуси many: статуси one: статус - other: статуси + other: статуса status_count_before: Који су написали terms: Услови коришћења user_count_after: few: корисници many: корисници one: корисник - other: корисници + other: корисника user_count_before: Дом за what_is_mastodon: Шта је Мастодон? accounts: @@ -54,7 +54,7 @@ sr: other: Пратиоци following: Пратим joined: Придружио/ла се %{date} - last_active: последњи активни + last_active: последњи пут активни link_verified_on: Власништво над овом везом је проверено %{date} media: Медији moved_html: "%{name} је прешао на %{new_profile_link}:" @@ -570,8 +570,8 @@ sr: people: few: "%{count} људе" many: "%{count} људе" - one: "%{count} особу" - other: "%{count} људе" + one: "%{count} особа/е" + other: "%{count} људи" errors: '403': Немате дозвола да видите ову страну. '404': Страна коју сте тражили не постоји. diff --git a/config/navigation.rb b/config/navigation.rb index 5b0b5c343..f74c98ab2 100644 --- a/config/navigation.rb +++ b/config/navigation.rb @@ -6,6 +6,7 @@ SimpleNavigation::Configuration.run do |navigation| primary.item :settings, safe_join([fa_icon('cog fw'), t('settings.settings')]), settings_profile_url do |settings| settings.item :profile, safe_join([fa_icon('user fw'), t('settings.edit_profile')]), settings_profile_url, highlights_on: %r{/settings/profile|/settings/migration} + settings.item :featured_tags, safe_join([fa_icon('hashtag fw'), t('settings.featured_tags')]), settings_featured_tags_url settings.item :preferences, safe_join([fa_icon('sliders fw'), t('settings.preferences')]), settings_preferences_url settings.item :notifications, safe_join([fa_icon('bell fw'), t('settings.notifications')]), settings_notifications_url settings.item :password, safe_join([fa_icon('lock fw'), t('auth.security')]), edit_user_registration_url, highlights_on: %r{/auth/edit|/settings/delete} diff --git a/config/routes.rb b/config/routes.rb index 976b25812..447a22794 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -74,6 +74,7 @@ Rails.application.routes.draw do get '/@:username', to: 'accounts#show', as: :short_account get '/@:username/with_replies', to: 'accounts#show', as: :short_account_with_replies get '/@:username/media', to: 'accounts#show', as: :short_account_media + get '/@:username/tagged/:tag', to: 'accounts#show', as: :short_account_tag get '/@:account_username/:id', to: 'statuses#show', as: :short_account_status get '/@:account_username/:id/embed', to: 'statuses#embed', as: :embed_short_account_status @@ -119,6 +120,7 @@ Rails.application.routes.draw do resource :migration, only: [:show, :update] resources :sessions, only: [:destroy] + resources :featured_tags, only: [:index, :create, :destroy] end resources :media, only: [:show] do diff --git a/db/migrate/20171005102658_create_account_moderation_notes.rb b/db/migrate/20171005102658_create_account_moderation_notes.rb index d1802b5b3..974ed9940 100644 --- a/db/migrate/20171005102658_create_account_moderation_notes.rb +++ b/db/migrate/20171005102658_create_account_moderation_notes.rb @@ -7,6 +7,7 @@ class CreateAccountModerationNotes < ActiveRecord::Migration[5.1] t.timestamps end + add_foreign_key :account_moderation_notes, :accounts, column: :target_account_id end end diff --git a/db/migrate/20190201012802_add_overwrite_to_imports.rb b/db/migrate/20190201012802_add_overwrite_to_imports.rb new file mode 100644 index 000000000..89b262cc7 --- /dev/null +++ b/db/migrate/20190201012802_add_overwrite_to_imports.rb @@ -0,0 +1,17 @@ +require Rails.root.join('lib', 'mastodon', 'migration_helpers') + +class AddOverwriteToImports < ActiveRecord::Migration[5.2] + include Mastodon::MigrationHelpers + + disable_ddl_transaction! + + def up + safety_assured do + add_column_with_default :imports, :overwrite, :boolean, default: false, allow_null: false + end + end + + def down + remove_column :imports, :overwrite, :boolean + end +end diff --git a/db/migrate/20190203180359_create_featured_tags.rb b/db/migrate/20190203180359_create_featured_tags.rb new file mode 100644 index 000000000..b08410a3a --- /dev/null +++ b/db/migrate/20190203180359_create_featured_tags.rb @@ -0,0 +1,12 @@ +class CreateFeaturedTags < ActiveRecord::Migration[5.2] + def change + create_table :featured_tags do |t| + t.references :account, foreign_key: { on_delete: :cascade } + t.references :tag, foreign_key: { on_delete: :cascade } + t.bigint :statuses_count, default: 0, null: false + t.datetime :last_status_at + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index c6c94609f..05d4deb1a 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2019_01_17_114553) do +ActiveRecord::Schema.define(version: 2019_02_03_180359) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -260,6 +260,17 @@ ActiveRecord::Schema.define(version: 2019_01_17_114553) do t.index ["status_id"], name: "index_favourites_on_status_id" end + create_table "featured_tags", force: :cascade do |t| + t.bigint "account_id" + t.bigint "tag_id" + t.bigint "statuses_count", default: 0, null: false + t.datetime "last_status_at" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["account_id"], name: "index_featured_tags_on_account_id" + t.index ["tag_id"], name: "index_featured_tags_on_tag_id" + end + create_table "follow_requests", force: :cascade do |t| t.datetime "created_at", null: false t.datetime "updated_at", null: false @@ -300,6 +311,7 @@ ActiveRecord::Schema.define(version: 2019_01_17_114553) do t.integer "data_file_size" t.datetime "data_updated_at" t.bigint "account_id", null: false + t.boolean "overwrite", default: false, null: false end create_table "invites", force: :cascade do |t| @@ -721,6 +733,8 @@ ActiveRecord::Schema.define(version: 2019_01_17_114553) do add_foreign_key "custom_filters", "accounts", on_delete: :cascade add_foreign_key "favourites", "accounts", name: "fk_5eb6c2b873", on_delete: :cascade add_foreign_key "favourites", "statuses", name: "fk_b0e856845e", on_delete: :cascade + add_foreign_key "featured_tags", "accounts", on_delete: :cascade + add_foreign_key "featured_tags", "tags", on_delete: :cascade add_foreign_key "follow_requests", "accounts", column: "target_account_id", name: "fk_9291ec025d", on_delete: :cascade add_foreign_key "follow_requests", "accounts", name: "fk_76d644b0e7", on_delete: :cascade add_foreign_key "follows", "accounts", column: "target_account_id", name: "fk_745ca29eac", on_delete: :cascade diff --git a/dist/mastodon-streaming.service b/dist/mastodon-streaming.service index 5d7c129df..c324fccf4 100644 --- a/dist/mastodon-streaming.service +++ b/dist/mastodon-streaming.service @@ -9,7 +9,7 @@ WorkingDirectory=/home/mastodon/live Environment="NODE_ENV=production" Environment="PORT=4000" Environment="STREAMING_CLUSTER_NUM=1" -ExecStart=/usr/bin/npm run start +ExecStart=/usr/bin/node ./streaming TimeoutSec=15 Restart=always diff --git a/jest.config.js b/jest.config.js index dc61b9a9d..ae6c96f33 100644 --- a/jest.config.js +++ b/jest.config.js @@ -23,4 +23,8 @@ module.exports = { '!app/javascript/mastodon/test_setup.js', ], coverageDirectory: '<rootDir>/coverage', + moduleDirectories: [ + '<rootDir>/node_modules', + '<rootDir>/app/javascript', + ], }; diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb index fad5b5c47..6911fef0d 100644 --- a/lib/mastodon/version.rb +++ b/lib/mastodon/version.rb @@ -13,7 +13,7 @@ module Mastodon end def patch - 1 + 2 end def pre diff --git a/public/robots.txt b/public/robots.txt index 3c9c7c01f..d93648bee 100644 --- a/public/robots.txt +++ b/public/robots.txt @@ -1,5 +1,4 @@ # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file -# -# To ban all spiders from the entire site uncomment the next two lines: -# User-agent: * -# Disallow: / + +User-agent: * +Disallow: /media_proxy/ diff --git a/spec/fabricators/featured_tag_fabricator.rb b/spec/fabricators/featured_tag_fabricator.rb new file mode 100644 index 000000000..25cbdaac0 --- /dev/null +++ b/spec/fabricators/featured_tag_fabricator.rb @@ -0,0 +1,6 @@ +Fabricator(:featured_tag) do + account + tag + statuses_count 1_337 + last_status_at Time.now.utc +end diff --git a/spec/lib/activitypub/activity/announce_spec.rb b/spec/lib/activitypub/activity/announce_spec.rb index 54dd52a60..aa58d9e23 100644 --- a/spec/lib/activitypub/activity/announce_spec.rb +++ b/spec/lib/activitypub/activity/announce_spec.rb @@ -1,7 +1,7 @@ require 'rails_helper' RSpec.describe ActivityPub::Activity::Announce do - let(:sender) { Fabricate(:account) } + let(:sender) { Fabricate(:account, followers_url: 'http://example.com/followers', uri: 'https://example.com/actor') } let(:recipient) { Fabricate(:account) } let(:status) { Fabricate(:status, account: recipient) } @@ -10,20 +10,162 @@ RSpec.describe ActivityPub::Activity::Announce do '@context': 'https://www.w3.org/ns/activitystreams', id: 'foo', type: 'Announce', - actor: ActivityPub::TagManager.instance.uri_for(sender), - object: ActivityPub::TagManager.instance.uri_for(status), + actor: 'https://example.com/actor', + object: object_json, }.with_indifferent_access end + let(:unknown_object_json) do + { + '@context': 'https://www.w3.org/ns/activitystreams', + id: 'https://example.com/actor/hello-world', + type: 'Note', + attributedTo: 'https://example.com/actor', + content: 'Hello world', + to: 'http://example.com/followers', + } + end + + subject { described_class.new(json, sender) } + describe '#perform' do - subject { described_class.new(json, sender) } + context 'when sender is followed by a local account' do + before do + Fabricate(:account).follow!(sender) + stub_request(:get, 'https://example.com/actor/hello-world').to_return(body: Oj.dump(unknown_object_json)) + subject.perform + end + + context 'a known status' do + let(:object_json) do + ActivityPub::TagManager.instance.uri_for(status) + end + + it 'creates a reblog by sender of status' do + expect(sender.reblogged?(status)).to be true + end + end + + context 'an unknown status' do + let(:object_json) { 'https://example.com/actor/hello-world' } + + it 'creates a reblog by sender of status' do + reblog = sender.statuses.first + + expect(reblog).to_not be_nil + expect(reblog.reblog.text).to eq 'Hello world' + end + end + + context 'self-boost of a previously unknown status with missing attributedTo' do + let(:object_json) do + { + id: 'https://example.com/actor#bar', + type: 'Note', + content: 'Lorem ipsum', + to: 'http://example.com/followers', + } + end + + it 'creates a reblog by sender of status' do + expect(sender.reblogged?(sender.statuses.first)).to be true + end + end + + context 'self-boost of a previously unknown status with correct attributedTo' do + let(:object_json) do + { + id: 'https://example.com/actor#bar', + type: 'Note', + content: 'Lorem ipsum', + attributedTo: 'https://example.com/actor', + to: 'http://example.com/followers', + } + end + + it 'creates a reblog by sender of status' do + expect(sender.reblogged?(sender.statuses.first)).to be true + end + end + end + + context 'when the status belongs to a local user' do + before do + subject.perform + end + + let(:object_json) do + ActivityPub::TagManager.instance.uri_for(status) + end + + it 'creates a reblog by sender of status' do + expect(sender.reblogged?(status)).to be true + end + end + + context 'when the sender is relayed' do + let!(:relay_account) { Fabricate(:account, inbox_url: 'https://relay.example.com/inbox') } + let!(:relay) { Fabricate(:relay, inbox_url: 'https://relay.example.com/inbox') } + + subject { described_class.new(json, sender, relayed_through_account: relay_account) } - before do - subject.perform + context 'and the relay is enabled' do + before do + relay.update(state: :accepted) + subject.perform + end + + let(:object_json) do + { + id: 'https://example.com/actor#bar', + type: 'Note', + content: 'Lorem ipsum', + to: 'http://example.com/followers', + } + end + + it 'creates a reblog by sender of status' do + expect(sender.statuses.count).to eq 2 + end + end + + context 'and the relay is disabled' do + before do + subject.perform + end + + let(:object_json) do + { + id: 'https://example.com/actor#bar', + type: 'Note', + content: 'Lorem ipsum', + to: 'http://example.com/followers', + } + end + + it 'does not create anything' do + expect(sender.statuses.count).to eq 0 + end + end end - it 'creates a reblog by sender of status' do - expect(sender.reblogged?(status)).to be true + context 'when the sender has no relevance to local activity' do + before do + subject.perform + end + + let(:object_json) do + { + id: 'https://example.com/actor#bar', + type: 'Note', + content: 'Lorem ipsum', + to: 'http://example.com/followers', + } + end + + it 'does not create anything' do + expect(sender.statuses.count).to eq 0 + end end end end diff --git a/spec/lib/activitypub/activity/create_spec.rb b/spec/lib/activitypub/activity/create_spec.rb index cd20b7c7c..26cb84871 100644 --- a/spec/lib/activitypub/activity/create_spec.rb +++ b/spec/lib/activitypub/activity/create_spec.rb @@ -13,8 +13,6 @@ RSpec.describe ActivityPub::Activity::Create do }.with_indifferent_access end - subject { described_class.new(json, sender) } - before do sender.update(uri: ActivityPub::TagManager.instance.uri_for(sender)) @@ -23,59 +21,407 @@ RSpec.describe ActivityPub::Activity::Create do end describe '#perform' do - before do - subject.perform - end - - context 'standalone' do - let(:object_json) do - { - id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, - type: 'Note', - content: 'Lorem ipsum', - } - end - - it 'creates status' do - status = sender.statuses.first - - expect(status).to_not be_nil - expect(status.text).to eq 'Lorem ipsum' - end - - it 'missing to/cc defaults to direct privacy' do - status = sender.statuses.first + context 'when fetching' do + subject { described_class.new(json, sender) } + + before do + subject.perform + end + + context 'standalone' do + let(:object_json) do + { + id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, + type: 'Note', + content: 'Lorem ipsum', + } + end + + it 'creates status' do + status = sender.statuses.first + + expect(status).to_not be_nil + expect(status.text).to eq 'Lorem ipsum' + end + + it 'missing to/cc defaults to direct privacy' do + status = sender.statuses.first + + expect(status).to_not be_nil + expect(status.visibility).to eq 'direct' + end + end + + context 'public' do + let(:object_json) do + { + id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, + type: 'Note', + content: 'Lorem ipsum', + to: 'https://www.w3.org/ns/activitystreams#Public', + } + end + + it 'creates status' do + status = sender.statuses.first + + expect(status).to_not be_nil + expect(status.visibility).to eq 'public' + end + end + + context 'unlisted' do + let(:object_json) do + { + id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, + type: 'Note', + content: 'Lorem ipsum', + cc: 'https://www.w3.org/ns/activitystreams#Public', + } + end + + it 'creates status' do + status = sender.statuses.first + + expect(status).to_not be_nil + expect(status.visibility).to eq 'unlisted' + end + end + + context 'private' do + let(:object_json) do + { + id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, + type: 'Note', + content: 'Lorem ipsum', + to: 'http://example.com/followers', + } + end + + it 'creates status' do + status = sender.statuses.first + + expect(status).to_not be_nil + expect(status.visibility).to eq 'private' + end + end + + context 'limited' do + let(:recipient) { Fabricate(:account) } + + let(:object_json) do + { + id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, + type: 'Note', + content: 'Lorem ipsum', + to: ActivityPub::TagManager.instance.uri_for(recipient), + } + end + + it 'creates status' do + status = sender.statuses.first + + expect(status).to_not be_nil + expect(status.visibility).to eq 'limited' + end + + it 'creates silent mention' do + status = sender.statuses.first + expect(status.mentions.first).to be_silent + end + end + + context 'direct' do + let(:recipient) { Fabricate(:account) } + + let(:object_json) do + { + id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, + type: 'Note', + content: 'Lorem ipsum', + to: ActivityPub::TagManager.instance.uri_for(recipient), + tag: { + type: 'Mention', + href: ActivityPub::TagManager.instance.uri_for(recipient), + }, + } + end + + it 'creates status' do + status = sender.statuses.first + + expect(status).to_not be_nil + expect(status.visibility).to eq 'direct' + end + end + + context 'as a reply' do + let(:original_status) { Fabricate(:status) } + + let(:object_json) do + { + id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, + type: 'Note', + content: 'Lorem ipsum', + inReplyTo: ActivityPub::TagManager.instance.uri_for(original_status), + } + end + + it 'creates status' do + status = sender.statuses.first + + expect(status).to_not be_nil + expect(status.thread).to eq original_status + expect(status.reply?).to be true + expect(status.in_reply_to_account).to eq original_status.account + expect(status.conversation).to eq original_status.conversation + end + end + + context 'with mentions' do + let(:recipient) { Fabricate(:account) } + + let(:object_json) do + { + id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, + type: 'Note', + content: 'Lorem ipsum', + tag: [ + { + type: 'Mention', + href: ActivityPub::TagManager.instance.uri_for(recipient), + }, + ], + } + end + + it 'creates status' do + status = sender.statuses.first + + expect(status).to_not be_nil + expect(status.mentions.map(&:account)).to include(recipient) + end + end + + context 'with mentions missing href' do + let(:object_json) do + { + id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, + type: 'Note', + content: 'Lorem ipsum', + tag: [ + { + type: 'Mention', + }, + ], + } + end + + it 'creates status' do + status = sender.statuses.first + expect(status).to_not be_nil + end + end + + context 'with media attachments' do + let(:object_json) do + { + id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, + type: 'Note', + content: 'Lorem ipsum', + attachment: [ + { + type: 'Document', + mediaType: 'image/png', + url: 'http://example.com/attachment.png', + }, + ], + } + end + + it 'creates status' do + status = sender.statuses.first + + expect(status).to_not be_nil + expect(status.media_attachments.map(&:remote_url)).to include('http://example.com/attachment.png') + end + end + + context 'with media attachments with focal points' do + let(:object_json) do + { + id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, + type: 'Note', + content: 'Lorem ipsum', + attachment: [ + { + type: 'Document', + mediaType: 'image/png', + url: 'http://example.com/attachment.png', + focalPoint: [0.5, -0.7], + }, + ], + } + end + + it 'creates status' do + status = sender.statuses.first + + expect(status).to_not be_nil + expect(status.media_attachments.map(&:focus)).to include('0.5,-0.7') + end + end + + context 'with media attachments missing url' do + let(:object_json) do + { + id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, + type: 'Note', + content: 'Lorem ipsum', + attachment: [ + { + type: 'Document', + mediaType: 'image/png', + }, + ], + } + end + + it 'creates status' do + status = sender.statuses.first + expect(status).to_not be_nil + end + end + + context 'with hashtags' do + let(:object_json) do + { + id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, + type: 'Note', + content: 'Lorem ipsum', + tag: [ + { + type: 'Hashtag', + href: 'http://example.com/blah', + name: '#test', + }, + ], + } + end + + it 'creates status' do + status = sender.statuses.first + + expect(status).to_not be_nil + expect(status.tags.map(&:name)).to include('test') + end + end + + context 'with hashtags missing name' do + let(:object_json) do + { + id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, + type: 'Note', + content: 'Lorem ipsum', + tag: [ + { + type: 'Hashtag', + href: 'http://example.com/blah', + }, + ], + } + end + + it 'creates status' do + status = sender.statuses.first + expect(status).to_not be_nil + end + end + + context 'with emojis' do + let(:object_json) do + { + id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, + type: 'Note', + content: 'Lorem ipsum :tinking:', + tag: [ + { + type: 'Emoji', + icon: { + url: 'http://example.com/emoji.png', + }, + name: 'tinking', + }, + ], + } + end + + it 'creates status' do + status = sender.statuses.first + + expect(status).to_not be_nil + expect(status.emojis.map(&:shortcode)).to include('tinking') + end + end + + context 'with emojis missing name' do + let(:object_json) do + { + id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, + type: 'Note', + content: 'Lorem ipsum :tinking:', + tag: [ + { + type: 'Emoji', + icon: { + url: 'http://example.com/emoji.png', + }, + }, + ], + } + end + + it 'creates status' do + status = sender.statuses.first + expect(status).to_not be_nil + end + end + + context 'with emojis missing icon' do + let(:object_json) do + { + id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, + type: 'Note', + content: 'Lorem ipsum :tinking:', + tag: [ + { + type: 'Emoji', + name: 'tinking', + }, + ], + } + end - expect(status).to_not be_nil - expect(status.visibility).to eq 'direct' + it 'creates status' do + status = sender.statuses.first + expect(status).to_not be_nil + end end end - context 'public' do - let(:object_json) do - { - id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, - type: 'Note', - content: 'Lorem ipsum', - to: 'https://www.w3.org/ns/activitystreams#Public', - } - end - - it 'creates status' do - status = sender.statuses.first + context 'when sender is followed by local users' do + subject { described_class.new(json, sender, delivery: true) } - expect(status).to_not be_nil - expect(status.visibility).to eq 'public' + before do + Fabricate(:account).follow!(sender) + subject.perform end - end - context 'unlisted' do let(:object_json) do { id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, type: 'Note', content: 'Lorem ipsum', - cc: 'https://www.w3.org/ns/activitystreams#Public', } end @@ -83,66 +429,25 @@ RSpec.describe ActivityPub::Activity::Create do status = sender.statuses.first expect(status).to_not be_nil - expect(status.visibility).to eq 'unlisted' - end - end - - context 'private' do - let(:object_json) do - { - id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, - type: 'Note', - content: 'Lorem ipsum', - to: 'http://example.com/followers', - } - end - - it 'creates status' do - status = sender.statuses.first - - expect(status).to_not be_nil - expect(status.visibility).to eq 'private' + expect(status.text).to eq 'Lorem ipsum' end end - context 'limited' do - let(:recipient) { Fabricate(:account) } + context 'when sender replies to local status' do + let!(:local_status) { Fabricate(:status) } - let(:object_json) do - { - id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, - type: 'Note', - content: 'Lorem ipsum', - to: ActivityPub::TagManager.instance.uri_for(recipient), - } - end - - it 'creates status' do - status = sender.statuses.first - - expect(status).to_not be_nil - expect(status.visibility).to eq 'limited' - end + subject { described_class.new(json, sender, delivery: true) } - it 'creates silent mention' do - status = sender.statuses.first - expect(status.mentions.first).to be_silent + before do + subject.perform end - end - - context 'direct' do - let(:recipient) { Fabricate(:account) } let(:object_json) do { id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, type: 'Note', content: 'Lorem ipsum', - to: ActivityPub::TagManager.instance.uri_for(recipient), - tag: { - type: 'Mention', - href: ActivityPub::TagManager.instance.uri_for(recipient), - }, + inReplyTo: ActivityPub::TagManager.instance.uri_for(local_status), } end @@ -150,47 +455,25 @@ RSpec.describe ActivityPub::Activity::Create do status = sender.statuses.first expect(status).to_not be_nil - expect(status.visibility).to eq 'direct' + expect(status.text).to eq 'Lorem ipsum' end end - context 'as a reply' do - let(:original_status) { Fabricate(:status) } + context 'when sender targets a local user' do + let!(:local_account) { Fabricate(:account) } - let(:object_json) do - { - id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, - type: 'Note', - content: 'Lorem ipsum', - inReplyTo: ActivityPub::TagManager.instance.uri_for(original_status), - } - end - - it 'creates status' do - status = sender.statuses.first + subject { described_class.new(json, sender, delivery: true) } - expect(status).to_not be_nil - expect(status.thread).to eq original_status - expect(status.reply?).to be true - expect(status.in_reply_to_account).to eq original_status.account - expect(status.conversation).to eq original_status.conversation + before do + subject.perform end - end - - context 'with mentions' do - let(:recipient) { Fabricate(:account) } let(:object_json) do { id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, type: 'Note', content: 'Lorem ipsum', - tag: [ - { - type: 'Mention', - href: ActivityPub::TagManager.instance.uri_for(recipient), - }, - ], + to: ActivityPub::TagManager.instance.uri_for(local_account), } end @@ -198,68 +481,25 @@ RSpec.describe ActivityPub::Activity::Create do status = sender.statuses.first expect(status).to_not be_nil - expect(status.mentions.map(&:account)).to include(recipient) - end - end - - context 'with mentions missing href' do - let(:object_json) do - { - id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, - type: 'Note', - content: 'Lorem ipsum', - tag: [ - { - type: 'Mention', - }, - ], - } - end - - it 'creates status' do - status = sender.statuses.first - expect(status).to_not be_nil + expect(status.text).to eq 'Lorem ipsum' end end - context 'with media attachments' do - let(:object_json) do - { - id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, - type: 'Note', - content: 'Lorem ipsum', - attachment: [ - { - type: 'Document', - mediaType: 'image/png', - url: 'http://example.com/attachment.png', - }, - ], - } - end + context 'when sender cc\'s a local user' do + let!(:local_account) { Fabricate(:account) } - it 'creates status' do - status = sender.statuses.first + subject { described_class.new(json, sender, delivery: true) } - expect(status).to_not be_nil - expect(status.media_attachments.map(&:remote_url)).to include('http://example.com/attachment.png') + before do + subject.perform end - end - context 'with media attachments with focal points' do let(:object_json) do { id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, type: 'Note', content: 'Lorem ipsum', - attachment: [ - { - type: 'Document', - mediaType: 'image/png', - url: 'http://example.com/attachment.png', - focalPoint: [0.5, -0.7], - }, - ], + cc: ActivityPub::TagManager.instance.uri_for(local_account), } end @@ -267,143 +507,27 @@ RSpec.describe ActivityPub::Activity::Create do status = sender.statuses.first expect(status).to_not be_nil - expect(status.media_attachments.map(&:focus)).to include('0.5,-0.7') + expect(status.text).to eq 'Lorem ipsum' end end - context 'with media attachments missing url' do - let(:object_json) do - { - id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, - type: 'Note', - content: 'Lorem ipsum', - attachment: [ - { - type: 'Document', - mediaType: 'image/png', - }, - ], - } - end + context 'when the sender has no relevance to local activity' do + subject { described_class.new(json, sender, delivery: true) } - it 'creates status' do - status = sender.statuses.first - expect(status).to_not be_nil + before do + subject.perform end - end - context 'with hashtags' do let(:object_json) do { id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, type: 'Note', content: 'Lorem ipsum', - tag: [ - { - type: 'Hashtag', - href: 'http://example.com/blah', - name: '#test', - }, - ], } end - it 'creates status' do - status = sender.statuses.first - - expect(status).to_not be_nil - expect(status.tags.map(&:name)).to include('test') - end - end - - context 'with hashtags missing name' do - let(:object_json) do - { - id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, - type: 'Note', - content: 'Lorem ipsum', - tag: [ - { - type: 'Hashtag', - href: 'http://example.com/blah', - }, - ], - } - end - - it 'creates status' do - status = sender.statuses.first - expect(status).to_not be_nil - end - end - - context 'with emojis' do - let(:object_json) do - { - id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, - type: 'Note', - content: 'Lorem ipsum :tinking:', - tag: [ - { - type: 'Emoji', - icon: { - url: 'http://example.com/emoji.png', - }, - name: 'tinking', - }, - ], - } - end - - it 'creates status' do - status = sender.statuses.first - - expect(status).to_not be_nil - expect(status.emojis.map(&:shortcode)).to include('tinking') - end - end - - context 'with emojis missing name' do - let(:object_json) do - { - id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, - type: 'Note', - content: 'Lorem ipsum :tinking:', - tag: [ - { - type: 'Emoji', - icon: { - url: 'http://example.com/emoji.png', - }, - }, - ], - } - end - - it 'creates status' do - status = sender.statuses.first - expect(status).to_not be_nil - end - end - - context 'with emojis missing icon' do - let(:object_json) do - { - id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, - type: 'Note', - content: 'Lorem ipsum :tinking:', - tag: [ - { - type: 'Emoji', - name: 'tinking', - }, - ], - } - end - - it 'creates status' do - status = sender.statuses.first - expect(status).to_not be_nil + it 'does not create anything' do + expect(sender.statuses.count).to eq 0 end end end diff --git a/spec/lib/formatter_spec.rb b/spec/lib/formatter_spec.rb index 0c1efe7c3..96d2fc7e0 100644 --- a/spec/lib/formatter_spec.rb +++ b/spec/lib/formatter_spec.rb @@ -74,10 +74,36 @@ RSpec.describe Formatter do end context 'given a URL with a query string' do - let(:text) { 'https://www.ruby-toolbox.com/search?utf8=%E2%9C%93&q=autolink' } + context 'with escaped unicode character' do + let(:text) { 'https://www.ruby-toolbox.com/search?utf8=%E2%9C%93&q=autolink' } - it 'matches the full URL' do - is_expected.to include 'href="https://www.ruby-toolbox.com/search?utf8=%E2%9C%93&q=autolink"' + it 'matches the full URL' do + is_expected.to include 'href="https://www.ruby-toolbox.com/search?utf8=%E2%9C%93&q=autolink"' + end + end + + context 'with unicode character' do + let(:text) { 'https://www.ruby-toolbox.com/search?utf8=✓&q=autolink' } + + it 'matches the full URL' do + is_expected.to include 'href="https://www.ruby-toolbox.com/search?utf8=✓&q=autolink"' + end + end + + context 'with unicode character at the end' do + let(:text) { 'https://www.ruby-toolbox.com/search?utf8=✓' } + + it 'matches the full URL' do + is_expected.to include 'href="https://www.ruby-toolbox.com/search?utf8=✓"' + end + end + + context 'with escaped and not escaped unicode characters' do + let(:text) { 'https://www.ruby-toolbox.com/search?utf8=%E2%9C%93&utf81=✓&q=autolink' } + + it 'preserves escaped unicode characters' do + is_expected.to include 'href="https://www.ruby-toolbox.com/search?utf8=%E2%9C%93&utf81=✓&q=autolink"' + end end end @@ -89,6 +115,22 @@ RSpec.describe Formatter do end end + context 'given a URL in quotation marks' do + let(:text) { '"https://example.com/"' } + + it 'does not match the quotation marks' do + is_expected.to include 'href="https://example.com/"' + end + end + + context 'given a URL in angle brackets' do + let(:text) { '<https://example.com/>' } + + it 'does not match the angle brackets' do + is_expected.to include 'href="https://example.com/"' + end + end + context 'given a URL with Japanese path string' do let(:text) { 'https://ja.wikipedia.org/wiki/日本' } @@ -105,6 +147,22 @@ RSpec.describe Formatter do end end + context 'given a URL with a full-width space' do + let(:text) { 'https://example.com/ abc123' } + + it 'does not match the full-width space' do + is_expected.to include 'href="https://example.com/"' + end + end + + context 'given a URL in Japanese quotation marks' do + let(:text) { '「[https://example.org/」' } + + it 'does not match the quotation marks' do + is_expected.to include 'href="https://example.org/"' + end + end + context 'given a URL with Simplified Chinese path string' do let(:text) { 'https://baike.baidu.com/item/中华人民共和国' } @@ -124,7 +182,11 @@ RSpec.describe Formatter do context 'given a URL containing unsafe code (XSS attack, visible part)' do let(:text) { %q{http://example.com/b<del>b</del>} } - it 'escapes the HTML in the URL' do + it 'does not include the HTML in the URL' do + is_expected.to include '"http://example.com/b"' + end + + it 'escapes the HTML' do is_expected.to include '<del>b</del>' end end @@ -132,7 +194,11 @@ RSpec.describe Formatter do context 'given a URL containing unsafe code (XSS attack, invisible part)' do let(:text) { %q{http://example.com/blahblahblahblah/a<script>alert("Hello")</script>} } - it 'escapes the HTML in the URL' do + it 'does not include the HTML in the URL' do + is_expected.to include '"http://example.com/blahblahblahblah/a"' + end + + it 'escapes the HTML' do is_expected.to include '<script>alert("Hello")</script>' end end @@ -168,6 +234,14 @@ RSpec.describe Formatter do is_expected.to include '/tags/hashtag" class="mention hashtag" rel="tag">#<span>hashtag</span></a>' end end + + context 'given text containing a hashtag with Unicode chars' do + let(:text) { '#hashtagタグ' } + + it 'creates a hashtag link' do + is_expected.to include '/tags/hashtag%E3%82%BF%E3%82%B0" class="mention hashtag" rel="tag">#<span>hashtagタグ</span></a>' + end + end end describe '#format_spoiler' do diff --git a/spec/models/concerns/account_interactions_spec.rb b/spec/models/concerns/account_interactions_spec.rb index 9c9b87daf..36e346f14 100644 --- a/spec/models/concerns/account_interactions_spec.rb +++ b/spec/models/concerns/account_interactions_spec.rb @@ -244,9 +244,9 @@ describe AccountInteractions do end describe '#block_domain!' do - let(:domain_block) { Fabricate(:domain_block) } + let(:domain) { 'example.com' } - subject { account.block_domain!(domain_block) } + subject { account.block_domain!(domain) } it 'creates and returns AccountDomainBlock' do expect do diff --git a/spec/models/featured_tag_spec.rb b/spec/models/featured_tag_spec.rb new file mode 100644 index 000000000..07533e0b9 --- /dev/null +++ b/spec/models/featured_tag_spec.rb @@ -0,0 +1,4 @@ +require 'rails_helper' + +RSpec.describe FeaturedTag, type: :model do +end diff --git a/spec/validators/email_mx_validator_spec.rb b/spec/validators/email_mx_validator_spec.rb index bc68f63cf..48e17a4f1 100644 --- a/spec/validators/email_mx_validator_spec.rb +++ b/spec/validators/email_mx_validator_spec.rb @@ -11,6 +11,7 @@ describe EmailMxValidator do allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::MX).and_return([]) allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::A).and_return([]) + allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::AAAA).and_return([]) allow(resolver).to receive(:timeouts=).and_return(nil) allow(Resolv::DNS).to receive(:open).and_yield(resolver) @@ -23,7 +24,9 @@ describe EmailMxValidator do allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::MX).and_return([double(exchange: 'mail.example.com')]) allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::A).and_return([]) + allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::AAAA).and_return([]) allow(resolver).to receive(:getresources).with('mail.example.com', Resolv::DNS::Resource::IN::A).and_return([]) + allow(resolver).to receive(:getresources).with('mail.example.com', Resolv::DNS::Resource::IN::AAAA).and_return([]) allow(resolver).to receive(:timeouts=).and_return(nil) allow(Resolv::DNS).to receive(:open).and_yield(resolver) @@ -37,6 +40,21 @@ describe EmailMxValidator do allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::MX).and_return([]) allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::A).and_return([double(address: '1.2.3.4')]) + allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::AAAA).and_return([]) + allow(resolver).to receive(:timeouts=).and_return(nil) + allow(Resolv::DNS).to receive(:open).and_yield(resolver) + + subject.validate(user) + expect(user.errors).to have_received(:add) + end + + it 'adds an error if the AAAA record is blacklisted' do + EmailDomainBlock.create!(domain: 'fd00::1') + resolver = double + + allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::MX).and_return([]) + allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::A).and_return([]) + allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::AAAA).and_return([double(address: 'fd00::1')]) allow(resolver).to receive(:timeouts=).and_return(nil) allow(Resolv::DNS).to receive(:open).and_yield(resolver) @@ -50,7 +68,25 @@ describe EmailMxValidator do allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::MX).and_return([double(exchange: 'mail.example.com')]) allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::A).and_return([]) + allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::AAAA).and_return([]) allow(resolver).to receive(:getresources).with('mail.example.com', Resolv::DNS::Resource::IN::A).and_return([double(address: '2.3.4.5')]) + allow(resolver).to receive(:getresources).with('mail.example.com', Resolv::DNS::Resource::IN::AAAA).and_return([]) + allow(resolver).to receive(:timeouts=).and_return(nil) + allow(Resolv::DNS).to receive(:open).and_yield(resolver) + + subject.validate(user) + expect(user.errors).to have_received(:add) + end + + it 'adds an error if the MX IPv6 record is blacklisted' do + EmailDomainBlock.create!(domain: 'fd00::2') + resolver = double + + allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::MX).and_return([double(exchange: 'mail.example.com')]) + allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::A).and_return([]) + allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::AAAA).and_return([]) + allow(resolver).to receive(:getresources).with('mail.example.com', Resolv::DNS::Resource::IN::A).and_return([]) + allow(resolver).to receive(:getresources).with('mail.example.com', Resolv::DNS::Resource::IN::AAAA).and_return([double(address: 'fd00::2')]) allow(resolver).to receive(:timeouts=).and_return(nil) allow(Resolv::DNS).to receive(:open).and_yield(resolver) @@ -64,7 +100,9 @@ describe EmailMxValidator do allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::MX).and_return([double(exchange: 'mail.example.com')]) allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::A).and_return([]) + allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::AAAA).and_return([]) allow(resolver).to receive(:getresources).with('mail.example.com', Resolv::DNS::Resource::IN::A).and_return([double(address: '2.3.4.5')]) + allow(resolver).to receive(:getresources).with('mail.example.com', Resolv::DNS::Resource::IN::AAAA).and_return([double(address: 'fd00::2')]) allow(resolver).to receive(:timeouts=).and_return(nil) allow(Resolv::DNS).to receive(:open).and_yield(resolver) |