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/CHANGELOG.md b/CHANGELOG.md index bc853aaa3..12d1563c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ Changelog All notable changes to this project will be documented in this file. -## Unreleased +## [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)) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 31f0a1319..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: diff --git a/Gemfile.lock b/Gemfile.lock index 45cfc27d8..8a63e1d65 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -364,7 +364,7 @@ GEM net-ssh (>= 2.6.5, < 7.0.0) net-ssh (6.1.0) nio4r (2.5.7) - nokogiri (1.11.3) + nokogiri (1.11.4) mini_portile2 (~> 2.5.0) racc (~> 1.4) nokogumbo (2.0.4) @@ -428,7 +428,7 @@ GEM pry-rails (0.3.9) pry (>= 0.10.4) public_suffix (4.0.6) - puma (5.3.0) + puma (5.3.1) nio4r (~> 2.0) pundit (2.1.0) activesupport (>= 3.0.0) diff --git a/README.md b/README.md index 94d0d2b71..3e0c4e97e 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ Mastodon acts as an OAuth2 provider so 3rd party apps can use the REST and Strea - **PostgreSQL** 9.5+ - **Redis** 4+ - **Ruby** 2.5+ -- **Node.js** 10.13+ +- **Node.js** 12+ The repository includes deployment configurations for **Docker and docker-compose**, but also a few specific platforms like **Heroku**, **Scalingo**, and **Nanobox**. The [**stand-alone** installation guide](https://docs.joinmastodon.org/admin/install/) is available in the documentation. 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 bfe2c374f..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"]} diff --git a/app/controllers/api/v1/accounts/statuses_controller.rb b/app/controllers/api/v1/accounts/statuses_controller.rb index 92ccb8061..f301666db 100644 --- a/app/controllers/api/v1/accounts/statuses_controller.rb +++ b/app/controllers/api/v1/accounts/statuses_controller.rb @@ -7,8 +7,9 @@ class Api::V1::Accounts::StatusesController < Api::BaseController after_action :insert_pagination_headers, unless: -> { truthy_param?(:pinned) } def index - @statuses = load_statuses - render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id) + @statuses = load_statuses + accountIds = @statuses.filter(&:quote?).map { |status| status.quote.account_id }.uniq + render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id), account_relationships: AccountRelationshipsPresenter.new(accountIds, current_user&.account_id) end private diff --git a/app/controllers/api/v1/bookmarks_controller.rb b/app/controllers/api/v1/bookmarks_controller.rb index aa3fb88f0..f666a1d8d 100644 --- a/app/controllers/api/v1/bookmarks_controller.rb +++ b/app/controllers/api/v1/bookmarks_controller.rb @@ -6,8 +6,9 @@ class Api::V1::BookmarksController < Api::BaseController after_action :insert_pagination_headers def index - @statuses = load_statuses - render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id) + @statuses = load_statuses + accountIds = @statuses.filter(&:quote?).map { |status| status.quote.account_id }.uniq + render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id), account_relationships: AccountRelationshipsPresenter.new(accountIds, current_user&.account_id) end private diff --git a/app/controllers/api/v1/favourites_controller.rb b/app/controllers/api/v1/favourites_controller.rb index 21836bc17..651a057d5 100644 --- a/app/controllers/api/v1/favourites_controller.rb +++ b/app/controllers/api/v1/favourites_controller.rb @@ -6,8 +6,9 @@ class Api::V1::FavouritesController < Api::BaseController after_action :insert_pagination_headers def index - @statuses = load_statuses - render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id) + @statuses = load_statuses + accountIds = @statuses.filter(&:quote?).map { |status| status.quote.account_id }.uniq + render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id), account_relationships: AccountRelationshipsPresenter.new(accountIds, current_user&.account_id) end private diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb index 106fc8224..d8f4db42f 100644 --- a/app/controllers/api/v1/statuses_controller.rb +++ b/app/controllers/api/v1/statuses_controller.rb @@ -30,8 +30,9 @@ class Api::V1::StatusesController < Api::BaseController @context = Context.new(ancestors: loaded_ancestors, descendants: loaded_descendants) statuses = [@status] + @context.ancestors + @context.descendants + accountIds = statuses.filter(&:quote?).map { |status| status.quote.account_id }.uniq - render json: @context, serializer: REST::ContextSerializer, relationships: StatusRelationshipsPresenter.new(statuses, current_user&.account_id) + render json: @context, serializer: REST::ContextSerializer, relationships: StatusRelationshipsPresenter.new(statuses, current_user&.account_id), account_relationships: AccountRelationshipsPresenter.new(accountIds, current_user&.account_id) end def create @@ -46,7 +47,8 @@ class Api::V1::StatusesController < Api::BaseController application: doorkeeper_token.application, poll: status_params[:poll], idempotency: request.headers['Idempotency-Key'], - with_rate_limit: true) + with_rate_limit: true, + quote_id: status_params[:quote_id].presence) render json: @status, serializer: @status.is_a?(ScheduledStatus) ? REST::ScheduledStatusSerializer : REST::StatusSerializer end @@ -85,6 +87,7 @@ class Api::V1::StatusesController < Api::BaseController :spoiler_text, :visibility, :scheduled_at, + :quote_id, media_ids: [], poll: [ :multiple, diff --git a/app/controllers/api/v1/timelines/home_controller.rb b/app/controllers/api/v1/timelines/home_controller.rb index ae6dbcb8b..c07e1a820 100644 --- a/app/controllers/api/v1/timelines/home_controller.rb +++ b/app/controllers/api/v1/timelines/home_controller.rb @@ -6,11 +6,13 @@ class Api::V1::Timelines::HomeController < Api::BaseController after_action :insert_pagination_headers, unless: -> { @statuses.empty? } def show - @statuses = load_statuses + @statuses = load_statuses + accountIds = @statuses.filter(&:quote?).map { |status| status.quote.account_id }.uniq render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id), + account_relationships: AccountRelationshipsPresenter.new(accountIds, current_user&.account_id), status: account_home_feed.regenerating? ? 206 : 200 end diff --git a/app/controllers/api/v1/timelines/list_controller.rb b/app/controllers/api/v1/timelines/list_controller.rb index a15eae468..bc13ed5ec 100644 --- a/app/controllers/api/v1/timelines/list_controller.rb +++ b/app/controllers/api/v1/timelines/list_controller.rb @@ -9,9 +9,12 @@ class Api::V1::Timelines::ListController < Api::BaseController after_action :insert_pagination_headers, unless: -> { @statuses.empty? } def show + accountIds = @statuses.filter(&:quote?).map { |status| status.quote.account_id }.uniq + render json: @statuses, each_serializer: REST::StatusSerializer, - relationships: StatusRelationshipsPresenter.new(@statuses, current_user.account_id) + relationships: StatusRelationshipsPresenter.new(@statuses, current_user.account_id), + account_relationships: AccountRelationshipsPresenter.new(accountIds, current_user&.account_id) end private diff --git a/app/controllers/api/v1/timelines/public_controller.rb b/app/controllers/api/v1/timelines/public_controller.rb index d253b744f..1a778012b 100644 --- a/app/controllers/api/v1/timelines/public_controller.rb +++ b/app/controllers/api/v1/timelines/public_controller.rb @@ -5,8 +5,10 @@ class Api::V1::Timelines::PublicController < Api::BaseController after_action :insert_pagination_headers, unless: -> { @statuses.empty? } def show - @statuses = load_statuses - render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id) + @statuses = load_statuses + accountIds = @statuses.filter(&:quote?).map { |status| status.quote.account_id }.uniq + + render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id), account_relationships: AccountRelationshipsPresenter.new(accountIds, current_user&.account_id) end private diff --git a/app/controllers/api/v1/timelines/tag_controller.rb b/app/controllers/api/v1/timelines/tag_controller.rb index 64a1db58d..c7afd4580 100644 --- a/app/controllers/api/v1/timelines/tag_controller.rb +++ b/app/controllers/api/v1/timelines/tag_controller.rb @@ -5,8 +5,9 @@ class Api::V1::Timelines::TagController < Api::BaseController after_action :insert_pagination_headers, unless: -> { @statuses.empty? } def show - @statuses = load_statuses - render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id) + @statuses = load_statuses + accountIds = @statuses.filter(&:quote?).map { |status| status.quote.account_id }.uniq + render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id), account_relationships: AccountRelationshipsPresenter.new(accountIds, current_user&.account_id) end private diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb index b60901040..0ebfab75d 100644 --- a/app/helpers/settings_helper.rb +++ b/app/helpers/settings_helper.rb @@ -18,6 +18,7 @@ module SettingsHelper en: 'English', eo: 'Esperanto', 'es-AR': 'Español (Argentina)', + 'es-MX': 'Español (México)', es: 'Español', et: 'Eesti', eu: 'Euskara', diff --git a/app/javascript/mastodon/actions/bookmarks.js b/app/javascript/mastodon/actions/bookmarks.js index 544ed2ff2..b3e0a2ab4 100644 --- a/app/javascript/mastodon/actions/bookmarks.js +++ b/app/javascript/mastodon/actions/bookmarks.js @@ -1,5 +1,7 @@ +import { fetchRelationships } from './accounts'; import api, { getLinks } from '../api'; import { importFetchedStatuses } from './importer'; +import { uniq } from '../utils/uniq'; export const BOOKMARKED_STATUSES_FETCH_REQUEST = 'BOOKMARKED_STATUSES_FETCH_REQUEST'; export const BOOKMARKED_STATUSES_FETCH_SUCCESS = 'BOOKMARKED_STATUSES_FETCH_SUCCESS'; @@ -20,6 +22,7 @@ export function fetchBookmarkedStatuses() { api(getState).get('/api/v1/bookmarks').then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); dispatch(importFetchedStatuses(response.data)); + dispatch(fetchRelationships(uniq(response.data.map(item => item.reblog ? item.reblog.account.id : item.account.id)))); dispatch(fetchBookmarkedStatusesSuccess(response.data, next ? next.uri : null)); }).catch(error => { dispatch(fetchBookmarkedStatusesFail(error)); @@ -61,6 +64,7 @@ export function expandBookmarkedStatuses() { api(getState).get(url).then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); dispatch(importFetchedStatuses(response.data)); + dispatch(fetchRelationships(uniq(response.data.map(item => item.reblog ? item.reblog.account.id : item.account.id)))); dispatch(expandBookmarkedStatusesSuccess(response.data, next ? next.uri : null)); }).catch(error => { dispatch(expandBookmarkedStatusesFail(error)); diff --git a/app/javascript/mastodon/actions/boosts.js b/app/javascript/mastodon/actions/boosts.js index 6e14065d6..c0f0f3acc 100644 --- a/app/javascript/mastodon/actions/boosts.js +++ b/app/javascript/mastodon/actions/boosts.js @@ -11,7 +11,7 @@ export function initBoostModal(props) { dispatch({ type: BOOSTS_INIT_MODAL, - privacy + privacy, }); dispatch(openModal('BOOST', props)); diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js index 891403969..fb4d26cf2 100644 --- a/app/javascript/mastodon/actions/compose.js +++ b/app/javascript/mastodon/actions/compose.js @@ -20,6 +20,8 @@ export const COMPOSE_SUBMIT_FAIL = 'COMPOSE_SUBMIT_FAIL'; export const COMPOSE_REPLY = 'COMPOSE_REPLY'; export const COMPOSE_REPLY_CANCEL = 'COMPOSE_REPLY_CANCEL'; export const COMPOSE_DIRECT = 'COMPOSE_DIRECT'; +export const COMPOSE_QUOTE = 'COMPOSE_QUOTE'; +export const COMPOSE_QUOTE_CANCEL = 'COMPOSE_QUOTE_CANCEL'; export const COMPOSE_MENTION = 'COMPOSE_MENTION'; export const COMPOSE_RESET = 'COMPOSE_RESET'; export const COMPOSE_UPLOAD_REQUEST = 'COMPOSE_UPLOAD_REQUEST'; @@ -100,6 +102,23 @@ export function cancelReplyCompose() { }; }; +export function quoteCompose(status, routerHistory) { + return (dispatch, getState) => { + dispatch({ + type: COMPOSE_QUOTE, + status: status, + }); + + ensureComposeIsVisible(getState, routerHistory); + }; +}; + +export function cancelQuoteCompose() { + return { + type: COMPOSE_QUOTE_CANCEL, + }; +}; + export function resetCompose() { return { type: COMPOSE_RESET, @@ -147,6 +166,7 @@ export function submitCompose(routerHistory) { spoiler_text: getState().getIn(['compose', 'spoiler']) ? getState().getIn(['compose', 'spoiler_text'], '') : '', visibility: getState().getIn(['compose', 'privacy']), poll: getState().getIn(['compose', 'poll'], null), + quote_id: getState().getIn(['compose', 'quote_from'], null), }, { headers: { 'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']), diff --git a/app/javascript/mastodon/actions/favourites.js b/app/javascript/mastodon/actions/favourites.js index 9448b1efe..9b28ac4c4 100644 --- a/app/javascript/mastodon/actions/favourites.js +++ b/app/javascript/mastodon/actions/favourites.js @@ -1,5 +1,7 @@ +import { fetchRelationships } from './accounts'; import api, { getLinks } from '../api'; import { importFetchedStatuses } from './importer'; +import { uniq } from '../utils/uniq'; export const FAVOURITED_STATUSES_FETCH_REQUEST = 'FAVOURITED_STATUSES_FETCH_REQUEST'; export const FAVOURITED_STATUSES_FETCH_SUCCESS = 'FAVOURITED_STATUSES_FETCH_SUCCESS'; @@ -20,6 +22,7 @@ export function fetchFavouritedStatuses() { api(getState).get('/api/v1/favourites').then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); dispatch(importFetchedStatuses(response.data)); + dispatch(fetchRelationships(uniq(response.data.map(item => item.reblog ? item.reblog.account.id : item.account.id)))); dispatch(fetchFavouritedStatusesSuccess(response.data, next ? next.uri : null)); }).catch(error => { dispatch(fetchFavouritedStatusesFail(error)); @@ -64,6 +67,7 @@ export function expandFavouritedStatuses() { api(getState).get(url).then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); dispatch(importFetchedStatuses(response.data)); + dispatch(fetchRelationships(uniq(response.data.map(item => item.reblog ? item.reblog.account.id : item.account.id)))); dispatch(expandFavouritedStatusesSuccess(response.data, next ? next.uri : null)); }).catch(error => { dispatch(expandFavouritedStatusesFail(error)); diff --git a/app/javascript/mastodon/actions/importer/index.js b/app/javascript/mastodon/actions/importer/index.js index f4372fb31..35032fc70 100644 --- a/app/javascript/mastodon/actions/importer/index.js +++ b/app/javascript/mastodon/actions/importer/index.js @@ -70,6 +70,10 @@ export function importFetchedStatuses(statuses) { processStatus(status.reblog); } + if (status.quote && status.quote.id) { + processStatus(status.quote); + } + if (status.poll && status.poll.id) { pushUnique(polls, normalizePoll(status.poll)); } diff --git a/app/javascript/mastodon/actions/importer/normalizer.js b/app/javascript/mastodon/actions/importer/normalizer.js index abd5681d4..1b9d781a7 100644 --- a/app/javascript/mastodon/actions/importer/normalizer.js +++ b/app/javascript/mastodon/actions/importer/normalizer.js @@ -60,7 +60,10 @@ 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'); + normalStatus.quote = normalOldStatus.get('quote'); + normalStatus.quote_hidden = normalOldStatus.get('quote_hidden'); } else { // If the status has a CW but no contents, treat the CW as if it were the // status' contents, to avoid having a CW toggle with seemingly no effect. @@ -77,6 +80,29 @@ export function normalizeStatus(status, normalOldStatus) { normalStatus.contentHtml = emojify(normalStatus.content, emojiMap); normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(spoilerText), emojiMap); normalStatus.hidden = expandSpoilers ? false : spoilerText.length > 0 || normalStatus.sensitive; + + if (status.quote && status.quote.id) { + const quote_spoilerText = status.quote.spoiler_text || ''; + const quote_searchContent = [quote_spoilerText, status.quote.content].join('\n\n').replace(//g, '\n').replace(/<\/p>

