diff --git a/.circleci/config.yml b/.circleci/config.yml index 862fa126b..2f3860d7c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -129,6 +129,13 @@ jobs: environment: *ruby_environment <<: *install_ruby_dependencies + install-ruby3.0: + <<: *defaults + docker: + - image: circleci/ruby:3.0-buster-node + environment: *ruby_environment + <<: *install_ruby_dependencies + build: <<: *defaults steps: @@ -187,6 +194,18 @@ jobs: - image: circleci/redis:5-alpine <<: *test_steps + test-ruby3.0: + <<: *defaults + docker: + - image: circleci/ruby:3.0-buster-node + environment: *ruby_environment + - image: circleci/postgres:12.2 + environment: + POSTGRES_USER: root + POSTGRES_HOST_AUTH_METHOD: trust + - image: circleci/redis:5-alpine + <<: *test_steps + test-webui: <<: *defaults docker: @@ -227,6 +246,10 @@ workflows: requires: - install - install-ruby2.7 + - install-ruby3.0: + requires: + - install + - install-ruby2.7 - build: requires: - install-ruby2.7 @@ -241,6 +264,10 @@ workflows: requires: - install-ruby2.6 - build + - test-ruby3.0: + requires: + - install-ruby3.0 + - build - test-webui: requires: - install diff --git a/.codeclimate.yml b/.codeclimate.yml index dc8ca9a6f..8701e5f3d 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -30,7 +30,7 @@ plugins: channel: eslint-7 rubocop: enabled: true - channel: rubocop-0-92 + channel: rubocop-1-9-1 sass-lint: enabled: true exclude_patterns: diff --git a/.deepsource.toml b/.deepsource.toml new file mode 100644 index 000000000..bcd310412 --- /dev/null +++ b/.deepsource.toml @@ -0,0 +1,23 @@ +version = 1 + +test_patterns = ["app/javascript/mastodon/**/__tests__/**"] + +exclude_patterns = [ + "db/migrate/**", + "db/post_migrate/**" +] + +[[analyzers]] +name = "ruby" +enabled = true + +[[analyzers]] +name = "javascript" +enabled = true + + [analyzers.meta] + environment = [ + "browser", + "jest", + "nodejs" + ] diff --git a/.dockerignore b/.dockerignore index bf918029b..9bc23d813 100644 --- a/.dockerignore +++ b/.dockerignore @@ -13,3 +13,4 @@ vendor/bundle postgres redis elasticsearch +chart diff --git a/.gitignore b/.gitignore index 4545270b3..b4d2712ff 100644 --- a/.gitignore +++ b/.gitignore @@ -43,10 +43,8 @@ /redis /elasticsearch -# ignore Helm lockfile, dependency charts, and local values file -/chart/Chart.lock +# ignore Helm dependency charts /chart/charts/*.tgz -/chart/values.yaml # Ignore Apple files .DS_Store diff --git a/.rubocop.yml b/.rubocop.yml index 14728bf0e..2af0f59bb 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -2,7 +2,8 @@ require: - rubocop-rails AllCops: - TargetRubyVersion: 2.4 + TargetRubyVersion: 2.5 + NewCops: disable Exclude: - 'spec/**/*' - 'db/**/*' diff --git a/AUTHORS.md b/AUTHORS.md index 43adc3bb1..96412ff63 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -5,39 +5,38 @@ Mastodon is available on [GitHub](https://github.com/tootsuite/mastodon) and provided thanks to the work of the following contributors: * [Gargron](https://github.com/Gargron) -* [ThibG](https://github.com/ThibG) -* [dependabot-preview[bot]](https://github.com/apps/dependabot-preview) * [dependabot[bot]](https://github.com/apps/dependabot) +* [ClearlyClaire](https://github.com/ClearlyClaire) +* [dependabot-preview[bot]](https://github.com/apps/dependabot-preview) * [ykzts](https://github.com/ykzts) * [akihikodaki](https://github.com/akihikodaki) * [mjankowski](https://github.com/mjankowski) * [unarist](https://github.com/unarist) -* [yiskah](https://github.com/yiskah) -* [nolanlawson](https://github.com/nolanlawson) * [abcang](https://github.com/abcang) +* [yiskah](https://github.com/yiskah) +* [noellabo](https://github.com/noellabo) +* [nolanlawson](https://github.com/nolanlawson) * [mayaeh](https://github.com/mayaeh) * [ysksn](https://github.com/ysksn) * [sorin-davidoi](https://github.com/sorin-davidoi) -* [noellabo](https://github.com/noellabo) * [lynlynlynx](https://github.com/lynlynlynx) * [m4sk1n](mailto:me@m4sk.in) * [Marcin Mikołajczak](mailto:me@m4sk.in) -* [Kjwon15](https://github.com/Kjwon15) +* [tribela](https://github.com/tribela) * [renatolond](https://github.com/renatolond) * [alpaca-tc](https://github.com/alpaca-tc) -* [jeroenpraat](https://github.com/jeroenpraat) +* [zunda](https://github.com/zunda) * [nclm](https://github.com/nclm) * [ineffyble](https://github.com/ineffyble) -* [zunda](https://github.com/zunda) * [shleeable](https://github.com/shleeable) * [Masoud Abkenar](mailto:ampbox@gmail.com) * [blackle](https://github.com/blackle) * [Quent-in](https://github.com/Quent-in) * [JantsoP](https://github.com/JantsoP) +* [ariasuni](https://github.com/ariasuni) * [nullkal](https://github.com/nullkal) * [yookoala](https://github.com/yookoala) * [Brawaru](https://github.com/Brawaru) -* [ariasuni](https://github.com/ariasuni) * [Aditoo17](https://github.com/Aditoo17) * [Quenty31](https://github.com/Quenty31) * [marek-lach](https://github.com/marek-lach) @@ -45,7 +44,9 @@ and provided thanks to the work of the following contributors: * [ashfurrow](https://github.com/ashfurrow) * [danhunsaker](https://github.com/danhunsaker) * [eramdam](https://github.com/eramdam) +* [Jeroen](mailto:jeroenpraat@users.noreply.github.com) * [takayamaki](https://github.com/takayamaki) +* [dunn](https://github.com/dunn) * [masarakki](https://github.com/masarakki) * [ticky](https://github.com/ticky) * [trwnh](https://github.com/trwnh) @@ -53,15 +54,15 @@ and provided thanks to the work of the following contributors: * [hinaloe](https://github.com/hinaloe) * [hcmiya](https://github.com/hcmiya) * [stephenburgess8](https://github.com/stephenburgess8) -* [Wonderfall](mailto:wonderfall@targaryen.house) +* [Wonderfall](https://github.com/Wonderfall) * [matteoaquila](https://github.com/matteoaquila) * [yukimochi](https://github.com/yukimochi) * [palindromordnilap](https://github.com/palindromordnilap) * [rkarabut](https://github.com/rkarabut) +* [jeroenpraat](mailto:jeroenpraat@users.noreply.github.com) * [nightpool](https://github.com/nightpool) * [Artoria2e5](https://github.com/Artoria2e5) * [marrus-sh](https://github.com/marrus-sh) -* [dunn](https://github.com/dunn) * [krainboltgreene](https://github.com/krainboltgreene) * [pfigel](https://github.com/pfigel) * [BoFFire](https://github.com/BoFFire) @@ -73,18 +74,19 @@ and provided thanks to the work of the following contributors: * [SerCom_KC](mailto:sercom-kc@users.noreply.github.com) * [Sylvhem](https://github.com/Sylvhem) * [MitarashiDango](https://github.com/MitarashiDango) +* [rinsuki](https://github.com/rinsuki) * [angristan](https://github.com/angristan) * [JeanGauthier](https://github.com/JeanGauthier) * [kschaper](https://github.com/kschaper) * [beatrix-bitrot](https://github.com/beatrix-bitrot) * [koyuawsmbrtn](https://github.com/koyuawsmbrtn) * [BenLubar](https://github.com/BenLubar) +* [mkljczk](https://github.com/mkljczk) * [adbelle](https://github.com/adbelle) * [evanminto](https://github.com/evanminto) * [MightyPork](https://github.com/MightyPork) * [ashleyhull-versent](https://github.com/ashleyhull-versent) * [yhirano55](https://github.com/yhirano55) -* [rinsuki](https://github.com/rinsuki) * [devkral](https://github.com/devkral) * [camponez](https://github.com/camponez) * [hugogameiro](https://github.com/hugogameiro) @@ -100,7 +102,6 @@ and provided thanks to the work of the following contributors: * [lindwurm](https://github.com/lindwurm) * [victorhck](mailto:victorhck@geeko.site) * [voidsatisfaction](https://github.com/voidsatisfaction) -* [mkljczk](https://github.com/mkljczk) * [hikari-no-yume](https://github.com/hikari-no-yume) * [seefood](https://github.com/seefood) * [jackjennings](https://github.com/jackjennings) @@ -135,10 +136,11 @@ and provided thanks to the work of the following contributors: * [kadiix](https://github.com/kadiix) * [kodacs](https://github.com/kodacs) * [marcin mikołajczak](mailto:me@m4sk.in) -* [JMendyk](https://github.com/JMendyk) * [KScl](https://github.com/KScl) * [sterdev](https://github.com/sterdev) +* [mashirozx](https://github.com/mashirozx) * [TheKinrar](https://github.com/TheKinrar) +* [007lva](https://github.com/007lva) * [AA4ch1](https://github.com/AA4ch1) * [alexgleason](https://github.com/alexgleason) * [Bèr Kessels](mailto:ber@berk.es) @@ -150,6 +152,7 @@ and provided thanks to the work of the following contributors: * [hendotcat](https://github.com/hendotcat) * [d6rkaiz](https://github.com/d6rkaiz) * [ladyisatis](https://github.com/ladyisatis) +* [JMendyk](https://github.com/JMendyk) * [JohnD28](https://github.com/JohnD28) * [znz](https://github.com/znz) * [saper](https://github.com/saper) @@ -159,6 +162,7 @@ and provided thanks to the work of the following contributors: * [ekiru](https://github.com/ekiru) * [geta6](https://github.com/geta6) * [happycoloredbanana](https://github.com/happycoloredbanana) +* [joenepraat](https://github.com/joenepraat) * [leopku](https://github.com/leopku) * [SansPseudoFix](https://github.com/SansPseudoFix) * [spla](mailto:sp@mastodont.cat) @@ -169,13 +173,13 @@ and provided thanks to the work of the following contributors: * [nzws](https://github.com/nzws) * [duxovni](https://github.com/duxovni) * [smorimoto](https://github.com/smorimoto) -* [mashirozx](https://github.com/mashirozx) * [178inaba](https://github.com/178inaba) * [acid-chicken](https://github.com/acid-chicken) * [xgess](https://github.com/xgess) * [alyssais](https://github.com/alyssais) * [aablinov](https://github.com/aablinov) * [stalker314314](https://github.com/stalker314314) +* [cohosh](https://github.com/cohosh) * [cutls](https://github.com/cutls) * [huertanix](https://github.com/huertanix) * [eleboucher](https://github.com/eleboucher) @@ -184,7 +188,7 @@ and provided thanks to the work of the following contributors: * [treby](https://github.com/treby) * [jpdevries](https://github.com/jpdevries) * [gdpelican](https://github.com/gdpelican) -* [Korbinian](mailto:kontakt@korbinian-michl.de) +* [MonaLisaOverrdrive](https://github.com/MonaLisaOverrdrive) * [Kurtis Rainbolt-Greene](mailto:me@kurtisrainboltgreene.name) * [panarom](https://github.com/panarom) * [Dar13](https://github.com/Dar13) @@ -204,6 +208,7 @@ and provided thanks to the work of the following contributors: * [gled-rs](https://github.com/gled-rs) * [Valentin_NC](mailto:valentin.ouvrard@nautile.sarl) * [R0ckweb](https://github.com/R0ckweb) +* [Izorkin](https://github.com/Izorkin) * [unasuke](https://github.com/unasuke) * [caasi](https://github.com/caasi) * [chr-1x](https://github.com/chr-1x) @@ -211,13 +216,14 @@ and provided thanks to the work of the following contributors: * [foxiehkins](https://github.com/foxiehkins) * [highemerly](https://github.com/highemerly) * [hoodie](mailto:hoodiekitten@outlook.com) +* [kaiyou](https://github.com/kaiyou) * [luzi82](https://github.com/luzi82) * [slice](https://github.com/slice) * [tmm576](https://github.com/tmm576) * [unsmell](mailto:unsmell@users.noreply.github.com) * [valerauko](https://github.com/valerauko) * [chriswmartin](https://github.com/chriswmartin) -* [vahnj](https://github.com/vahnj) +* [SuperSandro2000](https://github.com/SuperSandro2000) * [ikuradon](https://github.com/ikuradon) * [AndreLewin](https://github.com/AndreLewin) * [0xflotus](https://github.com/0xflotus) @@ -254,17 +260,20 @@ and provided thanks to the work of the following contributors: * [ian-kelling](https://github.com/ian-kelling) * [immae](https://github.com/immae) * [J0WI](https://github.com/J0WI) +* [vahnj](https://github.com/vahnj) * [foozmeat](https://github.com/foozmeat) * [jasonrhodes](https://github.com/jasonrhodes) * [Jason Snell](mailto:jason@newrelic.com) * [jviide](https://github.com/jviide) * [YuleZ](https://github.com/YuleZ) +* [jtracey](https://github.com/jtracey) * [crakaC](https://github.com/crakaC) * [tkbky](https://github.com/tkbky) * [Kaylee](mailto:kaylee@codethat.sucks) * [Kazhnuz](https://github.com/Kazhnuz) * [mkody](https://github.com/mkody) * [connyduck](https://github.com/connyduck) +* [Tak](https://github.com/Tak) * [LindseyB](https://github.com/LindseyB) * [Lorenz Diener](mailto:halcyon@icosahedron.website) * [Markus Amalthea Magnuson](mailto:markus.magnuson@gmail.com) @@ -282,9 +291,9 @@ and provided thanks to the work of the following contributors: * [lumenwrites](https://github.com/lumenwrites) * [remram44](https://github.com/remram44) * [sts10](https://github.com/sts10) -* [SuperSandro2000](https://github.com/SuperSandro2000) * [u1-liquid](https://github.com/u1-liquid) * [rosylilly](https://github.com/rosylilly) +* [withshubh](https://github.com/withshubh) * [sim6](https://github.com/sim6) * [Sir-Boops](https://github.com/Sir-Boops) * [stemid](https://github.com/stemid) @@ -305,15 +314,16 @@ and provided thanks to the work of the following contributors: * [anon5r](https://github.com/anon5r) * [aus-social](https://github.com/aus-social) * [bsky](mailto:me@imbsky.net) +* [chandrn7](https://github.com/chandrn7) * [codl](https://github.com/codl) * [cpsdqs](https://github.com/cpsdqs) * [barzamin](https://github.com/barzamin) +* [gol-cha](https://github.com/gol-cha) * [fhalna](https://github.com/fhalna) * [haoyayoi](https://github.com/haoyayoi) * [ik11235](https://github.com/ik11235) * [kawax](https://github.com/kawax) * [shrft](https://github.com/shrft) -* [007lva](https://github.com/007lva) * [mbajur](https://github.com/mbajur) * [matsurai25](https://github.com/matsurai25) * [mecab](https://github.com/mecab) @@ -353,7 +363,7 @@ and provided thanks to the work of the following contributors: * [a2](https://github.com/a2) * [alfiedotwtf](https://github.com/alfiedotwtf) * [0xa](https://github.com/0xa) -* [ArisuOngaku](https://github.com/ArisuOngaku) +* [ashpieboop](https://github.com/ashpieboop) * [virtualpain](https://github.com/virtualpain) * [sapphirus](https://github.com/sapphirus) * [amandavisconti](https://github.com/amandavisconti) @@ -367,6 +377,7 @@ and provided thanks to the work of the following contributors: * [orlea](https://github.com/orlea) * [armandfardeau](https://github.com/armandfardeau) * [raboof](https://github.com/raboof) +* [aldatsa](https://github.com/aldatsa) * [jumbosushi](https://github.com/jumbosushi) * [acuteaura](https://github.com/acuteaura) * [ayumin](https://github.com/ayumin) @@ -375,7 +386,7 @@ and provided thanks to the work of the following contributors: * [li-bei](https://github.com/li-bei) * [Benedikt Geißler](mailto:benedikt@g5r.eu) * [BenisonSebastian](https://github.com/BenisonSebastian) -* [blakebarnett](https://github.com/blakebarnett) +* [Blake](mailto:blake.barnett@postmates.com) * [Brad Janke](mailto:brad.janke@gmail.com) * [bclindner](https://github.com/bclindner) * [brycied00d](https://github.com/brycied00d) @@ -395,10 +406,12 @@ and provided thanks to the work of the following contributors: * [colindean](https://github.com/colindean) * [DeeUnderscore](https://github.com/DeeUnderscore) * [dachinat](https://github.com/dachinat) -* [monsterpit-firedemon](https://github.com/monsterpit-firedemon) +* [Daggertooth](mailto:dev@monsterpit.net) * [watilde](https://github.com/watilde) +* [dalehenries](https://github.com/dalehenries) * [daprice](https://github.com/daprice) * [da2x](https://github.com/da2x) +* [danieljakots](https://github.com/danieljakots) * [codesections](https://github.com/codesections) * [dar5hak](https://github.com/dar5hak) * [kant](https://github.com/kant) @@ -423,6 +436,7 @@ and provided thanks to the work of the following contributors: * [espenronnevik](https://github.com/espenronnevik) * [Expenses](mailto:expenses@airmail.cc) * [fabianonline](https://github.com/fabianonline) +* [shello](https://github.com/shello) * [Finariel](https://github.com/Finariel) * [siuying](https://github.com/siuying) * [zoc](https://github.com/zoc) @@ -433,7 +447,7 @@ and provided thanks to the work of the following contributors: * [hattori6789](https://github.com/hattori6789) * [algernon](https://github.com/algernon) * [Fastbyte01](https://github.com/Fastbyte01) -* [myfreeweb](https://github.com/myfreeweb) +* [unrelentingtech](https://github.com/unrelentingtech) * [gfaivre](https://github.com/gfaivre) * [Fiaxhs](https://github.com/Fiaxhs) * [rasjonell](https://github.com/rasjonell) @@ -445,17 +459,20 @@ and provided thanks to the work of the following contributors: * [Habu-Kagumba](https://github.com/Habu-Kagumba) * [suzukaze](https://github.com/suzukaze) * [Hiromi-Kai](https://github.com/Hiromi-Kai) -* [hishamhm](https://github.com/hishamhm) -* [Slaynash](https://github.com/Slaynash) -* [musashino205](https://github.com/musashino205) -* [iwaim](https://github.com/iwaim) -* [valrus](https://github.com/valrus) -* [IMcD23](https://github.com/IMcD23) -* [yi0713](https://github.com/yi0713) -* [iblech](https://github.com/iblech) +* [Hisham Muhammad](mailto:hisham@gobolinux.org) +* [Hugo "Slaynash" Flores](mailto:hugoflores@hotmail.fr) +* [INAGAKI Hiroshi](mailto:musashino205@users.noreply.github.com) +* [IWAI, Masaharu](mailto:iwaim.sub@gmail.com) +* [Ian McCowan](mailto:imccowan@gmail.com) +* [Ian McDowell](mailto:me@ianmcdowell.net) +* [Iijima Yasushi](mailto:kurage.cc@gmail.com) +* [Ikko Ashimine](mailto:eltociear@gmail.com) +* [Ingo Blechschmidt](mailto:iblech@web.de) * [J Yeary](mailto:usbsnowcrash@users.noreply.github.com) -* [jack-michaud](https://github.com/jack-michaud) -* [Floppy](https://github.com/Floppy) +* [Jack Michaud](mailto:jack-michaud@users.noreply.github.com) +* [Jakub Mendyk](mailto:jakubmendyk.szkola@gmail.com) +* [James](mailto:james.allen.vaughan@gmail.com) +* [James Smith](mailto:james@floppy.org.uk) * [Jarek Lipski](mailto:pub@loomchild.net) * [Jennifer Glauche](mailto:=^.^=@github19.jglauche.de) * [Jennifer Kruse](mailto:jenkr55@gmail.com) @@ -464,6 +481,7 @@ and provided thanks to the work of the following contributors: * [Jessica K. Litwin](mailto:jessica@litw.in) * [Jo Decker](mailto:trolldecker@users.noreply.github.com) * [Joan Montané](mailto:jmontane@users.noreply.github.com) +* [Joe](mailto:401283+htmlbyjoe@users.noreply.github.com) * [Jonathan Klee](mailto:klee.jonathan@gmail.com) * [Jordan Guerder](mailto:jguerder@fr.pulseheberg.net) * [Joseph Mingrone](mailto:jehops@users.noreply.github.com) @@ -483,7 +501,6 @@ and provided thanks to the work of the following contributors: * [Krzysztof Jurewicz](mailto:krzysztof.jurewicz@gmail.com) * [Leo Wzukw](mailto:leowzukw@users.noreply.github.com) * [Leonie](mailto:62470640+bubblineyuri@users.noreply.github.com) -* [Levi Bard](mailto:taktaktaktaktaktaktaktaktaktak@gmail.com) * [Lex Alexander](mailto:l.alexander10@gmail.com) * [Lorenz Diener](mailto:lorenzd@gmail.com) * [Luc Didry](mailto:ldidry@users.noreply.github.com) @@ -560,6 +577,7 @@ and provided thanks to the work of the following contributors: * [ScienJus](mailto:i@scienjus.com) * [Scott Larkin](mailto:scott@codeclimate.com) * [Scott Sweeny](mailto:scott@ssweeny.net) +* [Sean](mailto:sean@sean.taipei) * [Sebastian Hübner](mailto:imolein@users.noreply.github.com) * [Sebastian Morr](mailto:sebastian@morr.cc) * [Sergei Č](mailto:noiwex1911@gmail.com) @@ -570,8 +588,10 @@ and provided thanks to the work of the following contributors: * [Shouko Yu](mailto:imshouko@gmail.com) * [Sina Mashek](mailto:sina@mashek.xyz) * [Soft. Dev](mailto:24978+nileshkumar@users.noreply.github.com) +* [Sophie Parker](mailto:dev@cortices.me) * [Soshi Kato](mailto:mail@sossii.com) * [Spanky](mailto:2788886+spankyworks@users.noreply.github.com) +* [Stanislas](mailto:stanislas.lange@pm.me) * [StefOfficiel](mailto:pichard.stephane@free.fr) * [Steven Tappert](mailto:admin@dark-it.net) * [Stéphane Guillou](mailto:stephane.guillou@member.fsf.org) @@ -630,9 +650,9 @@ and provided thanks to the work of the following contributors: * [evilny0](mailto:evilny0@moomoocamp.net) * [febrezo](mailto:felixbrezo@gmail.com) * [fsubal](mailto:fsubal@users.noreply.github.com) +* [fusagiko / takayamaki](mailto:24884114+takayamaki@users.noreply.github.com) * [fusshi-](mailto:dikky1218@users.noreply.github.com) * [gentaro](mailto:gentaroooo@gmail.com) -* [gol-cha](mailto:info@mevo.xyz) * [guigeekz](mailto:pattusg@gmail.com) * [hakoai](mailto:hk--76@qa2.so-net.ne.jp) * [haosbvnker](mailto:github@chaosbunker.com) @@ -645,7 +665,7 @@ and provided thanks to the work of the following contributors: * [jooops](mailto:joops@autistici.org) * [jukper](mailto:jukkaperanto@gmail.com) * [jumoru](mailto:jumoru@mailbox.org) -* [kaiyou](mailto:pierre@jaury.eu) +* [kaias1jp](mailto:kaias1jp@gmail.com) * [karlyeurl](mailto:karl.yeurl@gmail.com) * [kawaguchi](mailto:jiikko@users.noreply.github.com) * [kedama](mailto:32974885+kedamadq@users.noreply.github.com) @@ -705,104 +725,131 @@ This document is provided for informational purposes only. Since it is only upda Following people have contributed to translation of Mastodon: -- ᏦᏁᎢᎵᏫ 😷 (KNTRO) (*Spanish, Argentina*) +- GunChleoc (*Scottish Gaelic*) +- ᛤᚤᛠᛥⴲ 👽 (KNTRO) (*Spanish, Argentina*) +- adrmzz (*Sardinian*) +- Hồ Nhất Duy (kantcer) (*Vietnamese*) +- Zoltán Gera (gerazo) (*Hungarian*) - Sveinn í Felli (sveinki) (*Icelandic*) - qezwan (*Persian, Sorani (Kurdish)*) -- Hồ Nhất Duy (kantcer) (*Vietnamese*) -- taicv (*Vietnamese*) -- Zoltán Gera (gerazo) (*Hungarian*) -- ButterflyOfFire (BoFFire) (*French, Arabic, Kabyle*) -- adrmzz (*Sardinian*) +- NCAA (*Danish*) - Ramdziana F Y (rafeyu) (*Indonesian*) -- Evert Prants (IcyDiamond) (*Estonian*) -- Daniele Lira Mereb (danilmereb) (*Portuguese, Brazilian*) +- taicv (*Vietnamese*) +- ButterflyOfFire (BoFFire) (*French, Arabic, Kabyle*) - Xosé M. (XoseM) (*Spanish, Galician*) -- Kristijan Tkalec (lapor) (*Slovenian*) -- stan ionut (stanionut12) (*Romanian*) +- Evert Prants (IcyDiamond) (*Estonian*) - Besnik_b (*Albanian*) - Emanuel Pina (emanuelpina) (*Portuguese*) -- Thai Localization (thl10n) (*Thai*) -- 奈卜拉 (nebula_moe) (*Chinese Simplified*) - Jeong Arm (Kjwon15) (*Japanese, Korean, Esperanto*) -- Michal Stanke (mstanke) (*Czech*) -- Alix Rossi (palindromordnilap) (*French, Corsican*) +- Alix Rossi (palindromordnilap) (*French, Esperanto, Corsican*) +- Thai Localization (thl10n) (*Thai*) +- Daniele Lira Mereb (danilmereb) (*Portuguese, Brazilian*) +- Joene (joenepraat) (*Dutch*) +- Kristijan Tkalec (lapor) (*Slovenian*) +- stan ionut (stanionut12) (*Romanian*) - spla (*Spanish, Catalan*) -- Imre Kristoffer Eilertsen (DandelionSprout) (*Norwegian*) -- Jeroen (jeroenpraat) (*Dutch*) -- borys_sh (*Ukrainian*) -- Miguel Mayol (mitcoes) (*Spanish, Catalan*) +- мачко (ma4ko) (*Bulgarian*) +- 奈卜拉 (nebula_moe) (*Chinese Simplified*) +- kamee (*Armenian*) +- AJ-عجائب البرمجة (Esmail_Hazem) (*Arabic*) +- Michal Stanke (mstanke) (*Czech*) - Danial Behzadi (danialbehzadi) (*Persian*) -- yeft (*Chinese Traditional, Chinese Traditional, Hong Kong*) +- borys_sh (*Ukrainian*) +- Asier Iturralde Sarasola (aldatsa) (*Basque*) +- Imre Kristoffer Eilertsen (DandelionSprout) (*Norwegian*) - koyu (*German*) +- yeft (*Chinese Traditional, Chinese Traditional, Hong Kong*) +- Miguel Mayol (mitcoes) (*Spanish, Catalan*) +- Sasha Sorokin (Brawaru) (*French, Catalan, Danish, German, Greek, Hungarian, Armenian, Korean, Russian, Albanian, Swedish, Ukrainian, Vietnamese, Galician*) +- Roboron (*Spanish*) - Koala Yeung (yookoala) (*Chinese Traditional, Hong Kong*) +- Ondřej Pokorný (unextro) (*Czech*) - Osoitz (*Basque*) - Peterandre (*Norwegian, Norwegian Nynorsk*) - tzium (*Sardinian*) +- Mélanie Chauvel (ariasuni) (*French, Arabic, Czech, German, Greek, Hungarian, Slovenian, Ukrainian, Chinese Simplified, Portuguese, Brazilian, Persian, Norwegian Nynorsk, Esperanto, Breton, Corsican, Sardinian, Kabyle*) - Iváns (Ivans_translator) (*Galician*) -- Sasha Sorokin (Sasha-Sorokin) (*French, Catalan, Danish, German, Greek, Hungarian, Armenian, Korean, Russian, Albanian, Swedish, Ukrainian, Vietnamese, Galician*) -- kamee (*Armenian*) +- Maya Minatsuki (mayaeh) (*Japanese*) +- Manuel Viens (manuelviens) (*French*) +- Alessandro Levati (Oct326) (*Italian*) +- lamnatos (*Greek*) +- Sean Young (assanges) (*Chinese Traditional*) - tolstoevsky (*Russian*) - enolp (*Asturian*) -- FédiQuébec (manuelviens) (*French*) -- lamnatos (*Greek*) -- Maya Minatsuki (mayaeh) (*Japanese*) +- Jasmine Cam Andrever (gourmas) (*Cornish*) +- gagik_ (*Armenian*) - Masoud Abkenar (mabkenar) (*Persian*) -- Alessandro Levati (Oct326) (*Italian*) - arshat (*Kazakh*) -- Roboron (*Spanish*) -- ariasuni (*French, Arabic, Czech, German, Greek, Hungarian, Slovenian, Ukrainian, Chinese Simplified, Portuguese, Brazilian, Persian, Norwegian Nynorsk, Esperanto, Breton, Corsican, Sardinian, Kabyle*) -- Ali Demirtaş (alidemirtas) (*Turkish*) -- Em St Cenydd (cancennau) (*Welsh*) +- Marcin Mikołajczak (mkljczkk) (*Czech, Polish, Russian*) - Marek Ľach (mareklach) (*Polish, Slovak*) +- Ali Demirtaş (alidemirtas) (*Turkish*) +- Blak Ouille (BlakOuille16) (*French*) +- Em St Cenydd (cancennau) (*Welsh*) +- Diluns (*Occitan*) - Muha Aliss (muhaaliss) (*Turkish*) - Jurica (ahjk) (*Croatian*) - Aditoo17 (*Czech*) -- Diluns (*Occitan*) -- gagik_ (*Armenian*) - vishnuvaratharajan (*Tamil*) -- Marcin Mikołajczak (mkljczkk) (*Czech, Polish, Russian*) +- pulmonarycosignerkindness (*Swedish*) +- cybergene (cyber-gene) (*Japanese*) +- Takeçi (polygoat) (*French, Italian*) +- xatier (*Chinese Traditional*) +- Ihor Hordiichuk (ihor_ck) (*Ukrainian*) - regulartranslator (*Portuguese, Brazilian*) +- ozzii (*French, Serbian (Cyrillic)*) +- Irfan (Irfan_Radz) (*Malay*) +- Saederup92 (*Danish*) - Akarshan Biswas (biswasab) (*Bengali, Sanskrit*) - Yi-Jyun Pan (pan93412) (*Chinese Traditional*) +- Rafael H L Moretti (Moretti) (*Portuguese, Brazilian*) - d5Ziif3K (*Ukrainian*) - GiorgioHerbie (*Italian*) -- Rafael H L Moretti (Moretti) (*Portuguese, Brazilian*) -- Saederup92 (*Danish*) - christalleras (*Norwegian Nynorsk*) -- cybergene (cyber-gene) (*Japanese*) - Taloran (*Norwegian Nynorsk*) - ThibG (*French, Icelandic*) -- xatier (*Chinese Traditional*) - otrapersona (*Spanish, Spanish, Mexico*) +- Store (HelaBasa) (*Sinhala*) +- Mauzi (*German, Swedish*) - atarashiako (*Chinese Simplified*) - 101010 (101010pl) (*Polish*) +- erictapen (*German*) +- Tagomago (tagomago) (*French, Spanish*) +- Jaz-Michael King (jazmichaelking) (*Welsh*) +- coxde (*Chinese Simplified*) +- T. E. Kalaycı (tekrei) (*Turkish*) - silkevicious (*Italian*) - Floxu (fredrikdim1) (*Norwegian Nynorsk*) +- Ryo (DrRyo) (*Korean*) - Bertil Hedkvist (Berrahed) (*Swedish*) - William(ѕ)ⁿ (wmlgr) (*Spanish*) - norayr (*Armenian*) +- Satnam S Virdi (pika10singh) (*Punjabi*) - Tiago Epifânio (tfve) (*Portuguese*) -- Ryo (DrRyo) (*Korean*) +- Balázs Meskó (mesko.balazs) (*Hungarian*) +- Sokratis Alichanidis (alichani) (*Greek*) - Mentor Gashi (mentorgashi.com) (*Albanian*) -- Jaz-Michael King (jazmichaelking) (*Welsh*) - carolinagiorno (*Portuguese, Brazilian*) +- Hayk Khachatryan (brutusromanus123) (*Armenian*) - Roby Thomas (roby.thomas) (*Malayalam*) - Bharat Kumar (Marwari) (*Hindi*) +- Austra Muizniece (aus_m) (*Latvian*) - ThonyVezbe (*Breton*) +- v4vachan (*Malayalam*) - dkdarshan760 (*Sanskrit*) -- Tagomago (tagomago) (*French, Spanish*) - tykayn (*French*) - axi (*Finnish*) -- Selyan Slimane AMIRI (slimane_AMIRI) (*Kabyle*) -- Balázs Meskó (mesko.balazs) (*Hungarian*) +- Selyan Slimane AMIRI (SelyanKab) (*Kabyle*) +- Timur Seber (seber) (*Tatar*) - taoxvx (*Danish*) - Hrach Mkrtchyan (mhrach87) (*Armenian*) - sabri (thetomatoisavegetable) (*Spanish, Spanish, Argentina*) - Dewi (Unkorneg) (*French, Breton*) -- Coelacanthus (*Chinese Simplified*) +- CoelacanthusHex (*Chinese Simplified*) - syncopams (*Chinese Simplified, Chinese Traditional, Chinese Traditional, Hong Kong*) +- Rhys Harrison (rhedders) (*Esperanto*) +- Hakim Oubouali (zenata1) (*Standard Moroccan Tamazight*) - SteinarK (*Norwegian Nynorsk*) -- Sokratis Alichanidis (alichani) (*Greek*) +- Lalo Tafolla (lalotafo) (*Spanish, Spanish, Mexico*) - Mathias B. Vagnes (vagnes) (*Norwegian*) - dashersyed (*Urdu (Pakistan)*) - Acolyte (666noob404) (*Ukrainian*) @@ -811,104 +858,124 @@ Following people have contributed to translation of Mastodon: - Damjan Dimitrioski (gnud) (*Macedonian*) - PPNplus (*Thai*) - shioko (*Chinese Simplified*) -- v4vachan (*Malayalam*) -- Hakim Oubouali (zenata1) (*Standard Moroccan Tamazight*) +- ZiriSut (*Kabyle*) - Evgeny Petrov (kondra007) (*Russian*) - Gwenn (Belvar) (*Breton*) - StanleyFrew (*French*) -- Hayk Khachatryan (brutusromanus123) (*Armenian*) +- Nikita Epifanov (Nikets) (*Russian*) - jaranta (*Finnish*) -- Felicia (midsommar) (*Swedish*) +- Slobodan Simić (Слободан Симић) (slsimic) (*Serbian (Cyrillic)*) +- Felicia Jongleur (midsommar) (*Swedish*) - Denys (dector) (*Ukrainian*) +- iVampireSP (*Chinese Simplified, Chinese Traditional*) - Pukima (pukimaaa) (*German*) +- 游荡 (MamaShip) (*Chinese Simplified*) - Vanege (*Esperanto*) +- Rikard Linde (rikardlinde) (*Swedish*) - Jess Rafn (therealyez) (*Danish*) - strubbl (*German*) - Stasiek Michalski (hellcp) (*Polish*) - dxwc (*Bengali*) - jmontane (*Catalan*) - Liboide (*Spanish*) +- Hexandcube (hexandcube) (*Polish*) +- Chris Kay (chriskarasoulis) (*Greek*) - Johan Schiff (schyffel) (*Swedish*) - Arunmozhi (tecoholic) (*Tamil*) +- zer0-x (ZER0-X) (*Arabic*) - kat (katktv) (*Russian, Ukrainian*) -- Rikard Linde (rikardlinde) (*Swedish*) +- Lauren Liberda (selfisekai) (*Polish*) +- mynameismonkey (*Welsh*) - oti4500 (*Hungarian, Ukrainian*) -- Laura (selfisekai) (*Polish*) -- Rachida S. (ZiriSut) (*Kabyle*) +- Mats Gunnar Ahlqvist (goqbi) (*Swedish*) - diazepan (*Spanish, Spanish, Argentina*) - marzuquccen (*Kabyle*) -- Juan José Salvador Piedra (JuanjoSalvador) (*Spanish*) +- VictorCorreia (victorcorreia1984) (*Afrikaans*) - Tigran (tigransimonyan) (*Armenian*) +- Juan José Salvador Piedra (JuanjoSalvador) (*Spanish*) - BurekzFinezt (*Serbian (Cyrillic)*) - SHeija (*Finnish*) +- Gearguy (*Finnish*) - atriix (*Swedish*) - Jack R (isaac.97_WT) (*Spanish*) - antonyho (*Chinese Traditional, Hong Kong*) +- asnomgtu (*Hungarian*) +- ahangarha (*Persian*) - andruhov (*Russian, Ukrainian*) -- Aryamik Sharma (Aryamik) (*Swedish, Hindi*) - phena109 (*Chinese Traditional, Hong Kong*) +- Aryamik Sharma (Aryamik) (*Swedish, Hindi*) +- Unmual (*Spanish*) - 森の子リスのミーコの大冒険 (Phroneris) (*Japanese*) - るいーね (ruine) (*Japanese*) -- ahangarha (*Persian*) - Sam Tux (imahbub) (*Bengali*) +- Kristoffer Grundström (Umeaboy) (*Swedish*) - igordrozniak (*Polish*) -- Unmual (*Spanish*) - Isaac Huang (caasih) (*Chinese Traditional*) - AW Unad (awcodify) (*Indonesian*) - Allen Zhong (AstroProfundis) (*Chinese Simplified*) - Cutls (cutls) (*Japanese*) -- Ray (Ipsumry) (*Spanish*) - Falling Snowdin (tghgg) (*Vietnamese*) -- coxde (*Chinese Simplified*) +- Ray (Ipsumry) (*Spanish*) +- Gianfranco Fronteddu (gianfro.gianfro) (*Sardinian*) - Rasmus Lindroth (RasmusLindroth) (*Swedish*) - Andrea Lo Iacono (niels0n) (*Italian*) +- Parodper (*Galician*) +- fucsia (*Italian*) +- NadieAishi (*Spanish, Spanish, Mexico*) - Kinshuk Sunil (kinshuksunil) (*Hindi*) - Ullas Joseph (ullasjoseph) (*Malayalam*) - Goudarz Jafari (Goudarz) (*Persian*) - Yu-Pai Liu (tedliou) (*Chinese Traditional*) - Amarin Cemthong (acitmaster) (*Thai*) +- Johannes Nilsson (nlssn) (*Swedish*) - juanda097 (juanda-097) (*Spanish*) - Anunnakey (*Macedonian*) -- fragola (*Italian*) +- erikkemp (*Dutch*) - erikstl (*Esperanto*) -- twpenguin (*Chinese Traditional*) - bobchao (*Chinese Traditional*) -- Esther (esthermations) (*Portuguese*) +- twpenguin (*Chinese Traditional*) - MadeInSteak (*Finnish*) -- Heimen Stoffels (vistausss) (*Dutch*) +- Esther (esthermations) (*Portuguese*) +- t_aus_m (*German*) +- Heimen Stoffels (Vistaus) (*Dutch*) - Rajarshi Guha (rajarshiguha) (*Bengali*) -- Andrew (iAndrew3) (*Romanian*) +- Mo_der Steven (SakuraPuare) (*Chinese Simplified*) - Gopal Sharma (gopalvirat) (*Hindi*) - arethsu (*Swedish*) -- Tofiq Abdula (Xwla) (*Sorani (Kurdish)*) - Carlos Solís (csolisr) (*Esperanto*) +- Tofiq Abdula (Xwla) (*Sorani (Kurdish)*) - Parthan S Ramanujam (parthan) (*Tamil*) - Kasper Nymand (KasperNymand) (*Danish*) +- Jeff Huang (s8321414) (*Chinese Traditional*) - TS (morte) (*Finnish*) - subram (*Turkish*) - SensDeViata (*Ukrainian*) - Ptrcmd (ptrcmd) (*Chinese Traditional*) - SergioFMiranda (*Portuguese, Brazilian*) -- Scvoet (scvoet) (*Chinese Simplified*) +- Percy (scvoet) (*Chinese Simplified*) +- Vivek K J (Vivekkj) (*Malayalam*) - hiroTS (*Chinese Traditional*) - johne32rus23 (*Russian*) - AzureNya (*Chinese Simplified*) - OctolinGamer (octolingamer) (*Portuguese, Brazilian*) - Ram varma (ram4varma) (*Tamil*) -- Hexandcube (hexandcube) (*Polish*) - 北䑓如法 (Nyoho) (*Japanese*) +- Pukima (Pukimaa) (*German*) +- diorama (*Italian*) +- Daniel Dimitrov (daniel.dimitrov) (*Bulgarian*) - frumble (*German*) - kekkepikkuni (*Tamil*) -- Neo_Chen (NeoChen1024) (*Chinese Traditional*) - oorsutri (*Tamil*) -- Rhys Harrison (rhedders) (*Esperanto*) +- Neo_Chen (NeoChen1024) (*Chinese Traditional*) - Nithin V (Nithin896) (*Tamil*) +- Marcus Myge (mygg-priv) (*Norwegian*) - Miro Rauhala (mirorauhala) (*Finnish*) -- diorama (*Italian*) - AlexKoala (alexkoala) (*Korean*) +- ಚಿರಾಗ್ ನಟರಾಜ್ (chiraag-nataraj) (*Kannada*) - Aswin C (officialcjunior) (*Malayalam*) - Guillaume Turchini (orion78fr) (*French*) - Ganesh D (auntgd) (*Marathi*) +- mawoka-myblock (mawoka) (*German*) - dragnucs2 (*Arabic*) - Ryan Ho (koungho) (*Chinese Traditional*) - Pedro Henrique (exploronauta) (*Portuguese, Brazilian*) @@ -916,203 +983,245 @@ Following people have contributed to translation of Mastodon: - Vasanthan (vasanthan) (*Tamil*) - 硫酸鶏 (acid_chicken) (*Japanese*) - clarmin b8 (clarminb8) (*Sorani (Kurdish)*) +- programizer (*German*) - manukp (*Malayalam*) -- psymyn (*Hebrew*) - earth dweller (sanethoughtyt) (*Marathi*) +- psymyn (*Hebrew*) - meijerivoi (toilet) (*Finnish*) - essaar (*Tamil*) - serubeena (*Swedish*) -- Karol Kosek (krkkPL) (*Polish*) - Rintan (*Japanese*) -- valarivan (*Tamil*) +- Karol Kosek (krkkPL) (*Polish*) +- Khó͘ Tiat-lêng (khotiatleng) (*Chinese Traditional, Taigi*) - Hernik (hernik27) (*Czech*) -- Sebastián Andil (Selrond) (*Slovak*) +- valarivan (*Tamil*) +- kuchengrab (*German*) +- friedbeans (*Croatian*) +- Abi Turi (abi123) (*Georgian*) - Hinaloe (hinaloe) (*Japanese*) -- filippodb (*Italian*) +- Sebastián Andil (Selrond) (*Slovak*) - KEINOS (*Japanese*) +- filippodb (*Italian*) +- Asbjørn Olling (a2) (*Danish*) - Balázs Meskó (meskobalazs) (*Hungarian*) - Bottle (suryasalem2010) (*Tamil*) -- JzshAC (*Chinese Simplified*) - Wrya ali (John12) (*Sorani (Kurdish)*) -- Khóo (khootiatling) (*Chinese Traditional*) -- Steven Tappert (sammy8806) (*German*) +- JzshAC (*Chinese Simplified*) - Antillion (antillion99) (*Spanish*) -- Pukima (Pukimaa) (*German*) +- Steven Tappert (sammy8806) (*German*) - Reg3xp (*Persian*) -- hiphipvargas (*Portuguese*) +- Wassim EL BOUHAMIDI (elbouhamidiw) (*Arabic*) - gowthamanb (*Tamil*) +- hiphipvargas (*Portuguese*) - Ch. (sftblw) (*Korean*) -- Jeff Huang (s8321414) (*Chinese Traditional*) - Arttu Ylhävuori (arttu.ylhavuori) (*Finnish*) - tctovsli (*Norwegian Nynorsk*) - Timo Tijhof (Krinkle) (*Dutch*) +- Mikkel B. Goldschmidt (mikkelbjoern) (*Danish*) +- mecqor labi (mecqorlabi) (*Persian*) +- Odyssey346 (alexader612) (*Norwegian*) - Yamagishi Kazutoshi (ykzts) (*Japanese, Icelandic, Sorani (Kurdish)*) +- Eban (ebanDev) (*French, Esperanto*) - vjasiegd (*Polish*) - SamitiMed (samiti3d) (*Thai*) +- Nícolas Lavinicki (nclavinicki) (*Portuguese, Brazilian*) +- snatcher (*Portuguese, Brazilian*) - Rekan Adl (rekan-adl1) (*Sorani (Kurdish)*) +- VSx86 (*Russian*) - umelard (*Hebrew*) - Antara2Cinta (Se7enTime) (*Indonesian*) -- VSx86 (*Russian*) -- Daniel Dimitrov (danny-dimitrov) (*Bulgarian*) - parnikkapore (*Thai*) -- mynameismonkey (*Welsh*) - Sherwan Othman (sherwanothman11) (*Sorani (Kurdish)*) - Yassine Aït-El-Mouden (yaitelmouden) (*Standard Moroccan Tamazight*) - SKELET (*Danish*) -- Mo_der Steven (SakuraPuare) (*Chinese Simplified*) - Fei Yang (Fei1Yang) (*Chinese Traditional*) -- ALEM FARID (faridatcemlulaqbayli) (*Kabyle*) -- enipra (*Armenian*) -- musix (*Persian*) +- Ğani (freegnu) (*Tatar*) - Renato "Lond" Cerqueira (renatolond) (*Portuguese, Brazilian*) +- enipra (*Armenian*) +- ALEM FARID (faridatcemlulaqbayli) (*Kabyle*) +- musix (*Persian*) - ギャラ (gyara) (*Japanese, Chinese Simplified*) - Hougo (hougo) (*French*) - ybardapurkar (*Marathi*) +- 亜緯丹穂 (ayiniho) (*Japanese*) - Adrián Lattes (haztecaso) (*Spanish*) +- Mordi Sacks (MordiSacks) (*Hebrew*) +- Trinsec (*Dutch*) +- Tigran's Tips (tigrank08) (*Armenian*) - TracyJacks (*Chinese Simplified*) +- Szabolcs Gál (galszabolcs810624) (*Hungarian*) +- Vladislav Săcrieriu (vladislavs14) (*Romanian*) +- danreznik (*Hebrew*) - rasheedgm (*Kannada*) -- GatoOscuro (*Spanish*) -- mecqor labi (mecqorlabi) (*Persian*) -- Belkacem Mohammed (belkacem77) (*Kabyle*) -- Navjot Singh (nspeaks) (*Hindi*) - omquylzu (*Latvian*) +- c6ristian (*German*) +- Belkacem Mohammed (belkacem77) (*Kabyle*) +- lexxai (*Ukrainian*) +- Navjot Singh (nspeaks) (*Hindi*) - Ozai (*German*) - Sahak Petrosyan (petrosyan) (*Armenian*) -- siamano (*Thai, Esperanto*) +- Oymate (*Bengali*) - Viorel-Cătălin Răpițeanu (rapiteanu) (*Romanian*) +- siamano (*Thai, Esperanto*) - Siddhartha Sarathi Basu (quinoa_biryani) (*Bengali*) - Pachara Chantawong (pachara2202) (*Thai*) -- mkljczk (*Polish*) -- Skew (noan.perrot) (*French*) - Zijian Zhao (jobs2512821228) (*Chinese Simplified*) -- turtle836 (*German*) +- Skew (noan.perrot) (*French*) +- mkljczk (*Polish*) - Guru Prasath Anandapadmanaban (guruprasath) (*Tamil*) -- Lamin (laminne) (*Japanese*) +- turtle836 (*German*) - Marcepanek_ (thekingmarcepan) (*Polish*) -- Feruz Oripov (FeruzOripov) (*Russian*) +- Lamin (laminne) (*Japanese*) - Yann Aguettaz (yann-a) (*French*) +- Feruz Oripov (FeruzOripov) (*Russian*) +- serapolis (*Chinese Simplified, Chinese Traditional*) - Mick Onio (xgc.redes) (*Asturian*) -- Tianqi Zhang (tina.zhang040609) (*Chinese Simplified*) - Malik Mann (dermalikmann) (*German*) - dadosch (*German*) - r3dsp1 (*Chinese Traditional, Hong Kong*) -- padulafacundo (*Spanish*) - hg6 (*Hindi*) +- Tianqi Zhang (tina.zhang040609) (*Chinese Simplified*) +- padulafacundo (*Spanish*) +- johannes hove-henriksen (J0hsHH) (*Norwegian*) - Orlando Murcio (Atos20) (*Spanish, Mexico*) +- Padraic Calpin (padraic-padraic) (*Slovenian*) +- cenegd (*Chinese Simplified*) - piupiupiudiu (*Chinese Simplified*) - shdy (*German*) -- Padraic Calpin (padraic-padraic) (*Slovenian*) - Ильзира Рахматуллина (rahmatullinailzira53) (*Tatar*) -- cenegd (*Chinese Simplified*) - Hugh Liu (youloveonlymeh) (*Chinese Simplified*) - Pixelcode (realpixelcode) (*German*) - Yogesh K S (yogi) (*Kannada*) +- Adithya K (adithyak04) (*Malayalam*) +- Dennis Reimund (reimunddennis7) (*German*) - Rakino (rakino) (*Chinese Simplified*) -- Miquel Sabaté Solà (mssola) (*Catalan*) +- Michał Sidor (michcioperz) (*Polish*) - AmazighNM (*Kabyle*) +- Miquel Sabaté Solà (mssola) (*Catalan*) - Jothipazhani Nagarajan (jothipazhani.n) (*Tamil*) -- Clash Clans (KURD12345) (*Sorani (Kurdish)*) - hallomaurits (*Dutch*) - alnd hezh (alndhezh) (*Sorani (Kurdish)*) +- Clash Clans (KURD12345) (*Sorani (Kurdish)*) - Solid Rhino (SolidRhino) (*Dutch*) -- k_taka (peaceroad) (*Japanese*) -- Hallo Abdullah (hallo_hamza12) (*Sorani (Kurdish)*) -- hussama (*Portuguese, Brazilian*) -- Sébastien Feugère (smonff) (*French*) +- Metehan Özyürek (MetehanOzyurek) (*Turkish*) - 林水溶 (shuiRong) (*Chinese Simplified*) -- eichkat3r (*German*) -- OminousCry (*Russian*) -- SnDer (*Dutch*) +- Sébastien Feugère (smonff) (*French*) +- Y.Yamashiro (uist1idrju3i) (*Japanese*) +- Takeshi Umeda (noellabo) (*Japanese*) +- k_taka (peaceroad) (*Japanese*) +- hussama (*Portuguese, Brazilian*) +- Hallo Abdullah (hallo_hamza12) (*Sorani (Kurdish)*) +- Ashok314 (ashok314) (*Hindi*) - PifyZ (*French*) +- OminousCry (*Russian*) +- Robert Yano (throwcalmbobaway) (*Spanish, Mexico*) - Tom_ (*Czech*) - Tagada (Tagadda) (*French*) - shafouz (*Portuguese, Brazilian*) +- Yasin İsa YILDIRIM (redsfyre) (*Turkish*) +- eichkat3r (*German*) +- SnDer (*Dutch*) - Kahina Mess (K_hina) (*Kabyle*) -- Nathaël Noguès (NatNgs) (*French*) -- Kk (kishorkumara3) (*Kannada*) - Swati Sani (swatisani) (*Urdu (Pakistan)*) -- Shrinivasan T (tshrinivasan) (*Tamil*) -- さっかりんにーさん (saccharin23) (*Japanese*) -- 夜楓Yoka (Yoka2627) (*Chinese Simplified*) +- Kk (kishorkumara3) (*Kannada*) - Daniel M. (daniconil) (*Catalan*) +- Shrinivasan T (tshrinivasan) (*Tamil*) +- 夜楓Yoka (Yoka2627) (*Chinese Simplified*) +- Nathaël Noguès (NatNgs) (*French*) +- さっかりんにーさん (saccharin23) (*Japanese*) +- Rex_sa (rex07) (*Arabic*) +- Robin van der Vliet (RobinvanderVliet) (*Esperanto*) - Vikatakavi (*Kannada*) -- SusVersiva (*Catalan*) - Tradjincal (tradjincal) (*French*) - pullopen (*Chinese Simplified*) -- Robin van der Vliet (RobinvanderVliet) (*Esperanto*) +- SusVersiva (*Catalan*) +- Marvin (magicmarvman) (*German*) - Zinkokooo (*Basque*) -- mmokhi (*Persian*) - Livingston Samuel (livingston) (*Tamil*) -- prabhjot (*Hindi*) -- sergioaraujo1 (*Portuguese, Brazilian*) - CyberAmoeba (pseudoobscura) (*Chinese Simplified*) - tsundoker (*Malayalam*) +- eorn (*Breton*) +- prabhjot (*Hindi*) +- mmokhi (*Persian*) +- sergioaraujo1 (*Portuguese, Brazilian*) +- Entelekheia-ousia (*Chinese Simplified*) +- Pierre Morvan (Iriep) (*Breton*) +- oscfd (*Spanish*) - skaaarrr (*German*) -- Ricardo Colin (rysard) (*Spanish*) - mkljczk (mykylyjczyk) (*Polish*) -- Philipp Fischbeck (PFischbeck) (*German*) - fedot (*Russian*) - Paz Galindo (paz.almendra.g) (*Spanish*) -- GaggiX (*Italian*) -- ralozkolya (*Georgian*) +- Ricardo Colin (rysard) (*Spanish*) +- Philipp Fischbeck (PFischbeck) (*German*) - Zoé Bőle (zoe1337) (*German*) +- EzigboOmenana (*Cornish*) +- GaggiX (*Italian*) - Lukas Fülling (lfuelling) (*German*) - JackXu (Merman-Jack) (*Chinese Simplified*) -- Aymeric (AymBroussier) (*French*) +- ralozkolya (*Georgian*) +- Apple (blackteaovo) (*Chinese Simplified*) +- asala4544 (*Basque*) +- Xurxo Guerra (xguerrap) (*Galician*) +- qwerty287 (*German*) - Anoop (anoopp) (*Malayalam*) - pezcurrel (*Italian*) +- Samir Tighzert (samir_t7) (*Kabyle*) - Dremski (*Bulgarian*) -- Xurxo Guerra (xguerrap) (*Galician*) +- Dennis Reimund (reimund_dennis) (*German*) +- ru_mactunnag (*Scottish Gaelic*) +- Nocta (*French*) +- Aymeric (AymBroussier) (*French*) - mashirozx (*Chinese Simplified*) - Albatroz Jeremias (albjeremias) (*Portuguese*) -- Samir Tighzert (samir_t7) (*Kabyle*) -- Apple (blackteaovo) (*Chinese Simplified*) -- Nocta (*French*) -- OpenAlgeria (*Arabic*) -- tamaina (*Japanese*) -- abidin toumi (Zet24) (*Arabic*) -- xpac1985 (xpac) (*German*) -- Kaede (kaedech) (*Japanese*) -- ÀŘǾŚ PÀŚĦÀÍ (arospashai) (*Sorani (Kurdish)*) - Matias Lavik (matiaslavik) (*Norwegian Nynorsk*) -- smedvedev (*Russian*) -- mikel (mikelalas) (*Spanish*) -- Doug (douglasalvespe) (*Portuguese, Brazilian*) -- Trond Boksasp (boksasp) (*Norwegian*) -- Fleva (*Sardinian*) -- Mohammad Adnan Mahmood (adnanmig) (*Arabic*) -- Sais Lakshmanan (Saislakshmanan) (*Tamil*) - Amith Raj Shetty (amithraj1989) (*Kannada*) +- abidin toumi (Zet24) (*Arabic*) +- mikel (mikelalas) (*Spanish*) +- OpenAlgeria (*Arabic*) - random_person (*Spanish*) -- djoerd (*Dutch*) -- Baban Abdulrahman (baban.abdulrehman) (*Sorani (Kurdish)*) -- ebrezhoneg (*Breton*) -- dashty (*Sorani (Kurdish)*) -- Salh_haji6 (*Sorani (Kurdish)*) -- Amir Kurdo (kuraking202) (*Sorani (Kurdish)*) -- おさ (osapon) (*Japanese*) -- Ranj A Abdulqadir (RanjAhmed) (*Sorani (Kurdish)*) -- umonaca (*Chinese Simplified*) -- Bartek Fijałkowski (brateq) (*Polish*) -- tateisu (*Japanese*) -- centumix (*Japanese*) -- Jari Ronkainen (ronchaine) (*Finnish*) -- Savarín Electrográfico Marmota Intergalactica (herrero.maty) (*Spanish*) -- Torsten Högel (torstenhoegel) (*German*) +- Sais Lakshmanan (Saislakshmanan) (*Tamil*) +- Trond Boksasp (boksasp) (*Norwegian*) +- xpac1985 (xpac) (*German*) +- Zlr- (cZeler) (*French*) +- Mohammad Adnan Mahmood (adnanmig) (*Arabic*) +- mimikun (*Japanese*) +- smedvedev (*Russian*) +- asretro (*Chinese Traditional, Hong Kong*) +- tamaina (*Japanese*) +- Aman Alam (aalam) (*Punjabi*) +- ÀŘǾŚ PÀŚĦÀÍ (arospashai) (*Sorani (Kurdish)*) +- Kaede (kaedech) (*Japanese*) +- Doug (douglasalvespe) (*Portuguese, Brazilian*) +- Fleva (*Sardinian*) - Abijeet Patro (Abijeet) (*Basque*) -- Ács Zoltán (acszoltan111) (*Hungarian*) -- Benjamin Cobb (benjamincobb) (*German*) -- waweic (*German*) -- Aries (orlea) (*Japanese*) -- silverscat_3 (SilversCat) (*Japanese*) -- kavitha129 (*Tamil*) -- dcapillae (*Spanish*) - SamOak (*Portuguese, Brazilian*) -- capiscuas (*Spanish*) +- Aries (orlea) (*Japanese*) +- Bartek Fijałkowski (brateq) (*Polish*) - NeverMine17 (*Russian*) -- Nithya Mary (nithyamary25) (*Tamil*) -- t_aus_m (*German*) +- Brodi (brodi1) (*Dutch*) +- Ács Zoltán (zoli111) (*Hungarian*) +- capiscuas (*Spanish*) +- Benjamin Cobb (benjamincobb) (*German*) +- djoerd (*Dutch*) +- waweic (*German*) +- Amir Kurdo (kuraking202) (*Sorani (Kurdish)*) - dobrado (*Portuguese, Brazilian*) +- Baban Abdulrahman (baban.abdulrehman) (*Sorani (Kurdish)*) +- dcapillae (*Spanish*) +- Azad ahmad (dashty) (*Sorani (Kurdish)*) +- Salh_haji6 (*Sorani (Kurdish)*) +- Ranj A Abdulqadir (RanjAhmed) (*Sorani (Kurdish)*) +- tateisu (*Japanese*) +- Savarín Electrográfico Marmota Intergalactica (herrero.maty) (*Spanish*) +- ebrezhoneg (*Breton*) +- 于晚霞 (xissshawww) (*Chinese Simplified*) +- silverscat_3 (SilversCat) (*Japanese*) +- centumix (*Japanese*) +- umonaca (*Chinese Simplified*) +- Ni Futchi (futchitwo) (*Japanese*) +- おさ (osapon) (*Japanese*) +- kavitha129 (*Tamil*) - Hannah (Aniqueper1) (*Chinese Simplified*) - Jiniux (*Italian*) -- 于晚霞 (xissshawww) (*Chinese Simplified*) +- Jari Ronkainen (ronchaine) (*Finnish*) +- Nithya Mary (nithyamary25) (*Tamil*) diff --git a/Aptfile b/Aptfile index 419d159ef..b2cbad714 100644 --- a/Aptfile +++ b/Aptfile @@ -22,7 +22,7 @@ libpixman-1-0 librsvg2-2 libthai-data libthai0 -libvpx5 +libvpx[5-9] libxcb-render0 libxcb-shm0 libxrender1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d749c255..12d1563c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,151 @@ Changelog All notable changes to this project will be documented in this file. +## [3.4.0] - 2021-05-16 +### Added + +- **Add follow recommendations for onboarding** ([Gargron](https://github.com/tootsuite/mastodon/pull/15945), [Gargron](https://github.com/tootsuite/mastodon/pull/16161), [Gargron](https://github.com/tootsuite/mastodon/pull/16060), [Gargron](https://github.com/tootsuite/mastodon/pull/16077), [Gargron](https://github.com/tootsuite/mastodon/pull/16078), [Gargron](https://github.com/tootsuite/mastodon/pull/16160), [Gargron](https://github.com/tootsuite/mastodon/pull/16079), [noellabo](https://github.com/tootsuite/mastodon/pull/16044), [noellabo](https://github.com/tootsuite/mastodon/pull/16045), [Gargron](https://github.com/tootsuite/mastodon/pull/16152), [Gargron](https://github.com/tootsuite/mastodon/pull/16153), [ClearlyClaire](https://github.com/tootsuite/mastodon/pull/16082), [ClearlyClaire](https://github.com/tootsuite/mastodon/pull/16173), [ClearlyClaire](https://github.com/tootsuite/mastodon/pull/16159), [ClearlyClaire](https://github.com/tootsuite/mastodon/pull/16189)) + - Tutorial on first web UI launch has been replaced with follow suggestions + - Follow suggestions take user locale into account and are a mix of accounts most followed by currently active local users, and accounts that wrote the most shared/favourited posts in the last 30 days + - Only accounts that have opted-in to being discoverable from their profile settings, and that do not require follow requests, will be suggested + - Moderators can review suggestions for every supported locale and suppress specific suggestions from appearing and admins can ensure certain accounts always show up in suggestions from the settings area + - New users no longer automatically follow admins +- **Add server rules** ([Gargron](https://github.com/tootsuite/mastodon/pull/15769), [ClearlyClaire](https://github.com/tootsuite/mastodon/pull/15778)) + - Admins can create and edit itemized server rules + - They are available through the REST API and on the about page +- **Add canonical e-mail blocks for suspended accounts** ([Gargron](https://github.com/tootsuite/mastodon/pull/16049)) + - Normally, people can make multiple accounts using the same e-mail address using the `+` trick or by inserting or removing `.` characters from the first part of their address + - Once an account is suspended, it will no longer be possible for the e-mail address used by that account to be used for new sign-ups in any of its forms +- Add management of delivery availability in admin UI ([noellabo](https://github.com/tootsuite/mastodon/pull/15771)) +- **Add system checks to dashboard in admin UI** ([Gargron](https://github.com/tootsuite/mastodon/pull/15989), [ClearlyClaire](https://github.com/tootsuite/mastodon/pull/15954), [ClearlyClaire](https://github.com/tootsuite/mastodon/pull/16002)) + - The dashboard will now warn you if you some Sidekiq queues are not being processed, if you have not defined any server rules, or if you forgot to run database migrations from the latest Mastodon upgrade +- Add inline description of moderation actions in admin UI ([ClearlyClaire](https://github.com/tootsuite/mastodon/pull/15792)) +- Add "recommended" label to activity/peers API toggles in admin UI ([Gargron](https://github.com/tootsuite/mastodon/pull/16081)) +- Add joined date to profiles in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/16169), [rinsuki](https://github.com/tootsuite/mastodon/pull/16186)) +- Add transition to media modal background in web UI ([mkljczk](https://github.com/tootsuite/mastodon/pull/15843)) +- Add option to opt-out of unread notification markers in web UI ([ClearlyClaire](https://github.com/tootsuite/mastodon/pull/15842)) +- Add borders to 📱, 🚲, and 📲 emojis in web UI ([ClearlyClaire](https://github.com/tootsuite/mastodon/pull/15794), [ClearlyClaire](https://github.com/tootsuite/mastodon/pull/16035)) +- Add dropdown for boost privacy in boost confirmation modal in web UI ([ClearlyClaire](https://github.com/tootsuite/mastodon/pull/15704)) +- Add support for Ruby 3.0 ([ClearlyClaire](https://github.com/tootsuite/mastodon/pull/16046), [ClearlyClaire](https://github.com/tootsuite/mastodon/pull/16174)) +- Add `Message-ID` header to outgoing emails ([ClearlyClaire](https://github.com/tootsuite/mastodon/pull/16076)) + - Some e-mail spam filters penalize e-mails that have a `Message-ID` header that uses a different domain name than the sending e-mail address. Now, the same domain will be used +- Add `af`, `gd` and `si` locales ([Gargron](https://github.com/tootsuite/mastodon/pull/16090)) +- Add guard against DNS rebinding attacks ([noellabo](https://github.com/tootsuite/mastodon/pull/16087), [noellabo](https://github.com/tootsuite/mastodon/pull/16095)) +- Add HTTP header to explicitly opt-out of FLoC by default ([ClearlyClaire](https://github.com/tootsuite/mastodon/pull/16036)) +- Add missing push notification title for polls and statuses ([ClearlyClaire](https://github.com/tootsuite/mastodon/pull/15929), [mkljczk](https://github.com/tootsuite/mastodon/pull/15564), [ClearlyClaire](https://github.com/tootsuite/mastodon/pull/15931)) +- Add `POST /api/v1/emails/confirmations` to REST API ([Gargron](https://github.com/tootsuite/mastodon/pull/15816), [Gargron](https://github.com/tootsuite/mastodon/pull/15949)) + - This method allows an app through which a user signed-up to request a new confirmation e-mail to be sent, or to change the e-mail of the account before it is confirmed +- Add `GET /api/v1/accounts/lookup` to REST API ([Gargron](https://github.com/tootsuite/mastodon/pull/15740), [ClearlyClaire](https://github.com/tootsuite/mastodon/pull/15750)) + - This method allows to quickly convert a username of a known account to an ID that can be used with the REST API, or to check if a username is available + for sign-up +- Add `policy` param to `POST /api/v1/push/subscriptions` in REST API ([Gargron](https://github.com/tootsuite/mastodon/pull/16040)) + - This param allows an app to control from whom notifications should be delivered as push notifications to the app +- Add `details` to error response for `POST /api/v1/accounts` in REST API ([Gargron](https://github.com/tootsuite/mastodon/pull/15803)) + - This attribute allows an app to display more helpful information to the user about why the sign-up did not succeed +- Add `SIDEKIQ_REDIS_URL` and related environment variables to optionally use a separate Redis server for Sidekiq ([noellabo](https://github.com/tootsuite/mastodon/pull/16188)) + +### Changed + +- Change trending hashtags to be affected be reblogs ([Gargron](https://github.com/tootsuite/mastodon/pull/16164)) + - Previously, only original posts contributed to a hashtag's trending score + - Now, reblogs of posts will also contribute to that hashtag's trending score +- Change e-mail confirmation link to always redirect to web UI ([ClearlyClaire](https://github.com/tootsuite/mastodon/pull/16151)) +- Change log level of worker lifecycle to WARN in streaming API ([Gargron](https://github.com/tootsuite/mastodon/pull/16110)) + - Since running with INFO log level in production is not always desirable, it is easy to miss when a worker is shutdown and a new one is started +- Change the nouns "toot" and "status" to "post" in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/16080), [Gargron](https://github.com/tootsuite/mastodon/pull/16089)) + - To be clear, the button still says "Toot!" +- Change order of dropdown menu on posts to be more intuitive in web UI ([ariasuni](https://github.com/tootsuite/mastodon/pull/15647)) +- Change description of keyboard shortcuts in web UI ([ariasuni](https://github.com/tootsuite/mastodon/pull/16129)) +- Change option labels on edit profile page ([Gargron](https://github.com/tootsuite/mastodon/pull/16041)) + - "Lock account" is now "Require follow requests" + - "List this account on the directory" is now "Suggest account to others" + - "Hide your network" is now "Hide your social graph" +- Change newly generated account IDs to not be enumerable ([ClearlyClaire](https://github.com/tootsuite/mastodon/pull/15844)) +- Change Web Push API deliveries to use request pooling ([Gargron](https://github.com/tootsuite/mastodon/pull/16014)) +- Change multiple mentions with same username to render with domain ([Gargron](https://github.com/tootsuite/mastodon/pull/15718), [noellabo](https://github.com/tootsuite/mastodon/pull/16038)) + - When a post contains mentions of two or more users who have the same username, but on different domains, render their names with domain to help disambiguate them + - Always render the domain of usernames used in profile metadata +- Change health check endpoint to reveal less information ([Gargron](https://github.com/tootsuite/mastodon/pull/15988)) +- Change account counters to use upsert (requires Postgres >= 9.5) ([ClearlyClaire](https://github.com/tootsuite/mastodon/pull/15913)) +- Change `mastodon:setup` to not call `assets:precompile` in Docker ([ClearlyClaire](https://github.com/tootsuite/mastodon/pull/13942)) +- **Change max. image dimensions to 1920x1080px (1080p)** ([Gargron](https://github.com/tootsuite/mastodon/pull/15690)) + - Previously, this was 1280x1280px + - This is the amount of pixels that original images get downsized to +- Change custom emoji to be animated when hovering container in web UI ([ClearlyClaire](https://github.com/tootsuite/mastodon/pull/15637)) +- Change streaming API from deprecated ClusterWS/cws to ws ([ClearlyClaire](https://github.com/tootsuite/mastodon/pull/15932)) +- Change systemd configuration to add sandboxing features ([Izorkin](https://github.com/tootsuite/mastodon/pull/15937), [Izorkin](https://github.com/tootsuite/mastodon/pull/16103), [Izorkin](https://github.com/tootsuite/mastodon/pull/16127)) +- Change nginx configuration to make running Onion service easier ([cohosh](https://github.com/tootsuite/mastodon/pull/15498)) +- Change Helm configuration ([dunn](https://github.com/tootsuite/mastodon/pull/15722), [dunn](https://github.com/tootsuite/mastodon/pull/15728), [dunn](https://github.com/tootsuite/mastodon/pull/15748), [dunn](https://github.com/tootsuite/mastodon/pull/15749), [dunn](https://github.com/tootsuite/mastodon/pull/15767)) +- Change Docker configuration ([SuperSandro2000](https://github.com/tootsuite/mastodon/pull/10823), [mashirozx](https://github.com/tootsuite/mastodon/pull/15978)) + +### Removed + +- Remove PubSubHubbub-related columns from accounts table ([Gargron](https://github.com/tootsuite/mastodon/pull/16170), [ClearlyClaire](https://github.com/tootsuite/mastodon/pull/15857)) +- Remove dependency on @babel/plugin-proposal-class-properties ([ykzts](https://github.com/tootsuite/mastodon/pull/16155)) +- Remove dependency on pluck_each gem ([Gargron](https://github.com/tootsuite/mastodon/pull/16012)) +- Remove spam check and dependency on nilsimsa gem ([Gargron](https://github.com/tootsuite/mastodon/pull/16011)) +- Remove MySQL-specific code from Mastodon::MigrationHelpers ([ClearlyClaire](https://github.com/tootsuite/mastodon/pull/15924)) +- Remove IE11 from supported browsers target ([gol-cha](https://github.com/tootsuite/mastodon/pull/15779)) + +### Fixed + +- Fix "You might be interested in" flashing while searching in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/16162)) +- Fix display of posts without text content in web UI ([ClearlyClaire](https://github.com/tootsuite/mastodon/pull/15665)) +- Fix Google Translate breaking web UI ([ClearlyClaire](https://github.com/tootsuite/mastodon/pull/15610), [ClearlyClaire](https://github.com/tootsuite/mastodon/pull/15611)) +- Fix web UI crashing when SVG support is disabled ([ClearlyClaire](https://github.com/tootsuite/mastodon/pull/15809)) +- Fix web UI crash when a status opened in the media modal is deleted ([kaias1jp](https://github.com/tootsuite/mastodon/pull/15701)) +- Fix OCR language data failing to load in web UI ([ClearlyClaire](https://github.com/tootsuite/mastodon/pull/15519)) +- Fix footer links not being clickable in Safari in web UI ([noellabo](https://github.com/tootsuite/mastodon/pull/15496)) +- Fix autofocus/autoselection not working on mobile in web UI ([ClearlyClaire](https://github.com/tootsuite/mastodon/pull/15555), [ClearlyClaire](https://github.com/tootsuite/mastodon/pull/15985)) +- Fix media redownload worker retrying on unexpected response codes ([Gargron](https://github.com/tootsuite/mastodon/pull/16111)) +- Fix thread resolve worker retrying when status no longer exists ([Gargron](https://github.com/tootsuite/mastodon/pull/16109)) +- Fix n+1 queries when rendering statuses in REST API ([abcang](https://github.com/tootsuite/mastodon/pull/15641)) +- Fix n+1 queries when rendering notifications in REST API ([abcang](https://github.com/tootsuite/mastodon/pull/15640)) +- Fix delete of local reply to local parent not being forwarded ([Gargron](https://github.com/tootsuite/mastodon/pull/16096)) +- Fix remote reporters not receiving suspend/unsuspend activities ([Gargron](https://github.com/tootsuite/mastodon/pull/16050)) +- Fix understanding (not fully qualified) `as:Public` and `Public` ([ClearlyClaire](https://github.com/tootsuite/mastodon/pull/15948)) +- Fix actor update not being distributed on profile picture deletion ([ClearlyClaire](https://github.com/tootsuite/mastodon/pull/15461)) +- Fix processing of incoming Delete activities ([ClearlyClaire](https://github.com/tootsuite/mastodon/pull/16084)) +- Fix processing of incoming Block activities ([ClearlyClaire](https://github.com/tootsuite/mastodon/pull/15546)) +- Fix processing of incoming Update activities of unknown accounts ([ClearlyClaire](https://github.com/tootsuite/mastodon/pull/15514)) +- Fix URIs of repeat follow requests not being recorded ([ClearlyClaire](https://github.com/tootsuite/mastodon/pull/15662)) +- Fix error on requests with no `Digest` header ([ClearlyClaire](https://github.com/tootsuite/mastodon/pull/15782)) +- Fix activity object not requiring signature in secure mode ([ClearlyClaire](https://github.com/tootsuite/mastodon/pull/15592)) +- Fix database serialization failure returning HTTP 500 ([Gargron](https://github.com/tootsuite/mastodon/pull/16101)) +- Fix media processing getting stuck on too much stdin/stderr ([Gargron](https://github.com/tootsuite/mastodon/pull/16136)) +- Fix some inefficient array manipulations ([007lva](https://github.com/tootsuite/mastodon/pull/15513), [007lva](https://github.com/tootsuite/mastodon/pull/15527)) +- Fix some inefficient regex matching ([007lva](https://github.com/tootsuite/mastodon/pull/15528)) +- Fix some inefficient SQL queries ([abcang](https://github.com/tootsuite/mastodon/pull/16104), [abcang](https://github.com/tootsuite/mastodon/pull/16106), [abcang](https://github.com/tootsuite/mastodon/pull/16105)) +- Fix trying to fetch key from empty URI when verifying HTTP signature ([Gargron](https://github.com/tootsuite/mastodon/pull/16100)) +- Fix `tootctl maintenance fix-duplicates` failures ([ClearlyClaire](https://github.com/tootsuite/mastodon/pull/15923), [ClearlyClaire](https://github.com/tootsuite/mastodon/pull/15515)) +- Fix error when removing status caused by race condition ([Gargron](https://github.com/tootsuite/mastodon/pull/16099)) +- Fix blocking someone not clearing up list feeds ([ClearlyClaire](https://github.com/tootsuite/mastodon/pull/16205)) +- Fix misspelled URLs character counting ([ClearlyClaire](https://github.com/tootsuite/mastodon/pull/15382)) +- Fix Sidekiq hanging forever due to a Resolv bug in Ruby 2.7.3 ([ClearlyClaire](https://github.com/tootsuite/mastodon/pull/16157)) +- Fix edge case where follow limit interferes with accepting a follow ([ClearlyClaire](https://github.com/tootsuite/mastodon/pull/16098)) +- Fix inconsistent lead text style in admin UI ([Gargron](https://github.com/tootsuite/mastodon/pull/16052), [ClearlyClaire](https://github.com/tootsuite/mastodon/pull/16086)) +- Fix reports of already suspended accounts being recorded ([Gargron](https://github.com/tootsuite/mastodon/pull/16047)) +- Fix sign-up restrictions based on IP addresses not being enforced ([ClearlyClaire](https://github.com/tootsuite/mastodon/pull/15607)) +- Fix YouTube embeds failing due to YouTube serving wrong OEmbed URLs ([Gargron](https://github.com/tootsuite/mastodon/pull/15716)) +- Fix error when rendering public pages with media without meta ([Gargron](https://github.com/tootsuite/mastodon/pull/16112)) +- Fix misaligned logo on follow button on public pages ([noellabo](https://github.com/tootsuite/mastodon/pull/15458)) +- Fix video modal not working on public pages ([noellabo](https://github.com/tootsuite/mastodon/pull/15469)) +- Fix race conditions on account migration creation ([ClearlyClaire](https://github.com/tootsuite/mastodon/pull/15597)) +- Fix not being able to change world filter expiration back to “Never” ([ClearlyClaire](https://github.com/tootsuite/mastodon/pull/15858)) +- Fix `.env.vagrant` not setting `RAILS_ENV` variable ([chandrn7](https://github.com/tootsuite/mastodon/pull/15709)) +- Fix error when muting users with `duration` in REST API ([Tak](https://github.com/tootsuite/mastodon/pull/15516)) +- Fix border padding on front page in light theme ([ClearlyClaire](https://github.com/tootsuite/mastodon/pull/15926)) +- Fix wrong URL to custom CSS when `CDN_HOST` is used ([ClearlyClaire](https://github.com/tootsuite/mastodon/pull/15927)) +- Fix `tootctl accounts unfollow` ([ClearlyClaire](https://github.com/tootsuite/mastodon/pull/15639)) +- Fix `tootctl emoji import` wasting time on MacOS shadow files ([cortices](https://github.com/tootsuite/mastodon/pull/15430)) +- Fix `tootctl emoji import` not treating shortcodes as case-insensitive ([angristan](https://github.com/tootsuite/mastodon/pull/15738)) +- Fix some issues with SAML account creation ([Gargron](https://github.com/tootsuite/mastodon/pull/15222), [kaiyou](https://github.com/tootsuite/mastodon/pull/15511)) +- Fix MX validation applying for explicitly allowed e-mail domains ([ClearlyClaire](https://github.com/tootsuite/mastodon/pull/15930)) +- Fix share page not using configured custom mascot ([tribela](https://github.com/tootsuite/mastodon/pull/15687)) +- Fix instance actor not being automatically created if it wasn't seeded properly ([ClearlyClaire](https://github.com/tootsuite/mastodon/pull/15693)) +- Fix HTTPS enforcement preventing Mastodon from being run as an Onion service ([cohosh](https://github.com/tootsuite/mastodon/pull/15560), [jtracey](https://github.com/tootsuite/mastodon/pull/15741), [ClearlyClaire](https://github.com/tootsuite/mastodon/pull/15712), [cohosh](https://github.com/tootsuite/mastodon/pull/15725)) +- Fix app name, website and redirect URIs not having a maximum length ([Gargron](https://github.com/tootsuite/mastodon/pull/16042)) + ## [3.3.0] - 2020-12-27 ### Added diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f7b8f17cc..0d563559b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,9 +24,17 @@ You can submit translations via [Crowdin](https://crowdin.com/project/mastodon). ## Pull requests -Please use clean, concise titles for your pull requests. We use commit squashing, so the final commit in the master branch will carry the title of the pull request. +**Please use clean, concise titles for your pull requests.** Unless the pull request is about refactoring code, updating dependencies or other internal tasks, assume that the person reading the pull request title is not a programmer or Mastodon developer, but instead a Mastodon user or server administrator, and **try to describe your change or fix from their perspective**. We use commit squashing, so the final commit in the main branch will carry the title of the pull request, and commits from the main branch are fed into the changelog. The changelog is separated into [keepachangelog.com categories](https://keepachangelog.com/en/1.0.0/), and while that spec does not prescribe how the entries ought to be named, for easier sorting, start your pull request titles using one of the verbs "Add", "Change", "Deprecate", "Remove", or "Fix" (present tense). -The smaller the set of changes in the pull request is, the quicker it can be reviewed and merged. Splitting tasks into multiple smaller pull requests is often preferable. +Example: + +|Not ideal|Better| +|---|----| +|Fixed NoMethodError in RemovalWorker|Fix nil error when removing statuses caused by race condition| + +It is not always possible to phrase every change in such a manner, but it is desired. + +**The smaller the set of changes in the pull request is, the quicker it can be reviewed and merged.** Splitting tasks into multiple smaller pull requests is often preferable. **Pull requests that do not pass automated checks may not be reviewed**. In particular, you need to keep in mind: @@ -36,4 +44,4 @@ The smaller the set of changes in the pull request is, the quicker it can be rev ## Documentation -The [Mastodon documentation](https://docs.joinmastodon.org) is a statically generated site. You can [submit merge requests to mastodon/docs](https://source.joinmastodon.org/mastodon/docs). +The [Mastodon documentation](https://docs.joinmastodon.org) is a statically generated site. You can [submit merge requests to tootsuite/documentation](https://github.com/tootsuite/documentation). diff --git a/Dockerfile b/Dockerfile index 95d45bab4..962e5a8c9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,10 +1,10 @@ FROM ubuntu:20.04 as build-dep # Use bash for the shell -SHELL ["bash", "-c"] +SHELL ["/bin/bash", "-c"] # Install Node v12 (LTS) -ENV NODE_VER="12.20.0" +ENV NODE_VER="12.21.0" RUN ARCH= && \ dpkgArch="$(dpkg --print-architecture)" && \ case "${dpkgArch##*-}" in \ @@ -17,35 +17,19 @@ RUN ARCH= && \ *) echo "unsupported architecture"; exit 1 ;; \ esac && \ echo "Etc/UTC" > /etc/localtime && \ - apt update && \ - apt -y install wget python && \ + apt-get update && \ + apt-get install -y --no-install-recommends ca-certificates wget python && \ cd ~ && \ - wget https://nodejs.org/download/release/v$NODE_VER/node-v$NODE_VER-linux-$ARCH.tar.gz && \ + wget -q https://nodejs.org/download/release/v$NODE_VER/node-v$NODE_VER-linux-$ARCH.tar.gz && \ tar xf node-v$NODE_VER-linux-$ARCH.tar.gz && \ rm node-v$NODE_VER-linux-$ARCH.tar.gz && \ mv node-v$NODE_VER-linux-$ARCH /opt/node -# Install jemalloc -ENV JE_VER="5.2.1" -RUN apt update && \ - apt -y install make autoconf gcc g++ && \ - cd ~ && \ - wget https://github.com/jemalloc/jemalloc/archive/$JE_VER.tar.gz && \ - tar xf $JE_VER.tar.gz && \ - cd jemalloc-$JE_VER && \ - ./autogen.sh && \ - ./configure --prefix=/opt/jemalloc && \ - make -j$(nproc) > /dev/null && \ - make install_bin install_include install_lib && \ - cd .. && rm -rf jemalloc-$JE_VER $JE_VER.tar.gz - # Install Ruby ENV RUBY_VER="2.7.2" -ENV CPPFLAGS="-I/opt/jemalloc/include" -ENV LDFLAGS="-L/opt/jemalloc/lib/" -RUN apt update && \ - apt -y install build-essential \ - bison libyaml-dev libgdbm-dev libreadline-dev \ +RUN apt-get update && \ + apt-get install -y --no-install-recommends build-essential \ + bison libyaml-dev libgdbm-dev libreadline-dev libjemalloc-dev \ libncurses5-dev libffi-dev zlib1g-dev libssl-dev && \ cd ~ && \ wget https://cache.ruby-lang.org/pub/ruby/${RUBY_VER%.*}/ruby-$RUBY_VER.tar.gz && \ @@ -55,25 +39,24 @@ RUN apt update && \ --with-jemalloc \ --with-shared \ --disable-install-doc && \ - ln -s /opt/jemalloc/lib/* /usr/lib/ && \ - make -j$(nproc) > /dev/null && \ + make -j"$(nproc)" > /dev/null && \ make install && \ - cd .. && rm -rf ruby-$RUBY_VER.tar.gz ruby-$RUBY_VER + rm -rf ../ruby-$RUBY_VER.tar.gz ../ruby-$RUBY_VER ENV PATH="${PATH}:/opt/ruby/bin:/opt/node/bin" RUN npm install -g yarn && \ gem install bundler && \ - apt update && \ - apt -y install git libicu-dev libidn11-dev \ - libpq-dev libprotobuf-dev protobuf-compiler + apt-get update && \ + apt-get install -y --no-install-recommends git libicu-dev libidn11-dev \ + libpq-dev libprotobuf-dev protobuf-compiler shared-mime-info COPY Gemfile* package.json yarn.lock /opt/mastodon/ RUN cd /opt/mastodon && \ bundle config set deployment 'true' && \ bundle config set without 'development test' && \ - bundle install -j$(nproc) && \ + bundle install -j"$(nproc)" && \ yarn install --pure-lockfile FROM ubuntu:20.04 @@ -81,7 +64,6 @@ FROM ubuntu:20.04 # Copy over all the langs needed for runtime COPY --from=build-dep /opt/node /opt/node COPY --from=build-dep /opt/ruby /opt/ruby -COPY --from=build-dep /opt/jemalloc /opt/jemalloc # Add more PATHs to the PATH ENV PATH="${PATH}:/opt/ruby/bin:/opt/node/bin:/opt/mastodon/bin" @@ -89,35 +71,26 @@ ENV PATH="${PATH}:/opt/ruby/bin:/opt/node/bin:/opt/mastodon/bin" # Create the mastodon user ARG UID=991 ARG GID=991 -RUN apt update && \ +SHELL ["/bin/bash", "-o", "pipefail", "-c"] +RUN apt-get update && \ echo "Etc/UTC" > /etc/localtime && \ - ln -s /opt/jemalloc/lib/* /usr/lib/ && \ - apt install -y whois wget && \ + apt-get install -y --no-install-recommends whois wget && \ addgroup --gid $GID mastodon && \ useradd -m -u $UID -g $GID -d /opt/mastodon mastodon && \ - echo "mastodon:`head /dev/urandom | tr -dc A-Za-z0-9 | head -c 24 | mkpasswd -s -m sha-256`" | chpasswd + echo "mastodon:$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 24 | mkpasswd -s -m sha-256)" | chpasswd && \ + rm -rf /var/lib/apt/lists/* # Install mastodon runtime deps -RUN apt -y --no-install-recommends install \ - libssl1.1 libpq5 imagemagick ffmpeg \ +RUN apt-get update && \ + apt-get -y --no-install-recommends install \ + libssl1.1 libpq5 imagemagick ffmpeg libjemalloc2 \ libicu66 libprotobuf17 libidn11 libyaml-0-2 \ - file ca-certificates tzdata libreadline8 && \ - apt -y install gcc && \ + file ca-certificates tzdata libreadline8 gcc tini && \ ln -s /opt/mastodon /mastodon && \ gem install bundler && \ rm -rf /var/cache && \ rm -rf /var/lib/apt/lists/* -# Add tini -ENV TINI_VERSION="0.19.0" -RUN dpkgArch="$(dpkg --print-architecture)" && \ - ARCH=$dpkgArch && \ - wget https://github.com/krallin/tini/releases/download/v${TINI_VERSION}/tini-$ARCH \ - https://github.com/krallin/tini/releases/download/v${TINI_VERSION}/tini-$ARCH.sha256sum && \ - cat tini-$ARCH.sha256sum | sha256sum -c - && \ - mv tini-$ARCH /tini && rm tini-$ARCH.sha256sum && \ - chmod +x /tini - # Copy over mastodon source, and dependencies from building, and set permissions COPY --chown=mastodon:mastodon . /opt/mastodon COPY --from=build-dep --chown=mastodon:mastodon /opt/mastodon /opt/mastodon @@ -140,5 +113,5 @@ RUN cd ~ && \ # Set the work dir and the container entry point WORKDIR /opt/mastodon -ENTRYPOINT ["/tini", "--"] +ENTRYPOINT ["/usr/bin/tini", "--"] EXPOSE 3000 4000 diff --git a/Gemfile b/Gemfile index 3aee5d7cc..3a4799347 100644 --- a/Gemfile +++ b/Gemfile @@ -1,46 +1,44 @@ # frozen_string_literal: true source 'https://rubygems.org' -ruby '>= 2.5.0', '< 3.0.0' +ruby '>= 2.5.0', '< 3.1.0' gem 'pkg-config', '~> 1.4' -gem 'puma', '~> 5.0' -gem 'rails', '~> 5.2.4.4' +gem 'puma', '~> 5.3' +gem 'rails', '~> 6.1.3' gem 'sprockets', '~> 3.7.2' -gem 'thor', '~> 1.0' +gem 'thor', '~> 1.1' gem 'rack', '~> 2.2.3' gem 'hamlit-rails', '~> 0.2' gem 'pg', '~> 1.2' -gem 'makara', '~> 0.4' -gem 'pghero', '~> 2.7' +gem 'makara', '~> 0.5' +gem 'pghero', '~> 2.8' gem 'dotenv-rails', '~> 2.7' -gem 'aws-sdk-s3', '~> 1.85', require: false +gem 'aws-sdk-s3', '~> 1.94', require: false gem 'fog-core', '<= 2.1.0' gem 'fog-openstack', '~> 0.3', require: false gem 'paperclip', '~> 6.0' -gem 'paperclip-av-transcoder', '~> 0.6' -gem 'streamio-ffmpeg', '~> 3.0' gem 'blurhash', '~> 0.1' gem 'active_model_serializers', '~> 0.10' gem 'addressable', '~> 2.7' -gem 'bootsnap', '~> 1.5', require: false +gem 'bootsnap', '~> 1.6.0', require: false gem 'browser' gem 'charlock_holmes', '~> 0.7.7' gem 'iso-639' -gem 'chewy', '~> 5.1' -gem 'cld3', '~> 3.3.0' -gem 'devise', '~> 4.7' -gem 'devise-two-factor', '~> 3.1' +gem 'chewy', '~> 5.2' +gem 'cld3', '~> 3.4.2' +gem 'devise', '~> 4.8' +gem 'devise-two-factor', '~> 4.0' group :pam_authentication, optional: true do gem 'devise_pam_authenticatable2', '~> 9.2' end -gem 'net-ldap', '~> 0.16' +gem 'net-ldap', '~> 0.17' gem 'omniauth-cas', '~> 2.0' gem 'omniauth-saml', '~> 1.10' gem 'omniauth', '~> 1.9' @@ -48,13 +46,12 @@ gem 'omniauth-rails_csrf_protection', '~> 0.1' gem 'color_diff', '~> 0.1' gem 'discard', '~> 1.2' -gem 'doorkeeper', '~> 5.4' +gem 'doorkeeper', '~> 5.5' gem 'ed25519', '~> 1.2' gem 'fast_blank', '~> 1.0' gem 'fastimage' gem 'hiredis', '~> 0.6' gem 'redis-namespace', '~> 1.8' -gem 'health_check', git: 'https://github.com/ianheggie/health_check', ref: '0b799ead604f900ed50685e9b2d469cd2befba5b' gem 'htmlentities', '~> 4.3' gem 'http', '~> 4.4' gem 'http_accept_language', '~> 2.1' @@ -63,40 +60,39 @@ gem 'idn-ruby', require: 'idn' gem 'kaminari', '~> 1.2' gem 'link_header', '~> 0.0' gem 'mime-types', '~> 3.3.1', require: 'mime/types/columnar' -gem 'nilsimsa', git: 'https://github.com/witgo/nilsimsa', ref: 'fd184883048b922b176939f851338d0a4971a532' -gem 'nokogiri', '~> 1.10' +gem 'nokogiri', '~> 1.11' gem 'nsa', '~> 0.2' -gem 'oj', '~> 3.10' -gem 'ox', '~> 2.13' +gem 'oj', '~> 3.11' +gem 'ox', '~> 2.14' gem 'parslet' gem 'parallel', '~> 1.20' gem 'posix-spawn' gem 'pundit', '~> 2.1' gem 'premailer-rails' -gem 'rack-attack', '~> 6.3' +gem 'rack-attack', '~> 6.5' gem 'rack-cors', '~> 1.1', require: 'rack/cors' -gem 'rails-i18n', '~> 5.1' +gem 'rails-i18n', '~> 6.0' gem 'rails-settings-cached', '~> 0.6' gem 'redis', '~> 4.2', require: ['redis', 'redis/connection/hiredis'] gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock' -gem 'rqrcode', '~> 1.1' -gem 'ruby-progressbar', '~> 1.10' +gem 'rqrcode', '~> 2.0' +gem 'ruby-progressbar', '~> 1.11' gem 'sanitize', '~> 5.2' gem 'scenic', '~> 1.5' -gem 'sidekiq', '~> 6.1' +gem 'sidekiq', '~> 6.2' gem 'sidekiq-scheduler', '~> 3.0' -gem 'sidekiq-unique-jobs', '~> 6.0' +gem 'sidekiq-unique-jobs', '~> 7.0' gem 'sidekiq-bulk', '~>0.2.0' gem 'simple-navigation', '~> 4.1' -gem 'simple_form', '~> 5.0' +gem 'simple_form', '~> 5.1' gem 'sprockets-rails', '~> 3.2', require: 'sprockets/railtie' gem 'stoplight', '~> 2.2.1' gem 'strong_migrations', '~> 0.7' -gem 'tty-prompt', '~> 0.22', require: false -gem 'twitter-text', '~> 1.14' -gem 'tzinfo-data', '~> 1.2020' -gem 'webpacker', '~> 5.2' -gem 'webpush' +gem 'tty-prompt', '~> 0.23', require: false +gem 'twitter-text', '~> 3.1.0' +gem 'tzinfo-data', '~> 1.2021' +gem 'webpacker', '~> 5.3' +gem 'webpush', '~> 0.3' gem 'webauthn', '~> 3.0.0.alpha1' gem 'json-ld' @@ -104,12 +100,12 @@ gem 'json-ld-preloaded', '~> 3.1' gem 'rdf-normalize', '~> 0.4' group :development, :test do - gem 'fabrication', '~> 2.21' + gem 'fabrication', '~> 2.22' gem 'fuubar', '~> 2.5' gem 'i18n-tasks', '~> 0.9', require: false gem 'pry-byebug', '~> 3.9' gem 'pry-rails', '~> 0.3' - gem 'rspec-rails', '~> 4.0' + gem 'rspec-rails', '~> 5.0' end group :production, :test do @@ -117,15 +113,15 @@ group :production, :test do end group :test do - gem 'capybara', '~> 3.33' + gem 'capybara', '~> 3.35' gem 'climate_control', '~> 0.2' - gem 'faker', '~> 2.14' + gem 'faker', '~> 2.17' gem 'microformats', '~> 4.2' gem 'rails-controller-testing', '~> 1.0' gem 'rspec-sidekiq', '~> 3.1' - gem 'simplecov', '~> 0.19', require: false - gem 'webmock', '~> 3.10' - gem 'parallel_tests', '~> 3.4' + gem 'simplecov', '~> 0.21', require: false + gem 'webmock', '~> 3.12' + gem 'parallel_tests', '~> 3.7' gem 'rspec_junit_formatter', '~> 0.4' end @@ -133,17 +129,17 @@ group :development do gem 'active_record_query_trace', '~> 1.8' gem 'annotate', '~> 3.1' gem 'better_errors', '~> 2.9' - gem 'binding_of_caller', '~> 0.7' + gem 'binding_of_caller', '~> 1.0' gem 'bullet', '~> 6.1' gem 'letter_opener', '~> 1.7' gem 'letter_opener_web', '~> 1.4' gem 'memory_profiler' - gem 'rubocop', '~> 1.3', require: false - gem 'rubocop-rails', '~> 2.8', require: false - gem 'brakeman', '~> 4.10', require: false - gem 'bundler-audit', '~> 0.7', require: false + gem 'rubocop', '~> 1.14', require: false + gem 'rubocop-rails', '~> 2.10', require: false + gem 'brakeman', '~> 5.0', require: false + gem 'bundler-audit', '~> 0.8', require: false - gem 'capistrano', '~> 3.14' + gem 'capistrano', '~> 3.16' gem 'capistrano-rails', '~> 1.6' gem 'capistrano-rbenv', '~> 2.2' gem 'capistrano-yarn', '~> 2.0' @@ -153,11 +149,11 @@ end group :production do gem 'lograge', '~> 0.11' - gem 'redis-rails', '~> 5.0' end gem 'concurrent-ruby', require: false gem 'connection_pool', require: false gem 'xorcist', '~> 1.1' -gem 'pluck_each', '~> 0.1.3' + +gem 'resolv', '~> 0.1.0' diff --git a/Gemfile.lock b/Gemfile.lock index 4960f1b4d..7fb2f9c84 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,68 +1,71 @@ -GIT - remote: https://github.com/ianheggie/health_check - revision: 0b799ead604f900ed50685e9b2d469cd2befba5b - ref: 0b799ead604f900ed50685e9b2d469cd2befba5b - specs: - health_check (4.0.0.pre) - rails (>= 4.0) - -GIT - remote: https://github.com/witgo/nilsimsa - revision: fd184883048b922b176939f851338d0a4971a532 - ref: fd184883048b922b176939f851338d0a4971a532 - specs: - nilsimsa (1.1.2) - GEM remote: https://rubygems.org/ specs: - actioncable (5.2.4.4) - actionpack (= 5.2.4.4) + actioncable (6.1.3.2) + actionpack (= 6.1.3.2) + activesupport (= 6.1.3.2) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailer (5.2.4.4) - actionpack (= 5.2.4.4) - actionview (= 5.2.4.4) - activejob (= 5.2.4.4) + actionmailbox (6.1.3.2) + actionpack (= 6.1.3.2) + activejob (= 6.1.3.2) + activerecord (= 6.1.3.2) + activestorage (= 6.1.3.2) + activesupport (= 6.1.3.2) + mail (>= 2.7.1) + actionmailer (6.1.3.2) + actionpack (= 6.1.3.2) + actionview (= 6.1.3.2) + activejob (= 6.1.3.2) + activesupport (= 6.1.3.2) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (5.2.4.4) - actionview (= 5.2.4.4) - activesupport (= 5.2.4.4) - rack (~> 2.0, >= 2.0.8) + actionpack (6.1.3.2) + actionview (= 6.1.3.2) + activesupport (= 6.1.3.2) + rack (~> 2.0, >= 2.0.9) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionview (5.2.4.4) - activesupport (= 5.2.4.4) + rails-html-sanitizer (~> 1.0, >= 1.2.0) + actiontext (6.1.3.2) + actionpack (= 6.1.3.2) + activerecord (= 6.1.3.2) + activestorage (= 6.1.3.2) + activesupport (= 6.1.3.2) + nokogiri (>= 1.8.5) + actionview (6.1.3.2) + activesupport (= 6.1.3.2) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.0.3) - active_model_serializers (0.10.10) - actionpack (>= 4.1, < 6.1) - activemodel (>= 4.1, < 6.1) + rails-html-sanitizer (~> 1.1, >= 1.2.0) + active_model_serializers (0.10.12) + actionpack (>= 4.1, < 6.2) + activemodel (>= 4.1, < 6.2) case_transform (>= 0.2) jsonapi-renderer (>= 0.1.1.beta1, < 0.3) active_record_query_trace (1.8) - activejob (5.2.4.4) - activesupport (= 5.2.4.4) + activejob (6.1.3.2) + activesupport (= 6.1.3.2) globalid (>= 0.3.6) - activemodel (5.2.4.4) - activesupport (= 5.2.4.4) - activerecord (5.2.4.4) - activemodel (= 5.2.4.4) - activesupport (= 5.2.4.4) - arel (>= 9.0) - activestorage (5.2.4.4) - actionpack (= 5.2.4.4) - activerecord (= 5.2.4.4) - marcel (~> 0.3.1) - activesupport (5.2.4.4) + activemodel (6.1.3.2) + activesupport (= 6.1.3.2) + activerecord (6.1.3.2) + activemodel (= 6.1.3.2) + activesupport (= 6.1.3.2) + activestorage (6.1.3.2) + actionpack (= 6.1.3.2) + activejob (= 6.1.3.2) + activerecord (= 6.1.3.2) + activesupport (= 6.1.3.2) + marcel (~> 1.0.0) + mini_mime (~> 1.0.2) + activesupport (6.1.3.2) concurrent-ruby (~> 1.0, >= 1.0.2) - i18n (>= 0.7, < 2) - minitest (~> 5.1) - tzinfo (~> 1.1) + i18n (>= 1.6, < 2) + minitest (>= 5.1) + tzinfo (~> 2.0) + zeitwerk (~> 2.3) addressable (2.7.0) public_suffix (>= 2.0.2, < 5.0) airbrussh (1.4.0) @@ -71,28 +74,25 @@ GEM annotate (3.1.1) activerecord (>= 3.2, < 7.0) rake (>= 10.4, < 14.0) - arel (9.0.0) - ast (2.4.1) + ast (2.4.2) attr_encrypted (3.1.0) encryptor (~> 3.0.0) - av (0.9.0) - cocaine (~> 0.5.3) awrence (1.1.1) - aws-eventstream (1.1.0) - aws-partitions (1.397.0) - aws-sdk-core (3.109.3) + aws-eventstream (1.1.1) + aws-partitions (1.452.0) + aws-sdk-core (3.114.0) aws-eventstream (~> 1, >= 1.0.2) aws-partitions (~> 1, >= 1.239.0) aws-sigv4 (~> 1.1) jmespath (~> 1.0) - aws-sdk-kms (1.39.0) - aws-sdk-core (~> 3, >= 3.109.0) + aws-sdk-kms (1.43.0) + aws-sdk-core (~> 3, >= 3.112.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.85.0) - aws-sdk-core (~> 3, >= 3.109.0) + aws-sdk-s3 (1.94.1) + aws-sdk-core (~> 3, >= 3.112.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.1) - aws-sigv4 (1.2.2) + aws-sigv4 (1.2.3) aws-eventstream (~> 1, >= 1.0.2) bcrypt (3.1.16) better_errors (2.9.1) @@ -100,23 +100,26 @@ GEM erubi (>= 1.0.0) rack (>= 0.9.0) bindata (2.4.8) - binding_of_caller (0.8.0) + binding_of_caller (1.0.0) debug_inspector (>= 0.0.1) - blurhash (0.1.4) - ffi (~> 1.10.0) - bootsnap (1.5.1) + blurhash (0.1.5) + ffi (~> 1.14) + bootsnap (1.6.0) msgpack (~> 1.0) - brakeman (4.10.0) + brakeman (5.0.1) browser (4.2.0) + brpoplpush-redis_script (0.1.2) + concurrent-ruby (~> 1.0, >= 1.0.5) + redis (>= 1.0, <= 5.0) builder (3.2.4) - bullet (6.1.0) + bullet (6.1.4) activesupport (>= 3.0.0) uniform_notifier (~> 1.11) - bundler-audit (0.7.0.1) + bundler-audit (0.8.0) bundler (>= 1.2.0, < 3) - thor (>= 0.18, < 2) + thor (~> 1.0) byebug (11.1.3) - capistrano (3.14.1) + capistrano (3.16.0) airbrussh (>= 1.0.0) i18n rake (>= 10.0.0) @@ -131,62 +134,61 @@ GEM sshkit (~> 1.3) capistrano-yarn (2.0.2) capistrano (~> 3.0) - capybara (3.33.0) + capybara (3.35.3) addressable mini_mime (>= 0.1.3) nokogiri (~> 1.8) rack (>= 1.6.0) rack-test (>= 0.6.3) - regexp_parser (~> 1.5) + regexp_parser (>= 1.5, < 3.0) xpath (~> 3.2) case_transform (0.2) activesupport cbor (0.5.9.6) charlock_holmes (0.7.7) - chewy (5.1.0) - activesupport (>= 4.0) + chewy (5.2.0) + activesupport (>= 5.2) elasticsearch (>= 2.0.0) elasticsearch-dsl - chunky_png (1.3.12) - cld3 (3.3.0) - ffi (>= 1.1.0, < 1.12.0) + chunky_png (1.4.0) + cld3 (3.4.2) + ffi (>= 1.1.0, < 1.16.0) climate_control (0.2.0) - cocaine (0.5.8) - climate_control (>= 0.0.3, < 1.0) coderay (1.1.3) color_diff (0.1) - concurrent-ruby (1.1.7) - connection_pool (2.2.3) + concurrent-ruby (1.1.8) + connection_pool (2.2.5) cose (1.0.0) cbor (~> 0.5.9) openssl-signature_algorithm (~> 0.4.0) - crack (0.4.4) + crack (0.4.5) + rexml crass (1.0.6) css_parser (1.7.1) addressable - debug_inspector (0.0.3) - devise (4.7.3) + debug_inspector (1.0.0) + devise (4.8.0) bcrypt (~> 3.0) orm_adapter (~> 0.1) railties (>= 4.1.0) responders warden (~> 1.2.3) - devise-two-factor (3.1.0) - activesupport (< 6.1) + devise-two-factor (4.0.0) + activesupport (< 6.2) attr_encrypted (>= 1.3, < 4, != 2) devise (~> 4.0) - railties (< 6.1) - rotp (~> 2.0) + railties (< 6.2) + rotp (~> 6.0) devise_pam_authenticatable2 (9.2.0) devise (>= 4.0.0) rpam2 (~> 4.0) diff-lcs (1.4.4) discard (1.2.0) activerecord (>= 4.2, < 7) - docile (1.3.2) + docile (1.3.4) domain_name (0.5.20190701) unf (>= 0.0.5, < 1.0.0) - doorkeeper (5.4.0) + doorkeeper (5.5.1) railties (>= 5) dotenv (2.7.6) dotenv-rails (2.7.6) @@ -194,28 +196,31 @@ GEM railties (>= 3.2) e2mmap (0.1.0) ed25519 (1.2.4) - elasticsearch (7.9.0) - elasticsearch-api (= 7.9.0) - elasticsearch-transport (= 7.9.0) - elasticsearch-api (7.9.0) + elasticsearch (7.10.1) + elasticsearch-api (= 7.10.1) + elasticsearch-transport (= 7.10.1) + elasticsearch-api (7.10.1) multi_json elasticsearch-dsl (0.1.9) - elasticsearch-transport (7.9.0) + elasticsearch-transport (7.10.1) faraday (~> 1) multi_json encryptor (3.0.0) - erubi (1.9.0) + erubi (1.10.0) et-orbi (1.2.4) tzinfo excon (0.76.0) - fabrication (2.21.1) - faker (2.14.0) + fabrication (2.22.0) + faker (2.17.0) i18n (>= 1.6, < 2) - faraday (1.0.1) + faraday (1.3.0) + faraday-net_http (~> 1.0) multipart-post (>= 1.2, < 3) + ruby2_keywords + faraday-net_http (1.0.1) fast_blank (1.0.0) - fastimage (2.2.0) - ffi (1.10.0) + fastimage (2.2.3) + ffi (1.15.0) ffi-compiler (1.0.1) ffi (>= 1.0.0) rake @@ -235,7 +240,7 @@ GEM fugit (1.3.9) et-orbi (~> 1.1, >= 1.1.8) raabro (~> 1.3) - fuubar (2.5.0) + fuubar (2.5.1) rspec-core (~> 3.0) ruby-progressbar (~> 1.4) globalid (0.4.2) @@ -271,9 +276,9 @@ GEM httplog (1.4.3) rack (>= 1.0) rainbow (>= 2.0.0) - i18n (1.8.5) + i18n (1.8.10) concurrent-ruby (~> 1.0) - i18n-tasks (0.9.31) + i18n-tasks (0.9.34) activesupport (>= 4.0.2) ast (>= 2.1.0) erubi @@ -287,16 +292,16 @@ GEM ipaddress (0.8.3) iso-639 (0.3.5) jmespath (1.4.0) - json (2.3.1) - json-canonicalization (0.2.0) - json-ld (3.1.5) + json (2.5.1) + json-canonicalization (0.2.1) + json-ld (3.1.9) htmlentities (~> 4.3) json-canonicalization (~> 0.2) link_header (~> 0.0, >= 0.0.8) multi_json (~> 1.14) rack (~> 2.0) rdf (~> 3.1) - json-ld-preloaded (3.1.3) + json-ld-preloaded (3.1.5) json-ld (~> 3.1) rdf (~> 3.1) jsonapi-renderer (0.2.2) @@ -327,20 +332,19 @@ GEM activesupport (>= 4) railties (>= 4) request_store (~> 1.0) - loofah (2.7.0) + loofah (2.9.1) crass (~> 1.0.2) nokogiri (>= 1.5.9) mail (2.7.1) mini_mime (>= 0.1.1) - makara (0.4.1) + makara (0.5.0) activerecord (>= 3.0.0) - marcel (0.3.3) - mimemagic (~> 0.3.2) + marcel (1.0.1) mario-redis-lock (1.2.1) redis (>= 3.0.5) - memory_profiler (0.9.14) + memory_profiler (1.0.0) method_source (1.0.0) - microformats (4.2.1) + microformats (4.3.1) json (~> 2.2) nokogiri (~> 1.10) mime-types (3.3.1) @@ -349,27 +353,28 @@ GEM mimemagic (0.3.10) nokogiri (~> 1) rake - mini_mime (1.0.2) - mini_portile2 (2.4.0) - minitest (5.14.2) - msgpack (1.3.3) + mini_mime (1.0.3) + mini_portile2 (2.5.1) + minitest (5.14.4) + msgpack (1.4.2) multi_json (1.15.0) multipart-post (2.1.1) - net-ldap (0.16.3) + net-ldap (0.17.0) net-scp (3.0.0) net-ssh (>= 2.6.5, < 7.0.0) net-ssh (6.1.0) - nio4r (2.5.4) - nokogiri (1.10.10) - mini_portile2 (~> 2.4.0) - nokogumbo (2.0.2) + nio4r (2.5.7) + nokogiri (1.11.3) + mini_portile2 (~> 2.5.0) + racc (~> 1.4) + nokogumbo (2.0.4) nokogiri (~> 1.8, >= 1.8.4) - nsa (0.2.7) - activesupport (>= 4.2, < 6) + nsa (0.2.8) + activesupport (>= 4.2, < 7) concurrent-ruby (~> 1.0, >= 1.0.2) sidekiq (>= 3.5) statsd-ruby (~> 1.4, >= 1.4.0) - oj (3.10.16) + oj (3.11.5) omniauth (1.9.1) hashie (>= 3.4.6) rack (>= 1.6.2, < 3) @@ -386,31 +391,25 @@ GEM openssl (2.2.0) openssl-signature_algorithm (0.4.0) orm_adapter (0.5.0) - ox (2.13.4) + ox (2.14.4) paperclip (6.0.0) activemodel (>= 4.2.0) activesupport (>= 4.2.0) mime-types mimemagic (~> 0.3.0) terrapin (~> 0.6.0) - paperclip-av-transcoder (0.6.4) - av (~> 0.9.0) - paperclip (>= 2.5.2) parallel (1.20.1) - parallel_tests (3.4.0) + parallel_tests (3.7.0) parallel - parser (2.7.2.0) + parser (3.0.1.1) ast (~> 2.4.1) parslet (2.0.0) pastel (0.8.0) tty-color (~> 0.5) pg (1.2.3) - pghero (2.7.2) + pghero (2.8.1) activerecord (>= 5) - pkg-config (1.4.4) - pluck_each (0.1.3) - activerecord (> 3.2.0) - activesupport (> 3.0.0) + pkg-config (1.4.6) posix-spawn (0.3.15) premailer (1.14.2) addressable @@ -429,13 +428,14 @@ GEM pry-rails (0.3.9) pry (>= 0.10.4) public_suffix (4.0.6) - puma (5.0.4) + puma (5.3.0) nio4r (~> 2.0) pundit (2.1.0) activesupport (>= 3.0.0) raabro (1.3.3) + racc (1.5.2) rack (2.2.3) - rack-attack (6.3.1) + rack-attack (6.5.0) rack (>= 1.0, < 3) rack-cors (1.1.1) rack (>= 2.0.0) @@ -443,18 +443,20 @@ GEM rack rack-test (1.1.0) rack (>= 1.0, < 3) - rails (5.2.4.4) - actioncable (= 5.2.4.4) - actionmailer (= 5.2.4.4) - actionpack (= 5.2.4.4) - actionview (= 5.2.4.4) - activejob (= 5.2.4.4) - activemodel (= 5.2.4.4) - activerecord (= 5.2.4.4) - activestorage (= 5.2.4.4) - activesupport (= 5.2.4.4) - bundler (>= 1.3.0) - railties (= 5.2.4.4) + rails (6.1.3.2) + actioncable (= 6.1.3.2) + actionmailbox (= 6.1.3.2) + actionmailer (= 6.1.3.2) + actionpack (= 6.1.3.2) + actiontext (= 6.1.3.2) + actionview (= 6.1.3.2) + activejob (= 6.1.3.2) + activemodel (= 6.1.3.2) + activerecord (= 6.1.3.2) + activestorage (= 6.1.3.2) + activesupport (= 6.1.3.2) + bundler (>= 1.15.0) + railties (= 6.1.3.2) sprockets-rails (>= 2.0.0) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) @@ -465,101 +467,87 @@ GEM nokogiri (>= 1.6) rails-html-sanitizer (1.3.0) loofah (~> 2.3) - rails-i18n (5.1.3) + rails-i18n (6.0.0) i18n (>= 0.7, < 2) - railties (>= 5.0, < 6) + railties (>= 6.0.0, < 7) rails-settings-cached (0.6.6) rails (>= 4.2.0) - railties (5.2.4.4) - actionpack (= 5.2.4.4) - activesupport (= 5.2.4.4) + railties (6.1.3.2) + actionpack (= 6.1.3.2) + activesupport (= 6.1.3.2) method_source rake (>= 0.8.7) - thor (>= 0.19.0, < 2.0) + thor (~> 1.0) rainbow (3.0.0) - rake (13.0.1) - rdf (3.1.7) + rake (13.0.3) + rdf (3.1.13) hamster (~> 3.0) link_header (~> 0.0, >= 0.0.8) rdf-normalize (0.4.0) rdf (~> 3.1) redis (4.2.5) - redis-actionpack (5.2.0) - actionpack (>= 5, < 7) - redis-rack (>= 2.1.0, < 3) - redis-store (>= 1.1.0, < 2) - redis-activesupport (5.2.0) - activesupport (>= 3, < 7) - redis-store (>= 1.3, < 2) - redis-namespace (1.8.0) + redis-namespace (1.8.1) redis (>= 3.0.4) - redis-rack (2.1.3) - rack (>= 2.0.8, < 3) - redis-store (>= 1.2, < 2) - redis-rails (5.0.2) - redis-actionpack (>= 5.0, < 6) - redis-activesupport (>= 5.0, < 6) - redis-store (>= 1.2, < 2) - redis-store (1.9.0) - redis (>= 4, < 5) - regexp_parser (1.8.2) + regexp_parser (2.1.1) request_store (1.5.0) rack (>= 1.4) + resolv (0.1.0) responders (3.0.1) actionpack (>= 5.0) railties (>= 5.0) - rexml (3.2.4) - rotp (2.1.2) + rexml (3.2.5) + rotp (6.2.0) rpam2 (4.0.2) - rqrcode (1.1.2) + rqrcode (2.0.0) chunky_png (~> 1.0) - rqrcode_core (~> 0.1) - rqrcode_core (0.1.2) - rspec-core (3.9.3) - rspec-support (~> 3.9.3) - rspec-expectations (3.9.2) + rqrcode_core (~> 1.0) + rqrcode_core (1.0.0) + rspec-core (3.10.1) + rspec-support (~> 3.10.0) + rspec-expectations (3.10.1) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.9.0) - rspec-mocks (3.9.1) + rspec-support (~> 3.10.0) + rspec-mocks (3.10.2) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.9.0) - rspec-rails (4.0.1) - actionpack (>= 4.2) - activesupport (>= 4.2) - railties (>= 4.2) - rspec-core (~> 3.9) - rspec-expectations (~> 3.9) - rspec-mocks (~> 3.9) - rspec-support (~> 3.9) + rspec-support (~> 3.10.0) + rspec-rails (5.0.1) + actionpack (>= 5.2) + activesupport (>= 5.2) + railties (>= 5.2) + rspec-core (~> 3.10) + rspec-expectations (~> 3.10) + rspec-mocks (~> 3.10) + rspec-support (~> 3.10) rspec-sidekiq (3.1.0) rspec-core (~> 3.0, >= 3.0.0) sidekiq (>= 2.4.0) - rspec-support (3.9.3) + rspec-support (3.10.2) rspec_junit_formatter (0.4.1) rspec-core (>= 2, < 4, != 2.12.0) - rubocop (1.3.1) + rubocop (1.14.0) parallel (~> 1.10) - parser (>= 2.7.1.5) + parser (>= 3.0.0.0) rainbow (>= 2.2.2, < 4.0) - regexp_parser (>= 1.8) + regexp_parser (>= 1.8, < 3.0) rexml - rubocop-ast (>= 1.1.1) + rubocop-ast (>= 1.5.0, < 2.0) ruby-progressbar (~> 1.7) - unicode-display_width (>= 1.4.0, < 2.0) - rubocop-ast (1.1.1) - parser (>= 2.7.1.5) - rubocop-rails (2.8.1) + unicode-display_width (>= 1.4.0, < 3.0) + rubocop-ast (1.5.0) + parser (>= 3.0.1.1) + rubocop-rails (2.10.1) activesupport (>= 4.2.0) rack (>= 1.1) - rubocop (>= 0.87.0) - ruby-progressbar (1.10.1) + rubocop (>= 1.7.0, < 2.0) + ruby-progressbar (1.11.0) ruby-saml (1.11.0) nokogiri (>= 1.5.10) + ruby2_keywords (0.0.4) rufus-scheduler (3.6.0) fugit (~> 1.1, >= 1.1.6) safety_net_attestation (0.4.0) jwt (~> 2.0) - sanitize (5.2.1) + sanitize (5.2.3) crass (~> 1.0.2) nokogiri (>= 1.8.0) nokogumbo (~> 2.0) @@ -567,8 +555,8 @@ GEM activerecord (>= 4.0.0) railties (>= 4.0.0) securecompare (1.0.0) - semantic_range (2.3.0) - sidekiq (6.1.2) + semantic_range (3.0.0) + sidekiq (6.2.1) connection_pool (>= 2.2.2) rack (~> 2.0) redis (>= 4.2.0) @@ -581,19 +569,22 @@ GEM sidekiq (>= 3) thwait tilt (>= 1.4.0) - sidekiq-unique-jobs (6.0.25) + sidekiq-unique-jobs (7.0.9) + brpoplpush-redis_script (> 0.1.1, <= 2.0.0) concurrent-ruby (~> 1.0, >= 1.0.5) - sidekiq (>= 4.0, < 7.0) + sidekiq (>= 5.0, < 7.0) thor (>= 0.20, < 2.0) simple-navigation (4.1.0) activesupport (>= 2.3.2) - simple_form (5.0.3) - actionpack (>= 5.0) - activemodel (>= 5.0) - simplecov (0.19.1) + simple_form (5.1.0) + actionpack (>= 5.2) + activemodel (>= 5.2) + simplecov (0.21.2) docile (~> 1.1) simplecov-html (~> 0.11) + simplecov_json_formatter (~> 0.1) simplecov-html (0.12.3) + simplecov_json_formatter (0.1.2) sprockets (3.7.2) concurrent-ruby (~> 1.0) rack (> 1, < 3) @@ -601,50 +592,48 @@ GEM actionpack (>= 4.0) activesupport (>= 4.0) sprockets (>= 3.0.0) - sshkit (1.21.0) + sshkit (1.21.2) net-scp (>= 1.1.2) net-ssh (>= 2.8.0) - stackprof (0.2.16) - statsd-ruby (1.4.0) + stackprof (0.2.17) + statsd-ruby (1.5.0) stoplight (2.2.1) - streamio-ffmpeg (3.0.2) - multi_json (~> 1.8) - strong_migrations (0.7.2) + strong_migrations (0.7.6) activerecord (>= 5) temple (0.8.2) - terminal-table (1.8.0) + terminal-table (3.0.0) unicode-display_width (~> 1.1, >= 1.1.1) terrapin (0.6.0) climate_control (>= 0.0.3, < 1.0) - thor (1.0.1) - thread_safe (0.3.6) + thor (1.1.0) thwait (0.2.0) e2mmap tilt (2.0.10) tpm-key_attestation (0.9.0) bindata (~> 2.4) openssl-signature_algorithm (~> 0.4.0) - tty-color (0.5.2) + tty-color (0.6.0) tty-cursor (0.7.1) - tty-prompt (0.22.0) + tty-prompt (0.23.1) pastel (~> 0.8) tty-reader (~> 0.8) - tty-reader (0.8.0) + tty-reader (0.9.0) tty-cursor (~> 0.7) tty-screen (~> 0.8) wisper (~> 2.0) tty-screen (0.8.1) - twitter-text (1.14.7) + twitter-text (3.1.0) + idn-ruby unf (~> 0.1.0) - tzinfo (1.2.7) - thread_safe (~> 0.1) - tzinfo-data (1.2020.4) + tzinfo (2.0.4) + concurrent-ruby (~> 1.0) + tzinfo-data (1.2021.1) tzinfo (>= 1.0.0) unf (0.1.4) unf_ext unf_ext (0.0.7.7) unicode-display_width (1.7.0) - uniform_notifier (1.13.0) + uniform_notifier (1.14.1) warden (1.2.9) rack (>= 2.0.9) webauthn (3.0.0.alpha1) @@ -657,11 +646,11 @@ GEM safety_net_attestation (~> 0.4.0) securecompare (~> 1.0) tpm-key_attestation (~> 0.9.0) - webmock (3.10.0) + webmock (3.12.2) addressable (>= 2.3.6) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) - webpacker (5.2.1) + webpacker (5.3.0) activesupport (>= 5.2) rack-proxy (>= 0.6.1) railties (>= 5.2) @@ -676,6 +665,7 @@ GEM xorcist (1.1.2) xpath (3.2.0) nokogiri (~> 1.8) + zeitwerk (2.4.2) PLATFORMS ruby @@ -685,43 +675,42 @@ DEPENDENCIES active_record_query_trace (~> 1.8) addressable (~> 2.7) annotate (~> 3.1) - aws-sdk-s3 (~> 1.85) + aws-sdk-s3 (~> 1.94) better_errors (~> 2.9) - binding_of_caller (~> 0.7) + binding_of_caller (~> 1.0) blurhash (~> 0.1) - bootsnap (~> 1.5) - brakeman (~> 4.10) + bootsnap (~> 1.6.0) + brakeman (~> 5.0) browser bullet (~> 6.1) - bundler-audit (~> 0.7) - capistrano (~> 3.14) + bundler-audit (~> 0.8) + capistrano (~> 3.16) capistrano-rails (~> 1.6) capistrano-rbenv (~> 2.2) capistrano-yarn (~> 2.0) - capybara (~> 3.33) + capybara (~> 3.35) charlock_holmes (~> 0.7.7) - chewy (~> 5.1) - cld3 (~> 3.3.0) + chewy (~> 5.2) + cld3 (~> 3.4.2) climate_control (~> 0.2) color_diff (~> 0.1) concurrent-ruby connection_pool - devise (~> 4.7) - devise-two-factor (~> 3.1) + devise (~> 4.8) + devise-two-factor (~> 4.0) devise_pam_authenticatable2 (~> 9.2) discard (~> 1.2) - doorkeeper (~> 5.4) + doorkeeper (~> 5.5) dotenv-rails (~> 2.7) ed25519 (~> 1.2) - fabrication (~> 2.21) - faker (~> 2.14) + fabrication (~> 2.22) + faker (~> 2.17) fast_blank (~> 1.0) fastimage fog-core (<= 2.1.0) fog-openstack (~> 0.3) fuubar (~> 2.5) hamlit-rails (~> 0.2) - health_check! hiredis (~> 0.6) htmlentities (~> 4.3) http (~> 4.4) @@ -737,78 +726,74 @@ DEPENDENCIES letter_opener_web (~> 1.4) link_header (~> 0.0) lograge (~> 0.11) - makara (~> 0.4) + makara (~> 0.5) mario-redis-lock (~> 1.2) memory_profiler microformats (~> 4.2) mime-types (~> 3.3.1) - net-ldap (~> 0.16) - nilsimsa! - nokogiri (~> 1.10) + net-ldap (~> 0.17) + nokogiri (~> 1.11) nsa (~> 0.2) - oj (~> 3.10) + oj (~> 3.11) omniauth (~> 1.9) omniauth-cas (~> 2.0) omniauth-rails_csrf_protection (~> 0.1) omniauth-saml (~> 1.10) - ox (~> 2.13) + ox (~> 2.14) paperclip (~> 6.0) - paperclip-av-transcoder (~> 0.6) parallel (~> 1.20) - parallel_tests (~> 3.4) + parallel_tests (~> 3.7) parslet pg (~> 1.2) - pghero (~> 2.7) + pghero (~> 2.8) pkg-config (~> 1.4) - pluck_each (~> 0.1.3) posix-spawn premailer-rails private_address_check (~> 0.5) pry-byebug (~> 3.9) pry-rails (~> 0.3) - puma (~> 5.0) + puma (~> 5.3) pundit (~> 2.1) rack (~> 2.2.3) - rack-attack (~> 6.3) + rack-attack (~> 6.5) rack-cors (~> 1.1) - rails (~> 5.2.4.4) + rails (~> 6.1.3) rails-controller-testing (~> 1.0) - rails-i18n (~> 5.1) + rails-i18n (~> 6.0) rails-settings-cached (~> 0.6) rdf-normalize (~> 0.4) redis (~> 4.2) redis-namespace (~> 1.8) - redis-rails (~> 5.0) - rqrcode (~> 1.1) - rspec-rails (~> 4.0) + resolv (~> 0.1.0) + rqrcode (~> 2.0) + rspec-rails (~> 5.0) rspec-sidekiq (~> 3.1) rspec_junit_formatter (~> 0.4) - rubocop (~> 1.3) - rubocop-rails (~> 2.8) - ruby-progressbar (~> 1.10) + rubocop (~> 1.14) + rubocop-rails (~> 2.10) + ruby-progressbar (~> 1.11) sanitize (~> 5.2) scenic (~> 1.5) - sidekiq (~> 6.1) + sidekiq (~> 6.2) sidekiq-bulk (~> 0.2.0) sidekiq-scheduler (~> 3.0) - sidekiq-unique-jobs (~> 6.0) + sidekiq-unique-jobs (~> 7.0) simple-navigation (~> 4.1) - simple_form (~> 5.0) - simplecov (~> 0.19) + simple_form (~> 5.1) + simplecov (~> 0.21) sprockets (~> 3.7.2) sprockets-rails (~> 3.2) stackprof stoplight (~> 2.2.1) - streamio-ffmpeg (~> 3.0) strong_migrations (~> 0.7) - thor (~> 1.0) - tty-prompt (~> 0.22) - twitter-text (~> 1.14) - tzinfo-data (~> 1.2020) + thor (~> 1.1) + tty-prompt (~> 0.23) + twitter-text (~> 3.1.0) + tzinfo-data (~> 1.2021) webauthn (~> 3.0.0.alpha1) - webmock (~> 3.10) - webpacker (~> 5.2) - webpush + webmock (~> 3.12) + webpacker (~> 5.3) + webpush (~> 0.3) xorcist (~> 1.1) RUBY VERSION diff --git a/Procfile.dev b/Procfile.dev index e589bbf63..ba04fb661 100644 --- a/Procfile.dev +++ b/Procfile.dev @@ -1,4 +1,4 @@ -web: env PORT=3000 bundle exec puma -C config/puma.rb -sidekiq: env PORT=3000 bundle exec sidekiq +web: env PORT=3000 RAILS_ENV=development bundle exec puma -C config/puma.rb +sidekiq: env PORT=3000 RAILS_ENV=development bundle exec sidekiq stream: env PORT=4000 yarn run start webpack: ./bin/webpack-dev-server --listen-host 0.0.0.0 diff --git a/README.md b/README.md index f9d555b0e..67983996f 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,7 @@ Setting up your Hometown development environment is [exactly like setting up you ## License -Copyright (C) 2016-2020 Eugen Rochko & other Mastodon contributors (see [AUTHORS.md](AUTHORS.md)) +Copyright (C) 2016-2021 Eugen Rochko & other Mastodon contributors (see [AUTHORS.md](AUTHORS.md)) This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. diff --git a/SECURITY.md b/SECURITY.md index 7625597fe..9d351fce6 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,8 +4,9 @@ | Version | Supported | | ------- | ------------------ | -| 3.1.x | :white_check_mark: | -| < 3.1 | :x: | +| 3.4.x | :white_check_mark: | +| 3.3.x | :white_check_mark: | +| < 3.3 | :x: | ## Reporting a Vulnerability diff --git a/Vagrantfile b/Vagrantfile index 7d0f7b3de..4d0cc0f76 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -12,7 +12,7 @@ curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - sudo apt-add-repository 'deb https://dl.yarnpkg.com/debian/ stable main' # Add repo for NodeJS -curl -sL https://deb.nodesource.com/setup_10.x | sudo bash - +curl -sL https://deb.nodesource.com/setup_12.x | sudo bash - # Add firewall rule to redirect 80 to PORT and save sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port #{ENV["PORT"]} @@ -72,10 +72,12 @@ bundle install yarn install # Build Mastodon +export RAILS_ENV=development export $(cat ".env.vagrant" | xargs) bundle exec rails db:setup # Configure automatic loading of environment variable +echo 'export RAILS_ENV=development' >> ~/.bash_profile echo 'export $(cat "/vagrant/.env.vagrant" | xargs)' >> ~/.bash_profile SCRIPT diff --git a/app/controllers/about_controller.rb b/app/controllers/about_controller.rb index dcad5d3b4..d7e78d6b9 100644 --- a/app/controllers/about_controller.rb +++ b/app/controllers/about_controller.rb @@ -20,6 +20,7 @@ class AboutController < ApplicationController toc_generator = TOCGenerator.new(@instance_presenter.site_extended_description) + @rules = Rule.ordered @contents = toc_generator.html @table_of_contents = toc_generator.toc @blocks = DomainBlock.with_user_facing_limitations.by_severity if display_blocks? diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index 67fa2d1a2..d5cd62f90 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -85,11 +85,7 @@ class AccountsController < ApplicationController end def only_media_scope - Status.where(id: account_media_status_ids) - end - - def account_media_status_ids - @account.media_attachments.attached.reorder(nil).select(:status_id).group(:status_id) + Status.joins(:media_attachments).merge(@account.media_attachments.reorder(nil)).group(:id) end def no_replies_scope @@ -143,15 +139,15 @@ class AccountsController < ApplicationController end def media_requested? - request.path.split('.').first.ends_with?('/media') && !tag_requested? + request.path.split('.').first.end_with?('/media') && !tag_requested? end def replies_requested? - request.path.split('.').first.ends_with?('/with_replies') && !tag_requested? + request.path.split('.').first.end_with?('/with_replies') && !tag_requested? end def tag_requested? - request.path.split('.').first.ends_with?(Addressable::URI.parse("/tagged/#{params[:tag]}").normalize) + request.path.split('.').first.end_with?(Addressable::URI.parse("/tagged/#{params[:tag]}").normalize) end def cached_filtered_status_page diff --git a/app/controllers/activitypub/inboxes_controller.rb b/app/controllers/activitypub/inboxes_controller.rb index d3044f180..92dcb5ac7 100644 --- a/app/controllers/activitypub/inboxes_controller.rb +++ b/app/controllers/activitypub/inboxes_controller.rb @@ -5,7 +5,7 @@ class ActivityPub::InboxesController < ActivityPub::BaseController include JsonLdHelper include AccountOwnedConcern - before_action :skip_unknown_actor_delete + before_action :skip_unknown_actor_activity before_action :require_signature! skip_before_action :authenticate_user! @@ -18,13 +18,13 @@ class ActivityPub::InboxesController < ActivityPub::BaseController private - def skip_unknown_actor_delete - head 202 if unknown_deleted_account? + def skip_unknown_actor_activity + head 202 if unknown_affected_account? end - def unknown_deleted_account? + def unknown_affected_account? json = Oj.load(body, mode: :strict) - json.is_a?(Hash) && json['type'] == 'Delete' && json['actor'].present? && json['actor'] == value_or_id(json['object']) && !Account.where(uri: json['actor']).exists? + json.is_a?(Hash) && %w(Delete Update).include?(json['type']) && json['actor'].present? && json['actor'] == value_or_id(json['object']) && !Account.where(uri: json['actor']).exists? rescue Oj::ParseError false end diff --git a/app/controllers/activitypub/outboxes_controller.rb b/app/controllers/activitypub/outboxes_controller.rb index 5fd735ad6..111285036 100644 --- a/app/controllers/activitypub/outboxes_controller.rb +++ b/app/controllers/activitypub/outboxes_controller.rb @@ -20,7 +20,7 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController def outbox_presenter if page_requested? ActivityPub::CollectionPresenter.new( - id: outbox_url(page_params), + id: outbox_url(**page_params), type: :ordered, part_of: outbox_url, prev: prev_page, diff --git a/app/controllers/admin/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb index 7c2951acb..c829ed98f 100644 --- a/app/controllers/admin/dashboard_controller.rb +++ b/app/controllers/admin/dashboard_controller.rb @@ -4,6 +4,7 @@ require 'sidekiq/api' module Admin class DashboardController < BaseController def index + @system_checks = Admin::SystemCheck.perform @users_count = User.count @pending_users_count = User.pending.count @registrations_week = Redis.current.get("activity:accounts:local:#{current_week}") || 0 @@ -34,7 +35,6 @@ module Admin @whitelist_enabled = whitelist_mode? @profile_directory = Setting.profile_directory @timeline_preview = Setting.timeline_preview - @spam_check_enabled = Setting.spam_check_enabled @trends_enabled = Setting.trends end diff --git a/app/controllers/admin/domain_blocks_controller.rb b/app/controllers/admin/domain_blocks_controller.rb index ba927b04a..b140c454c 100644 --- a/app/controllers/admin/domain_blocks_controller.rb +++ b/app/controllers/admin/domain_blocks_controller.rb @@ -22,7 +22,7 @@ module Admin if existing_domain_block.present? && !@domain_block.stricter_than?(existing_domain_block) @domain_block.save flash.now[:alert] = I18n.t('admin.domain_blocks.existing_domain_block_html', name: existing_domain_block.domain, unblock_url: admin_domain_block_path(existing_domain_block)).html_safe # rubocop:disable Rails/OutputSafety - @domain_block.errors[:domain].clear + @domain_block.errors.delete(:domain) render :new else if existing_domain_block.present? diff --git a/app/controllers/admin/follow_recommendations_controller.rb b/app/controllers/admin/follow_recommendations_controller.rb new file mode 100644 index 000000000..e3eac62b3 --- /dev/null +++ b/app/controllers/admin/follow_recommendations_controller.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +module Admin + class FollowRecommendationsController < BaseController + before_action :set_language + + def show + authorize :follow_recommendation, :show? + + @form = Form::AccountBatch.new + @accounts = filtered_follow_recommendations + end + + def update + @form = Form::AccountBatch.new(form_account_batch_params.merge(current_account: current_account, action: action_from_button)) + @form.save + rescue ActionController::ParameterMissing + # Do nothing + ensure + redirect_to admin_follow_recommendations_path(filter_params) + end + + private + + def set_language + @language = follow_recommendation_filter.language + end + + def filtered_follow_recommendations + follow_recommendation_filter.results + end + + def follow_recommendation_filter + @follow_recommendation_filter ||= FollowRecommendationFilter.new(filter_params) + end + + def form_account_batch_params + params.require(:form_account_batch).permit(:action, account_ids: []) + end + + def filter_params + params.slice(*FollowRecommendationFilter::KEYS).permit(*FollowRecommendationFilter::KEYS) + end + + def action_from_button + if params[:suppress] + 'suppress_follow_recommendation' + elsif params[:unsuppress] + 'unsuppress_follow_recommendation' + end + end + end +end diff --git a/app/controllers/admin/instances_controller.rb b/app/controllers/admin/instances_controller.rb index b5918d231..748c5de5a 100644 --- a/app/controllers/admin/instances_controller.rb +++ b/app/controllers/admin/instances_controller.rb @@ -3,7 +3,8 @@ module Admin class InstancesController < BaseController before_action :set_instances, only: :index - before_action :set_instance, only: :show + before_action :set_instance, except: :index + before_action :set_exhausted_deliveries_days, only: :show def index authorize :instance, :index? @@ -13,14 +14,55 @@ module Admin authorize :instance, :show? end + def clear_delivery_errors + authorize :delivery, :clear_delivery_errors? + + @instance.delivery_failure_tracker.clear_failures! + redirect_to admin_instance_path(@instance.domain) + end + + def restart_delivery + authorize :delivery, :restart_delivery? + + last_unavailable_domain = unavailable_domain + + if last_unavailable_domain.present? + @instance.delivery_failure_tracker.track_success! + log_action :destroy, last_unavailable_domain + end + + redirect_to admin_instance_path(@instance.domain) + end + + def stop_delivery + authorize :delivery, :stop_delivery? + + UnavailableDomain.create(domain: @instance.domain) + log_action :create, unavailable_domain + redirect_to admin_instance_path(@instance.domain) + end + private def set_instance @instance = Instance.find(params[:id]) end + def set_exhausted_deliveries_days + @exhausted_deliveries_days = @instance.delivery_failure_tracker.exhausted_deliveries_days + end + def set_instances @instances = filtered_instances.page(params[:page]) + warning_domains_map = DeliveryFailureTracker.warning_domains_map + + @instances.each do |instance| + instance.failure_days = warning_domains_map[instance.domain] + end + end + + def unavailable_domain + UnavailableDomain.find_by(domain: @instance.domain) end def filtered_instances diff --git a/app/controllers/admin/rules_controller.rb b/app/controllers/admin/rules_controller.rb new file mode 100644 index 000000000..f3bed3ad8 --- /dev/null +++ b/app/controllers/admin/rules_controller.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +module Admin + class RulesController < BaseController + before_action :set_rule, except: [:index, :create] + + def index + authorize :rule, :index? + + @rules = Rule.ordered + @rule = Rule.new + end + + def create + authorize :rule, :create? + + @rule = Rule.new(resource_params) + + if @rule.save + redirect_to admin_rules_path + else + @rules = Rule.ordered + render :index + end + end + + def edit + authorize @rule, :update? + end + + def update + authorize @rule, :update? + + if @rule.update(resource_params) + redirect_to admin_rules_path + else + render :edit + end + end + + def destroy + authorize @rule, :destroy? + + @rule.discard + + redirect_to admin_rules_path + end + + private + + def set_rule + @rule = Rule.find(params[:id]) + end + + def resource_params + params.require(:rule).permit(:text, :priority) + end + end +end diff --git a/app/controllers/admin/statuses_controller.rb b/app/controllers/admin/statuses_controller.rb index d7c192f0d..ef279509d 100644 --- a/app/controllers/admin/statuses_controller.rb +++ b/app/controllers/admin/statuses_controller.rb @@ -14,8 +14,7 @@ module Admin @statuses = @account.statuses.where(visibility: [:public, :unlisted]) if params[:media] - account_media_status_ids = @account.media_attachments.attached.reorder(nil).select(:status_id).group(:status_id) - @statuses.merge!(Status.where(id: account_media_status_ids)) + @statuses.merge!(Status.joins(:media_attachments).merge(@account.media_attachments.reorder(nil)).group(:id)) end @statuses = @statuses.preload(:media_attachments, :mentions).page(params[:page]).per(PER_PAGE) diff --git a/app/controllers/admin/tags_controller.rb b/app/controllers/admin/tags_controller.rb index 59df4470e..eed4feea2 100644 --- a/app/controllers/admin/tags_controller.rb +++ b/app/controllers/admin/tags_controller.rb @@ -59,8 +59,8 @@ module Admin .where(Status.arel_table[:id].gteq(Mastodon::Snowflake.id_at(Time.now.utc.beginning_of_day))) .joins(:account) .group('accounts.domain') - .reorder('statuses_count desc') - .pluck('accounts.domain, count(*) AS statuses_count') + .reorder(statuses_count: :desc) + .pluck(Arel.sql('accounts.domain, count(*) AS statuses_count')) end def set_counters diff --git a/app/controllers/api/v1/accounts/lookup_controller.rb b/app/controllers/api/v1/accounts/lookup_controller.rb new file mode 100644 index 000000000..aee6be18a --- /dev/null +++ b/app/controllers/api/v1/accounts/lookup_controller.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class Api::V1::Accounts::LookupController < Api::BaseController + before_action -> { authorize_if_got_token! :read, :'read:accounts' } + before_action :set_account + + def show + render json: @account, serializer: REST::AccountSerializer + end + + private + + def set_account + @account = ResolveAccountService.new.call(params[:acct], skip_webfinger: true) || raise(ActiveRecord::RecordNotFound) + end +end diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index 3e66ff212..95869f554 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -27,13 +27,15 @@ class Api::V1::AccountsController < Api::BaseController self.response_body = Oj.dump(response.body) self.status = response.status + rescue ActiveRecord::RecordInvalid => e + render json: ValidationErrorFormatter.new(e, :'account.username' => :username, :'invite_request.text' => :reason).as_json, status: :unprocessable_entity end def follow follow = FollowService.new.call(current_user.account, @account, reblogs: params.key?(:reblogs) ? truthy_param?(:reblogs) : nil, notify: params.key?(:notify) ? truthy_param?(:notify) : nil, with_rate_limit: true) options = @account.locked? || current_user.account.silenced? ? {} : { following_map: { @account.id => { reblogs: follow.show_reblogs?, notify: follow.notify? } }, requested_map: { @account.id => false } } - render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships(options) + render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships(**options) end def block @@ -42,7 +44,7 @@ class Api::V1::AccountsController < Api::BaseController end def mute - MuteService.new.call(current_user.account, @account, notifications: truthy_param?(:notifications), duration: (params[:duration] || 0)) + MuteService.new.call(current_user.account, @account, notifications: truthy_param?(:notifications), duration: (params[:duration]&.to_i || 0)) render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships end @@ -68,7 +70,7 @@ class Api::V1::AccountsController < Api::BaseController end def relationships(**options) - AccountRelationshipsPresenter.new([@account.id], current_user.account_id, options) + AccountRelationshipsPresenter.new([@account.id], current_user.account_id, **options) end def account_params diff --git a/app/controllers/api/v1/crypto/keys/claims_controller.rb b/app/controllers/api/v1/crypto/keys/claims_controller.rb index 34b21a380..f9d202d67 100644 --- a/app/controllers/api/v1/crypto/keys/claims_controller.rb +++ b/app/controllers/api/v1/crypto/keys/claims_controller.rb @@ -12,7 +12,7 @@ class Api::V1::Crypto::Keys::ClaimsController < Api::BaseController private def set_claim_results - @claim_results = devices.map { |device_params| ::Keys::ClaimService.new.call(current_account, device_params[:account_id], device_params[:device_id]) }.compact + @claim_results = devices.filter_map { |device_params| ::Keys::ClaimService.new.call(current_account, device_params[:account_id], device_params[:device_id]) } end def resource_params diff --git a/app/controllers/api/v1/crypto/keys/queries_controller.rb b/app/controllers/api/v1/crypto/keys/queries_controller.rb index 0851d797d..e6ce9f919 100644 --- a/app/controllers/api/v1/crypto/keys/queries_controller.rb +++ b/app/controllers/api/v1/crypto/keys/queries_controller.rb @@ -17,7 +17,7 @@ class Api::V1::Crypto::Keys::QueriesController < Api::BaseController end def set_query_results - @query_results = @accounts.map { |account| ::Keys::QueryService.new.call(account) }.compact + @query_results = @accounts.filter_map { |account| ::Keys::QueryService.new.call(account) } end def account_ids diff --git a/app/controllers/api/v1/emails/confirmations_controller.rb b/app/controllers/api/v1/emails/confirmations_controller.rb new file mode 100644 index 000000000..4a7aa9c32 --- /dev/null +++ b/app/controllers/api/v1/emails/confirmations_controller.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +class Api::V1::Emails::ConfirmationsController < Api::BaseController + before_action :doorkeeper_authorize! + before_action :require_user_owned_by_application! + + def create + if !current_user.confirmed? && current_user.unconfirmed_email.present? + current_user.update!(email: params[:email]) if params.key?(:email) + current_user.resend_confirmation_instructions + end + + render_empty + end + + private + + def require_user_owned_by_application! + render json: { error: 'This method is only available to the application the user originally signed-up with' }, status: :forbidden unless current_user && current_user.created_by_application_id == doorkeeper_token.application_id + end +end diff --git a/app/controllers/api/v1/follow_requests_controller.rb b/app/controllers/api/v1/follow_requests_controller.rb index b34c76f29..f4b2a74d0 100644 --- a/app/controllers/api/v1/follow_requests_controller.rb +++ b/app/controllers/api/v1/follow_requests_controller.rb @@ -29,7 +29,7 @@ class Api::V1::FollowRequestsController < Api::BaseController end def relationships(**options) - AccountRelationshipsPresenter.new([params[:id]], current_user.account_id, options) + AccountRelationshipsPresenter.new([params[:id]], current_user.account_id, **options) end def load_accounts diff --git a/app/controllers/api/v1/instances/rules_controller.rb b/app/controllers/api/v1/instances/rules_controller.rb new file mode 100644 index 000000000..93cf3c759 --- /dev/null +++ b/app/controllers/api/v1/instances/rules_controller.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class Api::V1::Instances::RulesController < Api::BaseController + skip_before_action :require_authenticated_user!, unless: :whitelist_mode? + + before_action :set_rules + + def index + render json: @rules, each_serializer: REST::RuleSerializer + end + + private + + def set_rules + @rules = Rule.ordered + end +end diff --git a/app/controllers/api/v1/markers_controller.rb b/app/controllers/api/v1/markers_controller.rb index 28c2ec791..867e6facf 100644 --- a/app/controllers/api/v1/markers_controller.rb +++ b/app/controllers/api/v1/markers_controller.rb @@ -7,7 +7,7 @@ class Api::V1::MarkersController < Api::BaseController before_action :require_user! def index - @markers = current_user.markers.where(timeline: Array(params[:timeline])).each_with_object({}) { |marker, h| h[marker.timeline] = marker } + @markers = current_user.markers.where(timeline: Array(params[:timeline])).index_by(&:timeline) render json: serialize_map(@markers) end diff --git a/app/controllers/api/v1/notifications_controller.rb b/app/controllers/api/v1/notifications_controller.rb index 522c35ba5..f9d780839 100644 --- a/app/controllers/api/v1/notifications_controller.rb +++ b/app/controllers/api/v1/notifications_controller.rb @@ -31,12 +31,13 @@ class Api::V1::NotificationsController < Api::BaseController private def load_notifications - cache_collection_paginated_by_id( - browserable_account_notifications, - Notification, + notifications = browserable_account_notifications.includes(from_account: :account_stat).to_a_paginated_by_id( limit_param(DEFAULT_NOTIFICATIONS_LIMIT), params_slice(:max_id, :since_id, :min_id) ) + Notification.preload_cache_collection_target_statuses(notifications) do |target_statuses| + cache_collection(target_statuses, Status) + end end def browserable_account_notifications diff --git a/app/controllers/api/v1/push/subscriptions_controller.rb b/app/controllers/api/v1/push/subscriptions_controller.rb index 0918c61e9..47f2e6440 100644 --- a/app/controllers/api/v1/push/subscriptions_controller.rb +++ b/app/controllers/api/v1/push/subscriptions_controller.rb @@ -3,13 +3,13 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController before_action -> { doorkeeper_authorize! :push } before_action :require_user! - before_action :set_web_push_subscription - before_action :check_web_push_subscription, only: [:show, :update] + before_action :set_push_subscription + before_action :check_push_subscription, only: [:show, :update] def create - @web_subscription&.destroy! + @push_subscription&.destroy! - @web_subscription = ::Web::PushSubscription.create!( + @push_subscription = Web::PushSubscription.create!( endpoint: subscription_params[:endpoint], key_p256dh: subscription_params[:keys][:p256dh], key_auth: subscription_params[:keys][:auth], @@ -18,31 +18,31 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController access_token_id: doorkeeper_token.id ) - render json: @web_subscription, serializer: REST::WebPushSubscriptionSerializer + render json: @push_subscription, serializer: REST::WebPushSubscriptionSerializer end def show - render json: @web_subscription, serializer: REST::WebPushSubscriptionSerializer + render json: @push_subscription, serializer: REST::WebPushSubscriptionSerializer end def update - @web_subscription.update!(data: data_params) - render json: @web_subscription, serializer: REST::WebPushSubscriptionSerializer + @push_subscription.update!(data: data_params) + render json: @push_subscription, serializer: REST::WebPushSubscriptionSerializer end def destroy - @web_subscription&.destroy! + @push_subscription&.destroy! render_empty end private - def set_web_push_subscription - @web_subscription = ::Web::PushSubscription.find_by(access_token_id: doorkeeper_token.id) + def set_push_subscription + @push_subscription = Web::PushSubscription.find_by(access_token_id: doorkeeper_token.id) end - def check_web_push_subscription - not_found if @web_subscription.nil? + def check_push_subscription + not_found if @push_subscription.nil? end def subscription_params @@ -52,6 +52,6 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController def data_params return {} if params[:data].blank? - params.require(:data).permit(alerts: [:follow, :follow_request, :favourite, :reblog, :mention, :poll, :status]) + params.require(:data).permit(:policy, alerts: [:follow, :follow_request, :favourite, :reblog, :mention, :poll, :status]) end end diff --git a/app/controllers/api/v1/suggestions_controller.rb b/app/controllers/api/v1/suggestions_controller.rb index 52054160d..9737ae5cb 100644 --- a/app/controllers/api/v1/suggestions_controller.rb +++ b/app/controllers/api/v1/suggestions_controller.rb @@ -5,20 +5,20 @@ class Api::V1::SuggestionsController < Api::BaseController before_action -> { doorkeeper_authorize! :read } before_action :require_user! - before_action :set_accounts def index - render json: @accounts, each_serializer: REST::AccountSerializer + suggestions = suggestions_source.get(current_account, limit: limit_param(DEFAULT_ACCOUNTS_LIMIT)) + render json: suggestions.map(&:account), each_serializer: REST::AccountSerializer end def destroy - PotentialFriendshipTracker.remove(current_account.id, params[:id]) + suggestions_source.remove(current_account, params[:id]) render_empty end private - def set_accounts - @accounts = PotentialFriendshipTracker.get(current_account.id, limit: limit_param(DEFAULT_ACCOUNTS_LIMIT)) + def suggestions_source + AccountSuggestions::PastInteractionsSource.new end end diff --git a/app/controllers/api/v2/suggestions_controller.rb b/app/controllers/api/v2/suggestions_controller.rb new file mode 100644 index 000000000..35eb276c0 --- /dev/null +++ b/app/controllers/api/v2/suggestions_controller.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class Api::V2::SuggestionsController < Api::BaseController + include Authorization + + before_action -> { doorkeeper_authorize! :read } + before_action :require_user! + before_action :set_suggestions + + def index + render json: @suggestions, each_serializer: REST::SuggestionSerializer + end + + private + + def set_suggestions + @suggestions = AccountSuggestions.get(current_account, limit_param(DEFAULT_ACCOUNTS_LIMIT)) + end +end diff --git a/app/controllers/api/web/push_subscriptions_controller.rb b/app/controllers/api/web/push_subscriptions_controller.rb index 1dce3e70f..bed57fc54 100644 --- a/app/controllers/api/web/push_subscriptions_controller.rb +++ b/app/controllers/api/web/push_subscriptions_controller.rb @@ -2,6 +2,7 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController before_action :require_user! + before_action :set_push_subscription, only: :update def create active_session = current_session @@ -15,9 +16,11 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController alerts_enabled = active_session.detection.device.mobile? || active_session.detection.device.tablet? data = { + policy: 'all', + alerts: { follow: alerts_enabled, - follow_request: false, + follow_request: alerts_enabled, favourite: alerts_enabled, reblog: alerts_enabled, mention: alerts_enabled, @@ -28,7 +31,7 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController data.deep_merge!(data_params) if params[:data] - web_subscription = ::Web::PushSubscription.create!( + push_subscription = ::Web::PushSubscription.create!( endpoint: subscription_params[:endpoint], key_p256dh: subscription_params[:keys][:p256dh], key_auth: subscription_params[:keys][:auth], @@ -37,27 +40,27 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController access_token_id: active_session.access_token_id ) - active_session.update!(web_push_subscription: web_subscription) + active_session.update!(web_push_subscription: push_subscription) - render json: web_subscription, serializer: REST::WebPushSubscriptionSerializer + render json: push_subscription, serializer: REST::WebPushSubscriptionSerializer end def update - params.require([:id]) - - web_subscription = ::Web::PushSubscription.find(params[:id]) - web_subscription.update!(data: data_params) - - render json: web_subscription, serializer: REST::WebPushSubscriptionSerializer + @push_subscription.update!(data: data_params) + render json: @push_subscription, serializer: REST::WebPushSubscriptionSerializer end private + def set_push_subscription + @push_subscription = ::Web::PushSubscription.find(params[:id]) + end + def subscription_params @subscription_params ||= params.require(:subscription).permit(:endpoint, keys: [:auth, :p256dh]) end def data_params - @data_params ||= params.require(:data).permit(alerts: [:follow, :follow_request, :favourite, :reblog, :mention, :poll, :status]) + @data_params ||= params.require(:data).permit(:policy, alerts: [:follow, :follow_request, :favourite, :reblog, :mention, :poll, :status]) end end diff --git a/app/controllers/api/web/settings_controller.rb b/app/controllers/api/web/settings_controller.rb index 3d65e46ed..601e25e6e 100644 --- a/app/controllers/api/web/settings_controller.rb +++ b/app/controllers/api/web/settings_controller.rb @@ -2,17 +2,16 @@ class Api::Web::SettingsController < Api::Web::BaseController before_action :require_user! + before_action :set_setting def update - setting.data = params[:data] - setting.save! - + @setting.update!(data: params[:data]) render_empty end private - def setting - @_setting ||= ::Web::Setting.where(user: current_user).first_or_initialize(user: current_user) + def set_setting + @setting = ::Web::Setting.where(user: current_user).first_or_initialize(user: current_user) end end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 44616d6e5..b4fb83661 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -5,8 +5,6 @@ class ApplicationController < ActionController::Base # For APIs, you may want to use :null_session instead. protect_from_forgery with: :exception - force_ssl if: :https_enabled? - include Localized include UserTrackingConcern include SessionTrackingConcern @@ -20,17 +18,16 @@ class ApplicationController < ActionController::Base helper_method :use_seamless_external_login? helper_method :whitelist_mode? - rescue_from ActionController::RoutingError, with: :not_found - rescue_from ActionController::InvalidAuthenticityToken, with: :unprocessable_entity - rescue_from ActionController::UnknownFormat, with: :not_acceptable - rescue_from ActionController::ParameterMissing, with: :bad_request - rescue_from Paperclip::AdapterRegistry::NoHandlerError, with: :bad_request - rescue_from ActiveRecord::RecordNotFound, with: :not_found + rescue_from ActionController::ParameterMissing, Paperclip::AdapterRegistry::NoHandlerError, with: :bad_request rescue_from Mastodon::NotPermittedError, with: :forbidden - rescue_from HTTP::Error, OpenSSL::SSL::SSLError, with: :internal_server_error - rescue_from Mastodon::RaceConditionError, Seahorse::Client::NetworkingError, Stoplight::Error::RedLight, with: :service_unavailable + rescue_from ActionController::RoutingError, ActiveRecord::RecordNotFound, with: :not_found + rescue_from ActionController::UnknownFormat, with: :not_acceptable + rescue_from ActionController::InvalidAuthenticityToken, with: :unprocessable_entity rescue_from Mastodon::RateLimitExceededError, with: :too_many_requests + rescue_from HTTP::Error, OpenSSL::SSL::SSLError, with: :internal_server_error + rescue_from Mastodon::RaceConditionError, Seahorse::Client::NetworkingError, Stoplight::Error::RedLight, ActiveRecord::SerializationFailure, with: :service_unavailable + before_action :store_current_location, except: :raise_not_found, unless: :devise_controller? before_action :require_functional!, if: :user_signed_in? @@ -42,10 +39,6 @@ class ApplicationController < ActionController::Base private - def https_enabled? - Rails.env.production? && !request.path.start_with?('/health') - end - def authorized_fetch_mode? ENV['AUTHORIZED_FETCH'] == 'true' || Rails.configuration.x.whitelist_mode end diff --git a/app/controllers/auth/confirmations_controller.rb b/app/controllers/auth/confirmations_controller.rb index 898525269..1475bbcef 100644 --- a/app/controllers/auth/confirmations_controller.rb +++ b/app/controllers/auth/confirmations_controller.rb @@ -17,7 +17,9 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController private def require_unconfirmed! - redirect_to edit_user_registration_path if user_signed_in? && current_user.confirmed? && current_user.unconfirmed_email.blank? + if user_signed_in? && current_user.confirmed? && current_user.unconfirmed_email.blank? + redirect_to(current_user.approved? ? root_path : edit_user_registration_path) + end end def set_body_classes diff --git a/app/controllers/concerns/cache_concern.rb b/app/controllers/concerns/cache_concern.rb index abbdb410a..05e431b19 100644 --- a/app/controllers/concerns/cache_concern.rb +++ b/app/controllers/concerns/cache_concern.rb @@ -31,21 +31,23 @@ module CacheConcern def cache_collection(raw, klass) return raw unless klass.respond_to?(:with_includes) - raw = raw.cache_ids.to_a if raw.is_a?(ActiveRecord::Relation) + raw = raw.cache_ids.to_a if raw.is_a?(ActiveRecord::Relation) + return [] if raw.empty? + cached_keys_with_value = Rails.cache.read_multi(*raw).transform_keys(&:id) uncached_ids = raw.map(&:id) - cached_keys_with_value.keys klass.reload_stale_associations!(cached_keys_with_value.values) if klass.respond_to?(:reload_stale_associations!) unless uncached_ids.empty? - uncached = klass.where(id: uncached_ids).with_includes.each_with_object({}) { |item, h| h[item.id] = item } + uncached = klass.where(id: uncached_ids).with_includes.index_by(&:id) uncached.each_value do |item| Rails.cache.write(item, item) end end - raw.map { |item| cached_keys_with_value[item.id] || uncached[item.id] }.compact + raw.filter_map { |item| cached_keys_with_value[item.id] || uncached[item.id] } end def cache_collection_paginated_by_id(raw, klass, limit, options) diff --git a/app/controllers/concerns/signature_verification.rb b/app/controllers/concerns/signature_verification.rb index fc3978fbb..4dd0cac55 100644 --- a/app/controllers/concerns/signature_verification.rb +++ b/app/controllers/concerns/signature_verification.rb @@ -133,6 +133,7 @@ module SignatureVerification def verify_body_digest! return unless signed_headers.include?('digest') + raise SignatureVerificationError, 'Digest header missing' unless request.headers.key?('Digest') digests = request.headers['Digest'].split(',').map { |digest| digest.split('=', 2) }.map { |key, value| [key.downcase, value] } sha256 = digests.assoc('sha-256') diff --git a/app/controllers/directories_controller.rb b/app/controllers/directories_controller.rb index f198ad5ba..f28c5b2af 100644 --- a/app/controllers/directories_controller.rb +++ b/app/controllers/directories_controller.rb @@ -6,7 +6,6 @@ class DirectoriesController < ApplicationController before_action :authenticate_user!, if: :whitelist_mode? before_action :require_enabled! before_action :set_instance_presenter - before_action :set_tag, only: :show before_action :set_accounts skip_before_action :require_functional!, unless: :whitelist_mode? @@ -15,23 +14,14 @@ class DirectoriesController < ApplicationController render :index end - def show - render :index - end - private def require_enabled! return not_found unless Setting.profile_directory end - def set_tag - @tag = Tag.discoverable.find_normalized!(params[:id]) - end - def set_accounts @accounts = Account.local.discoverable.by_recent_status.page(params[:page]).per(20).tap do |query| - query.merge!(Account.tagged_with(@tag.id)) if @tag query.merge!(Account.not_excluded_by_account(current_account)) if current_account end end diff --git a/app/controllers/health_controller.rb b/app/controllers/health_controller.rb new file mode 100644 index 000000000..2a22a0557 --- /dev/null +++ b/app/controllers/health_controller.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class HealthController < ActionController::Base + def show + render plain: 'OK' + end +end diff --git a/app/controllers/instance_actors_controller.rb b/app/controllers/instance_actors_controller.rb index 4b074ca19..b3b5476e2 100644 --- a/app/controllers/instance_actors_controller.rb +++ b/app/controllers/instance_actors_controller.rb @@ -13,7 +13,7 @@ class InstanceActorsController < ApplicationController private def set_account - @account = Account.find(-99) + @account = Account.representative end def restrict_fields_to diff --git a/app/controllers/media_proxy_controller.rb b/app/controllers/media_proxy_controller.rb index 0b1d09de9..1b610318d 100644 --- a/app/controllers/media_proxy_controller.rb +++ b/app/controllers/media_proxy_controller.rb @@ -37,7 +37,7 @@ class MediaProxyController < ApplicationController end def version - if request.path.ends_with?('/small') + if request.path.end_with?('/small') :small else :original diff --git a/app/controllers/settings/pictures_controller.rb b/app/controllers/settings/pictures_controller.rb index 28df65f8f..58a432530 100644 --- a/app/controllers/settings/pictures_controller.rb +++ b/app/controllers/settings/pictures_controller.rb @@ -7,8 +7,12 @@ module Settings def destroy if valid_picture? - msg = I18n.t('generic.changes_saved_msg') if UpdateAccountService.new.call(@account, { @picture => nil, "#{@picture}_remote_url" => '' }) - redirect_to settings_profile_path, notice: msg, status: 303 + if UpdateAccountService.new.call(@account, { @picture => nil, "#{@picture}_remote_url" => '' }) + ActivityPub::UpdateDistributionWorker.perform_async(@account.id) + redirect_to settings_profile_path, notice: I18n.t('generic.changes_saved_msg'), status: 303 + else + redirect_to settings_profile_path + end else bad_request end diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb index 17ddd31fb..c52170d08 100644 --- a/app/controllers/statuses_controller.rb +++ b/app/controllers/statuses_controller.rb @@ -8,7 +8,7 @@ class StatusesController < ApplicationController layout 'public' - before_action :require_signature!, only: :show, if: -> { request.format == :json && authorized_fetch_mode? } + before_action :require_signature!, only: [:show, :activity], if: -> { request.format == :json && authorized_fetch_mode? } before_action :set_status before_action :set_instance_presenter before_action :set_link_headers @@ -16,7 +16,6 @@ class StatusesController < ApplicationController before_action :set_referrer_policy_header, only: :show before_action :set_cache_headers before_action :set_body_classes - before_action :set_autoplay, only: :embed skip_around_action :set_locale, if: -> { request.format == :json } skip_before_action :require_functional!, only: [:show, :embed], unless: :whitelist_mode? @@ -82,8 +81,4 @@ class StatusesController < ApplicationController def set_referrer_policy_header response.headers['Referrer-Policy'] = 'origin' unless @status.distributable? end - - def set_autoplay - @autoplay = truthy_param?(:autoplay) - end end diff --git a/app/helpers/admin/action_logs_helper.rb b/app/helpers/admin/action_logs_helper.rb index 0f3ca36e2..e9a298a24 100644 --- a/app/helpers/admin/action_logs_helper.rb +++ b/app/helpers/admin/action_logs_helper.rb @@ -21,7 +21,7 @@ module Admin::ActionLogsHelper record.shortcode when 'Report' link_to "##{record.id}", admin_report_path(record) - when 'DomainBlock', 'DomainAllow', 'EmailDomainBlock' + when 'DomainBlock', 'DomainAllow', 'EmailDomainBlock', 'UnavailableDomain' link_to record.domain, "https://#{record.domain}" when 'Status' link_to record.account.acct, ActivityPub::TagManager.instance.url_for(record) @@ -38,7 +38,7 @@ module Admin::ActionLogsHelper case type when 'CustomEmoji' attributes['shortcode'] - when 'DomainBlock', 'DomainAllow', 'EmailDomainBlock' + when 'DomainBlock', 'DomainAllow', 'EmailDomainBlock', 'UnavailableDomain' link_to attributes['domain'], "https://#{attributes['domain']}" when 'Status' tmp_status = Status.new(attributes.except('reblogs_count', 'favourites_count')) diff --git a/app/helpers/email_helper.rb b/app/helpers/email_helper.rb new file mode 100644 index 000000000..360783c62 --- /dev/null +++ b/app/helpers/email_helper.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module EmailHelper + def self.included(base) + base.extend(self) + end + + def email_to_canonical_email(str) + username, domain = str.downcase.split('@', 2) + username, = username.gsub('.', '').split('+', 2) + + "#{username}@#{domain}" + end + + def email_to_canonical_email_hash(str) + Digest::SHA2.new(256).hexdigest(email_to_canonical_email(str)) + end +end diff --git a/app/helpers/jsonld_helper.rb b/app/helpers/jsonld_helper.rb index 1c473efa3..62eb50f78 100644 --- a/app/helpers/jsonld_helper.rb +++ b/app/helpers/jsonld_helper.rb @@ -67,7 +67,7 @@ module JsonLdHelper unless id json = fetch_resource_without_id_validation(uri, on_behalf_of) - return unless json + return if !json.is_a?(Hash) || unsupported_uri_scheme?(json['id']) uri = json['id'] end diff --git a/app/helpers/mascot_helper.rb b/app/helpers/mascot_helper.rb new file mode 100644 index 000000000..0124c74f1 --- /dev/null +++ b/app/helpers/mascot_helper.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module MascotHelper + def mascot_url + full_asset_url(instance_presenter.mascot&.file&.url || asset_pack_path('media/images/elephant_ui_plane.svg')) + end + + private + + def instance_presenter + @instance_presenter ||= InstancePresenter.new + end +end diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb index 5b39497b6..b60901040 100644 --- a/app/helpers/settings_helper.rb +++ b/app/helpers/settings_helper.rb @@ -2,6 +2,7 @@ module SettingsHelper HUMAN_LOCALES = { + af: 'Afrikaans', ar: 'العربية', ast: 'Asturianu', bg: 'Български', @@ -24,6 +25,7 @@ module SettingsHelper fi: 'Suomi', fr: 'Français', ga: 'Gaeilge', + gd: 'Gàidhlig', gl: 'Galego', he: 'עברית', hi: 'हिन्दी', @@ -59,6 +61,7 @@ module SettingsHelper ru: 'Русский', sa: 'संस्कृतम्', sc: 'Sardu', + si: 'සිංහල', sk: 'Slovenčina', sl: 'Slovenščina', sq: 'Shqip', diff --git a/app/helpers/statuses_helper.rb b/app/helpers/statuses_helper.rb index 1f654f34f..25f079e9d 100644 --- a/app/helpers/statuses_helper.rb +++ b/app/helpers/statuses_helper.rb @@ -130,4 +130,84 @@ module StatusesHelper def embedded_view? params[:controller] == EMBEDDED_CONTROLLER && params[:action] == EMBEDDED_ACTION end + + def render_video_component(status, **options) + video = status.media_attachments.first + + meta = video.file.meta || {} + + component_params = { + sensitive: sensitized?(status, current_account), + src: full_asset_url(video.file.url(:original)), + preview: full_asset_url(video.thumbnail.present? ? video.thumbnail.url : video.file.url(:small)), + alt: video.description, + blurhash: video.blurhash, + frameRate: meta.dig('original', 'frame_rate'), + inline: true, + media: [ + ActiveModelSerializers::SerializableResource.new(video, serializer: REST::MediaAttachmentSerializer), + ].as_json, + }.merge(**options) + + react_component :video, component_params do + render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments } + end + end + + def render_audio_component(status, **options) + audio = status.media_attachments.first + + meta = audio.file.meta || {} + + component_params = { + src: full_asset_url(audio.file.url(:original)), + poster: full_asset_url(audio.thumbnail.present? ? audio.thumbnail.url : status.account.avatar_static_url), + alt: audio.description, + backgroundColor: meta.dig('colors', 'background'), + foregroundColor: meta.dig('colors', 'foreground'), + accentColor: meta.dig('colors', 'accent'), + duration: meta.dig('original', 'duration'), + }.merge(**options) + + react_component :audio, component_params do + render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments } + end + end + + def render_media_gallery_component(status, **options) + component_params = { + sensitive: sensitized?(status, current_account), + autoplay: prefers_autoplay?, + media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json }, + }.merge(**options) + + react_component :media_gallery, component_params do + render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments } + end + end + + def render_card_component(status, **options) + component_params = { + sensitive: sensitized?(status, current_account), + maxDescription: 160, + card: ActiveModelSerializers::SerializableResource.new(status.preview_card, serializer: REST::PreviewCardSerializer).as_json, + }.merge(**options) + + react_component :card, component_params + end + + def render_poll_component(status, **options) + component_params = { + disabled: true, + poll: ActiveModelSerializers::SerializableResource.new(status.preloadable_poll, serializer: REST::PollSerializer, scope: current_user, scope_name: :current_user).as_json, + }.merge(**options) + + react_component :poll, component_params do + render partial: 'statuses/poll', locals: { status: status, poll: status.preloadable_poll, autoplay: prefers_autoplay? } + end + end + + def prefers_autoplay? + ActiveModel::Type::Boolean.new.cast(params[:autoplay]) || current_user&.setting_auto_play_gif + end end diff --git a/app/javascript/mastodon/actions/boosts.js b/app/javascript/mastodon/actions/boosts.js new file mode 100644 index 000000000..c0f0f3acc --- /dev/null +++ b/app/javascript/mastodon/actions/boosts.js @@ -0,0 +1,29 @@ +import { openModal } from './modal'; + +export const BOOSTS_INIT_MODAL = 'BOOSTS_INIT_MODAL'; +export const BOOSTS_CHANGE_PRIVACY = 'BOOSTS_CHANGE_PRIVACY'; + +export function initBoostModal(props) { + return (dispatch, getState) => { + const default_privacy = getState().getIn(['compose', 'default_privacy']); + + const privacy = props.status.get('visibility') === 'private' ? 'private' : default_privacy; + + dispatch({ + type: BOOSTS_INIT_MODAL, + privacy, + }); + + dispatch(openModal('BOOST', props)); + }; +} + + +export function changeBoostPrivacy(privacy) { + return dispatch => { + dispatch({ + type: BOOSTS_CHANGE_PRIVACY, + privacy, + }); + }; +} diff --git a/app/javascript/mastodon/actions/importer/normalizer.js b/app/javascript/mastodon/actions/importer/normalizer.js index dca44917a..5002292b9 100644 --- a/app/javascript/mastodon/actions/importer/normalizer.js +++ b/app/javascript/mastodon/actions/importer/normalizer.js @@ -24,6 +24,7 @@ export function normalizeAccount(account) { account.display_name_html = emojify(escapeTextContentForBrowser(displayName), emojiMap); account.note_emojified = emojify(account.note, emojiMap); + account.note_plain = unescapeHTML(account.note); if (account.fields) { account.fields = account.fields.map(pair => ({ @@ -59,8 +60,16 @@ export function normalizeStatus(status, normalOldStatus) { normalStatus.search_index = normalOldStatus.get('search_index'); normalStatus.contentHtml = normalOldStatus.get('contentHtml'); normalStatus.spoilerHtml = normalOldStatus.get('spoilerHtml'); + normalStatus.spoiler_text = normalOldStatus.get('spoiler_text'); normalStatus.hidden = normalOldStatus.get('hidden'); } else { + // If the status has a CW but no contents, treat the CW as if it were the + // status' contents, to avoid having a CW toggle with seemingly no effect. + if (normalStatus.spoiler_text && !normalStatus.content) { + normalStatus.content = normalStatus.spoiler_text; + normalStatus.spoiler_text = ''; + } + const spoilerText = normalStatus.spoiler_text || ''; const searchContent = ([spoilerText, status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).join('\n\n').replace(//g, '\n').replace(/<\/p>

