Merge 2.7.3 #8
70
AUTHORS.md
70
AUTHORS.md
|
@ -9,18 +9,18 @@ and provided thanks to the work of the following contributors:
|
||||||
* [akihikodaki](https://github.com/akihikodaki)
|
* [akihikodaki](https://github.com/akihikodaki)
|
||||||
* [ThibG](https://github.com/ThibG)
|
* [ThibG](https://github.com/ThibG)
|
||||||
* [mjankowski](https://github.com/mjankowski)
|
* [mjankowski](https://github.com/mjankowski)
|
||||||
|
* [dependabot[bot]](https://github.com/apps/dependabot)
|
||||||
* [unarist](https://github.com/unarist)
|
* [unarist](https://github.com/unarist)
|
||||||
* [m4sk1n](https://github.com/m4sk1n)
|
* [m4sk1n](https://github.com/m4sk1n)
|
||||||
* [dependabot[bot]](https://github.com/apps/dependabot)
|
|
||||||
* [yiskah](https://github.com/yiskah)
|
* [yiskah](https://github.com/yiskah)
|
||||||
* [nolanlawson](https://github.com/nolanlawson)
|
* [nolanlawson](https://github.com/nolanlawson)
|
||||||
* [sorin-davidoi](https://github.com/sorin-davidoi)
|
|
||||||
* [ysksn](https://github.com/ysksn)
|
* [ysksn](https://github.com/ysksn)
|
||||||
|
* [sorin-davidoi](https://github.com/sorin-davidoi)
|
||||||
* [abcang](https://github.com/abcang)
|
* [abcang](https://github.com/abcang)
|
||||||
* [lynlynlynx](https://github.com/lynlynlynx)
|
* [lynlynlynx](https://github.com/lynlynlynx)
|
||||||
* [alpaca-tc](https://github.com/alpaca-tc)
|
|
||||||
* [mayaeh](https://github.com/mayaeh)
|
* [mayaeh](https://github.com/mayaeh)
|
||||||
* [renatolond](https://github.com/renatolond)
|
* [renatolond](https://github.com/renatolond)
|
||||||
|
* [alpaca-tc](https://github.com/alpaca-tc)
|
||||||
* [nclm](https://github.com/nclm)
|
* [nclm](https://github.com/nclm)
|
||||||
* [ineffyble](https://github.com/ineffyble)
|
* [ineffyble](https://github.com/ineffyble)
|
||||||
* [jeroenpraat](https://github.com/jeroenpraat)
|
* [jeroenpraat](https://github.com/jeroenpraat)
|
||||||
|
@ -28,9 +28,9 @@ and provided thanks to the work of the following contributors:
|
||||||
* [Quent-in](https://github.com/Quent-in)
|
* [Quent-in](https://github.com/Quent-in)
|
||||||
* [JantsoP](https://github.com/JantsoP)
|
* [JantsoP](https://github.com/JantsoP)
|
||||||
* [mabkenar](https://github.com/mabkenar)
|
* [mabkenar](https://github.com/mabkenar)
|
||||||
|
* [Kjwon15](https://github.com/Kjwon15)
|
||||||
* [nullkal](https://github.com/nullkal)
|
* [nullkal](https://github.com/nullkal)
|
||||||
* [yookoala](https://github.com/yookoala)
|
* [yookoala](https://github.com/yookoala)
|
||||||
* [Kjwon15](https://github.com/Kjwon15)
|
|
||||||
* [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)
|
* [Quenty31](https://github.com/Quenty31)
|
||||||
|
@ -48,16 +48,16 @@ and provided thanks to the work of the following contributors:
|
||||||
* [rkarabut](https://github.com/rkarabut)
|
* [rkarabut](https://github.com/rkarabut)
|
||||||
* [yukimochi](https://github.com/yukimochi)
|
* [yukimochi](https://github.com/yukimochi)
|
||||||
* [Artoria2e5](https://github.com/Artoria2e5)
|
* [Artoria2e5](https://github.com/Artoria2e5)
|
||||||
|
* [nightpool](https://github.com/nightpool)
|
||||||
* [marrus-sh](https://github.com/marrus-sh)
|
* [marrus-sh](https://github.com/marrus-sh)
|
||||||
* [krainboltgreene](https://github.com/krainboltgreene)
|
* [krainboltgreene](https://github.com/krainboltgreene)
|
||||||
* [patf](https://github.com/patf)
|
* [pfigel](https://github.com/pfigel)
|
||||||
* [Aldarone](https://github.com/Aldarone)
|
* [Aldarone](https://github.com/Aldarone)
|
||||||
* [BoFFire](https://github.com/BoFFire)
|
* [BoFFire](https://github.com/BoFFire)
|
||||||
* [clworld](https://github.com/clworld)
|
* [clworld](https://github.com/clworld)
|
||||||
* [dracos](https://github.com/dracos)
|
* [dracos](https://github.com/dracos)
|
||||||
* [SerCom_KC](mailto:sercom-kc@users.noreply.github.com)
|
* [SerCom_KC](mailto:sercom-kc@users.noreply.github.com)
|
||||||
* [Sylvhem](https://github.com/Sylvhem)
|
* [Sylvhem](https://github.com/Sylvhem)
|
||||||
* [nightpool](https://github.com/nightpool)
|
|
||||||
* [MasterGroosha](https://github.com/MasterGroosha)
|
* [MasterGroosha](https://github.com/MasterGroosha)
|
||||||
* [JeanGauthier](https://github.com/JeanGauthier)
|
* [JeanGauthier](https://github.com/JeanGauthier)
|
||||||
* [kschaper](https://github.com/kschaper)
|
* [kschaper](https://github.com/kschaper)
|
||||||
|
@ -77,11 +77,14 @@ and provided thanks to the work of the following contributors:
|
||||||
* [johnsudaar](https://github.com/johnsudaar)
|
* [johnsudaar](https://github.com/johnsudaar)
|
||||||
* [trebmuh](https://github.com/trebmuh)
|
* [trebmuh](https://github.com/trebmuh)
|
||||||
* [Rakib Hasan](mailto:rmhasan@gmail.com)
|
* [Rakib Hasan](mailto:rmhasan@gmail.com)
|
||||||
|
* [ashleyhull-versent](https://github.com/ashleyhull-versent)
|
||||||
* [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)
|
||||||
|
@ -92,20 +95,20 @@ and provided thanks to the work of the following contributors:
|
||||||
* [dunn](https://github.com/dunn)
|
* [dunn](https://github.com/dunn)
|
||||||
* [xqus](https://github.com/xqus)
|
* [xqus](https://github.com/xqus)
|
||||||
* [hugogameiro](https://github.com/hugogameiro)
|
* [hugogameiro](https://github.com/hugogameiro)
|
||||||
|
* [ariasuni](https://github.com/ariasuni)
|
||||||
* [pfm-eyesightjp](https://github.com/pfm-eyesightjp)
|
* [pfm-eyesightjp](https://github.com/pfm-eyesightjp)
|
||||||
* [fakenine](https://github.com/fakenine)
|
* [fakenine](https://github.com/fakenine)
|
||||||
* [tsuwatch](https://github.com/tsuwatch)
|
* [tsuwatch](https://github.com/tsuwatch)
|
||||||
* [victorhck](https://github.com/victorhck)
|
* [victorhck](https://github.com/victorhck)
|
||||||
* [ashleyhull-versent](https://github.com/ashleyhull-versent)
|
|
||||||
* [kedamaDQ](https://github.com/kedamaDQ)
|
* [kedamaDQ](https://github.com/kedamaDQ)
|
||||||
* [puckipedia](https://github.com/puckipedia)
|
* [puckipedia](https://github.com/puckipedia)
|
||||||
* [fvh-P](https://github.com/fvh-P)
|
* [fvh-P](https://github.com/fvh-P)
|
||||||
* [contraexemplo](https://github.com/contraexemplo)
|
* [contraexemplo](https://github.com/contraexemplo)
|
||||||
|
* [Aditoo17](https://github.com/Aditoo17)
|
||||||
* [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)
|
||||||
* [diomed](https://github.com/diomed)
|
* [diomed](https://github.com/diomed)
|
||||||
* [ariasuni](https://github.com/ariasuni)
|
|
||||||
* [Neetshin](mailto:neetshin@neetsh.in)
|
* [Neetshin](mailto:neetshin@neetsh.in)
|
||||||
* [rainyday](https://github.com/rainyday)
|
* [rainyday](https://github.com/rainyday)
|
||||||
* [ProgVal](https://github.com/ProgVal)
|
* [ProgVal](https://github.com/ProgVal)
|
||||||
|
@ -114,7 +117,8 @@ and provided thanks to the work of the following contributors:
|
||||||
* [goofy-bz](mailto:goofy@babelzilla.org)
|
* [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)
|
||||||
* [rtucker](https://github.com/rtucker)
|
* [trwnh](https://github.com/trwnh)
|
||||||
|
* [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)
|
||||||
* [TheKinrar](https://github.com/TheKinrar)
|
* [TheKinrar](https://github.com/TheKinrar)
|
||||||
|
@ -125,16 +129,16 @@ and provided thanks to the work of the following contributors:
|
||||||
* [fhemberger](https://github.com/fhemberger)
|
* [fhemberger](https://github.com/fhemberger)
|
||||||
* [greysteil](https://github.com/greysteil)
|
* [greysteil](https://github.com/greysteil)
|
||||||
* [hensmith](https://github.com/hensmith)
|
* [hensmith](https://github.com/hensmith)
|
||||||
* [hinaloe](https://github.com/hinaloe)
|
|
||||||
* [d6rkaiz](https://github.com/d6rkaiz)
|
* [d6rkaiz](https://github.com/d6rkaiz)
|
||||||
* [Reverite](https://github.com/Reverite)
|
* [Reverite](https://github.com/Reverite)
|
||||||
* [JMendyk](https://github.com/JMendyk)
|
|
||||||
* [JohnD28](https://github.com/JohnD28)
|
* [JohnD28](https://github.com/JohnD28)
|
||||||
* [znz](https://github.com/znz)
|
* [znz](https://github.com/znz)
|
||||||
* [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)
|
||||||
* [reneklacan](https://github.com/reneklacan)
|
* [reneklacan](https://github.com/reneklacan)
|
||||||
* [ekiru](https://github.com/ekiru)
|
* [ekiru](https://github.com/ekiru)
|
||||||
|
* [noellabo](https://github.com/noellabo)
|
||||||
* [tcitworld](https://github.com/tcitworld)
|
* [tcitworld](https://github.com/tcitworld)
|
||||||
* [geta6](https://github.com/geta6)
|
* [geta6](https://github.com/geta6)
|
||||||
* [happycoloredbanana](https://github.com/happycoloredbanana)
|
* [happycoloredbanana](https://github.com/happycoloredbanana)
|
||||||
|
@ -144,9 +148,9 @@ and provided thanks to the work of the following contributors:
|
||||||
* [noraworld](https://github.com/noraworld)
|
* [noraworld](https://github.com/noraworld)
|
||||||
* [theboss](https://github.com/theboss)
|
* [theboss](https://github.com/theboss)
|
||||||
* [178inaba](https://github.com/178inaba)
|
* [178inaba](https://github.com/178inaba)
|
||||||
* [Aditoo17](https://github.com/Aditoo17)
|
|
||||||
* [alyssais](https://github.com/alyssais)
|
* [alyssais](https://github.com/alyssais)
|
||||||
* [kodnaplakal](https://github.com/kodnaplakal)
|
* [hiphref](https://github.com/hiphref)
|
||||||
|
* [BenLubar](https://github.com/BenLubar)
|
||||||
* [stalker314314](https://github.com/stalker314314)
|
* [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)
|
||||||
|
@ -157,6 +161,7 @@ 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)
|
||||||
|
@ -164,7 +169,6 @@ and provided thanks to the work of the following contributors:
|
||||||
* [Ram Lmn](mailto:ramlmn@users.noreply.github.com)
|
* [Ram Lmn](mailto:ramlmn@users.noreply.github.com)
|
||||||
* [harukasan](https://github.com/harukasan)
|
* [harukasan](https://github.com/harukasan)
|
||||||
* [stamak](https://github.com/stamak)
|
* [stamak](https://github.com/stamak)
|
||||||
* [noellabo](https://github.com/noellabo)
|
|
||||||
* [Technowix](mailto:technowix@users.noreply.github.com)
|
* [Technowix](mailto:technowix@users.noreply.github.com)
|
||||||
* [Eychics](https://github.com/Eychics)
|
* [Eychics](https://github.com/Eychics)
|
||||||
* [Thor Harald Johansen](mailto:thj@thj.no)
|
* [Thor Harald Johansen](mailto:thj@thj.no)
|
||||||
|
@ -179,21 +183,20 @@ and provided thanks to the work of the following contributors:
|
||||||
* [hoodie](mailto:hoodiekitten@outlook.com)
|
* [hoodie](mailto:hoodiekitten@outlook.com)
|
||||||
* [luzi82](https://github.com/luzi82)
|
* [luzi82](https://github.com/luzi82)
|
||||||
* [duxovni](https://github.com/duxovni)
|
* [duxovni](https://github.com/duxovni)
|
||||||
* [trwnh](https://github.com/trwnh)
|
* [tmm576](https://github.com/tmm576)
|
||||||
* [unsmell](https://github.com/unsmell)
|
* [unsmell](https://github.com/unsmell)
|
||||||
* [valerauko](https://github.com/valerauko)
|
* [valerauko](https://github.com/valerauko)
|
||||||
* [chriswmartin](https://github.com/chriswmartin)
|
* [chriswmartin](https://github.com/chriswmartin)
|
||||||
* [vahnj](https://github.com/vahnj)
|
* [vahnj](https://github.com/vahnj)
|
||||||
* [ikuradon](https://github.com/ikuradon)
|
* [ikuradon](https://github.com/ikuradon)
|
||||||
* [AndreLewin](https://github.com/AndreLewin)
|
* [AndreLewin](https://github.com/AndreLewin)
|
||||||
* [rinsuki](https://github.com/rinsuki)
|
|
||||||
* [0xflotus](https://github.com/0xflotus)
|
* [0xflotus](https://github.com/0xflotus)
|
||||||
* [redtachyons](https://github.com/redtachyons)
|
* [redtachyons](https://github.com/redtachyons)
|
||||||
* [thurloat](https://github.com/thurloat)
|
* [thurloat](https://github.com/thurloat)
|
||||||
* [aaribaud](https://github.com/aaribaud)
|
* [aaribaud](https://github.com/aaribaud)
|
||||||
|
* [pointlessone](https://github.com/pointlessone)
|
||||||
* [Andrew](mailto:andrewlchronister@gmail.com)
|
* [Andrew](mailto:andrewlchronister@gmail.com)
|
||||||
* [estuans](https://github.com/estuans)
|
* [estuans](https://github.com/estuans)
|
||||||
* [BenLubar](https://github.com/BenLubar)
|
|
||||||
* [dissolve](https://github.com/dissolve)
|
* [dissolve](https://github.com/dissolve)
|
||||||
* [PurpleBooth](https://github.com/PurpleBooth)
|
* [PurpleBooth](https://github.com/PurpleBooth)
|
||||||
* [bradurani](https://github.com/bradurani)
|
* [bradurani](https://github.com/bradurani)
|
||||||
|
@ -216,6 +219,7 @@ and provided thanks to the work of the following contributors:
|
||||||
* [ErikXXon](https://github.com/ErikXXon)
|
* [ErikXXon](https://github.com/ErikXXon)
|
||||||
* [ian-kelling](https://github.com/ian-kelling)
|
* [ian-kelling](https://github.com/ian-kelling)
|
||||||
* [immae](https://github.com/immae)
|
* [immae](https://github.com/immae)
|
||||||
|
* [J0WI](https://github.com/J0WI)
|
||||||
* [foozmeat](https://github.com/foozmeat)
|
* [foozmeat](https://github.com/foozmeat)
|
||||||
* [jasonrhodes](https://github.com/jasonrhodes)
|
* [jasonrhodes](https://github.com/jasonrhodes)
|
||||||
* [Jason Snell](mailto:jason@newrelic.com)
|
* [Jason Snell](mailto:jason@newrelic.com)
|
||||||
|
@ -230,6 +234,7 @@ and provided thanks to the work of the following contributors:
|
||||||
* [Lorenz Diener](mailto:halcyon@icosahedron.website)
|
* [Lorenz Diener](mailto:halcyon@icosahedron.website)
|
||||||
* [alimony](https://github.com/alimony)
|
* [alimony](https://github.com/alimony)
|
||||||
* [mig5](https://github.com/mig5)
|
* [mig5](https://github.com/mig5)
|
||||||
|
* [moritzheiber](https://github.com/moritzheiber)
|
||||||
* [ndarville](https://github.com/ndarville)
|
* [ndarville](https://github.com/ndarville)
|
||||||
* [Abzol](https://github.com/Abzol)
|
* [Abzol](https://github.com/Abzol)
|
||||||
* [pwoolcoc](https://github.com/pwoolcoc)
|
* [pwoolcoc](https://github.com/pwoolcoc)
|
||||||
|
@ -238,6 +243,7 @@ and provided thanks to the work of the following contributors:
|
||||||
* [ignisf](https://github.com/ignisf)
|
* [ignisf](https://github.com/ignisf)
|
||||||
* [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)
|
||||||
* [sascha-sl](https://github.com/sascha-sl)
|
* [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)
|
||||||
|
@ -288,6 +294,7 @@ and provided thanks to the work of the following contributors:
|
||||||
* [857b](https://github.com/857b)
|
* [857b](https://github.com/857b)
|
||||||
* [insom](https://github.com/insom)
|
* [insom](https://github.com/insom)
|
||||||
* [tachyons](https://github.com/tachyons)
|
* [tachyons](https://github.com/tachyons)
|
||||||
|
* [acid-chicken](https://github.com/acid-chicken)
|
||||||
* [Esteth](https://github.com/Esteth)
|
* [Esteth](https://github.com/Esteth)
|
||||||
* [unascribed](https://github.com/unascribed)
|
* [unascribed](https://github.com/unascribed)
|
||||||
* [Aguay-val](https://github.com/Aguay-val)
|
* [Aguay-val](https://github.com/Aguay-val)
|
||||||
|
@ -297,7 +304,6 @@ and provided thanks to the work of the following contributors:
|
||||||
* [unleashed](https://github.com/unleashed)
|
* [unleashed](https://github.com/unleashed)
|
||||||
* [alxrcs](https://github.com/alxrcs)
|
* [alxrcs](https://github.com/alxrcs)
|
||||||
* [console-cowboy](https://github.com/console-cowboy)
|
* [console-cowboy](https://github.com/console-cowboy)
|
||||||
* [pointlessone](https://github.com/pointlessone)
|
|
||||||
* [Alkarex](https://github.com/Alkarex)
|
* [Alkarex](https://github.com/Alkarex)
|
||||||
* [a2](https://github.com/a2)
|
* [a2](https://github.com/a2)
|
||||||
* [0xa](https://github.com/0xa)
|
* [0xa](https://github.com/0xa)
|
||||||
|
@ -329,6 +335,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)
|
||||||
* [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)
|
||||||
|
@ -356,6 +363,7 @@ and provided thanks to the work of the following contributors:
|
||||||
* [espenronnevik](https://github.com/espenronnevik)
|
* [espenronnevik](https://github.com/espenronnevik)
|
||||||
* [Finariel](https://github.com/Finariel)
|
* [Finariel](https://github.com/Finariel)
|
||||||
* [siuying](https://github.com/siuying)
|
* [siuying](https://github.com/siuying)
|
||||||
|
* [zoc](https://github.com/zoc)
|
||||||
* [fwenzel](https://github.com/fwenzel)
|
* [fwenzel](https://github.com/fwenzel)
|
||||||
* [GenbuHase](https://github.com/GenbuHase)
|
* [GenbuHase](https://github.com/GenbuHase)
|
||||||
* [hattori6789](https://github.com/hattori6789)
|
* [hattori6789](https://github.com/hattori6789)
|
||||||
|
@ -416,6 +424,7 @@ and provided thanks to the work of the following contributors:
|
||||||
* [martymcguire](https://github.com/martymcguire)
|
* [martymcguire](https://github.com/martymcguire)
|
||||||
* [marvinkopf](https://github.com/marvinkopf)
|
* [marvinkopf](https://github.com/marvinkopf)
|
||||||
* [otsune](https://github.com/otsune)
|
* [otsune](https://github.com/otsune)
|
||||||
|
* [mbugowski](https://github.com/mbugowski)
|
||||||
* [Mathias B](mailto:10813340+mathias-b@users.noreply.github.com)
|
* [Mathias B](mailto:10813340+mathias-b@users.noreply.github.com)
|
||||||
* [matt-auckland](https://github.com/matt-auckland)
|
* [matt-auckland](https://github.com/matt-auckland)
|
||||||
* [webroo](https://github.com/webroo)
|
* [webroo](https://github.com/webroo)
|
||||||
|
@ -434,7 +443,6 @@ and provided thanks to the work of the following contributors:
|
||||||
* [premist](https://github.com/premist)
|
* [premist](https://github.com/premist)
|
||||||
* [Mnkai](https://github.com/Mnkai)
|
* [Mnkai](https://github.com/Mnkai)
|
||||||
* [mitchhentges](https://github.com/mitchhentges)
|
* [mitchhentges](https://github.com/mitchhentges)
|
||||||
* [moritzheiber](https://github.com/moritzheiber)
|
|
||||||
* [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)
|
||||||
|
@ -458,17 +466,17 @@ and provided thanks to the work of the following contributors:
|
||||||
* [Pangoraw](https://github.com/Pangoraw)
|
* [Pangoraw](https://github.com/Pangoraw)
|
||||||
* [peterkeen](https://github.com/peterkeen)
|
* [peterkeen](https://github.com/peterkeen)
|
||||||
* [pgate](https://github.com/pgate)
|
* [pgate](https://github.com/pgate)
|
||||||
* [retokromer](https://github.com/retokromer)
|
* [Reto Kromer](mailto:retokromer@users.noreply.github.com)
|
||||||
* [rfwatson](https://github.com/rfwatson)
|
* [Rey Tucker](mailto:git@reytucker.us)
|
||||||
* [rfreebern](https://github.com/rfreebern)
|
* [Rob Watson](mailto:rfwatson@users.noreply.github.com)
|
||||||
|
* [Ryan Freebern](mailto:ryan@freebern.org)
|
||||||
* [Ryan Wade](mailto:ryan.wade@protonmail.com)
|
* [Ryan Wade](mailto:ryan.wade@protonmail.com)
|
||||||
* [sylph01](https://github.com/sylph01)
|
* [Ryo Kajiwara](mailto:kfe-fecn6.prussian@s01.info)
|
||||||
* [S-H-GAMELINKS](https://github.com/S-H-GAMELINKS)
|
* [S.H](mailto:gamelinks007@gmail.com)
|
||||||
* [staticsafe](https://github.com/staticsafe)
|
* [Sadiq Saif](mailto:staticsafe@users.noreply.github.com)
|
||||||
* [snwh](https://github.com/snwh)
|
* [Sam Hewitt](mailto:hewittsamuel@gmail.com)
|
||||||
* [sts10](https://github.com/sts10)
|
* [Satoshi KOJIMA](mailto:skoji@mac.com)
|
||||||
* [skoji](https://github.com/skoji)
|
* [ScienJus](mailto:i@scienjus.com)
|
||||||
* [ScienJus](https://github.com/ScienJus)
|
|
||||||
* [Scott Larkin](mailto:scott@codeclimate.com)
|
* [Scott Larkin](mailto:scott@codeclimate.com)
|
||||||
* [Sebastian Hübner](mailto:imolein@users.noreply.github.com)
|
* [Sebastian Hübner](mailto:imolein@users.noreply.github.com)
|
||||||
* [Sebastian Morr](mailto:sebastian@morr.cc)
|
* [Sebastian Morr](mailto:sebastian@morr.cc)
|
||||||
|
@ -483,6 +491,7 @@ and provided thanks to the work of the following contributors:
|
||||||
* [Sir-Boops](mailto:admin@boops.me)
|
* [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)
|
||||||
* [StefOfficiel](mailto:pichard.stephane@free.fr)
|
* [StefOfficiel](mailto:pichard.stephane@free.fr)
|
||||||
* [Steven Tappert](mailto:admin@dark-it.net)
|
* [Steven Tappert](mailto:admin@dark-it.net)
|
||||||
* [Svetlozar Todorov](mailto:svetlik@users.noreply.github.com)
|
* [Svetlozar Todorov](mailto:svetlik@users.noreply.github.com)
|
||||||
|
@ -532,6 +541,7 @@ and provided thanks to the work of the following contributors:
|
||||||
* [fsubal](mailto:fsubal@users.noreply.github.com)
|
* [fsubal](mailto:fsubal@users.noreply.github.com)
|
||||||
* [fusshi-](mailto:dikky1218@users.noreply.github.com)
|
* [fusshi-](mailto:dikky1218@users.noreply.github.com)
|
||||||
* [gentaro](mailto:gentaroooo@gmail.com)
|
* [gentaro](mailto:gentaroooo@gmail.com)
|
||||||
|
* [gol-cha](mailto:info@mevo.xyz)
|
||||||
* [hakoai](mailto:hk--76@qa2.so-net.ne.jp)
|
* [hakoai](mailto:hk--76@qa2.so-net.ne.jp)
|
||||||
* [haosbvnker](mailto:github@chaosbunker.com)
|
* [haosbvnker](mailto:github@chaosbunker.com)
|
||||||
* [isati](mailto:phil@juchnowi.cz)
|
* [isati](mailto:phil@juchnowi.cz)
|
||||||
|
@ -549,12 +559,12 @@ and provided thanks to the work of the following contributors:
|
||||||
* [luzpaz](mailto:luzpaz@users.noreply.github.com)
|
* [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)
|
||||||
* [mimikun](mailto:dzdzble_effort_311@outlook.jp)
|
* [mimikun](mailto:dzdzble_effort_311@outlook.jp)
|
||||||
* [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)
|
||||||
* [neetshin](mailto:neetshin@neetsh.in)
|
* [neetshin](mailto:neetshin@neetsh.in)
|
||||||
* [nightpool](mailto:nightpool@users.noreply.github.com)
|
|
||||||
* [rch850](mailto:rich850@gmail.com)
|
* [rch850](mailto:rich850@gmail.com)
|
||||||
* [roikale](mailto:roikale@users.noreply.github.com)
|
* [roikale](mailto:roikale@users.noreply.github.com)
|
||||||
* [rysiekpl](mailto:rysiek@hackerspace.pl)
|
* [rysiekpl](mailto:rysiek@hackerspace.pl)
|
||||||
|
|
58
CHANGELOG.md
58
CHANGELOG.md
|
@ -3,6 +3,64 @@ 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.7.3] - 2019-02-23
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Add domain filter to the admin federation page ([ThibG](https://github.com/tootsuite/mastodon/pull/10071))
|
||||||
|
- Add quick link from admin account view to block/unblock instance ([ThibG](https://github.com/tootsuite/mastodon/pull/10073))
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix video player width not being updated to fit container width ([ThibG](https://github.com/tootsuite/mastodon/pull/10069))
|
||||||
|
- Fix domain filter being shown in admin page when local filter is active ([ThibG](https://github.com/tootsuite/mastodon/pull/10074))
|
||||||
|
- Fix crash when conversations have no valid participants ([ThibG](https://github.com/tootsuite/mastodon/pull/10078))
|
||||||
|
- Fix error when performing admin actions on no statuses ([ThibG](https://github.com/tootsuite/mastodon/pull/10094))
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Change custom emojis to randomize stored file name ([hinaloe](https://github.com/tootsuite/mastodon/pull/10090))
|
||||||
|
|
||||||
|
## [2.7.2] - 2019-02-17
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Add support for IPv6 in e-mail validation ([zoc](https://github.com/tootsuite/mastodon/pull/10009))
|
||||||
|
- Add record of IP address used for signing up ([ThibG](https://github.com/tootsuite/mastodon/pull/10026))
|
||||||
|
- Add tight rate-limit for API deletions (30 per 30 minutes) ([Gargron](https://github.com/tootsuite/mastodon/pull/10042))
|
||||||
|
- Add support for embedded `Announce` objects attributed to the same actor ([ThibG](https://github.com/tootsuite/mastodon/pull/9998), [Gargron](https://github.com/tootsuite/mastodon/pull/10065))
|
||||||
|
- Add spam filter for `Create` and `Announce` activities ([Gargron](https://github.com/tootsuite/mastodon/pull/10005), [Gargron](https://github.com/tootsuite/mastodon/pull/10041), [Gargron](https://github.com/tootsuite/mastodon/pull/10062))
|
||||||
|
- Add `registrations` attribute to `GET /api/v1/instance` ([Gargron](https://github.com/tootsuite/mastodon/pull/10060))
|
||||||
|
- Add `vapid_key` to `POST /api/v1/apps` and `GET /api/v1/apps/verify_credentials` ([Gargron](https://github.com/tootsuite/mastodon/pull/10058))
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix link color and add link underlines in high-contrast theme ([Gargron](https://github.com/tootsuite/mastodon/pull/9949), [Gargron](https://github.com/tootsuite/mastodon/pull/10028))
|
||||||
|
- Fix unicode characters in URLs not being linkified ([JMendyk](https://github.com/tootsuite/mastodon/pull/8447), [hinaloe](https://github.com/tootsuite/mastodon/pull/9991))
|
||||||
|
- Fix URLs linkifier grabbing ending quotation as part of the link ([Gargron](https://github.com/tootsuite/mastodon/pull/9997))
|
||||||
|
- Fix authorized applications page design ([rinsuki](https://github.com/tootsuite/mastodon/pull/9969))
|
||||||
|
- Fix custom emojis not showing up in share page emoji picker ([rinsuki](https://github.com/tootsuite/mastodon/pull/9970))
|
||||||
|
- Fix too liberal application of whitespace in toots ([trwnh](https://github.com/tootsuite/mastodon/pull/9968))
|
||||||
|
- Fix misleading e-mail hint being displayed in admin view ([ThibG](https://github.com/tootsuite/mastodon/pull/9973))
|
||||||
|
- Fix tombstones not being cleared out ([abcang](https://github.com/tootsuite/mastodon/pull/9978))
|
||||||
|
- Fix some timeline jumps ([ThibG](https://github.com/tootsuite/mastodon/pull/9982), [ThibG](https://github.com/tootsuite/mastodon/pull/10001), [rinsuki](https://github.com/tootsuite/mastodon/pull/10046))
|
||||||
|
- Fix content warning input taking keyboard focus even when hidden ([hinaloe](https://github.com/tootsuite/mastodon/pull/10017))
|
||||||
|
- Fix hashtags select styling in default and high-contrast themes ([Gargron](https://github.com/tootsuite/mastodon/pull/10029))
|
||||||
|
- Fix style regressions on landing page ([Gargron](https://github.com/tootsuite/mastodon/pull/10030))
|
||||||
|
- Fix hashtag column not subscribing to stream on mount ([Gargron](https://github.com/tootsuite/mastodon/pull/10040))
|
||||||
|
- Fix relay enabling/disabling not resetting inbox availability status ([Gargron](https://github.com/tootsuite/mastodon/pull/10048))
|
||||||
|
- Fix mutes, blocks, domain blocks and follow requests not paginating ([Gargron](https://github.com/tootsuite/mastodon/pull/10057))
|
||||||
|
- Fix crash on public hashtag pages when streaming fails ([ThibG](https://github.com/tootsuite/mastodon/pull/10061))
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Change icon for unlisted visibility level ([clarcharr](https://github.com/tootsuite/mastodon/pull/9952))
|
||||||
|
- Change queue of actor deletes from push to pull for non-follower recipients ([ThibG](https://github.com/tootsuite/mastodon/pull/10016))
|
||||||
|
- Change robots.txt to exclude media proxy URLs ([nightpool](https://github.com/tootsuite/mastodon/pull/10038))
|
||||||
|
- Change upload description input to allow line breaks ([BenLubar](https://github.com/tootsuite/mastodon/pull/10036))
|
||||||
|
- Change `dist/mastodon-streaming.service` to recommend running node without intermediary npm command ([nolanlawson](https://github.com/tootsuite/mastodon/pull/10032))
|
||||||
|
- Change conversations to always show names of other participants ([Gargron](https://github.com/tootsuite/mastodon/pull/10047))
|
||||||
|
- Change buttons on timeline preview to open the interaction dialog ([Gargron](https://github.com/tootsuite/mastodon/pull/10054))
|
||||||
|
- Change error graphic to hover-to-play ([Gargron](https://github.com/tootsuite/mastodon/pull/10055))
|
||||||
|
|
||||||
## [2.7.1] - 2019-01-28
|
## [2.7.1] - 2019-01-28
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
|
|
@ -86,7 +86,7 @@ You can open issues for bugs you've found or features you think are missing. You
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Copyright (C) 2016-2018 Eugen Rochko & other Mastodon contributors (see [AUTHORS.md](AUTHORS.md))
|
Copyright (C) 2016-2019 Eugen Rochko & other Mastodon contributors (see [AUTHORS.md](AUTHORS.md))
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
|
|
@ -31,7 +31,7 @@ class StatusesIndex < Chewy::Index
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
define_type ::Status.unscoped.without_reblogs do
|
define_type ::Status.unscoped.without_reblogs.includes(:media_attachments) do
|
||||||
crutch :mentions do |collection|
|
crutch :mentions do |collection|
|
||||||
data = ::Mention.where(status_id: collection.map(&:id)).pluck(:status_id, :account_id)
|
data = ::Mention.where(status_id: collection.map(&:id)).pluck(:status_id, :account_id)
|
||||||
data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }
|
data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }
|
||||||
|
@ -50,7 +50,7 @@ class StatusesIndex < Chewy::Index
|
||||||
root date_detection: false do
|
root date_detection: false do
|
||||||
field :account_id, type: 'long'
|
field :account_id, type: 'long'
|
||||||
|
|
||||||
field :text, type: 'text', value: ->(status) { [status.spoiler_text, Formatter.instance.plaintext(status)].join("\n\n") } do
|
field :text, type: 'text', value: ->(status) { [status.spoiler_text, Formatter.instance.plaintext(status)].concat(status.media_attachments.map(&:description)).join("\n\n") } do
|
||||||
field :stemmed, type: 'text', analyzer: 'content'
|
field :stemmed, type: 'text', analyzer: 'content'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -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])
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,6 @@ class Api::V1::Apps::CredentialsController < Api::BaseController
|
||||||
respond_to :json
|
respond_to :json
|
||||||
|
|
||||||
def show
|
def show
|
||||||
render json: doorkeeper_token.application, serializer: REST::StatusSerializer::ApplicationSerializer
|
render json: doorkeeper_token.application, serializer: REST::ApplicationSerializer, fields: %i(name website vapid_key)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -28,6 +28,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
||||||
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 if resource.current_sign_in_ip.nil?
|
||||||
resource.build_account if resource.account.nil?
|
resource.build_account if resource.account.nil?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio
|
||||||
|
|
||||||
before_action :store_current_location
|
before_action :store_current_location
|
||||||
before_action :authenticate_resource_owner!
|
before_action :authenticate_resource_owner!
|
||||||
|
before_action :set_body_classes
|
||||||
|
|
||||||
include Localized
|
include Localized
|
||||||
|
|
||||||
|
@ -15,6 +16,10 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def set_body_classes
|
||||||
|
@body_classes = 'admin'
|
||||||
|
end
|
||||||
|
|
||||||
def store_current_location
|
def store_current_location
|
||||||
store_location_for(:user, request.url)
|
store_location_for(:user, request.url)
|
||||||
end
|
end
|
||||||
|
|
|
@ -6,7 +6,7 @@ module Admin::FilterHelper
|
||||||
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
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
|
|
|
@ -170,7 +170,7 @@ module StreamEntriesHelper
|
||||||
when 'public'
|
when 'public'
|
||||||
fa_icon 'globe fw'
|
fa_icon 'globe fw'
|
||||||
when 'unlisted'
|
when 'unlisted'
|
||||||
fa_icon 'unlock-alt fw'
|
fa_icon 'unlock fw'
|
||||||
when 'private'
|
when 'private'
|
||||||
fa_icon 'lock fw'
|
fa_icon 'lock fw'
|
||||||
when 'direct'
|
when 'direct'
|
||||||
|
|
|
@ -88,7 +88,7 @@ class Account extends ImmutablePureComponent {
|
||||||
if (requested) {
|
if (requested) {
|
||||||
buttons = <IconButton disabled icon='hourglass' title={intl.formatMessage(messages.requested)} />;
|
buttons = <IconButton disabled icon='hourglass' title={intl.formatMessage(messages.requested)} />;
|
||||||
} else if (blocking) {
|
} else if (blocking) {
|
||||||
buttons = <IconButton active icon='unlock-alt' title={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.handleBlock} />;
|
buttons = <IconButton active icon='unlock' title={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.handleBlock} />;
|
||||||
} else if (muting) {
|
} else if (muting) {
|
||||||
let hidingNotificationsButton;
|
let hidingNotificationsButton;
|
||||||
if (account.getIn(['relationship', 'muting_notifications'])) {
|
if (account.getIn(['relationship', 'muting_notifications'])) {
|
||||||
|
|
|
@ -11,26 +11,36 @@ export default class DisplayName extends React.PureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { account, others, localDomain } = this.props;
|
const { others, localDomain } = this.props;
|
||||||
const displayNameHtml = { __html: account.get('display_name_html') };
|
|
||||||
|
|
||||||
let suffix;
|
let displayName, suffix, account;
|
||||||
|
|
||||||
if (others && others.size > 1) {
|
if (others && others.size > 1) {
|
||||||
suffix = `+${others.size}`;
|
displayName = others.take(2).map(a => <bdi key={a.get('id')}><strong className='display-name__html' dangerouslySetInnerHTML={{ __html: a.get('display_name_html') }} /></bdi>).reduce((prev, cur) => [prev, ', ', cur]);
|
||||||
|
|
||||||
|
if (others.size - 2 > 0) {
|
||||||
|
suffix = `+${others.size - 2}`;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
if (others && others.size > 0) {
|
||||||
|
account = others.first();
|
||||||
|
} else {
|
||||||
|
account = this.props.account;
|
||||||
|
}
|
||||||
|
|
||||||
let acct = account.get('acct');
|
let acct = account.get('acct');
|
||||||
|
|
||||||
if (acct.indexOf('@') === -1 && localDomain) {
|
if (acct.indexOf('@') === -1 && localDomain) {
|
||||||
acct = `${acct}@${localDomain}`;
|
acct = `${acct}@${localDomain}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
displayName = <bdi><strong className='display-name__html' dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }} /></bdi>;
|
||||||
suffix = <span className='display-name__account'>@{acct}</span>;
|
suffix = <span className='display-name__account'>@{acct}</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className='display-name'>
|
<span className='display-name'>
|
||||||
<bdi><strong className='display-name__html' dangerouslySetInnerHTML={displayNameHtml} /></bdi> {suffix}
|
{displayName} {suffix}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,7 +32,7 @@ class Account extends ImmutablePureComponent {
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div className='domain__buttons'>
|
<div className='domain__buttons'>
|
||||||
<IconButton active icon='unlock-alt' title={intl.formatMessage(messages.unblockDomain, { domain })} onClick={this.handleDomainUnblock} />
|
<IconButton active icon='unlock' title={intl.formatMessage(messages.unblockDomain, { domain })} onClick={this.handleDomainUnblock} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -65,7 +65,7 @@ export default class IntersectionObserverArticle extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
updateStateAfterIntersection = (prevState) => {
|
updateStateAfterIntersection = (prevState) => {
|
||||||
if (prevState.isIntersecting && !this.entry.isIntersecting) {
|
if (prevState.isIntersecting !== false && !this.entry.isIntersecting) {
|
||||||
scheduleIdleTask(this.hideIfNotIntersecting);
|
scheduleIdleTask(this.hideIfNotIntersecting);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -194,6 +194,8 @@ class MediaGallery extends React.PureComponent {
|
||||||
height: PropTypes.number.isRequired,
|
height: PropTypes.number.isRequired,
|
||||||
onOpenMedia: PropTypes.func.isRequired,
|
onOpenMedia: PropTypes.func.isRequired,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
|
defaultWidth: PropTypes.number,
|
||||||
|
cacheWidth: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
|
@ -202,6 +204,7 @@ class MediaGallery extends React.PureComponent {
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
visible: displayMedia !== 'hide_all' && !this.props.sensitive || displayMedia === 'show_all',
|
visible: displayMedia !== 'hide_all' && !this.props.sensitive || displayMedia === 'show_all',
|
||||||
|
width: this.props.defaultWidth,
|
||||||
};
|
};
|
||||||
|
|
||||||
componentWillReceiveProps (nextProps) {
|
componentWillReceiveProps (nextProps) {
|
||||||
|
@ -221,6 +224,7 @@ class MediaGallery extends React.PureComponent {
|
||||||
handleRef = (node) => {
|
handleRef = (node) => {
|
||||||
if (node /*&& this.isStandaloneEligible()*/) {
|
if (node /*&& this.isStandaloneEligible()*/) {
|
||||||
// offsetWidth triggers a layout, so only calculate when we need to
|
// offsetWidth triggers a layout, so only calculate when we need to
|
||||||
|
if (this.props.cacheWidth) this.props.cacheWidth(node.offsetWidth);
|
||||||
this.setState({
|
this.setState({
|
||||||
width: node.offsetWidth,
|
width: node.offsetWidth,
|
||||||
});
|
});
|
||||||
|
@ -233,8 +237,10 @@ class MediaGallery extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { media, intl, sensitive, height } = this.props;
|
const { media, intl, sensitive, height, defaultWidth } = this.props;
|
||||||
const { width, visible } = this.state;
|
const { visible } = this.state;
|
||||||
|
|
||||||
|
const width = this.state.width || defaultWidth;
|
||||||
|
|
||||||
let children;
|
let children;
|
||||||
|
|
||||||
|
|
|
@ -40,6 +40,7 @@ export default class ScrollableList extends PureComponent {
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
fullscreen: null,
|
fullscreen: null,
|
||||||
|
cachedMediaWidth: 250, // Default media/card width using default Mastodon theme
|
||||||
};
|
};
|
||||||
|
|
||||||
intersectionObserverWrapper = new IntersectionObserverWrapper();
|
intersectionObserverWrapper = new IntersectionObserverWrapper();
|
||||||
|
@ -130,6 +131,20 @@ export default class ScrollableList extends PureComponent {
|
||||||
this.handleScroll();
|
this.handleScroll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getScrollPosition = () => {
|
||||||
|
if (this.node && (this.node.scrollTop > 0 || this.mouseMovedRecently)) {
|
||||||
|
return { height: this.node.scrollHeight, top: this.node.scrollTop };
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateScrollBottom = (snapshot) => {
|
||||||
|
const newScrollTop = this.node.scrollHeight - snapshot;
|
||||||
|
|
||||||
|
this.setScrollTop(newScrollTop);
|
||||||
|
}
|
||||||
|
|
||||||
getSnapshotBeforeUpdate (prevProps) {
|
getSnapshotBeforeUpdate (prevProps) {
|
||||||
const someItemInserted = React.Children.count(prevProps.children) > 0 &&
|
const someItemInserted = React.Children.count(prevProps.children) > 0 &&
|
||||||
React.Children.count(prevProps.children) < React.Children.count(this.props.children) &&
|
React.Children.count(prevProps.children) < React.Children.count(this.props.children) &&
|
||||||
|
@ -150,6 +165,12 @@ export default class ScrollableList extends PureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cacheMediaWidth = (width) => {
|
||||||
|
if (width && this.state.cachedMediaWidth !== width) {
|
||||||
|
this.setState({ cachedMediaWidth: width });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
componentWillUnmount () {
|
componentWillUnmount () {
|
||||||
this.clearMouseIdleTimer();
|
this.clearMouseIdleTimer();
|
||||||
this.detachScrollListener();
|
this.detachScrollListener();
|
||||||
|
@ -239,7 +260,12 @@ export default class ScrollableList extends PureComponent {
|
||||||
intersectionObserverWrapper={this.intersectionObserverWrapper}
|
intersectionObserverWrapper={this.intersectionObserverWrapper}
|
||||||
saveHeightKey={trackScroll ? `${this.context.router.route.location.key}:${scrollKey}` : null}
|
saveHeightKey={trackScroll ? `${this.context.router.route.location.key}:${scrollKey}` : null}
|
||||||
>
|
>
|
||||||
{child}
|
{React.cloneElement(child, {
|
||||||
|
getScrollPosition: this.getScrollPosition,
|
||||||
|
updateScrollBottom: this.updateScrollBottom,
|
||||||
|
cachedMediaWidth: this.state.cachedMediaWidth,
|
||||||
|
cacheMediaWidth: this.cacheMediaWidth,
|
||||||
|
})}
|
||||||
</IntersectionObserverArticleContainer>
|
</IntersectionObserverArticleContainer>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
|
|
@ -68,6 +68,10 @@ class Status extends ImmutablePureComponent {
|
||||||
onMoveUp: PropTypes.func,
|
onMoveUp: PropTypes.func,
|
||||||
onMoveDown: PropTypes.func,
|
onMoveDown: PropTypes.func,
|
||||||
showThread: PropTypes.bool,
|
showThread: PropTypes.bool,
|
||||||
|
getScrollPosition: PropTypes.func,
|
||||||
|
updateScrollBottom: PropTypes.func,
|
||||||
|
cacheMediaWidth: PropTypes.func,
|
||||||
|
cachedMediaWidth: PropTypes.number,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Avoid checking props that are functions (and whose equality will always
|
// Avoid checking props that are functions (and whose equality will always
|
||||||
|
@ -79,6 +83,43 @@ class Status extends ImmutablePureComponent {
|
||||||
'hidden',
|
'hidden',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Track height changes we know about to compensate scrolling
|
||||||
|
componentDidMount () {
|
||||||
|
this.didShowCard = !this.props.muted && !this.props.hidden && this.props.status && this.props.status.get('card');
|
||||||
|
}
|
||||||
|
|
||||||
|
getSnapshotBeforeUpdate () {
|
||||||
|
if (this.props.getScrollPosition) {
|
||||||
|
return this.props.getScrollPosition();
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compensate height changes
|
||||||
|
componentDidUpdate (prevProps, prevState, snapshot) {
|
||||||
|
const doShowCard = !this.props.muted && !this.props.hidden && this.props.status && this.props.status.get('card');
|
||||||
|
if (doShowCard && !this.didShowCard) {
|
||||||
|
this.didShowCard = true;
|
||||||
|
if (snapshot !== null && this.props.updateScrollBottom) {
|
||||||
|
if (this.node && this.node.offsetTop < snapshot.top) {
|
||||||
|
this.props.updateScrollBottom(snapshot.height - snapshot.top);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
if (this.node && this.props.getScrollPosition) {
|
||||||
|
const position = this.props.getScrollPosition();
|
||||||
|
if (position !== null && this.node.offsetTop < position.top) {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
this.props.updateScrollBottom(position.height - position.top);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handleClick = () => {
|
handleClick = () => {
|
||||||
if (this.props.onClick) {
|
if (this.props.onClick) {
|
||||||
this.props.onClick();
|
this.props.onClick();
|
||||||
|
@ -165,6 +206,10 @@ class Status extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleRef = c => {
|
||||||
|
this.node = c;
|
||||||
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
let media = null;
|
let media = null;
|
||||||
let statusAvatar, prepend, rebloggedByText;
|
let statusAvatar, prepend, rebloggedByText;
|
||||||
|
@ -179,7 +224,7 @@ class Status extends ImmutablePureComponent {
|
||||||
|
|
||||||
if (hidden) {
|
if (hidden) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div ref={this.handleRef}>
|
||||||
{status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])}
|
{status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])}
|
||||||
{status.get('content')}
|
{status.get('content')}
|
||||||
</div>
|
</div>
|
||||||
|
@ -194,7 +239,7 @@ class Status extends ImmutablePureComponent {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HotKeys handlers={minHandlers}>
|
<HotKeys handlers={minHandlers}>
|
||||||
<div className='status__wrapper status__wrapper--filtered focusable' tabIndex='0'>
|
<div className='status__wrapper status__wrapper--filtered focusable' tabIndex='0' ref={this.handleRef}>
|
||||||
<FormattedMessage id='status.filtered' defaultMessage='Filtered' />
|
<FormattedMessage id='status.filtered' defaultMessage='Filtered' />
|
||||||
</div>
|
</div>
|
||||||
</HotKeys>
|
</HotKeys>
|
||||||
|
@ -242,11 +287,12 @@ class Status extends ImmutablePureComponent {
|
||||||
preview={video.get('preview_url')}
|
preview={video.get('preview_url')}
|
||||||
src={video.get('url')}
|
src={video.get('url')}
|
||||||
alt={video.get('description')}
|
alt={video.get('description')}
|
||||||
width={239}
|
width={this.props.cachedMediaWidth}
|
||||||
height={110}
|
height={110}
|
||||||
inline
|
inline
|
||||||
sensitive={status.get('sensitive')}
|
sensitive={status.get('sensitive')}
|
||||||
onOpenVideo={this.handleOpenVideo}
|
onOpenVideo={this.handleOpenVideo}
|
||||||
|
cacheWidth={this.props.cacheMediaWidth}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Bundle>
|
</Bundle>
|
||||||
|
@ -254,7 +300,16 @@ class Status extends ImmutablePureComponent {
|
||||||
} else {
|
} else {
|
||||||
media = (
|
media = (
|
||||||
<Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery}>
|
<Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery}>
|
||||||
{Component => <Component media={status.get('media_attachments')} sensitive={status.get('sensitive')} height={110} onOpenMedia={this.props.onOpenMedia} />}
|
{Component => (
|
||||||
|
<Component
|
||||||
|
media={status.get('media_attachments')}
|
||||||
|
sensitive={status.get('sensitive')}
|
||||||
|
height={110}
|
||||||
|
onOpenMedia={this.props.onOpenMedia}
|
||||||
|
cacheWidth={this.props.cacheMediaWidth}
|
||||||
|
defaultWidth={this.props.cachedMediaWidth}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Bundle>
|
</Bundle>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -264,11 +319,13 @@ class Status extends ImmutablePureComponent {
|
||||||
onOpenMedia={this.props.onOpenMedia}
|
onOpenMedia={this.props.onOpenMedia}
|
||||||
card={status.get('card')}
|
card={status.get('card')}
|
||||||
compact
|
compact
|
||||||
|
cacheWidth={this.props.cacheMediaWidth}
|
||||||
|
defaultWidth={this.props.cachedMediaWidth}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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} />;
|
||||||
|
@ -290,7 +347,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'))}>
|
<div className={classNames('status__wrapper', `status__wrapper-${status.get('visibility')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id'), read: unread === false, focusable: !this.props.muted })} tabIndex={this.props.muted ? null : 0} data-featured={featured ? 'true' : null} aria-label={textForScreenReader(intl, status, rebloggedByText, !status.get('hidden'))} ref={this.handleRef}>
|
||||||
{prepend}
|
{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')}>
|
||||||
|
|
|
@ -77,7 +77,11 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
]
|
]
|
||||||
|
|
||||||
handleReplyClick = () => {
|
handleReplyClick = () => {
|
||||||
|
if (me) {
|
||||||
this.props.onReply(this.props.status, this.context.router.history);
|
this.props.onReply(this.props.status, this.context.router.history);
|
||||||
|
} else {
|
||||||
|
this._openInteractionDialog('reply');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleShareClick = () => {
|
handleShareClick = () => {
|
||||||
|
@ -90,11 +94,23 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleFavouriteClick = () => {
|
handleFavouriteClick = () => {
|
||||||
|
if (me) {
|
||||||
this.props.onFavourite(this.props.status);
|
this.props.onFavourite(this.props.status);
|
||||||
|
} else {
|
||||||
|
this._openInteractionDialog('favourite');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleReblogClick = (e) => {
|
handleReblogClick = e => {
|
||||||
|
if (me) {
|
||||||
this.props.onReblog(this.props.status, e);
|
this.props.onReblog(this.props.status, e);
|
||||||
|
} else {
|
||||||
|
this._openInteractionDialog('reblog');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_openInteractionDialog = type => {
|
||||||
|
window.open(`/interact/${this.props.status.get('id')}?type=${type}`, 'mastodon-intent', 'width=445,height=600,resizable=no,menubar=no,status=no,scrollbars=yes');
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDeleteClick = () => {
|
handleDeleteClick = () => {
|
||||||
|
@ -211,9 +227,9 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='status__action-bar'>
|
<div className='status__action-bar'>
|
||||||
<IconButton className='status__action-bar-button' disabled={anonymousAccess} title={replyTitle} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} />
|
<IconButton className='status__action-bar-button' title={replyTitle} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} />
|
||||||
<IconButton className='status__action-bar-button' disabled={anonymousAccess || !publicStatus} active={status.get('reblogged')} pressed={status.get('reblogged')} title={!publicStatus ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} />
|
<IconButton className='status__action-bar-button' disabled={!publicStatus} active={status.get('reblogged')} pressed={status.get('reblogged')} title={!publicStatus ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} />
|
||||||
<IconButton className='status__action-bar-button star-icon' disabled={anonymousAccess} animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='floppy-o' onClick={this.handleFavouriteClick} />
|
<IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='floppy-o' onClick={this.handleFavouriteClick} />
|
||||||
{shareButton}
|
{shareButton}
|
||||||
|
|
||||||
<div className='status__action-bar-dropdown'>
|
<div className='status__action-bar-dropdown'>
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { IntlProvider, addLocaleData } from 'react-intl';
|
||||||
import { getLocale } from '../locales';
|
import { getLocale } from '../locales';
|
||||||
import Compose from '../features/standalone/compose';
|
import Compose from '../features/standalone/compose';
|
||||||
import initialState from '../initial_state';
|
import initialState from '../initial_state';
|
||||||
|
import { fetchCustomEmojis } from '../actions/custom_emojis';
|
||||||
|
|
||||||
const { localeData, messages } = getLocale();
|
const { localeData, messages } = getLocale();
|
||||||
addLocaleData(localeData);
|
addLocaleData(localeData);
|
||||||
|
@ -17,6 +18,8 @@ if (initialState) {
|
||||||
store.dispatch(hydrateStore(initialState));
|
store.dispatch(hydrateStore(initialState));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
store.dispatch(fetchCustomEmojis());
|
||||||
|
|
||||||
export default class TimelineContainer extends React.PureComponent {
|
export default class TimelineContainer extends React.PureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
|
|
@ -132,7 +132,7 @@ class Header extends ImmutablePureComponent {
|
||||||
} else if (account.getIn(['relationship', 'blocking'])) {
|
} else if (account.getIn(['relationship', 'blocking'])) {
|
||||||
actionBtn = (
|
actionBtn = (
|
||||||
<div className='account--action-button'>
|
<div className='account--action-button'>
|
||||||
<IconButton size={26} icon='unlock-alt' title={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.props.onBlock} />
|
<IconButton size={26} icon='unlock' title={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.props.onBlock} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ const messages = defineMessages({
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
accountIds: state.getIn(['user_lists', 'blocks', 'items']),
|
accountIds: state.getIn(['user_lists', 'blocks', 'items']),
|
||||||
|
hasMore: !!state.getIn(['user_lists', 'blocks', 'next']),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default @connect(mapStateToProps)
|
export default @connect(mapStateToProps)
|
||||||
|
@ -29,6 +30,7 @@ class Blocks extends ImmutablePureComponent {
|
||||||
dispatch: PropTypes.func.isRequired,
|
dispatch: PropTypes.func.isRequired,
|
||||||
shouldUpdateScroll: PropTypes.func,
|
shouldUpdateScroll: PropTypes.func,
|
||||||
accountIds: ImmutablePropTypes.list,
|
accountIds: ImmutablePropTypes.list,
|
||||||
|
hasMore: PropTypes.bool,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -41,7 +43,7 @@ class Blocks extends ImmutablePureComponent {
|
||||||
}, 300, { leading: true });
|
}, 300, { leading: true });
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { intl, accountIds, shouldUpdateScroll } = this.props;
|
const { intl, accountIds, shouldUpdateScroll, hasMore } = this.props;
|
||||||
|
|
||||||
if (!accountIds) {
|
if (!accountIds) {
|
||||||
return (
|
return (
|
||||||
|
@ -59,6 +61,7 @@ class Blocks extends ImmutablePureComponent {
|
||||||
<ScrollableList
|
<ScrollableList
|
||||||
scrollKey='blocks'
|
scrollKey='blocks'
|
||||||
onLoadMore={this.handleLoadMore}
|
onLoadMore={this.handleLoadMore}
|
||||||
|
hasMore={hasMore}
|
||||||
shouldUpdateScroll={shouldUpdateScroll}
|
shouldUpdateScroll={shouldUpdateScroll}
|
||||||
emptyMessage={emptyMessage}
|
emptyMessage={emptyMessage}
|
||||||
>
|
>
|
||||||
|
|
|
@ -179,7 +179,7 @@ class ComposeForm extends ImmutablePureComponent {
|
||||||
<div className={`spoiler-input ${this.props.spoiler ? 'spoiler-input--visible' : ''}`}>
|
<div className={`spoiler-input ${this.props.spoiler ? 'spoiler-input--visible' : ''}`}>
|
||||||
<label>
|
<label>
|
||||||
<span style={{ display: 'none' }}>{intl.formatMessage(messages.spoiler_placeholder)}</span>
|
<span style={{ display: 'none' }}>{intl.formatMessage(messages.spoiler_placeholder)}</span>
|
||||||
<input placeholder={intl.formatMessage(messages.spoiler_placeholder)} value={this.props.spoiler_text} onChange={this.handleChangeSpoilerText} onKeyDown={this.handleKeyDown} type='text' className='spoiler-input__input' id='cw-spoiler-input' ref={this.setSpoilerText} />
|
<input placeholder={intl.formatMessage(messages.spoiler_placeholder)} value={this.props.spoiler_text} onChange={this.handleChangeSpoilerText} onKeyDown={this.handleKeyDown} tabIndex={this.props.spoiler ? 0 : -1} type='text' className='spoiler-input__input' id='cw-spoiler-input' ref={this.setSpoilerText} />
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -214,7 +214,7 @@ class PrivacyDropdown extends React.PureComponent {
|
||||||
|
|
||||||
this.options = [
|
this.options = [
|
||||||
{ icon: 'globe', value: 'public', text: formatMessage(messages.public_short), meta: formatMessage(messages.public_long) },
|
{ icon: 'globe', value: 'public', text: formatMessage(messages.public_short), meta: formatMessage(messages.public_long) },
|
||||||
{ icon: 'unlock-alt', value: 'unlisted', text: formatMessage(messages.unlisted_short), meta: formatMessage(messages.unlisted_long) },
|
{ icon: 'unlock', value: 'unlisted', text: formatMessage(messages.unlisted_short), meta: formatMessage(messages.unlisted_long) },
|
||||||
{ icon: 'lock', value: 'private', text: formatMessage(messages.private_short), meta: formatMessage(messages.private_long) },
|
{ icon: 'lock', value: 'private', text: formatMessage(messages.private_short), meta: formatMessage(messages.private_long) },
|
||||||
{ icon: 'envelope', value: 'direct', text: formatMessage(messages.direct_short), meta: formatMessage(messages.direct_long) },
|
{ icon: 'envelope', value: 'direct', text: formatMessage(messages.direct_short), meta: formatMessage(messages.direct_long) },
|
||||||
];
|
];
|
||||||
|
|
|
@ -107,9 +107,8 @@ class Upload extends ImmutablePureComponent {
|
||||||
<label>
|
<label>
|
||||||
<span style={{ display: 'none' }}>{intl.formatMessage(messages.description)}</span>
|
<span style={{ display: 'none' }}>{intl.formatMessage(messages.description)}</span>
|
||||||
|
|
||||||
<input
|
<textarea
|
||||||
placeholder={intl.formatMessage(messages.description)}
|
placeholder={intl.formatMessage(messages.description)}
|
||||||
type='text'
|
|
||||||
value={description}
|
value={description}
|
||||||
maxLength={420}
|
maxLength={420}
|
||||||
onFocus={this.handleInputFocus}
|
onFocus={this.handleInputFocus}
|
||||||
|
|
|
@ -19,6 +19,7 @@ const messages = defineMessages({
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
domains: state.getIn(['domain_lists', 'blocks', 'items']),
|
domains: state.getIn(['domain_lists', 'blocks', 'items']),
|
||||||
|
hasMore: !!state.getIn(['domain_lists', 'blocks', 'next']),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default @connect(mapStateToProps)
|
export default @connect(mapStateToProps)
|
||||||
|
@ -29,6 +30,7 @@ class Blocks extends ImmutablePureComponent {
|
||||||
params: PropTypes.object.isRequired,
|
params: PropTypes.object.isRequired,
|
||||||
dispatch: PropTypes.func.isRequired,
|
dispatch: PropTypes.func.isRequired,
|
||||||
shouldUpdateScroll: PropTypes.func,
|
shouldUpdateScroll: PropTypes.func,
|
||||||
|
hasMore: PropTypes.bool,
|
||||||
domains: ImmutablePropTypes.orderedSet,
|
domains: ImmutablePropTypes.orderedSet,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
@ -42,7 +44,7 @@ class Blocks extends ImmutablePureComponent {
|
||||||
}, 300, { leading: true });
|
}, 300, { leading: true });
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { intl, domains, shouldUpdateScroll } = this.props;
|
const { intl, domains, shouldUpdateScroll, hasMore } = this.props;
|
||||||
|
|
||||||
if (!domains) {
|
if (!domains) {
|
||||||
return (
|
return (
|
||||||
|
@ -60,6 +62,7 @@ class Blocks extends ImmutablePureComponent {
|
||||||
<ScrollableList
|
<ScrollableList
|
||||||
scrollKey='domain_blocks'
|
scrollKey='domain_blocks'
|
||||||
onLoadMore={this.handleLoadMore}
|
onLoadMore={this.handleLoadMore}
|
||||||
|
hasMore={hasMore}
|
||||||
shouldUpdateScroll={shouldUpdateScroll}
|
shouldUpdateScroll={shouldUpdateScroll}
|
||||||
emptyMessage={emptyMessage}
|
emptyMessage={emptyMessage}
|
||||||
>
|
>
|
||||||
|
|
|
@ -18,6 +18,7 @@ const messages = defineMessages({
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
accountIds: state.getIn(['user_lists', 'follow_requests', 'items']),
|
accountIds: state.getIn(['user_lists', 'follow_requests', 'items']),
|
||||||
|
hasMore: !!state.getIn(['user_lists', 'follow_requests', 'next']),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default @connect(mapStateToProps)
|
export default @connect(mapStateToProps)
|
||||||
|
@ -28,6 +29,7 @@ class FollowRequests extends ImmutablePureComponent {
|
||||||
params: PropTypes.object.isRequired,
|
params: PropTypes.object.isRequired,
|
||||||
dispatch: PropTypes.func.isRequired,
|
dispatch: PropTypes.func.isRequired,
|
||||||
shouldUpdateScroll: PropTypes.func,
|
shouldUpdateScroll: PropTypes.func,
|
||||||
|
hasMore: PropTypes.bool,
|
||||||
accountIds: ImmutablePropTypes.list,
|
accountIds: ImmutablePropTypes.list,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
@ -41,7 +43,7 @@ class FollowRequests extends ImmutablePureComponent {
|
||||||
}, 300, { leading: true });
|
}, 300, { leading: true });
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { intl, shouldUpdateScroll, accountIds } = this.props;
|
const { intl, shouldUpdateScroll, accountIds, hasMore } = this.props;
|
||||||
|
|
||||||
if (!accountIds) {
|
if (!accountIds) {
|
||||||
return (
|
return (
|
||||||
|
@ -59,6 +61,7 @@ class FollowRequests extends ImmutablePureComponent {
|
||||||
<ScrollableList
|
<ScrollableList
|
||||||
scrollKey='follow_requests'
|
scrollKey='follow_requests'
|
||||||
onLoadMore={this.handleLoadMore}
|
onLoadMore={this.handleLoadMore}
|
||||||
|
hasMore={hasMore}
|
||||||
shouldUpdateScroll={shouldUpdateScroll}
|
shouldUpdateScroll={shouldUpdateScroll}
|
||||||
emptyMessage={emptyMessage}
|
emptyMessage={emptyMessage}
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,10 +1,15 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import { injectIntl, FormattedMessage } from 'react-intl';
|
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
import Toggle from 'react-toggle';
|
import Toggle from 'react-toggle';
|
||||||
import AsyncSelect from 'react-select/lib/Async';
|
import AsyncSelect from 'react-select/lib/Async';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
placeholder: { id: 'hashtag.column_settings.select.placeholder', defaultMessage: 'Enter hashtags…' },
|
||||||
|
noOptions: { id: 'hashtag.column_settings.select.no_options_message', defaultMessage: 'No suggestions found' },
|
||||||
|
});
|
||||||
|
|
||||||
export default @injectIntl
|
export default @injectIntl
|
||||||
class ColumnSettings extends React.PureComponent {
|
class ColumnSettings extends React.PureComponent {
|
||||||
|
|
||||||
|
@ -25,6 +30,7 @@ class ColumnSettings extends React.PureComponent {
|
||||||
|
|
||||||
tags (mode) {
|
tags (mode) {
|
||||||
let tags = this.props.settings.getIn(['tags', mode]) || [];
|
let tags = this.props.settings.getIn(['tags', mode]) || [];
|
||||||
|
|
||||||
if (tags.toJSON) {
|
if (tags.toJSON) {
|
||||||
return tags.toJSON();
|
return tags.toJSON();
|
||||||
} else {
|
} else {
|
||||||
|
@ -32,33 +38,36 @@ class ColumnSettings extends React.PureComponent {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onSelect = (mode) => {
|
onSelect = mode => value => this.props.onChange(['tags', mode], value);
|
||||||
return (value) => {
|
|
||||||
this.props.onChange(['tags', mode], value);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
onToggle = () => {
|
onToggle = () => {
|
||||||
if (this.state.open && this.hasTags()) {
|
if (this.state.open && this.hasTags()) {
|
||||||
this.props.onChange('tags', {});
|
this.props.onChange('tags', {});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({ open: !this.state.open });
|
this.setState({ open: !this.state.open });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
noOptionsMessage = () => this.props.intl.formatMessage(messages.noOptions);
|
||||||
|
|
||||||
modeSelect (mode) {
|
modeSelect (mode) {
|
||||||
return (
|
return (
|
||||||
<div className='column-settings__section'>
|
<div className='column-settings__row'>
|
||||||
|
<span className='column-settings__section'>
|
||||||
{this.modeLabel(mode)}
|
{this.modeLabel(mode)}
|
||||||
|
</span>
|
||||||
|
|
||||||
<AsyncSelect
|
<AsyncSelect
|
||||||
isMulti
|
isMulti
|
||||||
autoFocus
|
autoFocus
|
||||||
value={this.tags(mode)}
|
value={this.tags(mode)}
|
||||||
settings={this.props.settings}
|
|
||||||
settingPath={['tags', mode]}
|
|
||||||
onChange={this.onSelect(mode)}
|
onChange={this.onSelect(mode)}
|
||||||
loadOptions={this.props.onLoad}
|
loadOptions={this.props.onLoad}
|
||||||
classNamePrefix='column-settings__hashtag-select'
|
className='column-select__container'
|
||||||
|
classNamePrefix='column-select'
|
||||||
name='tags'
|
name='tags'
|
||||||
|
placeholder={this.props.intl.formatMessage(messages.placeholder)}
|
||||||
|
noOptionsMessage={this.noOptionsMessage}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -66,11 +75,15 @@ class ColumnSettings extends React.PureComponent {
|
||||||
|
|
||||||
modeLabel (mode) {
|
modeLabel (mode) {
|
||||||
switch(mode) {
|
switch(mode) {
|
||||||
case 'any': return <FormattedMessage id='hashtag.column_settings.tag_mode.any' defaultMessage='Any of these' />;
|
case 'any':
|
||||||
case 'all': return <FormattedMessage id='hashtag.column_settings.tag_mode.all' defaultMessage='All of these' />;
|
return <FormattedMessage id='hashtag.column_settings.tag_mode.any' defaultMessage='Any of these' />;
|
||||||
case 'none': return <FormattedMessage id='hashtag.column_settings.tag_mode.none' defaultMessage='None of these' />;
|
case 'all':
|
||||||
}
|
return <FormattedMessage id='hashtag.column_settings.tag_mode.all' defaultMessage='All of these' />;
|
||||||
|
case 'none':
|
||||||
|
return <FormattedMessage id='hashtag.column_settings.tag_mode.none' defaultMessage='None of these' />;
|
||||||
|
default:
|
||||||
return '';
|
return '';
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
|
@ -78,23 +91,21 @@ class ColumnSettings extends React.PureComponent {
|
||||||
<div>
|
<div>
|
||||||
<div className='column-settings__row'>
|
<div className='column-settings__row'>
|
||||||
<div className='setting-toggle'>
|
<div className='setting-toggle'>
|
||||||
<Toggle
|
<Toggle id='hashtag.column_settings.tag_toggle' onChange={this.onToggle} checked={this.state.open} />
|
||||||
id='hashtag.column_settings.tag_toggle'
|
|
||||||
onChange={this.onToggle}
|
|
||||||
checked={this.state.open}
|
|
||||||
/>
|
|
||||||
<span className='setting-toggle__label'>
|
<span className='setting-toggle__label'>
|
||||||
<FormattedMessage id='hashtag.column_settings.tag_toggle' defaultMessage='Include additional tags in this column' />
|
<FormattedMessage id='hashtag.column_settings.tag_toggle' defaultMessage='Include additional tags in this column' />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{this.state.open &&
|
|
||||||
|
{this.state.open && (
|
||||||
<div className='column-settings__hashtags'>
|
<div className='column-settings__hashtags'>
|
||||||
{this.modeSelect('any')}
|
{this.modeSelect('any')}
|
||||||
{this.modeSelect('all')}
|
{this.modeSelect('all')}
|
||||||
{this.modeSelect('none')}
|
{this.modeSelect('none')}
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,15 +41,19 @@ class HashtagTimeline extends React.PureComponent {
|
||||||
|
|
||||||
title = () => {
|
title = () => {
|
||||||
let title = [this.props.params.id];
|
let title = [this.props.params.id];
|
||||||
|
|
||||||
if (this.additionalFor('any')) {
|
if (this.additionalFor('any')) {
|
||||||
title.push(' ', <FormattedMessage id='hashtag.column_header.tag_mode.any' values={{ additional: this.additionalFor('any') }} defaultMessage='or {additional}' />);
|
title.push(' ', <FormattedMessage key='any' id='hashtag.column_header.tag_mode.any' values={{ additional: this.additionalFor('any') }} defaultMessage='or {additional}' />);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.additionalFor('all')) {
|
if (this.additionalFor('all')) {
|
||||||
title.push(' ', <FormattedMessage id='hashtag.column_header.tag_mode.all' values={{ additional: this.additionalFor('all') }} defaultMessage='and {additional}' />);
|
title.push(' ', <FormattedMessage key='all' id='hashtag.column_header.tag_mode.all' values={{ additional: this.additionalFor('all') }} defaultMessage='and {additional}' />);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.additionalFor('none')) {
|
if (this.additionalFor('none')) {
|
||||||
title.push(' ', <FormattedMessage id='hashtag.column_header.tag_mode.none' values={{ additional: this.additionalFor('none') }} defaultMessage='without {additional}' />);
|
title.push(' ', <FormattedMessage key='none' id='hashtag.column_header.tag_mode.none' values={{ additional: this.additionalFor('none') }} defaultMessage='without {additional}' />);
|
||||||
}
|
}
|
||||||
|
|
||||||
return title;
|
return title;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,9 +81,10 @@ class HashtagTimeline extends React.PureComponent {
|
||||||
let all = (tags.all || []).map(tag => tag.value);
|
let all = (tags.all || []).map(tag => tag.value);
|
||||||
let none = (tags.none || []).map(tag => tag.value);
|
let none = (tags.none || []).map(tag => tag.value);
|
||||||
|
|
||||||
[id, ...any].map((tag) => {
|
[id, ...any].map(tag => {
|
||||||
this.disconnects.push(dispatch(connectHashtagStream(id, tag, (status) => {
|
this.disconnects.push(dispatch(connectHashtagStream(id, tag, status => {
|
||||||
let tags = status.tags.map(tag => tag.name);
|
let tags = status.tags.map(tag => tag.name);
|
||||||
|
|
||||||
return all.filter(tag => tags.includes(tag)).length === all.length &&
|
return all.filter(tag => tags.includes(tag)).length === all.length &&
|
||||||
none.filter(tag => tags.includes(tag)).length === 0;
|
none.filter(tag => tags.includes(tag)).length === 0;
|
||||||
})));
|
})));
|
||||||
|
@ -95,12 +100,14 @@ class HashtagTimeline extends React.PureComponent {
|
||||||
const { dispatch } = this.props;
|
const { dispatch } = this.props;
|
||||||
const { id, tags } = this.props.params;
|
const { id, tags } = this.props.params;
|
||||||
|
|
||||||
|
this._subscribe(dispatch, id, tags);
|
||||||
dispatch(expandHashtagTimeline(id, { tags }));
|
dispatch(expandHashtagTimeline(id, { tags }));
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps (nextProps) {
|
componentWillReceiveProps (nextProps) {
|
||||||
const { dispatch, params } = this.props;
|
const { dispatch, params } = this.props;
|
||||||
const { id, tags } = nextProps.params;
|
const { id, tags } = nextProps.params;
|
||||||
|
|
||||||
if (id !== params.id || !isEqual(tags, params.tags)) {
|
if (id !== params.id || !isEqual(tags, params.tags)) {
|
||||||
this._unsubscribe();
|
this._unsubscribe();
|
||||||
this._subscribe(dispatch, id, tags);
|
this._subscribe(dispatch, id, tags);
|
||||||
|
|
|
@ -18,6 +18,7 @@ const messages = defineMessages({
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
accountIds: state.getIn(['user_lists', 'mutes', 'items']),
|
accountIds: state.getIn(['user_lists', 'mutes', 'items']),
|
||||||
|
hasMore: !!state.getIn(['user_lists', 'mutes', 'next']),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default @connect(mapStateToProps)
|
export default @connect(mapStateToProps)
|
||||||
|
@ -28,6 +29,7 @@ class Mutes extends ImmutablePureComponent {
|
||||||
params: PropTypes.object.isRequired,
|
params: PropTypes.object.isRequired,
|
||||||
dispatch: PropTypes.func.isRequired,
|
dispatch: PropTypes.func.isRequired,
|
||||||
shouldUpdateScroll: PropTypes.func,
|
shouldUpdateScroll: PropTypes.func,
|
||||||
|
hasMore: PropTypes.bool,
|
||||||
accountIds: ImmutablePropTypes.list,
|
accountIds: ImmutablePropTypes.list,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
@ -41,7 +43,7 @@ class Mutes extends ImmutablePureComponent {
|
||||||
}, 300, { leading: true });
|
}, 300, { leading: true });
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { intl, shouldUpdateScroll, accountIds } = this.props;
|
const { intl, shouldUpdateScroll, hasMore, accountIds } = this.props;
|
||||||
|
|
||||||
if (!accountIds) {
|
if (!accountIds) {
|
||||||
return (
|
return (
|
||||||
|
@ -59,6 +61,7 @@ class Mutes extends ImmutablePureComponent {
|
||||||
<ScrollableList
|
<ScrollableList
|
||||||
scrollKey='mutes'
|
scrollKey='mutes'
|
||||||
onLoadMore={this.handleLoadMore}
|
onLoadMore={this.handleLoadMore}
|
||||||
|
hasMore={hasMore}
|
||||||
shouldUpdateScroll={shouldUpdateScroll}
|
shouldUpdateScroll={shouldUpdateScroll}
|
||||||
emptyMessage={emptyMessage}
|
emptyMessage={emptyMessage}
|
||||||
>
|
>
|
||||||
|
|
|
@ -34,6 +34,10 @@ class Notification extends ImmutablePureComponent {
|
||||||
onToggleHidden: PropTypes.func.isRequired,
|
onToggleHidden: PropTypes.func.isRequired,
|
||||||
status: PropTypes.option,
|
status: PropTypes.option,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
|
getScrollPosition: PropTypes.func,
|
||||||
|
updateScrollBottom: PropTypes.func,
|
||||||
|
cacheMediaWidth: PropTypes.func,
|
||||||
|
cachedMediaWidth: PropTypes.number,
|
||||||
};
|
};
|
||||||
|
|
||||||
handleMoveUp = () => {
|
handleMoveUp = () => {
|
||||||
|
@ -128,6 +132,10 @@ class Notification extends ImmutablePureComponent {
|
||||||
onMoveDown={this.handleMoveDown}
|
onMoveDown={this.handleMoveDown}
|
||||||
onMoveUp={this.handleMoveUp}
|
onMoveUp={this.handleMoveUp}
|
||||||
contextType='notifications'
|
contextType='notifications'
|
||||||
|
getScrollPosition={this.props.getScrollPosition}
|
||||||
|
updateScrollBottom={this.props.updateScrollBottom}
|
||||||
|
cachedMediaWidth={this.props.cachedMediaWidth}
|
||||||
|
cacheMediaWidth={this.props.cacheMediaWidth}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -148,7 +156,17 @@ class Notification extends ImmutablePureComponent {
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<StatusContainer id={notification.get('status')} account={notification.get('account')} muted withDismiss hidden={!!this.props.hidden} />
|
<StatusContainer
|
||||||
|
id={notification.get('status')}
|
||||||
|
account={notification.get('account')}
|
||||||
|
muted
|
||||||
|
withDismiss
|
||||||
|
hidden={!!this.props.hidden}
|
||||||
|
getScrollPosition={this.props.getScrollPosition}
|
||||||
|
updateScrollBottom={this.props.updateScrollBottom}
|
||||||
|
cachedMediaWidth={this.props.cachedMediaWidth}
|
||||||
|
cacheMediaWidth={this.props.cacheMediaWidth}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</HotKeys>
|
</HotKeys>
|
||||||
);
|
);
|
||||||
|
@ -170,7 +188,17 @@ class Notification extends ImmutablePureComponent {
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<StatusContainer id={notification.get('status')} account={notification.get('account')} muted withDismiss hidden={this.props.hidden} />
|
<StatusContainer
|
||||||
|
id={notification.get('status')}
|
||||||
|
account={notification.get('account')}
|
||||||
|
muted
|
||||||
|
withDismiss
|
||||||
|
hidden={this.props.hidden}
|
||||||
|
getScrollPosition={this.props.getScrollPosition}
|
||||||
|
updateScrollBottom={this.props.updateScrollBottom}
|
||||||
|
cachedMediaWidth={this.props.cachedMediaWidth}
|
||||||
|
cacheMediaWidth={this.props.cacheMediaWidth}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</HotKeys>
|
</HotKeys>
|
||||||
);
|
);
|
||||||
|
|
|
@ -60,6 +60,8 @@ export default class Card extends React.PureComponent {
|
||||||
maxDescription: PropTypes.number,
|
maxDescription: PropTypes.number,
|
||||||
onOpenMedia: PropTypes.func.isRequired,
|
onOpenMedia: PropTypes.func.isRequired,
|
||||||
compact: PropTypes.bool,
|
compact: PropTypes.bool,
|
||||||
|
defaultWidth: PropTypes.number,
|
||||||
|
cacheWidth: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
|
@ -68,7 +70,7 @@ export default class Card extends React.PureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
width: 280,
|
width: this.props.defaultWidth || 280,
|
||||||
embedded: false,
|
embedded: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -111,6 +113,7 @@ export default class Card extends React.PureComponent {
|
||||||
|
|
||||||
setRef = c => {
|
setRef = c => {
|
||||||
if (c) {
|
if (c) {
|
||||||
|
if (this.props.cacheWidth) this.props.cacheWidth(c.offsetWidth);
|
||||||
this.setState({ width: c.offsetWidth });
|
this.setState({ width: c.offsetWidth });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -86,7 +86,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const status = this.props.status.get('reblog') ? this.props.status.get('reblog') : this.props.status;
|
const status = (this.props.status && this.props.status.get('reblog')) ? this.props.status.get('reblog') : this.props.status;
|
||||||
const outerStyle = { boxSizing: 'border-box' };
|
const outerStyle = { boxSizing: 'border-box' };
|
||||||
const { compact } = this.props;
|
const { compact } = this.props;
|
||||||
|
|
||||||
|
|
|
@ -99,6 +99,7 @@ class Video extends React.PureComponent {
|
||||||
onCloseVideo: PropTypes.func,
|
onCloseVideo: PropTypes.func,
|
||||||
detailed: PropTypes.bool,
|
detailed: PropTypes.bool,
|
||||||
inline: PropTypes.bool,
|
inline: PropTypes.bool,
|
||||||
|
cacheWidth: PropTypes.func,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -108,7 +109,7 @@ class Video extends React.PureComponent {
|
||||||
volume: 0.5,
|
volume: 0.5,
|
||||||
paused: true,
|
paused: true,
|
||||||
dragging: false,
|
dragging: false,
|
||||||
containerWidth: false,
|
containerWidth: this.props.width,
|
||||||
fullscreen: false,
|
fullscreen: false,
|
||||||
hovered: false,
|
hovered: false,
|
||||||
muted: false,
|
muted: false,
|
||||||
|
@ -128,6 +129,7 @@ class Video extends React.PureComponent {
|
||||||
this.player = c;
|
this.player = c;
|
||||||
|
|
||||||
if (c) {
|
if (c) {
|
||||||
|
if (this.props.cacheWidth) this.props.cacheWidth(this.player.offsetWidth);
|
||||||
this.setState({
|
this.setState({
|
||||||
containerWidth: c.offsetWidth,
|
containerWidth: c.offsetWidth,
|
||||||
});
|
});
|
||||||
|
@ -344,7 +346,6 @@ class Video extends React.PureComponent {
|
||||||
width = containerWidth;
|
width = containerWidth;
|
||||||
height = containerWidth / (16/9);
|
height = containerWidth / (16/9);
|
||||||
|
|
||||||
playerStyle.width = width;
|
|
||||||
playerStyle.height = height;
|
playerStyle.height = height;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
import ready from '../mastodon/ready';
|
||||||
|
|
||||||
|
ready(() => {
|
||||||
|
const image = document.querySelector('img');
|
||||||
|
|
||||||
|
image.addEventListener('mouseenter', () => {
|
||||||
|
image.src = '/oops.gif';
|
||||||
|
});
|
||||||
|
|
||||||
|
image.addEventListener('mouseleave', () => {
|
||||||
|
image.src = '/oops.png';
|
||||||
|
});
|
||||||
|
});
|
|
@ -12,3 +12,58 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.rich-formatting a,
|
||||||
|
.rich-formatting p a,
|
||||||
|
.rich-formatting li a,
|
||||||
|
.landing-page__short-description p a,
|
||||||
|
.status__content a,
|
||||||
|
.reply-indicator__content a {
|
||||||
|
color: lighten($ui-highlight-color, 12%);
|
||||||
|
text-decoration: underline;
|
||||||
|
|
||||||
|
&.mention {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mention span {
|
||||||
|
text-decoration: underline;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus,
|
||||||
|
&:active {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus,
|
||||||
|
&:active {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.status__content__spoiler-link {
|
||||||
|
color: $secondary-text-color;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.status__content__read-more-button {
|
||||||
|
text-decoration: underline;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus,
|
||||||
|
&:active {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.getting-started__footer a {
|
||||||
|
text-decoration: underline;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus,
|
||||||
|
&:active {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -41,3 +41,34 @@
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@mixin search-popout() {
|
||||||
|
background: $simple-background-color;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
padding-bottom: 14px;
|
||||||
|
margin-top: 10px;
|
||||||
|
color: $light-text-color;
|
||||||
|
box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4);
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: $light-text-color;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
em {
|
||||||
|
font-weight: 500;
|
||||||
|
color: $inverted-text-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -49,15 +49,9 @@ $small-breakpoint: 960px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
strong,
|
||||||
em {
|
em {
|
||||||
display: inline;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
background: transparent;
|
|
||||||
font-family: inherit;
|
|
||||||
font-size: inherit;
|
|
||||||
line-height: inherit;
|
|
||||||
color: lighten($darker-text-color, 10%);
|
color: lighten($darker-text-color, 10%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -796,7 +790,7 @@ $small-breakpoint: 960px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row-reverse;
|
flex-direction: row-reverse;
|
||||||
flex-wrap: wrap;
|
flex-wrap: nowrap;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
@ -846,6 +840,7 @@ $small-breakpoint: 960px;
|
||||||
}
|
}
|
||||||
|
|
||||||
strong {
|
strong {
|
||||||
|
font-weight: 500;
|
||||||
display: inline;
|
display: inline;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
|
@ -100,6 +100,7 @@ body {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
margin: 20px;
|
margin: 20px;
|
||||||
|
|
||||||
|
&__illustration {
|
||||||
img {
|
img {
|
||||||
display: block;
|
display: block;
|
||||||
max-width: 470px;
|
max-width: 470px;
|
||||||
|
@ -107,6 +108,7 @@ body {
|
||||||
height: auto;
|
height: auto;
|
||||||
margin-top: -120px;
|
margin-top: -120px;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
|
|
|
@ -475,7 +475,7 @@
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity .1s ease;
|
transition: opacity .1s ease;
|
||||||
|
|
||||||
input {
|
textarea {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: $secondary-text-color;
|
color: $secondary-text-color;
|
||||||
border: 0;
|
border: 0;
|
||||||
|
@ -637,7 +637,6 @@
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: pre-wrap;
|
|
||||||
padding-top: 2px;
|
padding-top: 2px;
|
||||||
color: $primary-text-color;
|
color: $primary-text-color;
|
||||||
|
|
||||||
|
@ -661,6 +660,7 @@
|
||||||
|
|
||||||
p {
|
p {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
@ -3050,14 +3050,41 @@ a.status-card.compact:hover {
|
||||||
display: block;
|
display: block;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.column-settings__hashtag-select {
|
.column-settings__hashtags {
|
||||||
|
.column-settings__row {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-select {
|
||||||
&__control {
|
&__control {
|
||||||
@include search-input();
|
@include search-input();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__placeholder {
|
||||||
|
color: $dark-text-color;
|
||||||
|
padding-left: 2px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__value-container {
|
||||||
|
padding-left: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
&__multi-value {
|
&__multi-value {
|
||||||
background: lighten($ui-base-color, 8%);
|
background: lighten($ui-base-color, 8%);
|
||||||
|
|
||||||
|
&__remove {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:active,
|
||||||
|
&:focus {
|
||||||
|
background: lighten($ui-base-color, 12%);
|
||||||
|
color: lighten($darker-text-color, 4%);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__multi-value__label,
|
&__multi-value__label,
|
||||||
|
@ -3065,9 +3092,42 @@ a.status-card.compact:hover {
|
||||||
color: $darker-text-color;
|
color: $darker-text-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__indicator-separator,
|
&__clear-indicator,
|
||||||
&__dropdown-indicator {
|
&__dropdown-indicator {
|
||||||
display: none;
|
cursor: pointer;
|
||||||
|
transition: none;
|
||||||
|
color: $dark-text-color;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:active,
|
||||||
|
&:focus {
|
||||||
|
color: lighten($dark-text-color, 4%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__indicator-separator {
|
||||||
|
background-color: lighten($ui-base-color, 8%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__menu {
|
||||||
|
@include search-popout();
|
||||||
|
padding: 0;
|
||||||
|
background: $ui-secondary-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__menu-list {
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__option {
|
||||||
|
color: $inverted-text-color;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
&--is-focused,
|
||||||
|
&--is-selected {
|
||||||
|
background: darken($ui-secondary-color, 10%);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4933,34 +4993,7 @@ a.status-card.compact:hover {
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-popout {
|
.search-popout {
|
||||||
background: $simple-background-color;
|
@include search-popout();
|
||||||
border-radius: 4px;
|
|
||||||
padding: 10px 14px;
|
|
||||||
padding-bottom: 14px;
|
|
||||||
margin-top: 10px;
|
|
||||||
color: $light-text-color;
|
|
||||||
box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4);
|
|
||||||
|
|
||||||
h4 {
|
|
||||||
text-transform: uppercase;
|
|
||||||
color: $light-text-color;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 500;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
li {
|
|
||||||
padding: 4px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
em {
|
|
||||||
font-weight: 500;
|
|
||||||
color: $inverted-text-color;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
noscript {
|
noscript {
|
||||||
|
|
|
@ -4,6 +4,8 @@ class ActivityTracker
|
||||||
EXPIRE_AFTER = 90.days.seconds
|
EXPIRE_AFTER = 90.days.seconds
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
|
include Redisable
|
||||||
|
|
||||||
def increment(prefix)
|
def increment(prefix)
|
||||||
key = [prefix, current_week].join(':')
|
key = [prefix, current_week].join(':')
|
||||||
|
|
||||||
|
@ -20,10 +22,6 @@ class ActivityTracker
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def redis
|
|
||||||
Redis.current
|
|
||||||
end
|
|
||||||
|
|
||||||
def current_week
|
def current_week
|
||||||
Time.zone.today.cweek
|
Time.zone.today.cweek
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,6 +2,10 @@
|
||||||
|
|
||||||
class ActivityPub::Activity
|
class ActivityPub::Activity
|
||||||
include JsonLdHelper
|
include JsonLdHelper
|
||||||
|
include Redisable
|
||||||
|
|
||||||
|
SUPPORTED_TYPES = %w(Note).freeze
|
||||||
|
CONVERTED_TYPES = %w(Image Video Article Page).freeze
|
||||||
|
|
||||||
def initialize(json, account, **options)
|
def initialize(json, account, **options)
|
||||||
@json = json
|
@json = json
|
||||||
|
@ -70,8 +74,16 @@ class ActivityPub::Activity
|
||||||
@object_uri ||= value_or_id(@object)
|
@object_uri ||= value_or_id(@object)
|
||||||
end
|
end
|
||||||
|
|
||||||
def redis
|
def unsupported_object_type?
|
||||||
Redis.current
|
@object.is_a?(String) || !(supported_object_type? || converted_object_type?)
|
||||||
|
end
|
||||||
|
|
||||||
|
def supported_object_type?
|
||||||
|
equals_or_includes_any?(@object['type'], SUPPORTED_TYPES)
|
||||||
|
end
|
||||||
|
|
||||||
|
def converted_object_type?
|
||||||
|
equals_or_includes_any?(@object['type'], CONVERTED_TYPES)
|
||||||
end
|
end
|
||||||
|
|
||||||
def distribute(status)
|
def distribute(status)
|
||||||
|
@ -123,6 +135,24 @@ class ActivityPub::Activity
|
||||||
redis.setex("delete_upon_arrival:#{@account.id}:#{uri}", 6.hours.seconds, uri)
|
redis.setex("delete_upon_arrival:#{@account.id}:#{uri}", 6.hours.seconds, uri)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def status_from_object
|
||||||
|
# If the status is already known, return it
|
||||||
|
status = status_from_uri(object_uri)
|
||||||
|
|
||||||
|
return status unless status.nil?
|
||||||
|
|
||||||
|
# If the boosted toot is embedded and it is a self-boost, handle it like a Create
|
||||||
|
unless unsupported_object_type?
|
||||||
|
actor_id = value_or_id(first_of_value(@object['attributedTo'])) || @account.uri
|
||||||
|
|
||||||
|
if actor_id == @account.uri
|
||||||
|
return ActivityPub::Activity.factory({ 'type' => 'Create', 'actor' => actor_id, 'object' => @object }, @account).perform
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
fetch_remote_original_status
|
||||||
|
end
|
||||||
|
|
||||||
def fetch_remote_original_status
|
def fetch_remote_original_status
|
||||||
if object_uri.start_with?('http')
|
if object_uri.start_with?('http')
|
||||||
return if ActivityPub::TagManager.instance.local_uri?(object_uri)
|
return if ActivityPub::TagManager.instance.local_uri?(object_uri)
|
||||||
|
@ -137,4 +167,21 @@ class ActivityPub::Activity
|
||||||
ensure
|
ensure
|
||||||
redis.del(key)
|
redis.del(key)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def fetch?
|
||||||
|
!@options[:delivery]
|
||||||
|
end
|
||||||
|
|
||||||
|
def followed_by_local_accounts?
|
||||||
|
@account.passive_relationships.exists?
|
||||||
|
end
|
||||||
|
|
||||||
|
def requested_through_relay?
|
||||||
|
@options[:relayed_through_account] && Relay.find_by(inbox_url: @options[:relayed_through_account].inbox_url)&.enabled?
|
||||||
|
end
|
||||||
|
|
||||||
|
def reject_payload!
|
||||||
|
Rails.logger.info("Rejected #{@json['type']} activity #{@json['id']} from #{@account.uri}#{@options[:relayed_through_account] && "via #{@options[:relayed_through_account].uri}"}")
|
||||||
|
nil
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,10 +2,11 @@
|
||||||
|
|
||||||
class ActivityPub::Activity::Announce < ActivityPub::Activity
|
class ActivityPub::Activity::Announce < ActivityPub::Activity
|
||||||
def perform
|
def perform
|
||||||
original_status = status_from_uri(object_uri)
|
return reject_payload! if delete_arrived_first?(@json['id']) || !related_to_local_activity?
|
||||||
original_status ||= fetch_remote_original_status
|
|
||||||
|
|
||||||
return if original_status.nil? || delete_arrived_first?(@json['id']) || !announceable?(original_status)
|
original_status = status_from_object
|
||||||
|
|
||||||
|
return reject_payload! if original_status.nil? || !announceable?(original_status)
|
||||||
|
|
||||||
status = Status.find_by(account: @account, reblog: original_status)
|
status = Status.find_by(account: @account, reblog: original_status)
|
||||||
|
|
||||||
|
@ -41,4 +42,12 @@ class ActivityPub::Activity::Announce < ActivityPub::Activity
|
||||||
def announceable?(status)
|
def announceable?(status)
|
||||||
status.account_id == @account.id || status.public_visibility? || status.unlisted_visibility?
|
status.account_id == @account.id || status.public_visibility? || status.unlisted_visibility?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def related_to_local_activity?
|
||||||
|
followed_by_local_accounts? || requested_through_relay? || reblog_of_local_status?
|
||||||
|
end
|
||||||
|
|
||||||
|
def reblog_of_local_status?
|
||||||
|
status_from_uri(object_uri)&.account&.local?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,12 +1,8 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class ActivityPub::Activity::Create < ActivityPub::Activity
|
class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||||
SUPPORTED_TYPES = %w(Note).freeze
|
|
||||||
CONVERTED_TYPES = %w(Image Video Article Page).freeze
|
|
||||||
|
|
||||||
def perform
|
def perform
|
||||||
return if unsupported_object_type? || invalid_origin?(@object['id'])
|
return reject_payload! if unsupported_object_type? || invalid_origin?(@object['id']) || Tombstone.exists?(uri: @object['id']) || !related_to_local_activity?
|
||||||
return if Tombstone.exists?(uri: @object['id'])
|
|
||||||
|
|
||||||
RedisLock.acquire(lock_options) do |lock|
|
RedisLock.acquire(lock_options) do |lock|
|
||||||
if lock.acquired?
|
if lock.acquired?
|
||||||
|
@ -318,22 +314,10 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||||
@object['nameMap'].is_a?(Hash) && !@object['nameMap'].empty?
|
@object['nameMap'].is_a?(Hash) && !@object['nameMap'].empty?
|
||||||
end
|
end
|
||||||
|
|
||||||
def unsupported_object_type?
|
|
||||||
@object.is_a?(String) || !(supported_object_type? || converted_object_type?)
|
|
||||||
end
|
|
||||||
|
|
||||||
def unsupported_media_type?(mime_type)
|
def unsupported_media_type?(mime_type)
|
||||||
mime_type.present? && !(MediaAttachment::IMAGE_MIME_TYPES + MediaAttachment::VIDEO_MIME_TYPES).include?(mime_type)
|
mime_type.present? && !(MediaAttachment::IMAGE_MIME_TYPES + MediaAttachment::VIDEO_MIME_TYPES).include?(mime_type)
|
||||||
end
|
end
|
||||||
|
|
||||||
def supported_object_type?
|
|
||||||
equals_or_includes_any?(@object['type'], SUPPORTED_TYPES)
|
|
||||||
end
|
|
||||||
|
|
||||||
def converted_object_type?
|
|
||||||
equals_or_includes_any?(@object['type'], CONVERTED_TYPES)
|
|
||||||
end
|
|
||||||
|
|
||||||
def skip_download?
|
def skip_download?
|
||||||
return @skip_download if defined?(@skip_download)
|
return @skip_download if defined?(@skip_download)
|
||||||
@skip_download ||= DomainBlock.find_by(domain: @account.domain)&.reject_media?
|
@skip_download ||= DomainBlock.find_by(domain: @account.domain)&.reject_media?
|
||||||
|
@ -352,6 +336,25 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||||
!replied_to_status.nil? && replied_to_status.account.local?
|
!replied_to_status.nil? && replied_to_status.account.local?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def related_to_local_activity?
|
||||||
|
fetch? || followed_by_local_accounts? || requested_through_relay? ||
|
||||||
|
responds_to_followed_account? || addresses_local_accounts?
|
||||||
|
end
|
||||||
|
|
||||||
|
def responds_to_followed_account?
|
||||||
|
!replied_to_status.nil? && (replied_to_status.account.local? || replied_to_status.account.passive_relationships.exists?)
|
||||||
|
end
|
||||||
|
|
||||||
|
def addresses_local_accounts?
|
||||||
|
return true if @options[:delivered_to_account_id]
|
||||||
|
|
||||||
|
local_usernames = (as_array(@object['to']) + as_array(@object['cc'])).uniq.select { |uri| ActivityPub::TagManager.instance.local_uri?(uri) }.map { |uri| ActivityPub::TagManager.instance.uri_to_local_id(uri, :username) }
|
||||||
|
|
||||||
|
return false if local_usernames.empty?
|
||||||
|
|
||||||
|
Account.local.where(username: local_usernames).exists?
|
||||||
|
end
|
||||||
|
|
||||||
def forward_for_reply
|
def forward_for_reply
|
||||||
return unless @json['signature'].present? && reply_to_local?
|
return unless @json['signature'].present? && reply_to_local?
|
||||||
ActivityPub::RawDistributionWorker.perform_async(Oj.dump(@json), replied_to_status.account_id, [@account.preferred_inbox_url])
|
ActivityPub::RawDistributionWorker.perform_async(Oj.dump(@json), replied_to_status.account_id, [@account.preferred_inbox_url])
|
||||||
|
|
|
@ -4,6 +4,7 @@ require 'singleton'
|
||||||
|
|
||||||
class FeedManager
|
class FeedManager
|
||||||
include Singleton
|
include Singleton
|
||||||
|
include Redisable
|
||||||
|
|
||||||
MAX_ITEMS = 400
|
MAX_ITEMS = 400
|
||||||
|
|
||||||
|
@ -35,7 +36,7 @@ class FeedManager
|
||||||
|
|
||||||
def unpush_from_home(account, status)
|
def unpush_from_home(account, status)
|
||||||
return false unless remove_from_feed(:home, account.id, status)
|
return false unless remove_from_feed(:home, account.id, status)
|
||||||
Redis.current.publish("timeline:#{account.id}", Oj.dump(event: :delete, payload: status.id.to_s))
|
redis.publish("timeline:#{account.id}", Oj.dump(event: :delete, payload: status.id.to_s))
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -53,7 +54,7 @@ class FeedManager
|
||||||
|
|
||||||
def unpush_from_list(list, status)
|
def unpush_from_list(list, status)
|
||||||
return false unless remove_from_feed(:list, list.id, status)
|
return false unless remove_from_feed(:list, list.id, status)
|
||||||
Redis.current.publish("timeline:list:#{list.id}", Oj.dump(event: :delete, payload: status.id.to_s))
|
redis.publish("timeline:list:#{list.id}", Oj.dump(event: :delete, payload: status.id.to_s))
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -142,10 +143,6 @@ class FeedManager
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def redis
|
|
||||||
Redis.current
|
|
||||||
end
|
|
||||||
|
|
||||||
def push_update_required?(timeline_id)
|
def push_update_required?(timeline_id)
|
||||||
redis.exists("subscribed:#{timeline_id}")
|
redis.exists("subscribed:#{timeline_id}")
|
||||||
end
|
end
|
||||||
|
|
|
@ -99,7 +99,7 @@ class Formatter
|
||||||
end
|
end
|
||||||
|
|
||||||
def encode_and_link_urls(html, accounts = nil, options = {})
|
def encode_and_link_urls(html, accounts = nil, options = {})
|
||||||
entities = Extractor.extract_entities_with_indices(html, extract_url_without_protocol: false)
|
entities = utf8_friendly_extractor(html, extract_url_without_protocol: false)
|
||||||
|
|
||||||
if accounts.is_a?(Hash)
|
if accounts.is_a?(Hash)
|
||||||
options = accounts
|
options = accounts
|
||||||
|
@ -199,6 +199,53 @@ class Formatter
|
||||||
result.flatten.join
|
result.flatten.join
|
||||||
end
|
end
|
||||||
|
|
||||||
|
UNICODE_ESCAPE_BLACKLIST_RE = /\p{Z}|\p{P}/
|
||||||
|
|
||||||
|
def utf8_friendly_extractor(text, options = {})
|
||||||
|
old_to_new_index = [0]
|
||||||
|
|
||||||
|
escaped = text.chars.map do |c|
|
||||||
|
output = begin
|
||||||
|
if c.ord.to_s(16).length > 2 && UNICODE_ESCAPE_BLACKLIST_RE.match(c).nil?
|
||||||
|
CGI.escape(c)
|
||||||
|
else
|
||||||
|
c
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
old_to_new_index << old_to_new_index.last + output.length
|
||||||
|
|
||||||
|
output
|
||||||
|
end.join
|
||||||
|
|
||||||
|
# Note: I couldn't obtain list_slug with @user/list-name format
|
||||||
|
# for mention so this requires additional check
|
||||||
|
special = Extractor.extract_urls_with_indices(escaped, options).map do |extract|
|
||||||
|
# exactly one of :url, :hashtag, :screen_name, :cashtag keys is present
|
||||||
|
key = (extract.keys & [:url, :hashtag, :screen_name, :cashtag]).first
|
||||||
|
|
||||||
|
new_indices = [
|
||||||
|
old_to_new_index.find_index(extract[:indices].first),
|
||||||
|
old_to_new_index.find_index(extract[:indices].last),
|
||||||
|
]
|
||||||
|
|
||||||
|
has_prefix_char = [:hashtag, :screen_name, :cashtag].include?(key)
|
||||||
|
value_indices = [
|
||||||
|
new_indices.first + (has_prefix_char ? 1 : 0), # account for #, @ or $
|
||||||
|
new_indices.last - 1,
|
||||||
|
]
|
||||||
|
|
||||||
|
next extract.merge(
|
||||||
|
:indices => new_indices,
|
||||||
|
key => text[value_indices.first..value_indices.last]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
standard = Extractor.extract_entities_with_indices(text, options)
|
||||||
|
|
||||||
|
Extractor.remove_overlapping_entities(special + standard)
|
||||||
|
end
|
||||||
|
|
||||||
def link_to_url(entity, options = {})
|
def link_to_url(entity, options = {})
|
||||||
url = Addressable::URI.parse(entity[:url])
|
url = Addressable::URI.parse(entity[:url])
|
||||||
html_attrs = { target: '_blank', rel: 'nofollow noopener' }
|
html_attrs = { target: '_blank', rel: 'nofollow noopener' }
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class OStatus::Activity::Base
|
class OStatus::Activity::Base
|
||||||
|
include Redisable
|
||||||
|
|
||||||
def initialize(xml, account = nil, **options)
|
def initialize(xml, account = nil, **options)
|
||||||
@xml = xml
|
@xml = xml
|
||||||
@account = account
|
@account = account
|
||||||
|
@ -66,8 +68,4 @@ class OStatus::Activity::Base
|
||||||
Status.find_by(uri: uri)
|
Status.find_by(uri: uri)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def redis
|
|
||||||
Redis.current
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -11,6 +11,8 @@ class PotentialFriendshipTracker
|
||||||
}.freeze
|
}.freeze
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
|
include Redisable
|
||||||
|
|
||||||
def record(account_id, target_account_id, action)
|
def record(account_id, target_account_id, action)
|
||||||
return if account_id == target_account_id
|
return if account_id == target_account_id
|
||||||
|
|
||||||
|
@ -31,11 +33,5 @@ class PotentialFriendshipTracker
|
||||||
return [] if account_ids.empty?
|
return [] if account_ids.empty?
|
||||||
Account.searchable.where(id: account_ids)
|
Account.searchable.where(id: account_ids)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def redis
|
|
||||||
Redis.current
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -30,7 +30,8 @@ class AccountConversation < ApplicationRecord
|
||||||
if participant_account_ids.empty?
|
if participant_account_ids.empty?
|
||||||
[account]
|
[account]
|
||||||
else
|
else
|
||||||
Account.where(id: participant_account_ids)
|
participants = Account.where(id: participant_account_ids)
|
||||||
|
participants.empty? ? [account] : participants
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Redisable
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def redis
|
||||||
|
Redis.current
|
||||||
|
end
|
||||||
|
end
|
|
@ -24,6 +24,8 @@ class DomainBlock < ApplicationRecord
|
||||||
has_many :accounts, foreign_key: :domain, primary_key: :domain
|
has_many :accounts, foreign_key: :domain, primary_key: :domain
|
||||||
delegate :count, to: :accounts, prefix: true
|
delegate :count, to: :accounts, prefix: true
|
||||||
|
|
||||||
|
scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) }
|
||||||
|
|
||||||
def self.blocked?(domain)
|
def self.blocked?(domain)
|
||||||
where(domain: domain, severity: :suspend).exists?
|
where(domain: domain, severity: :suspend).exists?
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Feed
|
class Feed
|
||||||
|
include Redisable
|
||||||
|
|
||||||
def initialize(type, id)
|
def initialize(type, id)
|
||||||
@type = type
|
@type = type
|
||||||
@id = id
|
@id = id
|
||||||
|
@ -27,8 +29,4 @@ class Feed
|
||||||
def key
|
def key
|
||||||
FeedManager.instance.key(@type, @id)
|
FeedManager.instance.key(@type, @id)
|
||||||
end
|
end
|
||||||
|
|
||||||
def redis
|
|
||||||
Redis.current
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -9,9 +9,13 @@ class InstanceFilter
|
||||||
|
|
||||||
def results
|
def results
|
||||||
if params[:limited].present?
|
if params[:limited].present?
|
||||||
DomainBlock.order(id: :desc)
|
scope = DomainBlock
|
||||||
|
scope = scope.matches_domain(params[:by_domain]) if params[:by_domain].present?
|
||||||
|
scope.order(id: :desc)
|
||||||
else
|
else
|
||||||
Account.remote.by_domain_accounts
|
scope = Account.remote
|
||||||
|
scope = scope.matches_domain(params[:by_domain]) if params[:by_domain].present?
|
||||||
|
scope.by_domain_accounts
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -29,6 +29,7 @@ class Relay < ApplicationRecord
|
||||||
payload = Oj.dump(follow_activity(activity_id))
|
payload = Oj.dump(follow_activity(activity_id))
|
||||||
|
|
||||||
update!(state: :pending, follow_activity_id: activity_id)
|
update!(state: :pending, follow_activity_id: activity_id)
|
||||||
|
DeliveryFailureTracker.new(inbox_url).track_success!
|
||||||
ActivityPub::DeliveryWorker.perform_async(payload, some_local_account.id, inbox_url)
|
ActivityPub::DeliveryWorker.perform_async(payload, some_local_account.id, inbox_url)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -37,6 +38,7 @@ class Relay < ApplicationRecord
|
||||||
payload = Oj.dump(unfollow_activity(activity_id))
|
payload = Oj.dump(unfollow_activity(activity_id))
|
||||||
|
|
||||||
update!(state: :idle, follow_activity_id: nil)
|
update!(state: :idle, follow_activity_id: nil)
|
||||||
|
DeliveryFailureTracker.new(inbox_url).track_success!
|
||||||
ActivityPub::DeliveryWorker.perform_async(payload, some_local_account.id, inbox_url)
|
ActivityPub::DeliveryWorker.perform_async(payload, some_local_account.id, inbox_url)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,8 @@ class TrendingTags
|
||||||
THRESHOLD = 5
|
THRESHOLD = 5
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
|
include Redisable
|
||||||
|
|
||||||
def record_use!(tag, account, at_time = Time.now.utc)
|
def record_use!(tag, account, at_time = Time.now.utc)
|
||||||
return if disallowed_hashtags.include?(tag.name) || account.silenced? || account.bot?
|
return if disallowed_hashtags.include?(tag.name) || account.silenced? || account.bot?
|
||||||
|
|
||||||
|
@ -59,9 +61,5 @@ class TrendingTags
|
||||||
@disallowed_hashtags = @disallowed_hashtags.split(' ') if @disallowed_hashtags.is_a? String
|
@disallowed_hashtags = @disallowed_hashtags.split(' ') if @disallowed_hashtags.is_a? String
|
||||||
@disallowed_hashtags = @disallowed_hashtags.map(&:downcase)
|
@disallowed_hashtags = @disallowed_hashtags.map(&:downcase)
|
||||||
end
|
end
|
||||||
|
|
||||||
def redis
|
|
||||||
Redis.current
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,8 +3,8 @@
|
||||||
class ActivityPub::ActivitySerializer < ActiveModel::Serializer
|
class ActivityPub::ActivitySerializer < ActiveModel::Serializer
|
||||||
attributes :id, :type, :actor, :published, :to, :cc
|
attributes :id, :type, :actor, :published, :to, :cc
|
||||||
|
|
||||||
has_one :proper, key: :object, serializer: ActivityPub::NoteSerializer, unless: :announce?
|
has_one :proper, key: :object, serializer: ActivityPub::NoteSerializer, unless: :owned_announce?
|
||||||
attribute :proper_uri, key: :object, if: :announce?
|
attribute :proper_uri, key: :object, if: :owned_announce?
|
||||||
attribute :atom_uri, if: :announce?
|
attribute :atom_uri, if: :announce?
|
||||||
|
|
||||||
def id
|
def id
|
||||||
|
@ -42,4 +42,8 @@ class ActivityPub::ActivitySerializer < ActiveModel::Serializer
|
||||||
def announce?
|
def announce?
|
||||||
object.reblog?
|
object.reblog?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def owned_announce?
|
||||||
|
announce? && object.account == object.proper.account && object.proper.private_visibility?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
class REST::ApplicationSerializer < ActiveModel::Serializer
|
class REST::ApplicationSerializer < ActiveModel::Serializer
|
||||||
attributes :id, :name, :website, :redirect_uri,
|
attributes :id, :name, :website, :redirect_uri,
|
||||||
:client_id, :client_secret
|
:client_id, :client_secret, :vapid_key
|
||||||
|
|
||||||
def id
|
def id
|
||||||
object.id.to_s
|
object.id.to_s
|
||||||
|
@ -19,4 +19,8 @@ class REST::ApplicationSerializer < ActiveModel::Serializer
|
||||||
def website
|
def website
|
||||||
object.website.presence
|
object.website.presence
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def vapid_key
|
||||||
|
Rails.configuration.x.vapid_public_key
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,7 +5,7 @@ class REST::InstanceSerializer < ActiveModel::Serializer
|
||||||
|
|
||||||
attributes :uri, :title, :description, :email,
|
attributes :uri, :title, :description, :email,
|
||||||
:version, :urls, :stats, :thumbnail,
|
:version, :urls, :stats, :thumbnail,
|
||||||
:languages
|
:languages, :registrations
|
||||||
|
|
||||||
has_one :contact_account, serializer: REST::AccountSerializer
|
has_one :contact_account, serializer: REST::AccountSerializer
|
||||||
|
|
||||||
|
@ -51,6 +51,10 @@ class REST::InstanceSerializer < ActiveModel::Serializer
|
||||||
[I18n.default_locale]
|
[I18n.default_locale]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def registrations
|
||||||
|
Setting.open_registrations && !Rails.configuration.x.single_user_mode
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def instance_presenter
|
def instance_presenter
|
||||||
|
|
|
@ -212,7 +212,7 @@ class ActivityPub::ProcessAccountService < BaseService
|
||||||
end
|
end
|
||||||
|
|
||||||
def clear_tombstones!
|
def clear_tombstones!
|
||||||
Tombstone.delete_all(account_id: @account.id)
|
Tombstone.where(account_id: @account.id).delete_all
|
||||||
end
|
end
|
||||||
|
|
||||||
def protocol_changed?
|
def protocol_changed?
|
||||||
|
|
|
@ -44,6 +44,7 @@ class ActivityPub::ProcessCollectionService < BaseService
|
||||||
end
|
end
|
||||||
|
|
||||||
def verify_account!
|
def verify_account!
|
||||||
|
@options[:relayed_through_account] = @account
|
||||||
@account = ActivityPub::LinkedDataSignature.new(@json).verify_account!
|
@account = ActivityPub::LinkedDataSignature.new(@json).verify_account!
|
||||||
rescue JSON::LD::JsonLdError => e
|
rescue JSON::LD::JsonLdError => e
|
||||||
Rails.logger.debug "Could not verify LD-Signature for #{value_or_id(@json['actor'])}: #{e.message}"
|
Rails.logger.debug "Could not verify LD-Signature for #{value_or_id(@json['actor'])}: #{e.message}"
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
class BatchedRemoveStatusService < BaseService
|
class BatchedRemoveStatusService < BaseService
|
||||||
include StreamEntryRenderer
|
include StreamEntryRenderer
|
||||||
|
include Redisable
|
||||||
|
|
||||||
# Delete given statuses and reblogs of them
|
# Delete given statuses and reblogs of them
|
||||||
# Dispatch PuSH updates of the deleted statuses, but only local ones
|
# Dispatch PuSH updates of the deleted statuses, but only local ones
|
||||||
|
@ -109,10 +110,6 @@ class BatchedRemoveStatusService < BaseService
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def redis
|
|
||||||
Redis.current
|
|
||||||
end
|
|
||||||
|
|
||||||
def build_xml(stream_entry)
|
def build_xml(stream_entry)
|
||||||
return @activity_xml[stream_entry.id] if @activity_xml.key?(stream_entry.id)
|
return @activity_xml[stream_entry.id] if @activity_xml.key?(stream_entry.id)
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class FollowService < BaseService
|
class FollowService < BaseService
|
||||||
|
include Redisable
|
||||||
|
|
||||||
# Follow a remote user, notify remote user about the follow
|
# Follow a remote user, notify remote user about the follow
|
||||||
# @param [Account] source_account From which to follow
|
# @param [Account] source_account From which to follow
|
||||||
# @param [String, Account] uri User URI to follow in the form of username@domain (or account record)
|
# @param [String, Account] uri User URI to follow in the form of username@domain (or account record)
|
||||||
|
@ -67,10 +69,6 @@ class FollowService < BaseService
|
||||||
follow
|
follow
|
||||||
end
|
end
|
||||||
|
|
||||||
def redis
|
|
||||||
Redis.current
|
|
||||||
end
|
|
||||||
|
|
||||||
def build_follow_request_xml(follow_request)
|
def build_follow_request_xml(follow_request)
|
||||||
OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.follow_request_salmon(follow_request))
|
OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.follow_request_salmon(follow_request))
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class PostStatusService < BaseService
|
class PostStatusService < BaseService
|
||||||
|
include Redisable
|
||||||
|
|
||||||
MIN_SCHEDULE_OFFSET = 5.minutes.freeze
|
MIN_SCHEDULE_OFFSET = 5.minutes.freeze
|
||||||
|
|
||||||
# Post a text status update, fetch and notify remote users mentioned
|
# Post a text status update, fetch and notify remote users mentioned
|
||||||
|
@ -110,10 +112,6 @@ class PostStatusService < BaseService
|
||||||
ProcessHashtagsService.new
|
ProcessHashtagsService.new
|
||||||
end
|
end
|
||||||
|
|
||||||
def redis
|
|
||||||
Redis.current
|
|
||||||
end
|
|
||||||
|
|
||||||
def scheduled?
|
def scheduled?
|
||||||
@scheduled_at.present?
|
@scheduled_at.present?
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
class RemoveStatusService < BaseService
|
class RemoveStatusService < BaseService
|
||||||
include StreamEntryRenderer
|
include StreamEntryRenderer
|
||||||
|
include Redisable
|
||||||
|
|
||||||
def call(status, **options)
|
def call(status, **options)
|
||||||
@payload = Oj.dump(event: :delete, payload: status.id.to_s)
|
@payload = Oj.dump(event: :delete, payload: status.id.to_s)
|
||||||
|
@ -55,7 +56,7 @@ class RemoveStatusService < BaseService
|
||||||
|
|
||||||
def remove_from_affected
|
def remove_from_affected
|
||||||
@mentions.map(&:account).select(&:local?).each do |account|
|
@mentions.map(&:account).select(&:local?).each do |account|
|
||||||
Redis.current.publish("timeline:#{account.id}", @payload)
|
redis.publish("timeline:#{account.id}", @payload)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -133,26 +134,22 @@ class RemoveStatusService < BaseService
|
||||||
return unless @status.public_visibility?
|
return unless @status.public_visibility?
|
||||||
|
|
||||||
@tags.each do |hashtag|
|
@tags.each do |hashtag|
|
||||||
Redis.current.publish("timeline:hashtag:#{hashtag}", @payload)
|
redis.publish("timeline:hashtag:#{hashtag}", @payload)
|
||||||
Redis.current.publish("timeline:hashtag:#{hashtag}:local", @payload) if @status.local?
|
redis.publish("timeline:hashtag:#{hashtag}:local", @payload) if @status.local?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def remove_from_public
|
def remove_from_public
|
||||||
return unless @status.public_visibility?
|
return unless @status.public_visibility?
|
||||||
|
|
||||||
Redis.current.publish('timeline:public', @payload)
|
redis.publish('timeline:public', @payload)
|
||||||
Redis.current.publish('timeline:public:local', @payload) if @status.local?
|
redis.publish('timeline:public:local', @payload) if @status.local?
|
||||||
end
|
end
|
||||||
|
|
||||||
def remove_from_media
|
def remove_from_media
|
||||||
return unless @status.public_visibility?
|
return unless @status.public_visibility?
|
||||||
|
|
||||||
Redis.current.publish('timeline:public:media', @payload)
|
redis.publish('timeline:public:media', @payload)
|
||||||
Redis.current.publish('timeline:public:local:media', @payload) if @status.local?
|
redis.publish('timeline:public:local:media', @payload) if @status.local?
|
||||||
end
|
|
||||||
|
|
||||||
def redis
|
|
||||||
Redis.current
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -102,6 +102,10 @@ class SuspendAccountService < BaseService
|
||||||
ActivityPub::DeliveryWorker.push_bulk(delivery_inboxes) do |inbox_url|
|
ActivityPub::DeliveryWorker.push_bulk(delivery_inboxes) do |inbox_url|
|
||||||
[delete_actor_json, @account.id, inbox_url]
|
[delete_actor_json, @account.id, inbox_url]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
ActivityPub::LowPriorityDeliveryWorker.push_bulk(low_priority_delivery_inboxes) do |inbox_url|
|
||||||
|
[delete_actor_json, @account.id, inbox_url]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def delete_actor_json
|
def delete_actor_json
|
||||||
|
@ -117,7 +121,11 @@ class SuspendAccountService < BaseService
|
||||||
end
|
end
|
||||||
|
|
||||||
def delivery_inboxes
|
def delivery_inboxes
|
||||||
Account.inboxes + Relay.enabled.pluck(:inbox_url)
|
@delivery_inboxes ||= @account.followers.inboxes + Relay.enabled.pluck(:inbox_url)
|
||||||
|
end
|
||||||
|
|
||||||
|
def low_priority_delivery_inboxes
|
||||||
|
Account.inboxes - delivery_inboxes
|
||||||
end
|
end
|
||||||
|
|
||||||
def associations_for_destruction
|
def associations_for_destruction
|
||||||
|
|
|
@ -24,6 +24,7 @@ class EmailMxValidator < ActiveModel::Validator
|
||||||
|
|
||||||
([domain] + hostnames).uniq.each do |hostname|
|
([domain] + hostnames).uniq.each do |hostname|
|
||||||
ips.concat(dns.getresources(hostname, Resolv::DNS::Resource::IN::A).to_a.map { |e| e.address.to_s })
|
ips.concat(dns.getresources(hostname, Resolv::DNS::Resource::IN::A).to_a.map { |e| e.address.to_s })
|
||||||
|
ips.concat(dns.getresources(hostname, Resolv::DNS::Resource::IN::AAAA).to_a.map { |e| e.address.to_s })
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -26,6 +26,7 @@
|
||||||
= hidden_field_tag key, params[key]
|
= hidden_field_tag key, params[key]
|
||||||
|
|
||||||
- %i(username by_domain display_name email ip).each do |key|
|
- %i(username by_domain display_name email ip).each do |key|
|
||||||
|
- unless key == :by_domain && params[:remote].blank?
|
||||||
.input.string.optional
|
.input.string.optional
|
||||||
= text_field_tag key, params[key], class: 'string optional', placeholder: I18n.t("admin.accounts.#{key}")
|
= text_field_tag key, params[key], class: 'string optional', placeholder: I18n.t("admin.accounts.#{key}")
|
||||||
|
|
||||||
|
|
|
@ -166,6 +166,12 @@
|
||||||
- else
|
- else
|
||||||
= link_to t('admin.accounts.perform_full_suspension'), new_admin_account_action_path(@account.id, type: 'suspend'), class: 'button button--destructive' if can?(:suspend, @account)
|
= link_to t('admin.accounts.perform_full_suspension'), new_admin_account_action_path(@account.id, type: 'suspend'), class: 'button button--destructive' if can?(:suspend, @account)
|
||||||
|
|
||||||
|
- unless @account.local?
|
||||||
|
- if DomainBlock.where(domain: @account.domain).exists?
|
||||||
|
= link_to t('admin.domain_blocks.undo'), admin_instance_path(@account.domain), class: 'button'
|
||||||
|
- else
|
||||||
|
= link_to t('admin.domain_blocks.add_new'), new_admin_domain_block_path(_domain: @account.domain), class: 'button button--destructive'
|
||||||
|
|
||||||
%hr.spacer/
|
%hr.spacer/
|
||||||
|
|
||||||
- unless @warnings.empty?
|
- unless @warnings.empty?
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
= simple_form_for @user, url: admin_account_change_email_path(@account.id) do |f|
|
= simple_form_for @user, url: admin_account_change_email_path(@account.id) do |f|
|
||||||
.fields-group
|
.fields-group
|
||||||
= f.input :email, wrapper: :with_label, disabled: true, label: t('admin.accounts.change_email.current_email')
|
= f.input :email, wrapper: :with_label, hint: false, disabled: true, label: t('admin.accounts.change_email.current_email')
|
||||||
|
|
||||||
.fields-group
|
.fields-group
|
||||||
= f.input :unconfirmed_email, wrapper: :with_label, label: t('admin.accounts.change_email.new_email')
|
= f.input :unconfirmed_email, wrapper: :with_label, label: t('admin.accounts.change_email.new_email')
|
||||||
|
|
|
@ -11,6 +11,20 @@
|
||||||
%div{ style: 'flex: 1 1 auto; text-align: right' }
|
%div{ style: 'flex: 1 1 auto; text-align: right' }
|
||||||
= link_to t('admin.domain_blocks.add_new'), new_admin_domain_block_path, class: 'button'
|
= link_to t('admin.domain_blocks.add_new'), new_admin_domain_block_path, class: 'button'
|
||||||
|
|
||||||
|
= form_tag admin_instances_url, method: 'GET', class: 'simple_form' do
|
||||||
|
.fields-group
|
||||||
|
- Admin::FilterHelper::INSTANCES_FILTERS.each do |key|
|
||||||
|
- if params[key].present?
|
||||||
|
= hidden_field_tag key, params[key]
|
||||||
|
|
||||||
|
- %i(by_domain).each do |key|
|
||||||
|
.input.string.optional
|
||||||
|
= text_field_tag key, params[key], class: 'string optional', placeholder: I18n.t("admin.instances.#{key}")
|
||||||
|
|
||||||
|
.actions
|
||||||
|
%button= t('admin.accounts.search')
|
||||||
|
= link_to t('admin.accounts.reset'), admin_instances_path, class: 'button negative'
|
||||||
|
|
||||||
%hr.spacer/
|
%hr.spacer/
|
||||||
|
|
||||||
- @instances.each do |instance|
|
- @instances.each do |instance|
|
||||||
|
|
|
@ -7,8 +7,11 @@
|
||||||
%meta{ content: 'width=device-width,initial-scale=1', name: 'viewport' }/
|
%meta{ content: 'width=device-width,initial-scale=1', name: 'viewport' }/
|
||||||
= stylesheet_pack_tag 'common', media: 'all'
|
= stylesheet_pack_tag 'common', media: 'all'
|
||||||
= stylesheet_pack_tag Setting.default_settings['theme'], media: 'all'
|
= stylesheet_pack_tag Setting.default_settings['theme'], media: 'all'
|
||||||
|
= javascript_pack_tag 'common', integrity: true, crossorigin: 'anonymous'
|
||||||
|
= javascript_pack_tag 'error', integrity: true, crossorigin: 'anonymous'
|
||||||
%body.error
|
%body.error
|
||||||
.dialog
|
.dialog
|
||||||
%img{ alt: Setting.default_settings['site_title'], src: '/oops.gif' }/
|
.dialog__illustration
|
||||||
%div
|
%img{ alt: Setting.default_settings['site_title'], src: '/oops.png' }/
|
||||||
|
.dialog__message
|
||||||
%h1= yield :content
|
%h1= yield :content
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ActivityPub::LowPriorityDeliveryWorker < ActivityPub::DeliveryWorker
|
||||||
|
sidekiq_options queue: 'pull', retry: 8, dead: false
|
||||||
|
end
|
|
@ -6,6 +6,6 @@ class ActivityPub::ProcessingWorker
|
||||||
sidekiq_options backtrace: true
|
sidekiq_options backtrace: true
|
||||||
|
|
||||||
def perform(account_id, body, delivered_to_account_id = nil)
|
def perform(account_id, body, delivered_to_account_id = nil)
|
||||||
ActivityPub::ProcessCollectionService.new.call(body, Account.find(account_id), override_timestamps: true, delivered_to_account_id: delivered_to_account_id)
|
ActivityPub::ProcessCollectionService.new.call(body, Account.find(account_id), override_timestamps: true, delivered_to_account_id: delivered_to_account_id, delivery: true)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
class Scheduler::FeedCleanupScheduler
|
class Scheduler::FeedCleanupScheduler
|
||||||
include Sidekiq::Worker
|
include Sidekiq::Worker
|
||||||
|
include Redisable
|
||||||
|
|
||||||
sidekiq_options unique: :until_executed, retry: 0
|
sidekiq_options unique: :until_executed, retry: 0
|
||||||
|
|
||||||
|
@ -57,8 +58,4 @@ class Scheduler::FeedCleanupScheduler
|
||||||
def feed_manager
|
def feed_manager
|
||||||
FeedManager.instance
|
FeedManager.instance
|
||||||
end
|
end
|
||||||
|
|
||||||
def redis
|
|
||||||
Redis.current
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -46,14 +46,14 @@ class Rack::Attack
|
||||||
end
|
end
|
||||||
|
|
||||||
throttle('throttle_authenticated_api', limit: 300, period: 5.minutes) do |req|
|
throttle('throttle_authenticated_api', limit: 300, period: 5.minutes) do |req|
|
||||||
req.api_request? && req.authenticated_user_id
|
req.authenticated_user_id if req.api_request?
|
||||||
end
|
end
|
||||||
|
|
||||||
throttle('throttle_unauthenticated_api', limit: 7_500, period: 5.minutes) do |req|
|
throttle('throttle_unauthenticated_api', limit: 7_500, period: 5.minutes) do |req|
|
||||||
req.ip if req.api_request?
|
req.ip if req.api_request?
|
||||||
end
|
end
|
||||||
|
|
||||||
throttle('throttle_media', limit: 30, period: 30.minutes) do |req|
|
throttle('throttle_api_media', limit: 30, period: 30.minutes) do |req|
|
||||||
req.authenticated_user_id if req.post? && req.path.start_with?('/api/v1/media')
|
req.authenticated_user_id if req.post? && req.path.start_with?('/api/v1/media')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -61,6 +61,13 @@ class Rack::Attack
|
||||||
req.ip if req.post? && req.path == '/api/v1/accounts'
|
req.ip if req.post? && req.path == '/api/v1/accounts'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
API_DELETE_REBLOG_REGEX = /\A\/api\/v1\/statuses\/[\d]+\/unreblog/.freeze
|
||||||
|
API_DELETE_STATUS_REGEX = /\A\/api\/v1\/statuses\/[\d]+/.freeze
|
||||||
|
|
||||||
|
throttle('throttle_api_delete', limit: 30, period: 30.minutes) do |req|
|
||||||
|
req.authenticated_user_id if (req.post? && req.path =~ API_DELETE_REBLOG_REGEX) || (req.delete? && req.path =~ API_DELETE_STATUS_REGEX)
|
||||||
|
end
|
||||||
|
|
||||||
throttle('protected_paths', limit: 25, period: 5.minutes) do |req|
|
throttle('protected_paths', limit: 25, period: 5.minutes) do |req|
|
||||||
req.ip if req.post? && req.path =~ PROTECTED_PATHS_REGEX
|
req.ip if req.post? && req.path =~ PROTECTED_PATHS_REGEX
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
module Twitter
|
module Twitter
|
||||||
class Regex
|
class Regex
|
||||||
REGEXEN[:valid_general_url_path_chars] = /[^\p{White_Space}\(\)\?]/iou
|
REGEXEN[:valid_general_url_path_chars] = /[^\p{White_Space}<>\(\)\?]/iou
|
||||||
REGEXEN[:valid_url_path_ending_chars] = /[^\p{White_Space}\(\)\?!\*';:=\,\.\$%\[\]~&\|@]|(?:#{REGEXEN[:valid_url_balanced_parens]})/iou
|
REGEXEN[:valid_url_path_ending_chars] = /[^\p{White_Space}\(\)\?!\*"'「」<>;:=\,\.\$%\[\]~&\|@]|(?:#{REGEXEN[:valid_url_balanced_parens]})/iou
|
||||||
REGEXEN[:valid_url_balanced_parens] = /
|
REGEXEN[:valid_url_balanced_parens] = /
|
||||||
\(
|
\(
|
||||||
(?:
|
(?:
|
||||||
|
|
|
@ -302,6 +302,7 @@ en:
|
||||||
back_to_account: Back To Account
|
back_to_account: Back To Account
|
||||||
title: "%{acct}'s Followers"
|
title: "%{acct}'s Followers"
|
||||||
instances:
|
instances:
|
||||||
|
by_domain: Domain
|
||||||
delivery_available: Delivery is available
|
delivery_available: Delivery is available
|
||||||
known_accounts:
|
known_accounts:
|
||||||
one: "%{count} known account"
|
one: "%{count} known account"
|
||||||
|
@ -929,9 +930,9 @@ en:
|
||||||
<p>Originally adapted from the <a href="https://github.com/discourse/discourse">Discourse privacy policy</a>.</p>
|
<p>Originally adapted from the <a href="https://github.com/discourse/discourse">Discourse privacy policy</a>.</p>
|
||||||
title: "%{instance} Terms of Service and Privacy Policy"
|
title: "%{instance} Terms of Service and Privacy Policy"
|
||||||
themes:
|
themes:
|
||||||
contrast: High contrast
|
contrast: Mastodon (High contrast)
|
||||||
default: Cybrespace
|
default: Cybrespace
|
||||||
mastodon: Mastodon
|
mastodon: Mastodon (Dark)
|
||||||
mastodon-light: Mastodon (light)
|
mastodon-light: Mastodon (light)
|
||||||
win95: Windows 95
|
win95: Windows 95
|
||||||
light: Cybre Lite
|
light: Cybre Lite
|
||||||
|
|
|
@ -9,7 +9,7 @@ WorkingDirectory=/home/mastodon/live
|
||||||
Environment="NODE_ENV=production"
|
Environment="NODE_ENV=production"
|
||||||
Environment="PORT=4000"
|
Environment="PORT=4000"
|
||||||
Environment="STREAMING_CLUSTER_NUM=1"
|
Environment="STREAMING_CLUSTER_NUM=1"
|
||||||
ExecStart=/usr/bin/npm run start
|
ExecStart=/usr/bin/node ./streaming
|
||||||
TimeoutSec=15
|
TimeoutSec=15
|
||||||
Restart=always
|
Restart=always
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,7 @@ module Mastodon
|
||||||
end
|
end
|
||||||
|
|
||||||
def patch
|
def patch
|
||||||
1
|
3
|
||||||
end
|
end
|
||||||
|
|
||||||
def pre
|
def pre
|
||||||
|
@ -29,7 +29,7 @@ module Mastodon
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_s
|
def to_s
|
||||||
[to_a.join('.'), flags, '-cybre'].join
|
[to_a.join('.'), flags].join
|
||||||
end
|
end
|
||||||
|
|
||||||
def repository
|
def repository
|
||||||
|
@ -37,7 +37,7 @@ module Mastodon
|
||||||
end
|
end
|
||||||
|
|
||||||
def source_base_url
|
def source_base_url
|
||||||
'https://cybre.tech/cybrespace/mastodon'
|
"https://github.com/#{repository}"
|
||||||
end
|
end
|
||||||
|
|
||||||
# specify git tag or commit hash here
|
# specify git tag or commit hash here
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 20 KiB |
|
@ -1,5 +1,4 @@
|
||||||
# See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
|
# See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
|
||||||
#
|
|
||||||
# To ban all spiders from the entire site uncomment the next two lines:
|
User-agent: *
|
||||||
# User-agent: *
|
Disallow: /media_proxy/
|
||||||
# Disallow: /
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe ActivityPub::Activity::Announce do
|
RSpec.describe ActivityPub::Activity::Announce do
|
||||||
let(:sender) { Fabricate(:account) }
|
let(:sender) { Fabricate(:account, followers_url: 'http://example.com/followers', uri: 'https://example.com/actor') }
|
||||||
let(:recipient) { Fabricate(:account) }
|
let(:recipient) { Fabricate(:account) }
|
||||||
let(:status) { Fabricate(:status, account: recipient) }
|
let(:status) { Fabricate(:status, account: recipient) }
|
||||||
|
|
||||||
|
@ -10,20 +10,162 @@ RSpec.describe ActivityPub::Activity::Announce do
|
||||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||||
id: 'foo',
|
id: 'foo',
|
||||||
type: 'Announce',
|
type: 'Announce',
|
||||||
actor: ActivityPub::TagManager.instance.uri_for(sender),
|
actor: 'https://example.com/actor',
|
||||||
object: ActivityPub::TagManager.instance.uri_for(status),
|
object: object_json,
|
||||||
}.with_indifferent_access
|
}.with_indifferent_access
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#perform' do
|
let(:unknown_object_json) do
|
||||||
|
{
|
||||||
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||||
|
id: 'https://example.com/actor/hello-world',
|
||||||
|
type: 'Note',
|
||||||
|
attributedTo: 'https://example.com/actor',
|
||||||
|
content: 'Hello world',
|
||||||
|
to: 'http://example.com/followers',
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
subject { described_class.new(json, sender) }
|
subject { described_class.new(json, sender) }
|
||||||
|
|
||||||
|
describe '#perform' do
|
||||||
|
context 'when sender is followed by a local account' do
|
||||||
before do
|
before do
|
||||||
|
Fabricate(:account).follow!(sender)
|
||||||
|
stub_request(:get, 'https://example.com/actor/hello-world').to_return(body: Oj.dump(unknown_object_json))
|
||||||
subject.perform
|
subject.perform
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'a known status' do
|
||||||
|
let(:object_json) do
|
||||||
|
ActivityPub::TagManager.instance.uri_for(status)
|
||||||
|
end
|
||||||
|
|
||||||
it 'creates a reblog by sender of status' do
|
it 'creates a reblog by sender of status' do
|
||||||
expect(sender.reblogged?(status)).to be true
|
expect(sender.reblogged?(status)).to be true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'an unknown status' do
|
||||||
|
let(:object_json) { 'https://example.com/actor/hello-world' }
|
||||||
|
|
||||||
|
it 'creates a reblog by sender of status' do
|
||||||
|
reblog = sender.statuses.first
|
||||||
|
|
||||||
|
expect(reblog).to_not be_nil
|
||||||
|
expect(reblog.reblog.text).to eq 'Hello world'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'self-boost of a previously unknown status with missing attributedTo' do
|
||||||
|
let(:object_json) do
|
||||||
|
{
|
||||||
|
id: 'https://example.com/actor#bar',
|
||||||
|
type: 'Note',
|
||||||
|
content: 'Lorem ipsum',
|
||||||
|
to: 'http://example.com/followers',
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates a reblog by sender of status' do
|
||||||
|
expect(sender.reblogged?(sender.statuses.first)).to be true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'self-boost of a previously unknown status with correct attributedTo' do
|
||||||
|
let(:object_json) do
|
||||||
|
{
|
||||||
|
id: 'https://example.com/actor#bar',
|
||||||
|
type: 'Note',
|
||||||
|
content: 'Lorem ipsum',
|
||||||
|
attributedTo: 'https://example.com/actor',
|
||||||
|
to: 'http://example.com/followers',
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates a reblog by sender of status' do
|
||||||
|
expect(sender.reblogged?(sender.statuses.first)).to be true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the status belongs to a local user' do
|
||||||
|
before do
|
||||||
|
subject.perform
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:object_json) do
|
||||||
|
ActivityPub::TagManager.instance.uri_for(status)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates a reblog by sender of status' do
|
||||||
|
expect(sender.reblogged?(status)).to be true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the sender is relayed' do
|
||||||
|
let!(:relay_account) { Fabricate(:account, inbox_url: 'https://relay.example.com/inbox') }
|
||||||
|
let!(:relay) { Fabricate(:relay, inbox_url: 'https://relay.example.com/inbox') }
|
||||||
|
|
||||||
|
subject { described_class.new(json, sender, relayed_through_account: relay_account) }
|
||||||
|
|
||||||
|
context 'and the relay is enabled' do
|
||||||
|
before do
|
||||||
|
relay.update(state: :accepted)
|
||||||
|
subject.perform
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:object_json) do
|
||||||
|
{
|
||||||
|
id: 'https://example.com/actor#bar',
|
||||||
|
type: 'Note',
|
||||||
|
content: 'Lorem ipsum',
|
||||||
|
to: 'http://example.com/followers',
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates a reblog by sender of status' do
|
||||||
|
expect(sender.statuses.count).to eq 2
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'and the relay is disabled' do
|
||||||
|
before do
|
||||||
|
subject.perform
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:object_json) do
|
||||||
|
{
|
||||||
|
id: 'https://example.com/actor#bar',
|
||||||
|
type: 'Note',
|
||||||
|
content: 'Lorem ipsum',
|
||||||
|
to: 'http://example.com/followers',
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not create anything' do
|
||||||
|
expect(sender.statuses.count).to eq 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the sender has no relevance to local activity' do
|
||||||
|
before do
|
||||||
|
subject.perform
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:object_json) do
|
||||||
|
{
|
||||||
|
id: 'https://example.com/actor#bar',
|
||||||
|
type: 'Note',
|
||||||
|
content: 'Lorem ipsum',
|
||||||
|
to: 'http://example.com/followers',
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not create anything' do
|
||||||
|
expect(sender.statuses.count).to eq 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -13,8 +13,6 @@ RSpec.describe ActivityPub::Activity::Create do
|
||||||
}.with_indifferent_access
|
}.with_indifferent_access
|
||||||
end
|
end
|
||||||
|
|
||||||
subject { described_class.new(json, sender) }
|
|
||||||
|
|
||||||
before do
|
before do
|
||||||
sender.update(uri: ActivityPub::TagManager.instance.uri_for(sender))
|
sender.update(uri: ActivityPub::TagManager.instance.uri_for(sender))
|
||||||
|
|
||||||
|
@ -23,6 +21,9 @@ RSpec.describe ActivityPub::Activity::Create do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#perform' do
|
describe '#perform' do
|
||||||
|
context 'when fetching' do
|
||||||
|
subject { described_class.new(json, sender) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
subject.perform
|
subject.perform
|
||||||
end
|
end
|
||||||
|
@ -407,4 +408,127 @@ RSpec.describe ActivityPub::Activity::Create do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when sender is followed by local users' do
|
||||||
|
subject { described_class.new(json, sender, delivery: true) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
Fabricate(:account).follow!(sender)
|
||||||
|
subject.perform
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:object_json) do
|
||||||
|
{
|
||||||
|
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||||
|
type: 'Note',
|
||||||
|
content: 'Lorem ipsum',
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates status' do
|
||||||
|
status = sender.statuses.first
|
||||||
|
|
||||||
|
expect(status).to_not be_nil
|
||||||
|
expect(status.text).to eq 'Lorem ipsum'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when sender replies to local status' do
|
||||||
|
let!(:local_status) { Fabricate(:status) }
|
||||||
|
|
||||||
|
subject { described_class.new(json, sender, delivery: true) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
subject.perform
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:object_json) do
|
||||||
|
{
|
||||||
|
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||||
|
type: 'Note',
|
||||||
|
content: 'Lorem ipsum',
|
||||||
|
inReplyTo: ActivityPub::TagManager.instance.uri_for(local_status),
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates status' do
|
||||||
|
status = sender.statuses.first
|
||||||
|
|
||||||
|
expect(status).to_not be_nil
|
||||||
|
expect(status.text).to eq 'Lorem ipsum'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when sender targets a local user' do
|
||||||
|
let!(:local_account) { Fabricate(:account) }
|
||||||
|
|
||||||
|
subject { described_class.new(json, sender, delivery: true) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
subject.perform
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:object_json) do
|
||||||
|
{
|
||||||
|
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||||
|
type: 'Note',
|
||||||
|
content: 'Lorem ipsum',
|
||||||
|
to: ActivityPub::TagManager.instance.uri_for(local_account),
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates status' do
|
||||||
|
status = sender.statuses.first
|
||||||
|
|
||||||
|
expect(status).to_not be_nil
|
||||||
|
expect(status.text).to eq 'Lorem ipsum'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when sender cc\'s a local user' do
|
||||||
|
let!(:local_account) { Fabricate(:account) }
|
||||||
|
|
||||||
|
subject { described_class.new(json, sender, delivery: true) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
subject.perform
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:object_json) do
|
||||||
|
{
|
||||||
|
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||||
|
type: 'Note',
|
||||||
|
content: 'Lorem ipsum',
|
||||||
|
cc: ActivityPub::TagManager.instance.uri_for(local_account),
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates status' do
|
||||||
|
status = sender.statuses.first
|
||||||
|
|
||||||
|
expect(status).to_not be_nil
|
||||||
|
expect(status.text).to eq 'Lorem ipsum'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the sender has no relevance to local activity' do
|
||||||
|
subject { described_class.new(json, sender, delivery: true) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
subject.perform
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:object_json) do
|
||||||
|
{
|
||||||
|
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||||
|
type: 'Note',
|
||||||
|
content: 'Lorem ipsum',
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not create anything' do
|
||||||
|
expect(sender.statuses.count).to eq 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -74,6 +74,7 @@ RSpec.describe Formatter do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'given a URL with a query string' do
|
context 'given a URL with a query string' do
|
||||||
|
context 'with escaped unicode character' do
|
||||||
let(:text) { 'https://www.ruby-toolbox.com/search?utf8=%E2%9C%93&q=autolink' }
|
let(:text) { 'https://www.ruby-toolbox.com/search?utf8=%E2%9C%93&q=autolink' }
|
||||||
|
|
||||||
it 'matches the full URL' do
|
it 'matches the full URL' do
|
||||||
|
@ -81,6 +82,31 @@ RSpec.describe Formatter do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'with unicode character' do
|
||||||
|
let(:text) { 'https://www.ruby-toolbox.com/search?utf8=✓&q=autolink' }
|
||||||
|
|
||||||
|
it 'matches the full URL' do
|
||||||
|
is_expected.to include 'href="https://www.ruby-toolbox.com/search?utf8=✓&q=autolink"'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with unicode character at the end' do
|
||||||
|
let(:text) { 'https://www.ruby-toolbox.com/search?utf8=✓' }
|
||||||
|
|
||||||
|
it 'matches the full URL' do
|
||||||
|
is_expected.to include 'href="https://www.ruby-toolbox.com/search?utf8=✓"'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with escaped and not escaped unicode characters' do
|
||||||
|
let(:text) { 'https://www.ruby-toolbox.com/search?utf8=%E2%9C%93&utf81=✓&q=autolink' }
|
||||||
|
|
||||||
|
it 'preserves escaped unicode characters' do
|
||||||
|
is_expected.to include 'href="https://www.ruby-toolbox.com/search?utf8=%E2%9C%93&utf81=✓&q=autolink"'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'given a URL with parentheses in it' do
|
context 'given a URL with parentheses in it' do
|
||||||
let(:text) { 'https://en.wikipedia.org/wiki/Diaspora_(software)' }
|
let(:text) { 'https://en.wikipedia.org/wiki/Diaspora_(software)' }
|
||||||
|
|
||||||
|
@ -89,6 +115,22 @@ RSpec.describe Formatter do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'given a URL in quotation marks' do
|
||||||
|
let(:text) { '"https://example.com/"' }
|
||||||
|
|
||||||
|
it 'does not match the quotation marks' do
|
||||||
|
is_expected.to include 'href="https://example.com/"'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'given a URL in angle brackets' do
|
||||||
|
let(:text) { '<https://example.com/>' }
|
||||||
|
|
||||||
|
it 'does not match the angle brackets' do
|
||||||
|
is_expected.to include 'href="https://example.com/"'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'given a URL with Japanese path string' do
|
context 'given a URL with Japanese path string' do
|
||||||
let(:text) { 'https://ja.wikipedia.org/wiki/日本' }
|
let(:text) { 'https://ja.wikipedia.org/wiki/日本' }
|
||||||
|
|
||||||
|
@ -105,6 +147,22 @@ RSpec.describe Formatter do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'given a URL with a full-width space' do
|
||||||
|
let(:text) { 'https://example.com/ abc123' }
|
||||||
|
|
||||||
|
it 'does not match the full-width space' do
|
||||||
|
is_expected.to include 'href="https://example.com/"'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'given a URL in Japanese quotation marks' do
|
||||||
|
let(:text) { '「[https://example.org/」' }
|
||||||
|
|
||||||
|
it 'does not match the quotation marks' do
|
||||||
|
is_expected.to include 'href="https://example.org/"'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'given a URL with Simplified Chinese path string' do
|
context 'given a URL with Simplified Chinese path string' do
|
||||||
let(:text) { 'https://baike.baidu.com/item/中华人民共和国' }
|
let(:text) { 'https://baike.baidu.com/item/中华人民共和国' }
|
||||||
|
|
||||||
|
@ -124,7 +182,11 @@ RSpec.describe Formatter do
|
||||||
context 'given a URL containing unsafe code (XSS attack, visible part)' do
|
context 'given a URL containing unsafe code (XSS attack, visible part)' do
|
||||||
let(:text) { %q{http://example.com/b<del>b</del>} }
|
let(:text) { %q{http://example.com/b<del>b</del>} }
|
||||||
|
|
||||||
it 'escapes the HTML in the URL' do
|
it 'does not include the HTML in the URL' do
|
||||||
|
is_expected.to include '"http://example.com/b"'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'escapes the HTML' do
|
||||||
is_expected.to include '<del>b</del>'
|
is_expected.to include '<del>b</del>'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -132,7 +194,11 @@ RSpec.describe Formatter do
|
||||||
context 'given a URL containing unsafe code (XSS attack, invisible part)' do
|
context 'given a URL containing unsafe code (XSS attack, invisible part)' do
|
||||||
let(:text) { %q{http://example.com/blahblahblahblah/a<script>alert("Hello")</script>} }
|
let(:text) { %q{http://example.com/blahblahblahblah/a<script>alert("Hello")</script>} }
|
||||||
|
|
||||||
it 'escapes the HTML in the URL' do
|
it 'does not include the HTML in the URL' do
|
||||||
|
is_expected.to include '"http://example.com/blahblahblahblah/a"'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'escapes the HTML' do
|
||||||
is_expected.to include '<script>alert("Hello")</script>'
|
is_expected.to include '<script>alert("Hello")</script>'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -168,6 +234,14 @@ RSpec.describe Formatter do
|
||||||
is_expected.to include '/tags/hashtag" class="mention hashtag" rel="tag">#<span>hashtag</span></a>'
|
is_expected.to include '/tags/hashtag" class="mention hashtag" rel="tag">#<span>hashtag</span></a>'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'given text containing a hashtag with Unicode chars' do
|
||||||
|
let(:text) { '#hashtagタグ' }
|
||||||
|
|
||||||
|
it 'creates a hashtag link' do
|
||||||
|
is_expected.to include '/tags/hashtag%E3%82%BF%E3%82%B0" class="mention hashtag" rel="tag">#<span>hashtagタグ</span></a>'
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#format_spoiler' do
|
describe '#format_spoiler' do
|
||||||
|
|
|
@ -11,6 +11,7 @@ describe EmailMxValidator do
|
||||||
|
|
||||||
allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::MX).and_return([])
|
allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::MX).and_return([])
|
||||||
allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::A).and_return([])
|
allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::A).and_return([])
|
||||||
|
allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::AAAA).and_return([])
|
||||||
allow(resolver).to receive(:timeouts=).and_return(nil)
|
allow(resolver).to receive(:timeouts=).and_return(nil)
|
||||||
allow(Resolv::DNS).to receive(:open).and_yield(resolver)
|
allow(Resolv::DNS).to receive(:open).and_yield(resolver)
|
||||||
|
|
||||||
|
@ -23,7 +24,9 @@ describe EmailMxValidator do
|
||||||
|
|
||||||
allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::MX).and_return([double(exchange: 'mail.example.com')])
|
allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::MX).and_return([double(exchange: 'mail.example.com')])
|
||||||
allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::A).and_return([])
|
allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::A).and_return([])
|
||||||
|
allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::AAAA).and_return([])
|
||||||
allow(resolver).to receive(:getresources).with('mail.example.com', Resolv::DNS::Resource::IN::A).and_return([])
|
allow(resolver).to receive(:getresources).with('mail.example.com', Resolv::DNS::Resource::IN::A).and_return([])
|
||||||
|
allow(resolver).to receive(:getresources).with('mail.example.com', Resolv::DNS::Resource::IN::AAAA).and_return([])
|
||||||
allow(resolver).to receive(:timeouts=).and_return(nil)
|
allow(resolver).to receive(:timeouts=).and_return(nil)
|
||||||
allow(Resolv::DNS).to receive(:open).and_yield(resolver)
|
allow(Resolv::DNS).to receive(:open).and_yield(resolver)
|
||||||
|
|
||||||
|
@ -37,6 +40,21 @@ describe EmailMxValidator do
|
||||||
|
|
||||||
allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::MX).and_return([])
|
allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::MX).and_return([])
|
||||||
allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::A).and_return([double(address: '1.2.3.4')])
|
allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::A).and_return([double(address: '1.2.3.4')])
|
||||||
|
allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::AAAA).and_return([])
|
||||||
|
allow(resolver).to receive(:timeouts=).and_return(nil)
|
||||||
|
allow(Resolv::DNS).to receive(:open).and_yield(resolver)
|
||||||
|
|
||||||
|
subject.validate(user)
|
||||||
|
expect(user.errors).to have_received(:add)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'adds an error if the AAAA record is blacklisted' do
|
||||||
|
EmailDomainBlock.create!(domain: 'fd00::1')
|
||||||
|
resolver = double
|
||||||
|
|
||||||
|
allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::MX).and_return([])
|
||||||
|
allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::A).and_return([])
|
||||||
|
allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::AAAA).and_return([double(address: 'fd00::1')])
|
||||||
allow(resolver).to receive(:timeouts=).and_return(nil)
|
allow(resolver).to receive(:timeouts=).and_return(nil)
|
||||||
allow(Resolv::DNS).to receive(:open).and_yield(resolver)
|
allow(Resolv::DNS).to receive(:open).and_yield(resolver)
|
||||||
|
|
||||||
|
@ -50,7 +68,25 @@ describe EmailMxValidator do
|
||||||
|
|
||||||
allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::MX).and_return([double(exchange: 'mail.example.com')])
|
allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::MX).and_return([double(exchange: 'mail.example.com')])
|
||||||
allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::A).and_return([])
|
allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::A).and_return([])
|
||||||
|
allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::AAAA).and_return([])
|
||||||
allow(resolver).to receive(:getresources).with('mail.example.com', Resolv::DNS::Resource::IN::A).and_return([double(address: '2.3.4.5')])
|
allow(resolver).to receive(:getresources).with('mail.example.com', Resolv::DNS::Resource::IN::A).and_return([double(address: '2.3.4.5')])
|
||||||
|
allow(resolver).to receive(:getresources).with('mail.example.com', Resolv::DNS::Resource::IN::AAAA).and_return([])
|
||||||
|
allow(resolver).to receive(:timeouts=).and_return(nil)
|
||||||
|
allow(Resolv::DNS).to receive(:open).and_yield(resolver)
|
||||||
|
|
||||||
|
subject.validate(user)
|
||||||
|
expect(user.errors).to have_received(:add)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'adds an error if the MX IPv6 record is blacklisted' do
|
||||||
|
EmailDomainBlock.create!(domain: 'fd00::2')
|
||||||
|
resolver = double
|
||||||
|
|
||||||
|
allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::MX).and_return([double(exchange: 'mail.example.com')])
|
||||||
|
allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::A).and_return([])
|
||||||
|
allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::AAAA).and_return([])
|
||||||
|
allow(resolver).to receive(:getresources).with('mail.example.com', Resolv::DNS::Resource::IN::A).and_return([])
|
||||||
|
allow(resolver).to receive(:getresources).with('mail.example.com', Resolv::DNS::Resource::IN::AAAA).and_return([double(address: 'fd00::2')])
|
||||||
allow(resolver).to receive(:timeouts=).and_return(nil)
|
allow(resolver).to receive(:timeouts=).and_return(nil)
|
||||||
allow(Resolv::DNS).to receive(:open).and_yield(resolver)
|
allow(Resolv::DNS).to receive(:open).and_yield(resolver)
|
||||||
|
|
||||||
|
@ -64,7 +100,9 @@ describe EmailMxValidator do
|
||||||
|
|
||||||
allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::MX).and_return([double(exchange: 'mail.example.com')])
|
allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::MX).and_return([double(exchange: 'mail.example.com')])
|
||||||
allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::A).and_return([])
|
allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::A).and_return([])
|
||||||
|
allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::AAAA).and_return([])
|
||||||
allow(resolver).to receive(:getresources).with('mail.example.com', Resolv::DNS::Resource::IN::A).and_return([double(address: '2.3.4.5')])
|
allow(resolver).to receive(:getresources).with('mail.example.com', Resolv::DNS::Resource::IN::A).and_return([double(address: '2.3.4.5')])
|
||||||
|
allow(resolver).to receive(:getresources).with('mail.example.com', Resolv::DNS::Resource::IN::AAAA).and_return([double(address: 'fd00::2')])
|
||||||
allow(resolver).to receive(:timeouts=).and_return(nil)
|
allow(resolver).to receive(:timeouts=).and_return(nil)
|
||||||
allow(Resolv::DNS).to receive(:open).and_yield(resolver)
|
allow(Resolv::DNS).to receive(:open).and_yield(resolver)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue