Merge tag 'v2.8.0' into instance_only_statuses
This commit is contained in:
commit
546581d2ba
@ -41,6 +41,11 @@ module.exports = {
|
|||||||
'node_modules',
|
'node_modules',
|
||||||
'\\.(css|scss|json)$',
|
'\\.(css|scss|json)$',
|
||||||
],
|
],
|
||||||
|
'import/resolver': {
|
||||||
|
node: {
|
||||||
|
paths: ['app/javascript'],
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
rules: {
|
rules: {
|
||||||
|
@ -80,7 +80,7 @@ Rails/HttpStatus:
|
|||||||
Rails/Exit:
|
Rails/Exit:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'lib/mastodon/*'
|
- 'lib/mastodon/*'
|
||||||
- 'lib/cli'
|
- 'lib/cli.rb'
|
||||||
|
|
||||||
Style/ClassAndModuleChildren:
|
Style/ClassAndModuleChildren:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
@ -1 +1 @@
|
|||||||
2.6.0
|
2.6.1
|
||||||
|
283
AUTHORS.md
283
AUTHORS.md
@ -6,8 +6,8 @@ and provided thanks to the work of the following contributors:
|
|||||||
|
|
||||||
* [Gargron](https://github.com/Gargron)
|
* [Gargron](https://github.com/Gargron)
|
||||||
* [ykzts](https://github.com/ykzts)
|
* [ykzts](https://github.com/ykzts)
|
||||||
* [akihikodaki](https://github.com/akihikodaki)
|
|
||||||
* [ThibG](https://github.com/ThibG)
|
* [ThibG](https://github.com/ThibG)
|
||||||
|
* [akihikodaki](https://github.com/akihikodaki)
|
||||||
* [mjankowski](https://github.com/mjankowski)
|
* [mjankowski](https://github.com/mjankowski)
|
||||||
* [dependabot[bot]](https://github.com/apps/dependabot)
|
* [dependabot[bot]](https://github.com/apps/dependabot)
|
||||||
* [unarist](https://github.com/unarist)
|
* [unarist](https://github.com/unarist)
|
||||||
@ -27,14 +27,14 @@ and provided thanks to the work of the following contributors:
|
|||||||
* [blackle](https://github.com/blackle)
|
* [blackle](https://github.com/blackle)
|
||||||
* [Quent-in](https://github.com/Quent-in)
|
* [Quent-in](https://github.com/Quent-in)
|
||||||
* [JantsoP](https://github.com/JantsoP)
|
* [JantsoP](https://github.com/JantsoP)
|
||||||
* [mabkenar](https://github.com/mabkenar)
|
|
||||||
* [Kjwon15](https://github.com/Kjwon15)
|
* [Kjwon15](https://github.com/Kjwon15)
|
||||||
|
* [mabkenar](https://github.com/mabkenar)
|
||||||
* [nullkal](https://github.com/nullkal)
|
* [nullkal](https://github.com/nullkal)
|
||||||
* [yookoala](https://github.com/yookoala)
|
* [yookoala](https://github.com/yookoala)
|
||||||
* [shuheiktgw](https://github.com/shuheiktgw)
|
* [shuheiktgw](https://github.com/shuheiktgw)
|
||||||
* [ashfurrow](https://github.com/ashfurrow)
|
* [ashfurrow](https://github.com/ashfurrow)
|
||||||
* [Quenty31](https://github.com/Quenty31)
|
|
||||||
* [zunda](https://github.com/zunda)
|
* [zunda](https://github.com/zunda)
|
||||||
|
* [Quenty31](https://github.com/Quenty31)
|
||||||
* [eramdam](https://github.com/eramdam)
|
* [eramdam](https://github.com/eramdam)
|
||||||
* [takayamaki](https://github.com/takayamaki)
|
* [takayamaki](https://github.com/takayamaki)
|
||||||
* [masarakki](https://github.com/masarakki)
|
* [masarakki](https://github.com/masarakki)
|
||||||
@ -45,8 +45,8 @@ and provided thanks to the work of the following contributors:
|
|||||||
* [stephenburgess8](https://github.com/stephenburgess8)
|
* [stephenburgess8](https://github.com/stephenburgess8)
|
||||||
* [Wonderfall](https://github.com/Wonderfall)
|
* [Wonderfall](https://github.com/Wonderfall)
|
||||||
* [matteoaquila](https://github.com/matteoaquila)
|
* [matteoaquila](https://github.com/matteoaquila)
|
||||||
* [rkarabut](https://github.com/rkarabut)
|
|
||||||
* [yukimochi](https://github.com/yukimochi)
|
* [yukimochi](https://github.com/yukimochi)
|
||||||
|
* [rkarabut](https://github.com/rkarabut)
|
||||||
* [Artoria2e5](https://github.com/Artoria2e5)
|
* [Artoria2e5](https://github.com/Artoria2e5)
|
||||||
* [nightpool](https://github.com/nightpool)
|
* [nightpool](https://github.com/nightpool)
|
||||||
* [marrus-sh](https://github.com/marrus-sh)
|
* [marrus-sh](https://github.com/marrus-sh)
|
||||||
@ -64,11 +64,14 @@ and provided thanks to the work of the following contributors:
|
|||||||
* [MaciekBaron](https://github.com/MaciekBaron)
|
* [MaciekBaron](https://github.com/MaciekBaron)
|
||||||
* [MitarashiDango](mailto:mitarashidango@users.noreply.github.com)
|
* [MitarashiDango](mailto:mitarashidango@users.noreply.github.com)
|
||||||
* [beatrix-bitrot](https://github.com/beatrix-bitrot)
|
* [beatrix-bitrot](https://github.com/beatrix-bitrot)
|
||||||
|
* [Aditoo17](https://github.com/Aditoo17)
|
||||||
* [adbelle](https://github.com/adbelle)
|
* [adbelle](https://github.com/adbelle)
|
||||||
* [evanminto](https://github.com/evanminto)
|
* [evanminto](https://github.com/evanminto)
|
||||||
* [MightyPork](https://github.com/MightyPork)
|
* [MightyPork](https://github.com/MightyPork)
|
||||||
* [yhirano55](https://github.com/yhirano55)
|
* [yhirano55](https://github.com/yhirano55)
|
||||||
|
* [rinsuki](https://github.com/rinsuki)
|
||||||
* [camponez](https://github.com/camponez)
|
* [camponez](https://github.com/camponez)
|
||||||
|
* [hinaloe](https://github.com/hinaloe)
|
||||||
* [SerCom-KC](https://github.com/SerCom-KC)
|
* [SerCom-KC](https://github.com/SerCom-KC)
|
||||||
* [aschmitz](https://github.com/aschmitz)
|
* [aschmitz](https://github.com/aschmitz)
|
||||||
* [devkral](https://github.com/devkral)
|
* [devkral](https://github.com/devkral)
|
||||||
@ -81,10 +84,8 @@ and provided thanks to the work of the following contributors:
|
|||||||
* [lindwurm](https://github.com/lindwurm)
|
* [lindwurm](https://github.com/lindwurm)
|
||||||
* [victorhck](mailto:victorhck@geeko.site)
|
* [victorhck](mailto:victorhck@geeko.site)
|
||||||
* [voidsatisfaction](https://github.com/voidsatisfaction)
|
* [voidsatisfaction](https://github.com/voidsatisfaction)
|
||||||
* [rinsuki](https://github.com/rinsuki)
|
|
||||||
* [hikari-no-yume](https://github.com/hikari-no-yume)
|
* [hikari-no-yume](https://github.com/hikari-no-yume)
|
||||||
* [angristan](https://github.com/angristan)
|
* [angristan](https://github.com/angristan)
|
||||||
* [hinaloe](https://github.com/hinaloe)
|
|
||||||
* [seefood](https://github.com/seefood)
|
* [seefood](https://github.com/seefood)
|
||||||
* [jackjennings](https://github.com/jackjennings)
|
* [jackjennings](https://github.com/jackjennings)
|
||||||
* [spla](mailto:spla@mastodont.cat)
|
* [spla](mailto:spla@mastodont.cat)
|
||||||
@ -102,9 +103,10 @@ and provided thanks to the work of the following contributors:
|
|||||||
* [victorhck](https://github.com/victorhck)
|
* [victorhck](https://github.com/victorhck)
|
||||||
* [kedamaDQ](https://github.com/kedamaDQ)
|
* [kedamaDQ](https://github.com/kedamaDQ)
|
||||||
* [puckipedia](https://github.com/puckipedia)
|
* [puckipedia](https://github.com/puckipedia)
|
||||||
|
* [trwnh](https://github.com/trwnh)
|
||||||
* [fvh-P](https://github.com/fvh-P)
|
* [fvh-P](https://github.com/fvh-P)
|
||||||
* [contraexemplo](https://github.com/contraexemplo)
|
* [Anna e só](mailto:contraexemplos@gmail.com)
|
||||||
* [Aditoo17](https://github.com/Aditoo17)
|
* [BenLubar](https://github.com/BenLubar)
|
||||||
* [kazu9su](https://github.com/kazu9su)
|
* [kazu9su](https://github.com/kazu9su)
|
||||||
* [Komic](https://github.com/Komic)
|
* [Komic](https://github.com/Komic)
|
||||||
* [lmorchard](https://github.com/lmorchard)
|
* [lmorchard](https://github.com/lmorchard)
|
||||||
@ -117,7 +119,6 @@ and provided thanks to the work of the following contributors:
|
|||||||
* [goofy-bz](mailto:goofy@babelzilla.org)
|
* [goofy-bz](mailto:goofy@babelzilla.org)
|
||||||
* [kadiix](https://github.com/kadiix)
|
* [kadiix](https://github.com/kadiix)
|
||||||
* [kodacs](https://github.com/kodacs)
|
* [kodacs](https://github.com/kodacs)
|
||||||
* [trwnh](https://github.com/trwnh)
|
|
||||||
* [JMendyk](https://github.com/JMendyk)
|
* [JMendyk](https://github.com/JMendyk)
|
||||||
* [KScl](https://github.com/KScl)
|
* [KScl](https://github.com/KScl)
|
||||||
* [sterdev](https://github.com/sterdev)
|
* [sterdev](https://github.com/sterdev)
|
||||||
@ -133,6 +134,7 @@ and provided thanks to the work of the following contributors:
|
|||||||
* [Reverite](https://github.com/Reverite)
|
* [Reverite](https://github.com/Reverite)
|
||||||
* [JohnD28](https://github.com/JohnD28)
|
* [JohnD28](https://github.com/JohnD28)
|
||||||
* [znz](https://github.com/znz)
|
* [znz](https://github.com/znz)
|
||||||
|
* [marek-lach](https://github.com/marek-lach)
|
||||||
* [Naouak](https://github.com/Naouak)
|
* [Naouak](https://github.com/Naouak)
|
||||||
* [pawelngei](https://github.com/pawelngei)
|
* [pawelngei](https://github.com/pawelngei)
|
||||||
* [rtucker](https://github.com/rtucker)
|
* [rtucker](https://github.com/rtucker)
|
||||||
@ -150,7 +152,6 @@ and provided thanks to the work of the following contributors:
|
|||||||
* [178inaba](https://github.com/178inaba)
|
* [178inaba](https://github.com/178inaba)
|
||||||
* [alyssais](https://github.com/alyssais)
|
* [alyssais](https://github.com/alyssais)
|
||||||
* [hiphref](https://github.com/hiphref)
|
* [hiphref](https://github.com/hiphref)
|
||||||
* [BenLubar](https://github.com/BenLubar)
|
|
||||||
* [stalker314314](https://github.com/stalker314314)
|
* [stalker314314](https://github.com/stalker314314)
|
||||||
* [huertanix](https://github.com/huertanix)
|
* [huertanix](https://github.com/huertanix)
|
||||||
* [genesixx](https://github.com/genesixx)
|
* [genesixx](https://github.com/genesixx)
|
||||||
@ -161,16 +162,16 @@ and provided thanks to the work of the following contributors:
|
|||||||
* [kmichl](https://github.com/kmichl)
|
* [kmichl](https://github.com/kmichl)
|
||||||
* [Kurtis Rainbolt-Greene](mailto:me@kurtisrainboltgreene.name)
|
* [Kurtis Rainbolt-Greene](mailto:me@kurtisrainboltgreene.name)
|
||||||
* [saper](https://github.com/saper)
|
* [saper](https://github.com/saper)
|
||||||
* [marek-lach](https://github.com/marek-lach)
|
|
||||||
* [nevillepark](https://github.com/nevillepark)
|
* [nevillepark](https://github.com/nevillepark)
|
||||||
* [ornithocoder](https://github.com/ornithocoder)
|
* [ornithocoder](https://github.com/ornithocoder)
|
||||||
* [pierreozoux](https://github.com/pierreozoux)
|
* [pierreozoux](https://github.com/pierreozoux)
|
||||||
* [qguv](https://github.com/qguv)
|
* [qguv](https://github.com/qguv)
|
||||||
* [Ram Lmn](mailto:ramlmn@users.noreply.github.com)
|
* [Ram Lmn](mailto:ramlmn@users.noreply.github.com)
|
||||||
|
* [sascha-sl](https://github.com/sascha-sl)
|
||||||
* [harukasan](https://github.com/harukasan)
|
* [harukasan](https://github.com/harukasan)
|
||||||
* [stamak](https://github.com/stamak)
|
* [stamak](https://github.com/stamak)
|
||||||
* [Technowix](mailto:technowix@users.noreply.github.com)
|
* [Technowix](mailto:technowix@users.noreply.github.com)
|
||||||
* [Eychics](https://github.com/Eychics)
|
* [Zoeille](https://github.com/Zoeille)
|
||||||
* [Thor Harald Johansen](mailto:thj@thj.no)
|
* [Thor Harald Johansen](mailto:thj@thj.no)
|
||||||
* [0x70b1a5](https://github.com/0x70b1a5)
|
* [0x70b1a5](https://github.com/0x70b1a5)
|
||||||
* [gled-rs](https://github.com/gled-rs)
|
* [gled-rs](https://github.com/gled-rs)
|
||||||
@ -244,9 +245,9 @@ and provided thanks to the work of the following contributors:
|
|||||||
* [raymestalez](https://github.com/raymestalez)
|
* [raymestalez](https://github.com/raymestalez)
|
||||||
* [remram44](https://github.com/remram44)
|
* [remram44](https://github.com/remram44)
|
||||||
* [sts10](https://github.com/sts10)
|
* [sts10](https://github.com/sts10)
|
||||||
* [sascha-sl](https://github.com/sascha-sl)
|
|
||||||
* [u1-liquid](https://github.com/u1-liquid)
|
* [u1-liquid](https://github.com/u1-liquid)
|
||||||
* [sim6](https://github.com/sim6)
|
* [sim6](https://github.com/sim6)
|
||||||
|
* [Sir-Boops](https://github.com/Sir-Boops)
|
||||||
* [stemid](https://github.com/stemid)
|
* [stemid](https://github.com/stemid)
|
||||||
* [sumdog](https://github.com/sumdog)
|
* [sumdog](https://github.com/sumdog)
|
||||||
* [ThomasLeister](https://github.com/ThomasLeister)
|
* [ThomasLeister](https://github.com/ThomasLeister)
|
||||||
@ -316,8 +317,11 @@ and provided thanks to the work of the following contributors:
|
|||||||
* [Andreas Drop](mailto:andy@remline.de)
|
* [Andreas Drop](mailto:andy@remline.de)
|
||||||
* [andi1984](https://github.com/andi1984)
|
* [andi1984](https://github.com/andi1984)
|
||||||
* [schas002](https://github.com/schas002)
|
* [schas002](https://github.com/schas002)
|
||||||
|
* [contraexemplo](https://github.com/contraexemplo)
|
||||||
* [abackstrom](https://github.com/abackstrom)
|
* [abackstrom](https://github.com/abackstrom)
|
||||||
|
* [armandfardeau](https://github.com/armandfardeau)
|
||||||
* [jumbosushi](https://github.com/jumbosushi)
|
* [jumbosushi](https://github.com/jumbosushi)
|
||||||
|
* [aurelien-reeves](https://github.com/aurelien-reeves)
|
||||||
* [ayumin](https://github.com/ayumin)
|
* [ayumin](https://github.com/ayumin)
|
||||||
* [BaptisteGelez](https://github.com/BaptisteGelez)
|
* [BaptisteGelez](https://github.com/BaptisteGelez)
|
||||||
* [bzg](https://github.com/bzg)
|
* [bzg](https://github.com/bzg)
|
||||||
@ -335,7 +339,7 @@ and provided thanks to the work of the following contributors:
|
|||||||
* [Motoma](https://github.com/Motoma)
|
* [Motoma](https://github.com/Motoma)
|
||||||
* [chriswk](https://github.com/chriswk)
|
* [chriswk](https://github.com/chriswk)
|
||||||
* [csu](https://github.com/csu)
|
* [csu](https://github.com/csu)
|
||||||
* [clarcharr](https://github.com/clarcharr)
|
* [clarfon](https://github.com/clarfon)
|
||||||
* [kklleemm](https://github.com/kklleemm)
|
* [kklleemm](https://github.com/kklleemm)
|
||||||
* [colindean](https://github.com/colindean)
|
* [colindean](https://github.com/colindean)
|
||||||
* [dachinat](https://github.com/dachinat)
|
* [dachinat](https://github.com/dachinat)
|
||||||
@ -358,6 +362,7 @@ and provided thanks to the work of the following contributors:
|
|||||||
* [eai04191](https://github.com/eai04191)
|
* [eai04191](https://github.com/eai04191)
|
||||||
* [d3vgru](https://github.com/d3vgru)
|
* [d3vgru](https://github.com/d3vgru)
|
||||||
* [Elizafox](https://github.com/Elizafox)
|
* [Elizafox](https://github.com/Elizafox)
|
||||||
|
* [enewhuis](https://github.com/enewhuis)
|
||||||
* [ericblade](https://github.com/ericblade)
|
* [ericblade](https://github.com/ericblade)
|
||||||
* [mikoim](https://github.com/mikoim)
|
* [mikoim](https://github.com/mikoim)
|
||||||
* [espenronnevik](https://github.com/espenronnevik)
|
* [espenronnevik](https://github.com/espenronnevik)
|
||||||
@ -446,6 +451,7 @@ and provided thanks to the work of the following contributors:
|
|||||||
* [mouse-reeve](https://github.com/mouse-reeve)
|
* [mouse-reeve](https://github.com/mouse-reeve)
|
||||||
* [Mozinet-fr](https://github.com/Mozinet-fr)
|
* [Mozinet-fr](https://github.com/Mozinet-fr)
|
||||||
* [lae](https://github.com/lae)
|
* [lae](https://github.com/lae)
|
||||||
|
* [nosada](https://github.com/nosada)
|
||||||
* [Nanamachi](https://github.com/Nanamachi)
|
* [Nanamachi](https://github.com/Nanamachi)
|
||||||
* [orinthe](https://github.com/orinthe)
|
* [orinthe](https://github.com/orinthe)
|
||||||
* [NecroTechno](https://github.com/NecroTechno)
|
* [NecroTechno](https://github.com/NecroTechno)
|
||||||
@ -462,10 +468,11 @@ and provided thanks to the work of the following contributors:
|
|||||||
* [noppa](https://github.com/noppa)
|
* [noppa](https://github.com/noppa)
|
||||||
* [Otakan951](https://github.com/Otakan951)
|
* [Otakan951](https://github.com/Otakan951)
|
||||||
* [fahy](https://github.com/fahy)
|
* [fahy](https://github.com/fahy)
|
||||||
* [PatrickRWells](https://github.com/PatrickRWells)
|
* [PatrickRWells](mailto:32802366+patrickrwells@users.noreply.github.com)
|
||||||
* [Pangoraw](https://github.com/Pangoraw)
|
* [Paul](mailto:naydex.mc+github@gmail.com)
|
||||||
* [peterkeen](https://github.com/peterkeen)
|
* [Pete Keen](mailto:pete@petekeen.net)
|
||||||
* [pgate](https://github.com/pgate)
|
* [Pierre-Morgan Gate](mailto:pgate@users.noreply.github.com)
|
||||||
|
* [Ratmir Karabut](mailto:rkarabut@sfmodern.ru)
|
||||||
* [Reto Kromer](mailto:retokromer@users.noreply.github.com)
|
* [Reto Kromer](mailto:retokromer@users.noreply.github.com)
|
||||||
* [Rey Tucker](mailto:git@reytucker.us)
|
* [Rey Tucker](mailto:git@reytucker.us)
|
||||||
* [Rob Watson](mailto:rfwatson@users.noreply.github.com)
|
* [Rob Watson](mailto:rfwatson@users.noreply.github.com)
|
||||||
@ -488,7 +495,6 @@ and provided thanks to the work of the following contributors:
|
|||||||
* [Sho Kusano](mailto:rosylilly@aduca.org)
|
* [Sho Kusano](mailto:rosylilly@aduca.org)
|
||||||
* [Shouko Yu](mailto:imshouko@gmail.com)
|
* [Shouko Yu](mailto:imshouko@gmail.com)
|
||||||
* [Sina Mashek](mailto:sina@mashek.xyz)
|
* [Sina Mashek](mailto:sina@mashek.xyz)
|
||||||
* [Sir-Boops](mailto:admin@boops.me)
|
|
||||||
* [Soshi Kato](mailto:mail@sossii.com)
|
* [Soshi Kato](mailto:mail@sossii.com)
|
||||||
* [Spanky](mailto:2788886+spankyworks@users.noreply.github.com)
|
* [Spanky](mailto:2788886+spankyworks@users.noreply.github.com)
|
||||||
* [Stanislas](mailto:angristan@pm.me)
|
* [Stanislas](mailto:angristan@pm.me)
|
||||||
@ -555,12 +561,14 @@ and provided thanks to the work of the following contributors:
|
|||||||
* [karlyeurl](mailto:karl.yeurl@gmail.com)
|
* [karlyeurl](mailto:karl.yeurl@gmail.com)
|
||||||
* [kedama](mailto:32974885+kedamadq@users.noreply.github.com)
|
* [kedama](mailto:32974885+kedamadq@users.noreply.github.com)
|
||||||
* [kodai](mailto:shirafuta.kodai@gmail.com)
|
* [kodai](mailto:shirafuta.kodai@gmail.com)
|
||||||
|
* [koyu](mailto:me@koyu.space)
|
||||||
* [kuro5hin](mailto:rusty@kuro5hin.org)
|
* [kuro5hin](mailto:rusty@kuro5hin.org)
|
||||||
* [luzpaz](mailto:luzpaz@users.noreply.github.com)
|
* [luzpaz](mailto:luzpaz@users.noreply.github.com)
|
||||||
* [maxypy](mailto:maxime@mpigou.fr)
|
* [maxypy](mailto:maxime@mpigou.fr)
|
||||||
* [mhe](mailto:mail@marcus-herrmann.com)
|
* [mhe](mailto:mail@marcus-herrmann.com)
|
||||||
* [mike castleman](mailto:m@mlcastle.net)
|
* [mike castleman](mailto:m@mlcastle.net)
|
||||||
* [mimikun](mailto:dzdzble_effort_311@outlook.jp)
|
* [mimikun](mailto:dzdzble_effort_311@outlook.jp)
|
||||||
|
* [mohemohe](mailto:mohemohe@users.noreply.github.com)
|
||||||
* [mshrtkch](mailto:mshrtkch@users.noreply.github.com)
|
* [mshrtkch](mailto:mshrtkch@users.noreply.github.com)
|
||||||
* [muan](mailto:muan@github.com)
|
* [muan](mailto:muan@github.com)
|
||||||
* [namelessGonbai](mailto:43787036+namelessgonbai@users.noreply.github.com)
|
* [namelessGonbai](mailto:43787036+namelessgonbai@users.noreply.github.com)
|
||||||
@ -599,243 +607,338 @@ This document is provided for informational purposes only. Since it is only upda
|
|||||||
|
|
||||||
Following people have contributed to translation of Mastodon:
|
Following people have contributed to translation of Mastodon:
|
||||||
|
|
||||||
|
- **Albanian**
|
||||||
|
- Besnik Bleta
|
||||||
|
- Aditoo
|
||||||
- **Arabic**
|
- **Arabic**
|
||||||
- ButterflyOfFire
|
- ButterflyOfFire
|
||||||
|
- Aditoo
|
||||||
|
- Amrz0
|
||||||
- **Asturian**
|
- **Asturian**
|
||||||
- ButterflyOfFire
|
- ButterflyOfFire
|
||||||
- Enol P.
|
- Enol P.
|
||||||
|
- Aditoo
|
||||||
- **Basque**
|
- **Basque**
|
||||||
|
- Osoitz
|
||||||
|
- Aditoo
|
||||||
- Aitzol
|
- Aitzol
|
||||||
- ButterflyOfFire
|
- ButterflyOfFire
|
||||||
- Gorka Azkarate
|
|
||||||
- Osoitz
|
|
||||||
- Peru Iparragirre
|
- Peru Iparragirre
|
||||||
|
- Gorka Azkarate
|
||||||
|
- **Bengali**
|
||||||
|
- dxwc
|
||||||
- **Bulgarian**
|
- **Bulgarian**
|
||||||
- ButterflyOfFire
|
- ButterflyOfFire
|
||||||
|
- Aditoo
|
||||||
- **Catalan**
|
- **Catalan**
|
||||||
|
- spla
|
||||||
|
- Aditoo
|
||||||
- ButterflyOfFire
|
- ButterflyOfFire
|
||||||
- Joan Montané
|
- Joan Montané
|
||||||
- Jose Luis
|
- Jose Luis
|
||||||
- spla
|
|
||||||
- **Chinese (Hong Kong)**
|
- **Chinese (Hong Kong)**
|
||||||
- ButterflyOfFire
|
- ButterflyOfFire
|
||||||
- Luzi Leung
|
- Luzi Leung
|
||||||
|
- Aditoo
|
||||||
- **Chinese (Simplified)**
|
- **Chinese (Simplified)**
|
||||||
- Allen Zhong
|
- Allen Zhong
|
||||||
- ButterflyOfFire
|
- ButterflyOfFire
|
||||||
- SerCom_KC
|
- SerCom_KC
|
||||||
|
- martialarts
|
||||||
|
- Kaitian Xie
|
||||||
|
- Aditoo
|
||||||
|
- pan93412
|
||||||
- **Chinese (Traditional)**
|
- **Chinese (Traditional)**
|
||||||
|
- Aditoo
|
||||||
- ButterflyOfFire
|
- ButterflyOfFire
|
||||||
- James58899
|
- James58899
|
||||||
- Jeff Huang
|
- pan93412
|
||||||
- S1ttidoe477
|
- S1ttidoe477
|
||||||
- SHA265
|
- SHA265
|
||||||
|
- Jeff Huang
|
||||||
- **Corsican**
|
- **Corsican**
|
||||||
- Alix D. R.
|
- Alix D. R.
|
||||||
|
- Aditoo
|
||||||
- ButterflyOfFire
|
- ButterflyOfFire
|
||||||
- **Croatian**
|
- **Croatian**
|
||||||
- ButterflyOfFire
|
- ButterflyOfFire
|
||||||
|
- Aditoo
|
||||||
- **Czech**
|
- **Czech**
|
||||||
- ButterflyOfFire
|
- Aditoo
|
||||||
- Lorem Ipsum
|
|
||||||
- Marek Ľach
|
- Marek Ľach
|
||||||
|
- ButterflyOfFire
|
||||||
- **Danish**
|
- **Danish**
|
||||||
- ButterflyOfFire
|
- Einhjeriar
|
||||||
- Rasmus Sæderup
|
- Rasmus Sæderup
|
||||||
- **Dutch**
|
- Aditoo
|
||||||
|
- ButterflyOfFire
|
||||||
|
- **Dutch**
|
||||||
|
- Albakham
|
||||||
- ButterflyOfFire
|
- ButterflyOfFire
|
||||||
- Jelv
|
|
||||||
- jeroenpraat
|
- jeroenpraat
|
||||||
- rscmbbng
|
- rscmbbng
|
||||||
|
- Aditoo
|
||||||
|
- Jelv
|
||||||
- **English**
|
- **English**
|
||||||
- ButterflyOfFire
|
- ButterflyOfFire
|
||||||
- Renato "Lond" Cerqueira
|
- Renato "Lond" Cerqueira
|
||||||
|
- **English (United Kingdom)**
|
||||||
|
- Albakham
|
||||||
- **Esperanto**
|
- **Esperanto**
|
||||||
|
- Aditoo
|
||||||
- ButterflyOfFire
|
- ButterflyOfFire
|
||||||
|
- Becci Cat
|
||||||
- Jeong Arm
|
- Jeong Arm
|
||||||
- Martin Bodin
|
|
||||||
- Mélanie Chauvel
|
- Mélanie Chauvel
|
||||||
- Vanege
|
- Vanege
|
||||||
|
- Martin Bodin
|
||||||
- tuxayo/Victor Grousset
|
- tuxayo/Victor Grousset
|
||||||
- **Finnish**
|
- **Finnish**
|
||||||
- ButterflyOfFire
|
- ButterflyOfFire
|
||||||
- Jonne Arjoranta
|
- Mikko Poussu
|
||||||
- S Heija
|
|
||||||
- Taru Luojola
|
- Taru Luojola
|
||||||
|
- S Heija
|
||||||
|
- Aditoo
|
||||||
|
- Jonne Arjoranta
|
||||||
- **French**
|
- **French**
|
||||||
- Alda Marteau-Hardi
|
- Albakham
|
||||||
- Alix D. R.
|
- Alix D. R.
|
||||||
- Baptiste Jonglez
|
|
||||||
- ButterflyOfFire
|
- ButterflyOfFire
|
||||||
- Franck Paul
|
- codl
|
||||||
- Jean-Baptiste Holcroft
|
- Leia
|
||||||
|
- Alda Marteau-Hardi
|
||||||
|
- Mélanie Chauvel
|
||||||
|
- Paul Marques Mota
|
||||||
|
- azenet
|
||||||
|
- Olivier Humbert
|
||||||
|
- Aditoo
|
||||||
- Jonathan Chan
|
- Jonathan Chan
|
||||||
- Letiteuf55
|
- Letiteuf55
|
||||||
- Martin Bodin
|
- Baptiste Jonglez
|
||||||
- Mélanie Chauvel
|
- goofy-mdn
|
||||||
- Olivier Humbert
|
- Jean-Baptiste Holcroft
|
||||||
- Paul Marques Mota
|
|
||||||
- Sylvhem
|
|
||||||
- Technowix
|
- Technowix
|
||||||
- Thibaut Girka
|
- Martin Bodin
|
||||||
- Théodore
|
- Théodore
|
||||||
- azenet
|
- Thibaut Girka
|
||||||
- codl
|
- Franck Paul
|
||||||
|
- Sylvhem
|
||||||
- **Galician**
|
- **Galician**
|
||||||
- ButterflyOfFire
|
- ButterflyOfFire
|
||||||
- Xose M.
|
- Xose M.
|
||||||
|
- Aditoo
|
||||||
- manequim
|
- manequim
|
||||||
- **Georgian**
|
- **Georgian**
|
||||||
- ButterflyOfFire
|
- ButterflyOfFire
|
||||||
|
- Aditoo
|
||||||
- **German**
|
- **German**
|
||||||
- Benedikt Geißler
|
- Aditoo
|
||||||
- ButterflyOfFire
|
- ButterflyOfFire
|
||||||
- Daniel
|
- Daniel
|
||||||
- Eugen Rochko
|
|
||||||
- Koyu Berteon
|
|
||||||
- Patrick Figel
|
|
||||||
- Weblate Admin
|
|
||||||
- averageunicorn
|
- averageunicorn
|
||||||
- ePirat
|
- Koyu Berteon
|
||||||
- koyu
|
|
||||||
- larsreineke
|
- larsreineke
|
||||||
|
- koyu
|
||||||
|
- Austin Jones
|
||||||
- lilo
|
- lilo
|
||||||
|
- Benedikt Geißler
|
||||||
|
- ePirat
|
||||||
|
- Eugen Rochko
|
||||||
|
- Weblate Admin
|
||||||
|
- Patrick Figel
|
||||||
- **Greek**
|
- **Greek**
|
||||||
- Antonis
|
|
||||||
- ButterflyOfFire
|
|
||||||
- Dimitris Maroulidis
|
- Dimitris Maroulidis
|
||||||
|
- Antonis
|
||||||
|
- Aditoo
|
||||||
|
- ButterflyOfFire
|
||||||
- Konstantinos Grevenitis
|
- Konstantinos Grevenitis
|
||||||
- **Hebrew**
|
- **Hebrew**
|
||||||
- ButterflyOfFire
|
- ButterflyOfFire
|
||||||
|
- Aditoo
|
||||||
- Ira
|
- Ira
|
||||||
- Yaron Shahrabani
|
- Yaron Shahrabani
|
||||||
- **Hungarian**
|
- **Hungarian**
|
||||||
- Adam Paszternak
|
|
||||||
- ButterflyOfFire
|
- ButterflyOfFire
|
||||||
|
- Adam Paszternak
|
||||||
|
- Aditoo
|
||||||
- Tibike Miklós
|
- Tibike Miklós
|
||||||
- **Ido**
|
- **Ido**
|
||||||
- ButterflyOfFire
|
- ButterflyOfFire
|
||||||
|
- Aditoo
|
||||||
- **Indonesian**
|
- **Indonesian**
|
||||||
- Alfiana Sibuea
|
- afachri
|
||||||
- ButterflyOfFire
|
- ButterflyOfFire
|
||||||
- Dito Kurnia Pratama
|
- Dito Kurnia Pratama
|
||||||
- Eirworks
|
- Eirworks
|
||||||
- afachri
|
- Aditoo
|
||||||
|
- Alfiana Sibuea
|
||||||
- se7entime
|
- se7entime
|
||||||
|
- **Irish**
|
||||||
|
- Albakham
|
||||||
|
- Kevin Houlihan
|
||||||
- **Italian**
|
- **Italian**
|
||||||
- Alessandro Levati
|
- Alessandro Levati
|
||||||
|
- Albakham
|
||||||
- ButterflyOfFire
|
- ButterflyOfFire
|
||||||
|
- Marcin Mikołajczak
|
||||||
|
- Aditoo
|
||||||
- Giuseppe Pignataro
|
- Giuseppe Pignataro
|
||||||
- Stefano
|
- Stefano
|
||||||
- **Japanese**
|
- **Japanese**
|
||||||
- ButterflyOfFire
|
- Hinaloe
|
||||||
- Kumasun Morino
|
- 小鳥遊まりあ
|
||||||
- Yamagishi Kazutoshi
|
|
||||||
- mayaeh
|
- mayaeh
|
||||||
- osapon
|
- osapon
|
||||||
- unarist
|
|
||||||
- 小鳥遊まりあ
|
|
||||||
- 森の子リスのミーコの大冒険
|
- 森の子リスのミーコの大冒険
|
||||||
- **Korean**
|
- Kumasun Morino
|
||||||
|
- Yamagishi Kazutoshi
|
||||||
|
- Aditoo
|
||||||
- ButterflyOfFire
|
- ButterflyOfFire
|
||||||
- Jeong Arm
|
- Jeong Arm
|
||||||
|
- unarist
|
||||||
|
- **Kazakh**
|
||||||
|
- arshat
|
||||||
|
- Aditoo
|
||||||
|
- **Korean**
|
||||||
|
- Aditoo
|
||||||
|
- Jeong Arm
|
||||||
|
- ButterflyOfFire
|
||||||
- Minori Hiraoka
|
- Minori Hiraoka
|
||||||
- Yamagishi Kazutoshi
|
- Yamagishi Kazutoshi
|
||||||
|
- **Lithuanian**
|
||||||
|
- Sarunas Medeikis
|
||||||
- **Malay**
|
- **Malay**
|
||||||
- ButterflyOfFire
|
|
||||||
- Muhammad Nur Hidayat (MNH48)
|
- Muhammad Nur Hidayat (MNH48)
|
||||||
|
- Aditoo
|
||||||
|
- ButterflyOfFire
|
||||||
- **Norwegian (old code)**
|
- **Norwegian (old code)**
|
||||||
- ButterflyOfFire
|
- ButterflyOfFire
|
||||||
- Espen Rønnevik
|
- Espen Rønnevik
|
||||||
|
- Aditoo
|
||||||
- Tale
|
- Tale
|
||||||
- **Occitan**
|
- **Occitan**
|
||||||
|
- Aditoo
|
||||||
- ButterflyOfFire
|
- ButterflyOfFire
|
||||||
- Maxenç
|
|
||||||
- Quenti2
|
- Quenti2
|
||||||
- Quentí
|
- Quentí
|
||||||
|
- Maxenç
|
||||||
- **Persian**
|
- **Persian**
|
||||||
- ButterflyOfFire
|
|
||||||
- Masoud Abkenar
|
- Masoud Abkenar
|
||||||
- **Polish**
|
- Aditoo
|
||||||
|
- ButterflyOfFire
|
||||||
|
- **Polish**
|
||||||
|
- Aditoo
|
||||||
|
- Albakham
|
||||||
- ButterflyOfFire
|
- ButterflyOfFire
|
||||||
- Jakub Mendyk
|
|
||||||
- Marcin Mikołajczak
|
|
||||||
- Marek Ľach
|
|
||||||
- Stasiek Michalski
|
- Stasiek Michalski
|
||||||
|
- Marcin Mikołajczak
|
||||||
|
- Jakub Mendyk
|
||||||
|
- Marek Ľach
|
||||||
- krkk
|
- krkk
|
||||||
- **Portuguese**
|
- **Portuguese**
|
||||||
|
- Albakham
|
||||||
|
- João Pinheiro
|
||||||
|
- manequim
|
||||||
|
- Aditoo
|
||||||
- ButterflyOfFire
|
- ButterflyOfFire
|
||||||
- Hugo Gameiro
|
- Hugo Gameiro
|
||||||
- manequim
|
|
||||||
- **Portuguese (Brazil)**
|
- **Portuguese (Brazil)**
|
||||||
- André Andrade
|
- Aditoo
|
||||||
|
- Albakham
|
||||||
- Anna e só
|
- Anna e só
|
||||||
- ButterflyOfFire
|
|
||||||
- Renato "Lond" Cerqueira
|
- Renato "Lond" Cerqueira
|
||||||
- **Romanian**
|
- André Andrade
|
||||||
- ButterflyOfFire
|
- ButterflyOfFire
|
||||||
|
- **Romanian**
|
||||||
- adrianbblk
|
- adrianbblk
|
||||||
|
- ButterflyOfFire
|
||||||
|
- Aditoo
|
||||||
- **Russian**
|
- **Russian**
|
||||||
- Andrew Zyabin
|
- Albakham
|
||||||
- ButterflyOfFire
|
- ButterflyOfFire
|
||||||
- Evgeny Petrov
|
- Evgeny Petrov
|
||||||
|
- Aditoo
|
||||||
|
- Павел Гастелло
|
||||||
|
- Andrew Zyabin
|
||||||
- Yaron Shahrabani
|
- Yaron Shahrabani
|
||||||
- **Serbian**
|
- **Serbian**
|
||||||
- Branko Kokanovic
|
- Branko Kokanovic
|
||||||
- Burekz Finezt
|
- Burekz Finezt
|
||||||
|
- Aditoo
|
||||||
- ButterflyOfFire
|
- ButterflyOfFire
|
||||||
- **Serbian (latin)**
|
- **Serbian (latin)**
|
||||||
- ButterflyOfFire
|
- ButterflyOfFire
|
||||||
|
- Aditoo
|
||||||
- **Slovak**
|
- **Slovak**
|
||||||
|
- Aditoo
|
||||||
- ButterflyOfFire
|
- ButterflyOfFire
|
||||||
- Ivan Pleva
|
- Ivan Pleva
|
||||||
- Lorem Ipsum
|
|
||||||
- Marek Ľach
|
- Marek Ľach
|
||||||
- Peter
|
- Peter
|
||||||
- **Slovenian**
|
- **Slovenian**
|
||||||
- ButterflyOfFire
|
|
||||||
- Kristijan Tkalec
|
- Kristijan Tkalec
|
||||||
|
- Aditoo
|
||||||
|
- ButterflyOfFire
|
||||||
- **Spanish**
|
- **Spanish**
|
||||||
- Angeles Broullón
|
- Albakham
|
||||||
- Antón López
|
|
||||||
- ButterflyOfFire
|
- ButterflyOfFire
|
||||||
- Carlos Mondragon
|
- Carlos Mondragon
|
||||||
|
- Antón López
|
||||||
|
- Max Winkler
|
||||||
|
- Pablo de la Concepción Sanz
|
||||||
|
- Sergio Soriano
|
||||||
|
- Angeles Broullón
|
||||||
|
- Lothar Wolf
|
||||||
|
- Aditoo
|
||||||
- David Charte
|
- David Charte
|
||||||
- Emmanuel
|
- Emmanuel
|
||||||
- Lothar Wolf
|
|
||||||
- Pablo de la Concepción Sanz
|
|
||||||
- **Swedish**
|
- **Swedish**
|
||||||
- ButterflyOfFire
|
- ButterflyOfFire
|
||||||
- Elias Mårtenson
|
|
||||||
- Isak Holmström
|
- Isak Holmström
|
||||||
- Shellkr
|
- Shellkr
|
||||||
|
- Aditoo
|
||||||
|
- Elias Mårtenson
|
||||||
- Stefan Midjich
|
- Stefan Midjich
|
||||||
- Tim Stahel
|
- Tim Stahel
|
||||||
|
- Jonas Hultén
|
||||||
- **Telugu**
|
- **Telugu**
|
||||||
|
- avndp
|
||||||
|
- Ranjith Tellakula
|
||||||
|
- Aditoo
|
||||||
- ButterflyOfFire
|
- ButterflyOfFire
|
||||||
- Joseph Nuthalapati
|
- Joseph Nuthalapati
|
||||||
- Ranjith Tellakula
|
|
||||||
- avndp
|
|
||||||
- **Thai**
|
- **Thai**
|
||||||
- ButterflyOfFire
|
- ButterflyOfFire
|
||||||
|
- parnikkapore
|
||||||
|
- Thai Localization
|
||||||
|
- Aditoo
|
||||||
- **Turkish**
|
- **Turkish**
|
||||||
|
- Ali Demirtas
|
||||||
- ButterflyOfFire
|
- ButterflyOfFire
|
||||||
|
- Aditoo
|
||||||
- **Ukrainian**
|
- **Ukrainian**
|
||||||
- ButterflyOfFire
|
|
||||||
- Ivan Verchenko
|
|
||||||
- alexcleac
|
- alexcleac
|
||||||
- **Welsh**
|
|
||||||
- ButterflyOfFire
|
- ButterflyOfFire
|
||||||
- Jaz-Michael King
|
- Aditoo
|
||||||
- Kevin Beynon
|
- Ivan Verchenko
|
||||||
- Owain Rhys Lewis
|
- **Welsh**
|
||||||
- Renato "Lond" Cerqueira
|
|
||||||
- Rhoslyn Prys
|
|
||||||
- carl morris
|
- carl morris
|
||||||
|
- Jaz-Michael King
|
||||||
|
- Owain Rhys Lewis
|
||||||
|
- Rhoslyn Prys
|
||||||
|
- Aditoo
|
||||||
|
- ButterflyOfFire
|
||||||
|
- Renato "Lond" Cerqueira
|
||||||
|
- Albakham
|
||||||
|
- Kevin Beynon
|
||||||
- **Armenian**
|
- **Armenian**
|
||||||
|
- Aditoo
|
||||||
- ButterflyOfFire
|
- ButterflyOfFire
|
||||||
- **Latvian**
|
- **Latvian**
|
||||||
|
- Aditoo
|
||||||
- ButterflyOfFire
|
- ButterflyOfFire
|
||||||
|
- Maigonis
|
||||||
- **Tamil**
|
- **Tamil**
|
||||||
|
- Aditoo
|
||||||
- ButterflyOfFire
|
- ButterflyOfFire
|
||||||
- Prasanna Venkadesh
|
- Prasanna Venkadesh
|
||||||
|
114
CHANGELOG.md
114
CHANGELOG.md
@ -3,6 +3,120 @@ Changelog
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
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
|
## [2.7.2] - 2019-02-17
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
192
Dockerfile
192
Dockerfile
@ -1,90 +1,128 @@
|
|||||||
FROM node:8.15-alpine as node
|
FROM ubuntu:18.04 as build-dep
|
||||||
FROM ruby:2.6-alpine3.8
|
|
||||||
|
|
||||||
LABEL maintainer="https://github.com/tootsuite/mastodon" \
|
# Use bash for the shell
|
||||||
description="Your self-hosted, globally interconnected microblogging community"
|
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 UID=991
|
||||||
ARG GID=991
|
ARG GID=991
|
||||||
|
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
|
||||||
|
|
||||||
ENV PATH=/mastodon/bin:$PATH \
|
# Install masto runtime deps
|
||||||
RAILS_SERVE_STATIC_FILES=true \
|
RUN apt -y --no-install-recommends install \
|
||||||
RAILS_ENV=production \
|
libssl1.1 libpq5 imagemagick ffmpeg \
|
||||||
NODE_ENV=production
|
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
|
||||||
|
|
||||||
ARG LIBICONV_VERSION=1.15
|
# Add tini
|
||||||
ARG LIBICONV_DOWNLOAD_SHA256=ccf536620a45458d26ba83887a983b96827001e92a13847b45e4925cc8913178
|
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
|
||||||
|
|
||||||
EXPOSE 3000 4000
|
# 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
|
||||||
|
|
||||||
WORKDIR /mastodon
|
# Run masto services in prod mode
|
||||||
|
ENV RAILS_ENV="production"
|
||||||
|
ENV NODE_ENV="production"
|
||||||
|
|
||||||
COPY --from=node /usr/local/bin/node /usr/local/bin/node
|
# Tell rails to serve static files
|
||||||
COPY --from=node /usr/local/lib/node_modules /usr/local/lib/node_modules
|
ENV RAILS_SERVE_STATIC_FILES="true"
|
||||||
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
|
|
||||||
|
|
||||||
|
# Set the run user
|
||||||
USER mastodon
|
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", "--"]
|
||||||
|
28
Gemfile
28
Gemfile
@ -6,16 +6,16 @@ ruby '>= 2.4.0', '< 2.7.0'
|
|||||||
gem 'pkg-config', '~> 1.3'
|
gem 'pkg-config', '~> 1.3'
|
||||||
|
|
||||||
gem 'puma', '~> 3.12'
|
gem 'puma', '~> 3.12'
|
||||||
gem 'rails', '~> 5.2.2'
|
gem 'rails', '~> 5.2.3'
|
||||||
gem 'thor', '~> 0.20'
|
gem 'thor', '~> 0.20'
|
||||||
|
|
||||||
gem 'hamlit-rails', '~> 0.2'
|
gem 'hamlit-rails', '~> 0.2'
|
||||||
gem 'pg', '~> 1.1'
|
gem 'pg', '~> 1.1'
|
||||||
gem 'makara', '~> 0.4'
|
gem 'makara', '~> 0.4'
|
||||||
gem 'pghero', '~> 2.2'
|
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-core', '<= 2.1.0'
|
||||||
gem 'fog-openstack', '~> 0.3', require: false
|
gem 'fog-openstack', '~> 0.3', require: false
|
||||||
gem 'paperclip', '~> 6.0'
|
gem 'paperclip', '~> 6.0'
|
||||||
@ -24,13 +24,13 @@ gem 'streamio-ffmpeg', '~> 3.0'
|
|||||||
|
|
||||||
gem 'active_model_serializers', '~> 0.10'
|
gem 'active_model_serializers', '~> 0.10'
|
||||||
gem 'addressable', '~> 2.6'
|
gem 'addressable', '~> 2.6'
|
||||||
gem 'bootsnap', '~> 1.3', require: false
|
gem 'bootsnap', '~> 1.4', require: false
|
||||||
gem 'browser'
|
gem 'browser'
|
||||||
gem 'charlock_holmes', '~> 0.7.6'
|
gem 'charlock_holmes', '~> 0.7.6'
|
||||||
gem 'iso-639'
|
gem 'iso-639'
|
||||||
gem 'chewy', '~> 5.0'
|
gem 'chewy', '~> 5.0'
|
||||||
gem 'cld3', '~> 3.2.3'
|
gem 'cld3', '~> 3.2.3'
|
||||||
gem 'devise', '~> 4.5'
|
gem 'devise', '~> 4.6'
|
||||||
gem 'devise-two-factor', '~> 3.0'
|
gem 'devise-two-factor', '~> 3.0'
|
||||||
|
|
||||||
group :pam_authentication, optional: true do
|
group :pam_authentication, optional: true do
|
||||||
@ -85,8 +85,8 @@ gem 'strong_migrations', '~> 0.3'
|
|||||||
gem 'tty-command', '~> 0.8', require: false
|
gem 'tty-command', '~> 0.8', require: false
|
||||||
gem 'tty-prompt', '~> 0.18', require: false
|
gem 'tty-prompt', '~> 0.18', require: false
|
||||||
gem 'twitter-text', '~> 1.14'
|
gem 'twitter-text', '~> 1.14'
|
||||||
gem 'tzinfo-data', '~> 1.2018'
|
gem 'tzinfo-data', '~> 1.2019'
|
||||||
gem 'webpacker', '~> 3.5'
|
gem 'webpacker', '~> 4.0'
|
||||||
gem 'webpush'
|
gem 'webpush'
|
||||||
|
|
||||||
gem 'json-ld', '~> 3.0'
|
gem 'json-ld', '~> 3.0'
|
||||||
@ -97,7 +97,7 @@ group :development, :test do
|
|||||||
gem 'fabrication', '~> 2.20'
|
gem 'fabrication', '~> 2.20'
|
||||||
gem 'fuubar', '~> 2.3'
|
gem 'fuubar', '~> 2.3'
|
||||||
gem 'i18n-tasks', '~> 0.9', require: false
|
gem 'i18n-tasks', '~> 0.9', require: false
|
||||||
gem 'pry-byebug', '~> 3.6'
|
gem 'pry-byebug', '~> 3.7'
|
||||||
gem 'pry-rails', '~> 0.3'
|
gem 'pry-rails', '~> 0.3'
|
||||||
gem 'rspec-rails', '~> 3.8'
|
gem 'rspec-rails', '~> 3.8'
|
||||||
end
|
end
|
||||||
@ -107,19 +107,19 @@ group :production, :test do
|
|||||||
end
|
end
|
||||||
|
|
||||||
group :test do
|
group :test do
|
||||||
gem 'capybara', '~> 3.12'
|
gem 'capybara', '~> 3.16'
|
||||||
gem 'climate_control', '~> 0.2'
|
gem 'climate_control', '~> 0.2'
|
||||||
gem 'faker', '~> 1.9'
|
gem 'faker', '~> 1.9'
|
||||||
gem 'microformats', '~> 4.0'
|
gem 'microformats', '~> 4.1'
|
||||||
gem 'rails-controller-testing', '~> 1.0'
|
gem 'rails-controller-testing', '~> 1.0'
|
||||||
gem 'rspec-sidekiq', '~> 3.0'
|
gem 'rspec-sidekiq', '~> 3.0'
|
||||||
gem 'simplecov', '~> 0.16', require: false
|
gem 'simplecov', '~> 0.16', require: false
|
||||||
gem 'webmock', '~> 3.5'
|
gem 'webmock', '~> 3.5'
|
||||||
gem 'parallel_tests', '~> 2.27'
|
gem 'parallel_tests', '~> 2.28'
|
||||||
end
|
end
|
||||||
|
|
||||||
group :development do
|
group :development do
|
||||||
gem 'active_record_query_trace', '~> 1.5'
|
gem 'active_record_query_trace', '~> 1.6'
|
||||||
gem 'annotate', '~> 2.7'
|
gem 'annotate', '~> 2.7'
|
||||||
gem 'better_errors', '~> 2.5'
|
gem 'better_errors', '~> 2.5'
|
||||||
gem 'binding_of_caller', '~> 0.7'
|
gem 'binding_of_caller', '~> 0.7'
|
||||||
@ -127,8 +127,8 @@ group :development do
|
|||||||
gem 'letter_opener', '~> 1.7'
|
gem 'letter_opener', '~> 1.7'
|
||||||
gem 'letter_opener_web', '~> 1.3'
|
gem 'letter_opener_web', '~> 1.3'
|
||||||
gem 'memory_profiler'
|
gem 'memory_profiler'
|
||||||
gem 'rubocop', '~> 0.63', require: false
|
gem 'rubocop', '~> 0.67', require: false
|
||||||
gem 'brakeman', '~> 4.4', require: false
|
gem 'brakeman', '~> 4.5', require: false
|
||||||
gem 'bundler-audit', '~> 0.6', require: false
|
gem 'bundler-audit', '~> 0.6', require: false
|
||||||
gem 'scss_lint', '~> 0.57', require: false
|
gem 'scss_lint', '~> 0.57', require: false
|
||||||
|
|
||||||
|
241
Gemfile.lock
241
Gemfile.lock
@ -15,49 +15,49 @@ GIT
|
|||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
actioncable (5.2.2)
|
actioncable (5.2.3)
|
||||||
actionpack (= 5.2.2)
|
actionpack (= 5.2.3)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
websocket-driver (>= 0.6.1)
|
websocket-driver (>= 0.6.1)
|
||||||
actionmailer (5.2.2)
|
actionmailer (5.2.3)
|
||||||
actionpack (= 5.2.2)
|
actionpack (= 5.2.3)
|
||||||
actionview (= 5.2.2)
|
actionview (= 5.2.3)
|
||||||
activejob (= 5.2.2)
|
activejob (= 5.2.3)
|
||||||
mail (~> 2.5, >= 2.5.4)
|
mail (~> 2.5, >= 2.5.4)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
actionpack (5.2.2)
|
actionpack (5.2.3)
|
||||||
actionview (= 5.2.2)
|
actionview (= 5.2.3)
|
||||||
activesupport (= 5.2.2)
|
activesupport (= 5.2.3)
|
||||||
rack (~> 2.0)
|
rack (~> 2.0)
|
||||||
rack-test (>= 0.6.3)
|
rack-test (>= 0.6.3)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
rails-html-sanitizer (~> 1.0, >= 1.0.2)
|
rails-html-sanitizer (~> 1.0, >= 1.0.2)
|
||||||
actionview (5.2.2)
|
actionview (5.2.3)
|
||||||
activesupport (= 5.2.2)
|
activesupport (= 5.2.3)
|
||||||
builder (~> 3.1)
|
builder (~> 3.1)
|
||||||
erubi (~> 1.4)
|
erubi (~> 1.4)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
rails-html-sanitizer (~> 1.0, >= 1.0.3)
|
rails-html-sanitizer (~> 1.0, >= 1.0.3)
|
||||||
active_model_serializers (0.10.8)
|
active_model_serializers (0.10.9)
|
||||||
actionpack (>= 4.1, < 6)
|
actionpack (>= 4.1, < 6)
|
||||||
activemodel (>= 4.1, < 6)
|
activemodel (>= 4.1, < 6)
|
||||||
case_transform (>= 0.2)
|
case_transform (>= 0.2)
|
||||||
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
|
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
|
||||||
active_record_query_trace (1.5.4)
|
active_record_query_trace (1.6.2)
|
||||||
activejob (5.2.2)
|
activejob (5.2.3)
|
||||||
activesupport (= 5.2.2)
|
activesupport (= 5.2.3)
|
||||||
globalid (>= 0.3.6)
|
globalid (>= 0.3.6)
|
||||||
activemodel (5.2.2)
|
activemodel (5.2.3)
|
||||||
activesupport (= 5.2.2)
|
activesupport (= 5.2.3)
|
||||||
activerecord (5.2.2)
|
activerecord (5.2.3)
|
||||||
activemodel (= 5.2.2)
|
activemodel (= 5.2.3)
|
||||||
activesupport (= 5.2.2)
|
activesupport (= 5.2.3)
|
||||||
arel (>= 9.0)
|
arel (>= 9.0)
|
||||||
activestorage (5.2.2)
|
activestorage (5.2.3)
|
||||||
actionpack (= 5.2.2)
|
actionpack (= 5.2.3)
|
||||||
activerecord (= 5.2.2)
|
activerecord (= 5.2.3)
|
||||||
marcel (~> 0.3.1)
|
marcel (~> 0.3.1)
|
||||||
activesupport (5.2.2)
|
activesupport (5.2.3)
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||||
i18n (>= 0.7, < 2)
|
i18n (>= 0.7, < 2)
|
||||||
minitest (~> 5.1)
|
minitest (~> 5.1)
|
||||||
@ -75,32 +75,33 @@ GEM
|
|||||||
encryptor (~> 3.0.0)
|
encryptor (~> 3.0.0)
|
||||||
av (0.9.0)
|
av (0.9.0)
|
||||||
cocaine (~> 0.5.3)
|
cocaine (~> 0.5.3)
|
||||||
aws-eventstream (1.0.1)
|
aws-eventstream (1.0.2)
|
||||||
aws-partitions (1.131.0)
|
aws-partitions (1.147.0)
|
||||||
aws-sdk-core (3.45.0)
|
aws-sdk-core (3.48.3)
|
||||||
aws-eventstream (~> 1.0)
|
aws-eventstream (~> 1.0, >= 1.0.2)
|
||||||
aws-partitions (~> 1.0)
|
aws-partitions (~> 1.0)
|
||||||
aws-sigv4 (~> 1.0)
|
aws-sigv4 (~> 1.1)
|
||||||
jmespath (~> 1.0)
|
jmespath (~> 1.0)
|
||||||
aws-sdk-kms (1.13.0)
|
aws-sdk-kms (1.16.0)
|
||||||
aws-sdk-core (~> 3, >= 3.39.0)
|
aws-sdk-core (~> 3, >= 3.48.2)
|
||||||
aws-sigv4 (~> 1.0)
|
aws-sigv4 (~> 1.1)
|
||||||
aws-sdk-s3 (1.30.1)
|
aws-sdk-s3 (1.36.0)
|
||||||
aws-sdk-core (~> 3, >= 3.39.0)
|
aws-sdk-core (~> 3, >= 3.48.2)
|
||||||
aws-sdk-kms (~> 1)
|
aws-sdk-kms (~> 1)
|
||||||
aws-sigv4 (~> 1.0)
|
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)
|
bcrypt (3.1.12)
|
||||||
benchmark-ips (2.7.2)
|
benchmark-ips (2.7.2)
|
||||||
better_errors (2.5.0)
|
better_errors (2.5.1)
|
||||||
coderay (>= 1.0.0)
|
coderay (>= 1.0.0)
|
||||||
erubi (>= 1.0.0)
|
erubi (>= 1.0.0)
|
||||||
rack (>= 0.9.0)
|
rack (>= 0.9.0)
|
||||||
binding_of_caller (0.8.0)
|
binding_of_caller (0.8.0)
|
||||||
debug_inspector (>= 0.0.1)
|
debug_inspector (>= 0.0.1)
|
||||||
bootsnap (1.3.2)
|
bootsnap (1.4.3)
|
||||||
msgpack (~> 1.0)
|
msgpack (~> 1.0)
|
||||||
brakeman (4.4.0)
|
brakeman (4.5.0)
|
||||||
browser (2.5.3)
|
browser (2.5.3)
|
||||||
builder (3.2.3)
|
builder (3.2.3)
|
||||||
bullet (5.9.0)
|
bullet (5.9.0)
|
||||||
@ -109,7 +110,7 @@ GEM
|
|||||||
bundler-audit (0.6.1)
|
bundler-audit (0.6.1)
|
||||||
bundler (>= 1.2.0, < 3)
|
bundler (>= 1.2.0, < 3)
|
||||||
thor (~> 0.18)
|
thor (~> 0.18)
|
||||||
byebug (10.0.2)
|
byebug (11.0.0)
|
||||||
capistrano (3.11.0)
|
capistrano (3.11.0)
|
||||||
airbrussh (>= 1.0.0)
|
airbrussh (>= 1.0.0)
|
||||||
i18n
|
i18n
|
||||||
@ -126,7 +127,7 @@ GEM
|
|||||||
sshkit (~> 1.3)
|
sshkit (~> 1.3)
|
||||||
capistrano-yarn (2.0.2)
|
capistrano-yarn (2.0.2)
|
||||||
capistrano (~> 3.0)
|
capistrano (~> 3.0)
|
||||||
capybara (3.12.0)
|
capybara (3.16.1)
|
||||||
addressable
|
addressable
|
||||||
mini_mime (>= 0.1.3)
|
mini_mime (>= 0.1.3)
|
||||||
nokogiri (~> 1.8)
|
nokogiri (~> 1.8)
|
||||||
@ -148,7 +149,7 @@ GEM
|
|||||||
cocaine (0.5.8)
|
cocaine (0.5.8)
|
||||||
climate_control (>= 0.0.3, < 1.0)
|
climate_control (>= 0.0.3, < 1.0)
|
||||||
coderay (1.1.2)
|
coderay (1.1.2)
|
||||||
concurrent-ruby (1.1.4)
|
concurrent-ruby (1.1.5)
|
||||||
connection_pool (2.2.2)
|
connection_pool (2.2.2)
|
||||||
crack (0.4.3)
|
crack (0.4.3)
|
||||||
safe_yaml (~> 1.0.0)
|
safe_yaml (~> 1.0.0)
|
||||||
@ -164,7 +165,7 @@ GEM
|
|||||||
rack (>= 1)
|
rack (>= 1)
|
||||||
rake (> 10, < 13)
|
rake (> 10, < 13)
|
||||||
thor (~> 0.19)
|
thor (~> 0.19)
|
||||||
devise (4.5.0)
|
devise (4.6.2)
|
||||||
bcrypt (~> 3.0)
|
bcrypt (~> 3.0)
|
||||||
orm_adapter (~> 0.1)
|
orm_adapter (~> 0.1)
|
||||||
railties (>= 4.1.0, < 6.0)
|
railties (>= 4.1.0, < 6.0)
|
||||||
@ -185,10 +186,10 @@ GEM
|
|||||||
unf (>= 0.0.5, < 1.0.0)
|
unf (>= 0.0.5, < 1.0.0)
|
||||||
doorkeeper (5.0.2)
|
doorkeeper (5.0.2)
|
||||||
railties (>= 4.2)
|
railties (>= 4.2)
|
||||||
dotenv (2.6.0)
|
dotenv (2.7.2)
|
||||||
dotenv-rails (2.6.0)
|
dotenv-rails (2.7.2)
|
||||||
dotenv (= 2.6.0)
|
dotenv (= 2.7.2)
|
||||||
railties (>= 3.2, < 6.0)
|
railties (>= 3.2, < 6.1)
|
||||||
elasticsearch (6.0.2)
|
elasticsearch (6.0.2)
|
||||||
elasticsearch-api (= 6.0.2)
|
elasticsearch-api (= 6.0.2)
|
||||||
elasticsearch-transport (= 6.0.2)
|
elasticsearch-transport (= 6.0.2)
|
||||||
@ -205,7 +206,7 @@ GEM
|
|||||||
tzinfo
|
tzinfo
|
||||||
excon (0.62.0)
|
excon (0.62.0)
|
||||||
fabrication (2.20.1)
|
fabrication (2.20.1)
|
||||||
faker (1.9.1)
|
faker (1.9.3)
|
||||||
i18n (>= 0.7)
|
i18n (>= 0.7)
|
||||||
faraday (0.15.0)
|
faraday (0.15.0)
|
||||||
multipart-post (>= 1.2, < 3)
|
multipart-post (>= 1.2, < 3)
|
||||||
@ -232,18 +233,18 @@ GEM
|
|||||||
rspec-core (~> 3.0)
|
rspec-core (~> 3.0)
|
||||||
ruby-progressbar (~> 1.4)
|
ruby-progressbar (~> 1.4)
|
||||||
get_process_mem (0.2.3)
|
get_process_mem (0.2.3)
|
||||||
globalid (0.4.1)
|
globalid (0.4.2)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
goldfinger (2.1.0)
|
goldfinger (2.1.0)
|
||||||
addressable (~> 2.5)
|
addressable (~> 2.5)
|
||||||
http (~> 3.0)
|
http (~> 3.0)
|
||||||
nokogiri (~> 1.8)
|
nokogiri (~> 1.8)
|
||||||
oj (~> 3.0)
|
oj (~> 3.0)
|
||||||
hamlit (2.8.8)
|
hamlit (2.9.3)
|
||||||
temple (>= 0.8.0)
|
temple (>= 0.8.0)
|
||||||
thor
|
thor
|
||||||
tilt
|
tilt
|
||||||
hamlit-rails (0.2.0)
|
hamlit-rails (0.2.3)
|
||||||
actionpack (>= 4.0.1)
|
actionpack (>= 4.0.1)
|
||||||
activesupport (>= 4.0.1)
|
activesupport (>= 4.0.1)
|
||||||
hamlit (>= 1.2.0)
|
hamlit (>= 1.2.0)
|
||||||
@ -253,7 +254,7 @@ GEM
|
|||||||
hashdiff (0.3.7)
|
hashdiff (0.3.7)
|
||||||
hashie (3.6.0)
|
hashie (3.6.0)
|
||||||
heapy (0.1.4)
|
heapy (0.1.4)
|
||||||
highline (2.0.0)
|
highline (2.0.1)
|
||||||
hiredis (0.6.3)
|
hiredis (0.6.3)
|
||||||
hkdf (0.3.0)
|
hkdf (0.3.0)
|
||||||
htmlentities (4.3.4)
|
htmlentities (4.3.4)
|
||||||
@ -266,12 +267,12 @@ GEM
|
|||||||
domain_name (~> 0.5)
|
domain_name (~> 0.5)
|
||||||
http-form_data (2.1.1)
|
http-form_data (2.1.1)
|
||||||
http_accept_language (2.1.1)
|
http_accept_language (2.1.1)
|
||||||
httplog (1.2.0)
|
httplog (1.2.2)
|
||||||
rack (>= 1.0)
|
rack (>= 1.0)
|
||||||
rainbow (>= 2.0.0)
|
rainbow (>= 2.0.0)
|
||||||
i18n (1.5.2)
|
i18n (1.6.0)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
i18n-tasks (0.9.28)
|
i18n-tasks (0.9.29)
|
||||||
activesupport (>= 4.0.2)
|
activesupport (>= 4.0.2)
|
||||||
ast (>= 2.1.0)
|
ast (>= 2.1.0)
|
||||||
erubi
|
erubi
|
||||||
@ -327,25 +328,25 @@ GEM
|
|||||||
nokogiri (>= 1.5.9)
|
nokogiri (>= 1.5.9)
|
||||||
mail (2.7.1)
|
mail (2.7.1)
|
||||||
mini_mime (>= 0.1.1)
|
mini_mime (>= 0.1.1)
|
||||||
makara (0.4.0)
|
makara (0.4.1)
|
||||||
activerecord (>= 3.0.0)
|
activerecord (>= 3.0.0)
|
||||||
marcel (0.3.3)
|
marcel (0.3.3)
|
||||||
mimemagic (~> 0.3.2)
|
mimemagic (~> 0.3.2)
|
||||||
mario-redis-lock (1.2.1)
|
mario-redis-lock (1.2.1)
|
||||||
redis (>= 3.0.5)
|
redis (>= 3.0.5)
|
||||||
memory_profiler (0.9.12)
|
memory_profiler (0.9.13)
|
||||||
method_source (0.9.2)
|
method_source (0.9.2)
|
||||||
microformats (4.0.7)
|
microformats (4.1.0)
|
||||||
json
|
json (~> 2.1)
|
||||||
nokogiri
|
nokogiri (~> 1.8, >= 1.8.3)
|
||||||
mime-types (3.2.2)
|
mime-types (3.2.2)
|
||||||
mime-types-data (~> 3.2015)
|
mime-types-data (~> 3.2015)
|
||||||
mime-types-data (3.2018.0812)
|
mime-types-data (3.2018.0812)
|
||||||
mimemagic (0.3.2)
|
mimemagic (0.3.3)
|
||||||
mini_mime (1.0.1)
|
mini_mime (1.0.1)
|
||||||
mini_portile2 (2.4.0)
|
mini_portile2 (2.4.0)
|
||||||
minitest (5.11.3)
|
minitest (5.11.3)
|
||||||
msgpack (1.2.4)
|
msgpack (1.2.9)
|
||||||
multi_json (1.13.1)
|
multi_json (1.13.1)
|
||||||
multipart-post (2.0.0)
|
multipart-post (2.0.0)
|
||||||
necromancer (0.4.0)
|
necromancer (0.4.0)
|
||||||
@ -354,7 +355,7 @@ GEM
|
|||||||
net-ssh (>= 2.6.5)
|
net-ssh (>= 2.6.5)
|
||||||
net-ssh (5.0.2)
|
net-ssh (5.0.2)
|
||||||
nio4r (2.3.1)
|
nio4r (2.3.1)
|
||||||
nokogiri (1.10.1)
|
nokogiri (1.10.2)
|
||||||
mini_portile2 (~> 2.4.0)
|
mini_portile2 (~> 2.4.0)
|
||||||
nokogumbo (2.0.0)
|
nokogumbo (2.0.0)
|
||||||
nokogiri (~> 1.8, >= 1.8.4)
|
nokogiri (~> 1.8, >= 1.8.4)
|
||||||
@ -363,7 +364,7 @@ GEM
|
|||||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||||
sidekiq (>= 3.5)
|
sidekiq (>= 3.5)
|
||||||
statsd-ruby (~> 1.4, >= 1.4.0)
|
statsd-ruby (~> 1.4, >= 1.4.0)
|
||||||
oj (3.7.8)
|
oj (3.7.11)
|
||||||
omniauth (1.9.0)
|
omniauth (1.9.0)
|
||||||
hashie (>= 3.4.6, < 3.7.0)
|
hashie (>= 3.4.6, < 3.7.0)
|
||||||
rack (>= 1.6.2, < 3)
|
rack (>= 1.6.2, < 3)
|
||||||
@ -389,10 +390,10 @@ GEM
|
|||||||
paperclip-av-transcoder (0.6.4)
|
paperclip-av-transcoder (0.6.4)
|
||||||
av (~> 0.9.0)
|
av (~> 0.9.0)
|
||||||
paperclip (>= 2.5.2)
|
paperclip (>= 2.5.2)
|
||||||
parallel (1.13.0)
|
parallel (1.17.0)
|
||||||
parallel_tests (2.27.1)
|
parallel_tests (2.28.0)
|
||||||
parallel
|
parallel
|
||||||
parser (2.6.0.0)
|
parser (2.6.2.0)
|
||||||
ast (~> 2.4.0)
|
ast (~> 2.4.0)
|
||||||
pastel (0.7.2)
|
pastel (0.7.2)
|
||||||
equatable (~> 0.5.0)
|
equatable (~> 0.5.0)
|
||||||
@ -400,8 +401,7 @@ GEM
|
|||||||
pg (1.1.4)
|
pg (1.1.4)
|
||||||
pghero (2.2.0)
|
pghero (2.2.0)
|
||||||
activerecord
|
activerecord
|
||||||
pkg-config (1.3.2)
|
pkg-config (1.3.7)
|
||||||
powerpack (0.1.2)
|
|
||||||
premailer (1.11.1)
|
premailer (1.11.1)
|
||||||
addressable
|
addressable
|
||||||
css_parser (>= 1.6.0)
|
css_parser (>= 1.6.0)
|
||||||
@ -413,38 +413,39 @@ GEM
|
|||||||
pry (0.12.2)
|
pry (0.12.2)
|
||||||
coderay (~> 1.1.0)
|
coderay (~> 1.1.0)
|
||||||
method_source (~> 0.9.0)
|
method_source (~> 0.9.0)
|
||||||
pry-byebug (3.6.0)
|
pry-byebug (3.7.0)
|
||||||
byebug (~> 10.0)
|
byebug (~> 11.0)
|
||||||
pry (~> 0.10)
|
pry (~> 0.10)
|
||||||
pry-rails (0.3.9)
|
pry-rails (0.3.9)
|
||||||
pry (>= 0.10.4)
|
pry (>= 0.10.4)
|
||||||
|
psych (3.1.0)
|
||||||
public_suffix (3.0.3)
|
public_suffix (3.0.3)
|
||||||
puma (3.12.0)
|
puma (3.12.1)
|
||||||
pundit (2.0.1)
|
pundit (2.0.1)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
raabro (1.1.6)
|
raabro (1.1.6)
|
||||||
rack (2.0.6)
|
rack (2.0.7)
|
||||||
rack-attack (5.4.2)
|
rack-attack (5.4.2)
|
||||||
rack (>= 1.0, < 3)
|
rack (>= 1.0, < 3)
|
||||||
rack-cors (1.0.2)
|
rack-cors (1.0.3)
|
||||||
rack-protection (2.0.5)
|
rack-protection (2.0.5)
|
||||||
rack
|
rack
|
||||||
rack-proxy (0.6.4)
|
rack-proxy (0.6.5)
|
||||||
rack
|
rack
|
||||||
rack-test (1.1.0)
|
rack-test (1.1.0)
|
||||||
rack (>= 1.0, < 3)
|
rack (>= 1.0, < 3)
|
||||||
rails (5.2.2)
|
rails (5.2.3)
|
||||||
actioncable (= 5.2.2)
|
actioncable (= 5.2.3)
|
||||||
actionmailer (= 5.2.2)
|
actionmailer (= 5.2.3)
|
||||||
actionpack (= 5.2.2)
|
actionpack (= 5.2.3)
|
||||||
actionview (= 5.2.2)
|
actionview (= 5.2.3)
|
||||||
activejob (= 5.2.2)
|
activejob (= 5.2.3)
|
||||||
activemodel (= 5.2.2)
|
activemodel (= 5.2.3)
|
||||||
activerecord (= 5.2.2)
|
activerecord (= 5.2.3)
|
||||||
activestorage (= 5.2.2)
|
activestorage (= 5.2.3)
|
||||||
activesupport (= 5.2.2)
|
activesupport (= 5.2.3)
|
||||||
bundler (>= 1.3.0)
|
bundler (>= 1.3.0)
|
||||||
railties (= 5.2.2)
|
railties (= 5.2.3)
|
||||||
sprockets-rails (>= 2.0.0)
|
sprockets-rails (>= 2.0.0)
|
||||||
rails-controller-testing (1.0.4)
|
rails-controller-testing (1.0.4)
|
||||||
actionpack (>= 5.0.1.x)
|
actionpack (>= 5.0.1.x)
|
||||||
@ -455,14 +456,14 @@ GEM
|
|||||||
nokogiri (>= 1.6)
|
nokogiri (>= 1.6)
|
||||||
rails-html-sanitizer (1.0.4)
|
rails-html-sanitizer (1.0.4)
|
||||||
loofah (~> 2.2, >= 2.2.2)
|
loofah (~> 2.2, >= 2.2.2)
|
||||||
rails-i18n (5.1.2)
|
rails-i18n (5.1.3)
|
||||||
i18n (>= 0.7, < 2)
|
i18n (>= 0.7, < 2)
|
||||||
railties (>= 5.0, < 6)
|
railties (>= 5.0, < 6)
|
||||||
rails-settings-cached (0.6.6)
|
rails-settings-cached (0.6.6)
|
||||||
rails (>= 4.2.0)
|
rails (>= 4.2.0)
|
||||||
railties (5.2.2)
|
railties (5.2.3)
|
||||||
actionpack (= 5.2.2)
|
actionpack (= 5.2.3)
|
||||||
activesupport (= 5.2.2)
|
activesupport (= 5.2.3)
|
||||||
method_source
|
method_source
|
||||||
rake (>= 0.8.7)
|
rake (>= 0.8.7)
|
||||||
thor (>= 0.19.0, < 2.0)
|
thor (>= 0.19.0, < 2.0)
|
||||||
@ -498,9 +499,9 @@ GEM
|
|||||||
regexp_parser (1.3.0)
|
regexp_parser (1.3.0)
|
||||||
request_store (1.4.1)
|
request_store (1.4.1)
|
||||||
rack (>= 1.4)
|
rack (>= 1.4)
|
||||||
responders (2.4.0)
|
responders (2.4.1)
|
||||||
actionpack (>= 4.2.0, < 5.3)
|
actionpack (>= 4.2.0, < 6.0)
|
||||||
railties (>= 4.2.0, < 5.3)
|
railties (>= 4.2.0, < 6.0)
|
||||||
rotp (2.1.2)
|
rotp (2.1.2)
|
||||||
rpam2 (4.0.2)
|
rpam2 (4.0.2)
|
||||||
rqrcode (0.10.1)
|
rqrcode (0.10.1)
|
||||||
@ -525,14 +526,14 @@ GEM
|
|||||||
rspec-core (~> 3.0, >= 3.0.0)
|
rspec-core (~> 3.0, >= 3.0.0)
|
||||||
sidekiq (>= 2.4.0)
|
sidekiq (>= 2.4.0)
|
||||||
rspec-support (3.8.0)
|
rspec-support (3.8.0)
|
||||||
rubocop (0.63.1)
|
rubocop (0.67.1)
|
||||||
jaro_winkler (~> 1.5.1)
|
jaro_winkler (~> 1.5.1)
|
||||||
parallel (~> 1.10)
|
parallel (~> 1.10)
|
||||||
parser (>= 2.5, != 2.5.1.1)
|
parser (>= 2.5, != 2.5.1.1)
|
||||||
powerpack (~> 0.1)
|
psych (>= 3.1.0)
|
||||||
rainbow (>= 2.2.2, < 4.0)
|
rainbow (>= 2.2.2, < 4.0)
|
||||||
ruby-progressbar (~> 1.7)
|
ruby-progressbar (~> 1.7)
|
||||||
unicode-display_width (~> 1.4.0)
|
unicode-display_width (>= 1.4.0, < 1.6)
|
||||||
ruby-progressbar (1.10.0)
|
ruby-progressbar (1.10.0)
|
||||||
ruby-saml (1.9.0)
|
ruby-saml (1.9.0)
|
||||||
nokogiri (>= 1.5.10)
|
nokogiri (>= 1.5.10)
|
||||||
@ -563,9 +564,9 @@ GEM
|
|||||||
rufus-scheduler (~> 3.2)
|
rufus-scheduler (~> 3.2)
|
||||||
sidekiq (>= 3)
|
sidekiq (>= 3)
|
||||||
tilt (>= 1.4.0)
|
tilt (>= 1.4.0)
|
||||||
sidekiq-unique-jobs (6.0.8)
|
sidekiq-unique-jobs (6.0.12)
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.5)
|
concurrent-ruby (~> 1.0, >= 1.0.5)
|
||||||
sidekiq (>= 4.0, < 6.0)
|
sidekiq (>= 4.0, < 7.0)
|
||||||
thor (~> 0)
|
thor (~> 0)
|
||||||
simple-navigation (4.0.5)
|
simple-navigation (4.0.5)
|
||||||
activesupport (>= 2.3.2)
|
activesupport (>= 2.3.2)
|
||||||
@ -594,14 +595,14 @@ GEM
|
|||||||
multi_json (~> 1.8)
|
multi_json (~> 1.8)
|
||||||
strong_migrations (0.3.1)
|
strong_migrations (0.3.1)
|
||||||
activerecord (>= 3.2.0)
|
activerecord (>= 3.2.0)
|
||||||
temple (0.8.0)
|
temple (0.8.1)
|
||||||
terminal-table (1.8.0)
|
terminal-table (1.8.0)
|
||||||
unicode-display_width (~> 1.1, >= 1.1.1)
|
unicode-display_width (~> 1.1, >= 1.1.1)
|
||||||
terrapin (0.6.0)
|
terrapin (0.6.0)
|
||||||
climate_control (>= 0.0.3, < 1.0)
|
climate_control (>= 0.0.3, < 1.0)
|
||||||
thor (0.20.3)
|
thor (0.20.3)
|
||||||
thread_safe (0.3.6)
|
thread_safe (0.3.6)
|
||||||
tilt (2.0.8)
|
tilt (2.0.9)
|
||||||
timers (4.2.0)
|
timers (4.2.0)
|
||||||
tty-color (0.4.3)
|
tty-color (0.4.3)
|
||||||
tty-command (0.8.2)
|
tty-command (0.8.2)
|
||||||
@ -622,24 +623,24 @@ GEM
|
|||||||
unf (~> 0.1.0)
|
unf (~> 0.1.0)
|
||||||
tzinfo (1.2.5)
|
tzinfo (1.2.5)
|
||||||
thread_safe (~> 0.1)
|
thread_safe (~> 0.1)
|
||||||
tzinfo-data (1.2018.9)
|
tzinfo-data (1.2019.1)
|
||||||
tzinfo (>= 1.0.0)
|
tzinfo (>= 1.0.0)
|
||||||
unf (0.1.4)
|
unf (0.1.4)
|
||||||
unf_ext
|
unf_ext
|
||||||
unf_ext (0.0.7.5)
|
unf_ext (0.0.7.5)
|
||||||
unicode-display_width (1.4.1)
|
unicode-display_width (1.5.0)
|
||||||
uniform_notifier (1.12.1)
|
uniform_notifier (1.12.1)
|
||||||
warden (1.2.7)
|
warden (1.2.8)
|
||||||
rack (>= 1.0)
|
rack (>= 2.0.6)
|
||||||
webmock (3.5.1)
|
webmock (3.5.1)
|
||||||
addressable (>= 2.3.6)
|
addressable (>= 2.3.6)
|
||||||
crack (>= 0.3.2)
|
crack (>= 0.3.2)
|
||||||
hashdiff
|
hashdiff
|
||||||
webpacker (3.5.5)
|
webpacker (4.0.2)
|
||||||
activesupport (>= 4.2)
|
activesupport (>= 4.2)
|
||||||
rack-proxy (>= 0.6.1)
|
rack-proxy (>= 0.6.1)
|
||||||
railties (>= 4.2)
|
railties (>= 4.2)
|
||||||
webpush (0.3.6)
|
webpush (0.3.7)
|
||||||
hkdf (~> 0.2)
|
hkdf (~> 0.2)
|
||||||
jwt (~> 2.0)
|
jwt (~> 2.0)
|
||||||
websocket-driver (0.7.0)
|
websocket-driver (0.7.0)
|
||||||
@ -654,14 +655,14 @@ PLATFORMS
|
|||||||
|
|
||||||
DEPENDENCIES
|
DEPENDENCIES
|
||||||
active_model_serializers (~> 0.10)
|
active_model_serializers (~> 0.10)
|
||||||
active_record_query_trace (~> 1.5)
|
active_record_query_trace (~> 1.6)
|
||||||
addressable (~> 2.6)
|
addressable (~> 2.6)
|
||||||
annotate (~> 2.7)
|
annotate (~> 2.7)
|
||||||
aws-sdk-s3 (~> 1.30)
|
aws-sdk-s3 (~> 1.36)
|
||||||
better_errors (~> 2.5)
|
better_errors (~> 2.5)
|
||||||
binding_of_caller (~> 0.7)
|
binding_of_caller (~> 0.7)
|
||||||
bootsnap (~> 1.3)
|
bootsnap (~> 1.4)
|
||||||
brakeman (~> 4.4)
|
brakeman (~> 4.5)
|
||||||
browser
|
browser
|
||||||
bullet (~> 5.9)
|
bullet (~> 5.9)
|
||||||
bundler-audit (~> 0.6)
|
bundler-audit (~> 0.6)
|
||||||
@ -669,18 +670,18 @@ DEPENDENCIES
|
|||||||
capistrano-rails (~> 1.4)
|
capistrano-rails (~> 1.4)
|
||||||
capistrano-rbenv (~> 2.1)
|
capistrano-rbenv (~> 2.1)
|
||||||
capistrano-yarn (~> 2.0)
|
capistrano-yarn (~> 2.0)
|
||||||
capybara (~> 3.12)
|
capybara (~> 3.16)
|
||||||
charlock_holmes (~> 0.7.6)
|
charlock_holmes (~> 0.7.6)
|
||||||
chewy (~> 5.0)
|
chewy (~> 5.0)
|
||||||
cld3 (~> 3.2.3)
|
cld3 (~> 3.2.3)
|
||||||
climate_control (~> 0.2)
|
climate_control (~> 0.2)
|
||||||
concurrent-ruby
|
concurrent-ruby
|
||||||
derailed_benchmarks
|
derailed_benchmarks
|
||||||
devise (~> 4.5)
|
devise (~> 4.6)
|
||||||
devise-two-factor (~> 3.0)
|
devise-two-factor (~> 3.0)
|
||||||
devise_pam_authenticatable2 (~> 9.2)
|
devise_pam_authenticatable2 (~> 9.2)
|
||||||
doorkeeper (~> 5.0)
|
doorkeeper (~> 5.0)
|
||||||
dotenv-rails (~> 2.6)
|
dotenv-rails (~> 2.7)
|
||||||
fabrication (~> 2.20)
|
fabrication (~> 2.20)
|
||||||
faker (~> 1.9)
|
faker (~> 1.9)
|
||||||
fast_blank (~> 1.0)
|
fast_blank (~> 1.0)
|
||||||
@ -709,7 +710,7 @@ DEPENDENCIES
|
|||||||
makara (~> 0.4)
|
makara (~> 0.4)
|
||||||
mario-redis-lock (~> 1.2)
|
mario-redis-lock (~> 1.2)
|
||||||
memory_profiler
|
memory_profiler
|
||||||
microformats (~> 4.0)
|
microformats (~> 4.1)
|
||||||
mime-types (~> 3.2)
|
mime-types (~> 3.2)
|
||||||
net-ldap (~> 0.10)
|
net-ldap (~> 0.10)
|
||||||
nokogiri (~> 1.10)
|
nokogiri (~> 1.10)
|
||||||
@ -722,20 +723,20 @@ DEPENDENCIES
|
|||||||
ox (~> 2.10)
|
ox (~> 2.10)
|
||||||
paperclip (~> 6.0)
|
paperclip (~> 6.0)
|
||||||
paperclip-av-transcoder (~> 0.6)
|
paperclip-av-transcoder (~> 0.6)
|
||||||
parallel_tests (~> 2.27)
|
parallel_tests (~> 2.28)
|
||||||
pg (~> 1.1)
|
pg (~> 1.1)
|
||||||
pghero (~> 2.2)
|
pghero (~> 2.2)
|
||||||
pkg-config (~> 1.3)
|
pkg-config (~> 1.3)
|
||||||
posix-spawn!
|
posix-spawn!
|
||||||
premailer-rails
|
premailer-rails
|
||||||
private_address_check (~> 0.5)
|
private_address_check (~> 0.5)
|
||||||
pry-byebug (~> 3.6)
|
pry-byebug (~> 3.7)
|
||||||
pry-rails (~> 0.3)
|
pry-rails (~> 0.3)
|
||||||
puma (~> 3.12)
|
puma (~> 3.12)
|
||||||
pundit (~> 2.0)
|
pundit (~> 2.0)
|
||||||
rack-attack (~> 5.4)
|
rack-attack (~> 5.4)
|
||||||
rack-cors (~> 1.0)
|
rack-cors (~> 1.0)
|
||||||
rails (~> 5.2.2)
|
rails (~> 5.2.3)
|
||||||
rails-controller-testing (~> 1.0)
|
rails-controller-testing (~> 1.0)
|
||||||
rails-i18n (~> 5.1)
|
rails-i18n (~> 5.1)
|
||||||
rails-settings-cached (~> 0.6)
|
rails-settings-cached (~> 0.6)
|
||||||
@ -746,7 +747,7 @@ DEPENDENCIES
|
|||||||
rqrcode (~> 0.10)
|
rqrcode (~> 0.10)
|
||||||
rspec-rails (~> 3.8)
|
rspec-rails (~> 3.8)
|
||||||
rspec-sidekiq (~> 3.0)
|
rspec-sidekiq (~> 3.0)
|
||||||
rubocop (~> 0.63)
|
rubocop (~> 0.67)
|
||||||
sanitize (~> 5.0)
|
sanitize (~> 5.0)
|
||||||
scss_lint (~> 0.57)
|
scss_lint (~> 0.57)
|
||||||
sidekiq (~> 5.2)
|
sidekiq (~> 5.2)
|
||||||
@ -765,13 +766,13 @@ DEPENDENCIES
|
|||||||
tty-command (~> 0.8)
|
tty-command (~> 0.8)
|
||||||
tty-prompt (~> 0.18)
|
tty-prompt (~> 0.18)
|
||||||
twitter-text (~> 1.14)
|
twitter-text (~> 1.14)
|
||||||
tzinfo-data (~> 1.2018)
|
tzinfo-data (~> 1.2019)
|
||||||
webmock (~> 3.5)
|
webmock (~> 3.5)
|
||||||
webpacker (~> 3.5)
|
webpacker (~> 4.0)
|
||||||
webpush
|
webpush
|
||||||
|
|
||||||
RUBY VERSION
|
RUBY VERSION
|
||||||
ruby 2.6.0p0
|
ruby 2.6.1p33
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
1.17.3
|
1.17.3
|
||||||
|
13
Vagrantfile
vendored
13
Vagrantfile
vendored
@ -44,7 +44,18 @@ sudo apt-get install \
|
|||||||
|
|
||||||
# Install rvm
|
# Install rvm
|
||||||
read RUBY_VERSION < .ruby-version
|
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
|
curl -sSL https://raw.githubusercontent.com/rvm/rvm/stable/binscripts/rvm-installer | bash -s stable --ruby=$RUBY_VERSION
|
||||||
source /home/vagrant/.rvm/scripts/rvm
|
source /home/vagrant/.rvm/scripts/rvm
|
||||||
|
|
||||||
|
@ -48,6 +48,7 @@ class StatusesIndex < Chewy::Index
|
|||||||
end
|
end
|
||||||
|
|
||||||
root date_detection: false do
|
root date_detection: false do
|
||||||
|
field :id, type: 'long'
|
||||||
field :account_id, type: 'long'
|
field :account_id, type: 'long'
|
||||||
|
|
||||||
field :text, type: 'text', value: ->(status) { [status.spoiler_text, Formatter.instance.plaintext(status)].concat(status.media_attachments.map(&:description)).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
|
||||||
@ -55,7 +56,6 @@ class StatusesIndex < Chewy::Index
|
|||||||
end
|
end
|
||||||
|
|
||||||
field :searchable_by, type: 'long', value: ->(status, crutches) { status.searchable_by(crutches) }
|
field :searchable_by, type: 'long', value: ->(status, crutches) { status.searchable_by(crutches) }
|
||||||
field :created_at, type: 'date'
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -1,26 +1,25 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class AboutController < ApplicationController
|
class AboutController < ApplicationController
|
||||||
before_action :set_body_classes
|
layout 'public'
|
||||||
|
|
||||||
before_action :set_instance_presenter, only: [:show, :more, :terms]
|
before_action :set_instance_presenter, only: [:show, :more, :terms]
|
||||||
|
|
||||||
def show
|
def show
|
||||||
serializable_resource = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(initial_state_params), serializer: InitialStateSerializer)
|
@hide_navbar = true
|
||||||
@initial_state_json = serializable_resource.to_json
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def more
|
def more; end
|
||||||
render layout: 'public'
|
|
||||||
end
|
|
||||||
|
|
||||||
def terms
|
def terms; end
|
||||||
render layout: 'public'
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def new_user
|
def new_user
|
||||||
User.new.tap(&:build_account)
|
User.new.tap do |user|
|
||||||
|
user.build_account
|
||||||
|
user.build_invite_request
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
helper_method :new_user
|
helper_method :new_user
|
||||||
@ -28,15 +27,4 @@ class AboutController < ApplicationController
|
|||||||
def set_instance_presenter
|
def set_instance_presenter
|
||||||
@instance_presenter = InstancePresenter.new
|
@instance_presenter = InstancePresenter.new
|
||||||
end
|
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
|
end
|
||||||
|
@ -10,6 +10,8 @@ class AccountsController < ApplicationController
|
|||||||
def show
|
def show
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html do
|
format.html do
|
||||||
|
mark_cacheable! unless user_signed_in?
|
||||||
|
|
||||||
@body_classes = 'with-modals'
|
@body_classes = 'with-modals'
|
||||||
@pinned_statuses = []
|
@pinned_statuses = []
|
||||||
@endorsed_accounts = @account.endorsed_accounts.to_a.sample(4)
|
@endorsed_accounts = @account.endorsed_accounts.to_a.sample(4)
|
||||||
@ -30,17 +32,21 @@ class AccountsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
format.atom do
|
format.atom do
|
||||||
|
mark_cacheable!
|
||||||
|
|
||||||
@entries = @account.stream_entries.where(hidden: false).with_includes.without_local_only.paginate_by_max_id(PAGE_SIZE, params[:max_id], params[:since_id])
|
@entries = @account.stream_entries.where(hidden: false).with_includes.without_local_only.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? }))
|
render xml: OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.feed(@account, @entries.reject { |entry| entry.status.nil? }))
|
||||||
end
|
end
|
||||||
|
|
||||||
format.rss do
|
format.rss do
|
||||||
|
mark_cacheable!
|
||||||
|
|
||||||
@statuses = cache_collection(default_statuses.without_local_only.without_reblogs.without_replies.limit(PAGE_SIZE), Status)
|
@statuses = cache_collection(default_statuses.without_local_only.without_reblogs.without_replies.limit(PAGE_SIZE), Status)
|
||||||
render xml: RSS::AccountSerializer.render(@account, @statuses)
|
render xml: RSS::AccountSerializer.render(@account, @statuses)
|
||||||
end
|
end
|
||||||
|
|
||||||
format.json do
|
format.json do
|
||||||
skip_session!
|
mark_cacheable!
|
||||||
|
|
||||||
render_cached_json(['activitypub', 'actor', @account], content_type: 'application/activity+json') do
|
render_cached_json(['activitypub', 'actor', @account], content_type: 'application/activity+json') do
|
||||||
ActiveModelSerializers::SerializableResource.new(@account, serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter)
|
ActiveModelSerializers::SerializableResource.new(@account, serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter)
|
||||||
@ -52,11 +58,12 @@ class AccountsController < ApplicationController
|
|||||||
private
|
private
|
||||||
|
|
||||||
def show_pinned_statuses?
|
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
|
end
|
||||||
|
|
||||||
def filtered_statuses
|
def filtered_statuses
|
||||||
default_statuses.tap do |statuses|
|
default_statuses.tap do |statuses|
|
||||||
|
statuses.merge!(hashtag_scope) if tag_requested?
|
||||||
statuses.merge!(only_media_scope) if media_requested?
|
statuses.merge!(only_media_scope) if media_requested?
|
||||||
statuses.merge!(no_replies_scope) unless replies_requested?
|
statuses.merge!(no_replies_scope) unless replies_requested?
|
||||||
end
|
end
|
||||||
@ -82,12 +89,21 @@ class AccountsController < ApplicationController
|
|||||||
Status.without_replies
|
Status.without_replies
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_account
|
def hashtag_scope
|
||||||
@account = Account.find_local!(params[:username])
|
tag = Tag.find_normalized(params[:tag])
|
||||||
|
|
||||||
|
if tag
|
||||||
|
Status.tagged_with(tag.id)
|
||||||
|
else
|
||||||
|
Status.none
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def username_param
|
||||||
|
params[:username]
|
||||||
end
|
end
|
||||||
|
|
||||||
def older_url
|
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)
|
pagination_url(max_id: @statuses.last.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -96,7 +112,9 @@ class AccountsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def pagination_url(max_id: nil, min_id: nil)
|
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)
|
short_account_media_url(@account, max_id: max_id, min_id: min_id)
|
||||||
elsif replies_requested?
|
elsif replies_requested?
|
||||||
short_account_with_replies_url(@account, max_id: max_id, min_id: min_id)
|
short_account_with_replies_url(@account, max_id: max_id, min_id: min_id)
|
||||||
@ -113,6 +131,10 @@ class AccountsController < ApplicationController
|
|||||||
request.path.ends_with?('/with_replies')
|
request.path.ends_with?('/with_replies')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def tag_requested?
|
||||||
|
request.path.ends_with?(Addressable::URI.parse("/tagged/#{params[:tag]}").normalize)
|
||||||
|
end
|
||||||
|
|
||||||
def filtered_status_page(params)
|
def filtered_status_page(params)
|
||||||
if params[:min_id].present?
|
if params[:min_id].present?
|
||||||
filtered_statuses.paginate_by_min_id(PAGE_SIZE, params[:min_id]).reverse
|
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_account
|
||||||
before_action :set_size
|
before_action :set_size
|
||||||
before_action :set_statuses
|
before_action :set_statuses
|
||||||
|
before_action :set_cache_headers
|
||||||
|
|
||||||
def show
|
def show
|
||||||
render json: collection_presenter,
|
skip_session!
|
||||||
|
|
||||||
|
render_cached_json(['activitypub', 'collection', @account, params[:id]], content_type: 'application/activity+json') do
|
||||||
|
ActiveModelSerializers::SerializableResource.new(
|
||||||
|
collection_presenter,
|
||||||
serializer: ActivityPub::CollectionSerializer,
|
serializer: ActivityPub::CollectionSerializer,
|
||||||
adapter: ActivityPub::Adapter,
|
adapter: ActivityPub::Adapter,
|
||||||
content_type: 'application/activity+json',
|
|
||||||
skip_activities: true
|
skip_activities: true
|
||||||
|
)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
@ -2,11 +2,14 @@
|
|||||||
|
|
||||||
class ActivityPub::InboxesController < Api::BaseController
|
class ActivityPub::InboxesController < Api::BaseController
|
||||||
include SignatureVerification
|
include SignatureVerification
|
||||||
|
include JsonLdHelper
|
||||||
|
|
||||||
before_action :set_account
|
before_action :set_account
|
||||||
|
|
||||||
def create
|
def create
|
||||||
if signed_request_account
|
if unknown_deleted_account?
|
||||||
|
head 202
|
||||||
|
elsif signed_request_account
|
||||||
upgrade_account
|
upgrade_account
|
||||||
process_payload
|
process_payload
|
||||||
head 202
|
head 202
|
||||||
@ -17,12 +20,22 @@ class ActivityPub::InboxesController < Api::BaseController
|
|||||||
|
|
||||||
private
|
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
|
def set_account
|
||||||
@account = Account.find_local!(params[:account_username]) if params[:account_username]
|
@account = Account.find_local!(params[:account_username]) if params[:account_username]
|
||||||
end
|
end
|
||||||
|
|
||||||
def body
|
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
|
end
|
||||||
|
|
||||||
def upgrade_account
|
def upgrade_account
|
||||||
@ -36,6 +49,6 @@ class ActivityPub::InboxesController < Api::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def process_payload
|
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
|
||||||
end
|
end
|
||||||
|
@ -7,8 +7,14 @@ class ActivityPub::OutboxesController < Api::BaseController
|
|||||||
|
|
||||||
before_action :set_account
|
before_action :set_account
|
||||||
before_action :set_statuses
|
before_action :set_statuses
|
||||||
|
before_action :set_cache_headers
|
||||||
|
|
||||||
def show
|
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'
|
render json: outbox_presenter, serializer: ActivityPub::OutboxSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
module Admin
|
module Admin
|
||||||
class AccountsController < BaseController
|
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_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
|
def index
|
||||||
authorize :account, :index?
|
authorize :account, :index?
|
||||||
@ -45,6 +45,18 @@ module Admin
|
|||||||
redirect_to admin_account_path(@account.id)
|
redirect_to admin_account_path(@account.id)
|
||||||
end
|
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
|
def unsilence
|
||||||
authorize @account, :unsilence?
|
authorize @account, :unsilence?
|
||||||
@account.unsilence!
|
@account.unsilence!
|
||||||
@ -114,6 +126,7 @@ module Admin
|
|||||||
:remote,
|
:remote,
|
||||||
:by_domain,
|
:by_domain,
|
||||||
:active,
|
:active,
|
||||||
|
:pending,
|
||||||
:silenced,
|
:silenced,
|
||||||
:suspended,
|
:suspended,
|
||||||
:username,
|
:username,
|
||||||
|
@ -5,6 +5,9 @@ module Admin
|
|||||||
before_action :set_custom_emoji, except: [:index, :new, :create]
|
before_action :set_custom_emoji, except: [:index, :new, :create]
|
||||||
before_action :set_filter_params
|
before_action :set_filter_params
|
||||||
|
|
||||||
|
include ObfuscateFilename
|
||||||
|
obfuscate_filename [:custom_emoji, :image]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
authorize :custom_emoji, :index?
|
authorize :custom_emoji, :index?
|
||||||
@custom_emojis = filtered_custom_emojis.eager_load(:local_counterpart).page(params[:page])
|
@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
|
@interactions_week = Redis.current.get("activity:interactions:#{current_week}") || 0
|
||||||
@relay_enabled = Relay.enabled.exists?
|
@relay_enabled = Relay.enabled.exists?
|
||||||
@single_user_mode = Rails.configuration.x.single_user_mode
|
@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
|
@deletions_enabled = Setting.open_deletion
|
||||||
@invites_enabled = Setting.min_invite_role == 'user'
|
@invites_enabled = Setting.min_invite_role == 'user'
|
||||||
@search_enabled = Chewy.enabled?
|
@search_enabled = Chewy.enabled?
|
||||||
@ -29,6 +29,7 @@ module Admin
|
|||||||
@hidden_service = ENV['ALLOW_ACCESS_TO_HIDDEN_SERVICE'] == 'true'
|
@hidden_service = ENV['ALLOW_ACCESS_TO_HIDDEN_SERVICE'] == 'true'
|
||||||
@trending_hashtags = TrendingTags.get(7)
|
@trending_hashtags = TrendingTags.get(7)
|
||||||
@profile_directory = Setting.profile_directory
|
@profile_directory = Setting.profile_directory
|
||||||
|
@timeline_preview = Setting.timeline_preview
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
@ -38,7 +38,7 @@ module Admin
|
|||||||
end
|
end
|
||||||
|
|
||||||
def filter_params
|
def filter_params
|
||||||
params.permit(:limited)
|
params.permit(:limited, :by_domain)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
52
app/controllers/admin/pending_accounts_controller.rb
Normal file
52
app/controllers/admin/pending_accounts_controller.rb
Normal file
@ -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
|
@ -10,6 +10,10 @@ module Admin
|
|||||||
@form = Form::StatusBatch.new(form_status_batch_params.merge(current_account: current_account, action: action_from_button))
|
@form = Form::StatusBatch.new(form_status_batch_params.merge(current_account: current_account, action: action_from_button))
|
||||||
flash[:alert] = I18n.t('admin.statuses.failed_to_execute') unless @form.save
|
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)
|
redirect_to admin_report_path(@report)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -2,85 +2,29 @@
|
|||||||
|
|
||||||
module Admin
|
module Admin
|
||||||
class SettingsController < BaseController
|
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
|
def edit
|
||||||
authorize :settings, :show?
|
authorize :settings, :show?
|
||||||
|
|
||||||
@admin_settings = Form::AdminSettings.new
|
@admin_settings = Form::AdminSettings.new
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
authorize :settings, :update?
|
authorize :settings, :update?
|
||||||
|
|
||||||
settings_params.each do |key, value|
|
@admin_settings = Form::AdminSettings.new(settings_params)
|
||||||
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
|
|
||||||
|
|
||||||
|
if @admin_settings.save
|
||||||
flash[:notice] = I18n.t('generic.changes_saved_msg')
|
flash[:notice] = I18n.t('generic.changes_saved_msg')
|
||||||
redirect_to edit_admin_settings_path
|
redirect_to edit_admin_settings_path
|
||||||
|
else
|
||||||
|
render :edit
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def settings_params
|
def settings_params
|
||||||
params.require(:form_admin_settings).permit(ADMIN_SETTINGS)
|
params.require(:form_admin_settings).permit(*Form::AdminSettings::KEYS)
|
||||||
end
|
|
||||||
|
|
||||||
def value_for_update(key, value)
|
|
||||||
if BOOLEAN_SETTINGS.include?(key)
|
|
||||||
value == '1'
|
|
||||||
else
|
|
||||||
value
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -73,7 +73,9 @@ class Api::BaseController < ApplicationController
|
|||||||
elsif current_user.disabled?
|
elsif current_user.disabled?
|
||||||
render json: { error: 'Your login is currently disabled' }, status: 403
|
render json: { error: 'Your login is currently disabled' }, status: 403
|
||||||
elsif !current_user.confirmed?
|
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
|
else
|
||||||
set_user_activity
|
set_user_activity
|
||||||
end
|
end
|
||||||
|
30
app/controllers/api/proofs_controller.rb
Normal file
30
app/controllers/api/proofs_controller.rb
Normal file
@ -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
|
end
|
||||||
|
|
||||||
def load_accounts
|
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
|
default_accounts.merge(paginated_follows).to_a
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def hide_results?
|
||||||
|
(@account.user_hides_network? && current_account.id != @account.id) || (current_account && @account.blocking?(current_account))
|
||||||
|
end
|
||||||
|
|
||||||
def default_accounts
|
def default_accounts
|
||||||
Account.includes(:active_relationships, :account_stat).references(:active_relationships)
|
Account.includes(:active_relationships, :account_stat).references(:active_relationships)
|
||||||
end
|
end
|
||||||
|
@ -19,11 +19,15 @@ class Api::V1::Accounts::FollowingAccountsController < Api::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def load_accounts
|
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
|
default_accounts.merge(paginated_follows).to_a
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def hide_results?
|
||||||
|
(@account.user_hides_network? && current_account.id != @account.id) || (current_account && @account.blocking?(current_account))
|
||||||
|
end
|
||||||
|
|
||||||
def default_accounts
|
def default_accounts
|
||||||
Account.includes(:passive_relationships, :account_stat).references(:passive_relationships)
|
Account.includes(:passive_relationships, :account_stat).references(:passive_relationships)
|
||||||
end
|
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
|
def account_search
|
||||||
AccountSearchService.new.call(
|
AccountSearchService.new.call(
|
||||||
params[:q],
|
params[:q],
|
||||||
limit_param(DEFAULT_ACCOUNTS_LIMIT),
|
|
||||||
current_account,
|
current_account,
|
||||||
|
limit: limit_param(DEFAULT_ACCOUNTS_LIMIT),
|
||||||
resolve: truthy_param?(:resolve),
|
resolve: truthy_param?(:resolve),
|
||||||
following: truthy_param?(:following)
|
following: truthy_param?(:following),
|
||||||
|
offset: params[:offset]
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
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!(only_media_scope) if truthy_param?(:only_media)
|
||||||
statuses.merge!(no_replies_scope) if truthy_param?(:exclude_replies)
|
statuses.merge!(no_replies_scope) if truthy_param?(:exclude_replies)
|
||||||
statuses.merge!(no_reblogs_scope) if truthy_param?(:exclude_reblogs)
|
statuses.merge!(no_reblogs_scope) if truthy_param?(:exclude_reblogs)
|
||||||
|
statuses.merge!(hashtag_scope) if params[:tagged].present?
|
||||||
|
|
||||||
statuses
|
statuses
|
||||||
end
|
end
|
||||||
@ -50,7 +51,7 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
|
|||||||
# Also, Avoid getting slow by not narrowing down by `statuses.account_id`.
|
# 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
|
# 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.
|
# 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)
|
@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])
|
.paginate_by_max_id(limit_param(DEFAULT_STATUSES_LIMIT), params[:max_id], params[:since_id])
|
||||||
.reorder(id: :desc).distinct(:id).pluck(:id)
|
.reorder(id: :desc).distinct(:id).pluck(:id)
|
||||||
end
|
end
|
||||||
@ -67,6 +68,16 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
|
|||||||
Status.without_reblogs
|
Status.without_reblogs
|
||||||
end
|
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)
|
def pagination_params(core_params)
|
||||||
params.slice(:limit, :only_media, :exclude_replies).permit(:limit, :only_media, :exclude_replies).merge(core_params)
|
params.slice(:limit, :only_media, :exclude_replies).permit(:limit, :only_media, :exclude_replies).merge(core_params)
|
||||||
end
|
end
|
||||||
|
@ -80,6 +80,10 @@ class Api::V1::AccountsController < Api::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def check_enabled_registrations
|
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
|
||||||
end
|
end
|
||||||
|
29
app/controllers/api/v1/polls/votes_controller.rb
Normal file
29
app/controllers/api/v1/polls/votes_controller.rb
Normal file
@ -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
|
13
app/controllers/api/v1/polls_controller.rb
Normal file
13
app/controllers/api/v1/polls_controller.rb
Normal file
@ -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
|
12
app/controllers/api/v1/preferences_controller.rb
Normal file
12
app/controllers/api/v1/preferences_controller.rb
Normal file
@ -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
|
class Api::V1::SearchController < Api::BaseController
|
||||||
include Authorization
|
include Authorization
|
||||||
|
|
||||||
RESULTS_LIMIT = 5
|
RESULTS_LIMIT = 20
|
||||||
|
|
||||||
before_action -> { doorkeeper_authorize! :read, :'read:search' }
|
before_action -> { doorkeeper_authorize! :read, :'read:search' }
|
||||||
before_action :require_user!
|
before_action :require_user!
|
||||||
@ -11,30 +11,22 @@ class Api::V1::SearchController < Api::BaseController
|
|||||||
respond_to :json
|
respond_to :json
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@search = Search.new(search)
|
@search = Search.new(search_results)
|
||||||
render json: @search, serializer: REST::SearchSerializer
|
render json: @search, serializer: REST::SearchSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
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
|
def search_results
|
||||||
SearchService.new.call(
|
SearchService.new.call(
|
||||||
params[:q],
|
params[:q],
|
||||||
RESULTS_LIMIT,
|
current_account,
|
||||||
truthy_param?(:resolve),
|
limit_param(RESULTS_LIMIT),
|
||||||
current_account
|
search_params.merge(resolve: truthy_param?(:resolve))
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def search_params
|
||||||
|
params.permit(:type, :offset, :min_id, :max_id, :account_id)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -9,7 +9,7 @@ class Api::V1::Statuses::ReblogsController < Api::BaseController
|
|||||||
respond_to :json
|
respond_to :json
|
||||||
|
|
||||||
def create
|
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
|
render json: @status, serializer: REST::StatusSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -32,4 +32,8 @@ class Api::V1::Statuses::ReblogsController < Api::BaseController
|
|||||||
def status_for_destroy
|
def status_for_destroy
|
||||||
current_user.account.statuses.where(reblog_of_id: params[:status_id]).first!
|
current_user.account.statuses.where(reblog_of_id: params[:status_id]).first!
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def reblog_params
|
||||||
|
params.permit(:visibility)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -53,6 +53,7 @@ class Api::V1::StatusesController < Api::BaseController
|
|||||||
visibility: status_params[:visibility],
|
visibility: status_params[:visibility],
|
||||||
scheduled_at: status_params[:scheduled_at],
|
scheduled_at: status_params[:scheduled_at],
|
||||||
application: doorkeeper_token.application,
|
application: doorkeeper_token.application,
|
||||||
|
poll: status_params[:poll],
|
||||||
idempotency: request.headers['Idempotency-Key'],
|
idempotency: request.headers['Idempotency-Key'],
|
||||||
local_only: status_params[:local_only])
|
local_only: status_params[:local_only])
|
||||||
|
|
||||||
@ -74,12 +75,26 @@ class Api::V1::StatusesController < Api::BaseController
|
|||||||
@status = Status.find(params[:id])
|
@status = Status.find(params[:id])
|
||||||
authorize @status, :show?
|
authorize @status, :show?
|
||||||
rescue Mastodon::NotPermittedError
|
rescue Mastodon::NotPermittedError
|
||||||
# Reraise in order to get a 404 instead of a 403 error code
|
|
||||||
raise ActiveRecord::RecordNotFound
|
raise ActiveRecord::RecordNotFound
|
||||||
end
|
end
|
||||||
|
|
||||||
def status_params
|
def status_params
|
||||||
params.permit(:status, :in_reply_to_id, :sensitive, :spoiler_text, :visibility, :scheduled_at, :local_only, media_ids: [])
|
params.permit(
|
||||||
|
:status,
|
||||||
|
:in_reply_to_id,
|
||||||
|
:sensitive,
|
||||||
|
:spoiler_text,
|
||||||
|
:visibility,
|
||||||
|
:scheduled_at,
|
||||||
|
:local_only,
|
||||||
|
media_ids: [],
|
||||||
|
poll: [
|
||||||
|
:multiple,
|
||||||
|
:hide_totals,
|
||||||
|
:expires_in,
|
||||||
|
options: [],
|
||||||
|
]
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def pagination_params(core_params)
|
def pagination_params(core_params)
|
||||||
|
@ -14,7 +14,7 @@ class Api::V1::Timelines::TagController < Api::BaseController
|
|||||||
private
|
private
|
||||||
|
|
||||||
def load_tag
|
def load_tag
|
||||||
@tag = Tag.find_by(name: params[:id].downcase)
|
@tag = Tag.find_normalized(params[:id])
|
||||||
end
|
end
|
||||||
|
|
||||||
def load_statuses
|
def load_statuses
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
class Api::V2::SearchController < Api::V1::SearchController
|
class Api::V2::SearchController < Api::V1::SearchController
|
||||||
def index
|
def index
|
||||||
@search = Search.new(search)
|
@search = Search.new(search_results)
|
||||||
render json: @search, serializer: REST::V2::SearchSerializer
|
render json: @search, serializer: REST::V2::SearchSerializer
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -151,6 +151,11 @@ class ApplicationController < ActionController::Base
|
|||||||
response.headers['Vary'] = 'Accept'
|
response.headers['Vary'] = 'Accept'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def mark_cacheable!
|
||||||
|
skip_session!
|
||||||
|
expires_in 0, public: true
|
||||||
|
end
|
||||||
|
|
||||||
def skip_session!
|
def skip_session!
|
||||||
request.session_options[:skip] = true
|
request.session_options[:skip] = true
|
||||||
end
|
end
|
||||||
|
@ -10,6 +10,10 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
|||||||
before_action :set_instance_presenter, only: [:new, :create, :update]
|
before_action :set_instance_presenter, only: [:new, :create, :update]
|
||||||
before_action :set_body_classes, only: [:new, :create, :edit, :update]
|
before_action :set_body_classes, only: [:new, :create, :edit, :update]
|
||||||
|
|
||||||
|
def new
|
||||||
|
super(&:build_invite_request)
|
||||||
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
not_found
|
not_found
|
||||||
end
|
end
|
||||||
@ -27,14 +31,14 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
|||||||
resource.locale = I18n.locale
|
resource.locale = I18n.locale
|
||||||
resource.invite_code = params[:invite_code] if resource.invite_code.blank?
|
resource.invite_code = params[:invite_code] if resource.invite_code.blank?
|
||||||
resource.agreement = true
|
resource.agreement = true
|
||||||
|
resource.current_sign_in_ip = request.remote_ip
|
||||||
|
|
||||||
resource.current_sign_in_ip = request.remote_ip if resource.current_sign_in_ip.nil?
|
|
||||||
resource.build_account if resource.account.nil?
|
resource.build_account if resource.account.nil?
|
||||||
end
|
end
|
||||||
|
|
||||||
def configure_sign_up_params
|
def configure_sign_up_params
|
||||||
devise_parameter_sanitizer.permit(:sign_up) do |u|
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -65,7 +69,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def allowed_registrations?
|
def allowed_registrations?
|
||||||
Setting.open_registrations || @invite&.valid_for_use?
|
Setting.registrations_mode != 'none' || @invite&.valid_for_use?
|
||||||
end
|
end
|
||||||
|
|
||||||
def invite_code
|
def invite_code
|
||||||
|
@ -7,16 +7,18 @@ module AccountControllerConcern
|
|||||||
|
|
||||||
included do
|
included do
|
||||||
layout 'public'
|
layout 'public'
|
||||||
|
|
||||||
before_action :set_account
|
before_action :set_account
|
||||||
|
before_action :check_account_approval
|
||||||
|
before_action :check_account_suspension
|
||||||
before_action :set_instance_presenter
|
before_action :set_instance_presenter
|
||||||
before_action :set_link_headers
|
before_action :set_link_headers
|
||||||
before_action :check_account_suspension
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_account
|
def set_account
|
||||||
@account = Account.find_local!(params[:account_username])
|
@account = Account.find_local!(username_param)
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_instance_presenter
|
def set_instance_presenter
|
||||||
@ -33,6 +35,10 @@ module AccountControllerConcern
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def username_param
|
||||||
|
params[:account_username]
|
||||||
|
end
|
||||||
|
|
||||||
def webfinger_account_link
|
def webfinger_account_link
|
||||||
[
|
[
|
||||||
webfinger_account_url,
|
webfinger_account_url,
|
||||||
@ -58,7 +64,15 @@ module AccountControllerConcern
|
|||||||
webfinger_url(resource: @account.to_webfinger_s)
|
webfinger_url(resource: @account.to_webfinger_s)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def check_account_approval
|
||||||
|
not_found if @account.user_pending?
|
||||||
|
end
|
||||||
|
|
||||||
def check_account_suspension
|
def check_account_suspension
|
||||||
gone if @account.suspended?
|
if @account.suspended?
|
||||||
|
skip_session!
|
||||||
|
expires_in(3.minutes, public: true)
|
||||||
|
gone
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -32,7 +32,7 @@ class DirectoriesController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def set_accounts
|
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
|
query.merge!(Account.tagged_with(@tag.id)) if @tag
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -3,9 +3,13 @@
|
|||||||
class FollowerAccountsController < ApplicationController
|
class FollowerAccountsController < ApplicationController
|
||||||
include AccountControllerConcern
|
include AccountControllerConcern
|
||||||
|
|
||||||
|
before_action :set_cache_headers
|
||||||
|
|
||||||
def index
|
def index
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html do
|
format.html do
|
||||||
|
mark_cacheable! unless user_signed_in?
|
||||||
|
|
||||||
next if @account.user_hides_network?
|
next if @account.user_hides_network?
|
||||||
|
|
||||||
follows
|
follows
|
||||||
@ -15,6 +19,11 @@ class FollowerAccountsController < ApplicationController
|
|||||||
format.json do
|
format.json do
|
||||||
raise Mastodon::NotPermittedError if params[:page].present? && @account.user_hides_network?
|
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,
|
render json: collection_presenter,
|
||||||
serializer: ActivityPub::CollectionSerializer,
|
serializer: ActivityPub::CollectionSerializer,
|
||||||
adapter: ActivityPub::Adapter,
|
adapter: ActivityPub::Adapter,
|
||||||
|
@ -3,9 +3,13 @@
|
|||||||
class FollowingAccountsController < ApplicationController
|
class FollowingAccountsController < ApplicationController
|
||||||
include AccountControllerConcern
|
include AccountControllerConcern
|
||||||
|
|
||||||
|
before_action :set_cache_headers
|
||||||
|
|
||||||
def index
|
def index
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html do
|
format.html do
|
||||||
|
mark_cacheable! unless user_signed_in?
|
||||||
|
|
||||||
next if @account.user_hides_network?
|
next if @account.user_hides_network?
|
||||||
|
|
||||||
follows
|
follows
|
||||||
@ -15,6 +19,11 @@ class FollowingAccountsController < ApplicationController
|
|||||||
format.json do
|
format.json do
|
||||||
raise Mastodon::NotPermittedError if params[:page].present? && @account.user_hides_network?
|
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,
|
render json: collection_presenter,
|
||||||
serializer: ActivityPub::CollectionSerializer,
|
serializer: ActivityPub::CollectionSerializer,
|
||||||
adapter: ActivityPub::Adapter,
|
adapter: ActivityPub::Adapter,
|
||||||
|
@ -50,7 +50,7 @@ class HomeController < ApplicationController
|
|||||||
push_subscription: current_account.user.web_push_subscription(current_session),
|
push_subscription: current_account.user.web_push_subscription(current_session),
|
||||||
current_account: current_account,
|
current_account: current_account,
|
||||||
token: current_session.token,
|
token: current_session.token,
|
||||||
admin: Account.find_local(Setting.site_contact_username),
|
admin: Account.find_local(Setting.site_contact_username.strip.gsub(/\A@/, '')),
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
34
app/controllers/public_timelines_controller.rb
Normal file
34
app/controllers/public_timelines_controller.rb
Normal file
@ -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
|
104
app/controllers/relationships_controller.rb
Normal file
104
app/controllers/relationships_controller.rb
Normal file
@ -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
|
end
|
||||||
|
|
||||||
def create
|
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!
|
backup = current_user.backups.create!
|
||||||
|
else
|
||||||
|
raise Mastodon::RaceConditionError
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
BackupWorker.perform_async(backup.id)
|
BackupWorker.perform_async(backup.id)
|
||||||
|
|
||||||
redirect_to settings_export_path
|
redirect_to settings_export_path
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def lock_options
|
||||||
|
{ redis: Redis.current, key: "backup:#{current_user.id}" }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
51
app/controllers/settings/featured_tags_controller.rb
Normal file
51
app/controllers/settings/featured_tags_controller.rb
Normal file
@ -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
|
|
63
app/controllers/settings/identity_proofs_controller.rb
Normal file
63
app/controllers/settings/identity_proofs_controller.rb
Normal file
@ -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
|
@ -49,7 +49,8 @@ class Settings::PreferencesController < Settings::BaseController
|
|||||||
:setting_theme,
|
:setting_theme,
|
||||||
:setting_hide_network,
|
:setting_hide_network,
|
||||||
:setting_aggregate_reblogs,
|
: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)
|
interactions: %i(must_be_follower must_be_following)
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
@ -32,6 +32,6 @@ class Settings::ProfilesController < Settings::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def set_account
|
def set_account
|
||||||
@account = current_user.account
|
@account = current_account
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Settings::SessionsController < Settings::BaseController
|
class Settings::SessionsController < Settings::BaseController
|
||||||
|
before_action :authenticate_user!
|
||||||
before_action :set_session, only: :destroy
|
before_action :set_session, only: :destroy
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
|
@ -21,7 +21,7 @@ class SharesController < ApplicationController
|
|||||||
push_subscription: current_account.user.web_push_subscription(current_session),
|
push_subscription: current_account.user.web_push_subscription(current_session),
|
||||||
current_account: current_account,
|
current_account: current_account,
|
||||||
token: current_session.token,
|
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,
|
text: text,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
@ -18,6 +18,7 @@ class StatusesController < ApplicationController
|
|||||||
before_action :redirect_to_original, only: [:show]
|
before_action :redirect_to_original, only: [:show]
|
||||||
before_action :set_referrer_policy_header, only: [:show]
|
before_action :set_referrer_policy_header, only: [:show]
|
||||||
before_action :set_cache_headers
|
before_action :set_cache_headers
|
||||||
|
before_action :set_replies, only: [:replies]
|
||||||
|
|
||||||
content_security_policy only: :embed do |p|
|
content_security_policy only: :embed do |p|
|
||||||
p.frame_ancestors(false)
|
p.frame_ancestors(false)
|
||||||
@ -26,6 +27,8 @@ class StatusesController < ApplicationController
|
|||||||
def show
|
def show
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html do
|
format.html do
|
||||||
|
mark_cacheable! unless user_signed_in?
|
||||||
|
|
||||||
@body_classes = 'with-modals'
|
@body_classes = 'with-modals'
|
||||||
|
|
||||||
set_ancestors
|
set_ancestors
|
||||||
@ -35,7 +38,7 @@ class StatusesController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
format.json do
|
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
|
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)
|
ActiveModelSerializers::SerializableResource.new(@status, serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter)
|
||||||
@ -63,8 +66,37 @@ class StatusesController < ApplicationController
|
|||||||
render 'stream_entries/embed', layout: 'embedded'
|
render 'stream_entries/embed', layout: 'embedded'
|
||||||
end
|
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
|
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)
|
def create_descendant_thread(starting_depth, statuses)
|
||||||
depth = starting_depth + statuses.size
|
depth = starting_depth + statuses.size
|
||||||
if depth < DESCENDANTS_DEPTH_LIMIT
|
if depth < DESCENDANTS_DEPTH_LIMIT
|
||||||
@ -174,4 +206,27 @@ class StatusesController < ApplicationController
|
|||||||
return if @status.public_visibility? || @status.unlisted_visibility?
|
return if @status.public_visibility? || @status.unlisted_visibility?
|
||||||
response.headers['Referrer-Policy'] = 'origin'
|
response.headers['Referrer-Policy'] = 'origin'
|
||||||
end
|
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
|
end
|
||||||
|
@ -9,12 +9,14 @@ class TagsController < ApplicationController
|
|||||||
before_action :set_instance_presenter
|
before_action :set_instance_presenter
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@tag = Tag.find_by!(name: params[:id].downcase)
|
@tag = Tag.find_normalized!(params[:id])
|
||||||
|
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html do
|
format.html do
|
||||||
serializable_resource = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(initial_state_params), serializer: InitialStateSerializer)
|
@initial_state_json = ActiveModelSerializers::SerializableResource.new(
|
||||||
@initial_state_json = serializable_resource.to_json
|
InitialStatePresenter.new(settings: {}, token: current_session&.token),
|
||||||
|
serializer: InitialStateSerializer
|
||||||
|
).to_json
|
||||||
end
|
end
|
||||||
|
|
||||||
format.rss do
|
format.rss do
|
||||||
@ -25,8 +27,7 @@ class TagsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
format.json do
|
format.json do
|
||||||
@statuses = HashtagQueryService.new.call(@tag, params.slice(:any, :all, :none), current_account, params[:local])
|
@statuses = HashtagQueryService.new.call(@tag, params.slice(:any, :all, :none), current_account, params[:local]).paginate_by_max_id(PAGE_SIZE, params[:max_id])
|
||||||
.paginate_by_max_id(PAGE_SIZE, params[:max_id])
|
|
||||||
@statuses = cache_collection(@statuses, Status)
|
@statuses = cache_collection(@statuses, Status)
|
||||||
|
|
||||||
render json: collection_presenter,
|
render json: collection_presenter,
|
||||||
@ -55,11 +56,4 @@ class TagsController < ApplicationController
|
|||||||
items: @statuses.map { |s| ActivityPub::TagManager.instance.uri_for(s) }
|
items: @statuses.map { |s| ActivityPub::TagManager.instance.uri_for(s) }
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def initial_state_params
|
|
||||||
{
|
|
||||||
settings: {},
|
|
||||||
token: current_session&.token,
|
|
||||||
}
|
|
||||||
end
|
|
||||||
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
|
||||||
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)
|
def relevant_log_changes(log)
|
||||||
if log.target_type == 'CustomEmoji' && [:enable, :disable, :destroy].include?(log.action)
|
if log.target_type == 'CustomEmoji' && [:enable, :disable, :destroy].include?(log.action)
|
||||||
log.recorded_changes.slice('domain')
|
log.recorded_changes.slice('domain')
|
||||||
@ -111,4 +75,40 @@ module Admin::ActionLogsHelper
|
|||||||
def opposite_verbs?(log)
|
def opposite_verbs?(log)
|
||||||
%w(DomainBlock EmailDomainBlock AccountWarning).include?(log.target_type)
|
%w(DomainBlock EmailDomainBlock AccountWarning).include?(log.target_type)
|
||||||
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
|
||||||
end
|
end
|
||||||
|
10
app/helpers/admin/dashboard_helper.rb
Normal file
10
app/helpers/admin/dashboard_helper.rb
Normal file
@ -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
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module Admin::FilterHelper
|
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
|
REPORT_FILTERS = %i(resolved account_id target_account_id).freeze
|
||||||
INVITE_FILTER = %i(available expired).freeze
|
INVITE_FILTER = %i(available expired).freeze
|
||||||
CUSTOM_EMOJI_FILTERS = %i(local remote by_domain shortcode).freeze
|
CUSTOM_EMOJI_FILTERS = %i(local remote by_domain shortcode).freeze
|
||||||
TAGS_FILTERS = %i(hidden).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)
|
def filter_link_to(text, link_to_params, link_class_params = link_to_params)
|
||||||
new_url = filtered_url_for(link_to_params)
|
new_url = filtered_url_for(link_to_params)
|
||||||
|
@ -20,7 +20,23 @@ module ApplicationHelper
|
|||||||
end
|
end
|
||||||
|
|
||||||
def open_registrations?
|
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
|
end
|
||||||
|
|
||||||
def open_deletion?
|
def open_deletion?
|
||||||
@ -101,4 +117,9 @@ module ApplicationHelper
|
|||||||
def storage_host?
|
def storage_host?
|
||||||
ENV['S3_ALIAS_HOST'].present? || ENV['S3_CLOUDFRONT_HOST'].present?
|
ENV['S3_ALIAS_HOST'].present? || ENV['S3_CLOUDFRONT_HOST'].present?
|
||||||
end
|
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
|
end
|
||||||
|
@ -56,4 +56,22 @@ module HomeHelper
|
|||||||
'emojify'
|
'emojify'
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
@ -47,6 +47,15 @@ module JsonLdHelper
|
|||||||
!uri.start_with?('http://', 'https://')
|
!uri.start_with?('http://', 'https://')
|
||||||
end
|
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)
|
def canonicalize(json)
|
||||||
graph = RDF::Graph.new << JSON::LD::API.toRdf(json, documentLoader: method(:load_jsonld_context))
|
graph = RDF::Graph.new << JSON::LD::API.toRdf(json, documentLoader: method(:load_jsonld_context))
|
||||||
graph.dump(:normalize)
|
graph.dump(:normalize)
|
||||||
@ -63,12 +72,19 @@ module JsonLdHelper
|
|||||||
json.present? && json['id'] == uri ? json : nil
|
json.present? && json['id'] == uri ? json : nil
|
||||||
end
|
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|
|
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
|
return body_to_json(response.body_with_limit) if response.code == 200
|
||||||
end
|
end
|
||||||
# If request failed, retry without doing it on behalf of a user
|
# If request failed, retry without doing it on behalf of a user
|
||||||
|
return if on_behalf_of.nil?
|
||||||
build_request(uri).perform do |response|
|
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
|
response.code == 200 ? body_to_json(response.body_with_limit) : nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -91,6 +107,14 @@ module JsonLdHelper
|
|||||||
|
|
||||||
private
|
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)
|
def build_request(uri, on_behalf_of = nil)
|
||||||
request = Request.new(:get, uri)
|
request = Request.new(:get, uri)
|
||||||
request.on_behalf_of(on_behalf_of) if on_behalf_of
|
request.on_behalf_of(on_behalf_of) if on_behalf_of
|
||||||
|
@ -4,8 +4,9 @@ module SettingsHelper
|
|||||||
HUMAN_LOCALES = {
|
HUMAN_LOCALES = {
|
||||||
en: 'English',
|
en: 'English',
|
||||||
ar: 'العربية',
|
ar: 'العربية',
|
||||||
ast: 'l\'asturianu',
|
ast: 'Asturianu',
|
||||||
bg: 'Български',
|
bg: 'Български',
|
||||||
|
bn: 'বাংলা',
|
||||||
ca: 'Català',
|
ca: 'Català',
|
||||||
co: 'Corsu',
|
co: 'Corsu',
|
||||||
cs: 'Čeština',
|
cs: 'Čeština',
|
||||||
@ -19,8 +20,10 @@ module SettingsHelper
|
|||||||
fa: 'فارسی',
|
fa: 'فارسی',
|
||||||
fi: 'Suomi',
|
fi: 'Suomi',
|
||||||
fr: 'Français',
|
fr: 'Français',
|
||||||
|
ga: 'Gaeilge',
|
||||||
gl: 'Galego',
|
gl: 'Galego',
|
||||||
he: 'עברית',
|
he: 'עברית',
|
||||||
|
hi: 'हिन्दी',
|
||||||
hr: 'Hrvatski',
|
hr: 'Hrvatski',
|
||||||
hu: 'Magyar',
|
hu: 'Magyar',
|
||||||
hy: 'Հայերեն',
|
hy: 'Հայերեն',
|
||||||
@ -29,24 +32,29 @@ module SettingsHelper
|
|||||||
it: 'Italiano',
|
it: 'Italiano',
|
||||||
ja: '日本語',
|
ja: '日本語',
|
||||||
ka: 'ქართული',
|
ka: 'ქართული',
|
||||||
|
kk: 'Қазақша',
|
||||||
ko: '한국어',
|
ko: '한국어',
|
||||||
|
lt: 'Lietuvių',
|
||||||
|
lv: 'Latviešu',
|
||||||
ml: 'മലയാളം',
|
ml: 'മലയാളം',
|
||||||
|
ms: 'Bahasa Melayu',
|
||||||
nl: 'Nederlands',
|
nl: 'Nederlands',
|
||||||
no: 'Norsk',
|
no: 'Norsk',
|
||||||
oc: 'Occitan',
|
oc: 'Occitan',
|
||||||
pl: 'Polszczyzna',
|
pl: 'Polski',
|
||||||
pt: 'Português',
|
pt: 'Português',
|
||||||
'pt-BR': 'Português do Brasil',
|
'pt-BR': 'Português do Brasil',
|
||||||
ro: 'Limba română',
|
ro: 'Română',
|
||||||
ru: 'Русский',
|
ru: 'Русский',
|
||||||
sk: 'Slovenčina',
|
sk: 'Slovenčina',
|
||||||
sl: 'Slovenščina',
|
sl: 'Slovenščina',
|
||||||
|
sq: 'Shqip',
|
||||||
sr: 'Српски',
|
sr: 'Српски',
|
||||||
'sr-Latn': 'Srpski (latinica)',
|
'sr-Latn': 'Srpski (latinica)',
|
||||||
sv: 'Svenska',
|
sv: 'Svenska',
|
||||||
ta: 'தமிழ்',
|
ta: 'தமிழ்',
|
||||||
te: 'తెలుగు',
|
te: 'తెలుగు',
|
||||||
th: 'ภาษาไทย',
|
th: 'ไทย',
|
||||||
tr: 'Türkçe',
|
tr: 'Türkçe',
|
||||||
uk: 'Українська',
|
uk: 'Українська',
|
||||||
zh: '中文',
|
zh: '中文',
|
||||||
|
@ -23,7 +23,7 @@ module StreamEntriesHelper
|
|||||||
safe_join([render(file: Rails.root.join('app', 'javascript', 'images', 'logo.svg')), t('accounts.unfollow')])
|
safe_join([render(file: Rails.root.join('app', 'javascript', 'images', 'logo.svg')), t('accounts.unfollow')])
|
||||||
end
|
end
|
||||||
elsif !(account.memorial? || account.moved?)
|
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')])
|
safe_join([render(file: Rails.root.join('app', 'javascript', 'images', 'logo.svg')), t('accounts.follow')])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -104,9 +104,19 @@ module StreamEntriesHelper
|
|||||||
I18n.t('statuses.content_warning', warning: status.spoiler_text)
|
I18n.t('statuses.content_warning', warning: status.spoiler_text)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def poll_summary(status)
|
||||||
|
return unless status.preloadable_poll
|
||||||
|
status.preloadable_poll.options.map { |o| "[ ] #{o}" }.join("\n")
|
||||||
|
end
|
||||||
|
|
||||||
def status_description(status)
|
def status_description(status)
|
||||||
components = [[media_summary(status), status_text_summary(status)].reject(&:blank?).join(' · ')]
|
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")
|
components.reject(&:blank?).join("\n\n")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
1
app/javascript/images/logo_transparent_black.svg
Normal file
1
app/javascript/images/logo_transparent_black.svg
Normal file
@ -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>
|
After Width: | Height: | Size: 1.5 KiB |
BIN
app/javascript/images/proof_providers/keybase.png
Normal file
BIN
app/javascript/images/proof_providers/keybase.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
@ -22,7 +22,7 @@ export function clearAlert() {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function showAlert(title, message) {
|
export function showAlert(title = messages.unexpectedTitle, message = messages.unexpectedMessage) {
|
||||||
return {
|
return {
|
||||||
type: ALERT_SHOW,
|
type: ALERT_SHOW,
|
||||||
title,
|
title,
|
||||||
@ -34,6 +34,11 @@ export function showAlertForError(error) {
|
|||||||
if (error.response) {
|
if (error.response) {
|
||||||
const { data, status, statusText } = 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 message = statusText;
|
||||||
let title = `${status}`;
|
let title = `${status}`;
|
||||||
|
|
||||||
@ -44,6 +49,6 @@ export function showAlertForError(error) {
|
|||||||
return showAlert(title, message);
|
return showAlert(title, message);
|
||||||
} else {
|
} else {
|
||||||
console.error(error);
|
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 { importFetchedAccounts } from './importer';
|
||||||
import { updateTimeline } from './timelines';
|
import { updateTimeline } from './timelines';
|
||||||
import { showAlertForError } from './alerts';
|
import { showAlertForError } from './alerts';
|
||||||
|
import { showAlert } from './alerts';
|
||||||
|
import { defineMessages } from 'react-intl';
|
||||||
|
|
||||||
let cancelFetchComposeSuggestionsAccounts;
|
let cancelFetchComposeSuggestionsAccounts;
|
||||||
|
|
||||||
@ -50,6 +52,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_SUCCESS = 'COMPOSE_UPLOAD_UPDATE_SUCCESS';
|
||||||
export const COMPOSE_UPLOAD_CHANGE_FAIL = 'COMPOSE_UPLOAD_UPDATE_FAIL';
|
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) {
|
export function changeCompose(text) {
|
||||||
return {
|
return {
|
||||||
type: COMPOSE_CHANGE,
|
type: COMPOSE_CHANGE,
|
||||||
@ -126,6 +140,7 @@ export function submitCompose(routerHistory) {
|
|||||||
sensitive: getState().getIn(['compose', 'sensitive']),
|
sensitive: getState().getIn(['compose', 'sensitive']),
|
||||||
spoiler_text: getState().getIn(['compose', 'spoiler_text'], ''),
|
spoiler_text: getState().getIn(['compose', 'spoiler_text'], ''),
|
||||||
visibility: getState().getIn(['compose', 'privacy']),
|
visibility: getState().getIn(['compose', 'privacy']),
|
||||||
|
poll: getState().getIn(['compose', 'poll'], null),
|
||||||
local_only: !getState().getIn(['compose', 'federation']),
|
local_only: !getState().getIn(['compose', 'federation']),
|
||||||
}, {
|
}, {
|
||||||
headers: {
|
headers: {
|
||||||
@ -145,7 +160,9 @@ export function submitCompose(routerHistory) {
|
|||||||
// into the columns
|
// into the columns
|
||||||
|
|
||||||
const insertIfOnline = timelineId => {
|
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 }));
|
dispatch(updateTimeline(timelineId, { ...response.data }));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -186,22 +203,40 @@ export function submitComposeFail(error) {
|
|||||||
|
|
||||||
export function uploadCompose(files) {
|
export function uploadCompose(files) {
|
||||||
return function (dispatch, getState) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(uploadComposeRequest());
|
dispatch(uploadComposeRequest());
|
||||||
|
|
||||||
resizeImage(files[0]).then(file => {
|
for (const [i, f] of Array.from(files).entries()) {
|
||||||
|
if (media.size + i > 3) break;
|
||||||
|
|
||||||
|
resizeImage(f).then(file => {
|
||||||
const data = new FormData();
|
const data = new FormData();
|
||||||
data.append('file', file);
|
data.append('file', file);
|
||||||
|
|
||||||
return api(getState).post('/api/v1/media', data, {
|
return api(getState).post('/api/v1/media', data, {
|
||||||
onUploadProgress: ({ loaded, total }) => dispatch(uploadComposeProgress(loaded, total)),
|
onUploadProgress: function({ loaded }){
|
||||||
|
progress[i] = loaded;
|
||||||
|
dispatch(uploadComposeProgress(progress.reduce((a, v) => a + v, 0), total));
|
||||||
|
},
|
||||||
}).then(({ data }) => dispatch(uploadComposeSuccess(data)));
|
}).then(({ data }) => dispatch(uploadComposeSuccess(data)));
|
||||||
}).catch(error => dispatch(uploadComposeFail(error)));
|
}).catch(error => dispatch(uploadComposeFail(error)));
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export function changeUploadCompose(id, params) {
|
export function changeUploadCompose(id, params) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
@ -475,4 +510,46 @@ export function changeComposing(value) {
|
|||||||
type: COMPOSE_COMPOSING_CHANGE,
|
type: COMPOSE_COMPOSING_CHANGE,
|
||||||
value,
|
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']);
|
params.since_id = getState().getIn(['conversations', 'items', 0, 'last_status']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isLoadingRecent = !!params.since_id;
|
||||||
|
|
||||||
api(getState).get('/api/v1/conversations', { params })
|
api(getState).get('/api/v1/conversations', { params })
|
||||||
.then(response => {
|
.then(response => {
|
||||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||||
|
|
||||||
dispatch(importFetchedAccounts(response.data.reduce((aggr, item) => aggr.concat(item.accounts), [])));
|
dispatch(importFetchedAccounts(response.data.reduce((aggr, item) => aggr.concat(item.accounts), [])));
|
||||||
dispatch(importFetchedStatuses(response.data.map(item => item.last_status).filter(x => !!x)));
|
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)));
|
.catch(err => dispatch(expandConversationsFail(err)));
|
||||||
};
|
};
|
||||||
@ -56,10 +58,11 @@ export const expandConversationsRequest = () => ({
|
|||||||
type: CONVERSATIONS_FETCH_REQUEST,
|
type: CONVERSATIONS_FETCH_REQUEST,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const expandConversationsSuccess = (conversations, next) => ({
|
export const expandConversationsSuccess = (conversations, next, isLoadingRecent) => ({
|
||||||
type: CONVERSATIONS_FETCH_SUCCESS,
|
type: CONVERSATIONS_FETCH_SUCCESS,
|
||||||
conversations,
|
conversations,
|
||||||
next,
|
next,
|
||||||
|
isLoadingRecent,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const expandConversationsFail = error => ({
|
export const expandConversationsFail = error => ({
|
||||||
|
30
app/javascript/mastodon/actions/identity_proofs.js
Normal file
30
app/javascript/mastodon/actions/identity_proofs.js
Normal file
@ -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 { normalizeAccount, normalizeStatus, normalizePoll } from './normalizer';
|
||||||
// import { putAccounts, putStatuses } from '../../storage/modifier';
|
|
||||||
import { normalizeAccount, normalizeStatus } from './normalizer';
|
|
||||||
|
|
||||||
export const ACCOUNT_IMPORT = 'ACCOUNT_IMPORT';
|
export const ACCOUNT_IMPORT = 'ACCOUNT_IMPORT';
|
||||||
export const ACCOUNTS_IMPORT = 'ACCOUNTS_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 STATUSES_IMPORT = 'STATUSES_IMPORT';
|
||||||
|
export const POLLS_IMPORT = 'POLLS_IMPORT';
|
||||||
|
|
||||||
function pushUnique(array, object) {
|
function pushUnique(array, object) {
|
||||||
if (array.every(element => element.id !== object.id)) {
|
if (array.every(element => element.id !== object.id)) {
|
||||||
@ -29,6 +28,10 @@ export function importStatuses(statuses) {
|
|||||||
return { type: STATUSES_IMPORT, statuses };
|
return { type: STATUSES_IMPORT, statuses };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function importPolls(polls) {
|
||||||
|
return { type: POLLS_IMPORT, polls };
|
||||||
|
}
|
||||||
|
|
||||||
export function importFetchedAccount(account) {
|
export function importFetchedAccount(account) {
|
||||||
return importFetchedAccounts([account]);
|
return importFetchedAccounts([account]);
|
||||||
}
|
}
|
||||||
@ -45,7 +48,6 @@ export function importFetchedAccounts(accounts) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
accounts.forEach(processAccount);
|
accounts.forEach(processAccount);
|
||||||
//putAccounts(normalAccounts, !autoPlayGif);
|
|
||||||
|
|
||||||
return importAccounts(normalAccounts);
|
return importAccounts(normalAccounts);
|
||||||
}
|
}
|
||||||
@ -58,6 +60,7 @@ export function importFetchedStatuses(statuses) {
|
|||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
const accounts = [];
|
const accounts = [];
|
||||||
const normalStatuses = [];
|
const normalStatuses = [];
|
||||||
|
const polls = [];
|
||||||
|
|
||||||
function processStatus(status) {
|
function processStatus(status) {
|
||||||
pushUnique(normalStatuses, normalizeStatus(status, getState().getIn(['statuses', status.id])));
|
pushUnique(normalStatuses, normalizeStatus(status, getState().getIn(['statuses', status.id])));
|
||||||
@ -66,12 +69,22 @@ export function importFetchedStatuses(statuses) {
|
|||||||
if (status.reblog && status.reblog.id) {
|
if (status.reblog && status.reblog.id) {
|
||||||
processStatus(status.reblog);
|
processStatus(status.reblog);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (status.poll && status.poll.id) {
|
||||||
|
pushUnique(polls, normalizePoll(status.poll));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
statuses.forEach(processStatus);
|
statuses.forEach(processStatus);
|
||||||
//putStatuses(normalStatuses);
|
|
||||||
|
|
||||||
|
dispatch(importPolls(polls));
|
||||||
dispatch(importFetchedAccounts(accounts));
|
dispatch(importFetchedAccounts(accounts));
|
||||||
dispatch(importStatuses(normalStatuses));
|
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;
|
normalStatus.reblog = status.reblog.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (status.poll && status.poll.id) {
|
||||||
|
normalStatus.poll = status.poll.id;
|
||||||
|
}
|
||||||
|
|
||||||
// Only calculate these values when status first encountered
|
// Only calculate these values when status first encountered
|
||||||
// Otherwise keep the ones already in the reducer
|
// Otherwise keep the ones already in the reducer
|
||||||
if (normalOldStatus) {
|
if (normalOldStatus) {
|
||||||
@ -63,3 +67,16 @@ export function normalizeStatus(status, normalOldStatus) {
|
|||||||
|
|
||||||
return normalStatus;
|
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,
|
importFetchedStatus,
|
||||||
importFetchedStatuses,
|
importFetchedStatuses,
|
||||||
} from './importer';
|
} from './importer';
|
||||||
|
import { saveSettings } from './settings';
|
||||||
import { defineMessages } from 'react-intl';
|
import { defineMessages } from 'react-intl';
|
||||||
import { List as ImmutableList } from 'immutable';
|
import { List as ImmutableList } from 'immutable';
|
||||||
import { unescapeHTML } from '../utils/html';
|
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 excludeTypesFromSettings = state => state.getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS();
|
||||||
|
|
||||||
const excludeTypesFromFilter = filter => {
|
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();
|
return allTypes.filterNot(item => item === filter).toJS();
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -187,5 +188,6 @@ export function setFilter (filterType) {
|
|||||||
value: filterType,
|
value: filterType,
|
||||||
});
|
});
|
||||||
dispatch(expandNotifications());
|
dispatch(expandNotifications());
|
||||||
|
dispatch(saveSettings());
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
60
app/javascript/mastodon/actions/polls.js
Normal file
60
app/javascript/mastodon/actions/polls.js
Normal file
@ -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: {
|
params: {
|
||||||
q: value,
|
q: value,
|
||||||
resolve: true,
|
resolve: true,
|
||||||
|
limit: 5,
|
||||||
},
|
},
|
||||||
}).then(response => {
|
}).then(response => {
|
||||||
if (response.data.accounts) {
|
if (response.data.accounts) {
|
||||||
|
@ -140,7 +140,11 @@ export function redraft(status) {
|
|||||||
|
|
||||||
export function deleteStatus(id, router, withRedraft = false) {
|
export function deleteStatus(id, router, withRedraft = false) {
|
||||||
return (dispatch, getState) => {
|
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));
|
dispatch(deleteStatusRequest(id));
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ import {
|
|||||||
updateTimeline,
|
updateTimeline,
|
||||||
deleteFromTimelines,
|
deleteFromTimelines,
|
||||||
expandHomeTimeline,
|
expandHomeTimeline,
|
||||||
|
connectTimeline,
|
||||||
disconnectTimeline,
|
disconnectTimeline,
|
||||||
} from './timelines';
|
} from './timelines';
|
||||||
import { updateNotifications, expandNotifications } from './notifications';
|
import { updateNotifications, expandNotifications } from './notifications';
|
||||||
@ -16,7 +17,12 @@ export function connectTimelineStream (timelineId, path, pollingRefresh = null,
|
|||||||
|
|
||||||
return connectStream (path, pollingRefresh, (dispatch, getState) => {
|
return connectStream (path, pollingRefresh, (dispatch, getState) => {
|
||||||
const locale = getState().getIn(['meta', 'locale']);
|
const locale = getState().getIn(['meta', 'locale']);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
onConnect() {
|
||||||
|
dispatch(connectTimeline(timelineId));
|
||||||
|
},
|
||||||
|
|
||||||
onDisconnect() {
|
onDisconnect() {
|
||||||
dispatch(disconnectTimeline(timelineId));
|
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_SCROLL_TOP = 'TIMELINE_SCROLL_TOP';
|
||||||
|
|
||||||
|
export const TIMELINE_CONNECT = 'TIMELINE_CONNECT';
|
||||||
export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT';
|
export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT';
|
||||||
|
|
||||||
export function updateTimeline(timeline, status, accept) {
|
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) {
|
export function disconnectTimeline(timeline) {
|
||||||
return {
|
return {
|
||||||
type: TIMELINE_DISCONNECT,
|
type: TIMELINE_DISCONNECT,
|
||||||
|
@ -13,10 +13,14 @@ export const getLinks = response => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let csrfHeader = {};
|
let csrfHeader = {};
|
||||||
|
|
||||||
function setCSRFHeader() {
|
function setCSRFHeader() {
|
||||||
const csrfToken = document.querySelector('meta[name=csrf-token]').content;
|
const csrfToken = document.querySelector('meta[name=csrf-token]');
|
||||||
csrfHeader['X-CSRF-Token'] = csrfToken;
|
if (csrfToken) {
|
||||||
|
csrfHeader['X-CSRF-Token'] = csrfToken.content;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ready(setCSRFHeader);
|
ready(setCSRFHeader);
|
||||||
|
|
||||||
export default getState => axios.create({
|
export default getState => axios.create({
|
||||||
|
@ -2,6 +2,7 @@ import React from 'react';
|
|||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
import Icon from 'mastodon/components/icon';
|
||||||
|
|
||||||
const filename = url => url.split('/').pop().split('#')[0].split('?')[0];
|
const filename = url => url.split('/').pop().split('#')[0].split('?')[0];
|
||||||
|
|
||||||
@ -24,7 +25,7 @@ export default class AttachmentList extends ImmutablePureComponent {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<li key={attachment.get('id')}>
|
<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>
|
</li>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@ -36,7 +37,7 @@ export default class AttachmentList extends ImmutablePureComponent {
|
|||||||
return (
|
return (
|
||||||
<div className='attachment-list'>
|
<div className='attachment-list'>
|
||||||
<div className='attachment-list__icon'>
|
<div className='attachment-list__icon'>
|
||||||
<i className='fa fa-link' />
|
<Icon id='link' />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul className='attachment-list__list'>
|
<ul className='attachment-list__list'>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import Icon from 'mastodon/components/icon';
|
||||||
|
|
||||||
export default class ColumnBackButton extends React.PureComponent {
|
export default class ColumnBackButton extends React.PureComponent {
|
||||||
|
|
||||||
@ -19,7 +20,7 @@ export default class ColumnBackButton extends React.PureComponent {
|
|||||||
render () {
|
render () {
|
||||||
return (
|
return (
|
||||||
<button onClick={this.handleClick} className='column-back-button'>
|
<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' />
|
<FormattedMessage id='column_back_button.label' defaultMessage='Back' />
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
import ColumnBackButton from './column_back_button';
|
import ColumnBackButton from './column_back_button';
|
||||||
|
import Icon from 'mastodon/components/icon';
|
||||||
|
|
||||||
export default class ColumnBackButtonSlim extends ColumnBackButton {
|
export default class ColumnBackButtonSlim extends ColumnBackButton {
|
||||||
|
|
||||||
@ -8,7 +9,7 @@ export default class ColumnBackButtonSlim extends ColumnBackButton {
|
|||||||
return (
|
return (
|
||||||
<div className='column-back-button--slim'>
|
<div className='column-back-button--slim'>
|
||||||
<div role='button' tabIndex='0' onClick={this.handleClick} className='column-back-button column-back-button--slim-button'>
|
<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' />
|
<FormattedMessage id='column_back_button.label' defaultMessage='Back' />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,6 +2,7 @@ import React from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
|
import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
|
||||||
|
import Icon from 'mastodon/components/icon';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
show: { id: 'column_header.show_settings', defaultMessage: 'Show settings' },
|
show: { id: 'column_header.show_settings', defaultMessage: 'Show settings' },
|
||||||
@ -109,22 +110,22 @@ class ColumnHeader extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (multiColumn && pinned) {
|
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 = (
|
moveButtons = (
|
||||||
<div key='move-buttons' className='column-header__setting-arrows'>
|
<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.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}><i className='fa fa-chevron-right' /></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>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (multiColumn) {
|
} 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)) {
|
if (!pinned && (multiColumn || showBackButton)) {
|
||||||
backButton = (
|
backButton = (
|
||||||
<button onClick={this.handleBackClick} className='column-header__back-button'>
|
<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' />
|
<FormattedMessage id='column_back_button.label' defaultMessage='Back' />
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
@ -140,7 +141,7 @@ class ColumnHeader extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (children || multiColumn) {
|
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;
|
const hasTitle = icon && title;
|
||||||
@ -150,7 +151,7 @@ class ColumnHeader extends React.PureComponent {
|
|||||||
<h1 className={buttonClassName}>
|
<h1 className={buttonClassName}>
|
||||||
{hasTitle && (
|
{hasTitle && (
|
||||||
<button onClick={this.handleTitleClick}>
|
<button onClick={this.handleTitleClick}>
|
||||||
<i className={`fa fa-fw fa-${icon} column-header__icon`} />
|
<Icon id={icon} fixedWidth className='column-header__icon' />
|
||||||
{title}
|
{title}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
@ -22,7 +22,7 @@ export default class DisplayName extends React.PureComponent {
|
|||||||
suffix = `+${others.size - 2}`;
|
suffix = `+${others.size - 2}`;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (others) {
|
if (others && others.size > 0) {
|
||||||
account = others.first();
|
account = others.first();
|
||||||
} else {
|
} else {
|
||||||
account = this.props.account;
|
account = this.props.account;
|
||||||
|
39
app/javascript/mastodon/components/error_boundary.js
Normal file
39
app/javascript/mastodon/components/error_boundary.js
Normal file
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
21
app/javascript/mastodon/components/icon.js
Normal file
21
app/javascript/mastodon/components/icon.js
Normal file
@ -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 spring from 'react-motion/lib/spring';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import Icon from 'mastodon/components/icon';
|
||||||
|
|
||||||
export default class IconButton extends React.PureComponent {
|
export default class IconButton extends React.PureComponent {
|
||||||
|
|
||||||
@ -85,8 +86,9 @@ export default class IconButton extends React.PureComponent {
|
|||||||
onClick={this.handleClick}
|
onClick={this.handleClick}
|
||||||
style={style}
|
style={style}
|
||||||
tabIndex={tabIndex}
|
tabIndex={tabIndex}
|
||||||
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
<i className={`fa fa-fw fa-${icon}`} aria-hidden='true' />
|
<Icon id={icon} fixedWidth aria-hidden='true' />
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -103,8 +105,9 @@ export default class IconButton extends React.PureComponent {
|
|||||||
onClick={this.handleClick}
|
onClick={this.handleClick}
|
||||||
style={style}
|
style={style}
|
||||||
tabIndex={tabIndex}
|
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>
|
</button>
|
||||||
)}
|
)}
|
||||||
</Motion>
|
</Motion>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { injectIntl, defineMessages } from 'react-intl';
|
import { injectIntl, defineMessages } from 'react-intl';
|
||||||
|
import Icon from 'mastodon/components/icon';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
load_more: { id: 'status.load_more', defaultMessage: 'Load more' },
|
load_more: { id: 'status.load_more', defaultMessage: 'Load more' },
|
||||||
@ -25,7 +26,7 @@ class LoadGap extends React.PureComponent {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<button className='load-more load-gap' disabled={disabled} onClick={this.handleClick} aria-label={intl.formatMessage(messages.load_more)}>
|
<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>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
140
app/javascript/mastodon/components/poll.js
Normal file
140
app/javascript/mastodon/components/poll.js
Normal file
@ -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' },
|
minutes: { id: 'relative_time.minutes', defaultMessage: '{number}m' },
|
||||||
hours: { id: 'relative_time.hours', defaultMessage: '{number}h' },
|
hours: { id: 'relative_time.hours', defaultMessage: '{number}h' },
|
||||||
days: { id: 'relative_time.days', defaultMessage: '{number}d' },
|
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 = {
|
const dateFormatOptions = {
|
||||||
@ -86,6 +91,26 @@ export const timeAgoString = (intl, date, now, year) => {
|
|||||||
return relativeTime;
|
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
|
export default @injectIntl
|
||||||
class RelativeTimestamp extends React.Component {
|
class RelativeTimestamp extends React.Component {
|
||||||
|
|
||||||
@ -93,6 +118,7 @@ class RelativeTimestamp extends React.Component {
|
|||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
timestamp: PropTypes.string.isRequired,
|
timestamp: PropTypes.string.isRequired,
|
||||||
year: PropTypes.number.isRequired,
|
year: PropTypes.number.isRequired,
|
||||||
|
futureDate: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
@ -145,10 +171,10 @@ class RelativeTimestamp extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { timestamp, intl, year } = this.props;
|
const { timestamp, intl, year, futureDate } = this.props;
|
||||||
|
|
||||||
const date = new Date(timestamp);
|
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 (
|
return (
|
||||||
<time dateTime={timestamp} title={intl.formatDate(date, dateFormatOptions)}>
|
<time dateTime={timestamp} title={intl.formatDate(date, dateFormatOptions)}>
|
||||||
|
@ -15,6 +15,8 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
|
|||||||
import { MediaGallery, Video } from '../features/ui/util/async-components';
|
import { MediaGallery, Video } from '../features/ui/util/async-components';
|
||||||
import { HotKeys } from 'react-hotkeys';
|
import { HotKeys } from 'react-hotkeys';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import Icon from 'mastodon/components/icon';
|
||||||
|
import PollContainer from 'mastodon/containers/poll_container';
|
||||||
|
|
||||||
// We use the component (and not the container) since we do not want
|
// We use the component (and not the container) since we do not want
|
||||||
// to use the progress bar to show download progress
|
// to use the progress bar to show download progress
|
||||||
@ -249,7 +251,7 @@ class Status extends ImmutablePureComponent {
|
|||||||
if (featured) {
|
if (featured) {
|
||||||
prepend = (
|
prepend = (
|
||||||
<div className='status__prepend'>
|
<div className='status__prepend'>
|
||||||
<div className='status__prepend-icon-wrapper'><i className='fa fa-fw fa-thumb-tack status__prepend-icon' /></div>
|
<div className='status__prepend-icon-wrapper'><Icon id='thumb-tack' className='status__prepend-icon' fixedWidth /></div>
|
||||||
<FormattedMessage id='status.pinned' defaultMessage='Pinned toot' />
|
<FormattedMessage id='status.pinned' defaultMessage='Pinned toot' />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -258,7 +260,7 @@ class Status extends ImmutablePureComponent {
|
|||||||
|
|
||||||
prepend = (
|
prepend = (
|
||||||
<div className='status__prepend'>
|
<div className='status__prepend'>
|
||||||
<div className='status__prepend-icon-wrapper'><i className='fa fa-fw fa-retweet status__prepend-icon' /></div>
|
<div className='status__prepend-icon-wrapper'><Icon id='retweet' className='status__prepend-icon' fixedWidth /></div>
|
||||||
<FormattedMessage id='status.reblogged_by' defaultMessage='{name} boosted' values={{ name: <a onClick={this.handleAccountClick} data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} className='status__display-name muted'><bdi><strong dangerouslySetInnerHTML={display_name_html} /></bdi></a> }} />
|
<FormattedMessage id='status.reblogged_by' defaultMessage='{name} boosted' values={{ name: <a onClick={this.handleAccountClick} data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} className='status__display-name muted'><bdi><strong dangerouslySetInnerHTML={display_name_html} /></bdi></a> }} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -269,7 +271,9 @@ class Status extends ImmutablePureComponent {
|
|||||||
status = status.get('reblog');
|
status = status.get('reblog');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status.get('media_attachments').size > 0) {
|
if (status.get('poll')) {
|
||||||
|
media = <PollContainer pollId={status.get('poll')} />;
|
||||||
|
} else if (status.get('media_attachments').size > 0) {
|
||||||
if (this.props.muted || status.get('media_attachments').some(item => item.get('type') === 'unknown')) {
|
if (this.props.muted || status.get('media_attachments').some(item => item.get('type') === 'unknown')) {
|
||||||
media = (
|
media = (
|
||||||
<AttachmentList
|
<AttachmentList
|
||||||
@ -325,7 +329,7 @@ class Status extends ImmutablePureComponent {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (otherAccounts) {
|
if (otherAccounts && otherAccounts.size > 0) {
|
||||||
statusAvatar = <AvatarComposite accounts={otherAccounts} size={48} />;
|
statusAvatar = <AvatarComposite accounts={otherAccounts} size={48} />;
|
||||||
} else if (account === undefined || account === null) {
|
} else if (account === undefined || account === null) {
|
||||||
statusAvatar = <Avatar account={status.get('account')} size={48} />;
|
statusAvatar = <Avatar account={status.get('account')} size={48} />;
|
||||||
@ -347,7 +351,7 @@ class Status extends ImmutablePureComponent {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<HotKeys handlers={handlers}>
|
<HotKeys handlers={handlers}>
|
||||||
<div className={classNames('status__wrapper', `status__wrapper-${status.get('visibility')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id'), read: unread === false, focusable: !this.props.muted })} tabIndex={this.props.muted ? null : 0} data-featured={featured ? 'true' : null} aria-label={textForScreenReader(intl, status, rebloggedByText, !status.get('hidden'))} ref={this.handleRef}>
|
<div className={classNames('status__wrapper', `status__wrapper-${status.get('visibility')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id'), read: unread === false, focusable: !this.props.muted })} tabIndex={this.props.muted ? null : 0} data-featured={featured ? 'true' : null} aria-label={textForScreenReader(intl, status, rebloggedByText)} ref={this.handleRef}>
|
||||||
{prepend}
|
{prepend}
|
||||||
|
|
||||||
<div className={classNames('status', `status-${status.get('visibility')}`, { 'status-reply': !!status.get('in_reply_to_id'), muted: this.props.muted, read: unread === false })} data-id={status.get('id')}>
|
<div className={classNames('status', `status-${status.get('visibility')}`, { 'status-reply': !!status.get('in_reply_to_id'), muted: this.props.muted, read: unread === false })} data-id={status.get('id')}>
|
||||||
|
@ -33,6 +33,7 @@ const messages = defineMessages({
|
|||||||
embed: { id: 'status.embed', defaultMessage: 'Embed' },
|
embed: { id: 'status.embed', defaultMessage: 'Embed' },
|
||||||
admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
|
admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
|
||||||
admin_status: { id: 'status.admin_status', defaultMessage: 'Open this status in the moderation interface' },
|
admin_status: { id: 'status.admin_status', defaultMessage: 'Open this status in the moderation interface' },
|
||||||
|
copy: { id: 'status.copy', defaultMessage: 'Copy link to status' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const obfuscatedCount = count => {
|
const obfuscatedCount = count => {
|
||||||
@ -139,7 +140,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleBlockClick = () => {
|
handleBlockClick = () => {
|
||||||
this.props.onBlock(this.props.status.get('account'));
|
this.props.onBlock(this.props.status);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleOpen = () => {
|
handleOpen = () => {
|
||||||
@ -158,6 +159,25 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||||||
this.props.onMuteConversation(this.props.status);
|
this.props.onMuteConversation(this.props.status);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleCopy = () => {
|
||||||
|
const url = this.props.status.get('url');
|
||||||
|
const textarea = document.createElement('textarea');
|
||||||
|
|
||||||
|
textarea.textContent = url;
|
||||||
|
textarea.style.position = 'fixed';
|
||||||
|
|
||||||
|
document.body.appendChild(textarea);
|
||||||
|
|
||||||
|
try {
|
||||||
|
textarea.select();
|
||||||
|
document.execCommand('copy');
|
||||||
|
} catch (e) {
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
document.body.removeChild(textarea);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { status, intl, withDismiss } = this.props;
|
const { status, intl, withDismiss } = this.props;
|
||||||
|
|
||||||
@ -174,6 +194,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||||||
menu.push({ text: intl.formatMessage(messages.open), action: this.handleOpen });
|
menu.push({ text: intl.formatMessage(messages.open), action: this.handleOpen });
|
||||||
|
|
||||||
if (publicStatus) {
|
if (publicStatus) {
|
||||||
|
menu.push({ text: intl.formatMessage(messages.copy), action: this.handleCopy });
|
||||||
menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed });
|
menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -202,6 +223,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||||||
menu.push({ text: intl.formatMessage(messages.mute, { name: status.getIn(['account', 'username']) }), action: this.handleMuteClick });
|
menu.push({ text: intl.formatMessage(messages.mute, { name: status.getIn(['account', 'username']) }), action: this.handleMuteClick });
|
||||||
menu.push({ text: intl.formatMessage(messages.block, { name: status.getIn(['account', 'username']) }), action: this.handleBlockClick });
|
menu.push({ text: intl.formatMessage(messages.block, { name: status.getIn(['account', 'username']) }), action: this.handleBlockClick });
|
||||||
menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport });
|
menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport });
|
||||||
|
|
||||||
if (isStaff) {
|
if (isStaff) {
|
||||||
menu.push(null);
|
menu.push(null);
|
||||||
menu.push({ text: intl.formatMessage(messages.admin_account, { name: status.getIn(['account', 'username']) }), href: `/admin/accounts/${status.getIn(['account', 'id'])}` });
|
menu.push({ text: intl.formatMessage(messages.admin_account, { name: status.getIn(['account', 'username']) }), href: `/admin/accounts/${status.getIn(['account', 'id'])}` });
|
||||||
|
@ -5,6 +5,7 @@ import { isRtl } from '../rtl';
|
|||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
import Permalink from './permalink';
|
import Permalink from './permalink';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
|
import Icon from 'mastodon/components/icon';
|
||||||
|
|
||||||
const MAX_HEIGHT = 642; // 20px * 32 (+ 2px padding at the top)
|
const MAX_HEIGHT = 642; // 20px * 32 (+ 2px padding at the top)
|
||||||
|
|
||||||
@ -160,7 +161,7 @@ export default class StatusContent extends React.PureComponent {
|
|||||||
|
|
||||||
const readMoreButton = (
|
const readMoreButton = (
|
||||||
<button className='status__content__read-more-button' onClick={this.props.onClick} key='read-more'>
|
<button className='status__content__read-more-button' onClick={this.props.onClick} key='read-more'>
|
||||||
<FormattedMessage id='status.read_more' defaultMessage='Read more' /><i className='fa fa-fw fa-angle-right' />
|
<FormattedMessage id='status.read_more' defaultMessage='Read more' /><Icon id='angle-right' fixedWidth />
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -182,14 +183,14 @@ export default class StatusContent extends React.PureComponent {
|
|||||||
return (
|
return (
|
||||||
<div className={classNames} ref={this.setRef} tabIndex='0' style={directionStyle} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}>
|
<div className={classNames} ref={this.setRef} tabIndex='0' style={directionStyle} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}>
|
||||||
<p style={{ marginBottom: hidden && status.get('mentions').isEmpty() ? '0px' : null }}>
|
<p style={{ marginBottom: hidden && status.get('mentions').isEmpty() ? '0px' : null }}>
|
||||||
<span dangerouslySetInnerHTML={spoilerContent} />
|
<span dangerouslySetInnerHTML={spoilerContent} lang={status.get('language')} />
|
||||||
{' '}
|
{' '}
|
||||||
<button tabIndex='0' className={`status__content__spoiler-link ${hidden ? 'status__content__spoiler-link--show-more' : 'status__content__spoiler-link--show-less'}`} onClick={this.handleSpoilerClick}>{toggleText}</button>
|
<button tabIndex='0' className={`status__content__spoiler-link ${hidden ? 'status__content__spoiler-link--show-more' : 'status__content__spoiler-link--show-less'}`} onClick={this.handleSpoilerClick}>{toggleText}</button>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{mentionsPlaceholder}
|
{mentionsPlaceholder}
|
||||||
|
|
||||||
<div tabIndex={!hidden ? 0 : null} className={`status__content__text ${!hidden ? 'status__content__text--visible' : ''}`} style={directionStyle} dangerouslySetInnerHTML={content} />
|
<div tabIndex={!hidden ? 0 : null} className={`status__content__text ${!hidden ? 'status__content__text--visible' : ''}`} style={directionStyle} dangerouslySetInnerHTML={content} lang={status.get('language')} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (this.props.onClick) {
|
} else if (this.props.onClick) {
|
||||||
@ -201,6 +202,7 @@ export default class StatusContent extends React.PureComponent {
|
|||||||
className={classNames}
|
className={classNames}
|
||||||
style={directionStyle}
|
style={directionStyle}
|
||||||
dangerouslySetInnerHTML={content}
|
dangerouslySetInnerHTML={content}
|
||||||
|
lang={status.get('language')}
|
||||||
onMouseDown={this.handleMouseDown}
|
onMouseDown={this.handleMouseDown}
|
||||||
onMouseUp={this.handleMouseUp}
|
onMouseUp={this.handleMouseUp}
|
||||||
/>,
|
/>,
|
||||||
@ -219,6 +221,7 @@ export default class StatusContent extends React.PureComponent {
|
|||||||
className='status__content'
|
className='status__content'
|
||||||
style={directionStyle}
|
style={directionStyle}
|
||||||
dangerouslySetInnerHTML={content}
|
dangerouslySetInnerHTML={content}
|
||||||
|
lang={status.get('language')}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,7 @@ import { connectUserStream } from '../actions/streaming';
|
|||||||
import { IntlProvider, addLocaleData } from 'react-intl';
|
import { IntlProvider, addLocaleData } from 'react-intl';
|
||||||
import { getLocale } from '../locales';
|
import { getLocale } from '../locales';
|
||||||
import initialState from '../initial_state';
|
import initialState from '../initial_state';
|
||||||
|
import ErrorBoundary from '../components/error_boundary';
|
||||||
|
|
||||||
const { localeData, messages } = getLocale();
|
const { localeData, messages } = getLocale();
|
||||||
addLocaleData(localeData);
|
addLocaleData(localeData);
|
||||||
@ -75,7 +76,9 @@ export default class Mastodon extends React.PureComponent {
|
|||||||
return (
|
return (
|
||||||
<IntlProvider locale={locale} messages={messages}>
|
<IntlProvider locale={locale} messages={messages}>
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
|
<ErrorBoundary>
|
||||||
<MastodonMount />
|
<MastodonMount />
|
||||||
|
</ErrorBoundary>
|
||||||
</Provider>
|
</Provider>
|
||||||
</IntlProvider>
|
</IntlProvider>
|
||||||
);
|
);
|
||||||
|
@ -6,6 +6,7 @@ import { getLocale } from '../locales';
|
|||||||
import MediaGallery from '../components/media_gallery';
|
import MediaGallery from '../components/media_gallery';
|
||||||
import Video from '../features/video';
|
import Video from '../features/video';
|
||||||
import Card from '../features/status/components/card';
|
import Card from '../features/status/components/card';
|
||||||
|
import Poll from 'mastodon/components/poll';
|
||||||
import ModalRoot from '../components/modal_root';
|
import ModalRoot from '../components/modal_root';
|
||||||
import MediaModal from '../features/ui/components/media_modal';
|
import MediaModal from '../features/ui/components/media_modal';
|
||||||
import { List as ImmutableList, fromJS } from 'immutable';
|
import { List as ImmutableList, fromJS } from 'immutable';
|
||||||
@ -13,7 +14,7 @@ import { List as ImmutableList, fromJS } from 'immutable';
|
|||||||
const { localeData, messages } = getLocale();
|
const { localeData, messages } = getLocale();
|
||||||
addLocaleData(localeData);
|
addLocaleData(localeData);
|
||||||
|
|
||||||
const MEDIA_COMPONENTS = { MediaGallery, Video, Card };
|
const MEDIA_COMPONENTS = { MediaGallery, Video, Card, Poll };
|
||||||
|
|
||||||
export default class MediaContainer extends PureComponent {
|
export default class MediaContainer extends PureComponent {
|
||||||
|
|
||||||
@ -54,11 +55,12 @@ export default class MediaContainer extends PureComponent {
|
|||||||
{[].map.call(components, (component, i) => {
|
{[].map.call(components, (component, i) => {
|
||||||
const componentName = component.getAttribute('data-component');
|
const componentName = component.getAttribute('data-component');
|
||||||
const Component = MEDIA_COMPONENTS[componentName];
|
const Component = MEDIA_COMPONENTS[componentName];
|
||||||
const { media, card, ...props } = JSON.parse(component.getAttribute('data-props'));
|
const { media, card, poll, ...props } = JSON.parse(component.getAttribute('data-props'));
|
||||||
|
|
||||||
Object.assign(props, {
|
Object.assign(props, {
|
||||||
...(media ? { media: fromJS(media) } : {}),
|
...(media ? { media: fromJS(media) } : {}),
|
||||||
...(card ? { card: fromJS(card) } : {}),
|
...(card ? { card: fromJS(card) } : {}),
|
||||||
|
...(poll ? { poll: fromJS(poll) } : {}),
|
||||||
|
|
||||||
...(componentName === 'Video' ? {
|
...(componentName === 'Video' ? {
|
||||||
onOpenVideo: this.handleOpenVideo,
|
onOpenVideo: this.handleOpenVideo,
|
||||||
|
8
app/javascript/mastodon/containers/poll_container.js
Normal file
8
app/javascript/mastodon/containers/poll_container.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
import Poll from 'mastodon/components/poll';
|
||||||
|
|
||||||
|
const mapStateToProps = (state, { pollId }) => ({
|
||||||
|
poll: state.getIn(['polls', pollId]),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(mapStateToProps)(Poll);
|
@ -38,6 +38,7 @@ const messages = defineMessages({
|
|||||||
blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' },
|
blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' },
|
||||||
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
|
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
|
||||||
replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
|
replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
|
||||||
|
blockAndReport: { id: 'confirmations.block.block_and_report', defaultMessage: 'Block & Report' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const makeMapStateToProps = () => {
|
const makeMapStateToProps = () => {
|
||||||
@ -134,11 +135,17 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
|||||||
dispatch(openModal('VIDEO', { media, time }));
|
dispatch(openModal('VIDEO', { media, time }));
|
||||||
},
|
},
|
||||||
|
|
||||||
onBlock (account) {
|
onBlock (status) {
|
||||||
|
const account = status.get('account');
|
||||||
dispatch(openModal('CONFIRM', {
|
dispatch(openModal('CONFIRM', {
|
||||||
message: <FormattedMessage id='confirmations.block.message' defaultMessage='Are you sure you want to block {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
|
message: <FormattedMessage id='confirmations.block.message' defaultMessage='Are you sure you want to block {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
|
||||||
confirm: intl.formatMessage(messages.blockConfirm),
|
confirm: intl.formatMessage(messages.blockConfirm),
|
||||||
onConfirm: () => dispatch(blockAccount(account.get('id'))),
|
onConfirm: () => dispatch(blockAccount(account.get('id'))),
|
||||||
|
secondary: intl.formatMessage(messages.blockAndReport),
|
||||||
|
onSecondary: () => {
|
||||||
|
dispatch(blockAccount(account.get('id')));
|
||||||
|
dispatch(initReport(account, status));
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -7,7 +7,6 @@ import { hydrateStore } from '../actions/store';
|
|||||||
import { IntlProvider, addLocaleData } from 'react-intl';
|
import { IntlProvider, addLocaleData } from 'react-intl';
|
||||||
import { getLocale } from '../locales';
|
import { getLocale } from '../locales';
|
||||||
import PublicTimeline from '../features/standalone/public_timeline';
|
import PublicTimeline from '../features/standalone/public_timeline';
|
||||||
import CommunityTimeline from '../features/standalone/community_timeline';
|
|
||||||
import HashtagTimeline from '../features/standalone/hashtag_timeline';
|
import HashtagTimeline from '../features/standalone/hashtag_timeline';
|
||||||
import ModalContainer from '../features/ui/containers/modal_container';
|
import ModalContainer from '../features/ui/containers/modal_container';
|
||||||
import initialState from '../initial_state';
|
import initialState from '../initial_state';
|
||||||
@ -26,24 +25,22 @@ export default class TimelineContainer extends React.PureComponent {
|
|||||||
static propTypes = {
|
static propTypes = {
|
||||||
locale: PropTypes.string.isRequired,
|
locale: PropTypes.string.isRequired,
|
||||||
hashtag: PropTypes.string,
|
hashtag: PropTypes.string,
|
||||||
showPublicTimeline: PropTypes.bool.isRequired,
|
local: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
showPublicTimeline: initialState.settings.known_fediverse,
|
local: !initialState.settings.known_fediverse,
|
||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { locale, hashtag, showPublicTimeline } = this.props;
|
const { locale, hashtag, local } = this.props;
|
||||||
|
|
||||||
let timeline;
|
let timeline;
|
||||||
|
|
||||||
if (hashtag) {
|
if (hashtag) {
|
||||||
timeline = <HashtagTimeline hashtag={hashtag} />;
|
timeline = <HashtagTimeline hashtag={hashtag} />;
|
||||||
} else if (showPublicTimeline) {
|
|
||||||
timeline = <PublicTimeline />;
|
|
||||||
} else {
|
} else {
|
||||||
timeline = <CommunityTimeline />;
|
timeline = <PublicTimeline local={local} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -51,6 +48,7 @@ export default class TimelineContainer extends React.PureComponent {
|
|||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<Fragment>
|
<Fragment>
|
||||||
{timeline}
|
{timeline}
|
||||||
|
|
||||||
{ReactDOM.createPortal(
|
{ReactDOM.createPortal(
|
||||||
<ModalContainer />,
|
<ModalContainer />,
|
||||||
document.getElementById('modal-container'),
|
document.getElementById('modal-container'),
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user