/g, '\n\n'); + + const quote_emojiMap = makeEmojiMap(normalStatus.quote); + + const quote_account_emojiMap = makeEmojiMap(status.quote.account); + const displayName = normalStatus.quote.account.display_name.length === 0 ? normalStatus.quote.account.username : normalStatus.quote.account.display_name; + normalStatus.quote.account.display_name_html = emojify(escapeTextContentForBrowser(displayName), quote_account_emojiMap); + normalStatus.quote.search_index = domParser.parseFromString(quote_searchContent, 'text/html').documentElement.textContent; + let docElem = domParser.parseFromString(normalStatus.quote.content, 'text/html').documentElement; + Array.from(docElem.querySelectorAll('p,br'), line => { + let parentNode = line.parentNode; + if (line.nextSibling) { + parentNode.insertBefore(document.createTextNode(' '), line.nextSibling); + } + }); + let _contentHtml = docElem.textContent; + normalStatus.quote.contentHtml = '

'+emojify(_contentHtml.substr(0, 150), quote_emojiMap) + (_contentHtml.substr(150) ? '...' : '')+'

'; + normalStatus.quote.spoilerHtml = emojify(escapeTextContentForBrowser(quote_spoilerText), quote_emojiMap); + normalStatus.quote_hidden = expandSpoilers ? false : quote_spoilerText.length > 0 || normalStatus.quote.sensitive; + } } return normalStatus; diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js index 3464ac995..22f1e0cbb 100644 --- a/app/javascript/mastodon/actions/notifications.js +++ b/app/javascript/mastodon/actions/notifications.js @@ -46,7 +46,7 @@ defineMessages({ }); const fetchRelatedRelationships = (dispatch, notifications) => { - const accountIds = notifications.filter(item => item.type === 'follow').map(item => item.account.id); + const accountIds = notifications.map(item => item.account.id); if (accountIds.length > 0) { dispatch(fetchRelationships(accountIds)); diff --git a/app/javascript/mastodon/actions/statuses.js b/app/javascript/mastodon/actions/statuses.js index 3fc7c0702..0c520a796 100644 --- a/app/javascript/mastodon/actions/statuses.js +++ b/app/javascript/mastodon/actions/statuses.js @@ -30,6 +30,9 @@ export const STATUS_COLLAPSE = 'STATUS_COLLAPSE'; export const REDRAFT = 'REDRAFT'; +export const QUOTE_REVEAL = 'QUOTE_REVEAL'; +export const QUOTE_HIDE = 'QUOTE_HIDE'; + export function fetchStatusRequest(id, skipLoading) { return { type: STATUS_FETCH_REQUEST, @@ -272,3 +275,25 @@ export function toggleStatusCollapse(id, isCollapsed) { isCollapsed, }; } + +export function hideQuote(ids) { + if (!Array.isArray(ids)) { + ids = [ids]; + } + + return { + type: QUOTE_HIDE, + ids, + }; +}; + +export function revealQuote(ids) { + if (!Array.isArray(ids)) { + ids = [ids]; + } + + return { + type: QUOTE_REVEAL, + ids, + }; +}; diff --git a/app/javascript/mastodon/actions/timelines.js b/app/javascript/mastodon/actions/timelines.js index 31ae09e4a..d82bf1d0d 100644 --- a/app/javascript/mastodon/actions/timelines.js +++ b/app/javascript/mastodon/actions/timelines.js @@ -1,9 +1,11 @@ +import { fetchRelationships } from './accounts'; import { importFetchedStatus, importFetchedStatuses } from './importer'; import { submitMarkers } from './markers'; import api, { getLinks } from 'mastodon/api'; import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; import compareId from 'mastodon/compare_id'; import { usePendingItems as preferPendingItems } from 'mastodon/initial_state'; +import { uniq } from '../utils/uniq'; export const TIMELINE_UPDATE = 'TIMELINE_UPDATE'; export const TIMELINE_DELETE = 'TIMELINE_DELETE'; @@ -39,6 +41,7 @@ export function updateTimeline(timeline, status, accept) { } dispatch(importFetchedStatus(status)); + dispatch(fetchRelationships([status.reblog ? status.reblog.account.id : status.account.id, status.quote ? status.quote.account.id : null].filter(function(e){return e}))); dispatch({ type: TIMELINE_UPDATE, @@ -111,6 +114,7 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) { api(getState).get(path, { params }).then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); dispatch(importFetchedStatuses(response.data)); + dispatch(fetchRelationships(uniq(response.data.map(item => item.reblog ? item.reblog.account.id : item.account.id).concat(response.data.map(item => item.quote ? item.quote.account.id : null)).filter(function(e){return e})))); dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null, response.status === 206, isLoadingRecent, isLoadingMore, isLoadingRecent && preferPendingItems)); if (timelineId === 'home') { diff --git a/app/javascript/mastodon/components/media_gallery.js b/app/javascript/mastodon/components/media_gallery.js index 2e7ce2e60..5c303a1a0 100644 --- a/app/javascript/mastodon/components/media_gallery.js +++ b/app/javascript/mastodon/components/media_gallery.js @@ -236,10 +236,12 @@ class MediaGallery extends React.PureComponent { visible: PropTypes.bool, autoplay: PropTypes.bool, onToggleVisibility: PropTypes.func, + quote: PropTypes.bool, }; static defaultProps = { standalone: false, + quote: false, }; state = { @@ -310,7 +312,7 @@ class MediaGallery extends React.PureComponent { } render () { - const { media, intl, sensitive, height, defaultWidth, standalone, autoplay } = this.props; + const { media, intl, sensitive, height, defaultWidth, standalone, autoplay, quote } = this.props; const { visible } = this.state; const width = this.state.width || defaultWidth; @@ -332,6 +334,10 @@ class MediaGallery extends React.PureComponent { const size = media.take(4).size; const uncached = media.every(attachment => attachment.get('type') === 'unknown'); + if (quote && style.height) { + style.height /= 2; + } + if (standalone && this.isFullSizeEligible()) { children = ; } else { diff --git a/app/javascript/mastodon/components/poll.js b/app/javascript/mastodon/components/poll.js index 477f56e13..1126b0121 100644 --- a/app/javascript/mastodon/components/poll.js +++ b/app/javascript/mastodon/components/poll.js @@ -39,6 +39,9 @@ class Poll extends ImmutablePureComponent { static getDerivedStateFromProps (props, state) { const { poll, intl } = props; + if (!poll) { + return null; + } const expires_at = poll.get('expires_at'); const expired = poll.get('expired') || expires_at !== null && (new Date(expires_at)).getTime() < intl.now(); return (expired === state.expired) ? null : { expired }; @@ -59,7 +62,7 @@ class Poll extends ImmutablePureComponent { _setupTimer () { const { poll, intl } = this.props; clearTimeout(this._timer); - if (!this.state.expired) { + if (!this.state.expired && !!poll) { const delay = (new Date(poll.get('expires_at'))).getTime() - intl.now(); this._timer = setTimeout(() => { this.setState({ expired: true }); diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js index 295e83f58..a08bb291a 100644 --- a/app/javascript/mastodon/components/status.js +++ b/app/javascript/mastodon/components/status.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 Avatar from './avatar'; @@ -23,6 +24,29 @@ import PictureInPicturePlaceholder from 'mastodon/components/picture_in_picture_ // to use the progress bar to show download progress import Bundle from '../features/ui/components/bundle'; +const mapStateToProps = (state, props) => { + let status = props.status; + + if (status === null) { + return null; + } + + if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') { + status = status.get('reblog'); + } + + if (status.get('quote', null) === null) { + return { + quote_muted: status.get('quote_id', null) ? true : false, + }; + } + const id = status.getIn(['quote', 'account', 'id'], null); + + return { + quote_muted: id !== null && (state.getIn(['relationships', id, 'muting']) || state.getIn(['relationships', id, 'blocking']) || state.getIn(['relationships', id, 'blocked_by']) || state.getIn(['relationships', id, 'domain_blocking'])) || status.getIn(['quote', 'quote_muted']), + }; +}; + export const textForScreenReader = (intl, status, rebloggedByText = false) => { const displayName = status.getIn(['account', 'display_name']); @@ -59,7 +83,8 @@ const messages = defineMessages({ direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' }, }); -export default @injectIntl +export default @connect(mapStateToProps) +@injectIntl class Status extends ImmutablePureComponent { static contextTypes = { @@ -70,6 +95,7 @@ class Status extends ImmutablePureComponent { status: ImmutablePropTypes.map, account: ImmutablePropTypes.map, otherAccounts: ImmutablePropTypes.list, + quote_muted: PropTypes.bool, onClick: PropTypes.func, onReply: PropTypes.func, onFavourite: PropTypes.func, @@ -85,6 +111,7 @@ class Status extends ImmutablePureComponent { onHeightChange: PropTypes.func, onToggleHidden: PropTypes.func, onToggleCollapsed: PropTypes.func, + onQuoteToggleHidden: PropTypes.func, muted: PropTypes.bool, hidden: PropTypes.bool, unread: PropTypes.bool, @@ -101,6 +128,7 @@ class Status extends ImmutablePureComponent { inUse: PropTypes.bool, available: PropTypes.bool, }), + contextType: PropTypes.string, }; // Avoid checking props that are functions (and whose equality will always @@ -112,10 +140,12 @@ class Status extends ImmutablePureComponent { 'hidden', 'unread', 'pictureInPicture', + 'quote_muted', ]; state = { showMedia: defaultMediaVisibility(this.props.status), + showQuoteMedia: defaultMediaVisibility(this.props.status ? this.props.status.get('quote', null) : null), statusId: undefined, }; @@ -123,6 +153,7 @@ class Status extends ImmutablePureComponent { if (nextProps.status && nextProps.status.get('id') !== prevState.statusId) { return { showMedia: defaultMediaVisibility(nextProps.status), + showQuoteMedia: defaultMediaVisibility(nextProps.status.get('quote', null)), statusId: nextProps.status.get('id'), }; } else { @@ -134,6 +165,10 @@ class Status extends ImmutablePureComponent { this.setState({ showMedia: !this.state.showMedia }); } + handleToggleQuoteMediaVisibility = () => { + this.setState({ showQuoteMedia: !this.state.showQuoteMedia }); + } + handleClick = () => { if (this.props.onClick) { this.props.onClick(); @@ -164,6 +199,15 @@ class Status extends ImmutablePureComponent { } } + handleQuoteClick = () => { + if (!this.context.router) { + return; + } + + const { status } = this.props; + this.context.router.history.push(`/statuses/${status.getIn(['reblog', 'quote', 'id'], status.getIn(['quote', 'id']))}`); + } + handleAccountClick = (e) => { if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) { const id = e.currentTarget.getAttribute('data-id'); @@ -180,6 +224,10 @@ class Status extends ImmutablePureComponent { this.props.onToggleCollapsed(this._properStatus(), isCollapsed); } + handleExpandedQuoteToggle = () => { + this.props.onQuoteToggleHidden(this._properStatus()); + }; + renderLoadingMediaGallery () { return
; } @@ -197,10 +245,19 @@ class Status extends ImmutablePureComponent { this.props.onOpenVideo(status.get('id'), status.getIn(['media_attachments', 0]), options); } + handleOpenVideoQuote = (options) => { + const status = this._properQuoteStatus(); + this.props.onOpenVideo(status.get('id'), status.getIn(['media_attachments', 0]), options); + } + handleOpenMedia = (media, index) => { this.props.onOpenMedia(this._properStatus().get('id'), media, index); } + handleOpenMediaQuote = (media, index) => { + this.props.onOpenMedia(this._properQuoteStatus().get('id'), media, index); + } + handleHotkeyOpenMedia = e => { const { onOpenMedia, onOpenVideo } = this.props; const status = this._properStatus(); @@ -275,6 +332,16 @@ class Status extends ImmutablePureComponent { } } + _properQuoteStatus () { + const status = this._properStatus(); + + if (status.get('quote', null) !== null && typeof status.get('quote') === 'object') { + return status.get('quote'); + } else { + return status; + } + } + handleRef = c => { this.node = c; } @@ -283,7 +350,7 @@ class Status extends ImmutablePureComponent { let media = null; let statusAvatar, prepend, rebloggedByText; - const { intl, hidden, featured, otherAccounts, unread, showThread, scrollKey, pictureInPicture } = this.props; + const { intl, hidden, featured, otherAccounts, unread, showThread, scrollKey, pictureInPicture, contextType, quote_muted } = this.props; let { status, account, ...other } = this.props; @@ -354,10 +421,10 @@ class Status extends ImmutablePureComponent { status = status.get('reblog'); } - if (pictureInPicture.get('inUse')) { - media = ; - } else if (status.get('media_attachments').size > 0) { - if (this.props.muted) { + if (status.get('media_attachments').size > 0) { + if (pictureInPicture.get('inUse')) { + media = ; + } else if (this.props.muted) { media = ( 0) { + if (pictureInPicture.get('inUse')) { + quote_media = ; + } else if (this.props.muted) { + quote_media = ( + + ); + } else if (quote_status.getIn(['media_attachments', 0, 'type']) === 'audio') { + const attachment = quote_status.getIn(['media_attachments', 0]); + + quote_media = ( + + {Component => ( + + )} + + ); + } else if (quote_status.getIn(['media_attachments', 0, 'type']) === 'video') { + const attachment = quote_status.getIn(['media_attachments', 0]); + + quote_media = ( + + {Component => ( + + )} + + ); + } else { + quote_media = ( + + {Component => ( + + )} + + ); + } + } + + if (quote_muted) { + quote = ( +
+
+ +
+
+ ); + } else if (quote_status.get('visibility') === 'unlisted' && !!contextType && ['public', 'community', 'hashtag'].includes(contextType.split(':', 2)[0])) { + quote = ( +
+
+ +
+
+ ); + } else { + quote = ( +
+ + + {quote_media} +
+ ); + } + } else if (quote_muted) { + quote = ( +
+
+ +
+
+ ); + } + return (
@@ -483,6 +676,7 @@ class Status extends ImmutablePureComponent { + {quote} {media} diff --git a/app/javascript/mastodon/components/status_action_bar.js b/app/javascript/mastodon/components/status_action_bar.js index 7dbbb99cc..4c4b233b1 100644 --- a/app/javascript/mastodon/components/status_action_bar.js +++ b/app/javascript/mastodon/components/status_action_bar.js @@ -23,7 +23,9 @@ const messages = defineMessages({ reblog: { id: 'status.reblog', defaultMessage: 'Boost' }, reblog_private: { id: 'status.reblog_private', defaultMessage: 'Boost with original visibility' }, cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' }, + cannot_quote: { id: 'status.cannot_quote', defaultMessage: 'This post cannot be quoted' }, cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' }, + quote: { id: 'status.quote', defaultMessage: 'Quote' }, favourite: { id: 'status.favourite', defaultMessage: 'Favourite' }, bookmark: { id: 'status.bookmark', defaultMessage: 'Bookmark' }, removeBookmark: { id: 'status.remove_bookmark', defaultMessage: 'Remove bookmark' }, @@ -61,6 +63,7 @@ class StatusActionBar extends ImmutablePureComponent { onReply: PropTypes.func, onFavourite: PropTypes.func, onReblog: PropTypes.func, + onQuote: PropTypes.func, onDelete: PropTypes.func, onDirect: PropTypes.func, onMention: PropTypes.func, @@ -137,6 +140,10 @@ class StatusActionBar extends ImmutablePureComponent { this.props.onBookmark(this.props.status); } + handleQuoteClick = () => { + this.props.onQuote(this.props.status, this.context.router.history); + } + handleDeleteClick = () => { this.props.onDelete(this.props.status, this.context.router.history); } @@ -334,7 +341,7 @@ class StatusActionBar extends ImmutablePureComponent { - + {shareButton} diff --git a/app/javascript/mastodon/components/status_content.js b/app/javascript/mastodon/components/status_content.js index bf21a9fd6..71fbbd7c5 100644 --- a/app/javascript/mastodon/components/status_content.js +++ b/app/javascript/mastodon/components/status_content.js @@ -24,6 +24,7 @@ export default class StatusContent extends React.PureComponent { onClick: PropTypes.func, collapsable: PropTypes.bool, onCollapsedToggle: PropTypes.func, + quote: PropTypes.bool, }; state = { @@ -125,6 +126,15 @@ export default class StatusContent extends React.PureComponent { } } + onQuoteClick = (statusId, e) => { + let statusUrl = `/statuses/${statusId}`; + + if (this.context.router && e.button === 0) { + e.preventDefault(); + this.context.router.history.push(statusUrl); + } + } + handleMouseDown = (e) => { this.startXY = [e.clientX, e.clientY]; } @@ -168,11 +178,12 @@ export default class StatusContent extends React.PureComponent { } render () { - const { status } = this.props; + const { status, quote } = this.props; 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']); + const renderShowPoll = !!status.get('poll'); const content = { __html: status.get('contentHtml') }; const spoilerContent = { __html: status.get('spoilerHtml') }; @@ -194,6 +205,16 @@ export default class StatusContent extends React.PureComponent { ); + const showPollButton = ( + + ); + + const pollContainer = ( + + ); + if (status.get('spoiler_text').length > 0) { let mentionsPlaceholder = ''; @@ -221,7 +242,7 @@ export default class StatusContent extends React.PureComponent {
- {!hidden && !!status.get('poll') && } + {!hidden && renderShowPoll && quote ? showPollButton : pollContainer} {renderViewThread && showThreadButton}
@@ -231,7 +252,7 @@ export default class StatusContent extends React.PureComponent {
- {!!status.get('poll') && } + {renderShowPoll && quote ? showPollButton : pollContainer} {renderViewThread && showThreadButton}
, @@ -247,7 +268,7 @@ export default class StatusContent extends React.PureComponent {
- {!!status.get('poll') && } + {renderShowPoll && quote ? showPollButton : pollContainer} {renderViewThread && showThreadButton}
diff --git a/app/javascript/mastodon/containers/status_container.js b/app/javascript/mastodon/containers/status_container.js index 9abdec138..ce055cd96 100644 --- a/app/javascript/mastodon/containers/status_container.js +++ b/app/javascript/mastodon/containers/status_container.js @@ -4,6 +4,7 @@ import Status from '../components/status'; import { makeGetStatus, makeGetPictureInPicture } from '../selectors'; import { replyCompose, + quoteCompose, mentionCompose, directCompose, } from '../actions/compose'; @@ -24,6 +25,8 @@ import { hideStatus, revealStatus, toggleStatusCollapse, + hideQuote, + revealQuote, } from '../actions/statuses'; import { unmuteAccount, @@ -50,6 +53,8 @@ const messages = defineMessages({ redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.' }, replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' }, replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' }, + quoteConfirm: { id: 'confirmations.quote.confirm', defaultMessage: 'Quote' }, + quoteMessage: { id: 'confirmations.quote.message', defaultMessage: 'Quoting now will overwrite the message you are currently composing. Are you sure you want to proceed?' }, blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' }, }); @@ -99,6 +104,22 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ } }, + onQuote (status, router) { + dispatch((_, getState) => { + let state = getState(); + + if (state.getIn(['compose', 'text']).trim().length !== 0) { + dispatch(openModal('CONFIRM', { + message: intl.formatMessage(messages.quoteMessage), + confirm: intl.formatMessage(messages.quoteConfirm), + onConfirm: () => dispatch(quoteCompose(status, router)), + })); + } else { + dispatch(quoteCompose(status, router)); + } + }); + }, + onFavourite (status) { if (status.get('favourited')) { dispatch(unfavourite(status)); @@ -215,6 +236,14 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ dispatch(deployPictureInPicture(status.get('id'), status.getIn(['account', 'id']), type, mediaProps)); }, + onQuoteToggleHidden (status) { + if (status.get('quote_hidden')) { + dispatch(revealQuote(status.get('id'))); + } else { + dispatch(hideQuote(status.get('id'))); + } + }, + }); export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Status)); diff --git a/app/javascript/mastodon/containers/timeline_container.js b/app/javascript/mastodon/containers/timeline_container.js index ed8095f90..768169458 100644 --- a/app/javascript/mastodon/containers/timeline_container.js +++ b/app/javascript/mastodon/containers/timeline_container.js @@ -47,7 +47,9 @@ export default class TimelineContainer extends React.PureComponent { - {timeline} +
+ {timeline} +
{ReactDOM.createPortal( , diff --git a/app/javascript/mastodon/features/compose/components/compose_form.js b/app/javascript/mastodon/features/compose/components/compose_form.js index 4456935e3..81eb74c32 100644 --- a/app/javascript/mastodon/features/compose/components/compose_form.js +++ b/app/javascript/mastodon/features/compose/components/compose_form.js @@ -4,6 +4,7 @@ import Button from '../../../components/button'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; import ReplyIndicatorContainer from '../containers/reply_indicator_container'; +import QuoteIndicatorContainer from '../containers/quote_indicator_container'; import AutosuggestTextarea from '../../../components/autosuggest_textarea'; import AutosuggestInput from '../../../components/autosuggest_input'; import PollButtonContainer from '../containers/poll_button_container'; @@ -220,6 +221,7 @@ class ComposeForm extends ImmutablePureComponent { +
{ + this.props.onCancel(); + } + + handleAccountClick = (e) => { + if (e.button === 0 && !(e.ctrlKey || e.metaKey)) { + e.preventDefault(); + this.context.router.history.push(`/accounts/${this.props.status.getIn(['account', 'id'])}`); + } + } + + render () { + const { status, intl } = this.props; + + if (!status) { + return null; + } + + const content = { __html: status.get('contentHtml') }; + + return ( +
+
+
+ + +
+ +
+
+ +
+ + {status.get('media_attachments').size > 0 && ( + + )} +
+ ); + } + +} diff --git a/app/javascript/mastodon/features/compose/containers/quote_indicator_container.js b/app/javascript/mastodon/features/compose/containers/quote_indicator_container.js new file mode 100644 index 000000000..8a3ad4959 --- /dev/null +++ b/app/javascript/mastodon/features/compose/containers/quote_indicator_container.js @@ -0,0 +1,24 @@ +import { connect } from 'react-redux'; +import { cancelQuoteCompose } from '../../../actions/compose'; +import { makeGetStatus } from '../../../selectors'; +import QuoteIndicator from '../components/quote_indicator'; + +const makeMapStateToProps = () => { + const getStatus = makeGetStatus(); + + const mapStateToProps = state => ({ + status: getStatus(state, { id: state.getIn(['compose', 'quote_from']) }), + }); + + return mapStateToProps; +}; + +const mapDispatchToProps = dispatch => ({ + + onCancel () { + dispatch(cancelQuoteCompose()); + }, + +}); + +export default connect(makeMapStateToProps, mapDispatchToProps)(QuoteIndicator); diff --git a/app/javascript/mastodon/features/follow_recommendations/index.js b/app/javascript/mastodon/features/follow_recommendations/index.js index a35ff3e82..26c8b2471 100644 --- a/app/javascript/mastodon/features/follow_recommendations/index.js +++ b/app/javascript/mastodon/features/follow_recommendations/index.js @@ -76,7 +76,7 @@ class FollowRecommendations extends ImmutablePureComponent { return ( -
+

diff --git a/app/javascript/mastodon/features/notifications/containers/notification_container.js b/app/javascript/mastodon/features/notifications/containers/notification_container.js index 555d5e1b5..5c984197f 100644 --- a/app/javascript/mastodon/features/notifications/containers/notification_container.js +++ b/app/javascript/mastodon/features/notifications/containers/notification_container.js @@ -2,7 +2,6 @@ import { connect } from 'react-redux'; import { makeGetNotification, makeGetStatus } from '../../../selectors'; import Notification from '../components/notification'; import { initBoostModal } from '../../../actions/boosts'; -import { openModal } from '../../../actions/modal'; import { mentionCompose } from '../../../actions/compose'; import { reblog, 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 1ecb18bf8..c89b86ffe 100644 --- a/app/javascript/mastodon/features/picture_in_picture/components/footer.js +++ b/app/javascript/mastodon/features/picture_in_picture/components/footer.js @@ -7,7 +7,7 @@ import IconButton from 'mastodon/components/icon_button'; import classNames from 'classnames'; import { me, boostModal } from 'mastodon/initial_state'; import { defineMessages, injectIntl } from 'react-intl'; -import { replyCompose } from 'mastodon/actions/compose'; +import { replyCompose, quoteCompose } from 'mastodon/actions/compose'; import { reblog, favourite, unreblog, unfavourite } from 'mastodon/actions/interactions'; import { makeGetStatus } from 'mastodon/selectors'; import { initBoostModal } from 'mastodon/actions/boosts'; @@ -20,9 +20,13 @@ const messages = defineMessages({ reblog_private: { id: 'status.reblog_private', defaultMessage: 'Boost with original visibility' }, cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' }, cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' }, + cannot_quote: { id: 'status.cannot_quote', defaultMessage: 'This post cannot be quoted' }, + quote: { id: 'status.quote', defaultMessage: 'Quote' }, favourite: { id: 'status.favourite', defaultMessage: 'Favourite' }, replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' }, replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' }, + quoteConfirm: { id: 'confirmations.quote.confirm', defaultMessage: 'Quote' }, + quoteMessage: { id: 'confirmations.quote.message', defaultMessage: 'Quoting now will overwrite the message you are currently composing. Are you sure you want to proceed?' }, open: { id: 'status.open', defaultMessage: 'Expand this status' }, }); @@ -119,6 +123,31 @@ class Footer extends ImmutablePureComponent { router.history.push(`/statuses/${status.get('id')}`); } + _performQuote = () => { + const { dispatch, status, onClose } = this.props; + const { router } = this.context; + + if (onClose) { + onClose(); + } + + dispatch(quoteCompose(status, router.history)); + }; + + handleQuoteClick = () => { + const { dispatch, askReplyConfirmation, intl } = this.props; + + if (askReplyConfirmation) { + dispatch(openModal('CONFIRM', { + message: intl.formatMessage(messages.quoteMessage), + confirm: intl.formatMessage(messages.quoteConfirm), + onConfirm: this._performQuote, + })); + } else { + this._performQuote(); + } + } + render () { const { status, intl, withOpenButton } = this.props; @@ -152,6 +181,7 @@ class Footer extends ImmutablePureComponent { + {withOpenButton && }
); diff --git a/app/javascript/mastodon/features/status/components/action_bar.js b/app/javascript/mastodon/features/status/components/action_bar.js index ffa2510c0..61833f37b 100644 --- a/app/javascript/mastodon/features/status/components/action_bar.js +++ b/app/javascript/mastodon/features/status/components/action_bar.js @@ -17,7 +17,9 @@ const messages = defineMessages({ reblog: { id: 'status.reblog', defaultMessage: 'Boost' }, reblog_private: { id: 'status.reblog_private', defaultMessage: 'Boost with original visibility' }, cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' }, + cannot_quote: { id: 'status.cannot_quote', defaultMessage: 'This post cannot be quoted' }, cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' }, + quote: { id: 'status.quote', defaultMessage: 'Quote' }, favourite: { id: 'status.favourite', defaultMessage: 'Favourite' }, bookmark: { id: 'status.bookmark', defaultMessage: 'Bookmark' }, more: { id: 'status.more', defaultMessage: 'More' }, @@ -56,6 +58,7 @@ class ActionBar extends React.PureComponent { relationship: ImmutablePropTypes.map, onReply: PropTypes.func.isRequired, onReblog: PropTypes.func.isRequired, + onQuote: PropTypes.func.isRequired, onFavourite: PropTypes.func.isRequired, onBookmark: PropTypes.func.isRequired, onDelete: PropTypes.func.isRequired, @@ -82,6 +85,10 @@ class ActionBar extends React.PureComponent { this.props.onReblog(this.props.status, e); } + handleQuoteClick = () => { + this.props.onQuote(this.props.status, this.context.router.history); + } + handleFavouriteClick = () => { this.props.onFavourite(this.props.status); } @@ -277,6 +284,7 @@ class ActionBar extends React.PureComponent {
+
{shareButton}
diff --git a/app/javascript/mastodon/features/status/components/card.js b/app/javascript/mastodon/features/status/components/card.js index 90f9ae7ae..f4d95b4cb 100644 --- a/app/javascript/mastodon/features/status/components/card.js +++ b/app/javascript/mastodon/features/status/components/card.js @@ -60,6 +60,10 @@ const addAutoPlay = html => { export default class Card extends React.PureComponent { + static contextTypes = { + router: PropTypes.object, + }; + static propTypes = { card: ImmutablePropTypes.map, maxDescription: PropTypes.number, @@ -68,6 +72,7 @@ export default class Card extends React.PureComponent { defaultWidth: PropTypes.number, cacheWidth: PropTypes.func, sensitive: PropTypes.bool, + quote: PropTypes.bool, }; static defaultProps = { @@ -184,7 +189,7 @@ export default class Card extends React.PureComponent { } render () { - const { card, maxDescription, compact } = this.props; + const { card, maxDescription, compact, quote } = this.props; const { width, embedded, revealed } = this.state; if (card === null) { @@ -197,7 +202,11 @@ export default class Card extends React.PureComponent { const className = classnames('status-card', { horizontal, compact, interactive }); const title = interactive ? {card.get('title')} : {card.get('title')}; const ratio = card.get('width') / card.get('height'); - const height = (compact && !embedded) ? (width / (16 / 9)) : (width / ratio); + let height = (compact && !embedded) ? (width / (16 / 9)) : (width / ratio); + + if (quote && height) { + height /= 2; + } const description = (
diff --git a/app/javascript/mastodon/features/status/components/detailed_status.js b/app/javascript/mastodon/features/status/components/detailed_status.js index 043a749ed..9d16a9aa9 100644 --- a/app/javascript/mastodon/features/status/components/detailed_status.js +++ b/app/javascript/mastodon/features/status/components/detailed_status.js @@ -1,4 +1,5 @@ import React from 'react'; +import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import Avatar from '../../../components/avatar'; @@ -6,7 +7,7 @@ import DisplayName from '../../../components/display_name'; import StatusContent from '../../../components/status_content'; import MediaGallery from '../../../components/media_gallery'; import { Link } from 'react-router-dom'; -import { injectIntl, defineMessages, FormattedDate } from 'react-intl'; +import { injectIntl, defineMessages, FormattedDate, FormattedMessage } from 'react-intl'; import Card from './card'; import ImmutablePureComponent from 'react-immutable-pure-component'; import Video from '../../video'; @@ -24,7 +25,31 @@ const messages = defineMessages({ direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' }, }); -export default @injectIntl +const mapStateToProps = (state, props) => { + let status = props.status; + + if (status === null) { + return null; + } + + if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') { + status = status.get('reblog'); + } + + if (status.get('quote', null) === null) { + return { + quote_muted: status.get('quote_id', null) ? true : false, + }; + } + const id = status.getIn(['quote', 'account', 'id'], null); + + return { + quote_muted: id !== null && (state.getIn(['relationships', id, 'muting']) || state.getIn(['relationships', id, 'blocking']) || state.getIn(['relationships', id, 'blocked_by']) || state.getIn(['relationships', id, 'domain_blocking'])) || status.getIn(['quote', 'quote_muted']), + }; +}; + +export default @connect(mapStateToProps) +@injectIntl class DetailedStatus extends ImmutablePureComponent { static contextTypes = { @@ -33,8 +58,11 @@ class DetailedStatus extends ImmutablePureComponent { static propTypes = { status: ImmutablePropTypes.map, + quote_muted: PropTypes.bool, onOpenMedia: PropTypes.func.isRequired, onOpenVideo: PropTypes.func.isRequired, + onOpenMediaQuote: PropTypes.func.isRequired, + onOpenVideoQuote: PropTypes.func.isRequired, onToggleHidden: PropTypes.func.isRequired, measureHeight: PropTypes.bool, onHeightChange: PropTypes.func, @@ -46,6 +74,9 @@ class DetailedStatus extends ImmutablePureComponent { available: PropTypes.bool, }), onToggleMediaVisibility: PropTypes.func, + onQuoteToggleHidden: PropTypes.func.isRequired, + showQuoteMedia: PropTypes.bool, + onToggleQuoteMediaVisibility: PropTypes.func, }; state = { @@ -54,8 +85,9 @@ class DetailedStatus extends ImmutablePureComponent { handleAccountClick = (e) => { if (e.button === 0 && !(e.ctrlKey || e.metaKey) && this.context.router) { + const id = e.currentTarget.getAttribute('data-id'); e.preventDefault(); - this.context.router.history.push(`/accounts/${this.props.status.getIn(['account', 'id'])}`); + this.context.router.history.push(`/accounts/${id}`); } e.stopPropagation(); @@ -65,6 +97,10 @@ class DetailedStatus extends ImmutablePureComponent { this.props.onOpenVideo(this.props.status.getIn(['media_attachments', 0]), options); } + handleOpenVideoQuote = (options) => { + this.props.onOpenVideoQuote(this.props.status.getIn(['quote', 'media_attachments', 0]), options); + } + handleExpandedToggle = () => { this.props.onToggleHidden(this.props.status); } @@ -102,8 +138,22 @@ class DetailedStatus extends ImmutablePureComponent { window.open(href, 'mastodon-intent', 'width=445,height=600,resizable=no,menubar=no,status=no,scrollbars=yes'); } + handleExpandedQuoteToggle = () => { + this.props.onQuoteToggleHidden(this.props.status); + } + + handleQuoteClick = () => { + if (!this.context.router) { + return; + } + + const { status } = this.props; + this.context.router.history.push(`/statuses/${status.getIn(['quote', 'id'])}`); + } + render () { const status = (this.props.status && this.props.status.get('reblog')) ? this.props.status.get('reblog') : this.props.status; + const quote_muted = this.props.quote_muted const outerStyle = { boxSizing: 'border-box' }; const { intl, compact, pictureInPicture } = this.props; @@ -121,6 +171,95 @@ class DetailedStatus extends ImmutablePureComponent { outerStyle.height = `${this.state.height}px`; } + let quote = null; + if (status.get('quote', null) !== null) { + let quote_status = status.get('quote'); + + let quote_media = null; + if (quote_status.get('media_attachments').size > 0) { + + if (quote_status.getIn(['media_attachments', 0, 'type']) === 'audio') { + const attachment = quote_status.getIn(['media_attachments', 0]); + + quote_media = ( +