/g, '\n\n'); const emojiMap = makeEmojiMap(normalStatus); diff --git a/app/javascript/mastodon/actions/interactions.js b/app/javascript/mastodon/actions/interactions.js index 28c6b1a62..d60ccc1fb 100644 --- a/app/javascript/mastodon/actions/interactions.js +++ b/app/javascript/mastodon/actions/interactions.js @@ -41,11 +41,11 @@ export const UNBOOKMARK_REQUEST = 'UNBOOKMARKED_REQUEST'; export const UNBOOKMARK_SUCCESS = 'UNBOOKMARKED_SUCCESS'; export const UNBOOKMARK_FAIL = 'UNBOOKMARKED_FAIL'; -export function reblog(status) { +export function reblog(status, visibility) { return function (dispatch, getState) { dispatch(reblogRequest(status)); - api(getState).post(`/api/v1/statuses/${status.get('id')}/reblog`).then(function (response) { + api(getState).post(`/api/v1/statuses/${status.get('id')}/reblog`, { visibility }).then(function (response) { // The reblog API method returns a new status wrapped around the original. In this case we are only // interested in how the original is modified, hence passing it skipping the wrapper dispatch(importFetchedStatus(response.data.reblog)); diff --git a/app/javascript/mastodon/actions/onboarding.js b/app/javascript/mastodon/actions/onboarding.js index 42d8ea33f..a1dd3a731 100644 --- a/app/javascript/mastodon/actions/onboarding.js +++ b/app/javascript/mastodon/actions/onboarding.js @@ -1,21 +1,8 @@ import { changeSetting, saveSettings } from './settings'; -import { requestBrowserPermission } from './notifications'; export const INTRODUCTION_VERSION = 20181216044202; export const closeOnboarding = () => dispatch => { dispatch(changeSetting(['introductionVersion'], INTRODUCTION_VERSION)); dispatch(saveSettings()); - - dispatch(requestBrowserPermission((permission) => { - if (permission === 'granted') { - dispatch(changeSetting(['notifications', 'alerts', 'follow'], true)); - dispatch(changeSetting(['notifications', 'alerts', 'favourite'], true)); - dispatch(changeSetting(['notifications', 'alerts', 'reblog'], true)); - dispatch(changeSetting(['notifications', 'alerts', 'mention'], true)); - dispatch(changeSetting(['notifications', 'alerts', 'poll'], true)); - dispatch(changeSetting(['notifications', 'alerts', 'status'], true)); - dispatch(saveSettings()); - } - })); }; diff --git a/app/javascript/mastodon/actions/search.js b/app/javascript/mastodon/actions/search.js index a178faead..37560a74c 100644 --- a/app/javascript/mastodon/actions/search.js +++ b/app/javascript/mastodon/actions/search.js @@ -32,6 +32,7 @@ export function submitSearch() { const value = getState().getIn(['search', 'value']); if (value.length === 0) { + dispatch(fetchSearchSuccess({ accounts: [], statuses: [], hashtags: [] }, '')); return; } diff --git a/app/javascript/mastodon/actions/suggestions.js b/app/javascript/mastodon/actions/suggestions.js index b15bd916b..1f1116e75 100644 --- a/app/javascript/mastodon/actions/suggestions.js +++ b/app/javascript/mastodon/actions/suggestions.js @@ -1,5 +1,6 @@ import api from '../api'; import { importFetchedAccounts } from './importer'; +import { fetchRelationships } from './accounts'; export const SUGGESTIONS_FETCH_REQUEST = 'SUGGESTIONS_FETCH_REQUEST'; export const SUGGESTIONS_FETCH_SUCCESS = 'SUGGESTIONS_FETCH_SUCCESS'; @@ -7,13 +8,17 @@ export const SUGGESTIONS_FETCH_FAIL = 'SUGGESTIONS_FETCH_FAIL'; export const SUGGESTIONS_DISMISS = 'SUGGESTIONS_DISMISS'; -export function fetchSuggestions() { +export function fetchSuggestions(withRelationships = false) { return (dispatch, getState) => { dispatch(fetchSuggestionsRequest()); - api(getState).get('/api/v1/suggestions').then(response => { - dispatch(importFetchedAccounts(response.data)); + api(getState).get('/api/v2/suggestions', { params: { limit: 20 } }).then(response => { + dispatch(importFetchedAccounts(response.data.map(x => x.account))); dispatch(fetchSuggestionsSuccess(response.data)); + + if (withRelationships) { + dispatch(fetchRelationships(response.data.map(item => item.account.id))); + } }).catch(error => dispatch(fetchSuggestionsFail(error))); }; }; @@ -25,10 +30,10 @@ export function fetchSuggestionsRequest() { }; }; -export function fetchSuggestionsSuccess(accounts) { +export function fetchSuggestionsSuccess(suggestions) { return { type: SUGGESTIONS_FETCH_SUCCESS, - accounts, + suggestions, skipLoading: true, }; }; @@ -48,5 +53,12 @@ export const dismissSuggestion = accountId => (dispatch, getState) => { id: accountId, }); - api(getState).delete(`/api/v1/suggestions/${accountId}`); + api(getState).delete(`/api/v1/suggestions/${accountId}`).then(() => { + dispatch(fetchSuggestionsRequest()); + + api(getState).get('/api/v2/suggestions').then(response => { + dispatch(importFetchedAccounts(response.data.map(x => x.account))); + dispatch(fetchSuggestionsSuccess(response.data)); + }).catch(error => dispatch(fetchSuggestionsFail(error))); + }).catch(() => {}); }; diff --git a/app/javascript/mastodon/actions/timelines.js b/app/javascript/mastodon/actions/timelines.js index de1725acf..31ae09e4a 100644 --- a/app/javascript/mastodon/actions/timelines.js +++ b/app/javascript/mastodon/actions/timelines.js @@ -18,17 +18,26 @@ export const TIMELINE_LOAD_PENDING = 'TIMELINE_LOAD_PENDING'; export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT'; export const TIMELINE_CONNECT = 'TIMELINE_CONNECT'; +export const TIMELINE_MARK_AS_PARTIAL = 'TIMELINE_MARK_AS_PARTIAL'; + export const loadPending = timeline => ({ type: TIMELINE_LOAD_PENDING, timeline, }); export function updateTimeline(timeline, status, accept) { - return dispatch => { + return (dispatch, getState) => { if (typeof accept === 'function' && !accept(status)) { return; } + if (getState().getIn(['timelines', timeline, 'isPartial'])) { + // Prevent new items from being added to a partial timeline, + // since it will be reloaded anyway + + return; + } + dispatch(importFetchedStatus(status)); dispatch({ @@ -183,3 +192,8 @@ export const disconnectTimeline = timeline => ({ timeline, usePendingItems: preferPendingItems, }); + +export const markAsPartial = timeline => ({ + type: TIMELINE_MARK_AS_PARTIAL, + timeline, +}); diff --git a/app/javascript/mastodon/components/__tests__/__snapshots__/display_name-test.js.snap b/app/javascript/mastodon/components/__tests__/__snapshots__/display_name-test.js.snap index 29fdc2412..0f27473af 100644 --- a/app/javascript/mastodon/components/__tests__/__snapshots__/display_name-test.js.snap +++ b/app/javascript/mastodon/components/__tests__/__snapshots__/display_name-test.js.snap @@ -3,6 +3,8 @@ exports[` renders display name + account name 1`] = ` ; + if (actionIcon) { + if (onActionClick) { + buttons = ; + } } else if (account.get('id') !== me && account.get('relationship', null) !== null) { const following = account.getIn(['relationship', 'following']); const requested = account.getIn(['relationship', 'requested']); diff --git a/app/javascript/mastodon/components/autosuggest_input.js b/app/javascript/mastodon/components/autosuggest_input.js index 5187f95c8..12d44b5d0 100644 --- a/app/javascript/mastodon/components/autosuggest_input.js +++ b/app/javascript/mastodon/components/autosuggest_input.js @@ -6,7 +6,6 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; import ImmutablePureComponent from 'react-immutable-pure-component'; import classNames from 'classnames'; -import { List as ImmutableList } from 'immutable'; const textAtCursorMatchesToken = (str, caretPosition, searchTokens) => { let word; @@ -55,7 +54,7 @@ export default class AutosuggestInput extends ImmutablePureComponent { static defaultProps = { autoFocus: true, - searchTokens: ImmutableList(['@', ':', '#']), + searchTokens: ['@', ':', '#'], }; state = { diff --git a/app/javascript/mastodon/components/display_name.js b/app/javascript/mastodon/components/display_name.js index 70ef82789..7ccfbd0cd 100644 --- a/app/javascript/mastodon/components/display_name.js +++ b/app/javascript/mastodon/components/display_name.js @@ -11,45 +11,30 @@ export default class DisplayName extends React.PureComponent { localDomain: PropTypes.string, }; - _updateEmojis () { - const node = this.node; - - if (!node || autoPlayGif) { + handleMouseEnter = ({ currentTarget }) => { + if (autoPlayGif) { return; } - const emojis = node.querySelectorAll('.custom-emoji'); + const emojis = currentTarget.querySelectorAll('.custom-emoji'); for (var i = 0; i < emojis.length; i++) { let emoji = emojis[i]; - if (emoji.classList.contains('status-emoji')) { - continue; - } - emoji.classList.add('status-emoji'); - - emoji.addEventListener('mouseenter', this.handleEmojiMouseEnter, false); - emoji.addEventListener('mouseleave', this.handleEmojiMouseLeave, false); + emoji.src = emoji.getAttribute('data-original'); } } - componentDidMount () { - this._updateEmojis(); - } + handleMouseLeave = ({ currentTarget }) => { + if (autoPlayGif) { + return; + } - componentDidUpdate () { - this._updateEmojis(); - } + const emojis = currentTarget.querySelectorAll('.custom-emoji'); - handleEmojiMouseEnter = ({ target }) => { - target.src = target.getAttribute('data-original'); - } - - handleEmojiMouseLeave = ({ target }) => { - target.src = target.getAttribute('data-static'); - } - - setRef = (c) => { - this.node = c; + for (var i = 0; i < emojis.length; i++) { + let emoji = emojis[i]; + emoji.src = emoji.getAttribute('data-static'); + } } render () { @@ -81,7 +66,7 @@ export default class DisplayName extends React.PureComponent { } return ( - + {displayName} {suffix} ); diff --git a/app/javascript/mastodon/components/dropdown_menu.js b/app/javascript/mastodon/components/dropdown_menu.js index c6b4b1187..7d0588901 100644 --- a/app/javascript/mastodon/components/dropdown_menu.js +++ b/app/javascript/mastodon/components/dropdown_menu.js @@ -177,7 +177,6 @@ export default class Dropdown extends React.PureComponent { disabled: PropTypes.bool, status: ImmutablePropTypes.map, isUserTouching: PropTypes.func, - isModalOpen: PropTypes.bool.isRequired, onOpen: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired, dropdownPlacement: PropTypes.string, diff --git a/app/javascript/mastodon/components/hashtag.js b/app/javascript/mastodon/components/hashtag.js index d766ca90d..75fcf20e3 100644 --- a/app/javascript/mastodon/components/hashtag.js +++ b/app/javascript/mastodon/components/hashtag.js @@ -2,10 +2,35 @@ import React from 'react'; import { Sparklines, SparklinesCurve } from 'react-sparklines'; import { FormattedMessage } from 'react-intl'; +import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import Permalink from './permalink'; import ShortNumber from 'mastodon/components/short_number'; +class SilentErrorBoundary extends React.Component { + + static propTypes = { + children: PropTypes.node, + }; + + state = { + error: false, + }; + + componentDidCatch () { + this.setState({ error: true }); + } + + render () { + if (this.state.error) { + return null; + } + + return this.props.children; + } + +} + /** * Used to render counter of how much people are talking about hashtag * @@ -51,17 +76,19 @@ const Hashtag = ({ hashtag }) => (

- day.get('uses')) - .toArray()} - > - - + + day.get('uses')) + .toArray()} + > + + +
); diff --git a/app/javascript/mastodon/components/logo.js b/app/javascript/mastodon/components/logo.js new file mode 100644 index 000000000..d1c7f08a9 --- /dev/null +++ b/app/javascript/mastodon/components/logo.js @@ -0,0 +1,9 @@ +import React from 'react'; + +const Logo = () => ( + + + +); + +export default Logo; diff --git a/app/javascript/mastodon/components/media_gallery.js b/app/javascript/mastodon/components/media_gallery.js index 0a8f42585..2e7ce2e60 100644 --- a/app/javascript/mastodon/components/media_gallery.js +++ b/app/javascript/mastodon/components/media_gallery.js @@ -11,7 +11,7 @@ import { debounce } from 'lodash'; import Blurhash from 'mastodon/components/blurhash'; const messages = defineMessages({ - toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: 'Hide {number, plural, one {image} other {images}}' }, + toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: '{number, plural, one {Hide image} other {Hide images}}' }, }); class Item extends React.PureComponent { diff --git a/app/javascript/mastodon/components/poll.js b/app/javascript/mastodon/components/poll.js index 41c99710f..477f56e13 100644 --- a/app/javascript/mastodon/components/poll.js +++ b/app/javascript/mastodon/components/poll.js @@ -153,7 +153,7 @@ class Poll extends ImmutablePureComponent { } diff --git a/app/javascript/mastodon/components/regeneration_indicator.js b/app/javascript/mastodon/components/regeneration_indicator.js index faf88c6b5..52696a4a7 100644 --- a/app/javascript/mastodon/components/regeneration_indicator.js +++ b/app/javascript/mastodon/components/regeneration_indicator.js @@ -2,7 +2,7 @@ import React from 'react'; import { FormattedMessage } from 'react-intl'; import illustration from 'mastodon/../images/elephant_ui_working.svg'; -const MissingIndicator = () => ( +const RegenerationIndicator = () => (
@@ -15,4 +15,4 @@ const MissingIndicator = () => (
); -export default MissingIndicator; +export default RegenerationIndicator; diff --git a/app/javascript/mastodon/components/status_action_bar.js b/app/javascript/mastodon/components/status_action_bar.js index 3097334d5..c74b174a9 100644 --- a/app/javascript/mastodon/components/status_action_bar.js +++ b/app/javascript/mastodon/components/status_action_bar.js @@ -224,11 +224,12 @@ class StatusActionBar extends ImmutablePureComponent { render () { const { status, relationship, intl, withDismiss, scrollKey } = this.props; - const mutingConversation = status.get('muted'); const anonymousAccess = !me; const publicStatus = ['public', 'unlisted'].includes(status.get('visibility')); + const mutingConversation = status.get('muted'); const account = status.get('account'); const federated = !status.get('local_only'); + const writtenByMe = status.getIn(['account', 'id']) === me; let menu = []; @@ -239,19 +240,22 @@ class StatusActionBar extends ImmutablePureComponent { menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed }); } - menu.push({ text: intl.formatMessage(status.get('bookmarked') ? messages.removeBookmark : messages.bookmark), action: this.handleBookmarkClick }); menu.push(null); - if (status.getIn(['account', 'id']) === me || withDismiss) { + menu.push({ text: intl.formatMessage(status.get('bookmarked') ? messages.removeBookmark : messages.bookmark), action: this.handleBookmarkClick }); + + if (writtenByMe && publicStatus) { + menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick }); + } + + menu.push(null); + + if (writtenByMe || withDismiss) { menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick }); menu.push(null); } - if (status.getIn(['account', 'id']) === me) { - if (publicStatus) { - menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick }); - } - + if (writtenByMe) { menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick }); menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick }); } else { diff --git a/app/javascript/mastodon/components/status_content.js b/app/javascript/mastodon/components/status_content.js index f39bd602f..8542fedc3 100644 --- a/app/javascript/mastodon/components/status_content.js +++ b/app/javascript/mastodon/components/status_content.js @@ -75,35 +75,38 @@ export default class StatusContent extends React.PureComponent { } } - _updateStatusEmojis () { - const node = this.node; - - if (!node || autoPlayGif) { + handleMouseEnter = ({ currentTarget }) => { + if (autoPlayGif) { return; } - const emojis = node.querySelectorAll('.custom-emoji'); + const emojis = currentTarget.querySelectorAll('.custom-emoji'); for (var i = 0; i < emojis.length; i++) { let emoji = emojis[i]; - if (emoji.classList.contains('status-emoji')) { - continue; - } - emoji.classList.add('status-emoji'); + emoji.src = emoji.getAttribute('data-original'); + } + } - emoji.addEventListener('mouseenter', this.handleEmojiMouseEnter, false); - emoji.addEventListener('mouseleave', this.handleEmojiMouseLeave, false); + handleMouseLeave = ({ currentTarget }) => { + if (autoPlayGif) { + return; + } + + const emojis = currentTarget.querySelectorAll('.custom-emoji'); + + for (var i = 0; i < emojis.length; i++) { + let emoji = emojis[i]; + emoji.src = emoji.getAttribute('data-static'); } } componentDidMount () { this._updateStatusLinks(); - this._updateStatusEmojis(); } componentDidUpdate () { this._updateStatusLinks(); - this._updateStatusEmojis(); } onMentionClick = (mention, e) => { @@ -122,14 +125,6 @@ export default class StatusContent extends React.PureComponent { } } - handleEmojiMouseEnter = ({ target }) => { - target.src = target.getAttribute('data-original'); - } - - handleEmojiMouseLeave = ({ target }) => { - target.src = target.getAttribute('data-static'); - } - handleMouseDown = (e) => { this.startXY = [e.clientX, e.clientY]; } @@ -175,10 +170,6 @@ export default class StatusContent extends React.PureComponent { render () { const { status } = this.props; - if (status.get('content').length === 0) { - return null; - } - const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden; const renderReadMore = this.props.onClick && status.get('collapsed'); const renderViewThread = this.props.showThread && status.get('in_reply_to_id') && status.get('in_reply_to_account_id') === status.getIn(['account', 'id']); @@ -225,16 +216,16 @@ export default class StatusContent extends React.PureComponent { } const output = [ -
+
{mentionsPlaceholder} -
+
{!hidden && !!status.get('poll') && } {renderViewThread && showThreadButton} @@ -248,8 +239,8 @@ export default class StatusContent extends React.PureComponent { return output; } else if (this.props.onClick) { const output = [ -
-
+
+
{!!status.get('poll') && } @@ -264,8 +255,8 @@ export default class StatusContent extends React.PureComponent { return output; } else { return ( -
-
+
+
{!!status.get('poll') && } diff --git a/app/javascript/mastodon/containers/dropdown_menu_container.js b/app/javascript/mastodon/containers/dropdown_menu_container.js index 6ec9bbffd..c45bab40b 100644 --- a/app/javascript/mastodon/containers/dropdown_menu_container.js +++ b/app/javascript/mastodon/containers/dropdown_menu_container.js @@ -6,7 +6,6 @@ import DropdownMenu from '../components/dropdown_menu'; import { isUserTouching } from '../is_mobile'; const mapStateToProps = state => ({ - isModalOpen: state.get('modal').modalType === 'ACTIONS', dropdownPlacement: state.getIn(['dropdown_menu', 'placement']), openDropdownId: state.getIn(['dropdown_menu', 'openId']), openedViaKeyboard: state.getIn(['dropdown_menu', 'keyboard']), diff --git a/app/javascript/mastodon/containers/mastodon.js b/app/javascript/mastodon/containers/mastodon.js index 3ac58cf7c..513b59908 100644 --- a/app/javascript/mastodon/containers/mastodon.js +++ b/app/javascript/mastodon/containers/mastodon.js @@ -1,12 +1,10 @@ import React from 'react'; -import { Provider, connect } from 'react-redux'; +import { Provider } from 'react-redux'; import PropTypes from 'prop-types'; import configureStore from '../store/configureStore'; -import { INTRODUCTION_VERSION } from '../actions/onboarding'; import { BrowserRouter, Route } from 'react-router-dom'; import { ScrollContext } from 'react-router-scroll-4'; import UI from '../features/ui'; -import Introduction from '../features/introduction'; import { fetchCustomEmojis } from '../actions/custom_emojis'; import { hydrateStore } from '../actions/store'; import { connectUserStream } from '../actions/streaming'; @@ -26,39 +24,6 @@ const hydrateAction = hydrateStore(initialState); store.dispatch(hydrateAction); store.dispatch(fetchCustomEmojis()); -const mapStateToProps = state => ({ - showIntroduction: state.getIn(['settings', 'introductionVersion'], 0) < INTRODUCTION_VERSION, -}); - -@connect(mapStateToProps) -class MastodonMount extends React.PureComponent { - - static propTypes = { - showIntroduction: PropTypes.bool, - }; - - shouldUpdateScroll (_, { location }) { - return location.state !== previewMediaState && location.state !== previewVideoState; - } - - render () { - const { showIntroduction } = this.props; - - if (showIntroduction) { - return ; - } - - return ( - - - - - - ); - } - -} - export default class Mastodon extends React.PureComponent { static propTypes = { @@ -76,6 +41,10 @@ export default class Mastodon extends React.PureComponent { } } + shouldUpdateScroll (_, { location }) { + return location.state !== previewMediaState && location.state !== previewVideoState; + } + render () { const { locale } = this.props; @@ -83,7 +52,11 @@ export default class Mastodon extends React.PureComponent { - + + + + + diff --git a/app/javascript/mastodon/containers/media_container.js b/app/javascript/mastodon/containers/media_container.js index afed6868e..52fdc9294 100644 --- a/app/javascript/mastodon/containers/media_container.js +++ b/app/javascript/mastodon/containers/media_container.js @@ -2,7 +2,7 @@ import React, { PureComponent, Fragment } from 'react'; import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; import { IntlProvider, addLocaleData } from 'react-intl'; -import { List as ImmutableList, fromJS } from 'immutable'; +import { fromJS } from 'immutable'; import { getLocale } from 'mastodon/locales'; import { getScrollbarWidth } from 'mastodon/utils/scrollbar'; import MediaGallery from 'mastodon/components/media_gallery'; @@ -31,6 +31,7 @@ export default class MediaContainer extends PureComponent { index: null, time: null, backgroundColor: null, + options: null, }; handleOpenMedia = (media, index) => { @@ -40,13 +41,15 @@ export default class MediaContainer extends PureComponent { this.setState({ media, index }); } - handleOpenVideo = (video, time) => { - const media = ImmutableList([video]); + handleOpenVideo = (options) => { + const { components } = this.props; + const { media } = JSON.parse(components[options.componetIndex].getAttribute('data-props')); + const mediaList = fromJS(media); document.body.classList.add('with-modals--active'); document.documentElement.style.marginRight = `${getScrollbarWidth()}px`; - this.setState({ media, time }); + this.setState({ media: mediaList, options }); } handleCloseMedia = () => { @@ -58,6 +61,7 @@ export default class MediaContainer extends PureComponent { index: null, time: null, backgroundColor: null, + options: null, }); } @@ -83,6 +87,7 @@ export default class MediaContainer extends PureComponent { ...(hashtag ? { hashtag: fromJS(hashtag) } : {}), ...(componentName === 'Video' ? { + componetIndex: i, onOpenVideo: this.handleOpenVideo, } : { onOpenMedia: this.handleOpenMedia, @@ -100,7 +105,9 @@ export default class MediaContainer extends PureComponent { diff --git a/app/javascript/mastodon/containers/status_container.js b/app/javascript/mastodon/containers/status_container.js index d6bcb8973..9abdec138 100644 --- a/app/javascript/mastodon/containers/status_container.js +++ b/app/javascript/mastodon/containers/status_container.js @@ -35,6 +35,7 @@ import { } from '../actions/domain_blocks'; import { initMuteModal } from '../actions/mutes'; import { initBlockModal } from '../actions/blocks'; +import { initBoostModal } from '../actions/boosts'; import { initReport } from '../actions/reports'; import { openModal } from '../actions/modal'; import { deployPictureInPicture } from '../actions/picture_in_picture'; @@ -82,11 +83,11 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ }); }, - onModalReblog (status) { + onModalReblog (status, privacy) { if (status.get('reblogged')) { dispatch(unreblog(status)); } else { - dispatch(reblog(status)); + dispatch(reblog(status, privacy)); } }, @@ -94,7 +95,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ if ((e && e.shiftKey) || !boostModal) { this.onModalReblog(status); } else { - dispatch(openModal('BOOST', { status, onReblog: this.onModalReblog })); + dispatch(initBoostModal({ status, onReblog: this.onModalReblog })); } }, diff --git a/app/javascript/mastodon/features/account/components/header.js b/app/javascript/mastodon/features/account/components/header.js index b05d1968f..a347806bd 100644 --- a/app/javascript/mastodon/features/account/components/header.js +++ b/app/javascript/mastodon/features/account/components/header.js @@ -96,45 +96,30 @@ class Header extends ImmutablePureComponent { return !location.pathname.match(/\/(followers|following)\/?$/); } - _updateEmojis () { - const node = this.node; - - if (!node || autoPlayGif) { + handleMouseEnter = ({ currentTarget }) => { + if (autoPlayGif) { return; } - const emojis = node.querySelectorAll('.custom-emoji'); + const emojis = currentTarget.querySelectorAll('.custom-emoji'); for (var i = 0; i < emojis.length; i++) { let emoji = emojis[i]; - if (emoji.classList.contains('status-emoji')) { - continue; - } - emoji.classList.add('status-emoji'); - - emoji.addEventListener('mouseenter', this.handleEmojiMouseEnter, false); - emoji.addEventListener('mouseleave', this.handleEmojiMouseLeave, false); + emoji.src = emoji.getAttribute('data-original'); } } - componentDidMount () { - this._updateEmojis(); - } + handleMouseLeave = ({ currentTarget }) => { + if (autoPlayGif) { + return; + } - componentDidUpdate () { - this._updateEmojis(); - } + const emojis = currentTarget.querySelectorAll('.custom-emoji'); - handleEmojiMouseEnter = ({ target }) => { - target.src = target.getAttribute('data-original'); - } - - handleEmojiMouseLeave = ({ target }) => { - target.src = target.getAttribute('data-static'); - } - - setRef = (c) => { - this.node = c; + for (var i = 0; i < emojis.length; i++) { + let emoji = emojis[i]; + emoji.src = emoji.getAttribute('data-static'); + } } render () { @@ -276,7 +261,7 @@ class Header extends ImmutablePureComponent { } return ( -
+
{!suspended && info} @@ -328,9 +313,9 @@ class Header extends ImmutablePureComponent { ))} {fields.map((pair, i) => (
-
+
-
+
{pair.get('verified_at') && }
@@ -340,7 +325,9 @@ class Header extends ImmutablePureComponent { {account.get('id') !== me && !suspended && } - {account.get('note').length > 0 && account.get('note') !== '

' &&
} + {account.get('note').length > 0 && account.get('note') !== '

' &&
} + +
{!suspended && ( diff --git a/app/javascript/mastodon/features/compose/components/compose_form.js b/app/javascript/mastodon/features/compose/components/compose_form.js index 961081d00..c6f6ce44f 100644 --- a/app/javascript/mastodon/features/compose/components/compose_form.js +++ b/app/javascript/mastodon/features/compose/components/compose_form.js @@ -135,7 +135,15 @@ class ComposeForm extends ImmutablePureComponent { } } + componentDidMount () { + this._updateFocusAndSelection({ }); + } + componentDidUpdate (prevProps) { + this._updateFocusAndSelection(prevProps); + } + + _updateFocusAndSelection = (prevProps) => { // This statement does several things: // - If we're beginning a reply, and, // - Replying to zero or one users, places the cursor at the end of the textbox. diff --git a/app/javascript/mastodon/features/compose/components/privacy_dropdown.js b/app/javascript/mastodon/features/compose/components/privacy_dropdown.js index 309f46290..df59f46b3 100644 --- a/app/javascript/mastodon/features/compose/components/privacy_dropdown.js +++ b/app/javascript/mastodon/features/compose/components/privacy_dropdown.js @@ -127,7 +127,7 @@ class PrivacyDropdownMenu extends React.PureComponent { // It should not be transformed when mounting because the resulting // size will be used to determine the coordinate of the menu by // react-overlays -
+
{items.map(item => (
@@ -153,11 +153,12 @@ class PrivacyDropdown extends React.PureComponent { static propTypes = { isUserTouching: PropTypes.func, - isModalOpen: PropTypes.bool.isRequired, onModalOpen: PropTypes.func, onModalClose: PropTypes.func, value: PropTypes.string.isRequired, onChange: PropTypes.func.isRequired, + noDirect: PropTypes.bool, + container: PropTypes.func, intl: PropTypes.object.isRequired, }; @@ -167,7 +168,7 @@ class PrivacyDropdown extends React.PureComponent { }; handleToggle = ({ target }) => { - if (this.props.isUserTouching()) { + if (this.props.isUserTouching && this.props.isUserTouching()) { if (this.state.open) { this.props.onModalClose(); } else { @@ -236,12 +237,17 @@ class PrivacyDropdown extends React.PureComponent { { icon: 'globe', value: 'public', text: formatMessage(messages.public_short), meta: formatMessage(messages.public_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) }, ]; + + if (!this.props.noDirect) { + this.options.push( + { icon: 'envelope', value: 'direct', text: formatMessage(messages.direct_short), meta: formatMessage(messages.direct_long) }, + ); + } } render () { - const { value, intl } = this.props; + const { value, container, intl } = this.props; const { open, placement } = this.state; const valueOption = this.options.find(item => item.value === value); @@ -264,7 +270,7 @@ class PrivacyDropdown extends React.PureComponent { />
- +
-
+
{status.get('media_attachments').size > 0 && ( this.props.expandSearch('accounts'); handleLoadMoreStatuses = () => this.props.expandSearch('statuses'); @@ -42,7 +48,7 @@ class SearchResults extends ImmutablePureComponent { render () { const { intl, results, suggestions, dismissSuggestion, searchTerm } = this.props; - if (results.isEmpty() && !suggestions.isEmpty()) { + if (searchTerm === '' && !suggestions.isEmpty()) { return (
@@ -51,12 +57,12 @@ class SearchResults extends ImmutablePureComponent {
- {suggestions && suggestions.map(accountId => ( + {suggestions && suggestions.map(suggestion => ( ))} diff --git a/app/javascript/mastodon/features/compose/containers/privacy_dropdown_container.js b/app/javascript/mastodon/features/compose/containers/privacy_dropdown_container.js index 0ddf531d3..e5bbb94da 100644 --- a/app/javascript/mastodon/features/compose/containers/privacy_dropdown_container.js +++ b/app/javascript/mastodon/features/compose/containers/privacy_dropdown_container.js @@ -5,7 +5,6 @@ import { openModal, closeModal } from '../../../actions/modal'; import { isUserTouching } from '../../../is_mobile'; const mapStateToProps = state => ({ - isModalOpen: state.get('modal').modalType === 'ACTIONS', value: state.getIn(['compose', 'privacy']), }); diff --git a/app/javascript/mastodon/features/compose/util/counter.js b/app/javascript/mastodon/features/compose/util/counter.js index 700ba2163..7aa9e87b1 100644 --- a/app/javascript/mastodon/features/compose/util/counter.js +++ b/app/javascript/mastodon/features/compose/util/counter.js @@ -1,6 +1,6 @@ import { urlRegex } from './url_regex'; -const urlPlaceholder = 'xxxxxxxxxxxxxxxxxxxxxxx'; +const urlPlaceholder = '$2xxxxxxxxxxxxxxxxxxxxxxx'; export function countableText(inputText) { return inputText diff --git a/app/javascript/mastodon/features/compose/util/url_regex.js b/app/javascript/mastodon/features/compose/util/url_regex.js index 7f1e17620..9c2005c53 100644 --- a/app/javascript/mastodon/features/compose/util/url_regex.js +++ b/app/javascript/mastodon/features/compose/util/url_regex.js @@ -1,196 +1,30 @@ -const regexen = {}; +import regexSupplant from 'twitter-text/dist/lib/regexSupplant'; +import validUrlPrecedingChars from 'twitter-text/dist/regexp/validUrlPrecedingChars'; +import validDomain from 'twitter-text/dist/regexp/validDomain'; +import validPortNumber from 'twitter-text/dist/regexp/validPortNumber'; +import validUrlPath from 'twitter-text/dist/regexp/validUrlPath'; +import validUrlQueryChars from 'twitter-text/dist/regexp/validUrlQueryChars'; +import validUrlQueryEndingChars from 'twitter-text/dist/regexp/validUrlQueryEndingChars'; -const regexSupplant = function(regex, flags) { - flags = flags || ''; - if (typeof regex !== 'string') { - if (regex.global && flags.indexOf('g') < 0) { - flags += 'g'; - } - if (regex.ignoreCase && flags.indexOf('i') < 0) { - flags += 'i'; - } - if (regex.multiline && flags.indexOf('m') < 0) { - flags += 'm'; - } +// The difference with twitter-text's extractURL is that the protocol isn't +// optional. - regex = regex.source; - } - return new RegExp(regex.replace(/#\{(\w+)\}/g, function(match, name) { - var newRegex = regexen[name] || ''; - if (typeof newRegex !== 'string') { - newRegex = newRegex.source; - } - return newRegex; - }), flags); -}; - -const stringSupplant = function(str, values) { - return str.replace(/#\{(\w+)\}/g, function(match, name) { - return values[name] || ''; - }); -}; - -export const urlRegex = (function() { - regexen.spaces_group = /\x09-\x0D\x20\x85\xA0\u1680\u180E\u2000-\u200A\u2028\u2029\u202F\u205F\u3000/; - regexen.invalid_chars_group = /\uFFFE\uFEFF\uFFFF\u202A-\u202E/; - regexen.punct = /\!'#%&'\(\)*\+,\\\-\.\/:;<=>\?@\[\]\^_{|}~\$/; - regexen.validUrlPrecedingChars = regexSupplant(/(?:[^A-Za-z0-9@@$###{invalid_chars_group}]|^)/); - regexen.invalidDomainChars = stringSupplant('#{punct}#{spaces_group}#{invalid_chars_group}', regexen); - regexen.validDomainChars = regexSupplant(/[^#{invalidDomainChars}]/); - regexen.validSubdomain = regexSupplant(/(?:(?:#{validDomainChars}(?:[_-]|#{validDomainChars})*)?#{validDomainChars}\.)/); - regexen.validDomainName = regexSupplant(/(?:(?:#{validDomainChars}(?:-|#{validDomainChars})*)?#{validDomainChars}\.)/); - regexen.validGTLD = regexSupplant(RegExp( - '(?:(?:' + - '삼성|닷컴|닷넷|香格里拉|餐厅|食品|飞利浦|電訊盈科|集团|通販|购物|谷歌|诺基亚|联通|网络|网站|网店|网址|组织机构|移动|珠宝|点看|游戏|淡马锡|机构|書籍|时尚|新闻|政府|' + - '政务|手表|手机|我爱你|慈善|微博|广东|工行|家電|娱乐|天主教|大拿|大众汽车|在线|嘉里大酒店|嘉里|商标|商店|商城|公益|公司|八卦|健康|信息|佛山|企业|中文网|中信|世界|' + - 'ポイント|ファッション|セール|ストア|コム|グーグル|クラウド|みんな|คอม|संगठन|नेट|कॉम|همراه|موقع|موبايلي|كوم|كاثوليك|عرب|شبكة|' + - 'بيتك|بازار|العليان|ارامكو|اتصالات|ابوظبي|קום|сайт|рус|орг|онлайн|москва|ком|католик|дети|' + - 'zuerich|zone|zippo|zip|zero|zara|zappos|yun|youtube|you|yokohama|yoga|yodobashi|yandex|yamaxun|' + - 'yahoo|yachts|xyz|xxx|xperia|xin|xihuan|xfinity|xerox|xbox|wtf|wtc|wow|world|works|work|woodside|' + - 'wolterskluwer|wme|winners|wine|windows|win|williamhill|wiki|wien|whoswho|weir|weibo|wedding|wed|' + - 'website|weber|webcam|weatherchannel|weather|watches|watch|warman|wanggou|wang|walter|walmart|' + - 'wales|vuelos|voyage|voto|voting|vote|volvo|volkswagen|vodka|vlaanderen|vivo|viva|vistaprint|' + - 'vista|vision|visa|virgin|vip|vin|villas|viking|vig|video|viajes|vet|versicherung|' + - 'vermögensberatung|vermögensberater|verisign|ventures|vegas|vanguard|vana|vacations|ups|uol|uno|' + - 'university|unicom|uconnect|ubs|ubank|tvs|tushu|tunes|tui|tube|trv|trust|travelersinsurance|' + - 'travelers|travelchannel|travel|training|trading|trade|toys|toyota|town|tours|total|toshiba|' + - 'toray|top|tools|tokyo|today|tmall|tkmaxx|tjx|tjmaxx|tirol|tires|tips|tiffany|tienda|tickets|' + - 'tiaa|theatre|theater|thd|teva|tennis|temasek|telefonica|telecity|tel|technology|tech|team|tdk|' + - 'tci|taxi|tax|tattoo|tatar|tatamotors|target|taobao|talk|taipei|tab|systems|symantec|sydney|' + - 'swiss|swiftcover|swatch|suzuki|surgery|surf|support|supply|supplies|sucks|style|study|studio|' + - 'stream|store|storage|stockholm|stcgroup|stc|statoil|statefarm|statebank|starhub|star|staples|' + - 'stada|srt|srl|spreadbetting|spot|spiegel|space|soy|sony|song|solutions|solar|sohu|software|' + - 'softbank|social|soccer|sncf|smile|smart|sling|skype|sky|skin|ski|site|singles|sina|silk|shriram|' + - 'showtime|show|shouji|shopping|shop|shoes|shiksha|shia|shell|shaw|sharp|shangrila|sfr|sexy|sex|' + - 'sew|seven|ses|services|sener|select|seek|security|secure|seat|search|scot|scor|scjohnson|' + - 'science|schwarz|schule|school|scholarships|schmidt|schaeffler|scb|sca|sbs|sbi|saxo|save|sas|' + - 'sarl|sapo|sap|sanofi|sandvikcoromant|sandvik|samsung|samsclub|salon|sale|sakura|safety|safe|' + - 'saarland|ryukyu|rwe|run|ruhr|rugby|rsvp|room|rogers|rodeo|rocks|rocher|rmit|rip|rio|ril|' + - 'rightathome|ricoh|richardli|rich|rexroth|reviews|review|restaurant|rest|republican|report|' + - 'repair|rentals|rent|ren|reliance|reit|reisen|reise|rehab|redumbrella|redstone|red|recipes|' + - 'realty|realtor|realestate|read|raid|radio|racing|qvc|quest|quebec|qpon|pwc|pub|prudential|pru|' + - 'protection|property|properties|promo|progressive|prof|productions|prod|pro|prime|press|praxi|' + - 'pramerica|post|porn|politie|poker|pohl|pnc|plus|plumbing|playstation|play|place|pizza|pioneer|' + - 'pink|ping|pin|pid|pictures|pictet|pics|piaget|physio|photos|photography|photo|phone|philips|phd|' + - 'pharmacy|pfizer|pet|pccw|pay|passagens|party|parts|partners|pars|paris|panerai|panasonic|' + - 'pamperedchef|page|ovh|ott|otsuka|osaka|origins|orientexpress|organic|org|orange|oracle|open|ooo|' + - 'onyourside|online|onl|ong|one|omega|ollo|oldnavy|olayangroup|olayan|okinawa|office|off|observer|' + - 'obi|nyc|ntt|nrw|nra|nowtv|nowruz|now|norton|northwesternmutual|nokia|nissay|nissan|ninja|nikon|' + - 'nike|nico|nhk|ngo|nfl|nexus|nextdirect|next|news|newholland|new|neustar|network|netflix|netbank|' + - 'net|nec|nba|navy|natura|nationwide|name|nagoya|nadex|nab|mutuelle|mutual|museum|mtr|mtpc|mtn|' + - 'msd|movistar|movie|mov|motorcycles|moto|moscow|mortgage|mormon|mopar|montblanc|monster|money|' + - 'monash|mom|moi|moe|moda|mobily|mobile|mobi|mma|mls|mlb|mitsubishi|mit|mint|mini|mil|microsoft|' + - 'miami|metlife|merckmsd|meo|menu|men|memorial|meme|melbourne|meet|media|med|mckinsey|mcdonalds|' + - 'mcd|mba|mattel|maserati|marshalls|marriott|markets|marketing|market|map|mango|management|man|' + - 'makeup|maison|maif|madrid|macys|luxury|luxe|lupin|lundbeck|ltda|ltd|lplfinancial|lpl|love|lotto|' + - 'lotte|london|lol|loft|locus|locker|loans|loan|lixil|living|live|lipsy|link|linde|lincoln|limo|' + - 'limited|lilly|like|lighting|lifestyle|lifeinsurance|life|lidl|liaison|lgbt|lexus|lego|legal|' + - 'lefrak|leclerc|lease|lds|lawyer|law|latrobe|latino|lat|lasalle|lanxess|landrover|land|lancome|' + - 'lancia|lancaster|lamer|lamborghini|ladbrokes|lacaixa|kyoto|kuokgroup|kred|krd|kpn|kpmg|kosher|' + - 'komatsu|koeln|kiwi|kitchen|kindle|kinder|kim|kia|kfh|kerryproperties|kerrylogistics|kerryhotels|' + - 'kddi|kaufen|juniper|juegos|jprs|jpmorgan|joy|jot|joburg|jobs|jnj|jmp|jll|jlc|jio|jewelry|jetzt|' + - 'jeep|jcp|jcb|java|jaguar|iwc|iveco|itv|itau|istanbul|ist|ismaili|iselect|irish|ipiranga|' + - 'investments|intuit|international|intel|int|insure|insurance|institute|ink|ing|info|infiniti|' + - 'industries|immobilien|immo|imdb|imamat|ikano|iinet|ifm|ieee|icu|ice|icbc|ibm|hyundai|hyatt|' + - 'hughes|htc|hsbc|how|house|hotmail|hotels|hoteles|hot|hosting|host|hospital|horse|honeywell|' + - 'honda|homesense|homes|homegoods|homedepot|holiday|holdings|hockey|hkt|hiv|hitachi|hisamitsu|' + - 'hiphop|hgtv|hermes|here|helsinki|help|healthcare|health|hdfcbank|hdfc|hbo|haus|hangout|hamburg|' + - 'hair|guru|guitars|guide|guge|gucci|guardian|group|grocery|gripe|green|gratis|graphics|grainger|' + - 'gov|got|gop|google|goog|goodyear|goodhands|goo|golf|goldpoint|gold|godaddy|gmx|gmo|gmbh|gmail|' + - 'globo|global|gle|glass|glade|giving|gives|gifts|gift|ggee|george|genting|gent|gea|gdn|gbiz|' + - 'garden|gap|games|game|gallup|gallo|gallery|gal|fyi|futbol|furniture|fund|fun|fujixerox|fujitsu|' + - 'ftr|frontier|frontdoor|frogans|frl|fresenius|free|fox|foundation|forum|forsale|forex|ford|' + - 'football|foodnetwork|food|foo|fly|flsmidth|flowers|florist|flir|flights|flickr|fitness|fit|' + - 'fishing|fish|firmdale|firestone|fire|financial|finance|final|film|fido|fidelity|fiat|ferrero|' + - 'ferrari|feedback|fedex|fast|fashion|farmers|farm|fans|fan|family|faith|fairwinds|fail|fage|' + - 'extraspace|express|exposed|expert|exchange|everbank|events|eus|eurovision|etisalat|esurance|' + - 'estate|esq|erni|ericsson|equipment|epson|epost|enterprises|engineering|engineer|energy|emerck|' + - 'email|education|edu|edeka|eco|eat|earth|dvr|dvag|durban|dupont|duns|dunlop|duck|dubai|dtv|drive|' + - 'download|dot|doosan|domains|doha|dog|dodge|doctor|docs|dnp|diy|dish|discover|discount|directory|' + - 'direct|digital|diet|diamonds|dhl|dev|design|desi|dentist|dental|democrat|delta|deloitte|dell|' + - 'delivery|degree|deals|dealer|deal|dds|dclk|day|datsun|dating|date|data|dance|dad|dabur|cyou|' + - 'cymru|cuisinella|csc|cruises|cruise|crs|crown|cricket|creditunion|creditcard|credit|courses|' + - 'coupons|coupon|country|corsica|coop|cool|cookingchannel|cooking|contractors|contact|consulting|' + - 'construction|condos|comsec|computer|compare|company|community|commbank|comcast|com|cologne|' + - 'college|coffee|codes|coach|clubmed|club|cloud|clothing|clinique|clinic|click|cleaning|claims|' + - 'cityeats|city|citic|citi|citadel|cisco|circle|cipriani|church|chrysler|chrome|christmas|chloe|' + - 'chintai|cheap|chat|chase|channel|chanel|cfd|cfa|cern|ceo|center|ceb|cbs|cbre|cbn|cba|catholic|' + - 'catering|cat|casino|cash|caseih|case|casa|cartier|cars|careers|career|care|cards|caravan|car|' + - 'capitalone|capital|capetown|canon|cancerresearch|camp|camera|cam|calvinklein|call|cal|cafe|cab|' + - 'bzh|buzz|buy|business|builders|build|bugatti|budapest|brussels|brother|broker|broadway|' + - 'bridgestone|bradesco|box|boutique|bot|boston|bostik|bosch|boots|booking|book|boo|bond|bom|bofa|' + - 'boehringer|boats|bnpparibas|bnl|bmw|bms|blue|bloomberg|blog|blockbuster|blanco|blackfriday|' + - 'black|biz|bio|bingo|bing|bike|bid|bible|bharti|bet|bestbuy|best|berlin|bentley|beer|beauty|' + - 'beats|bcn|bcg|bbva|bbt|bbc|bayern|bauhaus|basketball|baseball|bargains|barefoot|barclays|' + - 'barclaycard|barcelona|bar|bank|band|bananarepublic|banamex|baidu|baby|azure|axa|aws|avianca|' + - 'autos|auto|author|auspost|audio|audible|audi|auction|attorney|athleta|associates|asia|asda|arte|' + - 'art|arpa|army|archi|aramco|arab|aquarelle|apple|app|apartments|aol|anz|anquan|android|analytics|' + - 'amsterdam|amica|amfam|amex|americanfamily|americanexpress|alstom|alsace|ally|allstate|allfinanz|' + - 'alipay|alibaba|alfaromeo|akdn|airtel|airforce|airbus|aigo|aig|agency|agakhan|africa|afl|' + - 'afamilycompany|aetna|aero|aeg|adult|ads|adac|actor|active|aco|accountants|accountant|accenture|' + - 'academy|abudhabi|abogado|able|abc|abbvie|abbott|abb|abarth|aarp|aaa|onion' + - ')(?=[^0-9a-zA-Z@]|$))')); - regexen.validCCTLD = regexSupplant(RegExp( - '(?:(?:' + - '한국|香港|澳門|新加坡|台灣|台湾|中國|中国|გე|ไทย|ලංකා|ഭാരതം|ಭಾರತ|భారత్|சிங்கப்பூர்|இலங்கை|இந்தியா|ଭାରତ|ભારત|ਭਾਰਤ|' + - 'ভাৰত|ভারত|বাংলা|भारोत|भारतम्|भारत|ڀارت|پاکستان|مليسيا|مصر|قطر|فلسطين|عمان|عراق|سورية|سودان|تونس|' + - 'بھارت|بارت|ایران|امارات|المغرب|السعودية|الجزائر|الاردن|հայ|қаз|укр|срб|рф|мон|мкд|ею|бел|бг|ελ|' + - 'zw|zm|za|yt|ye|ws|wf|vu|vn|vi|vg|ve|vc|va|uz|uy|us|um|uk|ug|ua|tz|tw|tv|tt|tr|tp|to|tn|tm|tl|tk|' + - 'tj|th|tg|tf|td|tc|sz|sy|sx|sv|su|st|ss|sr|so|sn|sm|sl|sk|sj|si|sh|sg|se|sd|sc|sb|sa|rw|ru|rs|ro|' + - 're|qa|py|pw|pt|ps|pr|pn|pm|pl|pk|ph|pg|pf|pe|pa|om|nz|nu|nr|np|no|nl|ni|ng|nf|ne|nc|na|mz|my|mx|' + - 'mw|mv|mu|mt|ms|mr|mq|mp|mo|mn|mm|ml|mk|mh|mg|mf|me|md|mc|ma|ly|lv|lu|lt|ls|lr|lk|li|lc|lb|la|kz|' + - 'ky|kw|kr|kp|kn|km|ki|kh|kg|ke|jp|jo|jm|je|it|is|ir|iq|io|in|im|il|ie|id|hu|ht|hr|hn|hm|hk|gy|gw|' + - 'gu|gt|gs|gr|gq|gp|gn|gm|gl|gi|gh|gg|gf|ge|gd|gb|ga|fr|fo|fm|fk|fj|fi|eu|et|es|er|eh|eg|ee|ec|dz|' + - 'do|dm|dk|dj|de|cz|cy|cx|cw|cv|cu|cr|co|cn|cm|cl|ck|ci|ch|cg|cf|cd|cc|ca|bz|by|bw|bv|bt|bs|br|bq|' + - 'bo|bn|bm|bl|bj|bi|bh|bg|bf|be|bd|bb|ba|az|ax|aw|au|at|as|ar|aq|ao|an|am|al|ai|ag|af|ae|ad|ac' + - ')(?=[^0-9a-zA-Z@]|$))')); - regexen.validPunycode = /(?:xn--[0-9a-z]+)/; - regexen.validSpecialCCTLD = /(?:(?:co|tv)(?=[^0-9a-zA-Z@]|$))/; - regexen.validDomain = regexSupplant(/(?:#{validSubdomain}*#{validDomainName}(?:#{validGTLD}|#{validCCTLD}|#{validPunycode}))/); - regexen.validPortNumber = /[0-9]+/; - regexen.pd = /\u002d\u058a\u05be\u1400\u1806\u2010-\u2015\u2e17\u2e1a\u2e3a\u2e40\u301c\u3030\u30a0\ufe31\ufe58\ufe63\uff0d/; - regexen.validGeneralUrlPathChars = regexSupplant(/[^#{spaces_group}\(\)\?]/i); - // Allow URL paths to contain up to two nested levels of balanced parens - // 1. Used in Wikipedia URLs like /Primer_(film) - // 2. Used in IIS sessions like /S(dfd346)/ - // 3. Used in Rdio URLs like /track/We_Up_(Album_Version_(Edited))/ - regexen.validUrlBalancedParens = regexSupplant( - '\\(' + - '(?:' + - '#{validGeneralUrlPathChars}+' + - '|' + - // allow one nested level of balanced parentheses - '(?:' + - '#{validGeneralUrlPathChars}*' + - '\\(' + - '#{validGeneralUrlPathChars}+' + - '\\)' + - '#{validGeneralUrlPathChars}*' + - ')' + - ')' + - '\\)', - 'i'); - // Valid end-of-path characters (so /foo. does not gobble the period). - // 1. Allow =&# for empty URL parameters and other URL-join artifacts - regexen.validUrlPathEndingChars = regexSupplant(/[^#{spaces_group}\(\)\?!\*';:=\,\.\$%\[\]#{pd}~&\|@]|(?:#{validUrlBalancedParens})/i); - // Allow @ in a url, but only in the middle. Catch things like http://example.com/@user/ - regexen.validUrlPath = regexSupplant('(?:' + - '(?:' + - '#{validGeneralUrlPathChars}*' + - '(?:#{validUrlBalancedParens}#{validGeneralUrlPathChars}*)*' + - '#{validUrlPathEndingChars}'+ - ')|(?:@#{validGeneralUrlPathChars}+\/)'+ - ')', 'i'); - regexen.validUrlQueryChars = /[a-z0-9!?\*'@\(\);:&=\+\$\/%#\[\]\-_\.,~|]/i; - regexen.validUrlQueryEndingChars = /[a-z0-9_&=#\/]/i; - regexen.validUrl = regexSupplant( - '(' + // $1 URL - '(https?:\\/\\/)' + // $2 Protocol - '(#{validDomain})' + // $3 Domain(s) - '(?::(#{validPortNumber}))?' + // $4 Port number (optional) - '(\\/#{validUrlPath}*)?' + // $5 URL Path - '(\\?#{validUrlQueryChars}*#{validUrlQueryEndingChars})?' + // $6 Query String - ')', - 'gi'); - return regexen.validUrl; -}()); +export const urlRegex = regexSupplant( + '(' + // $1 URL + '(#{validUrlPrecedingChars})' + // $2 + '(https?:\\/\\/)' + // $3 Protocol + '(#{validDomain})' + // $4 Domain(s) + '(?::(#{validPortNumber}))?' + // $5 Port number (optional) + '(\\/#{validUrlPath}*)?' + // $6 URL Path + '(\\?#{validUrlQueryChars}*#{validUrlQueryEndingChars})?' + // $7 Query String + ')', + { + validUrlPrecedingChars, + validDomain, + validPortNumber, + validUrlPath, + validUrlQueryChars, + validUrlQueryEndingChars, + }, + 'gi', +); diff --git a/app/javascript/mastodon/features/direct_timeline/components/conversation.js b/app/javascript/mastodon/features/direct_timeline/components/conversation.js index 6ecc27fac..43e1d77b9 100644 --- a/app/javascript/mastodon/features/direct_timeline/components/conversation.js +++ b/app/javascript/mastodon/features/direct_timeline/components/conversation.js @@ -44,41 +44,30 @@ class Conversation extends ImmutablePureComponent { intl: PropTypes.object.isRequired, }; - _updateEmojis () { - const node = this.namesNode; - - if (!node || autoPlayGif) { + handleMouseEnter = ({ currentTarget }) => { + if (autoPlayGif) { return; } - const emojis = node.querySelectorAll('.custom-emoji'); + const emojis = currentTarget.querySelectorAll('.custom-emoji'); for (var i = 0; i < emojis.length; i++) { let emoji = emojis[i]; - if (emoji.classList.contains('status-emoji')) { - continue; - } - emoji.classList.add('status-emoji'); - - emoji.addEventListener('mouseenter', this.handleEmojiMouseEnter, false); - emoji.addEventListener('mouseleave', this.handleEmojiMouseLeave, false); + emoji.src = emoji.getAttribute('data-original'); } } - componentDidMount () { - this._updateEmojis(); - } + handleMouseLeave = ({ currentTarget }) => { + if (autoPlayGif) { + return; + } - componentDidUpdate () { - this._updateEmojis(); - } + const emojis = currentTarget.querySelectorAll('.custom-emoji'); - handleEmojiMouseEnter = ({ target }) => { - target.src = target.getAttribute('data-original'); - } - - handleEmojiMouseLeave = ({ target }) => { - target.src = target.getAttribute('data-static'); + for (var i = 0; i < emojis.length; i++) { + let emoji = emojis[i]; + emoji.src = emoji.getAttribute('data-static'); + } } handleClick = () => { @@ -123,10 +112,6 @@ class Conversation extends ImmutablePureComponent { this.props.onToggleHidden(this.props.lastStatus); } - setNamesRef = (c) => { - this.namesNode = c; - } - render () { const { accounts, lastStatus, unread, scrollKey, intl } = this.props; @@ -171,7 +156,7 @@ class Conversation extends ImmutablePureComponent { {unread && }
-
+
{names} }} />
diff --git a/app/javascript/mastodon/features/directory/components/account_card.js b/app/javascript/mastodon/features/directory/components/account_card.js index 419ab9e11..8f0e8db4b 100644 --- a/app/javascript/mastodon/features/directory/components/account_card.js +++ b/app/javascript/mastodon/features/directory/components/account_card.js @@ -102,43 +102,32 @@ class AccountCard extends ImmutablePureComponent { onMute: PropTypes.func.isRequired, }; - _updateEmojis() { - const node = this.node; - - if (!node || autoPlayGif) { + handleMouseEnter = ({ currentTarget }) => { + if (autoPlayGif) { return; } - const emojis = node.querySelectorAll('.custom-emoji'); + const emojis = currentTarget.querySelectorAll('.custom-emoji'); for (var i = 0; i < emojis.length; i++) { let emoji = emojis[i]; - if (emoji.classList.contains('status-emoji')) { - continue; - } - emoji.classList.add('status-emoji'); - - emoji.addEventListener('mouseenter', this.handleEmojiMouseEnter, false); - emoji.addEventListener('mouseleave', this.handleEmojiMouseLeave, false); + emoji.src = emoji.getAttribute('data-original'); } } - componentDidMount() { - this._updateEmojis(); + handleMouseLeave = ({ currentTarget }) => { + if (autoPlayGif) { + return; + } + + const emojis = currentTarget.querySelectorAll('.custom-emoji'); + + for (var i = 0; i < emojis.length; i++) { + let emoji = emojis[i]; + emoji.src = emoji.getAttribute('data-static'); + } } - componentDidUpdate() { - this._updateEmojis(); - } - - handleEmojiMouseEnter = ({ target }) => { - target.src = target.getAttribute('data-original'); - }; - - handleEmojiMouseLeave = ({ target }) => { - target.src = target.getAttribute('data-static'); - }; - handleFollow = () => { this.props.onFollow(this.props.account); }; @@ -151,10 +140,6 @@ class AccountCard extends ImmutablePureComponent { this.props.onMute(this.props.account); }; - setRef = (c) => { - this.node = c; - }; - render() { const { account, intl } = this.props; @@ -239,9 +224,9 @@ class AccountCard extends ImmutablePureComponent {
-
+
diff --git a/app/javascript/mastodon/features/emoji/emoji.js b/app/javascript/mastodon/features/emoji/emoji.js index 4e37f3a80..fb1a3804c 100644 --- a/app/javascript/mastodon/features/emoji/emoji.js +++ b/app/javascript/mastodon/features/emoji/emoji.js @@ -11,7 +11,7 @@ const emojiFilenames = (emojis) => { }; // Emoji requiring extra borders depending on theme -const darkEmoji = emojiFilenames(['🎱', '🐜', '⚫', '🖤', '⬛', '◼️', '◾', '◼️', '✒️', '▪️', '💣', '🎳', '📷', '📸', '♣️', '🕶️', '✴️', '🔌', '💂‍♀️', '📽️', '🍳', '🦍', '💂', '🔪', '🕳️', '🕹️', '🕋', '🖊️', '🖋️', '💂‍♂️', '🎤', '🎓', '🎥', '🎼', '♠️', '🎩', '🦃', '📼', '📹', '🎮', '🐃', '🏴', '🐞', '🕺']); +const darkEmoji = emojiFilenames(['🎱', '🐜', '⚫', '🖤', '⬛', '◼️', '◾', '◼️', '✒️', '▪️', '💣', '🎳', '📷', '📸', '♣️', '🕶️', '✴️', '🔌', '💂‍♀️', '📽️', '🍳', '🦍', '💂', '🔪', '🕳️', '🕹️', '🕋', '🖊️', '🖋️', '💂‍♂️', '🎤', '🎓', '🎥', '🎼', '♠️', '🎩', '🦃', '📼', '📹', '🎮', '🐃', '🏴', '🐞', '🕺', '📱', '📲', '🚲']); const lightEmoji = emojiFilenames(['👽', '⚾', '🐔', '☁️', '💨', '🕊️', '👀', '🍥', '👻', '🐐', '❕', '❔', '⛸️', '🌩️', '🔊', '🔇', '📃', '🌧️', '🐏', '🍚', '🍙', '🐓', '🐑', '💀', '☠️', '🌨️', '🔉', '🔈', '💬', '💭', '🏐', '🏳️', '⚪', '⬜', '◽', '◻️', '▫️']); const emojiFilename = (filename) => { diff --git a/app/javascript/mastodon/features/follow_recommendations/components/account.js b/app/javascript/mastodon/features/follow_recommendations/components/account.js new file mode 100644 index 000000000..bd855aab0 --- /dev/null +++ b/app/javascript/mastodon/features/follow_recommendations/components/account.js @@ -0,0 +1,85 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import { connect } from 'react-redux'; +import { makeGetAccount } from 'mastodon/selectors'; +import Avatar from 'mastodon/components/avatar'; +import DisplayName from 'mastodon/components/display_name'; +import Permalink from 'mastodon/components/permalink'; +import IconButton from 'mastodon/components/icon_button'; +import { injectIntl, defineMessages } from 'react-intl'; +import { followAccount, unfollowAccount } from 'mastodon/actions/accounts'; + +const messages = defineMessages({ + follow: { id: 'account.follow', defaultMessage: 'Follow' }, + unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, +}); + +const makeMapStateToProps = () => { + const getAccount = makeGetAccount(); + + const mapStateToProps = (state, props) => ({ + account: getAccount(state, props.id), + }); + + return mapStateToProps; +}; + +const getFirstSentence = str => { + const arr = str.split(/(([\.\?!]+\s)|[.。?!\n•])/); + + return arr[0]; +}; + +export default @connect(makeMapStateToProps) +@injectIntl +class Account extends ImmutablePureComponent { + + static propTypes = { + account: ImmutablePropTypes.map.isRequired, + intl: PropTypes.object.isRequired, + dispatch: PropTypes.func.isRequired, + }; + + handleFollow = () => { + const { account, dispatch } = this.props; + + if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) { + dispatch(unfollowAccount(account.get('id'))); + } else { + dispatch(followAccount(account.get('id'))); + } + } + + render () { + const { account, intl } = this.props; + + let button; + + if (account.getIn(['relationship', 'following'])) { + button = ; + } else { + button = ; + } + + return ( +
+
+ +
+ + + +
{getFirstSentence(account.get('note_plain'))}
+
+ +
+ {button} +
+
+
+ ); + } + +} diff --git a/app/javascript/mastodon/features/follow_recommendations/index.js b/app/javascript/mastodon/features/follow_recommendations/index.js new file mode 100644 index 000000000..26c8b2471 --- /dev/null +++ b/app/javascript/mastodon/features/follow_recommendations/index.js @@ -0,0 +1,109 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { connect } from 'react-redux'; +import { FormattedMessage } from 'react-intl'; +import { fetchSuggestions } from 'mastodon/actions/suggestions'; +import { changeSetting, saveSettings } from 'mastodon/actions/settings'; +import { requestBrowserPermission } from 'mastodon/actions/notifications'; +import { markAsPartial } from 'mastodon/actions/timelines'; +import Column from 'mastodon/features/ui/components/column'; +import Account from './components/account'; +import Logo from 'mastodon/components/logo'; +import imageGreeting from 'mastodon/../images/elephant_ui_greeting.svg'; +import Button from 'mastodon/components/button'; + +const mapStateToProps = state => ({ + suggestions: state.getIn(['suggestions', 'items']), + isLoading: state.getIn(['suggestions', 'isLoading']), +}); + +export default @connect(mapStateToProps) +class FollowRecommendations extends ImmutablePureComponent { + + static contextTypes = { + router: PropTypes.object.isRequired, + }; + + static propTypes = { + dispatch: PropTypes.func.isRequired, + suggestions: ImmutablePropTypes.list, + isLoading: PropTypes.bool, + }; + + componentDidMount () { + const { dispatch, suggestions } = this.props; + + // Don't re-fetch if we're e.g. navigating backwards to this page, + // since we don't want followed accounts to disappear from the list + + if (suggestions.size === 0) { + dispatch(fetchSuggestions(true)); + } + } + + componentWillUnmount () { + const { dispatch } = this.props; + + // Force the home timeline to be reloaded when the user navigates + // to it; if the user is new, it would've been empty before + + dispatch(markAsPartial('home')); + } + + handleDone = () => { + const { dispatch } = this.props; + const { router } = this.context; + + dispatch(requestBrowserPermission((permission) => { + if (permission === 'granted') { + dispatch(changeSetting(['notifications', 'alerts', 'follow'], true)); + dispatch(changeSetting(['notifications', 'alerts', 'favourite'], true)); + dispatch(changeSetting(['notifications', 'alerts', 'reblog'], true)); + dispatch(changeSetting(['notifications', 'alerts', 'mention'], true)); + dispatch(changeSetting(['notifications', 'alerts', 'poll'], true)); + dispatch(changeSetting(['notifications', 'alerts', 'status'], true)); + dispatch(saveSettings()); + } + })); + + router.history.push('/timelines/home'); + } + + render () { + const { suggestions, isLoading } = this.props; + + return ( + +
+
+ +

+

+
+ + {!isLoading && ( + +
+ {suggestions.size > 0 ? suggestions.map(suggestion => ( + + )) : ( +
+ +
+ )} +
+ +
+ + +
+
+ )} +
+
+ ); + } + +} diff --git a/app/javascript/mastodon/features/follow_requests/components/account_authorize.js b/app/javascript/mastodon/features/follow_requests/components/account_authorize.js index a3b524db1..8269f5ae4 100644 --- a/app/javascript/mastodon/features/follow_requests/components/account_authorize.js +++ b/app/javascript/mastodon/features/follow_requests/components/account_authorize.js @@ -35,7 +35,7 @@ class AccountAuthorize extends ImmutablePureComponent { -
+
diff --git a/app/javascript/mastodon/features/getting_started/components/announcements.js b/app/javascript/mastodon/features/getting_started/components/announcements.js index 5bc3abac6..ff1566e05 100644 --- a/app/javascript/mastodon/features/getting_started/components/announcements.js +++ b/app/javascript/mastodon/features/getting_started/components/announcements.js @@ -39,35 +39,10 @@ class Content extends ImmutablePureComponent { componentDidMount () { this._updateLinks(); - this._updateEmojis(); } componentDidUpdate () { this._updateLinks(); - this._updateEmojis(); - } - - _updateEmojis () { - const node = this.node; - - if (!node || autoPlayGif) { - return; - } - - const emojis = node.querySelectorAll('.custom-emoji'); - - for (var i = 0; i < emojis.length; i++) { - let emoji = emojis[i]; - - if (emoji.classList.contains('status-emoji')) { - continue; - } - - emoji.classList.add('status-emoji'); - - emoji.addEventListener('mouseenter', this.handleEmojiMouseEnter, false); - emoji.addEventListener('mouseleave', this.handleEmojiMouseLeave, false); - } } _updateLinks () { @@ -132,12 +107,30 @@ class Content extends ImmutablePureComponent { } } - handleEmojiMouseEnter = ({ target }) => { - target.src = target.getAttribute('data-original'); + handleMouseEnter = ({ currentTarget }) => { + if (autoPlayGif) { + return; + } + + const emojis = currentTarget.querySelectorAll('.custom-emoji'); + + for (var i = 0; i < emojis.length; i++) { + let emoji = emojis[i]; + emoji.src = emoji.getAttribute('data-original'); + } } - handleEmojiMouseLeave = ({ target }) => { - target.src = target.getAttribute('data-static'); + handleMouseLeave = ({ currentTarget }) => { + if (autoPlayGif) { + return; + } + + const emojis = currentTarget.querySelectorAll('.custom-emoji'); + + for (var i = 0; i < emojis.length; i++) { + let emoji = emojis[i]; + emoji.src = emoji.getAttribute('data-static'); + } } render () { @@ -145,9 +138,11 @@ class Content extends ImmutablePureComponent { return (
); } 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 27300f020..de1127b0d 100644 --- a/app/javascript/mastodon/features/hashtag_timeline/components/column_settings.js +++ b/app/javascript/mastodon/features/hashtag_timeline/components/column_settings.js @@ -59,7 +59,7 @@ class ColumnSettings extends React.PureComponent { {this.modeLabel(mode)} - + this.props.dispatch(fetchAnnouncements()), 700); this._checkIfReloadNeeded(false, this.props.isPartial); } @@ -153,7 +153,7 @@ class HomeTimeline extends React.PureComponent { scrollKey={`home_timeline-${columnId}`} onLoadMore={this.handleLoadMore} timelineId='home' - emptyMessage={ }} />} + emptyMessage={ }} />} shouldUpdateScroll={shouldUpdateScroll} bindToDocument={!multiColumn} /> diff --git a/app/javascript/mastodon/features/list_timeline/index.js b/app/javascript/mastodon/features/list_timeline/index.js index 48b2ed9e2..2ae5e181b 100644 --- a/app/javascript/mastodon/features/list_timeline/index.js +++ b/app/javascript/mastodon/features/list_timeline/index.js @@ -194,7 +194,7 @@ class ListTimeline extends React.PureComponent {
{ ['none', 'list', 'followed'].map(policy => ( - + ))}
diff --git a/app/javascript/mastodon/features/notifications/components/column_settings.js b/app/javascript/mastodon/features/notifications/components/column_settings.js index 8339a367e..0c24c3294 100644 --- a/app/javascript/mastodon/features/notifications/components/column_settings.js +++ b/app/javascript/mastodon/features/notifications/components/column_settings.js @@ -55,6 +55,16 @@ export default class ColumnSettings extends React.PureComponent {
+
+ + + + +
+ +
+
+
diff --git a/app/javascript/mastodon/features/notifications/containers/notification_container.js b/app/javascript/mastodon/features/notifications/containers/notification_container.js index 78576c760..5c984197f 100644 --- a/app/javascript/mastodon/features/notifications/containers/notification_container.js +++ b/app/javascript/mastodon/features/notifications/containers/notification_container.js @@ -1,7 +1,7 @@ import { connect } from 'react-redux'; import { makeGetNotification, makeGetStatus } from '../../../selectors'; import Notification from '../components/notification'; -import { openModal } from '../../../actions/modal'; +import { initBoostModal } from '../../../actions/boosts'; import { mentionCompose } from '../../../actions/compose'; import { reblog, @@ -35,8 +35,8 @@ const mapDispatchToProps = dispatch => ({ dispatch(mentionCompose(account, router)); }, - onModalReblog (status) { - dispatch(reblog(status)); + onModalReblog (status, privacy) { + dispatch(reblog(status, privacy)); }, onReblog (status, e) { @@ -46,7 +46,7 @@ const mapDispatchToProps = dispatch => ({ if (e.shiftKey || !boostModal) { this.onModalReblog(status); } else { - dispatch(openModal('BOOST', { status, onReblog: this.onModalReblog })); + dispatch(initBoostModal({ status, onReblog: this.onModalReblog })); } } }, diff --git a/app/javascript/mastodon/features/notifications/index.js b/app/javascript/mastodon/features/notifications/index.js index 108470c9a..1a621eca9 100644 --- a/app/javascript/mastodon/features/notifications/index.js +++ b/app/javascript/mastodon/features/notifications/index.js @@ -60,8 +60,8 @@ const mapStateToProps = state => ({ isUnread: state.getIn(['notifications', 'unread']) > 0 || state.getIn(['notifications', 'pendingItems']).size > 0, hasMore: state.getIn(['notifications', 'hasMore']), numPending: state.getIn(['notifications', 'pendingItems'], ImmutableList()).size, - lastReadId: state.getIn(['notifications', 'readMarkerId']), - canMarkAsRead: state.getIn(['notifications', 'readMarkerId']) !== '0' && getNotifications(state).some(item => item !== null && compareId(item.get('id'), state.getIn(['notifications', 'readMarkerId'])) > 0), + lastReadId: state.getIn(['settings', 'notifications', 'showUnread']) ? state.getIn(['notifications', 'readMarkerId']) : '0', + canMarkAsRead: state.getIn(['settings', 'notifications', 'showUnread']) && state.getIn(['notifications', 'readMarkerId']) !== '0' && getNotifications(state).some(item => item !== null && compareId(item.get('id'), state.getIn(['notifications', 'readMarkerId'])) > 0), needsNotificationPermission: state.getIn(['settings', 'notifications', 'alerts']).includes(true) && state.getIn(['notifications', 'browserSupport']) && state.getIn(['notifications', 'browserPermission']) === 'default' && !state.getIn(['settings', 'notifications', 'dismissPermissionBanner']), }); @@ -178,7 +178,7 @@ class Notifications extends React.PureComponent { render () { const { intl, notifications, shouldUpdateScroll, isLoading, isUnread, columnId, multiColumn, hasMore, numPending, showFilterBar, lastReadId, canMarkAsRead, needsNotificationPermission } = this.props; const pinned = !!columnId; - const emptyMessage = ; + const emptyMessage = ; let scrollableContent = null; diff --git a/app/javascript/mastodon/features/picture_in_picture/components/footer.js b/app/javascript/mastodon/features/picture_in_picture/components/footer.js index 1b1ec6d54..1ecb18bf8 100644 --- a/app/javascript/mastodon/features/picture_in_picture/components/footer.js +++ b/app/javascript/mastodon/features/picture_in_picture/components/footer.js @@ -10,6 +10,7 @@ import { defineMessages, injectIntl } from 'react-intl'; import { replyCompose } from 'mastodon/actions/compose'; import { reblog, favourite, unreblog, unfavourite } from 'mastodon/actions/interactions'; import { makeGetStatus } from 'mastodon/selectors'; +import { initBoostModal } from 'mastodon/actions/boosts'; import { openModal } from 'mastodon/actions/modal'; const messages = defineMessages({ @@ -89,9 +90,9 @@ class Footer extends ImmutablePureComponent { } }; - _performReblog = () => { - const { dispatch, status } = this.props; - dispatch(reblog(status)); + _performReblog = (status, privacy) => { + const { dispatch } = this.props; + dispatch(reblog(status, privacy)); } handleReblogClick = e => { @@ -100,9 +101,9 @@ class Footer extends ImmutablePureComponent { if (status.get('reblogged')) { dispatch(unreblog(status)); } else if ((e && e.shiftKey) || !boostModal) { - this._performReblog(); + this._performReblog(status); } else { - dispatch(openModal('BOOST', { status, onReblog: this._performReblog })); + dispatch(initBoostModal({ status, onReblog: this._performReblog })); } }; diff --git a/app/javascript/mastodon/features/status/components/action_bar.js b/app/javascript/mastodon/features/status/components/action_bar.js index 3402d83ed..fa88074f8 100644 --- a/app/javascript/mastodon/features/status/components/action_bar.js +++ b/app/javascript/mastodon/features/status/components/action_bar.js @@ -188,10 +188,11 @@ class ActionBar extends React.PureComponent { render () { const { status, relationship, intl } = this.props; - const publicStatus = ['public', 'unlisted'].includes(status.get('visibility')); + const publicStatus = ['public', 'unlisted'].includes(status.get('visibility')); const mutingConversation = status.get('muted'); const federated = !status.get('local_only'); const account = status.get('account'); + const writtenByMe = status.getIn(['account', 'id']) === me; let menu = []; @@ -201,12 +202,12 @@ class ActionBar extends React.PureComponent { menu.push(null); } - if (me === status.getIn(['account', 'id'])) { + if (writtenByMe) { if (publicStatus) { menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick }); + menu.push(null); } - menu.push(null); menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick }); menu.push(null); menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick }); diff --git a/app/javascript/mastodon/features/status/containers/detailed_status_container.js b/app/javascript/mastodon/features/status/containers/detailed_status_container.js index 0ac4519c8..bfed16620 100644 --- a/app/javascript/mastodon/features/status/containers/detailed_status_container.js +++ b/app/javascript/mastodon/features/status/containers/detailed_status_container.js @@ -23,6 +23,7 @@ import { } from '../../../actions/statuses'; import { initMuteModal } from '../../../actions/mutes'; import { initBlockModal } from '../../../actions/blocks'; +import { initBoostModal } from '../../../actions/boosts'; import { initReport } from '../../../actions/reports'; import { openModal } from '../../../actions/modal'; import { defineMessages, injectIntl } from 'react-intl'; @@ -68,8 +69,8 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ }); }, - onModalReblog (status) { - dispatch(reblog(status)); + onModalReblog (status, privacy) { + dispatch(reblog(status, privacy)); }, onReblog (status, e) { @@ -79,7 +80,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ if (e.shiftKey || !boostModal) { this.onModalReblog(status); } else { - dispatch(openModal('BOOST', { status, onReblog: this.onModalReblog })); + dispatch(initBoostModal({ status, onReblog: this.onModalReblog })); } } }, diff --git a/app/javascript/mastodon/features/status/index.js b/app/javascript/mastodon/features/status/index.js index 09822f372..df8362a1b 100644 --- a/app/javascript/mastodon/features/status/index.js +++ b/app/javascript/mastodon/features/status/index.js @@ -42,6 +42,7 @@ import { } from '../../actions/domain_blocks'; import { initMuteModal } from '../../actions/mutes'; import { initBlockModal } from '../../actions/blocks'; +import { initBoostModal } from '../../actions/boosts'; import { initReport } from '../../actions/reports'; import { makeGetStatus, makeGetPictureInPicture } from '../../selectors'; import { ScrollContainer } from 'react-router-scroll-4'; @@ -234,8 +235,8 @@ class Status extends ImmutablePureComponent { } } - handleModalReblog = (status) => { - this.props.dispatch(reblog(status)); + handleModalReblog = (status, privacy) => { + this.props.dispatch(reblog(status, privacy)); } handleReblogClick = (status, e) => { @@ -245,7 +246,7 @@ class Status extends ImmutablePureComponent { if ((e && e.shiftKey) || !boostModal) { this.handleModalReblog(status); } else { - this.props.dispatch(openModal('BOOST', { status, onReblog: this.handleModalReblog })); + this.props.dispatch(initBoostModal({ status, onReblog: this.handleModalReblog })); } } } diff --git a/app/javascript/mastodon/features/ui/components/boost_modal.js b/app/javascript/mastodon/features/ui/components/boost_modal.js index 963bb5dc4..83229833b 100644 --- a/app/javascript/mastodon/features/ui/components/boost_modal.js +++ b/app/javascript/mastodon/features/ui/components/boost_modal.js @@ -1,4 +1,5 @@ import React from 'react'; +import { connect } from 'react-redux'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; @@ -10,7 +11,9 @@ import DisplayName from '../../../components/display_name'; import ImmutablePureComponent from 'react-immutable-pure-component'; import Icon from 'mastodon/components/icon'; import AttachmentList from 'mastodon/components/attachment_list'; +import PrivacyDropdown from 'mastodon/features/compose/components/privacy_dropdown'; import classNames from 'classnames'; +import { changeBoostPrivacy } from 'mastodon/actions/boosts'; const messages = defineMessages({ cancel_reblog: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' }, @@ -21,7 +24,22 @@ const messages = defineMessages({ direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' }, }); -export default @injectIntl +const mapStateToProps = state => { + return { + privacy: state.getIn(['boosts', 'new', 'privacy']), + }; +}; + +const mapDispatchToProps = dispatch => { + return { + onChangeBoostPrivacy(value) { + dispatch(changeBoostPrivacy(value)); + }, + }; +}; + +export default @connect(mapStateToProps, mapDispatchToProps) +@injectIntl class BoostModal extends ImmutablePureComponent { static contextTypes = { @@ -32,6 +50,8 @@ class BoostModal extends ImmutablePureComponent { status: ImmutablePropTypes.map.isRequired, onReblog: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired, + onChangeBoostPrivacy: PropTypes.func.isRequired, + privacy: PropTypes.string.isRequired, intl: PropTypes.object.isRequired, }; @@ -40,7 +60,7 @@ class BoostModal extends ImmutablePureComponent { } handleReblog = () => { - this.props.onReblog(this.props.status); + this.props.onReblog(this.props.status, this.props.privacy); this.props.onClose(); } @@ -52,12 +72,16 @@ class BoostModal extends ImmutablePureComponent { } } + _findContainer = () => { + return document.getElementsByClassName('modal-root__container')[0]; + }; + setRef = (c) => { this.button = c; } render () { - const { status, intl } = this.props; + const { status, privacy, intl } = this.props; const buttonText = status.get('reblogged') ? messages.cancel_reblog : messages.reblog; const visibilityIconInfo = { @@ -102,6 +126,14 @@ class BoostModal extends ImmutablePureComponent {
Shift + }} />
+ {status.get('visibility') !== 'private' && !status.get('reblogged') && ( + + )}
diff --git a/app/javascript/mastodon/features/ui/components/columns_area.js b/app/javascript/mastodon/features/ui/components/columns_area.js index 95136ec5a..6432c0196 100644 --- a/app/javascript/mastodon/features/ui/components/columns_area.js +++ b/app/javascript/mastodon/features/ui/components/columns_area.js @@ -53,7 +53,7 @@ const messages = defineMessages({ publish: { id: 'compose_form.publish', defaultMessage: 'Post' }, }); -const shouldHideFAB = path => path.match(/^\/statuses\/|^\/search|^\/getting-started/); +const shouldHideFAB = path => path.match(/^\/statuses\/|^\/search|^\/getting-started|^\/start/); export default @(component => injectIntl(component, { withRef: true })) class ColumnsArea extends ImmutablePureComponent { @@ -70,8 +70,12 @@ class ColumnsArea extends ImmutablePureComponent { children: PropTypes.node, }; + // Corresponds to (max-width: 600px + (285px * 1) + (10px * 1)) in SCSS + mediaQuery = 'matchMedia' in window && window.matchMedia('(max-width: 895px)'); + state = { shouldAnimate: false, + renderComposePanel: !(this.mediaQuery && this.mediaQuery.matches), } componentWillReceiveProps() { @@ -85,6 +89,15 @@ class ColumnsArea extends ImmutablePureComponent { this.node.addEventListener('wheel', this.handleWheel, supportsPassiveEvents ? { passive: true } : false); } + if (this.mediaQuery) { + if (this.mediaQuery.addEventListener) { + this.mediaQuery.addEventListener('change', this.handleLayoutChange); + } else { + this.mediaQuery.addListener(this.handleLayoutChange); + } + this.setState({ renderComposePanel: !this.mediaQuery.matches }); + } + this.lastIndex = getIndex(this.context.router.history.location.pathname); this.isRtlLayout = document.getElementsByTagName('body')[0].classList.contains('rtl'); @@ -114,6 +127,14 @@ class ColumnsArea extends ImmutablePureComponent { if (!this.props.singleColumn) { this.node.removeEventListener('wheel', this.handleWheel); } + + if (this.mediaQuery) { + if (this.mediaQuery.removeEventListener) { + this.mediaQuery.removeEventListener('change', this.handleLayoutChange); + } else { + this.mediaQuery.removeListener(this.handleLayouteChange); + } + } } handleChildrenContentChange() { @@ -123,6 +144,10 @@ class ColumnsArea extends ImmutablePureComponent { } } + handleLayoutChange = (e) => { + this.setState({ renderComposePanel: !e.matches }); + } + handleSwipe = (index) => { this.pendingIndex = index; @@ -186,7 +211,7 @@ class ColumnsArea extends ImmutablePureComponent { render () { const { columns, children, singleColumn, isModalOpen, intl } = this.props; - const { shouldAnimate } = this.state; + const { shouldAnimate, renderComposePanel } = this.state; const columnIndex = getIndex(this.context.router.history.location.pathname); @@ -205,7 +230,7 @@ class ColumnsArea extends ImmutablePureComponent {
- + {renderComposePanel && }
diff --git a/app/javascript/mastodon/features/ui/components/focal_point_modal.js b/app/javascript/mastodon/features/ui/components/focal_point_modal.js index 578375a7f..3457b7633 100644 --- a/app/javascript/mastodon/features/ui/components/focal_point_modal.js +++ b/app/javascript/mastodon/features/ui/components/focal_point_modal.js @@ -227,7 +227,7 @@ class FocalPointModal extends ImmutablePureComponent { const worker = createWorker({ workerPath: tesseractWorkerPath, corePath: tesseractCorePath, - langPath: assetHost, + langPath: `${assetHost}/ocr/lang-data/`, logger: ({ status, progress }) => { if (status === 'recognizing text') { this.setState({ ocrStatus: 'detecting', progress }); @@ -309,7 +309,7 @@ class FocalPointModal extends ImmutablePureComponent { return (
- +
diff --git a/app/javascript/mastodon/features/ui/components/media_modal.js b/app/javascript/mastodon/features/ui/components/media_modal.js index 7fe7ed094..08da10330 100644 --- a/app/javascript/mastodon/features/ui/components/media_modal.js +++ b/app/javascript/mastodon/features/ui/components/media_modal.js @@ -32,6 +32,9 @@ class MediaModal extends ImmutablePureComponent { onClose: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, onChangeBackgroundColor: PropTypes.func.isRequired, + currentTime: PropTypes.number, + autoPlay: PropTypes.bool, + volume: PropTypes.number, }; static contextTypes = { @@ -183,7 +186,7 @@ class MediaModal extends ImmutablePureComponent { /> ); } else if (image.get('type') === 'video') { - const { time } = this.props; + const { currentTime, autoPlay, volume } = this.props; return (