@@ -41,6 +41,11 @@ module.exports = { | |||
'node_modules', | |||
'\\.(css|scss|json)$', | |||
], | |||
'import/resolver': { | |||
node: { | |||
paths: ['app/javascript'], | |||
}, | |||
}, | |||
}, | |||
rules: { | |||
@@ -80,7 +80,7 @@ Rails/HttpStatus: | |||
Rails/Exit: | |||
Exclude: | |||
- 'lib/mastodon/*' | |||
- 'lib/cli' | |||
- 'lib/cli.rb' | |||
Style/ClassAndModuleChildren: | |||
Enabled: false | |||
@@ -1 +1 @@ | |||
2.6.0 | |||
2.6.1 |
@@ -6,35 +6,35 @@ and provided thanks to the work of the following contributors: | |||
* [Gargron](https://github.com/Gargron) | |||
* [ykzts](https://github.com/ykzts) | |||
* [akihikodaki](https://github.com/akihikodaki) | |||
* [ThibG](https://github.com/ThibG) | |||
* [akihikodaki](https://github.com/akihikodaki) | |||
* [mjankowski](https://github.com/mjankowski) | |||
* [dependabot[bot]](https://github.com/apps/dependabot) | |||
* [unarist](https://github.com/unarist) | |||
* [m4sk1n](https://github.com/m4sk1n) | |||
* [dependabot[bot]](https://github.com/apps/dependabot) | |||
* [yiskah](https://github.com/yiskah) | |||
* [nolanlawson](https://github.com/nolanlawson) | |||
* [sorin-davidoi](https://github.com/sorin-davidoi) | |||
* [ysksn](https://github.com/ysksn) | |||
* [sorin-davidoi](https://github.com/sorin-davidoi) | |||
* [abcang](https://github.com/abcang) | |||
* [lynlynlynx](https://github.com/lynlynlynx) | |||
* [alpaca-tc](https://github.com/alpaca-tc) | |||
* [mayaeh](https://github.com/mayaeh) | |||
* [renatolond](https://github.com/renatolond) | |||
* [alpaca-tc](https://github.com/alpaca-tc) | |||
* [nclm](https://github.com/nclm) | |||
* [ineffyble](https://github.com/ineffyble) | |||
* [jeroenpraat](https://github.com/jeroenpraat) | |||
* [blackle](https://github.com/blackle) | |||
* [Quent-in](https://github.com/Quent-in) | |||
* [JantsoP](https://github.com/JantsoP) | |||
* [Kjwon15](https://github.com/Kjwon15) | |||
* [mabkenar](https://github.com/mabkenar) | |||
* [nullkal](https://github.com/nullkal) | |||
* [yookoala](https://github.com/yookoala) | |||
* [Kjwon15](https://github.com/Kjwon15) | |||
* [shuheiktgw](https://github.com/shuheiktgw) | |||
* [ashfurrow](https://github.com/ashfurrow) | |||
* [Quenty31](https://github.com/Quenty31) | |||
* [zunda](https://github.com/zunda) | |||
* [Quenty31](https://github.com/Quenty31) | |||
* [eramdam](https://github.com/eramdam) | |||
* [takayamaki](https://github.com/takayamaki) | |||
* [masarakki](https://github.com/masarakki) | |||
@@ -45,30 +45,33 @@ and provided thanks to the work of the following contributors: | |||
* [stephenburgess8](https://github.com/stephenburgess8) | |||
* [Wonderfall](https://github.com/Wonderfall) | |||
* [matteoaquila](https://github.com/matteoaquila) | |||
* [rkarabut](https://github.com/rkarabut) | |||
* [yukimochi](https://github.com/yukimochi) | |||
* [rkarabut](https://github.com/rkarabut) | |||
* [Artoria2e5](https://github.com/Artoria2e5) | |||
* [nightpool](https://github.com/nightpool) | |||
* [marrus-sh](https://github.com/marrus-sh) | |||
* [krainboltgreene](https://github.com/krainboltgreene) | |||
* [patf](https://github.com/patf) | |||
* [pfigel](https://github.com/pfigel) | |||
* [Aldarone](https://github.com/Aldarone) | |||
* [BoFFire](https://github.com/BoFFire) | |||
* [clworld](https://github.com/clworld) | |||
* [dracos](https://github.com/dracos) | |||
* [SerCom_KC](mailto:sercom-kc@users.noreply.github.com) | |||
* [Sylvhem](https://github.com/Sylvhem) | |||
* [nightpool](https://github.com/nightpool) | |||
* [MasterGroosha](https://github.com/MasterGroosha) | |||
* [JeanGauthier](https://github.com/JeanGauthier) | |||
* [kschaper](https://github.com/kschaper) | |||
* [MaciekBaron](https://github.com/MaciekBaron) | |||
* [MitarashiDango](mailto:mitarashidango@users.noreply.github.com) | |||
* [beatrix-bitrot](https://github.com/beatrix-bitrot) | |||
* [Aditoo17](https://github.com/Aditoo17) | |||
* [adbelle](https://github.com/adbelle) | |||
* [evanminto](https://github.com/evanminto) | |||
* [MightyPork](https://github.com/MightyPork) | |||
* [yhirano55](https://github.com/yhirano55) | |||
* [rinsuki](https://github.com/rinsuki) | |||
* [camponez](https://github.com/camponez) | |||
* [hinaloe](https://github.com/hinaloe) | |||
* [SerCom-KC](https://github.com/SerCom-KC) | |||
* [aschmitz](https://github.com/aschmitz) | |||
* [devkral](https://github.com/devkral) | |||
@@ -77,6 +80,7 @@ and provided thanks to the work of the following contributors: | |||
* [johnsudaar](https://github.com/johnsudaar) | |||
* [trebmuh](https://github.com/trebmuh) | |||
* [Rakib Hasan](mailto:rmhasan@gmail.com) | |||
* [ashleyhull-versent](https://github.com/ashleyhull-versent) | |||
* [lindwurm](https://github.com/lindwurm) | |||
* [victorhck](mailto:victorhck@geeko.site) | |||
* [voidsatisfaction](https://github.com/voidsatisfaction) | |||
@@ -92,20 +96,21 @@ and provided thanks to the work of the following contributors: | |||
* [dunn](https://github.com/dunn) | |||
* [xqus](https://github.com/xqus) | |||
* [hugogameiro](https://github.com/hugogameiro) | |||
* [ariasuni](https://github.com/ariasuni) | |||
* [pfm-eyesightjp](https://github.com/pfm-eyesightjp) | |||
* [fakenine](https://github.com/fakenine) | |||
* [tsuwatch](https://github.com/tsuwatch) | |||
* [victorhck](https://github.com/victorhck) | |||
* [ashleyhull-versent](https://github.com/ashleyhull-versent) | |||
* [kedamaDQ](https://github.com/kedamaDQ) | |||
* [puckipedia](https://github.com/puckipedia) | |||
* [trwnh](https://github.com/trwnh) | |||
* [fvh-P](https://github.com/fvh-P) | |||
* [contraexemplo](https://github.com/contraexemplo) | |||
* [Anna e só](mailto:contraexemplos@gmail.com) | |||
* [BenLubar](https://github.com/BenLubar) | |||
* [kazu9su](https://github.com/kazu9su) | |||
* [Komic](https://github.com/Komic) | |||
* [lmorchard](https://github.com/lmorchard) | |||
* [diomed](https://github.com/diomed) | |||
* [ariasuni](https://github.com/ariasuni) | |||
* [Neetshin](mailto:neetshin@neetsh.in) | |||
* [rainyday](https://github.com/rainyday) | |||
* [ProgVal](https://github.com/ProgVal) | |||
@@ -114,7 +119,7 @@ and provided thanks to the work of the following contributors: | |||
* [goofy-bz](mailto:goofy@babelzilla.org) | |||
* [kadiix](https://github.com/kadiix) | |||
* [kodacs](https://github.com/kodacs) | |||
* [rtucker](https://github.com/rtucker) | |||
* [JMendyk](https://github.com/JMendyk) | |||
* [KScl](https://github.com/KScl) | |||
* [sterdev](https://github.com/sterdev) | |||
* [TheKinrar](https://github.com/TheKinrar) | |||
@@ -125,16 +130,17 @@ and provided thanks to the work of the following contributors: | |||
* [fhemberger](https://github.com/fhemberger) | |||
* [greysteil](https://github.com/greysteil) | |||
* [hensmith](https://github.com/hensmith) | |||
* [hinaloe](https://github.com/hinaloe) | |||
* [d6rkaiz](https://github.com/d6rkaiz) | |||
* [Reverite](https://github.com/Reverite) | |||
* [JMendyk](https://github.com/JMendyk) | |||
* [JohnD28](https://github.com/JohnD28) | |||
* [znz](https://github.com/znz) | |||
* [marek-lach](https://github.com/marek-lach) | |||
* [Naouak](https://github.com/Naouak) | |||
* [pawelngei](https://github.com/pawelngei) | |||
* [rtucker](https://github.com/rtucker) | |||
* [reneklacan](https://github.com/reneklacan) | |||
* [ekiru](https://github.com/ekiru) | |||
* [noellabo](https://github.com/noellabo) | |||
* [tcitworld](https://github.com/tcitworld) | |||
* [geta6](https://github.com/geta6) | |||
* [happycoloredbanana](https://github.com/happycoloredbanana) | |||
@@ -144,9 +150,8 @@ and provided thanks to the work of the following contributors: | |||
* [noraworld](https://github.com/noraworld) | |||
* [theboss](https://github.com/theboss) | |||
* [178inaba](https://github.com/178inaba) | |||
* [Aditoo17](https://github.com/Aditoo17) | |||
* [alyssais](https://github.com/alyssais) | |||
* [kodnaplakal](https://github.com/kodnaplakal) | |||
* [hiphref](https://github.com/hiphref) | |||
* [stalker314314](https://github.com/stalker314314) | |||
* [huertanix](https://github.com/huertanix) | |||
* [genesixx](https://github.com/genesixx) | |||
@@ -162,11 +167,11 @@ and provided thanks to the work of the following contributors: | |||
* [pierreozoux](https://github.com/pierreozoux) | |||
* [qguv](https://github.com/qguv) | |||
* [Ram Lmn](mailto:ramlmn@users.noreply.github.com) | |||
* [sascha-sl](https://github.com/sascha-sl) | |||
* [harukasan](https://github.com/harukasan) | |||
* [stamak](https://github.com/stamak) | |||
* [noellabo](https://github.com/noellabo) | |||
* [Technowix](mailto:technowix@users.noreply.github.com) | |||
* [Eychics](https://github.com/Eychics) | |||
* [Zoeille](https://github.com/Zoeille) | |||
* [Thor Harald Johansen](mailto:thj@thj.no) | |||
* [0x70b1a5](https://github.com/0x70b1a5) | |||
* [gled-rs](https://github.com/gled-rs) | |||
@@ -179,21 +184,20 @@ and provided thanks to the work of the following contributors: | |||
* [hoodie](mailto:hoodiekitten@outlook.com) | |||
* [luzi82](https://github.com/luzi82) | |||
* [duxovni](https://github.com/duxovni) | |||
* [trwnh](https://github.com/trwnh) | |||
* [tmm576](https://github.com/tmm576) | |||
* [unsmell](https://github.com/unsmell) | |||
* [valerauko](https://github.com/valerauko) | |||
* [chriswmartin](https://github.com/chriswmartin) | |||
* [vahnj](https://github.com/vahnj) | |||
* [ikuradon](https://github.com/ikuradon) | |||
* [AndreLewin](https://github.com/AndreLewin) | |||
* [rinsuki](https://github.com/rinsuki) | |||
* [0xflotus](https://github.com/0xflotus) | |||
* [redtachyons](https://github.com/redtachyons) | |||
* [thurloat](https://github.com/thurloat) | |||
* [aaribaud](https://github.com/aaribaud) | |||
* [pointlessone](https://github.com/pointlessone) | |||
* [Andrew](mailto:andrewlchronister@gmail.com) | |||
* [estuans](https://github.com/estuans) | |||
* [BenLubar](https://github.com/BenLubar) | |||
* [dissolve](https://github.com/dissolve) | |||
* [PurpleBooth](https://github.com/PurpleBooth) | |||
* [bradurani](https://github.com/bradurani) | |||
@@ -216,6 +220,7 @@ and provided thanks to the work of the following contributors: | |||
* [ErikXXon](https://github.com/ErikXXon) | |||
* [ian-kelling](https://github.com/ian-kelling) | |||
* [immae](https://github.com/immae) | |||
* [J0WI](https://github.com/J0WI) | |||
* [foozmeat](https://github.com/foozmeat) | |||
* [jasonrhodes](https://github.com/jasonrhodes) | |||
* [Jason Snell](mailto:jason@newrelic.com) | |||
@@ -230,6 +235,7 @@ and provided thanks to the work of the following contributors: | |||
* [Lorenz Diener](mailto:halcyon@icosahedron.website) | |||
* [alimony](https://github.com/alimony) | |||
* [mig5](https://github.com/mig5) | |||
* [moritzheiber](https://github.com/moritzheiber) | |||
* [ndarville](https://github.com/ndarville) | |||
* [Abzol](https://github.com/Abzol) | |||
* [pwoolcoc](https://github.com/pwoolcoc) | |||
@@ -238,9 +244,10 @@ and provided thanks to the work of the following contributors: | |||
* [ignisf](https://github.com/ignisf) | |||
* [raymestalez](https://github.com/raymestalez) | |||
* [remram44](https://github.com/remram44) | |||
* [sascha-sl](https://github.com/sascha-sl) | |||
* [sts10](https://github.com/sts10) | |||
* [u1-liquid](https://github.com/u1-liquid) | |||
* [sim6](https://github.com/sim6) | |||
* [Sir-Boops](https://github.com/Sir-Boops) | |||
* [stemid](https://github.com/stemid) | |||
* [sumdog](https://github.com/sumdog) | |||
* [ThomasLeister](https://github.com/ThomasLeister) | |||
@@ -288,6 +295,7 @@ and provided thanks to the work of the following contributors: | |||
* [857b](https://github.com/857b) | |||
* [insom](https://github.com/insom) | |||
* [tachyons](https://github.com/tachyons) | |||
* [acid-chicken](https://github.com/acid-chicken) | |||
* [Esteth](https://github.com/Esteth) | |||
* [unascribed](https://github.com/unascribed) | |||
* [Aguay-val](https://github.com/Aguay-val) | |||
@@ -297,7 +305,6 @@ and provided thanks to the work of the following contributors: | |||
* [unleashed](https://github.com/unleashed) | |||
* [alxrcs](https://github.com/alxrcs) | |||
* [console-cowboy](https://github.com/console-cowboy) | |||
* [pointlessone](https://github.com/pointlessone) | |||
* [Alkarex](https://github.com/Alkarex) | |||
* [a2](https://github.com/a2) | |||
* [0xa](https://github.com/0xa) | |||
@@ -310,8 +317,11 @@ and provided thanks to the work of the following contributors: | |||
* [Andreas Drop](mailto:andy@remline.de) | |||
* [andi1984](https://github.com/andi1984) | |||
* [schas002](https://github.com/schas002) | |||
* [contraexemplo](https://github.com/contraexemplo) | |||
* [abackstrom](https://github.com/abackstrom) | |||
* [armandfardeau](https://github.com/armandfardeau) | |||
* [jumbosushi](https://github.com/jumbosushi) | |||
* [aurelien-reeves](https://github.com/aurelien-reeves) | |||
* [ayumin](https://github.com/ayumin) | |||
* [BaptisteGelez](https://github.com/BaptisteGelez) | |||
* [bzg](https://github.com/bzg) | |||
@@ -329,6 +339,7 @@ and provided thanks to the work of the following contributors: | |||
* [Motoma](https://github.com/Motoma) | |||
* [chriswk](https://github.com/chriswk) | |||
* [csu](https://github.com/csu) | |||
* [clarfon](https://github.com/clarfon) | |||
* [kklleemm](https://github.com/kklleemm) | |||
* [colindean](https://github.com/colindean) | |||
* [dachinat](https://github.com/dachinat) | |||
@@ -351,11 +362,13 @@ and provided thanks to the work of the following contributors: | |||
* [eai04191](https://github.com/eai04191) | |||
* [d3vgru](https://github.com/d3vgru) | |||
* [Elizafox](https://github.com/Elizafox) | |||
* [enewhuis](https://github.com/enewhuis) | |||
* [ericblade](https://github.com/ericblade) | |||
* [mikoim](https://github.com/mikoim) | |||
* [espenronnevik](https://github.com/espenronnevik) | |||
* [Finariel](https://github.com/Finariel) | |||
* [siuying](https://github.com/siuying) | |||
* [zoc](https://github.com/zoc) | |||
* [fwenzel](https://github.com/fwenzel) | |||
* [GenbuHase](https://github.com/GenbuHase) | |||
* [hattori6789](https://github.com/hattori6789) | |||
@@ -416,6 +429,7 @@ and provided thanks to the work of the following contributors: | |||
* [martymcguire](https://github.com/martymcguire) | |||
* [marvinkopf](https://github.com/marvinkopf) | |||
* [otsune](https://github.com/otsune) | |||
* [mbugowski](https://github.com/mbugowski) | |||
* [Mathias B](mailto:10813340+mathias-b@users.noreply.github.com) | |||
* [matt-auckland](https://github.com/matt-auckland) | |||
* [webroo](https://github.com/webroo) | |||
@@ -434,10 +448,10 @@ and provided thanks to the work of the following contributors: | |||
* [premist](https://github.com/premist) | |||
* [Mnkai](https://github.com/Mnkai) | |||
* [mitchhentges](https://github.com/mitchhentges) | |||
* [moritzheiber](https://github.com/moritzheiber) | |||
* [mouse-reeve](https://github.com/mouse-reeve) | |||
* [Mozinet-fr](https://github.com/Mozinet-fr) | |||
* [lae](https://github.com/lae) | |||
* [nosada](https://github.com/nosada) | |||
* [Nanamachi](https://github.com/Nanamachi) | |||
* [orinthe](https://github.com/orinthe) | |||
* [NecroTechno](https://github.com/NecroTechno) | |||
@@ -454,21 +468,22 @@ and provided thanks to the work of the following contributors: | |||
* [noppa](https://github.com/noppa) | |||
* [Otakan951](https://github.com/Otakan951) | |||
* [fahy](https://github.com/fahy) | |||
* [PatrickRWells](https://github.com/PatrickRWells) | |||
* [Pangoraw](https://github.com/Pangoraw) | |||
* [peterkeen](https://github.com/peterkeen) | |||
* [pgate](https://github.com/pgate) | |||
* [retokromer](https://github.com/retokromer) | |||
* [rfwatson](https://github.com/rfwatson) | |||
* [rfreebern](https://github.com/rfreebern) | |||
* [PatrickRWells](mailto:32802366+patrickrwells@users.noreply.github.com) | |||
* [Paul](mailto:naydex.mc+github@gmail.com) | |||
* [Pete Keen](mailto:pete@petekeen.net) | |||
* [Pierre-Morgan Gate](mailto:pgate@users.noreply.github.com) | |||
* [Ratmir Karabut](mailto:rkarabut@sfmodern.ru) | |||
* [Reto Kromer](mailto:retokromer@users.noreply.github.com) | |||
* [Rey Tucker](mailto:git@reytucker.us) | |||
* [Rob Watson](mailto:rfwatson@users.noreply.github.com) | |||
* [Ryan Freebern](mailto:ryan@freebern.org) | |||
* [Ryan Wade](mailto:ryan.wade@protonmail.com) | |||
* [sylph01](https://github.com/sylph01) | |||
* [S-H-GAMELINKS](https://github.com/S-H-GAMELINKS) | |||
* [staticsafe](https://github.com/staticsafe) | |||
* [snwh](https://github.com/snwh) | |||
* [sts10](https://github.com/sts10) | |||
* [skoji](https://github.com/skoji) | |||
* [ScienJus](https://github.com/ScienJus) | |||
* [Ryo Kajiwara](mailto:kfe-fecn6.prussian@s01.info) | |||
* [S.H](mailto:gamelinks007@gmail.com) | |||
* [Sadiq Saif](mailto:staticsafe@users.noreply.github.com) | |||
* [Sam Hewitt](mailto:hewittsamuel@gmail.com) | |||
* [Satoshi KOJIMA](mailto:skoji@mac.com) | |||
* [ScienJus](mailto:i@scienjus.com) | |||
* [Scott Larkin](mailto:scott@codeclimate.com) | |||
* [Sebastian Hübner](mailto:imolein@users.noreply.github.com) | |||
* [Sebastian Morr](mailto:sebastian@morr.cc) | |||
@@ -480,9 +495,9 @@ and provided thanks to the work of the following contributors: | |||
* [Sho Kusano](mailto:rosylilly@aduca.org) | |||
* [Shouko Yu](mailto:imshouko@gmail.com) | |||
* [Sina Mashek](mailto:sina@mashek.xyz) | |||
* [Sir-Boops](mailto:admin@boops.me) | |||
* [Soshi Kato](mailto:mail@sossii.com) | |||
* [Spanky](mailto:2788886+spankyworks@users.noreply.github.com) | |||
* [Stanislas](mailto:angristan@pm.me) | |||
* [StefOfficiel](mailto:pichard.stephane@free.fr) | |||
* [Steven Tappert](mailto:admin@dark-it.net) | |||
* [Svetlozar Todorov](mailto:svetlik@users.noreply.github.com) | |||
@@ -532,6 +547,7 @@ and provided thanks to the work of the following contributors: | |||
* [fsubal](mailto:fsubal@users.noreply.github.com) | |||
* [fusshi-](mailto:dikky1218@users.noreply.github.com) | |||
* [gentaro](mailto:gentaroooo@gmail.com) | |||
* [gol-cha](mailto:info@mevo.xyz) | |||
* [hakoai](mailto:hk--76@qa2.so-net.ne.jp) | |||
* [haosbvnker](mailto:github@chaosbunker.com) | |||
* [isati](mailto:phil@juchnowi.cz) | |||
@@ -545,16 +561,18 @@ and provided thanks to the work of the following contributors: | |||
* [karlyeurl](mailto:karl.yeurl@gmail.com) | |||
* [kedama](mailto:32974885+kedamadq@users.noreply.github.com) | |||
* [kodai](mailto:shirafuta.kodai@gmail.com) | |||
* [koyu](mailto:me@koyu.space) | |||
* [kuro5hin](mailto:rusty@kuro5hin.org) | |||
* [luzpaz](mailto:luzpaz@users.noreply.github.com) | |||
* [maxypy](mailto:maxime@mpigou.fr) | |||
* [mhe](mailto:mail@marcus-herrmann.com) | |||
* [mike castleman](mailto:m@mlcastle.net) | |||
* [mimikun](mailto:dzdzble_effort_311@outlook.jp) | |||
* [mohemohe](mailto:mohemohe@users.noreply.github.com) | |||
* [mshrtkch](mailto:mshrtkch@users.noreply.github.com) | |||
* [muan](mailto:muan@github.com) | |||
* [namelessGonbai](mailto:43787036+namelessgonbai@users.noreply.github.com) | |||
* [neetshin](mailto:neetshin@neetsh.in) | |||
* [nightpool](mailto:nightpool@users.noreply.github.com) | |||
* [rch850](mailto:rich850@gmail.com) | |||
* [roikale](mailto:roikale@users.noreply.github.com) | |||
* [rysiekpl](mailto:rysiek@hackerspace.pl) | |||
@@ -589,243 +607,338 @@ This document is provided for informational purposes only. Since it is only upda | |||
Following people have contributed to translation of Mastodon: | |||
- **Albanian** | |||
- Besnik Bleta | |||
- Aditoo | |||
- **Arabic** | |||
- ButterflyOfFire | |||
- Aditoo | |||
- Amrz0 | |||
- **Asturian** | |||
- ButterflyOfFire | |||
- Enol P. | |||
- Aditoo | |||
- **Basque** | |||
- Osoitz | |||
- Aditoo | |||
- Aitzol | |||
- ButterflyOfFire | |||
- Gorka Azkarate | |||
- Osoitz | |||
- Peru Iparragirre | |||
- Gorka Azkarate | |||
- **Bengali** | |||
- dxwc | |||
- **Bulgarian** | |||
- ButterflyOfFire | |||
- Aditoo | |||
- **Catalan** | |||
- spla | |||
- Aditoo | |||
- ButterflyOfFire | |||
- Joan Montané | |||
- Jose Luis | |||
- spla | |||
- **Chinese (Hong Kong)** | |||
- ButterflyOfFire | |||
- Luzi Leung | |||
- Aditoo | |||
- **Chinese (Simplified)** | |||
- Allen Zhong | |||
- ButterflyOfFire | |||
- SerCom_KC | |||
- martialarts | |||
- Kaitian Xie | |||
- Aditoo | |||
- pan93412 | |||
- **Chinese (Traditional)** | |||
- Aditoo | |||
- ButterflyOfFire | |||
- James58899 | |||
- Jeff Huang | |||
- pan93412 | |||
- S1ttidoe477 | |||
- SHA265 | |||
- Jeff Huang | |||
- **Corsican** | |||
- Alix D. R. | |||
- Aditoo | |||
- ButterflyOfFire | |||
- **Croatian** | |||
- ButterflyOfFire | |||
- Aditoo | |||
- **Czech** | |||
- ButterflyOfFire | |||
- Lorem Ipsum | |||
- Aditoo | |||
- Marek Ľach | |||
- **Danish** | |||
- ButterflyOfFire | |||
- **Danish** | |||
- Einhjeriar | |||
- Rasmus Sæderup | |||
- Aditoo | |||
- ButterflyOfFire | |||
- **Dutch** | |||
- Albakham | |||
- ButterflyOfFire | |||
- Jelv | |||
- jeroenpraat | |||
- rscmbbng | |||
- Aditoo | |||
- Jelv | |||
- **English** | |||
- ButterflyOfFire | |||
- Renato "Lond" Cerqueira | |||
- **English (United Kingdom)** | |||
- Albakham | |||
- **Esperanto** | |||
- Aditoo | |||
- ButterflyOfFire | |||
- Becci Cat | |||
- Jeong Arm | |||
- Martin Bodin | |||
- Mélanie Chauvel | |||
- Vanege | |||
- Martin Bodin | |||
- tuxayo/Victor Grousset | |||
- **Finnish** | |||
- ButterflyOfFire | |||
- Jonne Arjoranta | |||
- S Heija | |||
- Mikko Poussu | |||
- Taru Luojola | |||
- S Heija | |||
- Aditoo | |||
- Jonne Arjoranta | |||
- **French** | |||
- Alda Marteau-Hardi | |||
- Albakham | |||
- Alix D. R. | |||
- Baptiste Jonglez | |||
- ButterflyOfFire | |||
- Franck Paul | |||
- Jean-Baptiste Holcroft | |||
- Jonathan Chan | |||
- Letiteuf55 | |||
- Martin Bodin | |||
- codl | |||
- Leia | |||
- Alda Marteau-Hardi | |||
- Mélanie Chauvel | |||
- Olivier Humbert | |||
- Paul Marques Mota | |||
- Sylvhem | |||
- azenet | |||
- Olivier Humbert | |||
- Aditoo | |||
- Jonathan Chan | |||
- Letiteuf55 | |||
- Baptiste Jonglez | |||
- goofy-mdn | |||
- Jean-Baptiste Holcroft | |||
- Technowix | |||
- Thibaut Girka | |||
- Martin Bodin | |||
- Théodore | |||
- azenet | |||
- codl | |||
- Thibaut Girka | |||
- Franck Paul | |||
- Sylvhem | |||
- **Galician** | |||
- ButterflyOfFire | |||
- Xose M. | |||
- Aditoo | |||
- manequim | |||
- **Georgian** | |||
- ButterflyOfFire | |||
- Aditoo | |||
- **German** | |||
- Benedikt Geißler | |||
- Aditoo | |||
- ButterflyOfFire | |||
- Daniel | |||
- Eugen Rochko | |||
- Koyu Berteon | |||
- Patrick Figel | |||
- Weblate Admin | |||
- averageunicorn | |||
- ePirat | |||
- koyu | |||
- Koyu Berteon | |||
- larsreineke | |||
- koyu | |||
- Austin Jones | |||
- lilo | |||
- Benedikt Geißler | |||
- ePirat | |||
- Eugen Rochko | |||
- Weblate Admin | |||
- Patrick Figel | |||
- **Greek** | |||
- Dimitris Maroulidis | |||
- Antonis | |||
- Aditoo | |||
- ButterflyOfFire | |||
- Dimitris Maroulidis | |||
- Konstantinos Grevenitis | |||
- **Hebrew** | |||
- ButterflyOfFire | |||
- Aditoo | |||
- Ira | |||
- Yaron Shahrabani | |||
- **Hungarian** | |||
- Adam Paszternak | |||
- ButterflyOfFire | |||
- Adam Paszternak | |||
- Aditoo | |||
- Tibike Miklós | |||
- **Ido** | |||
- ButterflyOfFire | |||
- Aditoo | |||
- **Indonesian** | |||
- Alfiana Sibuea | |||
- afachri | |||
- ButterflyOfFire | |||
- Dito Kurnia Pratama | |||
- Eirworks | |||
- afachri | |||
- Aditoo | |||
- Alfiana Sibuea | |||
- se7entime | |||
- **Irish** | |||
- Albakham | |||
- Kevin Houlihan | |||
- **Italian** | |||
- Alessandro Levati | |||
- Albakham | |||
- ButterflyOfFire | |||
- Marcin Mikołajczak | |||
- Aditoo | |||
- Giuseppe Pignataro | |||
- Stefano | |||
- **Japanese** | |||
- ButterflyOfFire | |||
- Kumasun Morino | |||
- Yamagishi Kazutoshi | |||
- Hinaloe | |||
- 小鳥遊まりあ | |||
- mayaeh | |||
- osapon | |||
- unarist | |||
- 小鳥遊まりあ | |||
- 森の子リスのミーコの大冒険 | |||
- **Korean** | |||
- Kumasun Morino | |||
- Yamagishi Kazutoshi | |||
- Aditoo | |||
- ButterflyOfFire | |||
- Jeong Arm | |||
- unarist | |||
- **Kazakh** | |||
- arshat | |||
- Aditoo | |||
- **Korean** | |||
- Aditoo | |||
- Jeong Arm | |||
- ButterflyOfFire | |||
- Minori Hiraoka | |||
- Yamagishi Kazutoshi | |||
- **Lithuanian** | |||
- Sarunas Medeikis | |||
- **Malay** | |||
- ButterflyOfFire | |||
- Muhammad Nur Hidayat (MNH48) | |||
- Aditoo | |||
- ButterflyOfFire | |||
- **Norwegian (old code)** | |||
- ButterflyOfFire | |||
- Espen Rønnevik | |||
- Aditoo | |||
- Tale | |||
- **Occitan** | |||
- Aditoo | |||
- ButterflyOfFire | |||
- Maxenç | |||
- Quenti2 | |||
- Quentí | |||
- Maxenç | |||
- **Persian** | |||
- ButterflyOfFire | |||
- Masoud Abkenar | |||
- Aditoo | |||
- ButterflyOfFire | |||
- **Polish** | |||
- Aditoo | |||
- Albakham | |||
- ButterflyOfFire | |||
- Jakub Mendyk | |||
- Stasiek Michalski | |||
- Marcin Mikołajczak | |||
- Jakub Mendyk | |||
- Marek Ľach | |||
- Stasiek Michalski | |||
- krkk | |||
- **Portuguese** | |||
- Albakham | |||
- João Pinheiro | |||
- manequim | |||
- Aditoo | |||
- ButterflyOfFire | |||
- Hugo Gameiro | |||
- manequim | |||
- **Portuguese (Brazil)** | |||
- André Andrade | |||
- Aditoo | |||
- Albakham | |||
- Anna e só | |||
- ButterflyOfFire | |||
- Renato "Lond" Cerqueira | |||
- **Romanian** | |||
- André Andrade | |||
- ButterflyOfFire | |||
- **Romanian** | |||
- adrianbblk | |||
- ButterflyOfFire | |||
- Aditoo | |||
- **Russian** | |||
- Andrew Zyabin | |||
- Albakham | |||
- ButterflyOfFire | |||
- Evgeny Petrov | |||
- Aditoo | |||
- Павел Гастелло | |||
- Andrew Zyabin | |||
- Yaron Shahrabani | |||
- **Serbian** | |||
- Branko Kokanovic | |||
- Burekz Finezt | |||
- Aditoo | |||
- ButterflyOfFire | |||
- **Serbian (latin)** | |||
- ButterflyOfFire | |||
- Aditoo | |||
- **Slovak** | |||
- Aditoo | |||
- ButterflyOfFire | |||
- Ivan Pleva | |||
- Lorem Ipsum | |||
- Marek Ľach | |||
- Peter | |||
- **Slovenian** | |||
- ButterflyOfFire | |||
- Kristijan Tkalec | |||
- Aditoo | |||
- ButterflyOfFire | |||
- **Spanish** | |||
- Angeles Broullón | |||
- Antón López | |||
- Albakham | |||
- ButterflyOfFire | |||
- Carlos Mondragon | |||
- Antón López | |||
- Max Winkler | |||
- Pablo de la Concepción Sanz | |||
- Sergio Soriano | |||
- Angeles Broullón | |||
- Lothar Wolf | |||
- Aditoo | |||
- David Charte | |||
- Emmanuel | |||
- Lothar Wolf | |||
- Pablo de la Concepción Sanz | |||
- **Swedish** | |||
- ButterflyOfFire | |||
- Elias Mårtenson | |||
- Isak Holmström | |||
- Shellkr | |||
- Aditoo | |||
- Elias Mårtenson | |||
- Stefan Midjich | |||
- Tim Stahel | |||
- Jonas Hultén | |||
- **Telugu** | |||
- avndp | |||
- Ranjith Tellakula | |||
- Aditoo | |||
- ButterflyOfFire | |||
- Joseph Nuthalapati | |||
- Ranjith Tellakula | |||
- avndp | |||
- **Thai** | |||
- ButterflyOfFire | |||
- parnikkapore | |||
- Thai Localization | |||
- Aditoo | |||
- **Turkish** | |||
- Ali Demirtas | |||
- ButterflyOfFire | |||
- Aditoo | |||
- **Ukrainian** | |||
- alexcleac | |||
- ButterflyOfFire | |||
- Aditoo | |||
- Ivan Verchenko | |||
- alexcleac | |||
- **Welsh** | |||
- ButterflyOfFire | |||
- carl morris | |||
- Jaz-Michael King | |||
- Kevin Beynon | |||
- Owain Rhys Lewis | |||
- Renato "Lond" Cerqueira | |||
- Rhoslyn Prys | |||
- carl morris | |||
- Aditoo | |||
- ButterflyOfFire | |||
- Renato "Lond" Cerqueira | |||
- Albakham | |||
- Kevin Beynon | |||
- **Armenian** | |||
- Aditoo | |||
- ButterflyOfFire | |||
- **Latvian** | |||
- Aditoo | |||
- ButterflyOfFire | |||
- Maigonis | |||
- **Tamil** | |||
- Aditoo | |||
- ButterflyOfFire | |||
- Prasanna Venkadesh |
@@ -3,6 +3,175 @@ Changelog | |||
All notable changes to this project will be documented in this file. | |||
## [2.8.0] - 2019-04-10 | |||
### Added | |||
- Add polls ([Gargron](https://github.com/tootsuite/mastodon/pull/10111), [ThibG](https://github.com/tootsuite/mastodon/pull/10155), [Gargron](https://github.com/tootsuite/mastodon/pull/10184), [ThibG](https://github.com/tootsuite/mastodon/pull/10196), [Gargron](https://github.com/tootsuite/mastodon/pull/10248), [ThibG](https://github.com/tootsuite/mastodon/pull/10255), [ThibG](https://github.com/tootsuite/mastodon/pull/10322), [Gargron](https://github.com/tootsuite/mastodon/pull/10138), [Gargron](https://github.com/tootsuite/mastodon/pull/10139), [Gargron](https://github.com/tootsuite/mastodon/pull/10144), [Gargron](https://github.com/tootsuite/mastodon/pull/10145),[Gargron](https://github.com/tootsuite/mastodon/pull/10146), [Gargron](https://github.com/tootsuite/mastodon/pull/10148), [Gargron](https://github.com/tootsuite/mastodon/pull/10151), [ThibG](https://github.com/tootsuite/mastodon/pull/10150), [Gargron](https://github.com/tootsuite/mastodon/pull/10168), [Gargron](https://github.com/tootsuite/mastodon/pull/10165), [Gargron](https://github.com/tootsuite/mastodon/pull/10172), [Gargron](https://github.com/tootsuite/mastodon/pull/10170), [Gargron](https://github.com/tootsuite/mastodon/pull/10171), [Gargron](https://github.com/tootsuite/mastodon/pull/10186), [Gargron](https://github.com/tootsuite/mastodon/pull/10189), [ThibG](https://github.com/tootsuite/mastodon/pull/10200), [rinsuki](https://github.com/tootsuite/mastodon/pull/10203), [Gargron](https://github.com/tootsuite/mastodon/pull/10213), [Gargron](https://github.com/tootsuite/mastodon/pull/10246), [Gargron](https://github.com/tootsuite/mastodon/pull/10265), [Gargron](https://github.com/tootsuite/mastodon/pull/10261), [ThibG](https://github.com/tootsuite/mastodon/pull/10333), [Gargron](https://github.com/tootsuite/mastodon/pull/10352), [ThibG](https://github.com/tootsuite/mastodon/pull/10140), [ThibG](https://github.com/tootsuite/mastodon/pull/10142), [ThibG](https://github.com/tootsuite/mastodon/pull/10141), [ThibG](https://github.com/tootsuite/mastodon/pull/10162), [ThibG](https://github.com/tootsuite/mastodon/pull/10161), [ThibG](https://github.com/tootsuite/mastodon/pull/10158), [ThibG](https://github.com/tootsuite/mastodon/pull/10156), [ThibG](https://github.com/tootsuite/mastodon/pull/10160), [Gargron](https://github.com/tootsuite/mastodon/pull/10185), [Gargron](https://github.com/tootsuite/mastodon/pull/10188), [ThibG](https://github.com/tootsuite/mastodon/pull/10195), [ThibG](https://github.com/tootsuite/mastodon/pull/10208), [Gargron](https://github.com/tootsuite/mastodon/pull/10187), [ThibG](https://github.com/tootsuite/mastodon/pull/10214), [ThibG](https://github.com/tootsuite/mastodon/pull/10209)) | |||
- Add follows & followers managing UI ([Gargron](https://github.com/tootsuite/mastodon/pull/10268), [Gargron](https://github.com/tootsuite/mastodon/pull/10308), [Gargron](https://github.com/tootsuite/mastodon/pull/10404), [Gargron](https://github.com/tootsuite/mastodon/pull/10293)) | |||
- Add identity proof integration with Keybase ([Gargron](https://github.com/tootsuite/mastodon/pull/10297), [xgess](https://github.com/tootsuite/mastodon/pull/10375), [Gargron](https://github.com/tootsuite/mastodon/pull/10338), [Gargron](https://github.com/tootsuite/mastodon/pull/10350), [Gargron](https://github.com/tootsuite/mastodon/pull/10414)) | |||
- Add option to overwrite imported data instead of merging ([Gargron](https://github.com/tootsuite/mastodon/pull/9962)) | |||
- Add featured hashtags to profiles ([Gargron](https://github.com/tootsuite/mastodon/pull/9755), [Gargron](https://github.com/tootsuite/mastodon/pull/10167), [Gargron](https://github.com/tootsuite/mastodon/pull/10249), [ThibG](https://github.com/tootsuite/mastodon/pull/10034)) | |||
- Add admission-based registrations mode ([Gargron](https://github.com/tootsuite/mastodon/pull/10250), [ThibG](https://github.com/tootsuite/mastodon/pull/10269), [Gargron](https://github.com/tootsuite/mastodon/pull/10264), [ThibG](https://github.com/tootsuite/mastodon/pull/10321), [Gargron](https://github.com/tootsuite/mastodon/pull/10349), [Gargron](https://github.com/tootsuite/mastodon/pull/10469)) | |||
- Add support for WebP uploads ([acid-chicken](https://github.com/tootsuite/mastodon/pull/9879)) | |||
- Add "copy link" item to status action bars in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/9983)) | |||
- Add list title editing in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/9748)) | |||
- Add a "Block & Report" button to the block confirmation dialog in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/10360)) | |||
- Add disappointed elephant when the page crashes in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/10275)) | |||
- Add ability to upload multiple files at once in web UI ([tmm576](https://github.com/tootsuite/mastodon/pull/9856)) | |||
- Add indication when you are not allowed to follow an account in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/10420), [Gargron](https://github.com/tootsuite/mastodon/pull/10491)) | |||
- Add validations to admin settings to catch common mistakes ([Gargron](https://github.com/tootsuite/mastodon/pull/10348), [ThibG](https://github.com/tootsuite/mastodon/pull/10354)) | |||
- Add `type`, `limit`, `offset`, `min_id`, `max_id`, `account_id` to search API ([Gargron](https://github.com/tootsuite/mastodon/pull/10091)) | |||
- Add a preferences API so apps can share basic behaviours ([Gargron](https://github.com/tootsuite/mastodon/pull/10109)) | |||
- Add `visibility` param to reblog REST API ([Gargron](https://github.com/tootsuite/mastodon/pull/9851), [ThibG](https://github.com/tootsuite/mastodon/pull/10302)) | |||
- Add `allowfullscreen` attribute to OEmbed iframe ([rinsuki](https://github.com/tootsuite/mastodon/pull/10370)) | |||
- Add `blocked_by` relationship to the REST API ([Gargron](https://github.com/tootsuite/mastodon/pull/10373)) | |||
- Add `tootctl statuses remove` to sweep unreferenced statuses ([Gargron](https://github.com/tootsuite/mastodon/pull/10063)) | |||
- Add `tootctl search deploy` to avoid ugly rake task syntax ([Gargron](https://github.com/tootsuite/mastodon/pull/10403)) | |||
- Add `tootctl self-destruct` to shut down server gracefully ([Gargron](https://github.com/tootsuite/mastodon/pull/10367)) | |||
- Add option to hide application used to toot ([ThibG](https://github.com/tootsuite/mastodon/pull/9897), [rinsuki](https://github.com/tootsuite/mastodon/pull/9994), [hinaloe](https://github.com/tootsuite/mastodon/pull/10086)) | |||
- Add `DB_SSLMODE` configuration variable ([sascha-sl](https://github.com/tootsuite/mastodon/pull/10210)) | |||
- Add click-to-copy UI to invites page ([Gargron](https://github.com/tootsuite/mastodon/pull/10259)) | |||
- Add self-replies fetching ([ThibG](https://github.com/tootsuite/mastodon/pull/10106), [ThibG](https://github.com/tootsuite/mastodon/pull/10128), [ThibG](https://github.com/tootsuite/mastodon/pull/10175), [ThibG](https://github.com/tootsuite/mastodon/pull/10201)) | |||
- Add rate limit for media proxy requests ([Gargron](https://github.com/tootsuite/mastodon/pull/10490)) | |||
- Add `tootctl emoji purge` ([Gargron](https://github.com/tootsuite/mastodon/pull/10481)) | |||
- Add `tootctl accounts approve` ([Gargron](https://github.com/tootsuite/mastodon/pull/10480)) | |||
- Add `tootctl accounts reset-relationships` ([noellabo](https://github.com/tootsuite/mastodon/pull/10483)) | |||
### Changed | |||
- Change design of landing page ([Gargron](https://github.com/tootsuite/mastodon/pull/10232), [Gargron](https://github.com/tootsuite/mastodon/pull/10260), [ThibG](https://github.com/tootsuite/mastodon/pull/10284), [ThibG](https://github.com/tootsuite/mastodon/pull/10291), [koyuawsmbrtn](https://github.com/tootsuite/mastodon/pull/10356), [Gargron](https://github.com/tootsuite/mastodon/pull/10245)) | |||
- Change design of profile column in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/10337), [Aditoo17](https://github.com/tootsuite/mastodon/pull/10387), [ThibG](https://github.com/tootsuite/mastodon/pull/10390), [mayaeh](https://github.com/tootsuite/mastodon/pull/10379), [ThibG](https://github.com/tootsuite/mastodon/pull/10411)) | |||
- Change language detector threshold from 140 characters to 4 words ([Gargron](https://github.com/tootsuite/mastodon/pull/10376)) | |||
- Change language detector to always kick in for non-latin alphabets ([Gargron](https://github.com/tootsuite/mastodon/pull/10276)) | |||
- Change icons of features on admin dashboard ([Gargron](https://github.com/tootsuite/mastodon/pull/10366)) | |||
- Change DNS timeouts from 1s to 5s ([ThibG](https://github.com/tootsuite/mastodon/pull/10238)) | |||
- Change Docker image to use Ubuntu with jemalloc ([Sir-Boops](https://github.com/tootsuite/mastodon/pull/10100), [BenLubar](https://github.com/tootsuite/mastodon/pull/10212)) | |||
- Change public pages to be cacheable by proxies ([BenLubar](https://github.com/tootsuite/mastodon/pull/9059)) | |||
- Change the 410 gone response for suspended accounts to be cacheable by proxies ([ThibG](https://github.com/tootsuite/mastodon/pull/10339)) | |||
- Change web UI to not not empty timeline of blocked users on block ([ThibG](https://github.com/tootsuite/mastodon/pull/10359)) | |||
- Change JSON serializer to remove unused `@context` values ([Gargron](https://github.com/tootsuite/mastodon/pull/10378)) | |||
- Change GIFV file size limit to be the same as for other videos ([rinsuki](https://github.com/tootsuite/mastodon/pull/9924)) | |||
- Change Webpack to not use @babel/preset-env to compile node_modules ([ykzts](https://github.com/tootsuite/mastodon/pull/10289)) | |||
- Change web UI to use new Web Share Target API ([gol-cha](https://github.com/tootsuite/mastodon/pull/9963)) | |||
- Change ActivityPub reports to have persistent URIs ([ThibG](https://github.com/tootsuite/mastodon/pull/10303)) | |||
- Change `tootctl accounts cull --dry-run` to list accounts that would be deleted ([BenLubar](https://github.com/tootsuite/mastodon/pull/10460)) | |||
- Change format of CSV exports of follows and mutes to include extra settings ([ThibG](https://github.com/tootsuite/mastodon/pull/10495), [ThibG](https://github.com/tootsuite/mastodon/pull/10335)) | |||
- Change ActivityPub collections to be cacheable by proxies ([ThibG](https://github.com/tootsuite/mastodon/pull/10467)) | |||
- Change REST API and public profiles to not return follows/followers for users that have blocked you ([Gargron](https://github.com/tootsuite/mastodon/pull/10491)) | |||
- Change the groupings of menu items in settings navigation ([Gargron](https://github.com/tootsuite/mastodon/pull/10533)) | |||
### Removed | |||
- Remove zopfli compression to speed up Webpack from 6min to 1min ([nolanlawson](https://github.com/tootsuite/mastodon/pull/10288)) | |||
- Remove stats.json generation to speed up Webpack ([nolanlawson](https://github.com/tootsuite/mastodon/pull/10290)) | |||
### Fixed | |||
- Fix public timelines being broken by new toots when they are not mounted in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/10131)) | |||
- Fix quick filter settings not being saved when selecting a different filter in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/10296)) | |||
- Fix remote interaction dialogs being indexed by search engines ([Gargron](https://github.com/tootsuite/mastodon/pull/10240)) | |||
- Fix maxed-out invites not showing up as expired in UI ([Gargron](https://github.com/tootsuite/mastodon/pull/10274)) | |||
- Fix scrollbar styles on compose textarea ([Gargron](https://github.com/tootsuite/mastodon/pull/10292)) | |||
- Fix timeline merge workers being queued for remote users ([Gargron](https://github.com/tootsuite/mastodon/pull/10355)) | |||
- Fix alternative relay support regression ([Gargron](https://github.com/tootsuite/mastodon/pull/10398)) | |||
- Fix trying to fetch keys of unknown accounts on a self-delete from them ([ThibG](https://github.com/tootsuite/mastodon/pull/10326)) | |||
- Fix CAS `:service_validate_url` option ([enewhuis](https://github.com/tootsuite/mastodon/pull/10328)) | |||
- Fix race conditions when creating backups ([ThibG](https://github.com/tootsuite/mastodon/pull/10234)) | |||
- Fix whitespace not being stripped out of username before validation ([aurelien-reeves](https://github.com/tootsuite/mastodon/pull/10239)) | |||
- Fix n+1 query when deleting status ([Gargron](https://github.com/tootsuite/mastodon/pull/10247)) | |||
- Fix exiting follows not being rejected when suspending a remote account ([ThibG](https://github.com/tootsuite/mastodon/pull/10230)) | |||
- Fix the underlying button element in a disabled icon button not being disabled ([ThibG](https://github.com/tootsuite/mastodon/pull/10194)) | |||
- Fix race condition when streaming out deleted statuses ([ThibG](https://github.com/tootsuite/mastodon/pull/10280)) | |||
- Fix performance of admin federation UI by caching account counts ([Gargron](https://github.com/tootsuite/mastodon/pull/10374)) | |||
- Fix JS error on pages that don't define a CSRF token ([hinaloe](https://github.com/tootsuite/mastodon/pull/10383)) | |||
- Fix `tootctl accounts cull` sometimes removing accounts that are temporarily unreachable ([BenLubar](https://github.com/tootsuite/mastodon/pull/10460)) | |||
## [2.7.4] - 2019-03-05 | |||
### Fixed | |||
- Fix web UI not cleaning up notifications after block ([Gargron](https://github.com/tootsuite/mastodon/pull/10108)) | |||
- Fix redundant HTTP requests when resolving private statuses ([ThibG](https://github.com/tootsuite/mastodon/pull/10115)) | |||
- Fix performance of account media query ([abcang](https://github.com/tootsuite/mastodon/pull/10121)) | |||
- Fix mention processing for unknown accounts ([ThibG](https://github.com/tootsuite/mastodon/pull/10125)) | |||
- Fix getting started column not scrolling on short screens ([trwnh](https://github.com/tootsuite/mastodon/pull/10075)) | |||
- Fix direct messages pagination in the web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/10126)) | |||
- Fix serialization of Announce activities ([ThibG](https://github.com/tootsuite/mastodon/pull/10129)) | |||
- Fix home timeline perpetually reloading when empty in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/10130)) | |||
- Fix lists export ([ThibG](https://github.com/tootsuite/mastodon/pull/10136)) | |||
- Fix edit profile page crash for suspended-then-unsuspended users ([ThibG](https://github.com/tootsuite/mastodon/pull/10178)) | |||
## [2.7.3] - 2019-02-23 | |||
### Added | |||
- Add domain filter to the admin federation page ([ThibG](https://github.com/tootsuite/mastodon/pull/10071)) | |||
- Add quick link from admin account view to block/unblock instance ([ThibG](https://github.com/tootsuite/mastodon/pull/10073)) | |||
### Fixed | |||
- Fix video player width not being updated to fit container width ([ThibG](https://github.com/tootsuite/mastodon/pull/10069)) | |||
- Fix domain filter being shown in admin page when local filter is active ([ThibG](https://github.com/tootsuite/mastodon/pull/10074)) | |||
- Fix crash when conversations have no valid participants ([ThibG](https://github.com/tootsuite/mastodon/pull/10078)) | |||
- Fix error when performing admin actions on no statuses ([ThibG](https://github.com/tootsuite/mastodon/pull/10094)) | |||
### Changed | |||
- Change custom emojis to randomize stored file name ([hinaloe](https://github.com/tootsuite/mastodon/pull/10090)) | |||
## [2.7.2] - 2019-02-17 | |||
### Added | |||
- Add support for IPv6 in e-mail validation ([zoc](https://github.com/tootsuite/mastodon/pull/10009)) | |||
- Add record of IP address used for signing up ([ThibG](https://github.com/tootsuite/mastodon/pull/10026)) | |||
- Add tight rate-limit for API deletions (30 per 30 minutes) ([Gargron](https://github.com/tootsuite/mastodon/pull/10042)) | |||
- Add support for embedded `Announce` objects attributed to the same actor ([ThibG](https://github.com/tootsuite/mastodon/pull/9998), [Gargron](https://github.com/tootsuite/mastodon/pull/10065)) | |||
- Add spam filter for `Create` and `Announce` activities ([Gargron](https://github.com/tootsuite/mastodon/pull/10005), [Gargron](https://github.com/tootsuite/mastodon/pull/10041), [Gargron](https://github.com/tootsuite/mastodon/pull/10062)) | |||
- Add `registrations` attribute to `GET /api/v1/instance` ([Gargron](https://github.com/tootsuite/mastodon/pull/10060)) | |||
- Add `vapid_key` to `POST /api/v1/apps` and `GET /api/v1/apps/verify_credentials` ([Gargron](https://github.com/tootsuite/mastodon/pull/10058)) | |||
### Fixed | |||
- Fix link color and add link underlines in high-contrast theme ([Gargron](https://github.com/tootsuite/mastodon/pull/9949), [Gargron](https://github.com/tootsuite/mastodon/pull/10028)) | |||
- Fix unicode characters in URLs not being linkified ([JMendyk](https://github.com/tootsuite/mastodon/pull/8447), [hinaloe](https://github.com/tootsuite/mastodon/pull/9991)) | |||
- Fix URLs linkifier grabbing ending quotation as part of the link ([Gargron](https://github.com/tootsuite/mastodon/pull/9997)) | |||
- Fix authorized applications page design ([rinsuki](https://github.com/tootsuite/mastodon/pull/9969)) | |||
- Fix custom emojis not showing up in share page emoji picker ([rinsuki](https://github.com/tootsuite/mastodon/pull/9970)) | |||
- Fix too liberal application of whitespace in toots ([trwnh](https://github.com/tootsuite/mastodon/pull/9968)) | |||
- Fix misleading e-mail hint being displayed in admin view ([ThibG](https://github.com/tootsuite/mastodon/pull/9973)) | |||
- Fix tombstones not being cleared out ([abcang](https://github.com/tootsuite/mastodon/pull/9978)) | |||
- Fix some timeline jumps ([ThibG](https://github.com/tootsuite/mastodon/pull/9982), [ThibG](https://github.com/tootsuite/mastodon/pull/10001), [rinsuki](https://github.com/tootsuite/mastodon/pull/10046)) | |||
- Fix content warning input taking keyboard focus even when hidden ([hinaloe](https://github.com/tootsuite/mastodon/pull/10017)) | |||
- Fix hashtags select styling in default and high-contrast themes ([Gargron](https://github.com/tootsuite/mastodon/pull/10029)) | |||
- Fix style regressions on landing page ([Gargron](https://github.com/tootsuite/mastodon/pull/10030)) | |||
- Fix hashtag column not subscribing to stream on mount ([Gargron](https://github.com/tootsuite/mastodon/pull/10040)) | |||
- Fix relay enabling/disabling not resetting inbox availability status ([Gargron](https://github.com/tootsuite/mastodon/pull/10048)) | |||
- Fix mutes, blocks, domain blocks and follow requests not paginating ([Gargron](https://github.com/tootsuite/mastodon/pull/10057)) | |||
- Fix crash on public hashtag pages when streaming fails ([ThibG](https://github.com/tootsuite/mastodon/pull/10061)) | |||
### Changed | |||
- Change icon for unlisted visibility level ([clarcharr](https://github.com/tootsuite/mastodon/pull/9952)) | |||
- Change queue of actor deletes from push to pull for non-follower recipients ([ThibG](https://github.com/tootsuite/mastodon/pull/10016)) | |||
- Change robots.txt to exclude media proxy URLs ([nightpool](https://github.com/tootsuite/mastodon/pull/10038)) | |||
- Change upload description input to allow line breaks ([BenLubar](https://github.com/tootsuite/mastodon/pull/10036)) | |||
- Change `dist/mastodon-streaming.service` to recommend running node without intermediary npm command ([nolanlawson](https://github.com/tootsuite/mastodon/pull/10032)) | |||
- Change conversations to always show names of other participants ([Gargron](https://github.com/tootsuite/mastodon/pull/10047)) | |||
- Change buttons on timeline preview to open the interaction dialog ([Gargron](https://github.com/tootsuite/mastodon/pull/10054)) | |||
- Change error graphic to hover-to-play ([Gargron](https://github.com/tootsuite/mastodon/pull/10055)) | |||
## [2.7.1] - 2019-01-28 | |||
### Fixed | |||
- Fix SSO authentication not working due to missing agreement boolean ([Gargron](https://github.com/tootsuite/mastodon/pull/9915)) | |||
- Fix slow fallback of CopyAccountStats migration setting stats to 0 ([Gargron](https://github.com/tootsuite/mastodon/pull/9930)) | |||
- Fix wrong command in migration error message ([angristan](https://github.com/tootsuite/mastodon/pull/9877)) | |||
- Fix initial value of volume slider in video player and handle volume changes ([ThibG](https://github.com/tootsuite/mastodon/pull/9929)) | |||
- Fix missing hotkeys for notifications ([ThibG](https://github.com/tootsuite/mastodon/pull/9927)) | |||
- Fix being able to attach unattached media created by other users ([ThibG](https://github.com/tootsuite/mastodon/pull/9921)) | |||
- Fix unrescued SSL error during link verification ([renatolond](https://github.com/tootsuite/mastodon/pull/9914)) | |||
- Fix Firefox scrollbar color regression ([trwnh](https://github.com/tootsuite/mastodon/pull/9908)) | |||
- Fix scheduled status with media immediately creating a status ([ThibG](https://github.com/tootsuite/mastodon/pull/9894)) | |||
- Fix missing strong style for landing page description ([Kjwon15](https://github.com/tootsuite/mastodon/pull/9892)) | |||
## [2.7.0] - 2019-01-20 | |||
### Added | |||
@@ -1,7 +1,7 @@ | |||
Contributing | |||
============ | |||
Thank you for considering contributing to Mastodon 🐘 | |||
Thank you for considering contributing to Mastodon 🐘 | |||
You can contribute in the following ways: | |||
@@ -10,6 +10,8 @@ You can contribute in the following ways: | |||
- Contributing code to Mastodon by fixing bugs or implementing features | |||
- Improving the documentation | |||
If your contributions are accepted into Mastodon, you can request to be paid through [our OpenCollective](https://opencollective.com/mastodon). | |||
## Bug reports | |||
Bug reports and feature suggestions can be submitted to [GitHub Issues](https://github.com/tootsuite/mastodon/issues). Please make sure that you are not submitting duplicates, and that a similar report or request has not already been resolved or rejected in the past using the search function. Please also use descriptive, concise titles. | |||
@@ -1,90 +1,128 @@ | |||
FROM node:8.15-alpine as node | |||
FROM ruby:2.6-alpine3.8 | |||
LABEL maintainer="https://github.com/tootsuite/mastodon" \ | |||
description="Your self-hosted, globally interconnected microblogging community" | |||
FROM ubuntu:18.04 as build-dep | |||
# Use bash for the shell | |||
SHELL ["bash", "-c"] | |||
# Install Node | |||
ENV NODE_VER="8.15.0" | |||
RUN echo "Etc/UTC" > /etc/localtime && \ | |||
apt update && \ | |||
apt -y dist-upgrade && \ | |||
apt -y install wget make gcc g++ python && \ | |||
cd ~ && \ | |||
wget https://nodejs.org/download/release/v$NODE_VER/node-v$NODE_VER.tar.gz && \ | |||
tar xf node-v$NODE_VER.tar.gz && \ | |||
cd node-v$NODE_VER && \ | |||
./configure --prefix=/opt/node && \ | |||
make -j$(nproc) > /dev/null && \ | |||
make install | |||
# Install jemalloc | |||
ENV JE_VER="5.1.0" | |||
RUN apt update && \ | |||
apt -y install autoconf && \ | |||
cd ~ && \ | |||
wget https://github.com/jemalloc/jemalloc/archive/$JE_VER.tar.gz && \ | |||
tar xf $JE_VER.tar.gz && \ | |||
cd jemalloc-$JE_VER && \ | |||
./autogen.sh && \ | |||
./configure --prefix=/opt/jemalloc && \ | |||
make -j$(nproc) > /dev/null && \ | |||
make install_bin install_include install_lib | |||
# Install ruby | |||
ENV RUBY_VER="2.6.1" | |||
ENV CPPFLAGS="-I/opt/jemalloc/include" | |||
ENV LDFLAGS="-L/opt/jemalloc/lib/" | |||
RUN apt update && \ | |||
apt -y install build-essential \ | |||
bison libyaml-dev libgdbm-dev libreadline-dev \ | |||
libncurses5-dev libffi-dev zlib1g-dev libssl-dev && \ | |||
cd ~ && \ | |||
wget https://cache.ruby-lang.org/pub/ruby/${RUBY_VER%.*}/ruby-$RUBY_VER.tar.gz && \ | |||
tar xf ruby-$RUBY_VER.tar.gz && \ | |||
cd ruby-$RUBY_VER && \ | |||
./configure --prefix=/opt/ruby \ | |||
--with-jemalloc \ | |||
--with-shared \ | |||
--disable-install-doc && \ | |||
ln -s /opt/jemalloc/lib/* /usr/lib/ && \ | |||
make -j$(nproc) > /dev/null && \ | |||
make install | |||
ENV PATH="${PATH}:/opt/ruby/bin:/opt/node/bin" | |||
RUN npm install -g yarn && \ | |||
gem install bundler && \ | |||
apt update && \ | |||
apt -y install git libicu-dev libidn11-dev \ | |||
libpq-dev libprotobuf-dev protobuf-compiler | |||
COPY Gemfile* package.json yarn.lock /opt/mastodon/ | |||
RUN cd /opt/mastodon && \ | |||
bundle install -j$(nproc) --deployment --without development test && \ | |||
yarn install --pure-lockfile | |||
FROM ubuntu:18.04 | |||
# Copy over all the langs needed for runtime | |||
COPY --from=build-dep /opt/node /opt/node | |||
COPY --from=build-dep /opt/ruby /opt/ruby | |||
COPY --from=build-dep /opt/jemalloc /opt/jemalloc | |||
# Add more PATHs to the PATH | |||
ENV PATH="${PATH}:/opt/ruby/bin:/opt/node/bin:/opt/mastodon/bin" | |||
# Create the mastodon user | |||
ARG UID=991 | |||
ARG GID=991 | |||
ENV PATH=/mastodon/bin:$PATH \ | |||
RAILS_SERVE_STATIC_FILES=true \ | |||
RAILS_ENV=production \ | |||
NODE_ENV=production | |||
ARG LIBICONV_VERSION=1.15 | |||
ARG LIBICONV_DOWNLOAD_SHA256=ccf536620a45458d26ba83887a983b96827001e92a13847b45e4925cc8913178 | |||
EXPOSE 3000 4000 | |||
WORKDIR /mastodon | |||
COPY --from=node /usr/local/bin/node /usr/local/bin/node | |||
COPY --from=node /usr/local/lib/node_modules /usr/local/lib/node_modules | |||
COPY --from=node /usr/local/bin/npm /usr/local/bin/npm | |||
COPY --from=node /opt/yarn-* /opt/yarn | |||
RUN apk -U upgrade \ | |||
&& apk add -t build-dependencies \ | |||
build-base \ | |||
icu-dev \ | |||
libidn-dev \ | |||
libressl \ | |||
libtool \ | |||
libxml2-dev \ | |||
libxslt-dev \ | |||
postgresql-dev \ | |||
protobuf-dev \ | |||
python \ | |||
&& apk add \ | |||
ca-certificates \ | |||
ffmpeg \ | |||
file \ | |||
git \ | |||
icu-libs \ | |||
imagemagick \ | |||
libidn \ | |||
libpq \ | |||
libxml2 \ | |||
libxslt \ | |||
protobuf \ | |||
tini \ | |||
tzdata \ | |||
&& update-ca-certificates \ | |||
&& ln -s /opt/yarn/bin/yarn /usr/local/bin/yarn \ | |||
&& ln -s /opt/yarn/bin/yarnpkg /usr/local/bin/yarnpkg \ | |||
&& mkdir -p /tmp/src /opt \ | |||
&& wget -O libiconv.tar.gz "https://ftp.gnu.org/pub/gnu/libiconv/libiconv-$LIBICONV_VERSION.tar.gz" \ | |||
&& echo "$LIBICONV_DOWNLOAD_SHA256 *libiconv.tar.gz" | sha256sum -c - \ | |||
&& tar -xzf libiconv.tar.gz -C /tmp/src \ | |||
&& rm libiconv.tar.gz \ | |||
&& cd /tmp/src/libiconv-$LIBICONV_VERSION \ | |||
&& ./configure --prefix=/usr/local \ | |||
&& make -j$(getconf _NPROCESSORS_ONLN)\ | |||
&& make install \ | |||
&& libtool --finish /usr/local/lib \ | |||
&& cd /mastodon \ | |||
&& rm -rf /tmp/* /var/cache/apk/* | |||
COPY Gemfile Gemfile.lock package.json yarn.lock .yarnclean /mastodon/ | |||
RUN bundle config build.nokogiri --use-system-libraries --with-iconv-lib=/usr/local/lib --with-iconv-include=/usr/local/include \ | |||
&& bundle install -j$(getconf _NPROCESSORS_ONLN) --deployment --without test development \ | |||
&& yarn install --pure-lockfile --ignore-engines \ | |||
&& yarn cache clean | |||
RUN addgroup -g ${GID} mastodon && adduser -h /mastodon -s /bin/sh -D -G mastodon -u ${UID} mastodon \ | |||
&& mkdir -p /mastodon/public/system /mastodon/public/assets /mastodon/public/packs \ | |||
&& chown -R mastodon:mastodon /mastodon/public | |||
COPY . /mastodon | |||
RUN chown -R mastodon:mastodon /mastodon | |||
VOLUME /mastodon/public/system | |||
RUN apt update && \ | |||
echo "Etc/UTC" > /etc/localtime && \ | |||
ln -s /opt/jemalloc/lib/* /usr/lib/ && \ | |||
apt -y dist-upgrade && \ | |||
apt install -y whois wget && \ | |||
addgroup --gid $GID mastodon && \ | |||
useradd -m -u $UID -g $GID -d /opt/mastodon mastodon && \ | |||
echo "mastodon:`head /dev/urandom | tr -dc A-Za-z0-9 | head -c 24 | mkpasswd -s -m sha-256`" | chpasswd | |||
# Install masto runtime deps | |||
RUN apt -y --no-install-recommends install \ | |||
libssl1.1 libpq5 imagemagick ffmpeg \ | |||
libicu60 libprotobuf10 libidn11 libyaml-0-2 \ | |||
file ca-certificates tzdata libreadline7 && \ | |||
apt -y install gcc && \ | |||
ln -s /opt/mastodon /mastodon && \ | |||
gem install bundler && \ | |||
rm -rf /var/cache && \ | |||
rm -rf /var/lib/apt | |||
# Add tini | |||
ENV TINI_VERSION="0.18.0" | |||
ENV TINI_SUM="12d20136605531b09a2c2dac02ccee85e1b874eb322ef6baf7561cd93f93c855" | |||
ADD https://github.com/krallin/tini/releases/download/v${TINI_VERSION}/tini /tini | |||
RUN echo "$TINI_SUM tini" | sha256sum -c - | |||
RUN chmod +x /tini | |||
# Copy over masto source, and dependencies from building, and set permissions | |||
COPY --chown=mastodon:mastodon . /opt/mastodon | |||
COPY --from=build-dep --chown=mastodon:mastodon /opt/mastodon /opt/mastodon | |||
# Run masto services in prod mode | |||
ENV RAILS_ENV="production" | |||
ENV NODE_ENV="production" | |||
# Tell rails to serve static files | |||
ENV RAILS_SERVE_STATIC_FILES="true" | |||
# Set the run user | |||
USER mastodon | |||
RUN OTP_SECRET=precompile_placeholder SECRET_KEY_BASE=precompile_placeholder bundle exec rails assets:precompile | |||
# Precompile assets | |||
RUN cd ~ && \ | |||
OTP_SECRET=precompile_placeholder SECRET_KEY_BASE=precompile_placeholder rails assets:precompile && \ | |||
yarn cache clean | |||
ENTRYPOINT ["/sbin/tini", "--"] | |||
# Set the work dir and the container entry point | |||
WORKDIR /opt/mastodon | |||
ENTRYPOINT ["/tini", "--"] |
@@ -6,16 +6,16 @@ ruby '>= 2.4.0', '< 2.7.0' | |||
gem 'pkg-config', '~> 1.3' | |||
gem 'puma', '~> 3.12' | |||
gem 'rails', '~> 5.2.2' | |||
gem 'rails', '~> 5.2.3' | |||
gem 'thor', '~> 0.20' | |||
gem 'hamlit-rails', '~> 0.2' | |||
gem 'pg', '~> 1.1' | |||
gem 'makara', '~> 0.4' | |||
gem 'pghero', '~> 2.2' | |||
gem 'dotenv-rails', '~> 2.6' | |||
gem 'dotenv-rails', '~> 2.7' | |||
gem 'aws-sdk-s3', '~> 1.30', require: false | |||
gem 'aws-sdk-s3', '~> 1.36', require: false | |||
gem 'fog-core', '<= 2.1.0' | |||
gem 'fog-openstack', '~> 0.3', require: false | |||
gem 'paperclip', '~> 6.0' | |||
@@ -23,14 +23,14 @@ gem 'paperclip-av-transcoder', '~> 0.6' | |||
gem 'streamio-ffmpeg', '~> 3.0' | |||
gem 'active_model_serializers', '~> 0.10' | |||
gem 'addressable', '~> 2.5' | |||
gem 'bootsnap', '~> 1.3', require: false | |||
gem 'addressable', '~> 2.6' | |||
gem 'bootsnap', '~> 1.4', require: false | |||
gem 'browser' | |||
gem 'charlock_holmes', '~> 0.7.6' | |||
gem 'iso-639' | |||
gem 'chewy', '~> 5.0' | |||
gem 'cld3', '~> 3.2.3' | |||
gem 'devise', '~> 4.5' | |||
gem 'devise', '~> 4.6' | |||
gem 'devise-two-factor', '~> 3.0' | |||
group :pam_authentication, optional: true do | |||
@@ -85,8 +85,8 @@ gem 'strong_migrations', '~> 0.3' | |||
gem 'tty-command', '~> 0.8', require: false | |||
gem 'tty-prompt', '~> 0.18', require: false | |||
gem 'twitter-text', '~> 1.14' | |||
gem 'tzinfo-data', '~> 1.2018' | |||
gem 'webpacker', '~> 3.5' | |||
gem 'tzinfo-data', '~> 1.2019' | |||
gem 'webpacker', '~> 4.0' | |||
gem 'webpush' | |||
gem 'json-ld', '~> 3.0' | |||
@@ -97,7 +97,7 @@ group :development, :test do | |||
gem 'fabrication', '~> 2.20' | |||
gem 'fuubar', '~> 2.3' | |||
gem 'i18n-tasks', '~> 0.9', require: false | |||
gem 'pry-byebug', '~> 3.6' | |||
gem 'pry-byebug', '~> 3.7' | |||
gem 'pry-rails', '~> 0.3' | |||
gem 'rspec-rails', '~> 3.8' | |||
end | |||
@@ -107,19 +107,19 @@ group :production, :test do | |||
end | |||
group :test do | |||
gem 'capybara', '~> 3.12' | |||
gem 'capybara', '~> 3.16' | |||
gem 'climate_control', '~> 0.2' | |||
gem 'faker', '~> 1.9' | |||
gem 'microformats', '~> 4.0' | |||
gem 'microformats', '~> 4.1' | |||
gem 'rails-controller-testing', '~> 1.0' | |||
gem 'rspec-sidekiq', '~> 3.0' | |||
gem 'simplecov', '~> 0.16', require: false | |||
gem 'webmock', '~> 3.5' | |||
gem 'parallel_tests', '~> 2.27' | |||
gem 'parallel_tests', '~> 2.28' | |||
end | |||
group :development do | |||
gem 'active_record_query_trace', '~> 1.5' | |||
gem 'active_record_query_trace', '~> 1.6' | |||
gem 'annotate', '~> 2.7' | |||
gem 'better_errors', '~> 2.5' | |||
gem 'binding_of_caller', '~> 0.7' | |||
@@ -127,8 +127,8 @@ group :development do | |||
gem 'letter_opener', '~> 1.7' | |||
gem 'letter_opener_web', '~> 1.3' | |||
gem 'memory_profiler' | |||
gem 'rubocop', '~> 0.63', require: false | |||
gem 'brakeman', '~> 4.4', require: false | |||
gem 'rubocop', '~> 0.67', require: false | |||
gem 'brakeman', '~> 4.5', require: false | |||
gem 'bundler-audit', '~> 0.6', require: false | |||
gem 'scss_lint', '~> 0.57', require: false | |||
@@ -15,54 +15,54 @@ GIT | |||
GEM | |||
remote: https://rubygems.org/ | |||
specs: | |||
actioncable (5.2.2) | |||
actionpack (= 5.2.2) | |||
actioncable (5.2.3) | |||
actionpack (= 5.2.3) | |||
nio4r (~> 2.0) | |||
websocket-driver (>= 0.6.1) | |||
actionmailer (5.2.2) | |||
actionpack (= 5.2.2) | |||
actionview (= 5.2.2) | |||
activejob (= 5.2.2) | |||
actionmailer (5.2.3) | |||
actionpack (= 5.2.3) | |||
actionview (= 5.2.3) | |||
activejob (= 5.2.3) | |||
mail (~> 2.5, >= 2.5.4) | |||
rails-dom-testing (~> 2.0) | |||
actionpack (5.2.2) | |||
actionview (= 5.2.2) | |||
activesupport (= 5.2.2) | |||
actionpack (5.2.3) | |||
actionview (= 5.2.3) | |||
activesupport (= 5.2.3) | |||
rack (~> 2.0) | |||
rack-test (>= 0.6.3) | |||
rails-dom-testing (~> 2.0) | |||
rails-html-sanitizer (~> 1.0, >= 1.0.2) | |||
actionview (5.2.2) | |||
activesupport (= 5.2.2) | |||
actionview (5.2.3) | |||
activesupport (= 5.2.3) | |||
builder (~> 3.1) | |||
erubi (~> 1.4) | |||
rails-dom-testing (~> 2.0) | |||
rails-html-sanitizer (~> 1.0, >= 1.0.3) | |||
active_model_serializers (0.10.8) | |||
active_model_serializers (0.10.9) | |||
actionpack (>= 4.1, < 6) | |||
activemodel (>= 4.1, < 6) | |||
case_transform (>= 0.2) | |||
jsonapi-renderer (>= 0.1.1.beta1, < 0.3) | |||
active_record_query_trace (1.5.4) | |||
activejob (5.2.2) | |||
activesupport (= 5.2.2) | |||
active_record_query_trace (1.6.2) | |||
activejob (5.2.3) | |||
activesupport (= 5.2.3) | |||
globalid (>= 0.3.6) | |||
activemodel (5.2.2) | |||
activesupport (= 5.2.2) | |||
activerecord (5.2.2) | |||
activemodel (= 5.2.2) | |||
activesupport (= 5.2.2) | |||
activemodel (5.2.3) | |||
activesupport (= 5.2.3) | |||
activerecord (5.2.3) | |||
activemodel (= 5.2.3) | |||
activesupport (= 5.2.3) | |||
arel (>= 9.0) | |||
activestorage (5.2.2) | |||
actionpack (= 5.2.2) | |||
activerecord (= 5.2.2) | |||
activestorage (5.2.3) | |||
actionpack (= 5.2.3) | |||
activerecord (= 5.2.3) | |||
marcel (~> 0.3.1) | |||
activesupport (5.2.2) | |||
activesupport (5.2.3) | |||
concurrent-ruby (~> 1.0, >= 1.0.2) | |||
i18n (>= 0.7, < 2) | |||
minitest (~> 5.1) | |||
tzinfo (~> 1.1) | |||
addressable (2.5.2) | |||
addressable (2.6.0) | |||
public_suffix (>= 2.0.2, < 4.0) | |||
airbrussh (1.3.0) | |||
sshkit (>= 1.6.1, != 1.7.0) | |||
@@ -75,32 +75,33 @@ GEM | |||
encryptor (~> 3.0.0) | |||
av (0.9.0) | |||
cocaine (~> 0.5.3) | |||
aws-eventstream (1.0.1) | |||
aws-partitions (1.131.0) | |||
aws-sdk-core (3.45.0) | |||
aws-eventstream (~> 1.0) | |||
aws-eventstream (1.0.2) | |||
aws-partitions (1.147.0) | |||
aws-sdk-core (3.48.3) | |||
aws-eventstream (~> 1.0, >= 1.0.2) | |||
aws-partitions (~> 1.0) | |||
aws-sigv4 (~> 1.0) | |||
aws-sigv4 (~> 1.1) | |||
jmespath (~> 1.0) | |||
aws-sdk-kms (1.13.0) | |||
aws-sdk-core (~> 3, >= 3.39.0) | |||
aws-sigv4 (~> 1.0) | |||
aws-sdk-s3 (1.30.1) | |||
aws-sdk-core (~> 3, >= 3.39.0) | |||
aws-sdk-kms (1.16.0) | |||
aws-sdk-core (~> 3, >= 3.48.2) | |||
aws-sigv4 (~> 1.1) | |||
aws-sdk-s3 (1.36.0) | |||
aws-sdk-core (~> 3, >= 3.48.2) | |||
aws-sdk-kms (~> 1) | |||
aws-sigv4 (~> 1.0) | |||
aws-sigv4 (1.0.3) | |||
aws-sigv4 (1.1.0) | |||
aws-eventstream (~> 1.0, >= 1.0.2) | |||
bcrypt (3.1.12) | |||
benchmark-ips (2.7.2) | |||
better_errors (2.5.0) | |||
better_errors (2.5.1) | |||
coderay (>= 1.0.0) | |||
erubi (>= 1.0.0) | |||
rack (>= 0.9.0) | |||
binding_of_caller (0.8.0) | |||
debug_inspector (>= 0.0.1) | |||
bootsnap (1.3.2) | |||
bootsnap (1.4.3) | |||
msgpack (~> 1.0) | |||
brakeman (4.4.0) | |||
brakeman (4.5.0) | |||
browser (2.5.3) | |||
builder (3.2.3) | |||
bullet (5.9.0) | |||
@@ -109,7 +110,7 @@ GEM | |||
bundler-audit (0.6.1) | |||
bundler (>= 1.2.0, < 3) | |||
thor (~> 0.18) | |||
byebug (10.0.2) | |||
byebug (11.0.0) | |||
capistrano (3.11.0) | |||
airbrussh (>= 1.0.0) | |||
i18n | |||
@@ -126,7 +127,7 @@ GEM | |||
sshkit (~> 1.3) | |||
capistrano-yarn (2.0.2) | |||
capistrano (~> 3.0) | |||
capybara (3.12.0) | |||
capybara (3.16.1) | |||
addressable | |||
mini_mime (>= 0.1.3) | |||
nokogiri (~> 1.8) | |||
@@ -148,7 +149,7 @@ GEM | |||
cocaine (0.5.8) | |||
climate_control (>= 0.0.3, < 1.0) | |||
coderay (1.1.2) | |||
concurrent-ruby (1.1.4) | |||
concurrent-ruby (1.1.5) | |||
connection_pool (2.2.2) | |||
crack (0.4.3) | |||
safe_yaml (~> 1.0.0) | |||
@@ -164,7 +165,7 @@ GEM | |||
rack (>= 1) | |||
rake (> 10, < 13) | |||
thor (~> 0.19) | |||
devise (4.5.0) | |||
devise (4.6.2) | |||
bcrypt (~> 3.0) | |||
orm_adapter (~> 0.1) | |||
railties (>= 4.1.0, < 6.0) | |||
@@ -185,10 +186,10 @@ GEM | |||
unf (>= 0.0.5, < 1.0.0) | |||
doorkeeper (5.0.2) | |||
railties (>= 4.2) | |||
dotenv (2.6.0) | |||
dotenv-rails (2.6.0) | |||
dotenv (= 2.6.0) | |||
railties (>= 3.2, < 6.0) | |||
dotenv (2.7.2) | |||
dotenv-rails (2.7.2) | |||
dotenv (= 2.7.2) | |||
railties (>= 3.2, < 6.1) | |||
elasticsearch (6.0.2) | |||
elasticsearch-api (= 6.0.2) | |||
elasticsearch-transport (= 6.0.2) | |||
@@ -205,7 +206,7 @@ GEM | |||
tzinfo | |||
excon (0.62.0) | |||
fabrication (2.20.1) | |||
faker (1.9.1) | |||
faker (1.9.3) | |||
i18n (>= 0.7) | |||
faraday (0.15.0) | |||
multipart-post (>= 1.2, < 3) | |||
@@ -232,18 +233,18 @@ GEM | |||
rspec-core (~> 3.0) | |||
ruby-progressbar (~> 1.4) | |||
get_process_mem (0.2.3) | |||
globalid (0.4.1) | |||
globalid (0.4.2) | |||
activesupport (>= 4.2.0) | |||
goldfinger (2.1.0) | |||
addressable (~> 2.5) | |||
http (~> 3.0) | |||
nokogiri (~> 1.8) | |||
oj (~> 3.0) | |||
hamlit (2.8.8) | |||
hamlit (2.9.3) | |||
temple (>= 0.8.0) | |||
thor | |||
tilt | |||
hamlit-rails (0.2.0) | |||
hamlit-rails (0.2.3) | |||
actionpack (>= 4.0.1) | |||
activesupport (>= 4.0.1) | |||
hamlit (>= 1.2.0) | |||
@@ -253,7 +254,7 @@ GEM | |||
hashdiff (0.3.7) | |||
hashie (3.6.0) | |||
heapy (0.1.4) | |||
highline (2.0.0) | |||
highline (2.0.1) | |||
hiredis (0.6.3) | |||
hkdf (0.3.0) | |||
htmlentities (4.3.4) | |||
@@ -266,12 +267,12 @@ GEM | |||
domain_name (~> 0.5) | |||
http-form_data (2.1.1) | |||
http_accept_language (2.1.1) | |||
httplog (1.2.0) | |||
httplog (1.2.2) | |||
rack (>= 1.0) | |||
rainbow (>= 2.0.0) | |||
i18n (1.5.2) | |||
i18n (1.6.0) | |||
concurrent-ruby (~> 1.0) | |||
i18n-tasks (0.9.28) | |||
i18n-tasks (0.9.29) | |||
activesupport (>= 4.0.2) | |||
ast (>= 2.1.0) | |||
erubi | |||
@@ -290,8 +291,8 @@ GEM | |||
json-ld (3.0.2) | |||
multi_json (~> 1.12) | |||
rdf (>= 2.2.8, < 4.0) | |||
json-ld-preloaded (3.0.0) | |||
json-ld (>= 2.2, < 4.0) | |||
json-ld-preloaded (3.0.2) | |||
json-ld (~> 3.0) | |||
multi_json (~> 1.12) | |||
rdf (~> 3.0) | |||
jsonapi-renderer (0.2.0) | |||
@@ -327,25 +328,25 @@ GEM | |||
nokogiri (>= 1.5.9) | |||
mail (2.7.1) | |||
mini_mime (>= 0.1.1) | |||
makara (0.4.0) | |||
makara (0.4.1) | |||
activerecord (>= 3.0.0) | |||
marcel (0.3.3) | |||
mimemagic (~> 0.3.2) | |||
mario-redis-lock (1.2.1) | |||
redis (>= 3.0.5) | |||
memory_profiler (0.9.12) | |||
memory_profiler (0.9.13) | |||
method_source (0.9.2) | |||
microformats (4.0.7) | |||
json | |||
nokogiri | |||
microformats (4.1.0) | |||
json (~> 2.1) | |||
nokogiri (~> 1.8, >= 1.8.3) | |||
mime-types (3.2.2) | |||
mime-types-data (~> 3.2015) | |||
mime-types-data (3.2018.0812) | |||
mimemagic (0.3.2) | |||
mimemagic (0.3.3) | |||
mini_mime (1.0.1) | |||
mini_portile2 (2.4.0) | |||
minitest (5.11.3) | |||
msgpack (1.2.4) | |||
msgpack (1.2.9) | |||
multi_json (1.13.1) | |||
multipart-post (2.0.0) | |||
necromancer (0.4.0) | |||
@@ -354,7 +355,7 @@ GEM | |||
net-ssh (>= 2.6.5) | |||
net-ssh (5.0.2) | |||
nio4r (2.3.1) | |||
nokogiri (1.10.1) | |||
nokogiri (1.10.2) | |||
mini_portile2 (~> 2.4.0) | |||
nokogumbo (2.0.0) | |||
nokogiri (~> 1.8, >= 1.8.4) | |||
@@ -363,7 +364,7 @@ GEM | |||
concurrent-ruby (~> 1.0, >= 1.0.2) | |||
sidekiq (>= 3.5) | |||
statsd-ruby (~> 1.4, >= 1.4.0) | |||
oj (3.7.7) | |||
oj (3.7.11) | |||
omniauth (1.9.0) | |||
hashie (>= 3.4.6, < 3.7.0) | |||
rack (>= 1.6.2, < 3) | |||
@@ -389,10 +390,10 @@ GEM | |||
paperclip-av-transcoder (0.6.4) | |||
av (~> 0.9.0) | |||
paperclip (>= 2.5.2) | |||
parallel (1.12.1) | |||
parallel_tests (2.27.1) | |||
parallel (1.17.0) | |||
parallel_tests (2.28.0) | |||
parallel | |||
parser (2.6.0.0) | |||
parser (2.6.2.0) | |||
ast (~> 2.4.0) | |||
pastel (0.7.2) | |||
equatable (~> 0.5.0) | |||
@@ -400,8 +401,7 @@ GEM | |||
pg (1.1.4) | |||
pghero (2.2.0) | |||
activerecord | |||
pkg-config (1.3.2) | |||
powerpack (0.1.2) | |||
pkg-config (1.3.7) | |||
premailer (1.11.1) | |||
addressable | |||
css_parser (>= 1.6.0) | |||
@@ -413,38 +413,39 @@ GEM | |||
pry (0.12.2) | |||
coderay (~> 1.1.0) | |||
method_source (~> 0.9.0) | |||
pry-byebug (3.6.0) | |||
byebug (~> 10.0) | |||
pry-byebug (3.7.0) | |||
byebug (~> 11.0) | |||
pry (~> 0.10) | |||
pry-rails (0.3.9) | |||
pry (>= 0.10.4) | |||
psych (3.1.0) | |||
public_suffix (3.0.3) | |||
puma (3.12.0) | |||
pundit (2.0.0) | |||
puma (3.12.1) | |||
pundit (2.0.1) | |||
activesupport (>= 3.0.0) | |||
raabro (1.1.6) | |||
rack (2.0.6) | |||
rack (2.0.7) | |||
rack-attack (5.4.2) | |||
rack (>= 1.0, < 3) | |||
rack-cors (1.0.2) | |||
rack-cors (1.0.3) | |||
rack-protection (2.0.5) | |||
rack | |||
rack-proxy (0.6.4) | |||
rack-proxy (0.6.5) | |||
rack | |||
rack-test (1.1.0) | |||
rack (>= 1.0, < 3) | |||
rails (5.2.2) | |||
actioncable (= 5.2.2) | |||
actionmailer (= 5.2.2) | |||
actionpack (= 5.2.2) | |||
actionview (= 5.2.2) | |||
activejob (= 5.2.2) | |||
activemodel (= 5.2.2) | |||
activerecord (= 5.2.2) | |||
activestorage (= 5.2.2) | |||
activesupport (= 5.2.2) | |||
rails (5.2.3) | |||
actioncable (= 5.2.3) | |||
actionmailer (= 5.2.3) | |||
actionpack (= 5.2.3) | |||
actionview (= 5.2.3) | |||
activejob (= 5.2.3) | |||
activemodel (= 5.2.3) | |||
activerecord (= 5.2.3) | |||
activestorage (= 5.2.3) | |||
activesupport (= 5.2.3) | |||
bundler (>= 1.3.0) | |||
railties (= 5.2.2) | |||
railties (= 5.2.3) | |||
sprockets-rails (>= 2.0.0) | |||
rails-controller-testing (1.0.4) | |||
actionpack (>= 5.0.1.x) | |||
@@ -455,14 +456,14 @@ GEM | |||
nokogiri (>= 1.6) | |||
rails-html-sanitizer (1.0.4) | |||
loofah (~> 2.2, >= 2.2.2) | |||
rails-i18n (5.1.2) | |||
rails-i18n (5.1.3) | |||
i18n (>= 0.7, < 2) | |||
railties (>= 5.0, < 6) | |||
rails-settings-cached (0.6.6) | |||
rails (>= 4.2.0) | |||
railties (5.2.2) | |||
actionpack (= 5.2.2) | |||
activesupport (= 5.2.2) | |||
railties (5.2.3) | |||
actionpack (= 5.2.3) | |||
activesupport (= 5.2.3) | |||
method_source | |||
rake (>= 0.8.7) | |||
thor (>= 0.19.0, < 2.0) | |||
@@ -498,9 +499,9 @@ GEM | |||
regexp_parser (1.3.0) | |||
request_store (1.4.1) | |||
rack (>= 1.4) | |||
responders (2.4.0) | |||
actionpack (>= 4.2.0, < 5.3) | |||
railties (>= 4.2.0, < 5.3) | |||
responders (2.4.1) | |||
actionpack (>= 4.2.0, < 6.0) | |||
railties (>= 4.2.0, < 6.0) | |||
rotp (2.1.2) | |||
rpam2 (4.0.2) | |||
rqrcode (0.10.1) | |||
@@ -513,7 +514,7 @@ GEM | |||
rspec-mocks (3.8.0) | |||
diff-lcs (>= 1.2.0, < 2.0) | |||
rspec-support (~> 3.8.0) | |||
rspec-rails (3.8.1) | |||
rspec-rails (3.8.2) | |||
actionpack (>= 3.0) | |||
activesupport (>= 3.0) | |||
railties (>= 3.0) | |||
@@ -525,14 +526,14 @@ GEM | |||
rspec-core (~> 3.0, >= 3.0.0) | |||
sidekiq (>= 2.4.0) | |||
rspec-support (3.8.0) | |||
rubocop (0.63.0) | |||
rubocop (0.67.1) | |||
jaro_winkler (~> 1.5.1) | |||
parallel (~> 1.10) | |||
parser (>= 2.5, != 2.5.1.1) | |||
powerpack (~> 0.1) | |||
psych (>= 3.1.0) | |||
rainbow (>= 2.2.2, < 4.0) | |||
ruby-progressbar (~> 1.7) | |||
unicode-display_width (~> 1.4.0) | |||
unicode-display_width (>= 1.4.0, < 1.6) | |||
ruby-progressbar (1.10.0) | |||
ruby-saml (1.9.0) | |||
nokogiri (>= 1.5.10) | |||
@@ -563,9 +564,9 @@ GEM | |||
rufus-scheduler (~> 3.2) | |||
sidekiq (>= 3) | |||
tilt (>= 1.4.0) | |||
sidekiq-unique-jobs (6.0.8) | |||
sidekiq-unique-jobs (6.0.12) | |||
concurrent-ruby (~> 1.0, >= 1.0.5) | |||
sidekiq (>= 4.0, < 6.0) | |||
sidekiq (>= 4.0, < 7.0) | |||
thor (~> 0) | |||
simple-navigation (4.0.5) | |||
activesupport (>= 2.3.2) | |||
@@ -594,14 +595,14 @@ GEM | |||
multi_json (~> 1.8) | |||
strong_migrations (0.3.1) | |||
activerecord (>= 3.2.0) | |||
temple (0.8.0) | |||
temple (0.8.1) | |||
terminal-table (1.8.0) | |||
unicode-display_width (~> 1.1, >= 1.1.1) | |||
terrapin (0.6.0) | |||
climate_control (>= 0.0.3, < 1.0) | |||
thor (0.20.3) | |||
thread_safe (0.3.6) | |||
tilt (2.0.8) | |||
tilt (2.0.9) | |||
timers (4.2.0) | |||
tty-color (0.4.3) | |||
tty-command (0.8.2) | |||
@@ -622,24 +623,24 @@ GEM | |||
unf (~> 0.1.0) | |||
tzinfo (1.2.5) | |||
thread_safe (~> 0.1) | |||
tzinfo-data (1.2018.9) | |||
tzinfo-data (1.2019.1) | |||
tzinfo (>= 1.0.0) | |||
unf (0.1.4) | |||
unf_ext | |||
unf_ext (0.0.7.5) | |||
unicode-display_width (1.4.1) | |||
unicode-display_width (1.5.0) | |||
uniform_notifier (1.12.1) | |||
warden (1.2.7) | |||
rack (>= 1.0) | |||
warden (1.2.8) | |||
rack (>= 2.0.6) | |||
webmock (3.5.1) | |||
addressable (>= 2.3.6) | |||
crack (>= 0.3.2) | |||
hashdiff | |||
webpacker (3.5.5) | |||
webpacker (4.0.2) | |||
activesupport (>= 4.2) | |||
rack-proxy (>= 0.6.1) | |||
railties (>= 4.2) | |||
webpush (0.3.6) | |||
webpush (0.3.7) | |||
hkdf (~> 0.2) | |||
jwt (~> 2.0) | |||
websocket-driver (0.7.0) | |||
@@ -654,14 +655,14 @@ PLATFORMS | |||
DEPENDENCIES | |||
active_model_serializers (~> 0.10) | |||
active_record_query_trace (~> 1.5) | |||
addressable (~> 2.5) | |||
active_record_query_trace (~> 1.6) | |||
addressable (~> 2.6) | |||
annotate (~> 2.7) | |||
aws-sdk-s3 (~> 1.30) | |||
aws-sdk-s3 (~> 1.36) | |||
better_errors (~> 2.5) | |||
binding_of_caller (~> 0.7) | |||
bootsnap (~> 1.3) | |||
brakeman (~> 4.4) | |||
bootsnap (~> 1.4) | |||
brakeman (~> 4.5) | |||
browser | |||
bullet (~> 5.9) | |||
bundler-audit (~> 0.6) | |||
@@ -669,18 +670,18 @@ DEPENDENCIES | |||
capistrano-rails (~> 1.4) | |||
capistrano-rbenv (~> 2.1) | |||
capistrano-yarn (~> 2.0) | |||
capybara (~> 3.12) | |||
capybara (~> 3.16) | |||
charlock_holmes (~> 0.7.6) | |||
chewy (~> 5.0) | |||
cld3 (~> 3.2.3) | |||
climate_control (~> 0.2) | |||
concurrent-ruby | |||
derailed_benchmarks | |||
devise (~> 4.5) | |||
devise (~> 4.6) | |||
devise-two-factor (~> 3.0) | |||
devise_pam_authenticatable2 (~> 9.2) | |||
doorkeeper (~> 5.0) | |||
dotenv-rails (~> 2.6) | |||
dotenv-rails (~> 2.7) | |||
fabrication (~> 2.20) | |||
faker (~> 1.9) | |||
fast_blank (~> 1.0) | |||
@@ -709,7 +710,7 @@ DEPENDENCIES | |||
makara (~> 0.4) | |||
mario-redis-lock (~> 1.2) | |||
memory_profiler | |||
microformats (~> 4.0) | |||
microformats (~> 4.1) | |||
mime-types (~> 3.2) | |||
net-ldap (~> 0.10) | |||
nokogiri (~> 1.10) | |||
@@ -722,20 +723,20 @@ DEPENDENCIES | |||
ox (~> 2.10) | |||
paperclip (~> 6.0) | |||
paperclip-av-transcoder (~> 0.6) | |||
parallel_tests (~> 2.27) | |||
parallel_tests (~> 2.28) | |||
pg (~> 1.1) | |||
pghero (~> 2.2) | |||
pkg-config (~> 1.3) | |||
posix-spawn! | |||
premailer-rails | |||
private_address_check (~> 0.5) | |||
pry-byebug (~> 3.6) | |||
pry-byebug (~> 3.7) | |||
pry-rails (~> 0.3) | |||
puma (~> 3.12) | |||
pundit (~> 2.0) | |||
rack-attack (~> 5.4) | |||
rack-cors (~> 1.0) | |||
rails (~> 5.2.2) | |||
rails (~> 5.2.3) | |||
rails-controller-testing (~> 1.0) | |||
rails-i18n (~> 5.1) | |||
rails-settings-cached (~> 0.6) | |||
@@ -746,7 +747,7 @@ DEPENDENCIES | |||
rqrcode (~> 0.10) | |||
rspec-rails (~> 3.8) | |||
rspec-sidekiq (~> 3.0) | |||
rubocop (~> 0.63) | |||
rubocop (~> 0.67) | |||
sanitize (~> 5.0) | |||
scss_lint (~> 0.57) | |||
sidekiq (~> 5.2) | |||
@@ -765,13 +766,13 @@ DEPENDENCIES | |||
tty-command (~> 0.8) | |||
tty-prompt (~> 0.18) | |||
twitter-text (~> 1.14) | |||
tzinfo-data (~> 1.2018) | |||
tzinfo-data (~> 1.2019) | |||
webmock (~> 3.5) | |||
webpacker (~> 3.5) | |||
webpacker (~> 4.0) | |||
webpush | |||
RUBY VERSION | |||
ruby 2.6.0p0 | |||
ruby 2.6.1p33 | |||
BUNDLED WITH | |||
1.17.3 |
@@ -21,7 +21,7 @@ Click below to **learn more** in a video: | |||
[youtube_demo]: https://www.youtube.com/watch?v=IPSbNdBmWKE | |||
## Navigation | |||
## Navigation | |||
- [Project homepage 🐘](https://joinmastodon.org) | |||
- [Support the development via Patreon][patreon] | |||
@@ -80,13 +80,13 @@ A **Vagrant** configuration is included for development purposes. | |||
Mastodon is **free, open source software** licensed under **AGPLv3**. | |||
You can open issues for bugs you've found or features you think are missing. You can also submit pull requests to this repository, or submit translations using Weblate. To get started, take a look at [CONTRIBUTING.md](CONTRIBUTING.md) | |||
You can open issues for bugs you've found or features you think are missing. You can also submit pull requests to this repository, or submit translations using Weblate. To get started, take a look at [CONTRIBUTING.md](CONTRIBUTING.md). If your contributions are accepted into Mastodon, you can request to be paid through [our OpenCollective](https://opencollective.com/mastodon). | |||
**IRC channel**: #mastodon on irc.freenode.net | |||
## License | |||
Copyright (C) 2016-2018 Eugen Rochko & other Mastodon contributors (see [AUTHORS.md](AUTHORS.md)) | |||
Copyright (C) 2016-2019 Eugen Rochko & other Mastodon contributors (see [AUTHORS.md](AUTHORS.md)) | |||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. | |||
@@ -44,7 +44,18 @@ sudo apt-get install \ | |||
# Install rvm | |||
read RUBY_VERSION < .ruby-version | |||
gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB | |||
gpg_command="gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB" | |||
$($gpg_command) | |||
if [ $? -ne 0 ];then | |||
echo "GPG command failed, This prevented RVM from installing." | |||
echo "Retrying once..." && $($gpg_command) | |||
if [ $? -ne 0 ];then | |||
echo "GPG failed for the second time, please ensure network connectivity." | |||
echo "Exiting..." && exit 1 | |||
fi | |||
fi | |||
curl -sSL https://raw.githubusercontent.com/rvm/rvm/stable/binscripts/rvm-installer | bash -s stable --ruby=$RUBY_VERSION | |||
source /home/vagrant/.rvm/scripts/rvm | |||
@@ -31,7 +31,7 @@ class StatusesIndex < Chewy::Index | |||
}, | |||
} | |||
define_type ::Status.unscoped.without_reblogs do | |||
define_type ::Status.unscoped.without_reblogs.includes(:media_attachments) do | |||
crutch :mentions do |collection| | |||
data = ::Mention.where(status_id: collection.map(&:id)).pluck(:status_id, :account_id) | |||
data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) } | |||
@@ -48,14 +48,14 @@ class StatusesIndex < Chewy::Index | |||
end | |||
root date_detection: false do | |||
field :id, type: 'long' | |||
field :account_id, type: 'long' | |||
field :text, type: 'text', value: ->(status) { [status.spoiler_text, Formatter.instance.plaintext(status)].join("\n\n") } do | |||
field :text, type: 'text', value: ->(status) { [status.spoiler_text, Formatter.instance.plaintext(status)].concat(status.media_attachments.map(&:description)).join("\n\n") } do | |||
field :stemmed, type: 'text', analyzer: 'content' | |||
end | |||
field :searchable_by, type: 'long', value: ->(status, crutches) { status.searchable_by(crutches) } | |||
field :created_at, type: 'date' | |||
end | |||
end | |||
end |
@@ -1,26 +1,25 @@ | |||
# frozen_string_literal: true | |||
class AboutController < ApplicationController | |||
before_action :set_body_classes | |||
layout 'public' | |||
before_action :set_instance_presenter, only: [:show, :more, :terms] | |||
def show | |||
serializable_resource = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(initial_state_params), serializer: InitialStateSerializer) | |||
@initial_state_json = serializable_resource.to_json | |||
@hide_navbar = true | |||
end | |||
def more | |||
render layout: 'public' | |||
end | |||
def more; end | |||
def terms | |||
render layout: 'public' | |||
end | |||
def terms; end | |||
private | |||
def new_user | |||
User.new.tap(&:build_account) | |||
User.new.tap do |user| | |||
user.build_account | |||
user.build_invite_request | |||
end | |||
end | |||
helper_method :new_user | |||
@@ -28,15 +27,4 @@ class AboutController < ApplicationController | |||
def set_instance_presenter | |||
@instance_presenter = InstancePresenter.new | |||
end | |||
def set_body_classes | |||
@body_classes = 'with-modals' | |||
end | |||
def initial_state_params | |||
{ | |||
settings: { known_fediverse: Setting.show_known_fediverse_at_about_page }, | |||
token: current_session&.token, | |||
} | |||
end | |||
end |
@@ -10,6 +10,8 @@ class AccountsController < ApplicationController | |||
def show | |||
respond_to do |format| | |||
format.html do | |||
mark_cacheable! unless user_signed_in? | |||
@body_classes = 'with-modals' | |||
@pinned_statuses = [] | |||
@endorsed_accounts = @account.endorsed_accounts.to_a.sample(4) | |||
@@ -30,17 +32,21 @@ class AccountsController < ApplicationController | |||
end | |||
format.atom do | |||
mark_cacheable! | |||
@entries = @account.stream_entries.where(hidden: false).with_includes.paginate_by_max_id(PAGE_SIZE, params[:max_id], params[:since_id]) | |||
render xml: OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.feed(@account, @entries.reject { |entry| entry.status.nil? })) | |||
end | |||
format.rss do | |||
mark_cacheable! | |||
@statuses = cache_collection(default_statuses.without_reblogs.without_replies.limit(PAGE_SIZE), Status) | |||
render xml: RSS::AccountSerializer.render(@account, @statuses) | |||
end | |||
format.json do | |||
skip_session! | |||
mark_cacheable! | |||
render_cached_json(['activitypub', 'actor', @account], content_type: 'application/activity+json') do | |||
ActiveModelSerializers::SerializableResource.new(@account, serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter) | |||
@@ -52,11 +58,12 @@ class AccountsController < ApplicationController | |||
private | |||
def show_pinned_statuses? | |||
[replies_requested?, media_requested?, params[:max_id].present?, params[:min_id].present?].none? | |||
[replies_requested?, media_requested?, tag_requested?, params[:max_id].present?, params[:min_id].present?].none? | |||
end | |||
def filtered_statuses | |||
default_statuses.tap do |statuses| | |||
statuses.merge!(hashtag_scope) if tag_requested? | |||
statuses.merge!(only_media_scope) if media_requested? | |||
statuses.merge!(no_replies_scope) unless replies_requested? | |||
end | |||
@@ -78,12 +85,21 @@ class AccountsController < ApplicationController | |||
Status.without_replies | |||
end | |||
def set_account | |||
@account = Account.find_local!(params[:username]) | |||
def hashtag_scope | |||
tag = Tag.find_normalized(params[:tag]) | |||
if tag | |||
Status.tagged_with(tag.id) | |||
else | |||
Status.none | |||
end | |||
end | |||
def username_param | |||
params[:username] | |||
end | |||
def older_url | |||
::Rails.logger.info("older: max_id #{@statuses.last.id}, url #{pagination_url(max_id: @statuses.last.id)}") | |||
pagination_url(max_id: @statuses.last.id) | |||
end | |||
@@ -92,7 +108,9 @@ class AccountsController < ApplicationController | |||
end | |||
def pagination_url(max_id: nil, min_id: nil) | |||
if media_requested? | |||
if tag_requested? | |||
short_account_tag_url(@account, params[:tag], max_id: max_id, min_id: min_id) | |||
elsif media_requested? | |||
short_account_media_url(@account, max_id: max_id, min_id: min_id) | |||
elsif replies_requested? | |||
short_account_with_replies_url(@account, max_id: max_id, min_id: min_id) | |||
@@ -109,6 +127,10 @@ class AccountsController < ApplicationController | |||
request.path.ends_with?('/with_replies') | |||
end | |||
def tag_requested? | |||
request.path.ends_with?(Addressable::URI.parse("/tagged/#{params[:tag]}").normalize) | |||
end | |||
def filtered_status_page(params) | |||
if params[:min_id].present? | |||
filtered_statuses.paginate_by_min_id(PAGE_SIZE, params[:min_id]).reverse | |||
@@ -6,13 +6,19 @@ class ActivityPub::CollectionsController < Api::BaseController | |||
before_action :set_account | |||
before_action :set_size | |||
before_action :set_statuses | |||
before_action :set_cache_headers | |||
def show | |||
render json: collection_presenter, | |||
serializer: ActivityPub::CollectionSerializer, | |||
adapter: ActivityPub::Adapter, | |||
content_type: 'application/activity+json', | |||
skip_activities: true | |||
skip_session! | |||
render_cached_json(['activitypub', 'collection', @account, params[:id]], content_type: 'application/activity+json') do | |||
ActiveModelSerializers::SerializableResource.new( | |||
collection_presenter, | |||
serializer: ActivityPub::CollectionSerializer, | |||
adapter: ActivityPub::Adapter, | |||
skip_activities: true | |||
) | |||
end | |||
end | |||
private | |||
@@ -2,11 +2,14 @@ | |||
class ActivityPub::InboxesController < Api::BaseController | |||
include SignatureVerification | |||
include JsonLdHelper | |||
before_action :set_account | |||
def create | |||
if signed_request_account | |||
if unknown_deleted_account? | |||
head 202 | |||
elsif signed_request_account | |||
upgrade_account | |||
process_payload | |||
head 202 | |||
@@ -17,12 +20,22 @@ class ActivityPub::InboxesController < Api::BaseController | |||
private | |||
def unknown_deleted_account? | |||
json = Oj.load(body, mode: :strict) | |||
json['type'] == 'Delete' && json['actor'].present? && json['actor'] == value_or_id(json['object']) && !Account.where(uri: json['actor']).exists? | |||
rescue Oj::ParseError | |||
false | |||
end | |||
def set_account | |||
@account = Account.find_local!(params[:account_username]) if params[:account_username] | |||
end | |||
def body | |||
@body ||= request.body.read | |||
return @body if defined?(@body) | |||
@body = request.body.read.force_encoding('UTF-8') | |||
request.body.rewind if request.body.respond_to?(:rewind) | |||
@body | |||
end | |||
def upgrade_account | |||
@@ -36,6 +49,6 @@ class ActivityPub::InboxesController < Api::BaseController | |||
end | |||
def process_payload | |||
ActivityPub::ProcessingWorker.perform_async(signed_request_account.id, body.force_encoding('UTF-8'), @account&.id) | |||
ActivityPub::ProcessingWorker.perform_async(signed_request_account.id, body, @account&.id) | |||
end | |||
end |
@@ -7,8 +7,14 @@ class ActivityPub::OutboxesController < Api::BaseController | |||
before_action :set_account | |||
before_action :set_statuses | |||
before_action :set_cache_headers | |||
def show | |||
unless page_requested? | |||
skip_session! | |||
expires_in 1.minute, public: true | |||
end | |||
render json: outbox_presenter, serializer: ActivityPub::OutboxSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json' | |||
end | |||
@@ -2,9 +2,9 @@ | |||
module Admin | |||
class AccountsController < BaseController | |||
before_action :set_account, only: [:show, :subscribe, :unsubscribe, :redownload, :remove_avatar, :remove_header, :enable, :unsilence, :unsuspend, :memorialize] | |||
before_action :set_account, only: [:show, :subscribe, :unsubscribe, :redownload, :remove_avatar, :remove_header, :enable, :unsilence, :unsuspend, :memorialize, :approve, :reject] | |||
before_action :require_remote_account!, only: [:subscribe, :unsubscribe, :redownload] | |||
before_action :require_local_account!, only: [:enable, :memorialize] | |||
before_action :require_local_account!, only: [:enable, :memorialize, :approve, :reject] | |||
def index | |||
authorize :account, :index? | |||
@@ -45,6 +45,18 @@ module Admin | |||
redirect_to admin_account_path(@account.id) | |||
end | |||
def approve | |||
authorize @account.user, :approve? | |||
@account.user.approve! | |||
redirect_to admin_accounts_path(pending: '1') | |||
end | |||
def reject | |||
authorize @account.user, :reject? | |||
SuspendAccountService.new.call(@account, including_user: true, destroy: true, skip_distribution: true) | |||
redirect_to admin_accounts_path(pending: '1') | |||
end | |||
def unsilence | |||
authorize @account, :unsilence? | |||
@account.unsilence! | |||
@@ -114,6 +126,7 @@ module Admin | |||
:remote, | |||
:by_domain, | |||
:active, | |||
:pending, | |||
:silenced, | |||
:suspended, | |||
:username, | |||
@@ -5,6 +5,9 @@ module Admin | |||
before_action :set_custom_emoji, except: [:index, :new, :create] | |||
before_action :set_filter_params | |||
include ObfuscateFilename | |||
obfuscate_filename [:custom_emoji, :image] | |||
def index | |||
authorize :custom_emoji, :index? | |||
@custom_emojis = filtered_custom_emojis.eager_load(:local_counterpart).page(params[:page]) | |||
@@ -10,7 +10,7 @@ module Admin | |||
@interactions_week = Redis.current.get("activity:interactions:#{current_week}") || 0 | |||
@relay_enabled = Relay.enabled.exists? | |||
@single_user_mode = Rails.configuration.x.single_user_mode | |||
@registrations_enabled = Setting.open_registrations | |||
@registrations_enabled = Setting.registrations_mode != 'none' | |||
@deletions_enabled = Setting.open_deletion | |||
@invites_enabled = Setting.min_invite_role == 'user' | |||
@search_enabled = Chewy.enabled? | |||
@@ -29,6 +29,7 @@ module Admin | |||
@hidden_service = ENV['ALLOW_ACCESS_TO_HIDDEN_SERVICE'] == 'true' | |||
@trending_hashtags = TrendingTags.get(7) | |||
@profile_directory = Setting.profile_directory | |||
@timeline_preview = Setting.timeline_preview | |||
end | |||
private | |||
@@ -38,7 +38,7 @@ module Admin | |||
end | |||
def filter_params | |||
params.permit(:limited) | |||
params.permit(:limited, :by_domain) | |||
end | |||
end | |||
end |
@@ -0,0 +1,52 @@ | |||
# frozen_string_literal: true | |||
module Admin | |||
class PendingAccountsController < BaseController | |||
before_action :set_accounts, only: :index | |||
def index | |||
@form = Form::AccountBatch.new | |||
end | |||
def batch | |||
@form = Form::AccountBatch.new(form_account_batch_params.merge(current_account: current_account, action: action_from_button)) | |||
@form.save | |||
rescue ActionController::ParameterMissing | |||
flash[:alert] = I18n.t('admin.accounts.no_account_selected') | |||
ensure | |||
redirect_to admin_pending_accounts_path(current_params) | |||
end | |||
def approve_all | |||
Form::AccountBatch.new(current_account: current_account, account_ids: User.pending.pluck(:account_id), action: 'approve').save | |||
redirect_to admin_pending_accounts_path(current_params) | |||
end | |||
def reject_all | |||
Form::AccountBatch.new(current_account: current_account, account_ids: User.pending.pluck(:account_id), action: 'reject').save | |||
redirect_to admin_pending_accounts_path(current_params) | |||
end | |||
private | |||
def set_accounts | |||
@accounts = Account.joins(:user).merge(User.pending.recent).includes(user: :invite_request).page(params[:page]) | |||
end | |||
def form_account_batch_params | |||
params.require(:form_account_batch).permit(:action, account_ids: []) | |||
end | |||
def action_from_button | |||
if params[:approve] | |||
'approve' | |||
elsif params[:reject] | |||
'reject' | |||
end | |||
end | |||
def current_params | |||
params.slice(:page).permit(:page) | |||
end | |||
end | |||
end |
@@ -11,6 +11,10 @@ module Admin | |||
flash[:alert] = I18n.t('admin.statuses.failed_to_execute') unless @form.save | |||
redirect_to admin_report_path(@report) | |||
rescue ActionController::ParameterMissing | |||
flash[:alert] = I18n.t('admin.statuses.no_status_selected') | |||
redirect_to admin_report_path(@report) | |||
end | |||
private | |||
@@ -2,85 +2,29 @@ | |||
module Admin | |||
class SettingsController < BaseController | |||
ADMIN_SETTINGS = %w( | |||
site_contact_username | |||
site_contact_email | |||
site_title | |||
site_short_description | |||
site_description | |||
site_extended_description | |||
site_terms | |||
open_registrations | |||
closed_registrations_message | |||
open_deletion | |||
timeline_preview | |||
show_staff_badge | |||
bootstrap_timeline_accounts | |||
theme | |||
thumbnail | |||
hero | |||
mascot | |||
min_invite_role | |||
activity_api_enabled | |||
peers_api_enabled | |||
show_known_fediverse_at_about_page | |||
preview_sensitive_media | |||
custom_css | |||
profile_directory | |||
).freeze | |||
BOOLEAN_SETTINGS = %w( | |||
open_registrations | |||
open_deletion | |||
timeline_preview | |||
show_staff_badge | |||
activity_api_enabled | |||
peers_api_enabled | |||
show_known_fediverse_at_about_page | |||
preview_sensitive_media | |||
profile_directory | |||
).freeze | |||
UPLOAD_SETTINGS = %w( | |||
thumbnail | |||
hero | |||
mascot | |||
).freeze | |||
def edit | |||
authorize :settings, :show? | |||
@admin_settings = Form::AdminSettings.new | |||
end | |||
def update | |||
authorize :settings, :update? | |||
settings_params.each do |key, value| | |||
if UPLOAD_SETTINGS.include?(key) | |||
upload = SiteUpload.where(var: key).first_or_initialize(var: key) | |||
upload.update(file: value) | |||
else | |||
setting = Setting.where(var: key).first_or_initialize(var: key) | |||
setting.update(value: value_for_update(key, value)) | |||
end | |||
end | |||
@admin_settings = Form::AdminSettings.new(settings_params) | |||
flash[:notice] = I18n.t('generic.changes_saved_msg') | |||
redirect_to edit_admin_settings_path | |||
if @admin_settings.save | |||
flash[:notice] = I18n.t('generic.changes_saved_msg') | |||
redirect_to edit_admin_settings_path | |||
else | |||
render :edit | |||
end | |||
end | |||
private | |||
def settings_params | |||
params.require(:form_admin_settings).permit(ADMIN_SETTINGS) | |||
end | |||
def value_for_update(key, value) | |||
if BOOLEAN_SETTINGS.include?(key) | |||
value == '1' | |||
else | |||
value | |||
end | |||
params.require(:form_admin_settings).permit(*Form::AdminSettings::KEYS) | |||
end | |||
end | |||
end |
@@ -73,7 +73,9 @@ class Api::BaseController < ApplicationController | |||
elsif current_user.disabled? | |||
render json: { error: 'Your login is currently disabled' }, status: 403 | |||
elsif !current_user.confirmed? | |||
render json: { error: 'Email confirmation is not completed' }, status: 403 | |||
render json: { error: 'Your login is missing a confirmed e-mail address' }, status: 403 | |||
elsif !current_user.approved? | |||
render json: { error: 'Your login is currently pending approval' }, status: 403 | |||
else | |||
set_user_activity | |||
end | |||
@@ -0,0 +1,30 @@ | |||
# frozen_string_literal: true | |||
class Api::ProofsController < Api::BaseController | |||
before_action :set_account | |||
before_action :set_provider | |||
before_action :check_account_approval | |||
before_action :check_account_suspension | |||
def index | |||
render json: @account, serializer: @provider.serializer_class | |||
end | |||
private | |||
def set_provider | |||
@provider = ProofProvider.find(params[:provider]) || raise(ActiveRecord::RecordNotFound) | |||
end | |||
def set_account | |||
@account = Account.find_local!(params[:username]) | |||
end | |||
def check_account_approval | |||
not_found if @account.user_pending? | |||
end | |||
def check_account_suspension | |||
gone if @account.suspended? | |||
end | |||
end |
@@ -19,11 +19,15 @@ class Api::V1::Accounts::FollowerAccountsController < Api::BaseController | |||
end | |||
def load_accounts | |||
return [] if @account.user_hides_network? && current_account.id != @account.id | |||
return [] if hide_results? | |||
default_accounts.merge(paginated_follows).to_a | |||
end | |||
def hide_results? | |||
(@account.user_hides_network? && current_account.id != @account.id) || (current_account && @account.blocking?(current_account)) | |||
end | |||
def default_accounts | |||
Account.includes(:active_relationships, :account_stat).references(:active_relationships) | |||
end | |||
@@ -19,11 +19,15 @@ class Api::V1::Accounts::FollowingAccountsController < Api::BaseController | |||
end | |||
def load_accounts | |||
return [] if @account.user_hides_network? && current_account.id != @account.id | |||
return [] if hide_results? | |||
default_accounts.merge(paginated_follows).to_a | |||
end | |||
def hide_results? | |||
(@account.user_hides_network? && current_account.id != @account.id) || (current_account && @account.blocking?(current_account)) | |||
end | |||
def default_accounts | |||
Account.includes(:passive_relationships, :account_stat).references(:passive_relationships) | |||
end | |||
@@ -0,0 +1,19 @@ | |||
# frozen_string_literal: true | |||
class Api::V1::Accounts::IdentityProofsController < Api::BaseController | |||
before_action :require_user! | |||
before_action :set_account | |||
respond_to :json | |||
def index | |||
@proofs = @account.identity_proofs.active | |||
render json: @proofs, each_serializer: REST::IdentityProofSerializer | |||
end | |||
private | |||
def set_account | |||
@account = Account.find(params[:account_id]) | |||
end | |||
end |
@@ -16,10 +16,11 @@ class Api::V1::Accounts::SearchController < Api::BaseController | |||
def account_search | |||
AccountSearchService.new.call( | |||
params[:q], | |||
limit_param(DEFAULT_ACCOUNTS_LIMIT), | |||
current_account, | |||
limit: limit_param(DEFAULT_ACCOUNTS_LIMIT), | |||
resolve: truthy_param?(:resolve), | |||
following: truthy_param?(:following) | |||
following: truthy_param?(:following), | |||
offset: params[:offset] | |||
) | |||
end | |||
end |
@@ -33,6 +33,7 @@ class Api::V1::Accounts::StatusesController < Api::BaseController | |||
statuses.merge!(only_media_scope) if truthy_param?(:only_media) | |||
statuses.merge!(no_replies_scope) if truthy_param?(:exclude_replies) | |||
statuses.merge!(no_reblogs_scope) if truthy_param?(:exclude_reblogs) | |||
statuses.merge!(hashtag_scope) if params[:tagged].present? | |||
statuses | |||
end | |||
@@ -50,9 +51,9 @@ class Api::V1::Accounts::StatusesController < Api::BaseController | |||
# Also, Avoid getting slow by not narrowing down by `statuses.account_id`. | |||
# When narrowing down by `statuses.account_id`, `index_statuses_20180106` will be used | |||
# and the table will be joined by `Merge Semi Join`, so the query will be slow. | |||
Status.joins(:media_attachments).merge(@account.media_attachments).permitted_for(@account, current_account) | |||
.paginate_by_max_id(limit_param(DEFAULT_STATUSES_LIMIT), params[:max_id], params[:since_id]) | |||
.reorder(id: :desc).distinct(:id).pluck(:id) | |||
@account.statuses.joins(:media_attachments).merge(@account.media_attachments).permitted_for(@account, current_account) | |||
.paginate_by_max_id(limit_param(DEFAULT_STATUSES_LIMIT), params[:max_id], params[:since_id]) | |||
.reorder(id: :desc).distinct(:id).pluck(:id) | |||
end | |||
def pinned_scope | |||
@@ -67,6 +68,16 @@ class Api::V1::Accounts::StatusesController < Api::BaseController | |||
Status.without_reblogs | |||
end | |||
def hashtag_scope | |||
tag = Tag.find_normalized(params[:tagged]) | |||
if tag | |||
Status.tagged_with(tag.id) | |||
else | |||
Status.none | |||
end | |||
end | |||
def pagination_params(core_params) | |||
params.slice(:limit, :only_media, :exclude_replies).permit(:limit, :only_media, :exclude_replies).merge(core_params) | |||
end | |||
@@ -80,6 +80,10 @@ class Api::V1::AccountsController < Api::BaseController | |||
end | |||
def check_enabled_registrations | |||
forbidden if single_user_mode? || !Setting.open_registrations | |||
forbidden if single_user_mode? || !allowed_registrations? | |||
end | |||
def allowed_registrations? | |||
Setting.registrations_mode != 'none' | |||
end | |||
end |
@@ -6,6 +6,6 @@ class Api::V1::Apps::CredentialsController < Api::BaseController | |||
respond_to :json | |||
def show | |||
render json: doorkeeper_token.application, serializer: REST::StatusSerializer::ApplicationSerializer | |||
render json: doorkeeper_token.application, serializer: REST::ApplicationSerializer, fields: %i(name website vapid_key) | |||
end | |||
end |
@@ -0,0 +1,29 @@ | |||
# frozen_string_literal: true | |||
class Api::V1::Polls::VotesController < Api::BaseController | |||
include Authorization | |||
before_action -> { doorkeeper_authorize! :write, :'write:statuses' } | |||
before_action :require_user! | |||
before_action :set_poll | |||
respond_to :json | |||
def create | |||
VoteService.new.call(current_account, @poll, vote_params[:choices]) | |||
render json: @poll, serializer: REST::PollSerializer | |||
end | |||
private | |||
def set_poll | |||
@poll = Poll.attached.find(params[:poll_id]) | |||
authorize @poll.status, :show? | |||
rescue Mastodon::NotPermittedError | |||
raise ActiveRecord::RecordNotFound | |||
end | |||
def vote_params | |||
params.permit(choices: []) | |||
end | |||
end |
@@ -0,0 +1,13 @@ | |||
# frozen_string_literal: true | |||
class Api::V1::PollsController < Api::BaseController | |||
before_action -> { authorize_if_got_token! :read, :'read:statuses' }, only: :show | |||
respond_to :json | |||
def show | |||
@poll = Poll.attached.find(params[:id]) | |||
ActivityPub::FetchRemotePollService.new.call(@poll, current_account) if user_signed_in? && @poll.possibly_stale? | |||
render json: @poll, serializer: REST::PollSerializer, include_results: true | |||
end | |||
end |
@@ -0,0 +1,12 @@ | |||
# frozen_string_literal: true | |||
class Api::V1::PreferencesController < Api::BaseController | |||
before_action -> { doorkeeper_authorize! :read, :'read:accounts' } | |||
before_action :require_user! | |||
respond_to :json | |||
def index | |||
render json: current_account, serializer: REST::PreferencesSerializer | |||
end | |||
end |
@@ -3,7 +3,7 @@ | |||
class Api::V1::SearchController < Api::BaseController | |||
include Authorization | |||
RESULTS_LIMIT = 5 | |||
RESULTS_LIMIT = 20 | |||
before_action -> { doorkeeper_authorize! :read, :'read:search' } | |||
before_action :require_user! | |||
@@ -11,30 +11,22 @@ class Api::V1::SearchController < Api::BaseController | |||
respond_to :json | |||
def index | |||
@search = Search.new(search) | |||
@search = Search.new(search_results) | |||
render json: @search, serializer: REST::SearchSerializer | |||
end | |||
private | |||
def search | |||
search_results.tap do |search| | |||
search[:statuses].keep_if do |status| | |||
begin | |||
authorize status, :show? | |||
rescue Mastodon::NotPermittedError | |||
false | |||
end | |||
end | |||
end | |||
end | |||
def search_results | |||
SearchService.new.call( | |||
params[:q], | |||
RESULTS_LIMIT, | |||
truthy_param?(:resolve), | |||
current_account | |||
current_account, | |||
limit_param(RESULTS_LIMIT), | |||
search_params.merge(resolve: truthy_param?(:resolve)) | |||
) | |||
end | |||
def search_params | |||
params.permit(:type, :offset, :min_id, :max_id, :account_id) | |||
end | |||
end |
@@ -9,7 +9,7 @@ class Api::V1::Statuses::ReblogsController < Api::BaseController | |||
respond_to :json | |||
def create | |||
@status = ReblogService.new.call(current_user.account, status_for_reblog) | |||
@status = ReblogService.new.call(current_user.account, status_for_reblog, reblog_params) | |||
render json: @status, serializer: REST::StatusSerializer | |||
end | |||
@@ -32,4 +32,8 @@ class Api::V1::Statuses::ReblogsController < Api::BaseController | |||
def status_for_destroy | |||
current_user.account.statuses.where(reblog_of_id: params[:status_id]).first! | |||
end | |||
def reblog_params | |||
params.permit(:visibility) | |||
end | |||
end |
@@ -53,6 +53,7 @@ class Api::V1::StatusesController < Api::BaseController | |||
visibility: status_params[:visibility], | |||
scheduled_at: status_params[:scheduled_at], | |||
application: doorkeeper_token.application, | |||
poll: status_params[:poll], | |||
idempotency: request.headers['Idempotency-Key']) | |||
render json: @status, serializer: @status.is_a?(ScheduledStatus) ? REST::ScheduledStatusSerializer : REST::StatusSerializer | |||
@@ -73,12 +74,25 @@ class Api::V1::StatusesController < Api::BaseController | |||
@status = Status.find(params[:id]) | |||
authorize @status, :show? | |||
rescue Mastodon::NotPermittedError | |||
# Reraise in order to get a 404 instead of a 403 error code | |||
raise ActiveRecord::RecordNotFound | |||
end | |||
def status_params | |||
params.permit(:status, :in_reply_to_id, :sensitive, :spoiler_text, :visibility, :scheduled_at, media_ids: []) | |||
params.permit( | |||
:status, | |||
:in_reply_to_id, | |||
:sensitive, | |||
:spoiler_text, | |||
:visibility, | |||
:scheduled_at, | |||
media_ids: [], | |||
poll: [ | |||
:multiple, | |||
:hide_totals, | |||
:expires_in, | |||
options: [], | |||
] | |||
) | |||
end | |||
def pagination_params(core_params) | |||
@@ -14,7 +14,7 @@ class Api::V1::Timelines::TagController < Api::BaseController | |||
private | |||
def load_tag | |||
@tag = Tag.find_by(name: params[:id].downcase) | |||
@tag = Tag.find_normalized(params[:id]) | |||
end | |||
def load_statuses | |||
@@ -2,7 +2,7 @@ | |||
class Api::V2::SearchController < Api::V1::SearchController | |||
def index | |||
@search = Search.new(search) | |||
@search = Search.new(search_results) | |||
render json: @search, serializer: REST::V2::SearchSerializer | |||
end | |||
end |
@@ -151,6 +151,11 @@ class ApplicationController < ActionController::Base | |||
response.headers['Vary'] = 'Accept' | |||
end | |||
def mark_cacheable! | |||
skip_session! | |||
expires_in 0, public: true | |||
end | |||
def skip_session! | |||
request.session_options[:skip] = true | |||
end | |||
@@ -10,6 +10,10 @@ class Auth::RegistrationsController < Devise::RegistrationsController | |||
before_action :set_instance_presenter, only: [:new, :create, :update] | |||
before_action :set_body_classes, only: [:new, :create, :edit, :update] | |||
def new | |||
super(&:build_invite_request) | |||
end | |||
def destroy | |||
not_found | |||
end | |||
@@ -24,16 +28,17 @@ class Auth::RegistrationsController < Devise::RegistrationsController | |||
def build_resource(hash = nil) | |||
super(hash) | |||
resource.locale = I18n.locale | |||
resource.invite_code = params[:invite_code] if resource.invite_code.blank? | |||
resource.agreement = true | |||
resource.locale = I18n.locale | |||
resource.invite_code = params[:invite_code] if resource.invite_code.blank? | |||
resource.agreement = true | |||
resource.current_sign_in_ip = request.remote_ip | |||
resource.build_account if resource.account.nil? | |||
end | |||
def configure_sign_up_params | |||
devise_parameter_sanitizer.permit(:sign_up) do |u| | |||
u.permit({ account_attributes: [:username] }, :email, :password, :password_confirmation, :invite_code) | |||
u.permit({ account_attributes: [:username], invite_request_attributes: [:text] }, :email, :password, :password_confirmation, :invite_code) | |||
end | |||
end | |||
@@ -64,7 +69,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController | |||
end | |||
def allowed_registrations? | |||
Setting.open_registrations || @invite&.valid_for_use? | |||
Setting.registrations_mode != 'none' || @invite&.valid_for_use? | |||
end | |||
def invite_code | |||
@@ -7,16 +7,18 @@ module AccountControllerConcern | |||
included do | |||
layout 'public' | |||
before_action :set_account | |||
before_action :check_account_approval | |||
before_action :check_account_suspension | |||
before_action :set_instance_presenter | |||
before_action :set_link_headers | |||
before_action :check_account_suspension | |||
end | |||
private | |||
def set_account | |||
@account = Account.find_local!(params[:account_username]) | |||
@account = Account.find_local!(username_param) | |||
end | |||
def set_instance_presenter | |||
@@ -33,6 +35,10 @@ module AccountControllerConcern | |||
) | |||
end | |||
def username_param | |||
params[:account_username] | |||
end | |||
def webfinger_account_link | |||
[ | |||
webfinger_account_url, | |||
@@ -58,7 +64,15 @@ module AccountControllerConcern | |||
webfinger_url(resource: @account.to_webfinger_s) | |||
end | |||
def check_account_approval | |||
not_found if @account.user_pending? | |||
end | |||
def check_account_suspension | |||
gone if @account.suspended? | |||
if @account.suspended? | |||
skip_session! | |||
expires_in(3.minutes, public: true) | |||
gone | |||
end | |||
end | |||
end |
@@ -32,7 +32,7 @@ class DirectoriesController < ApplicationController | |||
end | |||
def set_accounts | |||
@accounts = Account.discoverable.page(params[:page]).per(40).tap do |query| | |||
@accounts = Account.discoverable.by_recent_status.page(params[:page]).per(40).tap do |query| | |||
query.merge!(Account.tagged_with(@tag.id)) if @tag | |||
end | |||
end | |||
@@ -3,9 +3,13 @@ | |||
class FollowerAccountsController < ApplicationController | |||
include AccountControllerConcern | |||
before_action :set_cache_headers | |||
def index | |||
respond_to do |format| | |||
format.html do | |||
mark_cacheable! unless user_signed_in? | |||
next if @account.user_hides_network? | |||
follows | |||
@@ -15,6 +19,11 @@ class FollowerAccountsController < ApplicationController | |||
format.json do | |||
raise Mastodon::NotPermittedError if params[:page].present? && @account.user_hides_network? | |||
if params[:page].blank? | |||
skip_session! | |||
expires_in 3.minutes, public: true | |||
end | |||
render json: collection_presenter, | |||
serializer: ActivityPub::CollectionSerializer, | |||
adapter: ActivityPub::Adapter, | |||
@@ -3,9 +3,13 @@ | |||
class FollowingAccountsController < ApplicationController | |||
include AccountControllerConcern | |||
before_action :set_cache_headers | |||
def index | |||
respond_to do |format| | |||
format.html do | |||
mark_cacheable! unless user_signed_in? | |||
next if @account.user_hides_network? | |||
follows | |||
@@ -15,6 +19,11 @@ class FollowingAccountsController < ApplicationController | |||
format.json do | |||
raise Mastodon::NotPermittedError if params[:page].present? && @account.user_hides_network? | |||
if params[:page].blank? | |||
skip_session! | |||
expires_in 3.minutes, public: true | |||
end | |||
render json: collection_presenter, | |||
serializer: ActivityPub::CollectionSerializer, | |||
adapter: ActivityPub::Adapter, | |||
@@ -50,7 +50,7 @@ class HomeController < ApplicationController | |||
push_subscription: current_account.user.web_push_subscription(current_session), | |||
current_account: current_account, | |||
token: current_session.token, | |||
admin: Account.find_local(Setting.site_contact_username), | |||
admin: Account.find_local(Setting.site_contact_username.strip.gsub(/\A@/, '')), | |||
} | |||
end | |||
@@ -5,6 +5,7 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio | |||
before_action :store_current_location | |||
before_action :authenticate_resource_owner! | |||
before_action :set_body_classes | |||
include Localized | |||
@@ -15,6 +16,10 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio | |||
private | |||
def set_body_classes | |||
@body_classes = 'admin' | |||
end | |||
def store_current_location | |||
store_location_for(:user, request.url) | |||
end | |||
@@ -0,0 +1,34 @@ | |||
# frozen_string_literal: true | |||
class PublicTimelinesController < ApplicationController | |||
layout 'public' | |||
before_action :check_enabled | |||
before_action :set_body_classes | |||
before_action :set_instance_presenter | |||
def show | |||
respond_to do |format| | |||
format.html do | |||
@initial_state_json = ActiveModelSerializers::SerializableResource.new( | |||
InitialStatePresenter.new(settings: { known_fediverse: Setting.show_known_fediverse_at_about_page }, token: current_session&.token), | |||
serializer: InitialStateSerializer | |||
).to_json | |||
end | |||
end | |||
end | |||
private | |||
def check_enabled | |||
raise ActiveRecord::RecordNotFound unless Setting.timeline_preview | |||
end | |||
def set_body_classes | |||
@body_classes = 'with-modals' | |||
end | |||
def set_instance_presenter | |||
@instance_presenter = InstancePresenter.new | |||
end | |||
end |
@@ -0,0 +1,104 @@ | |||
# frozen_string_literal: true | |||
class RelationshipsController < ApplicationController | |||
layout 'admin' | |||
before_action :authenticate_user! | |||
before_action :set_accounts, only: :show | |||
before_action :set_body_classes | |||
helper_method :following_relationship?, :followed_by_relationship?, :mutual_relationship? | |||
def show | |||
@form = Form::AccountBatch.new | |||
end | |||
def update | |||
@form = Form::AccountBatch.new(form_account_batch_params.merge(current_account: current_account, action: action_from_button)) | |||
@form.save | |||
rescue ActionController::ParameterMissing | |||
# Do nothing | |||
ensure | |||
redirect_to relationships_path(current_params) | |||
end | |||
private | |||
def set_accounts | |||
@accounts = relationships_scope.page(params[:page]).per(40) | |||
end | |||
def relationships_scope | |||
scope = begin | |||
if following_relationship? | |||
current_account.following.eager_load(:account_stat).reorder(nil) | |||
else | |||
current_account.followers.eager_load(:account_stat).reorder(nil) | |||
end | |||
end | |||
scope.merge!(Follow.recent) if params[:order].blank? || params[:order] == 'recent' | |||
scope.merge!(Account.by_recent_status) if params[:order] == 'active' | |||
scope.merge!(mutual_relationship_scope) if mutual_relationship? | |||
scope.merge!(moved_account_scope) if params[:status] == 'moved' | |||
scope.merge!(primary_account_scope) if params[:status] == 'primary' | |||
scope.merge!(by_domain_scope) if params[:by_domain].present? | |||
scope.merge!(dormant_account_scope) if params[:activity] == 'dormant' | |||
scope | |||
end | |||
def mutual_relationship_scope | |||
Account.where(id: current_account.following) | |||
end | |||
def moved_account_scope | |||
Account.where.not(moved_to_account_id: nil) | |||
end | |||
def primary_account_scope | |||
Account.where(moved_to_account_id: nil) | |||
end | |||
def dormant_account_scope | |||
AccountStat.where(last_status_at: nil).or(AccountStat.where(AccountStat.arel_table[:last_status_at].lt(1.month.ago))) | |||
end | |||
def by_domain_scope | |||
Account.where(domain: params[:by_domain]) | |||
end | |||
def form_account_batch_params | |||
params.require(:form_account_batch).permit(:action, account_ids: []) | |||
end | |||
def following_relationship? | |||
params[:relationship].blank? || params[:relationship] == 'following' | |||
end | |||
def mutual_relationship? | |||
params[:relationship] == 'mutual' | |||
end | |||
def followed_by_relationship? | |||
params[:relationship] == 'followed_by' | |||
end | |||
def current_params | |||
params.slice(:page, :status, :relationship, :by_domain, :activity, :order).permit(:page, :status, :relationship, :by_domain, :activity, :order) | |||
end | |||
def action_from_button | |||
if params[:unfollow] | |||
'unfollow' | |||
elsif params[:remove_from_followers] | |||
'remove_from_followers' | |||
elsif params[:block_domains] | |||
'block_domains' | |||
end | |||
end | |||
def set_body_classes | |||
@body_classes = 'admin' | |||
end | |||
end |
@@ -13,11 +13,25 @@ class Settings::ExportsController < Settings::BaseController | |||
end | |||
def create | |||
authorize :backup, :create? | |||
raise Mastodon::NotPermittedError unless user_signed_in? | |||
backup = nil | |||
RedisLock.acquire(lock_options) do |lock| | |||
if lock.acquired? | |||
authorize :backup, :create? | |||
backup = current_user.backups.create! | |||
else | |||
raise Mastodon::RaceConditionError | |||
end | |||
end | |||
backup = current_user.backups.create! | |||
BackupWorker.perform_async(backup.id) | |||
redirect_to settings_export_path | |||
end | |||
def lock_options | |||
{ redis: Redis.current, key: "backup:#{current_user.id}" } | |||
end | |||
end |
@@ -0,0 +1,51 @@ | |||
# frozen_string_literal: true | |||
class Settings::FeaturedTagsController < Settings::BaseController | |||
layout 'admin' | |||
before_action :authenticate_user! | |||
before_action :set_featured_tags, only: :index | |||
before_action :set_featured_tag, except: [:index, :create] | |||
before_action :set_most_used_tags, only: :index | |||
def index | |||
@featured_tag = FeaturedTag.new | |||
end | |||
def create | |||
@featured_tag = current_account.featured_tags.new(featured_tag_params) | |||
@featured_tag.reset_data | |||
if @featured_tag.save | |||
redirect_to settings_featured_tags_path | |||
else | |||
set_featured_tags | |||
set_most_used_tags | |||
render :index | |||
end | |||
end | |||
def destroy | |||
@featured_tag.destroy! | |||
redirect_to settings_featured_tags_path | |||
end | |||
private | |||
def set_featured_tag | |||
@featured_tag = current_account.featured_tags.find(params[:id]) | |||
end | |||
def set_featured_tags | |||
@featured_tags = current_account.featured_tags.order(statuses_count: :desc).reject(&:new_record?) | |||
end | |||
def set_most_used_tags | |||
@most_used_tags = Tag.most_used(current_account).where.not(id: @featured_tags.map(&:id)).limit(10) | |||
end | |||
def featured_tag_params | |||
params.require(:featured_tag).permit(:name) | |||
end | |||
end |
@@ -1,28 +0,0 @@ | |||
# frozen_string_literal: true | |||
class Settings::FollowerDomainsController < Settings::BaseController | |||
layout 'admin' | |||
before_action :authenticate_user! | |||
def show | |||
@account = current_account | |||
@domains = current_account.followers.reorder(Arel.sql('MIN(follows.id) DESC')).group('accounts.domain').select('accounts.domain, count(accounts.id) as accounts_from_domain').page(params[:page]).per(10) | |||
end | |||
def update | |||
domains = bulk_params[:select] || [] | |||
AfterAccountDomainBlockWorker.push_bulk(domains) do |domain| | |||
[current_account.id, domain] | |||
end | |||
redirect_to settings_follower_domains_path, notice: I18n.t('followers.success', count: domains.size) | |||
end | |||
private | |||
def bulk_params | |||
params.permit(select: []) | |||
end | |||
end |
@@ -0,0 +1,63 @@ | |||
# frozen_string_literal: true | |||
class Settings::IdentityProofsController < Settings::BaseController | |||
layout 'admin' | |||
before_action :authenticate_user! | |||
before_action :check_required_params, only: :new | |||
def index | |||
@proofs = AccountIdentityProof.where(account: current_account).order(provider: :asc, provider_username: :asc) | |||
@proofs.each(&:refresh!) | |||
end | |||
def new | |||
@proof = current_account.identity_proofs.new( | |||
token: params[:token], | |||
provider: params[:provider], | |||
provider_username: params[:provider_username] | |||
) | |||
if current_account.username.casecmp(params[:username]).zero? | |||
render layout: 'auth' | |||
else | |||
flash[:alert] = I18n.t('identity_proofs.errors.wrong_user', proving: params[:username], current: current_account.username) | |||
redirect_to settings_identity_proofs_path | |||
end | |||
end | |||
def create | |||
@proof = current_account.identity_proofs.where(provider: resource_params[:provider], provider_username: resource_params[:provider_username]).first_or_initialize(resource_params) | |||
@proof.token = resource_params[:token] | |||
if @proof.save | |||
PostStatusService.new.call(current_user.account, text: post_params[:status_text]) if publish_proof? | |||
redirect_to @proof.on_success_path(params[:user_agent]) | |||
else | |||
flash[:alert] = I18n.t('identity_proofs.errors.failed', provider: @proof.provider.capitalize) | |||
redirect_to settings_identity_proofs_path | |||
end | |||
end | |||
private | |||
def check_required_params | |||
redirect_to settings_identity_proofs_path unless [:provider, :provider_username, :username, :token].all? { |k| params[k].present? } | |||
end | |||
def resource_params | |||
params.require(:account_identity_proof).permit(:provider, :provider_username, :token) | |||
end | |||
def publish_proof? | |||
ActiveModel::Type::Boolean.new.cast(post_params[:post_status]) | |||
end | |||
def post_params | |||
params.require(:account_identity_proof).permit(:post_status, :status_text) | |||
end | |||
def set_body_classes | |||
@body_classes = '' | |||
end | |||
end |
@@ -48,7 +48,8 @@ class Settings::PreferencesController < Settings::BaseController | |||
:setting_theme, | |||
:setting_hide_network, | |||
:setting_aggregate_reblogs, | |||
notification_emails: %i(follow follow_request reblog favourite mention digest report), | |||
:setting_show_application, | |||
notification_emails: %i(follow follow_request reblog favourite mention digest report pending_account), | |||
interactions: %i(must_be_follower must_be_following) | |||
) | |||
end | |||
@@ -32,6 +32,6 @@ class Settings::ProfilesController < Settings::BaseController | |||
end | |||
def set_account | |||
@account = current_user.account | |||
@account = current_account | |||
end | |||
end |
@@ -1,6 +1,7 @@ | |||
# frozen_string_literal: true | |||
class Settings::SessionsController < Settings::BaseController | |||
before_action :authenticate_user! | |||
before_action :set_session, only: :destroy | |||
def destroy | |||
@@ -21,7 +21,7 @@ class SharesController < ApplicationController | |||
push_subscription: current_account.user.web_push_subscription(current_session), | |||
current_account: current_account, | |||
token: current_session.token, | |||
admin: Account.find_local(Setting.site_contact_username), | |||
admin: Account.find_local(Setting.site_contact_username.strip.gsub(/\A@/, '')), | |||
text: text, | |||
} | |||
end | |||
@@ -18,6 +18,7 @@ class StatusesController < ApplicationController | |||
before_action :redirect_to_original, only: [:show] | |||
before_action :set_referrer_policy_header, only: [:show] | |||
before_action :set_cache_headers | |||
before_action :set_replies, only: [:replies] | |||
content_security_policy only: :embed do |p| | |||
p.frame_ancestors(false) | |||
@@ -26,6 +27,8 @@ class StatusesController < ApplicationController | |||
def show | |||
respond_to do |format| | |||
format.html do | |||
mark_cacheable! unless user_signed_in? | |||
@body_classes = 'with-modals' | |||
set_ancestors | |||
@@ -35,7 +38,7 @@ class StatusesController < ApplicationController | |||
end | |||
format.json do | |||
skip_session! unless @stream_entry.hidden? | |||
mark_cacheable! unless @stream_entry.hidden? | |||
render_cached_json(['activitypub', 'note', @status], content_type: 'application/activity+json', public: !@stream_entry.hidden?) do | |||
ActiveModelSerializers::SerializableResource.new(@status, serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter) | |||
@@ -63,8 +66,37 @@ class StatusesController < ApplicationController | |||
render 'stream_entries/embed', layout: 'embedded' | |||
end | |||
def replies | |||
skip_session! | |||
render json: replies_collection_presenter, | |||
serializer: ActivityPub::CollectionSerializer, | |||
adapter: ActivityPub::Adapter, | |||
content_type: 'application/activity+json', | |||
skip_activities: true | |||
end | |||
private | |||
def replies_collection_presenter | |||
page = ActivityPub::CollectionPresenter.new( | |||
id: replies_account_status_url(@account, @status, page_params), | |||
type: :unordered, | |||
part_of: replies_account_status_url(@account, @status), | |||
next: next_page, | |||
items: @replies.map { |status| status.local ? status : status.id } | |||
) | |||
if page_requested? | |||
page | |||
else | |||
ActivityPub::CollectionPresenter.new( | |||
id: replies_account_status_url(@account, @status), | |||
type: :unordered, | |||
first: page | |||
) | |||
end | |||
end | |||
def create_descendant_thread(starting_depth, statuses) | |||
depth = starting_depth + statuses.size | |||
if depth < DESCENDANTS_DEPTH_LIMIT | |||
@@ -174,4 +206,27 @@ class StatusesController < ApplicationController | |||
return if @status.public_visibility? || @status.unlisted_visibility? | |||
response.headers['Referrer-Policy'] = 'origin' | |||
end | |||
def page_requested? | |||
params[:page] == 'true' | |||
end | |||
def set_replies | |||
@replies = page_params[:other_accounts] ? Status.where.not(account_id: @account.id) : @account.statuses | |||
@replies = @replies.where(in_reply_to_id: @status.id, visibility: [:public, :unlisted]) | |||
@replies = @replies.paginate_by_min_id(DESCENDANTS_LIMIT, params[:min_id]) | |||
end | |||
def next_page | |||
last_reply = @replies.last | |||
return if last_reply.nil? | |||
same_account = last_reply.account_id == @account.id | |||
return unless same_account || @replies.size == DESCENDANTS_LIMIT | |||
same_account = false unless @replies.size == DESCENDANTS_LIMIT | |||
replies_account_status_url(@account, @status, page: true, min_id: last_reply.id, other_accounts: !same_account) | |||
end | |||
def page_params | |||
{ page: true, other_accounts: params[:other_accounts], min_id: params[:min_id] }.compact | |||
end | |||
end |
@@ -9,12 +9,14 @@ class TagsController < ApplicationController | |||
before_action :set_instance_presenter | |||
def show | |||
@tag = Tag.find_by!(name: params[:id].downcase) | |||
@tag = Tag.find_normalized!(params[:id]) | |||
respond_to do |format| | |||
format.html do | |||
serializable_resource = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(initial_state_params), serializer: InitialStateSerializer) | |||
@initial_state_json = serializable_resource.to_json | |||
@initial_state_json = ActiveModelSerializers::SerializableResource.new( | |||
InitialStatePresenter.new(settings: {}, token: current_session&.token), | |||
serializer: InitialStateSerializer | |||
).to_json | |||
end | |||
format.rss do | |||
@@ -25,8 +27,7 @@ class TagsController < ApplicationController | |||
end | |||
format.json do | |||
@statuses = HashtagQueryService.new.call(@tag, params.slice(:any, :all, :none), current_account, params[:local]) | |||
.paginate_by_max_id(PAGE_SIZE, params[:max_id]) | |||
@statuses = HashtagQueryService.new.call(@tag, params.slice(:any, :all, :none), current_account, params[:local]).paginate_by_max_id(PAGE_SIZE, params[:max_id]) | |||
@statuses = cache_collection(@statuses, Status) | |||
render json: collection_presenter, | |||
@@ -55,11 +56,4 @@ class TagsController < ApplicationController | |||
items: @statuses.map { |s| ActivityPub::TagManager.instance.uri_for(s) } | |||
) | |||
end | |||
def initial_state_params | |||
{ | |||
settings: {}, | |||
token: current_session&.token, | |||
} | |||
end | |||
end |
@@ -0,0 +1,9 @@ | |||
# frozen_string_literal: true | |||
module WellKnown | |||
class KeybaseProofConfigController < ActionController::Base | |||
def show | |||
render json: {}, serializer: ProofProvider::Keybase::ConfigSerializer | |||
end | |||
end | |||
end |
@@ -9,42 +9,6 @@ module Admin::ActionLogsHelper | |||
end | |||
end | |||
def linkable_log_target(record) | |||
case record.class.name | |||
when 'Account' | |||
link_to record.acct, admin_account_path(record.id) | |||
when 'User' | |||
link_to record.account.acct, admin_account_path(record.account_id) | |||
when 'CustomEmoji' | |||
record.shortcode | |||
when 'Report' | |||
link_to "##{record.id}", admin_report_path(record) | |||
when 'DomainBlock', 'EmailDomainBlock' | |||
link_to record.domain, "https://#{record.domain}" | |||
when 'Status' | |||
link_to record.account.acct, TagManager.instance.url_for(record) | |||
when 'AccountWarning' | |||
link_to record.target_account.acct, admin_account_path(record.target_account_id) | |||
end | |||
end | |||
def log_target_from_history(type, attributes) | |||
case type | |||
when 'CustomEmoji' | |||
attributes['shortcode'] | |||
when 'DomainBlock', 'EmailDomainBlock' | |||
link_to attributes['domain'], "https://#{attributes['domain']}" | |||
when 'Status' | |||
tmp_status = Status.new(attributes.except('reblogs_count', 'favourites_count')) | |||
if tmp_status.account | |||
link_to tmp_status.account&.acct || "##{tmp_status.account_id}", admin_account_path(tmp_status.account_id) | |||
else | |||
I18n.t('admin.action_logs.deleted_status') | |||
end | |||
end | |||
end | |||
def relevant_log_changes(log) | |||
if log.target_type == 'CustomEmoji' && [:enable, :disable, :destroy].include?(log.action) | |||
log.recorded_changes.slice('domain') | |||
@@ -111,4 +75,40 @@ module Admin::ActionLogsHelper | |||
def opposite_verbs?(log) | |||
%w(DomainBlock EmailDomainBlock AccountWarning).include?(log.target_type) | |||
end | |||
def linkable_log_target(record) | |||
case record.class.name | |||
when 'Account' | |||
link_to record.acct, admin_account_path(record.id) | |||
when 'User' | |||
link_to record.account.acct, admin_account_path(record.account_id) | |||
when 'CustomEmoji' | |||
record.shortcode | |||
when 'Report' | |||
link_to "##{record.id}", admin_report_path(record) | |||
when 'DomainBlock', 'EmailDomainBlock' | |||
link_to record.domain, "https://#{record.domain}" | |||
when 'Status' | |||
link_to record.account.acct, TagManager.instance.url_for(record) | |||
when 'AccountWarning' | |||
link_to record.target_account.acct, admin_account_path(record.target_account_id) | |||
end | |||
end | |||
def log_target_from_history(type, attributes) | |||
case type | |||
when 'CustomEmoji' | |||
attributes['shortcode'] | |||
when 'DomainBlock', 'EmailDomainBlock' | |||
link_to attributes['domain'], "https://#{attributes['domain']}" | |||
when 'Status' | |||
tmp_status = Status.new(attributes.except('reblogs_count', 'favourites_count')) | |||
if tmp_status.account | |||
link_to tmp_status.account&.acct || "##{tmp_status.account_id}", admin_account_path(tmp_status.account_id) | |||
else | |||
I18n.t('admin.action_logs.deleted_status') | |||
end | |||
end | |||
end | |||
end |
@@ -0,0 +1,10 @@ | |||
# frozen_string_literal: true | |||
module Admin::DashboardHelper | |||
def feature_hint(feature, enabled) | |||
indicator = safe_join([enabled ? t('simple_form.yes') : t('simple_form.no'), fa_icon('power-off fw')], ' ') | |||
class_names = enabled ? 'pull-right positive-hint' : 'pull-right neutral-hint' | |||
safe_join([feature, content_tag(:span, indicator, class: class_names)]) | |||
end | |||
end |
@@ -1,14 +1,15 @@ | |||
# frozen_string_literal: true | |||
module Admin::FilterHelper | |||
ACCOUNT_FILTERS = %i(local remote by_domain active silenced suspended username display_name email ip staff).freeze | |||
ACCOUNT_FILTERS = %i(local remote by_domain active pending silenced suspended username display_name email ip staff).freeze | |||
REPORT_FILTERS = %i(resolved account_id target_account_id).freeze | |||
INVITE_FILTER = %i(available expired).freeze | |||
CUSTOM_EMOJI_FILTERS = %i(local remote by_domain shortcode).freeze | |||
TAGS_FILTERS = %i(hidden).freeze | |||
INSTANCES_FILTERS = %i(limited).freeze | |||
INSTANCES_FILTERS = %i(limited by_domain).freeze | |||
FOLLOWERS_FILTERS = %i(relationship status by_domain activity order).freeze | |||
FILTERS = ACCOUNT_FILTERS + REPORT_FILTERS + INVITE_FILTER + CUSTOM_EMOJI_FILTERS + TAGS_FILTERS + INSTANCES_FILTERS | |||
FILTERS = ACCOUNT_FILTERS + REPORT_FILTERS + INVITE_FILTER + CUSTOM_EMOJI_FILTERS + TAGS_FILTERS + INSTANCES_FILTERS + FOLLOWERS_FILTERS | |||
def filter_link_to(text, link_to_params, link_class_params = link_to_params) | |||
new_url = filtered_url_for(link_to_params) | |||
@@ -20,7 +20,23 @@ module ApplicationHelper | |||
end | |||
def open_registrations? | |||
Setting.open_registrations | |||
Setting.registrations_mode == 'open' | |||
end | |||
def approved_registrations? | |||
Setting.registrations_mode == 'approved' | |||
end | |||
def closed_registrations? | |||
Setting.registrations_mode == 'none' | |||
end | |||
def available_sign_up_path | |||
if closed_registrations? | |||
'https://joinmastodon.org/#getting-started' | |||
else | |||
new_user_registration_path | |||
end | |||
end | |||
def open_deletion? | |||
@@ -101,4 +117,9 @@ module ApplicationHelper | |||
def storage_host? | |||
ENV['S3_ALIAS_HOST'].present? || ENV['S3_CLOUDFRONT_HOST'].present? | |||
end | |||
def quote_wrap(text, line_width: 80, break_sequence: "\n") | |||
text = word_wrap(text, line_width: line_width - 2, break_sequence: break_sequence) | |||
text.split("\n").map { |line| '> ' + line }.join("\n") | |||
end | |||
end |
@@ -56,4 +56,22 @@ module HomeHelper | |||
'emojify' | |||
end | |||
end | |||
def optional_link_to(condition, path, options = {}, &block) | |||
if condition | |||
link_to(path, options, &block) | |||
else | |||
content_tag(:div, &block) | |||
end | |||
end | |||
def sign_up_message | |||
if closed_registrations? | |||
t('auth.registration_closed', instance: site_hostname) | |||
elsif open_registrations? | |||
t('auth.register') | |||
elsif approved_registrations? | |||
t('auth.apply_for_account') | |||
end | |||
end | |||
end |
@@ -47,6 +47,15 @@ module JsonLdHelper | |||
!uri.start_with?('http://', 'https://') | |||
end | |||
def invalid_origin?(url) | |||
return true if unsupported_uri_scheme?(url) | |||
needle = Addressable::URI.parse(url).host | |||
haystack = Addressable::URI.parse(@account.uri).host | |||
!haystack.casecmp(needle).zero? | |||
end | |||
def canonicalize(json) | |||
graph = RDF::Graph.new << JSON::LD::API.toRdf(json, documentLoader: method(:load_jsonld_context)) | |||
graph.dump(:normalize) | |||
@@ -63,12 +72,19 @@ module JsonLdHelper | |||
json.present? && json['id'] == uri ? json : nil | |||
end | |||
def fetch_resource_without_id_validation(uri, on_behalf_of = nil) | |||
def fetch_resource_without_id_validation(uri, on_behalf_of = nil, raise_on_temporary_error = false) | |||
build_request(uri, on_behalf_of).perform do |response| | |||
unless response_successful?(response) || response_error_unsalvageable?(response) || !raise_on_temporary_error | |||
raise Mastodon::UnexpectedResponseError, response | |||
end | |||
return body_to_json(response.body_with_limit) if response.code == 200 | |||
end | |||
# If request failed, retry without doing it on behalf of a user | |||
return if on_behalf_of.nil? | |||
build_request(uri).perform do |response| | |||
unless response_successful?(response) || response_error_unsalvageable?(response) || !raise_on_temporary_error | |||
raise Mastodon::UnexpectedResponseError, response | |||
end | |||
response.code == 200 ? body_to_json(response.body_with_limit) : nil | |||
end | |||
end | |||
@@ -91,6 +107,14 @@ module JsonLdHelper | |||
private | |||
def response_successful?(response) | |||
(200...300).cover?(response.code) | |||
end | |||
def response_error_unsalvageable?(response) | |||
(400...500).cover?(response.code) && response.code != 429 | |||
end | |||
def build_request(uri, on_behalf_of = nil) | |||
request = Request.new(:get, uri) | |||
request.on_behalf_of(on_behalf_of) if on_behalf_of | |||
@@ -4,8 +4,9 @@ module SettingsHelper | |||
HUMAN_LOCALES = { | |||
en: 'English', | |||
ar: 'العربية', | |||
ast: 'l\'asturianu', | |||
ast: 'Asturianu', | |||
bg: 'Български', | |||
bn: 'বাংলা', | |||
ca: 'Català', | |||
co: 'Corsu', | |||
cs: 'Čeština', | |||
@@ -19,8 +20,10 @@ module SettingsHelper | |||
fa: 'فارسی', | |||
fi: 'Suomi', | |||
fr: 'Français', | |||
ga: 'Gaeilge', | |||
gl: 'Galego', | |||
he: 'עברית', | |||
hi: 'हिन्दी', | |||
hr: 'Hrvatski', | |||
hu: 'Magyar', | |||
hy: 'Հայերեն', | |||
@@ -29,24 +32,29 @@ module SettingsHelper | |||
it: 'Italiano', | |||
ja: '日本語', | |||
ka: 'ქართული', | |||
kk: 'Қазақша', | |||
ko: '한국어', | |||
lt: 'Lietuvių', | |||
lv: 'Latviešu', | |||
ml: 'മലയാളം', | |||
ms: 'Bahasa Melayu', | |||
nl: 'Nederlands', | |||
no: 'Norsk', | |||
oc: 'Occitan', | |||
pl: 'Polszczyzna', | |||
pl: 'Polski', | |||
pt: 'Português', | |||
'pt-BR': 'Português do Brasil', | |||
ro: 'Limba română', | |||
ro: 'Română', | |||
ru: 'Русский', | |||
sk: 'Slovenčina', | |||
sl: 'Slovenščina', | |||
sq: 'Shqip', | |||
sr: 'Српски', | |||
'sr-Latn': 'Srpski (latinica)', | |||
sv: 'Svenska', | |||
ta: 'தமிழ்', | |||
te: 'తెలుగు', | |||
th: 'ภาษาไทย', | |||
th: 'ไทย', | |||
tr: 'Türkçe', | |||
uk: 'Українська', | |||
zh: '中文', | |||
@@ -23,7 +23,7 @@ module StreamEntriesHelper | |||
safe_join([render(file: Rails.root.join('app', 'javascript', 'images', 'logo.svg')), t('accounts.unfollow')]) | |||
end | |||
elsif !(account.memorial? || account.moved?) | |||
link_to account_follow_path(account), class: 'button logo-button', data: { method: :post } do | |||
link_to account_follow_path(account), class: "button logo-button#{account.blocking?(current_account) ? ' disabled' : ''}", data: { method: :post } do | |||
safe_join([render(file: Rails.root.join('app', 'javascript', 'images', 'logo.svg')), t('accounts.follow')]) | |||
end | |||
end | |||
@@ -104,9 +104,19 @@ module StreamEntriesHelper | |||
I18n.t('statuses.content_warning', warning: status.spoiler_text) | |||
end | |||
def poll_summary(status) | |||
return unless status.preloadable_poll | |||
status.preloadable_poll.options.map { |o| "[ ] #{o}" }.join("\n") | |||
end | |||
def status_description(status) | |||
components = [[media_summary(status), status_text_summary(status)].reject(&:blank?).join(' · ')] | |||
components << status.text if status.spoiler_text.blank? | |||
if status.spoiler_text.blank? | |||
components << status.text | |||
components << poll_summary(status) | |||
end | |||
components.reject(&:blank?).join("\n\n") | |||
end | |||
@@ -170,7 +180,7 @@ module StreamEntriesHelper | |||
when 'public' | |||
fa_icon 'globe fw' | |||
when 'unlisted' | |||
fa_icon 'unlock-alt fw' | |||
fa_icon 'unlock fw' | |||
when 'private' | |||
fa_icon 'lock fw' | |||
when 'direct' | |||
@@ -0,0 +1 @@ | |||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 216.4144 232.00976"><path d="M107.86523 0C78.203984.2425 49.672422 3.4535937 33.044922 11.089844c0 0-32.97656262 14.752031-32.97656262 65.082031 0 11.525-.224375 25.306175.140625 39.919925 1.19750002 49.22 9.02375002 97.72843 54.53124962 109.77343 20.9825 5.55375 38.99711 6.71547 53.505856 5.91797 26.31125-1.45875 41.08203-9.38867 41.08203-9.38867l-.86914-19.08984s-18.80171 5.92758-39.91796 5.20508c-20.921254-.7175-43.006879-2.25516-46.390629-27.94141-.3125-2.25625-.46875-4.66938-.46875-7.20313 0 0 20.536953 5.0204 46.564449 6.21289 15.915.73001 30.8393-.93343 45.99805-2.74218 29.07-3.47125 54.38125-21.3818 57.5625-37.74805 5.0125-25.78125 4.59961-62.916015 4.59961-62.916015 0-50.33-32.97461-65.082031-32.97461-65.082031C166.80539 3.4535938 138.255.2425 108.59375 0h-.72852zM74.296875 39.326172c12.355 0 21.710234 4.749297 27.896485 14.248047l6.01367 10.080078 6.01563-10.080078c6.185-9.49875 15.54023-14.248047 27.89648-14.248047 10.6775 0 19.28156 3.753672 25.85156 11.076172 6.36875 7.3225 9.53907 17.218828 9.53907 29.673828v60.941408h-24.14454V81.869141c0-12.46875-5.24453-18.798829-15.73828-18.798829-11.6025 0-17.41797 7.508516-17.41797 22.353516v32.375002H96.207031V85.423828c0-14.845-5.815468-22.353515-17.417969-22.353516-10.49375 0-15.740234 6.330079-15.740234 18.798829v59.148439H38.904297V80.076172c0-12.455 3.171016-22.351328 9.541015-29.673828 6.568751-7.3225 15.172813-11.076172 25.851563-11.076172z" fill="#000"/></svg> |
@@ -22,7 +22,7 @@ export function clearAlert() { | |||
}; | |||
}; | |||
export function showAlert(title, message) { | |||
export function showAlert(title = messages.unexpectedTitle, message = messages.unexpectedMessage) { | |||
return { | |||
type: ALERT_SHOW, | |||
title, | |||
@@ -34,6 +34,11 @@ export function showAlertForError(error) { | |||
if (error.response) { | |||
const { data, status, statusText } = error.response; | |||
if (status === 404 || status === 410) { | |||
// Skip these errors as they are reflected in the UI | |||
return {}; | |||
} | |||
let message = statusText; | |||
let title = `${status}`; | |||
@@ -44,6 +49,6 @@ export function showAlertForError(error) { | |||
return showAlert(title, message); | |||
} else { | |||
console.error(error); | |||
return showAlert(messages.unexpectedTitle, messages.unexpectedMessage); | |||
return showAlert(); | |||
} | |||
} |
@@ -8,6 +8,8 @@ import resizeImage from '../utils/resize_image'; | |||
import { importFetchedAccounts } from './importer'; | |||
import { updateTimeline } from './timelines'; | |||
import { showAlertForError } from './alerts'; | |||
import { showAlert } from './alerts'; | |||
import { defineMessages } from 'react-intl'; | |||
let cancelFetchComposeSuggestionsAccounts; | |||
@@ -49,6 +51,18 @@ export const COMPOSE_UPLOAD_CHANGE_REQUEST = 'COMPOSE_UPLOAD_UPDATE_REQUEST' | |||
export const COMPOSE_UPLOAD_CHANGE_SUCCESS = 'COMPOSE_UPLOAD_UPDATE_SUCCESS'; | |||
export const COMPOSE_UPLOAD_CHANGE_FAIL = 'COMPOSE_UPLOAD_UPDATE_FAIL'; | |||
export const COMPOSE_POLL_ADD = 'COMPOSE_POLL_ADD'; | |||
export const COMPOSE_POLL_REMOVE = 'COMPOSE_POLL_REMOVE'; | |||
export const COMPOSE_POLL_OPTION_ADD = 'COMPOSE_POLL_OPTION_ADD'; | |||
export const COMPOSE_POLL_OPTION_CHANGE = 'COMPOSE_POLL_OPTION_CHANGE'; | |||
export const COMPOSE_POLL_OPTION_REMOVE = 'COMPOSE_POLL_OPTION_REMOVE'; | |||
export const COMPOSE_POLL_SETTINGS_CHANGE = 'COMPOSE_POLL_SETTINGS_CHANGE'; | |||
const messages = defineMessages({ | |||
uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' }, | |||
uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' }, | |||
}); | |||
export function changeCompose(text) { | |||
return { | |||
type: COMPOSE_CHANGE, | |||
@@ -125,6 +139,7 @@ export function submitCompose(routerHistory) { | |||
sensitive: getState().getIn(['compose', 'sensitive']), | |||
spoiler_text: getState().getIn(['compose', 'spoiler_text'], ''), | |||
visibility: getState().getIn(['compose', 'privacy']), | |||
poll: getState().getIn(['compose', 'poll'], null), | |||
}, { | |||
headers: { | |||
'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']), | |||
@@ -143,7 +158,9 @@ export function submitCompose(routerHistory) { | |||
// into the columns | |||
const insertIfOnline = timelineId => { | |||
if (getState().getIn(['timelines', timelineId, 'items', 0]) !== null) { | |||
const timeline = getState().getIn(['timelines', timelineId]); | |||
if (timeline && timeline.get('items').size > 0 && timeline.getIn(['items', 0]) !== null && timeline.get('online')) { | |||
dispatch(updateTimeline(timelineId, { ...response.data })); | |||
} | |||
}; | |||
@@ -184,20 +201,38 @@ export function submitComposeFail(error) { | |||
export function uploadCompose(files) { | |||
return function (dispatch, getState) { | |||
if (getState().getIn(['compose', 'media_attachments']).size > 3) { | |||
const uploadLimit = 4; | |||
const media = getState().getIn(['compose', 'media_attachments']); | |||
const total = Array.from(files).reduce((a, v) => a + v.size, 0); | |||
const progress = new Array(files.length).fill(0); | |||
if (files.length + media.size > uploadLimit) { | |||
dispatch(showAlert(undefined, messages.uploadErrorLimit)); | |||
return; | |||
} | |||
if (getState().getIn(['compose', 'poll'])) { | |||
dispatch(showAlert(undefined, messages.uploadErrorPoll)); | |||
return; | |||
} | |||
dispatch(uploadComposeRequest()); | |||
resizeImage(files[0]).then(file => { | |||
const data = new FormData(); | |||
data.append('file', file); | |||
for (const [i, f] of Array.from(files).entries()) { | |||
if (media.size + i > 3) break; | |||
resizeImage(f).then(file => { | |||
const data = new FormData(); | |||
data.append('file', file); | |||
return api(getState).post('/api/v1/media', data, { | |||
onUploadProgress: ({ loaded, total }) => dispatch(uploadComposeProgress(loaded, total)), | |||
}).then(({ data }) => dispatch(uploadComposeSuccess(data))); | |||
}).catch(error => dispatch(uploadComposeFail(error))); | |||
return api(getState).post('/api/v1/media', data, { | |||
onUploadProgress: function({ loaded }){ | |||
progress[i] = loaded; | |||
dispatch(uploadComposeProgress(progress.reduce((a, v) => a + v, 0), total)); | |||
}, | |||
}).then(({ data }) => dispatch(uploadComposeSuccess(data))); | |||
}).catch(error => dispatch(uploadComposeFail(error))); | |||
}; | |||
}; | |||
}; | |||
@@ -466,4 +501,46 @@ export function changeComposing(value) { | |||
type: COMPOSE_COMPOSING_CHANGE, | |||
value, | |||
}; | |||
} | |||
}; | |||
export function addPoll() { | |||
return { | |||
type: COMPOSE_POLL_ADD, | |||
}; | |||
}; | |||
export function removePoll() { | |||
return { | |||
type: COMPOSE_POLL_REMOVE, | |||
}; | |||
}; | |||
export function addPollOption(title) { | |||
return { | |||
type: COMPOSE_POLL_OPTION_ADD, | |||
title, | |||
}; | |||
}; | |||
export function changePollOption(index, title) { | |||
return { | |||
type: COMPOSE_POLL_OPTION_CHANGE, | |||
index, | |||
title, | |||
}; | |||
}; | |||
export function removePollOption(index) { | |||
return { | |||
type: COMPOSE_POLL_OPTION_REMOVE, | |||
index, | |||
}; | |||
}; | |||
export function changePollSettings(expiresIn, isMultiple) { | |||
return { | |||
type: COMPOSE_POLL_SETTINGS_CHANGE, | |||
expiresIn, | |||
isMultiple, | |||
}; | |||
}; |
@@ -41,13 +41,15 @@ export const expandConversations = ({ maxId } = {}) => (dispatch, getState) => { | |||
params.since_id = getState().getIn(['conversations', 'items', 0, 'last_status']); | |||
} | |||
const isLoadingRecent = !!params.since_id; | |||
api(getState).get('/api/v1/conversations', { params }) | |||
.then(response => { | |||
const next = getLinks(response).refs.find(link => link.rel === 'next'); | |||
dispatch(importFetchedAccounts(response.data.reduce((aggr, item) => aggr.concat(item.accounts), []))); | |||
dispatch(importFetchedStatuses(response.data.map(item => item.last_status).filter(x => !!x))); | |||
dispatch(expandConversationsSuccess(response.data, next ? next.uri : null)); | |||
dispatch(expandConversationsSuccess(response.data, next ? next.uri : null, isLoadingRecent)); | |||
}) | |||
.catch(err => dispatch(expandConversationsFail(err))); | |||
}; | |||
@@ -56,10 +58,11 @@ export const expandConversationsRequest = () => ({ | |||
type: CONVERSATIONS_FETCH_REQUEST, | |||
}); | |||
export const expandConversationsSuccess = (conversations, next) => ({ | |||
export const expandConversationsSuccess = (conversations, next, isLoadingRecent) => ({ | |||
type: CONVERSATIONS_FETCH_SUCCESS, | |||
conversations, | |||
next, | |||
isLoadingRecent, | |||
}); | |||
export const expandConversationsFail = error => ({ | |||
@@ -0,0 +1,30 @@ | |||
import api from '../api'; | |||
export const IDENTITY_PROOFS_ACCOUNT_FETCH_REQUEST = 'IDENTITY_PROOFS_ACCOUNT_FETCH_REQUEST'; | |||
export const IDENTITY_PROOFS_ACCOUNT_FETCH_SUCCESS = 'IDENTITY_PROOFS_ACCOUNT_FETCH_SUCCESS'; | |||
export const IDENTITY_PROOFS_ACCOUNT_FETCH_FAIL = 'IDENTITY_PROOFS_ACCOUNT_FETCH_FAIL'; | |||
export const fetchAccountIdentityProofs = accountId => (dispatch, getState) => { | |||
dispatch(fetchAccountIdentityProofsRequest(accountId)); | |||
api(getState).get(`/api/v1/accounts/${accountId}/identity_proofs`) | |||
.then(({ data }) => dispatch(fetchAccountIdentityProofsSuccess(accountId, data))) | |||
.catch(err => dispatch(fetchAccountIdentityProofsFail(accountId, err))); | |||
}; | |||
export const fetchAccountIdentityProofsRequest = id => ({ | |||
type: IDENTITY_PROOFS_ACCOUNT_FETCH_REQUEST, | |||
id, | |||
}); | |||
export const fetchAccountIdentityProofsSuccess = (accountId, identity_proofs) => ({ | |||
type: IDENTITY_PROOFS_ACCOUNT_FETCH_SUCCESS, | |||
accountId, | |||
identity_proofs, | |||
}); | |||
export const fetchAccountIdentityProofsFail = (accountId, err) => ({ | |||
type: IDENTITY_PROOFS_ACCOUNT_FETCH_FAIL, | |||
accountId, | |||
err, | |||
}); |
@@ -1,11 +1,10 @@ | |||
// import { autoPlayGif } from '../../initial_state'; | |||
// import { putAccounts, putStatuses } from '../../storage/modifier'; | |||
import { normalizeAccount, normalizeStatus } from './normalizer'; | |||
import { normalizeAccount, normalizeStatus, normalizePoll } from './normalizer'; | |||
export const ACCOUNT_IMPORT = 'ACCOUNT_IMPORT'; | |||
export const ACCOUNT_IMPORT = 'ACCOUNT_IMPORT'; | |||
export const ACCOUNTS_IMPORT = 'ACCOUNTS_IMPORT'; | |||
export const STATUS_IMPORT = 'STATUS_IMPORT'; | |||
export const STATUS_IMPORT = 'STATUS_IMPORT'; | |||
export const STATUSES_IMPORT = 'STATUSES_IMPORT'; | |||
export const POLLS_IMPORT = 'POLLS_IMPORT'; | |||
function pushUnique(array, object) { | |||
if (array.every(element => element.id !== object.id)) { | |||
@@ -29,6 +28,10 @@ export function importStatuses(statuses) { | |||
return { type: STATUSES_IMPORT, statuses }; | |||
} | |||
export function importPolls(polls) { | |||
return { type: POLLS_IMPORT, polls }; | |||
} | |||
export function importFetchedAccount(account) { | |||
return importFetchedAccounts([account]); | |||
} | |||
@@ -45,7 +48,6 @@ export function importFetchedAccounts(accounts) { | |||
} | |||
accounts.forEach(processAccount); | |||
//putAccounts(normalAccounts, !autoPlayGif); | |||
return importAccounts(normalAccounts); | |||
} | |||
@@ -58,6 +60,7 @@ export function importFetchedStatuses(statuses) { | |||
return (dispatch, getState) => { | |||
const accounts = []; | |||
const normalStatuses = []; | |||
const polls = []; | |||
function processStatus(status) { | |||
pushUnique(normalStatuses, normalizeStatus(status, getState().getIn(['statuses', status.id]))); | |||
@@ -66,12 +69,22 @@ export function importFetchedStatuses(statuses) { | |||
if (status.reblog && status.reblog.id) { | |||
processStatus(status.reblog); | |||
} | |||
if (status.poll && status.poll.id) { | |||
pushUnique(polls, normalizePoll(status.poll)); | |||
} | |||
} | |||
statuses.forEach(processStatus); | |||
//putStatuses(normalStatuses); | |||
dispatch(importPolls(polls)); | |||
dispatch(importFetchedAccounts(accounts)); | |||
dispatch(importStatuses(normalStatuses)); | |||
}; | |||
} | |||
export function importFetchedPoll(poll) { | |||
return dispatch => { | |||
dispatch(importPolls([normalizePoll(poll)])); | |||
}; | |||
} |
@@ -43,6 +43,10 @@ export function normalizeStatus(status, normalOldStatus) { | |||
normalStatus.reblog = status.reblog.id; | |||
} | |||
if (status.poll && status.poll.id) { | |||
normalStatus.poll = status.poll.id; | |||
} | |||
// Only calculate these values when status first encountered | |||
// Otherwise keep the ones already in the reducer | |||
if (normalOldStatus) { | |||
@@ -63,3 +67,16 @@ export function normalizeStatus(status, normalOldStatus) { | |||
return normalStatus; | |||
} | |||
export function normalizePoll(poll) { | |||
const normalPoll = { ...poll }; | |||
const emojiMap = makeEmojiMap(normalPoll); | |||
normalPoll.options = poll.options.map(option => ({ | |||
...option, | |||
title_emojified: emojify(escapeTextContentForBrowser(option.title), emojiMap), | |||
})); | |||
return normalPoll; | |||
} |
@@ -7,6 +7,7 @@ import { | |||
importFetchedStatus, | |||
importFetchedStatuses, | |||
} from './importer'; | |||
import { saveSettings } from './settings'; | |||
import { defineMessages } from 'react-intl'; | |||
import { List as ImmutableList } from 'immutable'; | |||
import { unescapeHTML } from '../utils/html'; | |||
@@ -92,7 +93,7 @@ export function updateNotifications(notification, intlMessages, intlLocale) { | |||
const excludeTypesFromSettings = state => state.getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS(); | |||
const excludeTypesFromFilter = filter => { | |||
const allTypes = ImmutableList(['follow', 'favourite', 'reblog', 'mention']); | |||
const allTypes = ImmutableList(['follow', 'favourite', 'reblog', 'mention', 'poll']); | |||
return allTypes.filterNot(item => item === filter).toJS(); | |||
}; | |||
@@ -187,5 +188,6 @@ export function setFilter (filterType) { | |||
value: filterType, | |||
}); | |||
dispatch(expandNotifications()); | |||
dispatch(saveSettings()); | |||
}; | |||
}; |
@@ -0,0 +1,60 @@ | |||
import api from '../api'; | |||
import { importFetchedPoll } from './importer'; | |||
export const POLL_VOTE_REQUEST = 'POLL_VOTE_REQUEST'; | |||
export const POLL_VOTE_SUCCESS = 'POLL_VOTE_SUCCESS'; | |||
export const POLL_VOTE_FAIL = 'POLL_VOTE_FAIL'; | |||
export const POLL_FETCH_REQUEST = 'POLL_FETCH_REQUEST'; | |||
export const POLL_FETCH_SUCCESS = 'POLL_FETCH_SUCCESS'; | |||
export const POLL_FETCH_FAIL = 'POLL_FETCH_FAIL'; | |||
export const vote = (pollId, choices) => (dispatch, getState) => { | |||
dispatch(voteRequest()); | |||
api(getState).post(`/api/v1/polls/${pollId}/votes`, { choices }) | |||
.then(({ data }) => { | |||
dispatch(importFetchedPoll(data)); | |||
dispatch(voteSuccess(data)); | |||
}) | |||
.catch(err => dispatch(voteFail(err))); | |||
}; | |||
export const fetchPoll = pollId => (dispatch, getState) => { | |||
dispatch(fetchPollRequest()); | |||
api(getState).get(`/api/v1/polls/${pollId}`) | |||
.then(({ data }) => { | |||
dispatch(importFetchedPoll(data)); | |||
dispatch(fetchPollSuccess(data)); | |||
}) | |||
.catch(err => dispatch(fetchPollFail(err))); | |||
}; | |||
export const voteRequest = () => ({ | |||
type: POLL_VOTE_REQUEST, | |||
}); | |||
export const voteSuccess = poll => ({ | |||
type: POLL_VOTE_SUCCESS, | |||
poll, | |||
}); | |||
export const voteFail = error => ({ | |||
type: POLL_VOTE_FAIL, | |||
error, | |||
}); | |||
export const fetchPollRequest = () => ({ | |||
type: POLL_FETCH_REQUEST, | |||
}); | |||
export const fetchPollSuccess = poll => ({ | |||
type: POLL_FETCH_SUCCESS, | |||
poll, | |||
}); | |||
export const fetchPollFail = error => ({ | |||
type: POLL_FETCH_FAIL, | |||
error, | |||
}); |
@@ -37,6 +37,7 @@ export function submitSearch() { | |||
params: { | |||
q: value, | |||
resolve: true, | |||
limit: 5, | |||
}, | |||
}).then(response => { | |||
if (response.data.accounts) { | |||
@@ -140,7 +140,11 @@ export function redraft(status) { | |||
export function deleteStatus(id, router, withRedraft = false) { | |||
return (dispatch, getState) => { | |||
const status = getState().getIn(['statuses', id]); | |||
let status = getState().getIn(['statuses', id]); | |||
if (status.get('poll')) { | |||
status = status.set('poll', getState().getIn(['polls', status.get('poll')])); | |||
} | |||
dispatch(deleteStatusRequest(id)); | |||
@@ -3,6 +3,7 @@ import { | |||
updateTimeline, | |||
deleteFromTimelines, | |||
expandHomeTimeline, | |||
connectTimeline, | |||
disconnectTimeline, | |||
} from './timelines'; | |||
import { updateNotifications, expandNotifications } from './notifications'; | |||
@@ -16,7 +17,12 @@ export function connectTimelineStream (timelineId, path, pollingRefresh = null, | |||
return connectStream (path, pollingRefresh, (dispatch, getState) => { | |||
const locale = getState().getIn(['meta', 'locale']); | |||
return { | |||
onConnect() { | |||
dispatch(connectTimeline(timelineId)); | |||
}, | |||
onDisconnect() { | |||
dispatch(disconnectTimeline(timelineId)); | |||
}, | |||
@@ -12,6 +12,7 @@ export const TIMELINE_EXPAND_FAIL = 'TIMELINE_EXPAND_FAIL'; | |||
export const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP'; | |||
export const TIMELINE_CONNECT = 'TIMELINE_CONNECT'; | |||
export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT'; | |||
export function updateTimeline(timeline, status, accept) { | |||
@@ -143,6 +144,13 @@ export function scrollTopTimeline(timeline, top) { | |||
}; | |||
}; | |||
export function connectTimeline(timeline) { | |||
return { | |||
type: TIMELINE_CONNECT, | |||
timeline, | |||
}; | |||
}; | |||
export function disconnectTimeline(timeline) { | |||
return { | |||
type: TIMELINE_DISCONNECT, | |||
@@ -13,10 +13,14 @@ export const getLinks = response => { | |||
}; | |||
let csrfHeader = {}; | |||
function setCSRFHeader() { | |||
const csrfToken = document.querySelector('meta[name=csrf-token]').content; | |||
csrfHeader['X-CSRF-Token'] = csrfToken; | |||
const csrfToken = document.querySelector('meta[name=csrf-token]'); | |||
if (csrfToken) { | |||
csrfHeader['X-CSRF-Token'] = csrfToken.content; | |||
} | |||
} | |||
ready(setCSRFHeader); | |||
export default getState => axios.create({ | |||
@@ -88,7 +88,7 @@ class Account extends ImmutablePureComponent { | |||
if (requested) { | |||
buttons = <IconButton disabled icon='hourglass' title={intl.formatMessage(messages.requested)} />; | |||
} else if (blocking) { | |||
buttons = <IconButton active icon='unlock-alt' title={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.handleBlock} />; | |||
buttons = <IconButton active icon='unlock' title={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.handleBlock} />; | |||
} else if (muting) { | |||
let hidingNotificationsButton; | |||
if (account.getIn(['relationship', 'muting_notifications'])) { | |||
@@ -2,6 +2,7 @@ import React from 'react'; | |||
import ImmutablePropTypes from 'react-immutable-proptypes'; | |||
import PropTypes from 'prop-types'; | |||
import ImmutablePureComponent from 'react-immutable-pure-component'; | |||
import Icon from 'mastodon/components/icon'; | |||
const filename = url => url.split('/').pop().split('#')[0].split('?')[0]; | |||
@@ -24,7 +25,7 @@ export default class AttachmentList extends ImmutablePureComponent { | |||
return ( | |||
<li key={attachment.get('id')}> | |||
<a href={displayUrl} target='_blank' rel='noopener'><i className='fa fa-link' /> {filename(displayUrl)}</a> | |||
<a href={displayUrl} target='_blank' rel='noopener'><Icon id='link' /> {filename(displayUrl)}</a> | |||
</li> | |||
); | |||
})} | |||
@@ -36,7 +37,7 @@ export default class AttachmentList extends ImmutablePureComponent { | |||
return ( | |||
<div className='attachment-list'> | |||
<div className='attachment-list__icon'> | |||
<i className='fa fa-link' /> | |||
<Icon id='link' /> | |||
</div> | |||
<ul className='attachment-list__list'> | |||
@@ -1,6 +1,7 @@ | |||
import React from 'react'; | |||
import { FormattedMessage } from 'react-intl'; | |||
import PropTypes from 'prop-types'; | |||
import Icon from 'mastodon/components/icon'; | |||
export default class ColumnBackButton extends React.PureComponent { | |||
@@ -19,7 +20,7 @@ export default class ColumnBackButton extends React.PureComponent { | |||
render () { | |||
return ( | |||
<button onClick={this.handleClick} className='column-back-button'> | |||
<i className='fa fa-fw fa-chevron-left column-back-button__icon' /> | |||
<Icon id='chevron-left' className='column-back-button__icon' fixedWidth /> | |||
<FormattedMessage id='column_back_button.label' defaultMessage='Back' /> | |||
</button> | |||
); | |||
@@ -1,6 +1,7 @@ | |||
import React from 'react'; | |||
import { FormattedMessage } from 'react-intl'; | |||
import ColumnBackButton from './column_back_button'; | |||
import Icon from 'mastodon/components/icon'; | |||
export default class ColumnBackButtonSlim extends ColumnBackButton { | |||
@@ -8,7 +9,7 @@ export default class ColumnBackButtonSlim extends ColumnBackButton { | |||
return ( | |||
<div className='column-back-button--slim'> | |||
<div role='button' tabIndex='0' onClick={this.handleClick} className='column-back-button column-back-button--slim-button'> | |||
<i className='fa fa-fw fa-chevron-left column-back-button__icon' /> | |||
<Icon id='chevron-left' className='column-back-button__icon' fixedWidth /> | |||
<FormattedMessage id='column_back_button.label' defaultMessage='Back' /> | |||
</div> | |||
</div> | |||
@@ -2,6 +2,7 @@ import React from 'react'; | |||
import PropTypes from 'prop-types'; | |||
import classNames from 'classnames'; | |||
import { FormattedMessage, injectIntl, defineMessages } from 'react-intl'; | |||
import Icon from 'mastodon/components/icon'; | |||
const messages = defineMessages({ | |||
show: { id: 'column_header.show_settings', defaultMessage: 'Show settings' }, | |||
@@ -109,22 +110,22 @@ class ColumnHeader extends React.PureComponent { | |||
} | |||
if (multiColumn && pinned) { | |||
pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={this.handlePin}><i className='fa fa fa-times' /> <FormattedMessage id='column_header.unpin' defaultMessage='Unpin' /></button>; | |||
pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={this.handlePin}><Icon id='times' /> <FormattedMessage id='column_header.unpin' defaultMessage='Unpin' /></button>; | |||
moveButtons = ( | |||
<div key='move-buttons' className='column-header__setting-arrows'> | |||
<button title={formatMessage(messages.moveLeft)} aria-label={formatMessage(messages.moveLeft)} className='text-btn column-header__setting-btn' onClick={this.handleMoveLeft}><i className='fa fa-chevron-left' /></button> | |||
<button title={formatMessage(messages.moveRight)} aria-label={formatMessage(messages.moveRight)} className='text-btn column-header__setting-btn' onClick={this.handleMoveRight}><i className='fa fa-chevron-right' /></button> | |||
<button title={formatMessage(messages.moveLeft)} aria-label={formatMessage(messages.moveLeft)} className='text-btn column-header__setting-btn' onClick={this.handleMoveLeft}><Icon id='chevron-left' /></button> | |||
<button title={formatMessage(messages.moveRight)} aria-label={formatMessage(messages.moveRight)} className='text-btn column-header__setting-btn' onClick={this.handleMoveRight}><Icon id='chevron-right' /></button> | |||
</div> | |||
); | |||
} else if (multiColumn) { | |||
pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={this.handlePin}><i className='fa fa fa-plus' /> <FormattedMessage id='column_header.pin' defaultMessage='Pin' /></button>; | |||
pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={this.handlePin}><Icon id='plus' /> <FormattedMessage id='column_header.pin' defaultMessage='Pin' /></button>; | |||
} | |||
if (!pinned && (multiColumn || showBackButton)) { | |||
backButton = ( | |||
<button onClick={this.handleBackClick} className='column-header__back-button'> | |||
<i className='fa fa-fw fa-chevron-left column-back-button__icon' /> | |||
<Icon id='chevron-left' className='column-back-button__icon' fixedWidth /> | |||
<FormattedMessage id='column_back_button.label' defaultMessage='Back' /> | |||
</button> | |||
); | |||
@@ -140,7 +141,7 @@ class ColumnHeader extends React.PureComponent { | |||
} | |||
if (children || multiColumn) { | |||
collapseButton = <button className={collapsibleButtonClassName} title={formatMessage(collapsed ? messages.show : messages.hide)} aria-label={formatMessage(collapsed ? messages.show : messages.hide)} aria-pressed={collapsed ? 'false' : 'true'} onClick={this.handleToggleClick}><i className='fa fa-sliders' /></button>; | |||
collapseButton = <button className={collapsibleButtonClassName} title={formatMessage(collapsed ? messages.show : messages.hide)} aria-label={formatMessage(collapsed ? messages.show : messages.hide)} aria-pressed={collapsed ? 'false' : 'true'} onClick={this.handleToggleClick}><Icon id='sliders' /></button>; | |||
} | |||
const hasTitle = icon && title; | |||
@@ -150,7 +151,7 @@ class ColumnHeader extends React.PureComponent { | |||
<h1 className={buttonClassName}> | |||
{hasTitle && ( | |||
<button onClick={this.handleTitleClick}> | |||
<i className={`fa fa-fw fa-${icon} column-header__icon`} /> | |||
<Icon id={icon} fixedWidth className='column-header__icon' /> | |||
{title} | |||
</button> | |||
)} | |||
@@ -11,26 +11,36 @@ export default class DisplayName extends React.PureComponent { | |||
}; | |||
render () { | |||
const { account, others, localDomain } = this.props; | |||
const displayNameHtml = { __html: account.get('display_name_html') }; | |||
const { others, localDomain } = this.props; | |||
let suffix; | |||
let displayName, suffix, account; | |||
if (others && others.size > 1) { | |||
suffix = `+${others.size}`; | |||
displayName = others.take(2).map(a => <bdi key={a.get('id')}><strong className='display-name__html' dangerouslySetInnerHTML={{ __html: a.get('display_name_html') }} /></bdi>).reduce((prev, cur) => [prev, ', ', cur]); | |||
if (others.size - 2 > 0) { | |||
suffix = `+${others.size - 2}`; | |||
} | |||
} else { | |||
if (others && others.size > 0) { | |||
account = others.first(); | |||
} else { | |||
account = this.props.account; | |||
} | |||
let acct = account.get('acct'); | |||
if (acct.indexOf('@') === -1 && localDomain) { | |||
acct = `${acct}@${localDomain}`; | |||
} | |||
suffix = <span className='display-name__account'>@{acct}</span>; | |||
displayName = <bdi><strong className='display-name__html' dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }} /></bdi>; | |||
suffix = <span className='display-name__account'>@{acct}</span>; | |||
} | |||
return ( | |||
<span className='display-name'> | |||
<bdi><strong className='display-name__html' dangerouslySetInnerHTML={displayNameHtml} /></bdi> {suffix} | |||
{displayName} {suffix} | |||
</span> | |||
); | |||
} | |||
@@ -32,7 +32,7 @@ class Account extends ImmutablePureComponent { | |||
</span> | |||
<div className='domain__buttons'> | |||
<IconButton active icon='unlock-alt' title={intl.formatMessage(messages.unblockDomain, { domain })} onClick={this.handleDomainUnblock} /> | |||
<IconButton active icon='unlock' title={intl.formatMessage(messages.unblockDomain, { domain })} onClick={this.handleDomainUnblock} /> | |||
</div> | |||
</div> | |||
</div> | |||
@@ -0,0 +1,39 @@ | |||
import React from 'react'; | |||
import PropTypes from 'prop-types'; | |||
import illustration from '../../images/elephant_ui_disappointed.svg'; | |||
export default class ErrorBoundary extends React.PureComponent { | |||
static propTypes = { | |||
children: PropTypes.node, | |||
}; | |||
state = { | |||
hasError: false, | |||
stackTrace: undefined, | |||
componentStack: undefined, | |||
} | |||
componentDidCatch(error, info) { | |||
this.setState({ | |||
hasError: true, | |||
stackTrace: error.stack, | |||
componentStack: info && info.componentStack, | |||
}); | |||
} | |||
render() { | |||
const { hasError } = this.state; | |||
if (!hasError) { | |||
return this.props.children; | |||
} | |||
return ( | |||
<div> | |||
<img src={illustration} alt='' /> | |||
</div> | |||
); | |||
} | |||
} |
@@ -0,0 +1,21 @@ | |||
import React from 'react'; | |||
import PropTypes from 'prop-types'; | |||
import classNames from 'classnames'; | |||
export default class Icon extends React.PureComponent { | |||
static propTypes = { | |||
id: PropTypes.string.isRequired, | |||
className: PropTypes.string, | |||
fixedWidth: PropTypes.bool, | |||
}; | |||
render () { | |||
const { id, className, fixedWidth, ...other } = this.props; | |||
return ( | |||
<i role='img' className={classNames('fa', `fa-${id}`, className, { 'fa-fw': fixedWidth })} {...other} /> | |||
); | |||
} | |||
} |
@@ -3,6 +3,7 @@ import Motion from '../features/ui/util/optional_motion'; | |||
import spring from 'react-motion/lib/spring'; | |||
import PropTypes from 'prop-types'; | |||
import classNames from 'classnames'; | |||
import Icon from 'mastodon/components/icon'; | |||
export default class IconButton extends React.PureComponent { | |||
@@ -85,8 +86,9 @@ export default class IconButton extends React.PureComponent { | |||
onClick={this.handleClick} | |||
style={style} | |||
tabIndex={tabIndex} | |||
disabled={disabled} | |||
> | |||
<i className={`fa fa-fw fa-${icon}`} aria-hidden='true' /> | |||
<Icon id={icon} fixedWidth aria-hidden='true' /> | |||
</button> | |||
); | |||
} | |||
@@ -103,8 +105,9 @@ export default class IconButton extends React.PureComponent { | |||
onClick={this.handleClick} | |||
style={style} | |||
tabIndex={tabIndex} | |||
disabled={disabled} | |||
> | |||
<i style={{ transform: `rotate(${rotate}deg)` }} className={`fa fa-fw fa-${icon}`} aria-hidden='true' /> | |||
<Icon id={icon} style={{ transform: `rotate(${rotate}deg)` }} fixedWidth aria-hidden='true' /> | |||
</button> | |||
)} | |||
</Motion> | |||
@@ -65,7 +65,7 @@ export default class IntersectionObserverArticle extends React.Component { | |||
} | |||
updateStateAfterIntersection = (prevState) => { | |||
if (prevState.isIntersecting && !this.entry.isIntersecting) { | |||
if (prevState.isIntersecting !== false && !this.entry.isIntersecting) { | |||
scheduleIdleTask(this.hideIfNotIntersecting); | |||
} | |||
return { | |||
@@ -1,6 +1,7 @@ | |||
import React from 'react'; | |||
import PropTypes from 'prop-types'; | |||
import { injectIntl, defineMessages } from 'react-intl'; | |||
import Icon from 'mastodon/components/icon'; | |||
const messages = defineMessages({ | |||
load_more: { id: 'status.load_more', defaultMessage: 'Load more' }, | |||
@@ -25,7 +26,7 @@ class LoadGap extends React.PureComponent { | |||
return ( | |||
<button className='load-more load-gap' disabled={disabled} onClick={this.handleClick} aria-label={intl.formatMessage(messages.load_more)}> | |||
<i className='fa fa-ellipsis-h' /> | |||
<Icon id='ellipsis-h' /> | |||
</button> | |||
); | |||
} | |||
@@ -194,6 +194,8 @@ class MediaGallery extends React.PureComponent { | |||
height: PropTypes.number.isRequired, | |||
onOpenMedia: PropTypes.func.isRequired, | |||
intl: PropTypes.object.isRequired, | |||
defaultWidth: PropTypes.number, | |||
cacheWidth: PropTypes.func, | |||
}; | |||
static defaultProps = { | |||
@@ -202,6 +204,7 @@ class MediaGallery extends React.PureComponent { | |||
state = { | |||
visible: displayMedia !== 'hide_all' && !this.props.sensitive || displayMedia === 'show_all', | |||
width: this.props.defaultWidth, | |||
}; | |||
componentWillReceiveProps (nextProps) { | |||
@@ -221,6 +224,7 @@ class MediaGallery extends React.PureComponent { | |||
handleRef = (node) => { | |||
if (node /*&& this.isStandaloneEligible()*/) { | |||
// offsetWidth triggers a layout, so only calculate when we need to | |||
if (this.props.cacheWidth) this.props.cacheWidth(node.offsetWidth); | |||
this.setState({ | |||
width: node.offsetWidth, | |||
}); | |||
@@ -233,8 +237,10 @@ class MediaGallery extends React.PureComponent { | |||
} | |||
render () { | |||
const { media, intl, sensitive, height } = this.props; | |||
const { width, visible } = this.state; | |||
const { media, intl, sensitive, height, defaultWidth } = this.props; | |||
const { visible } = this.state; | |||
const width = this.state.width || defaultWidth; | |||
let children; | |||
@@ -0,0 +1,140 @@ | |||
import React from 'react'; | |||
import PropTypes from 'prop-types'; | |||
import ImmutablePropTypes from 'react-immutable-proptypes'; | |||
import ImmutablePureComponent from 'react-immutable-pure-component'; | |||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; | |||
import classNames from 'classnames'; | |||
import { vote, fetchPoll } from 'mastodon/actions/polls'; | |||
import Motion from 'mastodon/features/ui/util/optional_motion'; | |||
import spring from 'react-motion/lib/spring'; | |||
import escapeTextContentForBrowser from 'escape-html'; | |||
import emojify from 'mastodon/features/emoji/emoji'; | |||
import RelativeTimestamp from './relative_timestamp'; | |||
const messages = defineMessages({ | |||
closed: { id: 'poll.closed', defaultMessage: 'Closed' }, | |||
}); | |||
const makeEmojiMap = record => record.get('emojis').reduce((obj, emoji) => { | |||
obj[`:${emoji.get('shortcode')}:`] = emoji.toJS(); | |||
return obj; | |||
}, {}); | |||
export default @injectIntl | |||
class Poll extends ImmutablePureComponent { | |||
static propTypes = { | |||
poll: ImmutablePropTypes.map, | |||
intl: PropTypes.object.isRequired, | |||
dispatch: PropTypes.func, | |||
disabled: PropTypes.bool, | |||
}; | |||
state = { | |||
selected: {}, | |||
}; | |||
handleOptionChange = e => { | |||
const { target: { value } } = e; | |||
if (this.props.poll.get('multiple')) { | |||
const tmp = { ...this.state.selected }; | |||
if (tmp[value]) { | |||
delete tmp[value]; | |||
} else { | |||
tmp[value] = true; | |||
} | |||
this.setState({ selected: tmp }); | |||
} else { | |||
const tmp = {}; | |||
tmp[value] = true; | |||
this.setState({ selected: tmp }); | |||
} | |||
}; | |||
handleVote = () => { | |||
if (this.props.disabled) { | |||
return; | |||
} | |||
this.props.dispatch(vote(this.props.poll.get('id'), Object.keys(this.state.selected))); | |||
}; | |||
handleRefresh = () => { | |||
if (this.props.disabled) { | |||
return; | |||
} | |||
this.props.dispatch(fetchPoll(this.props.poll.get('id'))); | |||
}; | |||
renderOption (option, optionIndex) { | |||
const { poll, disabled } = this.props; | |||
const percent = poll.get('votes_count') === 0 ? 0 : (option.get('votes_count') / poll.get('votes_count')) * 100; | |||
const leading = poll.get('options').filterNot(other => other.get('title') === option.get('title')).every(other => option.get('votes_count') > other.get('votes_count')); | |||
const active = !!this.state.selected[`${optionIndex}`]; | |||
const showResults = poll.get('voted') || poll.get('expired'); | |||
let titleEmojified = option.get('title_emojified'); | |||
if (!titleEmojified) { | |||
const emojiMap = makeEmojiMap(poll); | |||
titleEmojified = emojify(escapeTextContentForBrowser(option.get('title')), emojiMap); | |||
} | |||
return ( | |||
<li key={option.get('title')}> | |||
{showResults && ( | |||
<Motion defaultStyle={{ width: 0 }} style={{ width: spring(percent, { stiffness: 180, damping: 12 }) }}> | |||
{({ width }) => | |||
<span className={classNames('poll__chart', { leading })} style={{ width: `${width}%` }} /> | |||
} | |||
</Motion> | |||
)} | |||
<label className={classNames('poll__text', { selectable: !showResults })}> | |||
<input | |||
name='vote-options' | |||
type={poll.get('multiple') ? 'checkbox' : 'radio'} | |||
value={optionIndex} | |||
checked={active} | |||
onChange={this.handleOptionChange} | |||
disabled={disabled} | |||
/> | |||
{!showResults && <span className={classNames('poll__input', { checkbox: poll.get('multiple'), active })} />} | |||
{showResults && <span className='poll__number'>{Math.round(percent)}%</span>} | |||
<span dangerouslySetInnerHTML={{ __html: titleEmojified }} /> | |||
</label> | |||
</li> | |||
); | |||
} | |||
render () { | |||
const { poll, intl } = this.props; | |||
if (!poll) { | |||
return null; | |||
} | |||
const timeRemaining = poll.get('expired') ? intl.formatMessage(messages.closed) : <RelativeTimestamp timestamp={poll.get('expires_at')} futureDate />; | |||
const showResults = poll.get('voted') || poll.get('expired'); | |||
const disabled = this.props.disabled || Object.entries(this.state.selected).every(item => !item); | |||
return ( | |||
<div className='poll'> | |||
<ul> | |||
{poll.get('options').map((option, i) => this.renderOption(option, i))} | |||
</ul> | |||
<div className='poll__footer'> | |||
{!showResults && <button className='button button-secondary' disabled={disabled} onClick={this.handleVote}><FormattedMessage id='poll.vote' defaultMessage='Vote' /></button>} | |||
{showResults && !this.props.disabled && <span><button className='poll__link' onClick={this.handleRefresh}><FormattedMessage id='poll.refresh' defaultMessage='Refresh' /></button> · </span>} | |||
<FormattedMessage id='poll.total_votes' defaultMessage='{count, plural, one {# vote} other {# votes}}' values={{ count: poll.get('votes_count') }} /> | |||
{poll.get('expires_at') && <span> · {timeRemaining}</span>} | |||
</div> | |||
</div> | |||
); | |||
} | |||
} |
@@ -8,6 +8,11 @@ const messages = defineMessages({ | |||
minutes: { id: 'relative_time.minutes', defaultMessage: '{number}m' }, | |||
hours: { id: 'relative_time.hours', defaultMessage: '{number}h' }, | |||
days: { id: 'relative_time.days', defaultMessage: '{number}d' }, | |||
moments_remaining: { id: 'time_remaining.moments', defaultMessage: 'Moments remaining' }, | |||
seconds_remaining: { id: 'time_remaining.seconds', defaultMessage: '{number, plural, one {# second} other {# seconds}} left' }, | |||
minutes_remaining: { id: 'time_remaining.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}} left' }, | |||
hours_remaining: { id: 'time_remaining.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}} left' }, | |||
days_remaining: { id: 'time_remaining.days', defaultMessage: '{number, plural, one {# day} other {# days}} left' }, | |||
}); | |||
const dateFormatOptions = { | |||
@@ -86,6 +91,26 @@ export const timeAgoString = (intl, date, now, year) => { | |||
return relativeTime; | |||
}; | |||
const timeRemainingString = (intl, date, now) => { | |||
const delta = date.getTime() - now; | |||
let relativeTime; | |||
if (delta < 10 * SECOND) { | |||
relativeTime = intl.formatMessage(messages.moments_remaining); | |||
} else if (delta < MINUTE) { | |||
relativeTime = intl.formatMessage(messages.seconds_remaining, { number: Math.floor(delta / SECOND) }); | |||
} else if (delta < HOUR) { | |||
relativeTime = intl.formatMessage(messages.minutes_remaining, { number: Math.floor(delta / MINUTE) }); | |||
} else if (delta < DAY) { | |||
relativeTime = intl.formatMessage(messages.hours_remaining, { number: Math.floor(delta / HOUR) }); | |||
} else { | |||
relativeTime = intl.formatMessage(messages.days_remaining, { number: Math.floor(delta / DAY) }); | |||
} | |||
return relativeTime; | |||
}; | |||
export default @injectIntl | |||
class RelativeTimestamp extends React.Component { | |||
@@ -93,6 +118,7 @@ class RelativeTimestamp extends React.Component { | |||
intl: PropTypes.object.isRequired, | |||
timestamp: PropTypes.string.isRequired, | |||
year: PropTypes.number.isRequired, | |||
futureDate: PropTypes.bool, | |||
}; | |||
state = { | |||
@@ -145,10 +171,10 @@ class RelativeTimestamp extends React.Component { | |||
} | |||
render () { | |||
const { timestamp, intl, year } = this.props; | |||
const { timestamp, intl, year, futureDate } = this.props; | |||
const date = new Date(timestamp); | |||
const relativeTime = timeAgoString(intl, date, this.state.now, year); | |||
const relativeTime = futureDate ? timeRemainingString(intl, date, this.state.now) : timeAgoString(intl, date, this.state.now, year); | |||
return ( | |||
<time dateTime={timestamp} title={intl.formatDate(date, dateFormatOptions)}> | |||