Merge tag 'v2.6.0rc1' into instance_only_statuses

This commit is contained in:
Renato "Lond" Cerqueira 2018-10-23 08:32:55 +02:00
commit fde9668bae
570 changed files with 11506 additions and 5693 deletions

View File

@ -1,66 +0,0 @@
{
"presets": [
"react",
[
"env",
{
"exclude": ["transform-async-to-generator", "transform-regenerator"],
"loose": true,
"modules": false,
"targets": {
"browsers": ["last 2 versions", "IE >= 11", "iOS >= 9"]
}
}
]
],
"plugins": [
"syntax-dynamic-import",
["transform-object-rest-spread", { "useBuiltIns": true }],
"transform-decorators-legacy",
"transform-class-properties",
[
"react-intl",
{
"messagesDir": "./build/messages"
}
],
"preval"
],
"env": {
"development": {
"plugins": [
"transform-react-jsx-source",
"transform-react-jsx-self"
]
},
"production": {
"plugins": [
"lodash",
[
"transform-react-remove-prop-types",
{
"mode": "remove",
"removeImport": true,
"additionalLibraries": [
"react-immutable-proptypes"
]
}
],
"transform-react-inline-elements",
[
"transform-runtime",
{
"helpers": true,
"polyfill": false,
"regenerator": false
}
]
]
},
"test": {
"plugins": [
"transform-es2015-modules-commonjs"
]
}
}
}

View File

@ -13,6 +13,9 @@ aliases:
ALLOW_NOPAM: true ALLOW_NOPAM: true
CONTINUOUS_INTEGRATION: true CONTINUOUS_INTEGRATION: true
DISABLE_SIMPLECOV: true DISABLE_SIMPLECOV: true
PAM_ENABLED: true
PAM_DEFAULT_SERVICE: pam_test
PAM_CONTROLLED_SERVICE: pam_test_controlled
working_directory: ~/projects/mastodon/ working_directory: ~/projects/mastodon/
- &attach_workspace - &attach_workspace

View File

@ -3,7 +3,3 @@ NODE_ENV=test
# Federation # Federation
LOCAL_DOMAIN=cb6e6126.ngrok.io LOCAL_DOMAIN=cb6e6126.ngrok.io
LOCAL_HTTPS=true LOCAL_HTTPS=true
# test pam authentication
PAM_ENABLED=true
PAM_DEFAULT_SERVICE=pam_test
PAM_CONTROLLED_SERVICE=pam_test_controlled

View File

@ -1,2 +1,2 @@
VAGRANT=true VAGRANT=true
LOCAL_DOMAIN=mastodon.dev LOCAL_DOMAIN=mastodon.local

View File

@ -1,12 +1,27 @@
--- ---
name: Bug Report name: Bug Report
about: Create a report to help us improve about: If something isn't working as expected
--- ---
[Issue text goes here]. <!-- Make sure that you are submitting a new bug that was not previously reported or already fixed -->
* * * * <!-- Please use a concise and distinct title for the issue -->
- [ ] I searched or browsed the repos other issues to ensure this is not a duplicate. ### Expected behaviour
- [ ] This bug happens on a [tagged release](https://github.com/tootsuite/mastodon/releases) and not on `master` (If you're a user, don't worry about this).
<!-- What should have happened? -->
### Actual behaviour
<!-- What happened? -->
### Steps to reproduce the problem
<!-- What were you trying to do? -->
### Specifications
<!-- What version or commit hash of Mastodon did you find this bug in? -->
<!-- If a front-end issue, what browser and operating systems were you using? -->

View File

@ -1,11 +1,17 @@
--- ---
name: Feature Request name: Feature Request
about: Suggest an idea for this project about: I have a suggestion
--- ---
[Issue text goes here]. <!-- Please use a concise and distinct title for the issue -->
* * * * <!-- Consider: Could it be implemented as a 3rd party app using the REST API instead? -->
- [ ] I searched or browsed the repos other issues to ensure this is not a duplicate. ### Pitch
<!-- Describe your idea for a feature. Make sure it has not already been suggested/implemented/turned down before -->
### Motivation
<!-- Why do you think this feature is needed? Who would benefit from it? -->

10
.github/ISSUE_TEMPLATE/support.md vendored Normal file
View File

@ -0,0 +1,10 @@
---
name: Support
about: Ask for help with your deployment
---
We primarily use GitHub as a bug and feature tracker. For usage questions, troubleshooting of deployments and other individual technical assistance, please use one of the resources below:
- https://discourse.joinmastodon.org
- #mastodon on irc.freenode.net

View File

@ -77,6 +77,11 @@ Rails/SkipsModelValidations:
Rails/HttpStatus: Rails/HttpStatus:
Enabled: false Enabled: false
Rails/Exit:
Exclude:
- 'lib/mastodon/*'
- 'lib/cli'
Style/ClassAndModuleChildren: Style/ClassAndModuleChildren:
Enabled: false Enabled: false

View File

@ -1 +1 @@
2.5.1 2.5.3

View File

@ -3,76 +3,85 @@ and provided thanks to the work of the following contributors:
* [Gargron](https://github.com/Gargron) * [Gargron](https://github.com/Gargron)
* [ykzts](https://github.com/ykzts) * [ykzts](https://github.com/ykzts)
* [mjankowski](https://github.com/mjankowski)
* [akihikodaki](https://github.com/akihikodaki) * [akihikodaki](https://github.com/akihikodaki)
* [mjankowski](https://github.com/mjankowski)
* [ThibG](https://github.com/ThibG)
* [unarist](https://github.com/unarist) * [unarist](https://github.com/unarist)
* [yiskah](https://github.com/yiskah)
* [m4sk1n](https://github.com/m4sk1n) * [m4sk1n](https://github.com/m4sk1n)
* [yiskah](https://github.com/yiskah)
* [nolanlawson](https://github.com/nolanlawson) * [nolanlawson](https://github.com/nolanlawson)
* [sorin-davidoi](https://github.com/sorin-davidoi) * [sorin-davidoi](https://github.com/sorin-davidoi)
* [abcang](https://github.com/abcang) * [abcang](https://github.com/abcang)
* [ThibG](https://github.com/ThibG)
* [lynlynlynx](https://github.com/lynlynlynx) * [lynlynlynx](https://github.com/lynlynlynx)
* [dependabot[bot]](https://github.com/apps/dependabot)
* [alpaca-tc](https://github.com/alpaca-tc) * [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)
* [renatolond](https://github.com/renatolond)
* [jeroenpraat](https://github.com/jeroenpraat) * [jeroenpraat](https://github.com/jeroenpraat)
* [mayaeh](https://github.com/mayaeh)
* [blackle](https://github.com/blackle) * [blackle](https://github.com/blackle)
* [Quent-in](https://github.com/Quent-in) * [Quent-in](https://github.com/Quent-in)
* [JantsoP](https://github.com/JantsoP) * [JantsoP](https://github.com/JantsoP)
* [nullkal](https://github.com/nullkal) * [nullkal](https://github.com/nullkal)
* [yookoala](https://github.com/yookoala) * [yookoala](https://github.com/yookoala)
* [ysksn](https://github.com/ysksn) * [ysksn](https://github.com/ysksn)
* [shuheiktgw](https://github.com/shuheiktgw)
* [ashfurrow](https://github.com/ashfurrow) * [ashfurrow](https://github.com/ashfurrow)
* [eramdam](https://github.com/eramdam) * [mabkenar](https://github.com/mabkenar)
* [mayaeh](https://github.com/mayaeh)
* [zunda](https://github.com/zunda) * [zunda](https://github.com/zunda)
* [ticky](https://github.com/ticky) * [Kjwon15](https://github.com/Kjwon15)
* [eramdam](https://github.com/eramdam)
* [masarakki](https://github.com/masarakki) * [masarakki](https://github.com/masarakki)
* [ticky](https://github.com/ticky)
* [takayamaki](https://github.com/takayamaki)
* [Quenty31](https://github.com/Quenty31)
* [danhunsaker](https://github.com/danhunsaker)
* [ThisIsMissEm](https://github.com/ThisIsMissEm)
* [hcmiya](https://github.com/hcmiya)
* [stephenburgess8](https://github.com/stephenburgess8)
* [Wonderfall](https://github.com/Wonderfall) * [Wonderfall](https://github.com/Wonderfall)
* [matteoaquila](https://github.com/matteoaquila) * [matteoaquila](https://github.com/matteoaquila)
* [rkarabut](https://github.com/rkarabut) * [rkarabut](https://github.com/rkarabut)
* [stephenburgess8](https://github.com/stephenburgess8)
* [Kjwon15](https://github.com/Kjwon15)
* [Artoria2e5](https://github.com/Artoria2e5)
* [yukimochi](https://github.com/yukimochi) * [yukimochi](https://github.com/yukimochi)
* [Artoria2e5](https://github.com/Artoria2e5)
* [marrus-sh](https://github.com/marrus-sh) * [marrus-sh](https://github.com/marrus-sh)
* [krainboltgreene](https://github.com/krainboltgreene) * [krainboltgreene](https://github.com/krainboltgreene)
* [renatolond](https://github.com/renatolond) * [patf](https://github.com/patf)
* [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)
* [danhunsaker](https://github.com/danhunsaker) * [dracos](https://github.com/dracos)
* [patf](https://github.com/patf) * [SerCom_KC](mailto:sercom-kc@users.noreply.github.com)
* [Quenty31](https://github.com/Quenty31) * [Sylvhem](https://github.com/Sylvhem)
* [MitarashiDango](https://github.com/MitarashiDango) * [nightpool](https://github.com/nightpool)
* [Aldarone](https://github.com/Aldarone) * [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)
* [takayamaki](https://github.com/takayamaki) * [MaciekBaron](https://github.com/MaciekBaron)
* [MitarashiDango](mailto:mitarashidango@users.noreply.github.com)
* [beatrix-bitrot](https://github.com/beatrix-bitrot)
* [adbelle](https://github.com/adbelle) * [adbelle](https://github.com/adbelle)
* [evanminto](https://github.com/evanminto) * [evanminto](https://github.com/evanminto)
* [mabkenar](https://github.com/mabkenar)
* [MightyPork](https://github.com/MightyPork) * [MightyPork](https://github.com/MightyPork)
* [beatrix-bitrot](https://github.com/beatrix-bitrot)
* [yhirano55](https://github.com/yhirano55) * [yhirano55](https://github.com/yhirano55)
* [camponez](https://github.com/camponez) * [camponez](https://github.com/camponez)
* [SerCom-KC](https://github.com/SerCom-KC)
* [aschmitz](https://github.com/aschmitz) * [aschmitz](https://github.com/aschmitz)
* [devkral](https://github.com/devkral)
* [fpiesche](https://github.com/fpiesche) * [fpiesche](https://github.com/fpiesche)
* [gandaro](https://github.com/gandaro) * [gandaro](https://github.com/gandaro)
* [johnsudaar](https://github.com/johnsudaar) * [johnsudaar](https://github.com/johnsudaar)
* [trebmuh](https://github.com/trebmuh) * [trebmuh](https://github.com/trebmuh)
* [Sylvhem](https://github.com/Sylvhem) * [Rakib Hasan](mailto:rmhasan@gmail.com)
* [lindwurm](https://github.com/lindwurm) * [lindwurm](https://github.com/lindwurm)
* [victorhck](mailto:victorhck@geeko.site)
* [voidsatisfaction](https://github.com/voidsatisfaction) * [voidsatisfaction](https://github.com/voidsatisfaction)
* [neetshin](https://github.com/neetshin)
* [valentin2105](https://github.com/valentin2105)
* [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)
* [seefood](https://github.com/seefood) * [seefood](https://github.com/seefood)
* [jackjennings](https://github.com/jackjennings) * [jackjennings](https://github.com/jackjennings)
* [hcmiya](https://github.com/hcmiya) * [spla](mailto:spla@mastodont.cat)
* [nightpool](https://github.com/nightpool)
* [salvadorpla](https://github.com/salvadorpla)
* [expenses](https://github.com/expenses) * [expenses](https://github.com/expenses)
* [walf443](https://github.com/walf443) * [walf443](https://github.com/walf443)
* [JoelQ](https://github.com/JoelQ) * [JoelQ](https://github.com/JoelQ)
@ -84,74 +93,92 @@ and provided thanks to the work of the following contributors:
* [tsuwatch](https://github.com/tsuwatch) * [tsuwatch](https://github.com/tsuwatch)
* [victorhck](https://github.com/victorhck) * [victorhck](https://github.com/victorhck)
* [puckipedia](https://github.com/puckipedia) * [puckipedia](https://github.com/puckipedia)
* [fvh-P](https://github.com/fvh-P)
* [contraexemplo](https://github.com/contraexemplo) * [contraexemplo](https://github.com/contraexemplo)
* [hugogameiro](https://github.com/hugogameiro)
* [kazu9su](https://github.com/kazu9su) * [kazu9su](https://github.com/kazu9su)
* [Komic](https://github.com/Komic) * [Komic](https://github.com/Komic)
* [diomed](https://github.com/diomed) * [diomed](https://github.com/diomed)
* [ariasuni](https://github.com/ariasuni)
* [Neetshin](mailto:neetshin@neetsh.in)
* [rainyday](https://github.com/rainyday) * [rainyday](https://github.com/rainyday)
* [ProgVal](https://github.com/ProgVal)
* [valentin2105](https://github.com/valentin2105)
* [yuntan](https://github.com/yuntan)
* [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)
* [ProgVal](https://github.com/ProgVal) * [rtucker](https://github.com/rtucker)
* [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)
* [AA4ch1](https://github.com/AA4ch1) * [AA4ch1](https://github.com/AA4ch1)
* [alexgleason](https://github.com/alexgleason) * [alexgleason](https://github.com/alexgleason)
* [cpytel](https://github.com/cpytel) * [cpytel](https://github.com/cpytel)
* [northerner](https://github.com/northerner) * [northerner](https://github.com/northerner)
* [fhemberger](https://github.com/fhemberger)
* [greysteil](https://github.com/greysteil)
* [hnrysmth](https://github.com/hnrysmth) * [hnrysmth](https://github.com/hnrysmth)
* [hugogameiro](https://github.com/hugogameiro) * [d6rkaiz](https://github.com/d6rkaiz)
* [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)
* [rtucker](https://github.com/rtucker)
* [reneklacan](https://github.com/reneklacan) * [reneklacan](https://github.com/reneklacan)
* [KScl](https://github.com/KScl) * [ekiru](https://github.com/ekiru)
* [SerCom-KC](https://github.com/SerCom-KC)
* [tcitworld](https://github.com/tcitworld) * [tcitworld](https://github.com/tcitworld)
* [ashleyhull-versent](https://github.com/ashleyhull-versent)
* [geta6](https://github.com/geta6) * [geta6](https://github.com/geta6)
* [goofy-bz](https://github.com/goofy-bz)
* [happycoloredbanana](https://github.com/happycoloredbanana) * [happycoloredbanana](https://github.com/happycoloredbanana)
* [leopku](https://github.com/leopku) * [leopku](https://github.com/leopku)
* [SansPseudoFix](https://github.com/SansPseudoFix) * [SansPseudoFix](https://github.com/SansPseudoFix)
* [tomfhowe](https://github.com/tomfhowe) * [tomfhowe](https://github.com/tomfhowe)
* [noraworld](https://github.com/noraworld) * [noraworld](https://github.com/noraworld)
* [fvh-P](https://github.com/fvh-P) * [theboss](https://github.com/theboss)
* [178inaba](https://github.com/178inaba) * [178inaba](https://github.com/178inaba)
* [devkral](https://github.com/devkral)
* [alyssais](https://github.com/alyssais) * [alyssais](https://github.com/alyssais)
* [kodnaplakal](https://github.com/kodnaplakal) * [kodnaplakal](https://github.com/kodnaplakal)
* [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)
* [fhemberger](https://github.com/fhemberger)
* [halkeye](https://github.com/halkeye) * [halkeye](https://github.com/halkeye)
* [hinaloe](https://github.com/hinaloe)
* [treby](https://github.com/treby) * [treby](https://github.com/treby)
* [d6rkaiz](https://github.com/d6rkaiz) * [Reverite](https://github.com/Reverite)
* [jpdevries](https://github.com/jpdevries) * [jpdevries](https://github.com/jpdevries)
* [rndm-stranger](https://github.com/rndm-stranger) * [00x9d](https://github.com/00x9d)
* [Kurtis Rainbolt-Greene](mailto:me@kurtisrainboltgreene.name)
* [saper](https://github.com/saper) * [saper](https://github.com/saper)
* [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)
* [ramlmn](https://github.com/ramlmn) * [qguv](https://github.com/qguv)
* [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)
* [Technowix](mailto:technowix@users.noreply.github.com)
* [Eychics](https://github.com/Eychics) * [Eychics](https://github.com/Eychics)
* [thor-the-norseman](https://github.com/thor-the-norseman) * [Thor Harald Johansen](mailto:thj@thj.no)
* [0x70b1a5](https://github.com/0x70b1a5) * [0x70b1a5](https://github.com/0x70b1a5)
* [gled-rs](https://github.com/gled-rs) * [gled-rs](https://github.com/gled-rs)
* [Valentin_NC](mailto:valentin.ouvrard@nautile.sarl)
* [R0ckweb](https://github.com/R0ckweb) * [R0ckweb](https://github.com/R0ckweb)
* [caasi](https://github.com/caasi)
* [esetomo](https://github.com/esetomo) * [esetomo](https://github.com/esetomo)
* [foxiehkins](https://github.com/foxiehkins) * [foxiehkins](https://github.com/foxiehkins)
* [sdukhovni](https://github.com/sdukhovni) * [hoodie](mailto:hoodiekitten@outlook.com)
* [luzi82](https://github.com/luzi82)
* [duxovni](https://github.com/duxovni)
* [unsmell](https://github.com/unsmell) * [unsmell](https://github.com/unsmell)
* [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)
* [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)
* [Andrew](mailto:andrewlchronister@gmail.com)
* [estuans](https://github.com/estuans) * [estuans](https://github.com/estuans)
* [dissolve](https://github.com/dissolve) * [dissolve](https://github.com/dissolve)
* [PurpleBooth](https://github.com/PurpleBooth) * [PurpleBooth](https://github.com/PurpleBooth)
@ -165,36 +192,44 @@ and provided thanks to the work of the following contributors:
* [farlistener](https://github.com/farlistener) * [farlistener](https://github.com/farlistener)
* [DavidLibeau](https://github.com/DavidLibeau) * [DavidLibeau](https://github.com/DavidLibeau)
* [SirCmpwn](https://github.com/SirCmpwn) * [SirCmpwn](https://github.com/SirCmpwn)
* [MasterGroosha](https://github.com/MasterGroosha)
* [Fjoerfoks](https://github.com/Fjoerfoks) * [Fjoerfoks](https://github.com/Fjoerfoks)
* [fmauNeko](https://github.com/fmauNeko) * [fmauNeko](https://github.com/fmauNeko)
* [gloaec](https://github.com/gloaec) * [gloaec](https://github.com/gloaec)
* [greysteil](https://github.com/greysteil)
* [unstabler](https://github.com/unstabler) * [unstabler](https://github.com/unstabler)
* [potato4d](https://github.com/potato4d) * [potato4d](https://github.com/potato4d)
* [h-izumi](https://github.com/h-izumi) * [h-izumi](https://github.com/h-izumi)
* [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)
* [foozmeat](https://github.com/foozmeat) * [foozmeat](https://github.com/foozmeat)
* [jasonrhodes](https://github.com/jasonrhodes) * [jasonrhodes](https://github.com/jasonrhodes)
* [asm](https://github.com/asm) * [Jason Snell](mailto:jason@newrelic.com)
* [jviide](https://github.com/jviide) * [jviide](https://github.com/jviide)
* [crakaC](https://github.com/crakaC) * [crakaC](https://github.com/crakaC)
* [tkbky](https://github.com/tkbky) * [tkbky](https://github.com/tkbky)
* [Kaylee](mailto:kaylee@codethat.sucks)
* [Kazhnuz](https://github.com/Kazhnuz) * [Kazhnuz](https://github.com/Kazhnuz)
* [connyduck](https://github.com/connyduck)
* [Lindsey Bieda](mailto:lindseyb@users.noreply.github.com)
* [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)
* [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)
* [xPaw](https://github.com/xPaw) * [xPaw](https://github.com/xPaw)
* [petzah](https://github.com/petzah)
* [ignisf](https://github.com/ignisf)
* [raymestalez](https://github.com/raymestalez) * [raymestalez](https://github.com/raymestalez)
* [u1-liquid](https://github.com/u1-liquid)
* [sim6](https://github.com/sim6) * [sim6](https://github.com/sim6)
* [ekiru](https://github.com/ekiru) * [stemid](https://github.com/stemid)
* [Technowix](https://github.com/Technowix)
* [ThomasLeister](https://github.com/ThomasLeister) * [ThomasLeister](https://github.com/ThomasLeister)
* [mcat-ee](https://github.com/mcat-ee) * [mcat-ee](https://github.com/mcat-ee)
* [tototoshi](https://github.com/tototoshi) * [tototoshi](https://github.com/tototoshi)
* [TrashMacNugget](https://github.com/TrashMacNugget)
* [VirtuBox](https://github.com/VirtuBox) * [VirtuBox](https://github.com/VirtuBox)
* [Vladyslav](mailto:vaden@tuta.io)
* [kaniini](https://github.com/kaniini) * [kaniini](https://github.com/kaniini)
* [vayan](https://github.com/vayan) * [vayan](https://github.com/vayan)
* [yannicka](https://github.com/yannicka) * [yannicka](https://github.com/yannicka)
@ -202,12 +237,18 @@ and provided thanks to the work of the following contributors:
* [zacanger](https://github.com/zacanger) * [zacanger](https://github.com/zacanger)
* [amazedkoumei](https://github.com/amazedkoumei) * [amazedkoumei](https://github.com/amazedkoumei)
* [anon5r](https://github.com/anon5r) * [anon5r](https://github.com/anon5r)
* [aus-social](https://github.com/aus-social)
* [imbsky](https://github.com/imbsky)
* [bsky](mailto:me@imbsky.net)
* [chr-1x](https://github.com/chr-1x)
* [codl](https://github.com/codl) * [codl](https://github.com/codl)
* [cpsdqs](https://github.com/cpsdqs)
* [barzamin](https://github.com/barzamin) * [barzamin](https://github.com/barzamin)
* [fhalna](https://github.com/fhalna) * [fhalna](https://github.com/fhalna)
* [haoyayoi](https://github.com/haoyayoi) * [haoyayoi](https://github.com/haoyayoi)
* [ik11235](https://github.com/ik11235) * [ik11235](https://github.com/ik11235)
* [kawax](https://github.com/kawax) * [kawax](https://github.com/kawax)
* [kedamaDQ](https://github.com/kedamaDQ)
* [007lva](https://github.com/007lva) * [007lva](https://github.com/007lva)
* [matsurai25](https://github.com/matsurai25) * [matsurai25](https://github.com/matsurai25)
* [mecab](https://github.com/mecab) * [mecab](https://github.com/mecab)
@ -215,32 +256,42 @@ and provided thanks to the work of the following contributors:
* [oliverkeeble](https://github.com/oliverkeeble) * [oliverkeeble](https://github.com/oliverkeeble)
* [pinfort](https://github.com/pinfort) * [pinfort](https://github.com/pinfort)
* [rbaumert](https://github.com/rbaumert) * [rbaumert](https://github.com/rbaumert)
* [rhoio](https://github.com/rhoio)
* [trwnh](https://github.com/trwnh)
* [usagi-f](https://github.com/usagi-f) * [usagi-f](https://github.com/usagi-f)
* [vidarlee](https://github.com/vidarlee) * [vidarlee](https://github.com/vidarlee)
* [vjackson725](https://github.com/vjackson725) * [vjackson725](https://github.com/vjackson725)
* [wxcafe](https://github.com/wxcafe) * [wxcafe](https://github.com/wxcafe)
* [rinsuki](https://github.com/rinsuki) * [新都心(Neet Shin)](mailto:nucx@dio-vox.com)
* [cygnan](https://github.com/cygnan) * [cygnan](https://github.com/cygnan)
* [Awea](https://github.com/Awea) * [Awea](https://github.com/Awea)
* [halcy](https://github.com/halcy) * [halcy](https://github.com/halcy)
* [bounshi](https://github.com/bounshi) * [naaaaaaaaaaaf](https://github.com/naaaaaaaaaaaf)
* [NecroTechno](https://github.com/NecroTechno)
* [8398a7](https://github.com/8398a7) * [8398a7](https://github.com/8398a7)
* [857b](https://github.com/857b) * [857b](https://github.com/857b)
* [insom](https://github.com/insom)
* [Aditoo17](https://github.com/Aditoo17)
* [unascribed](https://github.com/unascribed) * [unascribed](https://github.com/unascribed)
* [Aguay-val](https://github.com/Aguay-val) * [Aguay-val](https://github.com/Aguay-val)
* [knu](https://github.com/knu) * [knu](https://github.com/knu)
* [h3poteto](https://github.com/h3poteto)
* [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) * [pointlessone](https://github.com/pointlessone)
* [a2](https://github.com/a2) * [a2](https://github.com/a2)
* [0xa](https://github.com/0xa) * [0xa](https://github.com/0xa)
* [palindromordnilap](https://github.com/palindromordnilap)
* [virtualpain](https://github.com/virtualpain) * [virtualpain](https://github.com/virtualpain)
* [sapphirus](https://github.com/sapphirus) * [sapphirus](https://github.com/sapphirus)
* [amandavisconti](https://github.com/amandavisconti) * [amandavisconti](https://github.com/amandavisconti)
* [ameliavoncat](https://github.com/ameliavoncat) * [ameliavoncat](https://github.com/ameliavoncat)
* [ilpianista](https://github.com/ilpianista) * [ilpianista](https://github.com/ilpianista)
* [andydrop](https://github.com/andydrop) * [Andreas Drop](mailto:andy@remline.de)
* [andi1984](https://github.com/andi1984)
* [schas002](https://github.com/schas002) * [schas002](https://github.com/schas002)
* [abackstrom](https://github.com/abackstrom)
* [jumbosushi](https://github.com/jumbosushi) * [jumbosushi](https://github.com/jumbosushi)
* [ayumin](https://github.com/ayumin) * [ayumin](https://github.com/ayumin)
* [BaptisteGelez](https://github.com/BaptisteGelez) * [BaptisteGelez](https://github.com/BaptisteGelez)
@ -251,6 +302,7 @@ and provided thanks to the work of the following contributors:
* [brycied00d](https://github.com/brycied00d) * [brycied00d](https://github.com/brycied00d)
* [carlosjs23](https://github.com/carlosjs23) * [carlosjs23](https://github.com/carlosjs23)
* [cgxxx](https://github.com/cgxxx) * [cgxxx](https://github.com/cgxxx)
* [kibitan](https://github.com/kibitan)
* [chrisheninger](https://github.com/chrisheninger) * [chrisheninger](https://github.com/chrisheninger)
* [chris-martin](https://github.com/chris-martin) * [chris-martin](https://github.com/chris-martin)
* [DoubleMalt](https://github.com/DoubleMalt) * [DoubleMalt](https://github.com/DoubleMalt)
@ -259,45 +311,60 @@ and provided thanks to the work of the following contributors:
* [chriswk](https://github.com/chriswk) * [chriswk](https://github.com/chriswk)
* [csu](https://github.com/csu) * [csu](https://github.com/csu)
* [kklleemm](https://github.com/kklleemm) * [kklleemm](https://github.com/kklleemm)
* [monsterpit-daggertooth](https://github.com/monsterpit-daggertooth) * [colindean](https://github.com/colindean)
* [dachinat](https://github.com/dachinat)
* [multiple-creatures](https://github.com/multiple-creatures)
* [watilde](https://github.com/watilde) * [watilde](https://github.com/watilde)
* [daprice](https://github.com/daprice) * [daprice](https://github.com/daprice)
* [dar5hak](https://github.com/dar5hak) * [dar5hak](https://github.com/dar5hak)
* [kant](https://github.com/kant) * [kant](https://github.com/kant)
* [maxolasersquad](https://github.com/maxolasersquad)
* [singingwolfboy](https://github.com/singingwolfboy) * [singingwolfboy](https://github.com/singingwolfboy)
* [davidcelis](https://github.com/davidcelis) * [davidcelis](https://github.com/davidcelis)
* [davefp](https://github.com/davefp)
* [yipdw](https://github.com/yipdw) * [yipdw](https://github.com/yipdw)
* [debanshuk](https://github.com/debanshuk) * [debanshuk](https://github.com/debanshuk)
* [Derek Lewis](mailto:derekcecillewis@gmail.com)
* [dblandin](https://github.com/dblandin) * [dblandin](https://github.com/dblandin)
* [aranaur](https://github.com/aranaur) * [Drew Gates](mailto:aranaur@users.noreply.github.com)
* [dtschust](https://github.com/dtschust)
* [Dryusdan](https://github.com/Dryusdan)
* [eai04191](https://github.com/eai04191)
* [d3vgru](https://github.com/d3vgru) * [d3vgru](https://github.com/d3vgru)
* [Elizafox](https://github.com/Elizafox) * [Elizafox](https://github.com/Elizafox)
* [ericblade](https://github.com/ericblade) * [ericblade](https://github.com/ericblade)
* [mikoim](https://github.com/mikoim) * [mikoim](https://github.com/mikoim)
* [espenronnevik](https://github.com/espenronnevik)
* [Finariel](https://github.com/Finariel)
* [siuying](https://github.com/siuying) * [siuying](https://github.com/siuying)
* [GenbuHase](https://github.com/GenbuHase)
* [hattori6789](https://github.com/hattori6789) * [hattori6789](https://github.com/hattori6789)
* [algernon](https://github.com/algernon) * [algernon](https://github.com/algernon)
* [Fastbyte01](https://github.com/Fastbyte01) * [Fastbyte01](https://github.com/Fastbyte01)
* [Gomasy](https://github.com/Gomasy)
* [myfreeweb](https://github.com/myfreeweb) * [myfreeweb](https://github.com/myfreeweb)
* [gfaivre](https://github.com/gfaivre) * [gfaivre](https://github.com/gfaivre)
* [Fiaxhs](https://github.com/Fiaxhs) * [Fiaxhs](https://github.com/Fiaxhs)
* [reedcourty](https://github.com/reedcourty) * [reedcourty](https://github.com/reedcourty)
* [anneau](https://github.com/anneau) * [anneau](https://github.com/anneau)
* [lanodan](https://github.com/lanodan)
* [Harmon758](https://github.com/Harmon758)
* [HellPie](https://github.com/HellPie) * [HellPie](https://github.com/HellPie)
* [Habu-Kagumba](https://github.com/Habu-Kagumba) * [Habu-Kagumba](https://github.com/Habu-Kagumba)
* [hinaloe](https://github.com/hinaloe)
* [suzukaze](https://github.com/suzukaze) * [suzukaze](https://github.com/suzukaze)
* [Hiromi-Kai](https://github.com/Hiromi-Kai) * [Hiromi-Kai](https://github.com/Hiromi-Kai)
* [hishamhm](https://github.com/hishamhm)
* [musashino205](https://github.com/musashino205) * [musashino205](https://github.com/musashino205)
* [iwaim](https://github.com/iwaim) * [iwaim](https://github.com/iwaim)
* [valrus](https://github.com/valrus) * [valrus](https://github.com/valrus)
* [IMcD23](https://github.com/IMcD23) * [IMcD23](https://github.com/IMcD23)
* [yi0713](https://github.com/yi0713) * [yi0713](https://github.com/yi0713)
* [immae](https://github.com/immae)
* [iblech](https://github.com/iblech) * [iblech](https://github.com/iblech)
* [usbsnowcrash](https://github.com/usbsnowcrash)
* [jack-michaud](https://github.com/jack-michaud) * [jack-michaud](https://github.com/jack-michaud)
* [Floppy](https://github.com/Floppy) * [Floppy](https://github.com/Floppy)
* [loomchild](https://github.com/loomchild) * [loomchild](https://github.com/loomchild)
* [jenkr55](https://github.com/jenkr55)
* [docjkl](https://github.com/docjkl) * [docjkl](https://github.com/docjkl)
* [TrollDecker](https://github.com/TrollDecker) * [TrollDecker](https://github.com/TrollDecker)
* [jmontane](https://github.com/jmontane) * [jmontane](https://github.com/jmontane)
@ -305,30 +372,37 @@ and provided thanks to the work of the following contributors:
* [jguerder](https://github.com/jguerder) * [jguerder](https://github.com/jguerder)
* [Jehops](https://github.com/Jehops) * [Jehops](https://github.com/Jehops)
* [joshuap](https://github.com/joshuap) * [joshuap](https://github.com/joshuap)
* [YuleZ](https://github.com/YuleZ)
* [Tiwy57](https://github.com/Tiwy57) * [Tiwy57](https://github.com/Tiwy57)
* [xuv](https://github.com/xuv) * [xuv](https://github.com/xuv)
* [Jnsll](https://github.com/Jnsll) * [Jnsll](https://github.com/Jnsll)
* [j0k3r](https://github.com/j0k3r) * [j0k3r](https://github.com/j0k3r)
* [KEINOS](https://github.com/KEINOS) * [KEINOS](https://github.com/KEINOS)
* [futoase](https://github.com/futoase) * [futoase](https://github.com/futoase)
* [abjectio](https://github.com/abjectio) * [Pneumaticat](https://github.com/Pneumaticat)
* [Kit Redgrave](mailto:qwertyitis@gmail.com)
* [Knut Erik](mailto:abjectio@users.noreply.github.com)
* [mkody](https://github.com/mkody) * [mkody](https://github.com/mkody)
* [connyduck](https://github.com/connyduck)
* [k0ta0uchi](https://github.com/k0ta0uchi) * [k0ta0uchi](https://github.com/k0ta0uchi)
* [KrzysiekJ](https://github.com/KrzysiekJ) * [KrzysiekJ](https://github.com/KrzysiekJ)
* [leowzukw](https://github.com/leowzukw) * [leowzukw](https://github.com/leowzukw)
* [lmorchard](https://github.com/lmorchard) * [lmorchard](https://github.com/lmorchard)
* [Tak](https://github.com/Tak)
* [cacheflow](https://github.com/cacheflow) * [cacheflow](https://github.com/cacheflow)
* [ldidry](https://github.com/ldidry) * [ldidry](https://github.com/ldidry)
* [jemus42](https://github.com/jemus42) * [jemus42](https://github.com/jemus42)
* [lfuelling](https://github.com/lfuelling) * [lfuelling](https://github.com/lfuelling)
* [Grabacr07](https://github.com/Grabacr07) * [Grabacr07](https://github.com/Grabacr07)
* [mistermantas](https://github.com/mistermantas) * [mistermantas](https://github.com/mistermantas)
* [mareklach](https://github.com/mareklach)
* [wirehack7](https://github.com/wirehack7) * [wirehack7](https://github.com/wirehack7)
* [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)
* [m-blc](https://github.com/m-blc) * [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)
* [matthiasbeyer](https://github.com/matthiasbeyer)
* [mattjmattj](https://github.com/mattjmattj) * [mattjmattj](https://github.com/mattjmattj)
* [mtparet](https://github.com/mtparet) * [mtparet](https://github.com/mtparet)
* [maximeborges](https://github.com/maximeborges) * [maximeborges](https://github.com/maximeborges)
@ -336,16 +410,20 @@ and provided thanks to the work of the following contributors:
* [michaeljdeeb](https://github.com/michaeljdeeb) * [michaeljdeeb](https://github.com/michaeljdeeb)
* [Themimitoof](https://github.com/Themimitoof) * [Themimitoof](https://github.com/Themimitoof)
* [cyweo](https://github.com/cyweo) * [cyweo](https://github.com/cyweo)
* [M1dgard](https://github.com/M1dgard) * [Midgard](mailto:m1dgard@users.noreply.github.com)
* [mike-burns](https://github.com/mike-burns) * [mike-burns](https://github.com/mike-burns)
* [verymilan](https://github.com/verymilan) * [verymilan](https://github.com/verymilan)
* [milmazz](https://github.com/milmazz) * [milmazz](https://github.com/milmazz)
* [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) * [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)
* [lae](https://github.com/lae) * [lae](https://github.com/lae)
* [Nanamachi](https://github.com/Nanamachi) * [Nanamachi](https://github.com/Nanamachi)
* [orinthe](https://github.com/orinthe)
* [Dar13](https://github.com/Dar13)
* [ngerakines](https://github.com/ngerakines) * [ngerakines](https://github.com/ngerakines)
* [vonneudeck](https://github.com/vonneudeck) * [vonneudeck](https://github.com/vonneudeck)
* [Ninetailed](https://github.com/Ninetailed) * [Ninetailed](https://github.com/Ninetailed)
@ -355,18 +433,24 @@ and provided thanks to the work of the following contributors:
* [norayr](https://github.com/norayr) * [norayr](https://github.com/norayr)
* [joyeusenoelle](https://github.com/joyeusenoelle) * [joyeusenoelle](https://github.com/joyeusenoelle)
* [OlivierNicole](https://github.com/OlivierNicole) * [OlivierNicole](https://github.com/OlivierNicole)
* [noppa](https://github.com/noppa)
* [Otakan951](https://github.com/Otakan951) * [Otakan951](https://github.com/Otakan951)
* [fahy](https://github.com/fahy) * [fahy](https://github.com/fahy)
* [PatrickRWells](https://github.com/PatrickRWells)
* [Pangoraw](https://github.com/Pangoraw) * [Pangoraw](https://github.com/Pangoraw)
* [pwoolcoc](https://github.com/pwoolcoc)
* [peterkeen](https://github.com/peterkeen) * [peterkeen](https://github.com/peterkeen)
* [petzah](https://github.com/petzah) * [pgate](https://github.com/pgate)
* [ignisf](https://github.com/ignisf) * [remram44](https://github.com/remram44)
* [retokromer](https://github.com/retokromer)
* [rfwatson](https://github.com/rfwatson) * [rfwatson](https://github.com/rfwatson)
* [rfreebern](https://github.com/rfreebern) * [rfreebern](https://github.com/rfreebern)
* [Ryan Wade](mailto:ryan.wade@protonmail.com)
* [sylph01](https://github.com/sylph01) * [sylph01](https://github.com/sylph01)
* [S-H-GAMELINKS](https://github.com/S-H-GAMELINKS)
* [staticsafe](https://github.com/staticsafe) * [staticsafe](https://github.com/staticsafe)
* [snwh](https://github.com/snwh) * [snwh](https://github.com/snwh)
* [sts10](https://github.com/sts10)
* [sascha-sl](https://github.com/sascha-sl)
* [skoji](https://github.com/skoji) * [skoji](https://github.com/skoji)
* [ScienJus](https://github.com/ScienJus) * [ScienJus](https://github.com/ScienJus)
* [larkinscott](https://github.com/larkinscott) * [larkinscott](https://github.com/larkinscott)
@ -378,73 +462,103 @@ and provided thanks to the work of the following contributors:
* [ernix](https://github.com/ernix) * [ernix](https://github.com/ernix)
* [rosylilly](https://github.com/rosylilly) * [rosylilly](https://github.com/rosylilly)
* [shouko](https://github.com/shouko) * [shouko](https://github.com/shouko)
* [Sina Mashek](mailto:sina@mashek.xyz)
* [sossii](https://github.com/sossii) * [sossii](https://github.com/sossii)
* [StefOfficiel](https://github.com/StefOfficiel) * [SpankyWorks](https://github.com/SpankyWorks)
* [svetlik](https://github.com/svetlik) * [StefOfficiel](mailto:pichard.stephane@free.fr)
* [dereckson](https://github.com/dereckson) * [Svetlozar Todorov](mailto:svetlik@users.noreply.github.com)
* [theboss](https://github.com/theboss) * [Sébastien Santoro](mailto:dereckson@espace-win.org)
* [takp](https://github.com/takp) * [Tad Thorley](mailto:phaedryx@users.noreply.github.com)
* [tkusano](https://github.com/tkusano) * [Takayoshi Nishida](mailto:takayoshi.nishida@gmail.com)
* [TheInventrix](https://github.com/TheInventrix) * [Takayuki KUSANO](mailto:github@tkusano.jp)
* [shug0](https://github.com/shug0) * [TakesxiSximada](mailto:takesxi.sximada@gmail.com)
* [Fortyseven](https://github.com/Fortyseven) * [TheInventrix](mailto:theinventrix@users.noreply.github.com)
* [tobypinder](https://github.com/tobypinder) * [Thomas Alberola](mailto:thomas@needacoffee.fr)
* [tomosm](https://github.com/tomosm) * [Toby Deshane](mailto:fortyseven@users.noreply.github.com)
* [TomoyaShibata](https://github.com/TomoyaShibata) * [Toby Pinder](mailto:gigitrix@gmail.com)
* [TrashMacNugget](https://github.com/TrashMacNugget) * [Tomonori Murakami](mailto:crosslife777@gmail.com)
* [treyssatvincent](https://github.com/treyssatvincent) * [TomoyaShibata](mailto:wind.of.hometown@gmail.com)
* [optikfluffel](https://github.com/optikfluffel) * [Treyssat-Vincent Nino](mailto:treyssatvincent@users.noreply.github.com)
* [vmincev](https://github.com/vmincev) * [Udo Kramer](mailto:optik@fluffel.io)
* [waldyrious](https://github.com/waldyrious) * [Una](mailto:una@unascribed.com)
* [tahnok](https://github.com/tahnok) * [Ushitora Anqou](mailto:ushitora_anqou@yahoo.co.jp)
* [YDrogen](https://github.com/YDrogen) * [Valentin Lorentz](mailto:progval+git@progval.net)
* [YOSHIOKAEiichiro](https://github.com/YOSHIOKAEiichiro) * [Vladimir Mincev](mailto:vladimir@canicinteractive.com)
* [S-YOU](https://github.com/S-YOU) * [Waldir Pimenta](mailto:waldyrious@gmail.com)
* [YaQ00](https://github.com/YaQ00) * [Wesley Ellis](mailto:tahnok@gmail.com)
* [yanakend](https://github.com/yanakend) * [Wiktor](mailto:wiktor@metacode.biz)
* [orzFly](https://github.com/orzFly) * [Wonderfall](mailto:wonderfall@schrodinger.io)
* [chansuke](https://github.com/chansuke) * [YDrogen](mailto:ydrogen45@gmail.com)
* [yuntan](https://github.com/yuntan) * [YMHuang](mailto:ymhuang@fmbase.tw)
* [LogicalDash](https://github.com/LogicalDash) * [YOSHIOKA Eiichiro](mailto:yoshioka.eiichiro@gmail.com)
* [ZiiX](https://github.com/ZiiX) * [YOU](mailto:stackexchange.you@gmail.com)
* [benklop](https://github.com/benklop) * [YaQ](mailto:i_k_o_m_a_7@yahoo.co.jp)
* [caasi](https://github.com/caasi) * [Yanaken](mailto:yanakend@gmail.com)
* [caesarologia](https://github.com/caesarologia) * [Yann Klis](mailto:yann.klis@gmail.com)
* [chrolis](https://github.com/chrolis) * [Yeechan Lu](mailto:wz.bluesnow@gmail.com)
* [cormojs](https://github.com/cormojs) * [Yusuke Abe](mailto:moonset20@gmail.com)
* [cpsdqs](https://github.com/cpsdqs) * [Zachary Spector](mailto:logicaldash@gmail.com)
* [d0p1s4m4](https://github.com/d0p1s4m4) * [ZiiX](mailto:ziix@users.noreply.github.com)
* [evilny0](https://github.com/evilny0) * [asria-jp](mailto:is@alicematic.com)
* [febrezo](https://github.com/febrezo) * [ava](mailto:vladooku@users.noreply.github.com)
* [fsubal](https://github.com/fsubal) * [benklop](mailto:benklop@gmail.com)
* [dikky1218](https://github.com/dikky1218) * [bsky](mailto:git@imbsky.net)
* [gentarok](https://github.com/gentarok) * [caesarologia](mailto:lopesgemelli.1@gmail.com)
* [hakoai](https://github.com/hakoai) * [cbayerlein](mailto:c.bayerlein@gmail.com)
* [chaosbunker](https://github.com/chaosbunker) * [chrolis](mailto:chrolis@users.noreply.github.com)
* [isati](https://github.com/isati) * [cormo](mailto:cormorant2+github@gmail.com)
* [jkap](https://github.com/jkap) * [d0p1](mailto:dopi-sama@hush.com)
* [jirayudech](https://github.com/jirayudech) * [evilny0](mailto:evilny0@moomoocamp.net)
* [jukper](https://github.com/jukper) * [febrezo](mailto:felixbrezo@gmail.com)
* [karlyeurl](https://github.com/karlyeurl) * [fsubal](mailto:fsubal@users.noreply.github.com)
* [kedamaDQ](https://github.com/kedamaDQ) * [fusshi-](mailto:dikky1218@users.noreply.github.com)
* [kuro5hin](https://github.com/kuro5hin) * [gentaro](mailto:gentaroooo@gmail.com)
* [maxypy](https://github.com/maxypy) * [hakoai](mailto:hk--76@qa2.so-net.ne.jp)
* [marcus-herrmann](https://github.com/marcus-herrmann) * [haosbvnker](mailto:github@chaosbunker.com)
* [mshrtkch](https://github.com/mshrtkch) * [isati](mailto:phil@juchnowi.cz)
* [muan](https://github.com/muan) * [jacob](mailto:jacobherringtondeveloper@gmail.com)
* [rch850](https://github.com/rch850) * [jenn kaplan](mailto:me@jkap.io)
* [roikale](https://github.com/roikale) * [jirayudech](mailto:jirayudech@gmail.com)
* [rysiekpl](https://github.com/rysiekpl) * [jooops](mailto:joops@autistici.org)
* [saturday06](https://github.com/saturday06) * [jukper](mailto:jukkaperanto@gmail.com)
* [scriptjunkie](https://github.com/scriptjunkie) * [jumoru](mailto:jumoru@mailbox.org)
* [seekr](https://github.com/seekr) * [karlyeurl](mailto:karl.yeurl@gmail.com)
* [syui](https://github.com/syui) * [kedama](mailto:32974885+kedamadq@users.noreply.github.com)
* [tackeyy](https://github.com/tackeyy) * [kuro5hin](mailto:rusty@kuro5hin.org)
* [tmyt](https://github.com/tmyt) * [luzpaz](mailto:luzpaz@users.noreply.github.com)
* [utam0k](https://github.com/utam0k) * [maxypy](mailto:maxime@mpigou.fr)
* [vpzomtrrfrt](https://github.com/vpzomtrrfrt) * [mhe](mailto:mail@marcus-herrmann.com)
* [walfie](https://github.com/walfie) * [mimikun](mailto:dzdzble_effort_311@outlook.jp)
* [y-temp4](https://github.com/y-temp4) * [mshrtkch](mailto:mshrtkch@users.noreply.github.com)
* [ymmtmdk](https://github.com/ymmtmdk) * [muan](mailto:muan@github.com)
* [neetshin](mailto:neetshin@neetsh.in)
* [nightpool](mailto:nightpool@users.noreply.github.com)
* [rch850](mailto:rich850@gmail.com)
* [roikale](mailto:roikale@users.noreply.github.com)
* [rysiekpl](mailto:rysiek@hackerspace.pl)
* [saturday06](mailto:dyob@lunaport.net)
* [scriptjunkie](mailto:scriptjunkie@scriptjunkie.us)
* [seekr](mailto:mario.drs@gmail.com)
* [sundevour](mailto:31990469+sundevour@users.noreply.github.com)
* [syui](mailto:syui@users.noreply.github.com)
* [tackeyy](mailto:mailto.takita.yusuke@gmail.com)
* [tateisu](mailto:tateisu@gmail.com)
* [tmyt](mailto:shigure@refy.net)
* [trevDev()](mailto:trev@trevdev.ca)
* [utam0k](mailto:k0ma@utam0k.jp)
* [vpzomtrrfrt](mailto:vpzomtrrfrt@gmail.com)
* [walfie](mailto:walfington@gmail.com)
* [y-temp4](mailto:y.temp4@gmail.com)
* [ymmtmdk](mailto:ymmtmdk@gmail.com)
* [yoshipc](mailto:yoooo@yoshipc.net)
* [Özcan Zafer AYAN](mailto:ozcanzaferayan@gmail.com)
* [ばん](mailto:detteiu0321@gmail.com)
* [みたらしだんご](mailto:mitarashidango@users.noreply.github.com)
* [りんすき](mailto:6533808+rinsuki@users.noreply.github.com)
* [ヨイツの賢狼ホロ | 3rd style](mailto:horo@yoitsu.moe)
* [猫吸血鬼ディフリス / 猫ロキP](mailto:deflis@gmail.com)
* [艮 鮟鱇](mailto:ushitora_anqou@yahoo.co.jp)
* [西小倉宏信](mailto:nishiko@mindia.jp)
* [雨宮美羽](mailto:k737566@gmail.com)
This document is provided for informational purposes only. Since it is only updated once per release, the version you are looking at may be currently out of date. To see the full list of contributors, consider looking at the [git history](https://github.com/tootsuite/mastodon/graphs/contributors) instead. This document is provided for informational purposes only. Since it is only updated once per release, the version you are looking at may be currently out of date. To see the full list of contributors, consider looking at the [git history](https://github.com/tootsuite/mastodon/graphs/contributors) instead.

17
Aptfile
View File

@ -10,3 +10,20 @@ libxdamage1
libxfixes3 libxfixes3
protobuf-compiler protobuf-compiler
zlib1g-dev zlib1g-dev
libcairo2
libcroco3
libdatrie1
libgdk-pixbuf2.0-0
libgraphite2-3
libharfbuzz0b
libpango-1.0-0
libpangocairo-1.0-0
libpangoft2-1.0-0
libpixman-1-0
librsvg2-2
libthai-data
libthai0
libvpx5
libxcb-render0
libxcb-shm0
libxrender1

107
CHANGELOG.md Normal file
View File

@ -0,0 +1,107 @@
Changelog
=========
All notable changes to this project will be documented in this file.
## [Unreleased]
### Added
- Add link ownership verification (#8703)
- Add conversations API (#8832)
- Add limit for the number of people that can be followed from one account (#8807)
- Add admin setting to customize mascot (#8766)
- Add support for more granular ActivityPub audiences from other software, i.e. circles (#8950)
- Add option to block all reports from a domain (#8830)
- Add user preference to always expand toots marked with content warnings (#8762)
- Add user preference to always hide all media (#8569)
- Add `force_login` param to OAuth authorize page (#8655)
- Add `tootctl accounts backup` (#8642, #8811)
- Add `tootctl accounts create` (#8642, #8811)
- Add `tootctl accounts cull` (#8642, #8811)
- Add `tootctl accounts delete` (#8642, #8811)
- Add `tootctl accounts modify` (#8642, #8811)
- Add `tootctl accounts refresh` (#8642, #8811)
- Add `tootctl feeds build` (#8642, #8811)
- Add `tootctl feeds clear` (#8642, #8811)
- Add `tootctl settings registrations open` (#8642, #8811)
- Add `tootctl settings registrations close` (#8642, #8811)
- Add `min_id` param to REST API to support backwards pagination (#8736)
- Add a confirmation dialog when hitting reply and the compose box isn't empty (#8893)
- Add PostgreSQL disk space growth tracking in PGHero (#8906)
- Add button for disabling local account to report quick actions bar (#9024)
- Add Czech language (#8594)
- Add `Clear-Site-Data` header when logging out (#8627)
- Add `same-site` (`lax`) attribute to cookies (#8626)
- Add support for styled scrollbars in Firefox Nightly (#8653)
- Add highlight to the active tab in web UI profiles (#8673)
- Add auto-focus for comment textarea in report modal (#8689)
- Add auto-focus for emoji picker's search field (#8688)
- Add nginx and systemd templates to `dist/` directory (#8770)
- Add support for `/.well-known/change-password` (#8828)
- Add option to override FFMPEG binary path (#8855)
- Add `dns-prefetch` tag when using different host for assets or uploads (#8942)
- Add `description` meta tag (#8941)
- Add `Content-Security-Policy` header (#8957)
- Add cache for the instance info API (#8765)
### Changed
- Change forms design (#8703)
- Change reports overview to group by target account (#8674)
- Change web UI to show "read more" link on overly long in-stream statuses (#8205)
- Change design of direct messages column (#8832, #9022)
- Change home timelines to exclude DMs (#8940)
- Change list timelines to exclude all replies (#8683)
- Change admin accounts UI default sort to most recent (#8813)
- Change documentation URL in the UI (#8898)
- Change style of success and failure messages (#8973)
- Change DM filtering to always allow DMs from staff (#8993)
- Change recommended Ruby version to 2.5.3 (#9003)
### Deprecated
- `GET /api/v1/timelines/direct``GET /api/v1/conversations` (#8832)
- `POST /api/v1/notifications/dismiss``POST /api/v1/notifications/:id/dismiss` (#8905)
### Removed
- Remove "on this device" label in column push settings (#8704)
- Remove rake tasks in favour of tootctl commands (#8675)
### Fixed
- Fix remote statuses using instance's default locale if no language given (#8861)
- Fix streaming API not exiting when port or socket is unavailable (#9023)
- Fix network calls being performed in database transaction in ActivityPub handler (#8951)
- Fix dropdown arrow position (#8637)
- Fix first element of dropdowns being focused even if not using keyboard (#8679)
- Fix tootctl requiring `bundle exec` invocation (#8619)
- Fix public pages not using animation preference for avatars (#8614)
- Fix OEmbed/OpenGraph cards not understanding relative URLs (#8669)
- Fix some dark emojis not having a white outline (#8597)
- Fix media description not being displayed in various media modals (#8678)
- Fix generated URLs of desktop notifications missing base URL (#8758)
- Fix RTL styles (#8764, #8767, #8823, #8897, #9005, #9007, #9018, #9021)
- Fix crash in streaming API when tag param missing (#8955)
- Fix hotkeys not working when no element is focused (#8998)
- Fix some hotkeys not working on detailed status view (#9006)
## [2.5.2] - 2018-10-12
### Security
- Fix XSS vulnerability (#8959)
## [2.5.1] - 2018-10-07
### Fixed
- Fix database migrations for PostgreSQL below 9.5 (#8903)
- Fix class autoloading issue in ActivityPub Create handler (#8820)
- Fix cache statistics not being sent via statsd when statsd enabled (#8831)
- Bump puma from 3.11.4 to 3.12.0 (#8883)
### Security
- Fix some local images not having their EXIF metadata stripped on upload (#8714)
- Fix being able to enable a disabled relay via ActivityPub Accept handler (#8864)
- Bump nokogiri from 1.8.4 to 1.8.5 (#8881)
- Fix being able to report statuses not belonging to the reported account (#8916)

View File

@ -1,56 +1,37 @@
CONTRIBUTING Contributing
============ ============
There are three ways in which you can contribute to this repository: Thank you for considering contributing to Mastodon 🐘
1. By improving the documentation You can contribute in the following ways:
2. By working on the back-end application
3. By working on the front-end application
Choosing what to work on in a large open source project is not easy. The list of [GitHub issues](https://github.com/tootsuite/mastodon/issues) may provide some ideas, but not every feature request has been greenlit. Likewise, not every change or feature that resolves a personal itch will be merged into the main repository. Some communication ahead of time may be wise. If your addition creates a new feature or setting, or otherwise changes how things work in some substantial way, please remember to submit a correlating pull request to document your changes in the [documentation](http://github.com/tootsuite/documentation). - Finding and reporting bugs
- Translating the Mastodon interface into various languages
- Contributing code to Mastodon by fixing bugs or implementing features
- Improving the documentation
Below are the guidelines for working on pull requests: ## Bug reports
## General Bug reports and feature suggestions can be submitted to [GitHub Issues](https://github.com/tootsuite/mastodon/issues). Please make sure that you are not submitting duplicates, and that a similar report or request has not already been resolved or rejected in the past using the search function. Please also use descriptive, concise titles.
- 2 spaces indentation ## Translations
You can submit translations via [Weblate](https://weblate.joinmastodon.org/). They are periodically merged into the codebase.
[![Mastodon translation statistics by language](https://weblate.joinmastodon.org/widgets/mastodon/-/multi-auto.svg)](https://weblate.joinmastodon.org/)
## Pull requests
Please use clean, concise titles for your pull requests. We use commit squashing, so the final commit in the master branch will carry the title of the pull request.
The smaller the set of changes in the pull request is, the quicker it can be reviewed and merged. Splitting tasks into multiple smaller pull requests is often preferable.
**Pull requests that do not pass automated checks may not be reviewed**. In particular, you need to keep in mind:
- Unit and integration tests (rspec, jest)
- Code style rules (rubocop, eslint)
- Normalization of locale files (i18n-tasks)
## Documentation ## Documentation
- No spelling mistakes The [Mastodon documentation](https://docs.joinmastodon.org) is a statically generated site. You can [submit merge requests to mastodon/docs](https://source.joinmastodon.org/mastodon/docs).
- No orthographic mistakes
- No Markdown syntax errors
## Requirements
- Ruby
- Node.js
- PostgreSQL
- Redis
- Nginx (optional)
## Back-end application
It is expected that you have a working development environment set up. The development environment includes [rubocop](https://github.com/bbatsov/rubocop), which checks your Ruby code for compliance with our style guide and best practices. Sublime Text, likely like other editors, has a [Rubocop plugin](https://github.com/pderichs/sublime_rubocop) that runs checks on files as you edit them. The codebase also has a test suite.
* The codebase is not perfect, at the time of writing, but it is expected that you do not introduce new code style violations
* The rspec test suite must pass
* To the extent that it is possible, verify your changes. In the best case, by adding new tests to the test suite. At the very least, by running the server or console and checking it manually
* If you are introducing new strings to the user interface, they must be using localization methods
If your code has syntax errors that won't let it run, it's a good sign that the pull request isn't ready for submission yet.
## Front-end application
It is expected that you have a working development environment set up (see back-end application section). This project includes an ESLint configuration file, with which you can lint your changes.
* Avoid grave ESLint violations
* Verify that your changes work
* If you are introducing new strings, they must be using localization methods
If the JavaScript or CSS assets won't compile due to a syntax error, it's a good sign that the pull request isn't ready for submission yet.
## Translate
You can contribute to translating Mastodon via Weblate at [weblate.joinmastodon.org](https://weblate.joinmastodon.org/).
[![Mastodon translation statistics by language](https://weblate.joinmastodon.org/widgets/mastodon/-/multi-auto.svg)](https://weblate.joinmastodon.org/)

View File

@ -1,5 +1,5 @@
FROM node:8.11.3-alpine as node FROM node:8.12.0-alpine as node
FROM ruby:2.4.4-alpine3.6 FROM ruby:2.4.4-alpine3.7
LABEL maintainer="https://github.com/tootsuite/mastodon" \ LABEL maintainer="https://github.com/tootsuite/mastodon" \
description="Your self-hosted, globally interconnected microblogging community" description="Your self-hosted, globally interconnected microblogging community"

69
Gemfile
View File

@ -5,36 +5,36 @@ ruby '>= 2.3.0', '< 2.6.0'
gem 'pkg-config', '~> 1.3' gem 'pkg-config', '~> 1.3'
gem 'puma', '~> 3.11' gem 'puma', '~> 3.12'
gem 'rails', '~> 5.2.1' gem 'rails', '~> 5.2.1'
gem 'thor', '~> 0.20' gem 'thor', '~> 0.20'
gem 'hamlit-rails', '~> 0.2' gem 'hamlit-rails', '~> 0.2'
gem 'pg', '~> 1.0' gem 'pg', '~> 1.1'
gem 'makara', '~> 0.4' gem 'makara', '~> 0.4'
gem 'pghero', '~> 2.1' gem 'pghero', '~> 2.2'
gem 'dotenv-rails', '~> 2.2', '< 2.3' gem 'dotenv-rails', '~> 2.5'
gem 'aws-sdk-s3', '~> 1.9', require: false gem 'aws-sdk-s3', '~> 1.21', require: false
gem 'fog-core', '~> 1.45' gem 'fog-core', '~> 2.1'
gem 'fog-openstack', '~> 0.1', require: false gem 'fog-openstack', '~> 1.0', require: false
gem 'paperclip', '~> 6.0' gem 'paperclip', '~> 6.0'
gem 'paperclip-av-transcoder', '~> 0.6' gem 'paperclip-av-transcoder', '~> 0.6'
gem 'streamio-ffmpeg', '~> 3.0' gem 'streamio-ffmpeg', '~> 3.0'
gem 'active_model_serializers', '~> 0.10' gem 'active_model_serializers', '~> 0.10'
gem 'addressable', '~> 2.5' gem 'addressable', '~> 2.5'
gem 'bootsnap', '~> 1.3' gem 'bootsnap', '~> 1.3', require: false
gem 'browser' gem 'browser'
gem 'charlock_holmes', '~> 0.7.6' gem 'charlock_holmes', '~> 0.7.6'
gem 'iso-639' gem 'iso-639'
gem 'chewy', '~> 5.0' gem 'chewy', '~> 5.0'
gem 'cld3', '~> 3.2.0' gem 'cld3', '~> 3.2.0'
gem 'devise', '~> 4.4' gem 'devise', '~> 4.5'
gem 'devise-two-factor', '~> 3.0' gem 'devise-two-factor', '~> 3.0'
group :pam_authentication, optional: true do group :pam_authentication, optional: true do
gem 'devise_pam_authenticatable2', '~> 9.1' gem 'devise_pam_authenticatable2', '~> 9.2'
end end
gem 'net-ldap', '~> 0.10' gem 'net-ldap', '~> 0.10'
@ -49,45 +49,44 @@ gem 'goldfinger', '~> 2.1'
gem 'hiredis', '~> 0.6' gem 'hiredis', '~> 0.6'
gem 'redis-namespace', '~> 1.5' gem 'redis-namespace', '~> 1.5'
gem 'htmlentities', '~> 4.3' gem 'htmlentities', '~> 4.3'
gem 'http', '~> 3.2' gem 'http', '~> 3.3'
gem 'http_accept_language', '~> 2.1' gem 'http_accept_language', '~> 2.1'
gem 'http_parser.rb', '~> 0.6', git: 'https://github.com/tmm1/http_parser.rb', ref: '54b17ba8c7d8d20a16dfc65d1775241833219cf2' gem 'http_parser.rb', '~> 0.6', git: 'https://github.com/tmm1/http_parser.rb', ref: '54b17ba8c7d8d20a16dfc65d1775241833219cf2'
gem 'httplog', '~> 1.0' gem 'httplog', '~> 1.1'
gem 'idn-ruby', require: 'idn' gem 'idn-ruby', require: 'idn'
gem 'kaminari', '~> 1.1' gem 'kaminari', '~> 1.1'
gem 'link_header', '~> 0.0' gem 'link_header', '~> 0.0'
gem 'mime-types', '~> 3.1', require: 'mime/types/columnar' gem 'mime-types', '~> 3.2', require: 'mime/types/columnar'
gem 'nokogiri', '~> 1.8' gem 'nokogiri', '~> 1.8'
gem 'nsa', '~> 0.2' gem 'nsa', '~> 0.2'
gem 'oj', '~> 3.5' gem 'oj', '~> 3.6'
gem 'ostatus2', '~> 2.0' gem 'ostatus2', '~> 2.0'
gem 'ox', '~> 2.9' gem 'ox', '~> 2.10'
gem 'posix-spawn', git: 'https://github.com/rtomayko/posix-spawn', ref: '58465d2e213991f8afb13b984854a49fcdcc980c' gem 'posix-spawn', git: 'https://github.com/rtomayko/posix-spawn', ref: '58465d2e213991f8afb13b984854a49fcdcc980c'
gem 'pundit', '~> 1.1' gem 'pundit', '~> 2.0'
gem 'premailer-rails' gem 'premailer-rails'
gem 'rack-attack', '~> 5.2' gem 'rack-attack', '~> 5.4'
gem 'rack-cors', '~> 1.0', require: 'rack/cors' gem 'rack-cors', '~> 1.0', require: 'rack/cors'
gem 'rails-i18n', '~> 5.1' gem 'rails-i18n', '~> 5.1'
gem 'rails-settings-cached', '~> 0.6' gem 'rails-settings-cached', '~> 0.6'
gem 'redis', '~> 4.0', require: ['redis', 'redis/connection/hiredis'] gem 'redis', '~> 4.0', require: ['redis', 'redis/connection/hiredis']
gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock' gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock'
gem 'rqrcode', '~> 0.10' gem 'rqrcode', '~> 0.10'
gem 'ruby-progressbar', '~> 1.4'
gem 'sanitize', '~> 4.6' gem 'sanitize', '~> 4.6'
gem 'sidekiq', '~> 5.1' gem 'sidekiq', '~> 5.2'
gem 'sidekiq-scheduler', '~> 2.2' gem 'sidekiq-scheduler', '~> 3.0'
gem 'sidekiq-unique-jobs', '~> 5.0' gem 'sidekiq-unique-jobs', '~> 5.0'
gem 'sidekiq-bulk', '~>0.1.1' gem 'sidekiq-bulk', '~>0.1.1'
gem 'simple-navigation', '~> 4.0' gem 'simple-navigation', '~> 4.0'
gem 'simple_form', '~> 4.0' gem 'simple_form', '~> 4.0'
gem 'sprockets-rails', '~> 3.2', require: 'sprockets/railtie' gem 'sprockets-rails', '~> 3.2', require: 'sprockets/railtie'
gem 'stoplight', '~> 2.1.3' gem 'stoplight', '~> 2.1.3'
gem 'strong_migrations', '~> 0.2' gem 'strong_migrations', '~> 0.3'
gem 'tty-command', '~> 0.8', require: false gem 'tty-command', '~> 0.8', require: false
gem 'tty-prompt', '~> 0.16', require: false gem 'tty-prompt', '~> 0.17', require: false
gem 'twitter-text', '~> 1.14' gem 'twitter-text', '~> 1.14'
gem 'tzinfo-data', '~> 1.2018' gem 'tzinfo-data', '~> 1.2018'
gem 'webpacker', '~> 3.4' gem 'webpacker', '~> 3.5'
gem 'webpush' gem 'webpush'
gem 'json-ld', '~> 2.2' gem 'json-ld', '~> 2.2'
@ -95,45 +94,45 @@ gem 'rdf-normalize', '~> 0.3'
group :development, :test do group :development, :test do
gem 'fabrication', '~> 2.20' gem 'fabrication', '~> 2.20'
gem 'fuubar', '~> 2.2' gem 'fuubar', '~> 2.3'
gem 'i18n-tasks', '~> 0.9', require: false gem 'i18n-tasks', '~> 0.9', require: false
gem 'pry-byebug', '~> 3.6' gem 'pry-byebug', '~> 3.6'
gem 'pry-rails', '~> 0.3' gem 'pry-rails', '~> 0.3'
gem 'rspec-rails', '~> 3.7' gem 'rspec-rails', '~> 3.8'
end end
group :production, :test do group :production, :test do
gem 'private_address_check', '~> 0.4.1' gem 'private_address_check', '~> 0.5'
end end
group :test do group :test do
gem 'capybara', '~> 2.18' gem 'capybara', '~> 3.9'
gem 'climate_control', '~> 0.2' gem 'climate_control', '~> 0.2'
gem 'faker', '~> 1.8' gem 'faker', '~> 1.9'
gem 'microformats', '~> 4.0' gem 'microformats', '~> 4.0'
gem 'rails-controller-testing', '~> 1.0' gem 'rails-controller-testing', '~> 1.0'
gem 'rspec-sidekiq', '~> 3.0' gem 'rspec-sidekiq', '~> 3.0'
gem 'simplecov', '~> 0.16', require: false gem 'simplecov', '~> 0.16', require: false
gem 'webmock', '~> 3.3' gem 'webmock', '~> 3.4'
gem 'parallel_tests', '~> 2.21' gem 'parallel_tests', '~> 2.23'
end end
group :development do group :development do
gem 'active_record_query_trace', '~> 1.5' gem 'active_record_query_trace', '~> 1.5'
gem 'annotate', '~> 2.7' gem 'annotate', '~> 2.7'
gem 'better_errors', '~> 2.4' gem 'better_errors', '~> 2.5'
gem 'binding_of_caller', '~> 0.7' gem 'binding_of_caller', '~> 0.7'
gem 'bullet', '~> 5.7' gem 'bullet', '~> 5.7'
gem 'letter_opener', '~> 1.4' gem 'letter_opener', '~> 1.4'
gem 'letter_opener_web', '~> 1.3' gem 'letter_opener_web', '~> 1.3'
gem 'memory_profiler' gem 'memory_profiler'
gem 'rubocop', '~> 0.55', require: false gem 'rubocop', '~> 0.59', require: false
gem 'brakeman', '~> 4.2', require: false gem 'brakeman', '~> 4.3', require: false
gem 'bundler-audit', '~> 0.6', require: false gem 'bundler-audit', '~> 0.6', require: false
gem 'scss_lint', '~> 0.57', require: false gem 'scss_lint', '~> 0.57', require: false
gem 'capistrano', '~> 3.10' gem 'capistrano', '~> 3.11'
gem 'capistrano-rails', '~> 1.3' gem 'capistrano-rails', '~> 1.4'
gem 'capistrano-rbenv', '~> 2.1' gem 'capistrano-rbenv', '~> 2.1'
gem 'capistrano-yarn', '~> 2.0' gem 'capistrano-yarn', '~> 2.0'

View File

@ -66,7 +66,7 @@ GEM
public_suffix (>= 2.0.2, < 4.0) public_suffix (>= 2.0.2, < 4.0)
airbrussh (1.3.0) airbrussh (1.3.0)
sshkit (>= 1.6.1, != 1.7.0) sshkit (>= 1.6.1, != 1.7.0)
annotate (2.7.3) annotate (2.7.4)
activerecord (>= 3.2, < 6.0) activerecord (>= 3.2, < 6.0)
rake (>= 10.4, < 13.0) rake (>= 10.4, < 13.0)
arel (9.0.0) arel (9.0.0)
@ -75,40 +75,42 @@ GEM
encryptor (~> 3.0.0) encryptor (~> 3.0.0)
av (0.9.0) av (0.9.0)
cocaine (~> 0.5.3) cocaine (~> 0.5.3)
aws-partitions (1.80.0) aws-eventstream (1.0.1)
aws-sdk-core (3.19.0) aws-partitions (1.105.0)
aws-sdk-core (3.30.0)
aws-eventstream (~> 1.0)
aws-partitions (~> 1.0) aws-partitions (~> 1.0)
aws-sigv4 (~> 1.0) aws-sigv4 (~> 1.0)
jmespath (~> 1.0) jmespath (~> 1.0)
aws-sdk-kms (1.5.0) aws-sdk-kms (1.9.0)
aws-sdk-core (~> 3) aws-sdk-core (~> 3, >= 3.26.0)
aws-sigv4 (~> 1.0) aws-sigv4 (~> 1.0)
aws-sdk-s3 (1.9.1) aws-sdk-s3 (1.21.0)
aws-sdk-core (~> 3) aws-sdk-core (~> 3, >= 3.26.0)
aws-sdk-kms (~> 1) aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.0) aws-sigv4 (~> 1.0)
aws-sigv4 (1.0.2) aws-sigv4 (1.0.3)
bcrypt (3.1.12) bcrypt (3.1.12)
benchmark-ips (2.7.2) benchmark-ips (2.7.2)
better_errors (2.4.0) better_errors (2.5.0)
coderay (>= 1.0.0) coderay (>= 1.0.0)
erubi (>= 1.0.0) erubi (>= 1.0.0)
rack (>= 0.9.0) rack (>= 0.9.0)
binding_of_caller (0.8.0) binding_of_caller (0.8.0)
debug_inspector (>= 0.0.1) debug_inspector (>= 0.0.1)
bootsnap (1.3.0) bootsnap (1.3.2)
msgpack (~> 1.0) msgpack (~> 1.0)
brakeman (4.2.1) brakeman (4.3.1)
browser (2.5.3) browser (2.5.3)
builder (3.2.3) builder (3.2.3)
bullet (5.7.5) bullet (5.7.6)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
uniform_notifier (~> 1.11.0) uniform_notifier (~> 1.11.0)
bundler-audit (0.6.0) bundler-audit (0.6.0)
bundler (~> 1.2) bundler (~> 1.2)
thor (~> 0.18) thor (~> 0.18)
byebug (10.0.2) byebug (10.0.2)
capistrano (3.10.2) capistrano (3.11.0)
airbrussh (>= 1.0.0) airbrussh (>= 1.0.0)
i18n i18n
rake (>= 10.0.0) rake (>= 10.0.0)
@ -116,21 +118,21 @@ GEM
capistrano-bundler (1.3.0) capistrano-bundler (1.3.0)
capistrano (~> 3.1) capistrano (~> 3.1)
sshkit (~> 1.2) sshkit (~> 1.2)
capistrano-rails (1.3.1) capistrano-rails (1.4.0)
capistrano (~> 3.1) capistrano (~> 3.1)
capistrano-bundler (~> 1.1) capistrano-bundler (~> 1.1)
capistrano-rbenv (2.1.3) capistrano-rbenv (2.1.4)
capistrano (~> 3.1) capistrano (~> 3.1)
sshkit (~> 1.3) sshkit (~> 1.3)
capistrano-yarn (2.0.2) capistrano-yarn (2.0.2)
capistrano (~> 3.0) capistrano (~> 3.0)
capybara (2.18.0) capybara (3.9.0)
addressable addressable
mini_mime (>= 0.1.3) mini_mime (>= 0.1.3)
nokogiri (>= 1.3.3) nokogiri (~> 1.8)
rack (>= 1.0.0) rack (>= 1.6.0)
rack-test (>= 0.5.4) rack-test (>= 0.6.3)
xpath (>= 2.0, < 4.0) xpath (~> 3.1)
case_transform (0.2) case_transform (0.2)
activesupport activesupport
charlock_holmes (0.7.6) charlock_holmes (0.7.6)
@ -145,16 +147,15 @@ GEM
cocaine (0.5.8) cocaine (0.5.8)
climate_control (>= 0.0.3, < 1.0) climate_control (>= 0.0.3, < 1.0)
coderay (1.1.2) coderay (1.1.2)
colorize (0.8.1)
concurrent-ruby (1.0.5) concurrent-ruby (1.0.5)
connection_pool (2.2.1) connection_pool (2.2.2)
crack (0.4.3) crack (0.4.3)
safe_yaml (~> 1.0.0) safe_yaml (~> 1.0.0)
crass (1.0.4) crass (1.0.4)
css_parser (1.6.0) css_parser (1.6.0)
addressable addressable
debug_inspector (0.0.3) debug_inspector (0.0.3)
derailed_benchmarks (1.3.4) derailed_benchmarks (1.3.5)
benchmark-ips (~> 2) benchmark-ips (~> 2)
get_process_mem (~> 0) get_process_mem (~> 0)
heapy (~> 0) heapy (~> 0)
@ -162,7 +163,7 @@ GEM
rack (>= 1) rack (>= 1)
rake (> 10, < 13) rake (> 10, < 13)
thor (~> 0.19) thor (~> 0.19)
devise (4.4.3) devise (4.5.0)
bcrypt (~> 3.0) bcrypt (~> 3.0)
orm_adapter (~> 0.1) orm_adapter (~> 0.1)
railties (>= 4.1.0, < 6.0) railties (>= 4.1.0, < 6.0)
@ -174,22 +175,19 @@ GEM
devise (~> 4.0) devise (~> 4.0)
railties (< 5.3) railties (< 5.3)
rotp (~> 2.0) rotp (~> 2.0)
devise_pam_authenticatable2 (9.1.1) devise_pam_authenticatable2 (9.2.0)
devise (>= 4.0.0) devise (>= 4.0.0)
rpam2 (~> 4.0) rpam2 (~> 4.0)
diff-lcs (1.3) diff-lcs (1.3)
docile (1.3.0) docile (1.3.0)
domain_name (0.5.20180417) domain_name (0.5.20180417)
unf (>= 0.0.5, < 1.0.0) unf (>= 0.0.5, < 1.0.0)
doorkeeper (5.0.0) doorkeeper (5.0.1)
railties (>= 4.2) railties (>= 4.2)
dotenv (2.2.2) dotenv (2.5.0)
dotenv-rails (2.2.2) dotenv-rails (2.5.0)
dotenv (= 2.2.2) dotenv (= 2.5.0)
railties (>= 3.2, < 6.0) railties (>= 3.2, < 6.0)
easy_translate (0.5.1)
thread
thread_safe
elasticsearch (6.0.2) elasticsearch (6.0.2)
elasticsearch-api (= 6.0.2) elasticsearch-api (= 6.0.2)
elasticsearch-transport (= 6.0.2) elasticsearch-transport (= 6.0.2)
@ -202,33 +200,37 @@ GEM
encryptor (3.0.0) encryptor (3.0.0)
equatable (0.5.0) equatable (0.5.0)
erubi (1.7.1) erubi (1.7.1)
et-orbi (1.1.0) et-orbi (1.1.6)
tzinfo tzinfo
excon (0.62.0) excon (0.62.0)
fabrication (2.20.1) fabrication (2.20.1)
faker (1.8.7) faker (1.9.1)
i18n (>= 0.7) i18n (>= 0.7)
faraday (0.15.0) faraday (0.15.0)
multipart-post (>= 1.2, < 3) multipart-post (>= 1.2, < 3)
fast_blank (1.0.0) fast_blank (1.0.0)
fastimage (2.1.1) fastimage (2.1.4)
ffi (1.9.23) ffi (1.9.25)
fog-core (1.45.0) fog-core (2.1.2)
builder builder
excon (~> 0.58) excon (~> 0.58)
formatador (~> 0.2) formatador (~> 0.2)
fog-json (1.0.2) mime-types
fog-core (~> 1.0) fog-json (1.2.0)
fog-core
multi_json (~> 1.10) multi_json (~> 1.10)
fog-openstack (0.1.25) fog-openstack (1.0.3)
fog-core (~> 1.40) fog-core (~> 2.1)
fog-json (>= 1.0) fog-json (>= 1.0)
ipaddress (>= 0.8) ipaddress (>= 0.8)
formatador (0.2.5) formatador (0.2.5)
fuubar (2.3.1) fugit (1.1.6)
et-orbi (~> 1.1, >= 1.1.6)
raabro (~> 1.1)
fuubar (2.3.2)
rspec-core (~> 3.0) rspec-core (~> 3.0)
ruby-progressbar (~> 1.4) ruby-progressbar (~> 1.4)
get_process_mem (0.2.1) get_process_mem (0.2.2)
globalid (0.4.1) globalid (0.4.1)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
goldfinger (2.1.0) goldfinger (2.1.0)
@ -249,32 +251,31 @@ GEM
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
hashdiff (0.3.7) hashdiff (0.3.7)
hashie (3.5.7) hashie (3.5.7)
heapy (0.1.3) heapy (0.1.4)
highline (1.7.10) highline (2.0.0)
hiredis (0.6.1) hiredis (0.6.1)
hitimes (1.2.6) hitimes (1.3.0)
hkdf (0.3.0) hkdf (0.3.0)
htmlentities (4.3.4) htmlentities (4.3.4)
http (3.2.0) http (3.3.0)
addressable (~> 2.3) addressable (~> 2.3)
http-cookie (~> 1.0) http-cookie (~> 1.0)
http-form_data (~> 2.0) http-form_data (~> 2.0)
http_parser.rb (~> 0.6.0) http_parser.rb (~> 0.6.0)
http-cookie (1.0.3) http-cookie (1.0.3)
domain_name (~> 0.5) domain_name (~> 0.5)
http-form_data (2.1.0) http-form_data (2.1.1)
http_accept_language (2.1.1) http_accept_language (2.1.1)
httplog (1.0.2) httplog (1.1.1)
colorize (~> 0.8)
rack (>= 1.0) rack (>= 1.0)
i18n (1.1.0) rainbow (>= 2.0.0)
i18n (1.1.1)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
i18n-tasks (0.9.21) i18n-tasks (0.9.25)
activesupport (>= 4.0.2) activesupport (>= 4.0.2)
ast (>= 2.1.0) ast (>= 2.1.0)
easy_translate (>= 0.5.1)
erubi erubi
highline (>= 1.7.3) highline (>= 2.0.0)
i18n i18n
parser (>= 2.2.3.0) parser (>= 2.2.3.0)
rainbow (>= 2.2.2, < 4.0) rainbow (>= 2.2.2, < 4.0)
@ -282,6 +283,7 @@ GEM
idn-ruby (0.1.0) idn-ruby (0.1.0)
ipaddress (0.8.3) ipaddress (0.8.3)
iso-639 (0.2.8) iso-639 (0.2.8)
jaro_winkler (1.5.1)
jmespath (1.4.0) jmespath (1.4.0)
json (2.1.0) json (2.1.0)
json-ld (2.2.1) json-ld (2.2.1)
@ -326,16 +328,16 @@ GEM
mimemagic (~> 0.3.2) mimemagic (~> 0.3.2)
mario-redis-lock (1.2.1) mario-redis-lock (1.2.1)
redis (>= 3.0.5) redis (>= 3.0.5)
memory_profiler (0.9.10) memory_profiler (0.9.12)
method_source (0.9.0) method_source (0.9.0)
microformats (4.0.7) microformats (4.0.7)
json json
nokogiri nokogiri
mime-types (3.1) mime-types (3.2.2)
mime-types-data (~> 3.2015) mime-types-data (~> 3.2015)
mime-types-data (3.2016.0521) mime-types-data (3.2018.0812)
mimemagic (0.3.2) mimemagic (0.3.2)
mini_mime (1.0.0) mini_mime (1.0.1)
mini_portile2 (2.3.0) mini_portile2 (2.3.0)
minitest (5.11.3) minitest (5.11.3)
msgpack (1.2.4) msgpack (1.2.4)
@ -345,9 +347,9 @@ GEM
net-ldap (0.16.1) net-ldap (0.16.1)
net-scp (1.2.1) net-scp (1.2.1)
net-ssh (>= 2.6.5) net-ssh (>= 2.6.5)
net-ssh (4.2.0) net-ssh (5.0.2)
nio4r (2.3.1) nio4r (2.3.1)
nokogiri (1.8.4) nokogiri (1.8.5)
mini_portile2 (~> 2.3.0) mini_portile2 (~> 2.3.0)
nokogumbo (1.5.0) nokogumbo (1.5.0)
nokogiri nokogiri
@ -356,7 +358,7 @@ GEM
concurrent-ruby (~> 1.0.0) concurrent-ruby (~> 1.0.0)
sidekiq (>= 3.5.0) sidekiq (>= 3.5.0)
statsd-ruby (~> 1.2.0) statsd-ruby (~> 1.2.0)
oj (3.5.1) oj (3.6.12)
omniauth (1.8.1) omniauth (1.8.1)
hashie (>= 3.4.6, < 3.6.0) hashie (>= 3.4.6, < 3.6.0)
rack (>= 1.6.2, < 3) rack (>= 1.6.2, < 3)
@ -364,7 +366,7 @@ GEM
addressable (~> 2.3) addressable (~> 2.3)
nokogiri (~> 1.5) nokogiri (~> 1.5)
omniauth (~> 1.2) omniauth (~> 1.2)
omniauth-saml (1.10.0) omniauth-saml (1.10.1)
omniauth (~> 1.3, >= 1.3.2) omniauth (~> 1.3, >= 1.3.2)
ruby-saml (~> 1.7) ruby-saml (~> 1.7)
orm_adapter (0.5.0) orm_adapter (0.5.0)
@ -372,7 +374,7 @@ GEM
addressable (~> 2.5) addressable (~> 2.5)
http (~> 3.0) http (~> 3.0)
nokogiri (~> 1.8) nokogiri (~> 1.8)
ox (2.9.2) ox (2.10.0)
paperclip (6.0.0) paperclip (6.0.0)
activemodel (>= 4.2.0) activemodel (>= 4.2.0)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
@ -383,18 +385,18 @@ GEM
av (~> 0.9.0) av (~> 0.9.0)
paperclip (>= 2.5.2) paperclip (>= 2.5.2)
parallel (1.12.1) parallel (1.12.1)
parallel_tests (2.21.3) parallel_tests (2.23.0)
parallel parallel
parser (2.5.1.0) parser (2.5.1.2)
ast (~> 2.4.0) ast (~> 2.4.0)
pastel (0.7.2) pastel (0.7.2)
equatable (~> 0.5.0) equatable (~> 0.5.0)
tty-color (~> 0.4.0) tty-color (~> 0.4.0)
pg (1.0.0) pg (1.1.3)
pghero (2.1.0) pghero (2.2.0)
activerecord activerecord
pkg-config (1.3.0) pkg-config (1.3.1)
powerpack (0.1.1) powerpack (0.1.2)
premailer (1.11.1) premailer (1.11.1)
addressable addressable
css_parser (>= 1.6.0) css_parser (>= 1.6.0)
@ -402,7 +404,7 @@ GEM
premailer-rails (1.10.2) premailer-rails (1.10.2)
actionmailer (>= 3, < 6) actionmailer (>= 3, < 6)
premailer (~> 1.7, >= 1.7.9) premailer (~> 1.7, >= 1.7.9)
private_address_check (0.4.1) private_address_check (0.5.0)
pry (0.11.3) pry (0.11.3)
coderay (~> 1.1.0) coderay (~> 1.1.0)
method_source (~> 0.9.0) method_source (~> 0.9.0)
@ -411,15 +413,16 @@ GEM
pry (~> 0.10) pry (~> 0.10)
pry-rails (0.3.6) pry-rails (0.3.6)
pry (>= 0.10.4) pry (>= 0.10.4)
public_suffix (3.0.2) public_suffix (3.0.3)
puma (3.11.4) puma (3.12.0)
pundit (1.1.0) pundit (2.0.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
raabro (1.1.6)
rack (2.0.5) rack (2.0.5)
rack-attack (5.2.0) rack-attack (5.4.1)
rack rack (>= 1.0, < 3)
rack-cors (1.0.2) rack-cors (1.0.2)
rack-protection (2.0.1) rack-protection (2.0.4)
rack rack
rack-proxy (0.6.4) rack-proxy (0.6.4)
rack rack
@ -468,7 +471,7 @@ GEM
link_header (~> 0.0, >= 0.0.8) link_header (~> 0.0, >= 0.0.8)
rdf-normalize (0.3.3) rdf-normalize (0.3.3)
rdf (>= 2.2, < 4.0) rdf (>= 2.2, < 4.0)
redis (4.0.1) redis (4.0.2)
redis-actionpack (5.0.2) redis-actionpack (5.0.2)
actionpack (>= 4.0, < 6) actionpack (>= 4.0, < 6)
redis-rack (>= 1, < 3) redis-rack (>= 1, < 3)
@ -496,60 +499,60 @@ GEM
rpam2 (4.0.2) rpam2 (4.0.2)
rqrcode (0.10.1) rqrcode (0.10.1)
chunky_png (~> 1.0) chunky_png (~> 1.0)
rspec-core (3.7.1) rspec-core (3.8.0)
rspec-support (~> 3.7.0) rspec-support (~> 3.8.0)
rspec-expectations (3.7.0) rspec-expectations (3.8.1)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.7.0) rspec-support (~> 3.8.0)
rspec-mocks (3.7.0) rspec-mocks (3.8.0)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.7.0) rspec-support (~> 3.8.0)
rspec-rails (3.7.2) rspec-rails (3.8.0)
actionpack (>= 3.0) actionpack (>= 3.0)
activesupport (>= 3.0) activesupport (>= 3.0)
railties (>= 3.0) railties (>= 3.0)
rspec-core (~> 3.7.0) rspec-core (~> 3.8.0)
rspec-expectations (~> 3.7.0) rspec-expectations (~> 3.8.0)
rspec-mocks (~> 3.7.0) rspec-mocks (~> 3.8.0)
rspec-support (~> 3.7.0) rspec-support (~> 3.8.0)
rspec-sidekiq (3.0.3) rspec-sidekiq (3.0.3)
rspec-core (~> 3.0, >= 3.0.0) rspec-core (~> 3.0, >= 3.0.0)
sidekiq (>= 2.4.0) sidekiq (>= 2.4.0)
rspec-support (3.7.1) rspec-support (3.8.0)
rubocop (0.55.0) rubocop (0.59.2)
jaro_winkler (~> 1.5.1)
parallel (~> 1.10) parallel (~> 1.10)
parser (>= 2.5) parser (>= 2.5, != 2.5.1.1)
powerpack (~> 0.1) powerpack (~> 0.1)
rainbow (>= 2.2.2, < 4.0) rainbow (>= 2.2.2, < 4.0)
ruby-progressbar (~> 1.7) ruby-progressbar (~> 1.7)
unicode-display_width (~> 1.0, >= 1.0.1) unicode-display_width (~> 1.0, >= 1.0.1)
ruby-progressbar (1.9.0) ruby-progressbar (1.9.0)
ruby-saml (1.7.2) ruby-saml (1.9.0)
nokogiri (>= 1.5.10) nokogiri (>= 1.5.10)
rufus-scheduler (3.4.2) rufus-scheduler (3.5.2)
et-orbi (~> 1.0) fugit (~> 1.1, >= 1.1.5)
safe_yaml (1.0.4) safe_yaml (1.0.4)
sanitize (4.6.4) sanitize (4.6.6)
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.4.4) nokogiri (>= 1.4.4)
nokogumbo (~> 1.4) nokogumbo (~> 1.4)
sass (3.5.6) sass (3.6.0)
sass-listen (~> 4.0.0) sass-listen (~> 4.0.0)
sass-listen (4.0.0) sass-listen (4.0.0)
rb-fsevent (~> 0.9, >= 0.9.4) rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7) rb-inotify (~> 0.9, >= 0.9.7)
scss_lint (0.57.0) scss_lint (0.57.1)
rake (>= 0.9, < 13) rake (>= 0.9, < 13)
sass (~> 3.5.5) sass (~> 3.5, >= 3.5.5)
sidekiq (5.1.3) sidekiq (5.2.2)
concurrent-ruby (~> 1.0) connection_pool (~> 2.2, >= 2.2.2)
connection_pool (~> 2.2, >= 2.2.0)
rack-protection (>= 1.5.0) rack-protection (>= 1.5.0)
redis (>= 3.3.5, < 5) redis (>= 3.3.5, < 5)
sidekiq-bulk (0.1.1) sidekiq-bulk (0.1.1)
activesupport activesupport
sidekiq sidekiq
sidekiq-scheduler (2.2.1) sidekiq-scheduler (3.0.0)
redis (>= 3, < 5) redis (>= 3, < 5)
rufus-scheduler (~> 3.2) rufus-scheduler (~> 3.2)
sidekiq (>= 3) sidekiq (>= 3)
@ -559,9 +562,9 @@ GEM
thor (~> 0) thor (~> 0)
simple-navigation (4.0.5) simple-navigation (4.0.5)
activesupport (>= 2.3.2) activesupport (>= 2.3.2)
simple_form (4.0.0) simple_form (4.0.1)
actionpack (> 4) actionpack (>= 5.0)
activemodel (> 4) activemodel (>= 5.0)
simplecov (0.16.1) simplecov (0.16.1)
docile (~> 1.1) docile (~> 1.1)
json (>= 1.8, < 3) json (>= 1.8, < 3)
@ -574,15 +577,15 @@ GEM
actionpack (>= 4.0) actionpack (>= 4.0)
activesupport (>= 4.0) activesupport (>= 4.0)
sprockets (>= 3.0.0) sprockets (>= 3.0.0)
sshkit (1.16.0) sshkit (1.17.0)
net-scp (>= 1.1.2) net-scp (>= 1.1.2)
net-ssh (>= 2.8.0) net-ssh (>= 2.8.0)
stackprof (0.2.11) stackprof (0.2.12)
statsd-ruby (1.2.1) statsd-ruby (1.2.1)
stoplight (2.1.3) stoplight (2.1.3)
streamio-ffmpeg (3.0.2) streamio-ffmpeg (3.0.2)
multi_json (~> 1.8) multi_json (~> 1.8)
strong_migrations (0.2.2) strong_migrations (0.3.1)
activerecord (>= 3.2.0) activerecord (>= 3.2.0)
temple (0.8.0) temple (0.8.0)
terminal-table (1.8.0) terminal-table (1.8.0)
@ -590,55 +593,54 @@ GEM
terrapin (0.6.0) terrapin (0.6.0)
climate_control (>= 0.0.3, < 1.0) climate_control (>= 0.0.3, < 1.0)
thor (0.20.0) thor (0.20.0)
thread (0.2.2)
thread_safe (0.3.6) thread_safe (0.3.6)
tilt (2.0.8) tilt (2.0.8)
timers (4.1.2) timers (4.1.2)
hitimes hitimes
tty-color (0.4.2) tty-color (0.4.3)
tty-command (0.8.0) tty-command (0.8.2)
pastel (~> 0.7.0) pastel (~> 0.7.0)
tty-cursor (0.5.0) tty-cursor (0.6.0)
tty-prompt (0.16.0) tty-prompt (0.17.1)
necromancer (~> 0.4.0) necromancer (~> 0.4.0)
pastel (~> 0.7.0) pastel (~> 0.7.0)
timers (~> 4.0) timers (~> 4.0)
tty-cursor (~> 0.5.0) tty-cursor (~> 0.6.0)
tty-reader (~> 0.2.0) tty-reader (~> 0.4.0)
tty-reader (0.2.0) tty-reader (0.4.0)
tty-cursor (~> 0.5.0) tty-cursor (~> 0.6.0)
tty-screen (~> 0.6.4) tty-screen (~> 0.6.4)
wisper (~> 2.0.0) wisper (~> 2.0.0)
tty-screen (0.6.4) tty-screen (0.6.5)
twitter-text (1.14.7) twitter-text (1.14.7)
unf (~> 0.1.0) unf (~> 0.1.0)
tzinfo (1.2.5) tzinfo (1.2.5)
thread_safe (~> 0.1) thread_safe (~> 0.1)
tzinfo-data (1.2018.4) tzinfo-data (1.2018.6)
tzinfo (>= 1.0.0) tzinfo (>= 1.0.0)
unf (0.1.4) unf (0.1.4)
unf_ext unf_ext
unf_ext (0.0.7.5) unf_ext (0.0.7.5)
unicode-display_width (1.3.2) unicode-display_width (1.4.0)
uniform_notifier (1.11.0) uniform_notifier (1.11.0)
warden (1.2.7) warden (1.2.7)
rack (>= 1.0) rack (>= 1.0)
webmock (3.3.0) webmock (3.4.2)
addressable (>= 2.3.6) addressable (>= 2.3.6)
crack (>= 0.3.2) crack (>= 0.3.2)
hashdiff hashdiff
webpacker (3.4.3) webpacker (3.5.5)
activesupport (>= 4.2) activesupport (>= 4.2)
rack-proxy (>= 0.6.1) rack-proxy (>= 0.6.1)
railties (>= 4.2) railties (>= 4.2)
webpush (0.3.3) webpush (0.3.4)
hkdf (~> 0.2) hkdf (~> 0.2)
jwt (~> 2.0) jwt (~> 2.0)
websocket-driver (0.7.0) websocket-driver (0.7.0)
websocket-extensions (>= 0.1.0) websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.3) websocket-extensions (0.1.3)
wisper (2.0.0) wisper (2.0.0)
xpath (3.0.0) xpath (3.1.0)
nokogiri (~> 1.8) nokogiri (~> 1.8)
PLATFORMS PLATFORMS
@ -649,44 +651,44 @@ DEPENDENCIES
active_record_query_trace (~> 1.5) active_record_query_trace (~> 1.5)
addressable (~> 2.5) addressable (~> 2.5)
annotate (~> 2.7) annotate (~> 2.7)
aws-sdk-s3 (~> 1.9) aws-sdk-s3 (~> 1.21)
better_errors (~> 2.4) better_errors (~> 2.5)
binding_of_caller (~> 0.7) binding_of_caller (~> 0.7)
bootsnap (~> 1.3) bootsnap (~> 1.3)
brakeman (~> 4.2) brakeman (~> 4.3)
browser browser
bullet (~> 5.7) bullet (~> 5.7)
bundler-audit (~> 0.6) bundler-audit (~> 0.6)
capistrano (~> 3.10) capistrano (~> 3.11)
capistrano-rails (~> 1.3) capistrano-rails (~> 1.4)
capistrano-rbenv (~> 2.1) capistrano-rbenv (~> 2.1)
capistrano-yarn (~> 2.0) capistrano-yarn (~> 2.0)
capybara (~> 2.18) capybara (~> 3.9)
charlock_holmes (~> 0.7.6) charlock_holmes (~> 0.7.6)
chewy (~> 5.0) chewy (~> 5.0)
cld3 (~> 3.2.0) cld3 (~> 3.2.0)
climate_control (~> 0.2) climate_control (~> 0.2)
derailed_benchmarks derailed_benchmarks
devise (~> 4.4) devise (~> 4.5)
devise-two-factor (~> 3.0) devise-two-factor (~> 3.0)
devise_pam_authenticatable2 (~> 9.1) devise_pam_authenticatable2 (~> 9.2)
doorkeeper (~> 5.0) doorkeeper (~> 5.0)
dotenv-rails (~> 2.2, < 2.3) dotenv-rails (~> 2.5)
fabrication (~> 2.20) fabrication (~> 2.20)
faker (~> 1.8) faker (~> 1.9)
fast_blank (~> 1.0) fast_blank (~> 1.0)
fastimage fastimage
fog-core (~> 1.45) fog-core (~> 2.1)
fog-openstack (~> 0.1) fog-openstack (~> 1.0)
fuubar (~> 2.2) fuubar (~> 2.3)
goldfinger (~> 2.1) goldfinger (~> 2.1)
hamlit-rails (~> 0.2) hamlit-rails (~> 0.2)
hiredis (~> 0.6) hiredis (~> 0.6)
htmlentities (~> 4.3) htmlentities (~> 4.3)
http (~> 3.2) http (~> 3.3)
http_accept_language (~> 2.1) http_accept_language (~> 2.1)
http_parser.rb (~> 0.6)! http_parser.rb (~> 0.6)!
httplog (~> 1.0) httplog (~> 1.1)
i18n-tasks (~> 0.9) i18n-tasks (~> 0.9)
idn-ruby idn-ruby
iso-639 iso-639
@ -700,30 +702,30 @@ DEPENDENCIES
mario-redis-lock (~> 1.2) mario-redis-lock (~> 1.2)
memory_profiler memory_profiler
microformats (~> 4.0) microformats (~> 4.0)
mime-types (~> 3.1) mime-types (~> 3.2)
net-ldap (~> 0.10) net-ldap (~> 0.10)
nokogiri (~> 1.8) nokogiri (~> 1.8)
nsa (~> 0.2) nsa (~> 0.2)
oj (~> 3.5) oj (~> 3.6)
omniauth (~> 1.2) omniauth (~> 1.2)
omniauth-cas (~> 1.1) omniauth-cas (~> 1.1)
omniauth-saml (~> 1.10) omniauth-saml (~> 1.10)
ostatus2 (~> 2.0) ostatus2 (~> 2.0)
ox (~> 2.9) ox (~> 2.10)
paperclip (~> 6.0) paperclip (~> 6.0)
paperclip-av-transcoder (~> 0.6) paperclip-av-transcoder (~> 0.6)
parallel_tests (~> 2.21) parallel_tests (~> 2.23)
pg (~> 1.0) pg (~> 1.1)
pghero (~> 2.1) pghero (~> 2.2)
pkg-config (~> 1.3) pkg-config (~> 1.3)
posix-spawn! posix-spawn!
premailer-rails premailer-rails
private_address_check (~> 0.4.1) private_address_check (~> 0.5)
pry-byebug (~> 3.6) pry-byebug (~> 3.6)
pry-rails (~> 0.3) pry-rails (~> 0.3)
puma (~> 3.11) puma (~> 3.12)
pundit (~> 1.1) pundit (~> 2.0)
rack-attack (~> 5.2) rack-attack (~> 5.4)
rack-cors (~> 1.0) rack-cors (~> 1.0)
rails (~> 5.2.1) rails (~> 5.2.1)
rails-controller-testing (~> 1.0) rails-controller-testing (~> 1.0)
@ -734,15 +736,14 @@ DEPENDENCIES
redis-namespace (~> 1.5) redis-namespace (~> 1.5)
redis-rails (~> 5.0) redis-rails (~> 5.0)
rqrcode (~> 0.10) rqrcode (~> 0.10)
rspec-rails (~> 3.7) rspec-rails (~> 3.8)
rspec-sidekiq (~> 3.0) rspec-sidekiq (~> 3.0)
rubocop (~> 0.55) rubocop (~> 0.59)
ruby-progressbar (~> 1.4)
sanitize (~> 4.6) sanitize (~> 4.6)
scss_lint (~> 0.57) scss_lint (~> 0.57)
sidekiq (~> 5.1) sidekiq (~> 5.2)
sidekiq-bulk (~> 0.1.1) sidekiq-bulk (~> 0.1.1)
sidekiq-scheduler (~> 2.2) sidekiq-scheduler (~> 3.0)
sidekiq-unique-jobs (~> 5.0) sidekiq-unique-jobs (~> 5.0)
simple-navigation (~> 4.0) simple-navigation (~> 4.0)
simple_form (~> 4.0) simple_form (~> 4.0)
@ -751,18 +752,18 @@ DEPENDENCIES
stackprof stackprof
stoplight (~> 2.1.3) stoplight (~> 2.1.3)
streamio-ffmpeg (~> 3.0) streamio-ffmpeg (~> 3.0)
strong_migrations (~> 0.2) strong_migrations (~> 0.3)
thor (~> 0.20) thor (~> 0.20)
tty-command (~> 0.8) tty-command (~> 0.8)
tty-prompt (~> 0.16) tty-prompt (~> 0.17)
twitter-text (~> 1.14) twitter-text (~> 1.14)
tzinfo-data (~> 1.2018) tzinfo-data (~> 1.2018)
webmock (~> 3.3) webmock (~> 3.4)
webpacker (~> 3.4) webpacker (~> 3.5)
webpush webpush
RUBY VERSION RUBY VERSION
ruby 2.5.0p0 ruby 2.5.3p105
BUNDLED WITH BUNDLED WITH
1.16.3 1.16.5

View File

@ -1,98 +1,95 @@
![Mastodon](https://i.imgur.com/NhZc40l.png) ![Mastodon](https://i.imgur.com/NhZc40l.png)
======== ========
[![GitHub release](https://img.shields.io/github/release/tootsuite/mastodon.svg)][releases]
[![Build Status](https://img.shields.io/circleci/project/github/tootsuite/mastodon.svg)][circleci] [![Build Status](https://img.shields.io/circleci/project/github/tootsuite/mastodon.svg)][circleci]
[![Code Climate](https://img.shields.io/codeclimate/maintainability/tootsuite/mastodon.svg)][code_climate] [![Code Climate](https://img.shields.io/codeclimate/maintainability/tootsuite/mastodon.svg)][code_climate]
[![Translation status](https://weblate.joinmastodon.org/widgets/mastodon/-/svg-badge.svg)][weblate] [![Translation status](https://weblate.joinmastodon.org/widgets/mastodon/-/svg-badge.svg)][weblate]
[![Docker Pulls](https://img.shields.io/docker/pulls/tootsuite/mastodon.svg)][docker]
[releases]: https://github.com/tootsuite/mastodon/releases
[circleci]: https://circleci.com/gh/tootsuite/mastodon [circleci]: https://circleci.com/gh/tootsuite/mastodon
[code_climate]: https://codeclimate.com/github/tootsuite/mastodon [code_climate]: https://codeclimate.com/github/tootsuite/mastodon
[weblate]: https://weblate.joinmastodon.org/engage/mastodon/ [weblate]: https://weblate.joinmastodon.org/engage/mastodon/
[docker]: https://hub.docker.com/r/tootsuite/mastodon/
Mastodon is a **free, open-source social network server** based on **open web protocols** like ActivityPub and OStatus. The social focus of the project is a viable decentralized alternative to commercial social media silos that returns the control of the content distribution channels to the people. The technical focus of the project is a good user interface, a clean REST API for 3rd party apps and robust anti-abuse tools. Mastodon is a **free, open-source social network server** based on ActivityPub. Follow friends and discover new ones. Publish anything you want: links, pictures, text, video. All servers of Mastodon are interoperable as a federated network, i.e. users on one server can seamlessly communicate with users from another one. This includes non-Mastodon software that also implements ActivityPub!
Click on the screenshot below to watch a demo of the UI: Click below to **learn more** in a video:
[![Screenshot](https://i.imgur.com/qrNOiSp.png)][youtube_demo] [![Screenshot](https://blog.joinmastodon.org/2018/06/why-activitypub-is-the-future/ezgif-2-60f1b00403.gif)][youtube_demo]
[youtube_demo]: https://www.youtube.com/watch?v=IPSbNdBmWKE [youtube_demo]: https://www.youtube.com/watch?v=IPSbNdBmWKE
**Ruby on Rails** is used for the back-end, while **React.js** and Redux are used for the dynamic front-end. A static front-end for public resources (profiles and statuses) is also provided. ## Navigation
If you would like, you can [support the development of this project on Patreon][patreon]. - [Project homepage 🐘](https://joinmastodon.org)
- [Support the development via Patreon][patreon]
- [View sponsors](https://joinmastodon.org/sponsors)
- [Blog](https://blog.joinmastodon.org)
- [Documentation](https://docs.joinmastodon.org)
- [Browse Mastodon servers](https://joinmastodon.org/#getting-started)
- [Browse Mastodon apps](https://joinmastodon.org/apps)
[patreon]: https://www.patreon.com/mastodon [patreon]: https://www.patreon.com/mastodon
---
## Resources
- [Quick start guide](https://blog.joinmastodon.org/2018/08/mastodon-quick-start-guide/)
- [Find Twitter friends on Mastodon](https://bridge.joinmastodon.org)
- [API overview](https://github.com/tootsuite/documentation/blob/master/Using-the-API/API.md)
- [Documentation](https://github.com/tootsuite/documentation)
- [List of servers](https://joinmastodon.org/#getting-started)
- [List of apps](https://joinmastodon.org/apps)
- [List of sponsors](https://joinmastodon.org/sponsors)
## Features ## Features
<img src="https://docs.joinmastodon.org/elephant.svg" align="right" width="30%" />
**No vendor lock-in: Fully interoperable with any conforming platform** **No vendor lock-in: Fully interoperable with any conforming platform**
It doesn't have to be Mastodon, whatever implements ActivityPub or OStatus is part of the social network! It doesn't have to be Mastodon, whatever implements ActivityPub is part of the social network! [Learn more](https://blog.joinmastodon.org/2018/06/why-activitypub-is-the-future/)
**Real-time timeline updates** **Real-time, chronological timeline updates**
See the updates of people you're following appear in real-time in the UI via WebSockets. There's a firehose view as well! See the updates of people you're following appear in real-time in the UI via WebSockets. There's a firehose view as well!
**Federated thread resolving**
If someone you follow replies to a user unknown to the server, the server fetches the full thread so you can view it without leaving the UI
**Media attachments like images and short videos** **Media attachments like images and short videos**
Upload and view images and WebM/MP4 videos attached to the updates. Videos with no audio track are treated like GIFs; normal videos are looped - like vines! Upload and view images and WebM/MP4 videos attached to the updates. Videos with no audio track are treated like GIFs; normal videos are looped - like vines!
**Safety and moderation tools**
Private posts, locked accounts, phrase filtering, muting, blocking and all sorts of other features, along with a reporting and moderation system. [Learn more](https://blog.joinmastodon.org/2018/07/cage-the-mastodon/)
**OAuth2 and a straightforward REST API** **OAuth2 and a straightforward REST API**
Mastodon acts as an OAuth2 provider so 3rd party apps can use the API Mastodon acts as an OAuth2 provider so 3rd party apps can use the REST and Streaming APIs, resulting in a rich app ecosystem with a lot of choice!
**Fast response times**
Mastodon tries to be as fast and responsive as possible, so all long-running tasks are delegated to background processing
**Deployable via Docker**
You don't need to mess with dependencies and configuration if you want to try Mastodon, if you have Docker and Docker Compose the deployment is extremely easy
---
## Development
Please follow the [development guide](https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Development-guide.md) from the documentation repository.
## Deployment ## Deployment
There are guides in the documentation repository for [deploying on various platforms](https://github.com/tootsuite/documentation#running-mastodon). **Tech stack:**
- **Ruby on Rails** powers the REST API and other web pages
- **React.js** and Redux are used for the dynamic parts of the interface
- **Node.js** powers the streaming API
**Requirements:**
- **PostgreSQL** 9.5+
- **Redis**
- **Ruby** 2.4+
- **Node.js** 8+
The repository includes deployment configurations for **Docker and docker-compose**, but also a few specific platforms like **Heroku**, **Scalingo**, and **Nanobox**. The [**stand-alone** installation guide](https://docs.joinmastodon.org/administration/installation/) is available in the documentation.
A **Vagrant** configuration is included for development purposes.
## Contributing ## Contributing
You can open issues for bugs you've found or features you think are missing. You can also submit pull requests to this repository. [Here are the guidelines for code contributions](CONTRIBUTING.md) Mastodon is **free, open source software** licensed under **AGPLv3**.
You can open issues for bugs you've found or features you think are missing. You can also submit pull requests to this repository, or submit translations using Weblate. To get started, take a look at [CONTRIBUTING.md](CONTRIBUTING.md)
**IRC channel**: #mastodon on irc.freenode.net **IRC channel**: #mastodon on irc.freenode.net
## License ## License
Copyright (C) 2016-2018 Eugen Rochko & other Mastodon contributors (see AUTHORS.md) Copyright (C) 2016-2018 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.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
---
## Extra credits
The elephant friend illustrations are created by [Dopatwo](https://mastodon.social/@dopatwo)

14
Vagrantfile vendored
View File

@ -85,6 +85,9 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
config.vm.provider :virtualbox do |vb| config.vm.provider :virtualbox do |vb|
vb.name = "mastodon" vb.name = "mastodon"
vb.customize ["modifyvm", :id, "--memory", "2048"] vb.customize ["modifyvm", :id, "--memory", "2048"]
# Increase the number of CPUs. Uncomment and adjust to
# increase performance
# vb.customize ["modifyvm", :id, "--cpus", "3"]
# Disable VirtualBox DNS proxy to skip long-delay IPv6 resolutions. # Disable VirtualBox DNS proxy to skip long-delay IPv6 resolutions.
# https://github.com/mitchellh/vagrant/issues/1172 # https://github.com/mitchellh/vagrant/issues/1172
@ -97,19 +100,22 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
end end
config.vm.hostname = "mastodon.dev"
# This uses the vagrant-hostsupdater plugin, and lets you # This uses the vagrant-hostsupdater plugin, and lets you
# access the development site at http://mastodon.dev. # access the development site at http://mastodon.local.
# If you change it, also change it in .env.vagrant before provisioning
# the vagrant server to update the development build.
#
# To install: # To install:
# $ vagrant plugin install vagrant-hostsupdater # $ vagrant plugin install vagrant-hostsupdater
config.vm.hostname = "mastodon.local"
if defined?(VagrantPlugins::HostsUpdater) if defined?(VagrantPlugins::HostsUpdater)
config.vm.network :private_network, ip: "192.168.42.42", nictype: "virtio" config.vm.network :private_network, ip: "192.168.42.42", nictype: "virtio"
config.hostsupdater.remove_on_suspend = false config.hostsupdater.remove_on_suspend = false
end end
if config.vm.networks.any? { |type, options| type == :private_network } if config.vm.networks.any? { |type, options| type == :private_network }
config.vm.synced_folder ".", "/vagrant", type: "nfs", mount_options: ['rw', 'vers=3', 'tcp'] config.vm.synced_folder ".", "/vagrant", type: "nfs", mount_options: ['rw', 'vers=3', 'tcp', 'actimeo=1']
else else
config.vm.synced_folder ".", "/vagrant" config.vm.synced_folder ".", "/vagrant"
end end

View File

@ -95,7 +95,7 @@ module Admin
:remote, :remote,
:by_domain, :by_domain,
:silenced, :silenced,
:recent, :alphabetic,
:suspended, :suspended,
:username, :username,
:display_name, :display_name,

View File

@ -46,7 +46,7 @@ module Admin
end end
def resource_params def resource_params
params.require(:domain_block).permit(:domain, :severity, :reject_media, :retroactive) params.require(:domain_block).permit(:domain, :severity, :reject_media, :reject_reports, :retroactive)
end end
def retroactive_unblock? def retroactive_unblock?

View File

@ -44,6 +44,14 @@ module Admin
when 'resolve' when 'resolve'
@report.resolve!(current_account) @report.resolve!(current_account)
log_action :resolve, @report log_action :resolve, @report
when 'disable'
@report.resolve!(current_account)
@report.target_account.user.disable!
log_action :resolve, @report
log_action :disable, @report.target_account.user
resolve_all_target_account_reports
when 'silence' when 'silence'
@report.resolve!(current_account) @report.resolve!(current_account)
@report.target_account.update!(silenced: true) @report.target_account.update!(silenced: true)
@ -55,6 +63,7 @@ module Admin
else else
raise ActiveRecord::RecordNotFound raise ActiveRecord::RecordNotFound
end end
@report.reload @report.reload
end end

View File

@ -19,6 +19,7 @@ module Admin
theme theme
thumbnail thumbnail
hero hero
mascot
min_invite_role min_invite_role
activity_api_enabled activity_api_enabled
peers_api_enabled peers_api_enabled
@ -41,6 +42,7 @@ module Admin
UPLOAD_SETTINGS = %w( UPLOAD_SETTINGS = %w(
thumbnail thumbnail
hero hero
mascot
).freeze ).freeze
def edit def edit

View File

@ -53,8 +53,8 @@ class Api::BaseController < ApplicationController
[params[:limit].to_i.abs, default_limit * 2].min [params[:limit].to_i.abs, default_limit * 2].min
end end
def truthy_param?(key) def params_slice(*keys)
ActiveModel::Type::Boolean.new.cast(params[key]) params.slice(*keys).permit(*keys)
end end
def current_resource_owner def current_resource_owner

View File

@ -28,10 +28,9 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
def account_statuses def account_statuses
statuses = truthy_param?(:pinned) ? pinned_scope : permitted_account_statuses statuses = truthy_param?(:pinned) ? pinned_scope : permitted_account_statuses
statuses = statuses.paginate_by_max_id( statuses = statuses.paginate_by_id(
limit_param(DEFAULT_STATUSES_LIMIT), limit_param(DEFAULT_STATUSES_LIMIT),
params[:max_id], params_slice(:max_id, :since_id, :min_id)
params[:since_id]
) )
statuses.merge!(only_media_scope) if truthy_param?(:only_media) statuses.merge!(only_media_scope) if truthy_param?(:only_media)
@ -82,7 +81,7 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
def prev_path def prev_path
unless @statuses.empty? unless @statuses.empty?
api_v1_account_statuses_url pagination_params(since_id: pagination_since_id) api_v1_account_statuses_url pagination_params(min_id: pagination_since_id)
end end
end end

View File

@ -0,0 +1,71 @@
# frozen_string_literal: true
class Api::V1::ConversationsController < Api::BaseController
LIMIT = 20
before_action -> { doorkeeper_authorize! :read, :'read:statuses' }, only: :index
before_action -> { doorkeeper_authorize! :write, :'write:conversations' }, except: :index
before_action :require_user!
before_action :set_conversation, except: :index
after_action :insert_pagination_headers, only: :index
respond_to :json
def index
@conversations = paginated_conversations
render json: @conversations, each_serializer: REST::ConversationSerializer
end
def read
@conversation.update!(unread: false)
render json: @conversation, serializer: REST::ConversationSerializer
end
def destroy
@conversation.destroy!
render_empty
end
private
def set_conversation
@conversation = AccountConversation.where(account: current_account).find(params[:id])
end
def paginated_conversations
AccountConversation.where(account: current_account)
.paginate_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
end
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def next_path
if records_continue?
api_v1_conversations_url pagination_params(max_id: pagination_max_id)
end
end
def prev_path
unless @conversations.empty?
api_v1_conversations_url pagination_params(min_id: pagination_since_id)
end
end
def pagination_max_id
@conversations.last.last_status_id
end
def pagination_since_id
@conversations.first.last_status_id
end
def records_continue?
@conversations.size == limit_param(LIMIT)
end
def pagination_params(core_params)
params.slice(:limit).permit(:limit).merge(core_params)
end
end

View File

@ -26,10 +26,9 @@ class Api::V1::FavouritesController < Api::BaseController
end end
def results def results
@_results ||= account_favourites.paginate_by_max_id( @_results ||= account_favourites.paginate_by_id(
limit_param(DEFAULT_STATUSES_LIMIT), limit_param(DEFAULT_STATUSES_LIMIT),
params[:max_id], params_slice(:max_id, :since_id, :min_id)
params[:since_id]
) )
end end
@ -49,7 +48,7 @@ class Api::V1::FavouritesController < Api::BaseController
def prev_path def prev_path
unless results.empty? unless results.empty?
api_v1_favourites_url pagination_params(since_id: pagination_since_id) api_v1_favourites_url pagination_params(min_id: pagination_since_id)
end end
end end

View File

@ -4,6 +4,8 @@ class Api::V1::InstancesController < Api::BaseController
respond_to :json respond_to :json
def show def show
render json: {}, serializer: REST::InstanceSerializer render_cached_json('api:v1:instances', expires_in: 5.minutes) do
ActiveModelSerializers::SerializableResource.new({}, serializer: REST::InstanceSerializer)
end
end end
end end

View File

@ -37,10 +37,9 @@ class Api::V1::NotificationsController < Api::BaseController
end end
def paginated_notifications def paginated_notifications
browserable_account_notifications.paginate_by_max_id( browserable_account_notifications.paginate_by_id(
limit_param(DEFAULT_NOTIFICATIONS_LIMIT), limit_param(DEFAULT_NOTIFICATIONS_LIMIT),
params[:max_id], params_slice(:max_id, :since_id, :min_id)
params[:since_id]
) )
end end
@ -64,7 +63,7 @@ class Api::V1::NotificationsController < Api::BaseController
def prev_path def prev_path
unless @notifications.empty? unless @notifications.empty?
api_v1_notifications_url pagination_params(since_id: pagination_since_id) api_v1_notifications_url pagination_params(min_id: pagination_since_id)
end end
end end

View File

@ -1,17 +1,11 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::V1::ReportsController < Api::BaseController class Api::V1::ReportsController < Api::BaseController
before_action -> { doorkeeper_authorize! :read, :'read:reports' }, except: [:create]
before_action -> { doorkeeper_authorize! :write, :'write:reports' }, only: [:create] before_action -> { doorkeeper_authorize! :write, :'write:reports' }, only: [:create]
before_action :require_user! before_action :require_user!
respond_to :json respond_to :json
def index
@reports = current_account.reports
render json: @reports, each_serializer: REST::ReportSerializer
end
def create def create
@report = ReportService.new.call( @report = ReportService.new.call(
current_account, current_account,
@ -27,7 +21,7 @@ class Api::V1::ReportsController < Api::BaseController
private private
def reported_status_ids def reported_status_ids
Status.find(status_ids).pluck(:id) reported_account.statuses.find(status_ids).pluck(:id)
end end
def status_ids def status_ids

View File

@ -30,7 +30,8 @@ class Api::V1::Timelines::HomeController < Api::BaseController
account_home_feed.get( account_home_feed.get(
limit_param(DEFAULT_STATUSES_LIMIT), limit_param(DEFAULT_STATUSES_LIMIT),
params[:max_id], params[:max_id],
params[:since_id] params[:since_id],
params[:min_id]
) )
end end
@ -51,7 +52,7 @@ class Api::V1::Timelines::HomeController < Api::BaseController
end end
def prev_path def prev_path
api_v1_timelines_home_url pagination_params(since_id: pagination_since_id) api_v1_timelines_home_url pagination_params(min_id: pagination_since_id)
end end
def pagination_max_id def pagination_max_id

View File

@ -32,7 +32,8 @@ class Api::V1::Timelines::ListController < Api::BaseController
list_feed.get( list_feed.get(
limit_param(DEFAULT_STATUSES_LIMIT), limit_param(DEFAULT_STATUSES_LIMIT),
params[:max_id], params[:max_id],
params[:since_id] params[:since_id],
params[:min_id]
) )
end end
@ -53,7 +54,7 @@ class Api::V1::Timelines::ListController < Api::BaseController
end end
def prev_path def prev_path
api_v1_timelines_list_url params[:id], pagination_params(since_id: pagination_since_id) api_v1_timelines_list_url params[:id], pagination_params(min_id: pagination_since_id)
end end
def pagination_max_id def pagination_max_id

View File

@ -21,10 +21,9 @@ class Api::V1::Timelines::PublicController < Api::BaseController
end end
def public_statuses def public_statuses
statuses = public_timeline_statuses.paginate_by_max_id( statuses = public_timeline_statuses.paginate_by_id(
limit_param(DEFAULT_STATUSES_LIMIT), limit_param(DEFAULT_STATUSES_LIMIT),
params[:max_id], params_slice(:max_id, :since_id, :min_id)
params[:since_id]
) )
if truthy_param?(:only_media) if truthy_param?(:only_media)
@ -53,7 +52,7 @@ class Api::V1::Timelines::PublicController < Api::BaseController
end end
def prev_path def prev_path
api_v1_timelines_public_url pagination_params(since_id: pagination_since_id) api_v1_timelines_public_url pagination_params(min_id: pagination_since_id)
end end
def pagination_max_id def pagination_max_id

View File

@ -29,10 +29,9 @@ class Api::V1::Timelines::TagController < Api::BaseController
if @tag.nil? if @tag.nil?
[] []
else else
statuses = tag_timeline_statuses.paginate_by_max_id( statuses = tag_timeline_statuses.paginate_by_id(
limit_param(DEFAULT_STATUSES_LIMIT), limit_param(DEFAULT_STATUSES_LIMIT),
params[:max_id], params_slice(:max_id, :since_id, :min_id)
params[:since_id]
) )
if truthy_param?(:only_media) if truthy_param?(:only_media)
@ -62,7 +61,7 @@ class Api::V1::Timelines::TagController < Api::BaseController
end end
def prev_path def prev_path
api_v1_timelines_tag_url params[:id], pagination_params(since_id: pagination_since_id) api_v1_timelines_tag_url params[:id], pagination_params(min_id: pagination_since_id)
end end
def pagination_max_id def pagination_max_id

View File

@ -58,6 +58,10 @@ class ApplicationController < ActionController::Base
protected protected
def truthy_param?(key)
ActiveModel::Type::Boolean.new.cast(params[key])
end
def forbidden def forbidden
respond_with_error(403) respond_with_error(403)
end end

View File

@ -10,6 +10,7 @@ class Auth::SessionsController < Devise::SessionsController
prepend_before_action :authenticate_with_two_factor, if: :two_factor_enabled?, only: [:create] prepend_before_action :authenticate_with_two_factor, if: :two_factor_enabled?, only: [:create]
before_action :set_instance_presenter, only: [:new] before_action :set_instance_presenter, only: [:new]
before_action :set_body_classes before_action :set_body_classes
after_action :clear_site_data, only: [:destroy]
def new def new
Devise.omniauth_configs.each do |provider, config| Devise.omniauth_configs.each do |provider, config|
@ -27,8 +28,10 @@ class Auth::SessionsController < Devise::SessionsController
end end
def destroy def destroy
tmp_stored_location = stored_location_for(:user)
super super
flash.delete(:notice) flash.delete(:notice)
store_location_for(:user, tmp_stored_location) if continue_after?
end end
protected protected
@ -121,4 +124,16 @@ class Auth::SessionsController < Devise::SessionsController
end end
paths paths
end end
def clear_site_data
return if continue_after?
# Should be '"*"' but that doesn't work in Chrome (neither does '"executionContexts"')
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Clear-Site-Data
response.headers['Clear-Site-Data'] = '"cache", "cookies", "storage"'
end
def continue_after?
truthy_param?(:continue)
end
end end

View File

@ -22,6 +22,12 @@ module SignatureVerification
return return
end end
if request.headers['Date'].present? && !matches_time_window?
@signature_verification_failure_reason = 'Signed request date outside acceptable time window'
@signed_request_account = nil
return
end
raw_signature = request.headers['Signature'] raw_signature = request.headers['Signature']
signature_params = {} signature_params = {}
@ -76,7 +82,7 @@ module SignatureVerification
def build_signed_string(signed_headers) def build_signed_string(signed_headers)
signed_headers = 'date' if signed_headers.blank? signed_headers = 'date' if signed_headers.blank?
signed_headers.split(' ').map do |signed_header| signed_headers.downcase.split(' ').map do |signed_header|
if signed_header == Request::REQUEST_TARGET if signed_header == Request::REQUEST_TARGET
"#{Request::REQUEST_TARGET}: #{request.method.downcase} #{request.path}" "#{Request::REQUEST_TARGET}: #{request.method.downcase} #{request.path}"
elsif signed_header == 'digest' elsif signed_header == 'digest'
@ -89,12 +95,12 @@ module SignatureVerification
def matches_time_window? def matches_time_window?
begin begin
time_sent = DateTime.httpdate(request.headers['Date']) time_sent = Time.httpdate(request.headers['Date'])
rescue ArgumentError rescue ArgumentError
return false return false
end end
(Time.now.utc - time_sent).abs <= 30 (Time.now.utc - time_sent).abs <= 12.hours
end end
def body_digest def body_digest

View File

@ -13,4 +13,18 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
def store_current_location def store_current_location
store_location_for(:user, request.url) store_location_for(:user, request.url)
end end
def render_success
if skip_authorization? || (matching_token? && !truthy_param?('force_login'))
redirect_or_render authorize_response
elsif Doorkeeper.configuration.api_only
render json: pre_auth
else
render :new
end
end
def truthy_param?(key)
ActiveModel::Type::Boolean.new.cast(params[key])
end
end end

View File

@ -41,7 +41,8 @@ class Settings::PreferencesController < ApplicationController
:setting_boost_modal, :setting_boost_modal,
:setting_delete_modal, :setting_delete_modal,
:setting_auto_play_gif, :setting_auto_play_gif,
:setting_display_sensitive_media, :setting_display_media,
:setting_expand_spoilers,
:setting_reduce_motion, :setting_reduce_motion,
:setting_system_font_ui, :setting_system_font_ui,
:setting_noindex, :setting_noindex,

View File

@ -19,6 +19,10 @@ class StatusesController < ApplicationController
before_action :set_referrer_policy_header, only: [:show] before_action :set_referrer_policy_header, only: [:show]
before_action :set_cache_headers before_action :set_cache_headers
content_security_policy only: :embed do |p|
p.frame_ancestors(false)
end
def show def show
respond_to do |format| respond_to do |format|
format.html do format.html do

View File

@ -4,7 +4,7 @@ module Admin::AccountModerationNotesHelper
def admin_account_link_to(account) def admin_account_link_to(account)
return if account.nil? return if account.nil?
link_to admin_account_path(account.id), class: name_tag_classes(account) do link_to admin_account_path(account.id), class: name_tag_classes(account), title: account.acct do
safe_join([ safe_join([
image_tag(account.avatar.url, width: 15, height: 15, alt: display_name(account), class: 'avatar'), image_tag(account.avatar.url, width: 15, height: 15, alt: display_name(account), class: 'avatar'),
content_tag(:span, account.acct, class: 'username'), content_tag(:span, account.acct, class: 'username'),
@ -15,7 +15,7 @@ module Admin::AccountModerationNotesHelper
def admin_account_inline_link_to(account) def admin_account_inline_link_to(account)
return if account.nil? return if account.nil?
link_to admin_account_path(account.id), class: name_tag_classes(account, true) do link_to admin_account_path(account.id), class: name_tag_classes(account, true), title: account.acct do
content_tag(:span, account.acct, class: 'username') content_tag(:span, account.acct, class: 'username')
end end
end end

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
module Admin::FilterHelper module Admin::FilterHelper
ACCOUNT_FILTERS = %i(local remote by_domain silenced suspended recent username display_name email ip staff).freeze ACCOUNT_FILTERS = %i(local remote by_domain silenced suspended alphabetic username display_name email ip staff).freeze
REPORT_FILTERS = %i(resolved account_id target_account_id).freeze REPORT_FILTERS = %i(resolved account_id target_account_id).freeze
INVITE_FILTER = %i(available expired).freeze INVITE_FILTER = %i(available expired).freeze
CUSTOM_EMOJI_FILTERS = %i(local remote by_domain shortcode).freeze CUSTOM_EMOJI_FILTERS = %i(local remote by_domain shortcode).freeze

View File

@ -7,8 +7,8 @@ module ApplicationHelper
follow follow
).freeze ).freeze
def active_nav_class(path) def active_nav_class(*paths)
current_page?(path) ? 'active' : '' paths.any? { |path| current_page?(path) } ? 'active' : ''
end end
def active_link_to(label, path, **options) def active_link_to(label, path, **options)
@ -81,4 +81,20 @@ module ApplicationHelper
output << 'rtl' if locale_direction == 'rtl' output << 'rtl' if locale_direction == 'rtl'
output.reject(&:blank?).join(' ') output.reject(&:blank?).join(' ')
end end
def cdn_host
Rails.configuration.action_controller.asset_host
end
def cdn_host?
cdn_host.present?
end
def storage_host
"https://#{ENV['S3_ALIAS_HOST'].presence || ENV['S3_CLOUDFRONT_HOST']}"
end
def storage_host?
ENV['S3_ALIAS_HOST'].present? || ENV['S3_CLOUDFRONT_HOST'].present?
end
end end

View File

@ -7,13 +7,13 @@ module HomeHelper
} }
end end
def account_link_to(account, button = '') def account_link_to(account, button = '', size: 36, path: nil)
content_tag(:div, class: 'account') do content_tag(:div, class: 'account') do
content_tag(:div, class: 'account__wrapper') do content_tag(:div, class: 'account__wrapper') do
section = if account.nil? section = if account.nil?
content_tag(:div, class: 'account__display-name') do content_tag(:div, class: 'account__display-name') do
content_tag(:div, class: 'account__avatar-wrapper') do content_tag(:div, class: 'account__avatar-wrapper') do
content_tag(:div, '', class: 'account__avatar', style: "background-image: url(#{full_asset_url('avatars/original/missing.png', skip_pipeline: true)})") content_tag(:div, '', class: 'account__avatar', style: "width: #{size}px; height: #{size}px; background-size: #{size}px #{size}px; background-image: url(#{full_asset_url('avatars/original/missing.png', skip_pipeline: true)})")
end + end +
content_tag(:span, class: 'display-name') do content_tag(:span, class: 'display-name') do
content_tag(:strong, t('about.contact_missing')) + content_tag(:strong, t('about.contact_missing')) +
@ -21,9 +21,9 @@ module HomeHelper
end end
end end
else else
link_to(TagManager.instance.url_for(account), class: 'account__display-name') do link_to(path || TagManager.instance.url_for(account), class: 'account__display-name') do
content_tag(:div, class: 'account__avatar-wrapper') do content_tag(:div, class: 'account__avatar-wrapper') do
content_tag(:div, '', class: 'account__avatar', style: "background-image: url(#{account.avatar.url})") content_tag(:div, '', class: 'account__avatar', style: "width: #{size}px; height: #{size}px; background-size: #{size}px #{size}px; background-image: url(#{account.avatar.url})")
end + end +
content_tag(:span, class: 'display-name') do content_tag(:span, class: 'display-name') do
content_tag(:bdi) do content_tag(:bdi) do
@ -48,4 +48,12 @@ module HomeHelper
'1+' '1+'
end end
end end
def custom_field_classes(field)
if field.verified?
'verified'
else
'emojify'
end
end
end end

View File

@ -8,6 +8,7 @@ module SettingsHelper
bg: 'Български', bg: 'Български',
ca: 'Català', ca: 'Català',
co: 'Corsu', co: 'Corsu',
cs: 'Čeština',
cy: 'Cymraeg', cy: 'Cymraeg',
da: 'Dansk', da: 'Dansk',
de: 'Deutsch', de: 'Deutsch',

View File

@ -57,7 +57,7 @@ export function changeCompose(text) {
}; };
}; };
export function replyCompose(status, router) { export function replyCompose(status, routerHistory) {
return (dispatch, getState) => { return (dispatch, getState) => {
dispatch({ dispatch({
type: COMPOSE_REPLY, type: COMPOSE_REPLY,
@ -65,7 +65,7 @@ export function replyCompose(status, router) {
}); });
if (!getState().getIn(['compose', 'mounted'])) { if (!getState().getIn(['compose', 'mounted'])) {
router.push('/statuses/new'); routerHistory.push('/statuses/new');
} }
}; };
}; };
@ -82,7 +82,7 @@ export function resetCompose() {
}; };
}; };
export function mentionCompose(account, router) { export function mentionCompose(account, routerHistory) {
return (dispatch, getState) => { return (dispatch, getState) => {
dispatch({ dispatch({
type: COMPOSE_MENTION, type: COMPOSE_MENTION,
@ -90,12 +90,12 @@ export function mentionCompose(account, router) {
}); });
if (!getState().getIn(['compose', 'mounted'])) { if (!getState().getIn(['compose', 'mounted'])) {
router.push('/statuses/new'); routerHistory.push('/statuses/new');
} }
}; };
}; };
export function directCompose(account, router) { export function directCompose(account, routerHistory) {
return (dispatch, getState) => { return (dispatch, getState) => {
dispatch({ dispatch({
type: COMPOSE_DIRECT, type: COMPOSE_DIRECT,
@ -103,12 +103,12 @@ export function directCompose(account, router) {
}); });
if (!getState().getIn(['compose', 'mounted'])) { if (!getState().getIn(['compose', 'mounted'])) {
router.push('/statuses/new'); routerHistory.push('/statuses/new');
} }
}; };
}; };
export function submitCompose() { export function submitCompose(routerHistory) {
return function (dispatch, getState) { return function (dispatch, getState) {
const status = getState().getIn(['compose', 'text'], ''); const status = getState().getIn(['compose', 'text'], '');
const media = getState().getIn(['compose', 'media_attachments']); const media = getState().getIn(['compose', 'media_attachments']);
@ -135,21 +135,22 @@ export function submitCompose() {
dispatch(insertIntoTagHistory(response.data.tags, status)); dispatch(insertIntoTagHistory(response.data.tags, status));
dispatch(submitComposeSuccess({ ...response.data })); dispatch(submitComposeSuccess({ ...response.data }));
// To make the app more responsive, immediately get the status into the columns // To make the app more responsive, immediately push the status
// into the columns
const insertIfOnline = (timelineId) => { const insertIfOnline = timelineId => {
if (getState().getIn(['timelines', timelineId, 'items', 0]) !== null) { if (getState().getIn(['timelines', timelineId, 'items', 0]) !== null) {
dispatch(updateTimeline(timelineId, { ...response.data })); dispatch(updateTimeline(timelineId, { ...response.data }));
} }
}; };
insertIfOnline('home'); if (response.data.visibility === 'direct' && getState().getIn(['conversations', 'mounted']) <= 0) {
routerHistory.push('/timelines/direct');
if (response.data.in_reply_to_id === null && response.data.visibility === 'public') { } else if (response.data.visibility !== 'direct') {
insertIfOnline('home');
} else if (response.data.in_reply_to_id === null && response.data.visibility === 'public') {
insertIfOnline('community'); insertIfOnline('community');
insertIfOnline('public'); insertIfOnline('public');
} else if (response.data.visibility === 'direct') {
insertIfOnline('direct');
} }
}).catch(function (error) { }).catch(function (error) {
dispatch(submitComposeFail(error)); dispatch(submitComposeFail(error));

View File

@ -0,0 +1,81 @@
import api, { getLinks } from '../api';
import {
importFetchedAccounts,
importFetchedStatuses,
importFetchedStatus,
} from './importer';
export const CONVERSATIONS_MOUNT = 'CONVERSATIONS_MOUNT';
export const CONVERSATIONS_UNMOUNT = 'CONVERSATIONS_UNMOUNT';
export const CONVERSATIONS_FETCH_REQUEST = 'CONVERSATIONS_FETCH_REQUEST';
export const CONVERSATIONS_FETCH_SUCCESS = 'CONVERSATIONS_FETCH_SUCCESS';
export const CONVERSATIONS_FETCH_FAIL = 'CONVERSATIONS_FETCH_FAIL';
export const CONVERSATIONS_UPDATE = 'CONVERSATIONS_UPDATE';
export const CONVERSATIONS_READ = 'CONVERSATIONS_READ';
export const mountConversations = () => ({
type: CONVERSATIONS_MOUNT,
});
export const unmountConversations = () => ({
type: CONVERSATIONS_UNMOUNT,
});
export const markConversationRead = conversationId => (dispatch, getState) => {
dispatch({
type: CONVERSATIONS_READ,
id: conversationId,
});
api(getState).post(`/api/v1/conversations/${conversationId}/read`);
};
export const expandConversations = ({ maxId } = {}) => (dispatch, getState) => {
dispatch(expandConversationsRequest());
const params = { max_id: maxId };
if (!maxId) {
params.since_id = getState().getIn(['conversations', 0, 'last_status']);
}
api(getState).get('/api/v1/conversations', { params })
.then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedAccounts(response.data.reduce((aggr, item) => aggr.concat(item.accounts), [])));
dispatch(importFetchedStatuses(response.data.map(item => item.last_status).filter(x => !!x)));
dispatch(expandConversationsSuccess(response.data, next ? next.uri : null));
})
.catch(err => dispatch(expandConversationsFail(err)));
};
export const expandConversationsRequest = () => ({
type: CONVERSATIONS_FETCH_REQUEST,
});
export const expandConversationsSuccess = (conversations, next) => ({
type: CONVERSATIONS_FETCH_SUCCESS,
conversations,
next,
});
export const expandConversationsFail = error => ({
type: CONVERSATIONS_FETCH_FAIL,
error,
});
export const updateConversations = conversation => dispatch => {
dispatch(importFetchedAccounts(conversation.accounts));
if (conversation.last_status) {
dispatch(importFetchedStatus(conversation.last_status));
}
dispatch({
type: CONVERSATIONS_UPDATE,
conversation,
});
};

View File

@ -1,8 +1,8 @@
export const DROPDOWN_MENU_OPEN = 'DROPDOWN_MENU_OPEN'; export const DROPDOWN_MENU_OPEN = 'DROPDOWN_MENU_OPEN';
export const DROPDOWN_MENU_CLOSE = 'DROPDOWN_MENU_CLOSE'; export const DROPDOWN_MENU_CLOSE = 'DROPDOWN_MENU_CLOSE';
export function openDropdownMenu(id, placement) { export function openDropdownMenu(id, placement, keyboard) {
return { type: DROPDOWN_MENU_OPEN, id, placement }; return { type: DROPDOWN_MENU_OPEN, id, placement, keyboard };
} }
export function closeDropdownMenu(id) { export function closeDropdownMenu(id) {

View File

@ -1,6 +1,7 @@
import escapeTextContentForBrowser from 'escape-html'; import escapeTextContentForBrowser from 'escape-html';
import emojify from '../../features/emoji/emoji'; import emojify from '../../features/emoji/emoji';
import { unescapeHTML } from '../../utils/html'; import { unescapeHTML } from '../../utils/html';
import { expandSpoilers } from '../../initial_state';
const domParser = new DOMParser(); const domParser = new DOMParser();
@ -57,7 +58,7 @@ export function normalizeStatus(status, normalOldStatus) {
normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent; normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent;
normalStatus.contentHtml = emojify(normalStatus.content, emojiMap); normalStatus.contentHtml = emojify(normalStatus.content, emojiMap);
normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(spoilerText), emojiMap); normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(spoilerText), emojiMap);
normalStatus.hidden = spoilerText.length > 0 || normalStatus.sensitive; normalStatus.hidden = expandSpoilers ? false : spoilerText.length > 0 || normalStatus.sensitive;
} }
return normalStatus; return normalStatus;

View File

@ -6,6 +6,7 @@ import {
disconnectTimeline, disconnectTimeline,
} from './timelines'; } from './timelines';
import { updateNotifications, expandNotifications } from './notifications'; import { updateNotifications, expandNotifications } from './notifications';
import { updateConversations } from './conversations';
import { fetchFilters } from './filters'; import { fetchFilters } from './filters';
import { getLocale } from '../locales'; import { getLocale } from '../locales';
@ -31,6 +32,9 @@ export function connectTimelineStream (timelineId, path, pollingRefresh = null)
case 'notification': case 'notification':
dispatch(updateNotifications(JSON.parse(data.payload), messages, locale)); dispatch(updateNotifications(JSON.parse(data.payload), messages, locale));
break; break;
case 'conversation':
dispatch(updateConversations(JSON.parse(data.payload)));
break;
case 'filters_changed': case 'filters_changed':
dispatch(fetchFilters()); dispatch(fetchFilters());
break; break;

View File

@ -76,7 +76,6 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) {
export const expandHomeTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId }, done); export const expandHomeTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId }, done);
export const expandPublicTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`public${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { max_id: maxId, only_media: !!onlyMedia }, done); export const expandPublicTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`public${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { max_id: maxId, only_media: !!onlyMedia }, done);
export const expandCommunityTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia }, done); export const expandCommunityTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia }, done);
export const expandDirectTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('direct', '/api/v1/timelines/direct', { max_id: maxId }, done);
export const expandAccountTimeline = (accountId, { maxId, withReplies } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, max_id: maxId }); export const expandAccountTimeline = (accountId, { maxId, withReplies } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, max_id: maxId });
export const expandAccountFeaturedTimeline = accountId => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true }); export const expandAccountFeaturedTimeline = accountId => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true });
export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true }); export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true });

View File

@ -0,0 +1,27 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<AutosuggestEmoji /> renders emoji with custom url 1`] = `
<div
className="autosuggest-emoji"
>
<img
alt="foobar"
className="emojione"
src="http://example.com/emoji.png"
/>
:foobar:
</div>
`;
exports[`<AutosuggestEmoji /> renders native emoji 1`] = `
<div
className="autosuggest-emoji"
>
<img
alt="💙"
className="emojione"
src="/emoji/1f499.svg"
/>
:foobar:
</div>
`;

View File

@ -3,7 +3,6 @@
exports[`<Button /> adds class "button-secondary" if props.secondary given 1`] = ` exports[`<Button /> adds class "button-secondary" if props.secondary given 1`] = `
<button <button
className="button button-secondary" className="button button-secondary"
disabled={undefined}
onClick={[Function]} onClick={[Function]}
style={ style={
Object { Object {
@ -18,7 +17,6 @@ exports[`<Button /> adds class "button-secondary" if props.secondary given 1`] =
exports[`<Button /> renders a button element 1`] = ` exports[`<Button /> renders a button element 1`] = `
<button <button
className="button" className="button"
disabled={undefined}
onClick={[Function]} onClick={[Function]}
style={ style={
Object { Object {
@ -48,7 +46,6 @@ exports[`<Button /> renders a disabled attribute if props.disabled given 1`] = `
exports[`<Button /> renders class="button--block" if props.block given 1`] = ` exports[`<Button /> renders class="button--block" if props.block given 1`] = `
<button <button
className="button button--block" className="button button--block"
disabled={undefined}
onClick={[Function]} onClick={[Function]}
style={ style={
Object { Object {
@ -63,7 +60,6 @@ exports[`<Button /> renders class="button--block" if props.block given 1`] = `
exports[`<Button /> renders the children 1`] = ` exports[`<Button /> renders the children 1`] = `
<button <button
className="button" className="button"
disabled={undefined}
onClick={[Function]} onClick={[Function]}
style={ style={
Object { Object {
@ -82,7 +78,6 @@ exports[`<Button /> renders the children 1`] = `
exports[`<Button /> renders the given text 1`] = ` exports[`<Button /> renders the given text 1`] = `
<button <button
className="button" className="button"
disabled={undefined}
onClick={[Function]} onClick={[Function]}
style={ style={
Object { Object {
@ -99,7 +94,6 @@ exports[`<Button /> renders the given text 1`] = `
exports[`<Button /> renders the props.text instead of children 1`] = ` exports[`<Button /> renders the props.text instead of children 1`] = `
<button <button
className="button" className="button"
disabled={undefined}
onClick={[Function]} onClick={[Function]}
style={ style={
Object { Object {

View File

@ -0,0 +1,29 @@
import React from 'react';
import renderer from 'react-test-renderer';
import AutosuggestEmoji from '../autosuggest_emoji';
describe('<AutosuggestEmoji />', () => {
it('renders native emoji', () => {
const emoji = {
native: '💙',
colons: ':foobar:',
};
const component = renderer.create(<AutosuggestEmoji emoji={emoji} />);
const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
it('renders emoji with custom url', () => {
const emoji = {
custom: true,
imageUrl: 'http://example.com/emoji.png',
native: 'foobar',
colons: ':foobar:',
};
const component = renderer.create(<AutosuggestEmoji emoji={emoji} />);
const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
});

View File

@ -19,8 +19,8 @@ const messages = defineMessages({
unmute_notifications: { id: 'account.unmute_notifications', defaultMessage: 'Unmute notifications from @{name}' }, unmute_notifications: { id: 'account.unmute_notifications', defaultMessage: 'Unmute notifications from @{name}' },
}); });
@injectIntl export default @injectIntl
export default class Account extends ImmutablePureComponent { class Account extends ImmutablePureComponent {
static propTypes = { static propTypes = {
account: ImmutablePropTypes.map.isRequired, account: ImmutablePropTypes.map.isRequired,

View File

@ -0,0 +1,96 @@
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { autoPlayGif } from '../initial_state';
export default class AvatarComposite extends React.PureComponent {
static propTypes = {
accounts: ImmutablePropTypes.list.isRequired,
animate: PropTypes.bool,
size: PropTypes.number.isRequired,
};
static defaultProps = {
animate: autoPlayGif,
};
renderItem (account, size, index) {
const { animate } = this.props;
let width = 50;
let height = 100;
let top = 'auto';
let left = 'auto';
let bottom = 'auto';
let right = 'auto';
if (size === 1) {
width = 100;
}
if (size === 4 || (size === 3 && index > 0)) {
height = 50;
}
if (size === 2) {
if (index === 0) {
right = '2px';
} else {
left = '2px';
}
} else if (size === 3) {
if (index === 0) {
right = '2px';
} else if (index > 0) {
left = '2px';
}
if (index === 1) {
bottom = '2px';
} else if (index > 1) {
top = '2px';
}
} else if (size === 4) {
if (index === 0 || index === 2) {
right = '2px';
}
if (index === 1 || index === 3) {
left = '2px';
}
if (index < 2) {
bottom = '2px';
} else {
top = '2px';
}
}
const style = {
left: left,
top: top,
right: right,
bottom: bottom,
width: `${width}%`,
height: `${height}%`,
backgroundSize: 'cover',
backgroundImage: `url(${account.get(animate ? 'avatar' : 'avatar_static')})`,
};
return (
<div key={account.get('id')} style={style} />
);
}
render() {
const { accounts, size } = this.props;
return (
<div className='account__avatar-composite' style={{ width: `${size}px`, height: `${size}px` }}>
{accounts.take(4).map((account, i) => this.renderItem(account, accounts.size, i))}
</div>
);
}
}

View File

@ -10,8 +10,8 @@ const messages = defineMessages({
moveRight: { id: 'column_header.moveRight_settings', defaultMessage: 'Move column to the right' }, moveRight: { id: 'column_header.moveRight_settings', defaultMessage: 'Move column to the right' },
}); });
@injectIntl export default @injectIntl
export default class ColumnHeader extends React.PureComponent { class ColumnHeader extends React.PureComponent {
static contextTypes = { static contextTypes = {
router: PropTypes.object, router: PropTypes.object,

View File

@ -5,14 +5,24 @@ export default class DisplayName extends React.PureComponent {
static propTypes = { static propTypes = {
account: ImmutablePropTypes.map.isRequired, account: ImmutablePropTypes.map.isRequired,
others: ImmutablePropTypes.list,
}; };
render () { render () {
const displayNameHtml = { __html: this.props.account.get('display_name_html') }; const { account, others } = this.props;
const displayNameHtml = { __html: account.get('display_name_html') };
let suffix;
if (others && others.size > 1) {
suffix = `+${others.size}`;
} else {
suffix = <span className='display-name__account'>@{account.get('acct')}</span>;
}
return ( return (
<span className='display-name'> <span className='display-name'>
<bdi><strong className='display-name__html' dangerouslySetInnerHTML={displayNameHtml} /></bdi> <span className='display-name__account'>@{this.props.account.get('acct')}</span> <bdi><strong className='display-name__html' dangerouslySetInnerHTML={displayNameHtml} /></bdi> {suffix}
</span> </span>
); );
} }

View File

@ -8,8 +8,8 @@ const messages = defineMessages({
unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' }, unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' },
}); });
@injectIntl export default @injectIntl
export default class Account extends ImmutablePureComponent { class Account extends ImmutablePureComponent {
static propTypes = { static propTypes = {
domain: PropTypes.string, domain: PropTypes.string,

View File

@ -23,6 +23,7 @@ class DropdownMenu extends React.PureComponent {
placement: PropTypes.string, placement: PropTypes.string,
arrowOffsetLeft: PropTypes.string, arrowOffsetLeft: PropTypes.string,
arrowOffsetTop: PropTypes.string, arrowOffsetTop: PropTypes.string,
openedViaKeyboard: PropTypes.bool,
}; };
static defaultProps = { static defaultProps = {
@ -42,13 +43,15 @@ class DropdownMenu extends React.PureComponent {
componentDidMount () { componentDidMount () {
document.addEventListener('click', this.handleDocumentClick, false); document.addEventListener('click', this.handleDocumentClick, false);
document.addEventListener('keydown', this.handleKeyDown, false);
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
if (this.focusedItem) this.focusedItem.focus(); if (this.focusedItem && this.props.openedViaKeyboard) this.focusedItem.focus();
this.setState({ mounted: true }); this.setState({ mounted: true });
} }
componentWillUnmount () { componentWillUnmount () {
document.removeEventListener('click', this.handleDocumentClick, false); document.removeEventListener('click', this.handleDocumentClick, false);
document.removeEventListener('keydown', this.handleKeyDown, false);
document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions); document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
} }
@ -62,13 +65,10 @@ class DropdownMenu extends React.PureComponent {
handleKeyDown = e => { handleKeyDown = e => {
const items = Array.from(this.node.getElementsByTagName('a')); const items = Array.from(this.node.getElementsByTagName('a'));
const index = items.indexOf(e.currentTarget); const index = items.indexOf(document.activeElement);
let element; let element;
switch(e.key) { switch(e.key) {
case 'Enter':
this.handleClick(e);
break;
case 'ArrowDown': case 'ArrowDown':
element = items[index+1]; element = items[index+1];
if (element) { if (element) {
@ -96,6 +96,12 @@ class DropdownMenu extends React.PureComponent {
} }
} }
handleItemKeyDown = e => {
if (e.key === 'Enter') {
this.handleClick(e);
}
}
handleClick = e => { handleClick = e => {
const i = Number(e.currentTarget.getAttribute('data-index')); const i = Number(e.currentTarget.getAttribute('data-index'));
const { action, to } = this.props.items[i]; const { action, to } = this.props.items[i];
@ -120,7 +126,7 @@ class DropdownMenu extends React.PureComponent {
return ( return (
<li className='dropdown-menu__item' key={`${text}-${i}`}> <li className='dropdown-menu__item' key={`${text}-${i}`}>
<a href={href} target='_blank' rel='noopener' role='button' tabIndex='0' ref={i === 0 ? this.setFocusRef : null} onClick={this.handleClick} onKeyDown={this.handleKeyDown} data-index={i}> <a href={href} target='_blank' rel='noopener' role='button' tabIndex='0' ref={i === 0 ? this.setFocusRef : null} onClick={this.handleClick} onKeyDown={this.handleItemKeyDown} data-index={i}>
{text} {text}
</a> </a>
</li> </li>
@ -170,6 +176,7 @@ export default class Dropdown extends React.PureComponent {
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
dropdownPlacement: PropTypes.string, dropdownPlacement: PropTypes.string,
openDropdownId: PropTypes.number, openDropdownId: PropTypes.number,
openedViaKeyboard: PropTypes.bool,
}; };
static defaultProps = { static defaultProps = {
@ -180,14 +187,14 @@ export default class Dropdown extends React.PureComponent {
id: id++, id: id++,
}; };
handleClick = ({ target }) => { handleClick = ({ target, type }) => {
if (this.state.id === this.props.openDropdownId) { if (this.state.id === this.props.openDropdownId) {
this.handleClose(); this.handleClose();
} else { } else {
const { top } = target.getBoundingClientRect(); const { top } = target.getBoundingClientRect();
const placement = top * 2 < innerHeight ? 'bottom' : 'top'; const placement = top * 2 < innerHeight ? 'bottom' : 'top';
this.props.onOpen(this.state.id, this.handleItemClick, placement); this.props.onOpen(this.state.id, this.handleItemClick, placement, type !== 'click');
} }
} }
@ -197,6 +204,11 @@ export default class Dropdown extends React.PureComponent {
handleKeyDown = e => { handleKeyDown = e => {
switch(e.key) { switch(e.key) {
case ' ':
case 'Enter':
this.handleClick(e);
e.preventDefault();
break;
case 'Escape': case 'Escape':
this.handleClose(); this.handleClose();
break; break;
@ -233,7 +245,7 @@ export default class Dropdown extends React.PureComponent {
} }
render () { render () {
const { icon, items, size, title, disabled, dropdownPlacement, openDropdownId } = this.props; const { icon, items, size, title, disabled, dropdownPlacement, openDropdownId, openedViaKeyboard } = this.props;
const open = this.state.id === openDropdownId; const open = this.state.id === openDropdownId;
return ( return (
@ -249,7 +261,7 @@ export default class Dropdown extends React.PureComponent {
/> />
<Overlay show={open} placement={dropdownPlacement} target={this.findTarget}> <Overlay show={open} placement={dropdownPlacement} target={this.findTarget}>
<DropdownMenu items={items} onClose={this.handleClose} /> <DropdownMenu items={items} onClose={this.handleClose} openedViaKeyboard={openedViaKeyboard} />
</Overlay> </Overlay>
</div> </div>
); );

View File

@ -6,8 +6,8 @@ const messages = defineMessages({
load_more: { id: 'status.load_more', defaultMessage: 'Load more' }, load_more: { id: 'status.load_more', defaultMessage: 'Load more' },
}); });
@injectIntl export default @injectIntl
export default class LoadGap extends React.PureComponent { class LoadGap extends React.PureComponent {
static propTypes = { static propTypes = {
disabled: PropTypes.bool, disabled: PropTypes.bool,

View File

@ -6,7 +6,7 @@ import IconButton from './icon_button';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { isIOS } from '../is_mobile'; import { isIOS } from '../is_mobile';
import classNames from 'classnames'; import classNames from 'classnames';
import { autoPlayGif, displaySensitiveMedia } from '../initial_state'; import { autoPlayGif, displayMedia } from '../initial_state';
const messages = defineMessages({ const messages = defineMessages({
toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: 'Toggle visibility' }, toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: 'Toggle visibility' },
@ -179,8 +179,8 @@ class Item extends React.PureComponent {
} }
@injectIntl export default @injectIntl
export default class MediaGallery extends React.PureComponent { class MediaGallery extends React.PureComponent {
static propTypes = { static propTypes = {
sensitive: PropTypes.bool, sensitive: PropTypes.bool,
@ -197,7 +197,7 @@ export default class MediaGallery extends React.PureComponent {
}; };
state = { state = {
visible: !this.props.sensitive || displaySensitiveMedia, visible: displayMedia !== 'hide_all' && !this.props.sensitive || displayMedia === 'show_all',
}; };
componentWillReceiveProps (nextProps) { componentWillReceiveProps (nextProps) {

View File

@ -86,8 +86,8 @@ export const timeAgoString = (intl, date, now, year) => {
return relativeTime; return relativeTime;
}; };
@injectIntl export default @injectIntl
export default class RelativeTimestamp extends React.Component { class RelativeTimestamp extends React.Component {
static propTypes = { static propTypes = {
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,

View File

@ -3,6 +3,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Avatar from './avatar'; import Avatar from './avatar';
import AvatarOverlay from './avatar_overlay'; import AvatarOverlay from './avatar_overlay';
import AvatarComposite from './avatar_composite';
import RelativeTimestamp from './relative_timestamp'; import RelativeTimestamp from './relative_timestamp';
import DisplayName from './display_name'; import DisplayName from './display_name';
import StatusContent from './status_content'; import StatusContent from './status_content';
@ -35,8 +36,8 @@ export const textForScreenReader = (intl, status, rebloggedByText = false) => {
return values.join(', '); return values.join(', ');
}; };
@injectIntl export default @injectIntl
export default class Status extends ImmutablePureComponent { class Status extends ImmutablePureComponent {
static contextTypes = { static contextTypes = {
router: PropTypes.object, router: PropTypes.object,
@ -45,6 +46,8 @@ export default class Status extends ImmutablePureComponent {
static propTypes = { static propTypes = {
status: ImmutablePropTypes.map, status: ImmutablePropTypes.map,
account: ImmutablePropTypes.map, account: ImmutablePropTypes.map,
otherAccounts: ImmutablePropTypes.list,
onClick: PropTypes.func,
onReply: PropTypes.func, onReply: PropTypes.func,
onFavourite: PropTypes.func, onFavourite: PropTypes.func,
onReblog: PropTypes.func, onReblog: PropTypes.func,
@ -60,6 +63,7 @@ export default class Status extends ImmutablePureComponent {
onToggleHidden: PropTypes.func, onToggleHidden: PropTypes.func,
muted: PropTypes.bool, muted: PropTypes.bool,
hidden: PropTypes.bool, hidden: PropTypes.bool,
unread: PropTypes.bool,
onMoveUp: PropTypes.func, onMoveUp: PropTypes.func,
onMoveDown: PropTypes.func, onMoveDown: PropTypes.func,
}; };
@ -74,6 +78,11 @@ export default class Status extends ImmutablePureComponent {
] ]
handleClick = () => { handleClick = () => {
if (this.props.onClick) {
this.props.onClick();
return;
}
if (!this.context.router) { if (!this.context.router) {
return; return;
} }
@ -158,7 +167,7 @@ export default class Status extends ImmutablePureComponent {
let media = null; let media = null;
let statusAvatar, prepend, rebloggedByText; let statusAvatar, prepend, rebloggedByText;
const { intl, hidden, featured } = this.props; const { intl, hidden, featured, otherAccounts, unread } = this.props;
let { status, account, ...other } = this.props; let { status, account, ...other } = this.props;
@ -249,9 +258,11 @@ export default class Status extends ImmutablePureComponent {
} }
} }
if (account === undefined || account === null) { if (otherAccounts) {
statusAvatar = <AvatarComposite accounts={otherAccounts} size={48} />;
} else if (account === undefined || account === null) {
statusAvatar = <Avatar account={status.get('account')} size={48} />; statusAvatar = <Avatar account={status.get('account')} size={48} />;
}else{ } else {
statusAvatar = <AvatarOverlay account={status.get('account')} friend={account} />; statusAvatar = <AvatarOverlay account={status.get('account')} friend={account} />;
} }
@ -269,10 +280,10 @@ export default 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'), 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'))}>
{prepend} {prepend}
<div className={classNames('status', `status-${status.get('visibility')}`, { 'status-reply': !!status.get('in_reply_to_id'), muted: this.props.muted })} 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')}>
<div className='status__info'> <div className='status__info'>
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a> <a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a>
@ -281,11 +292,11 @@ export default class Status extends ImmutablePureComponent {
{statusAvatar} {statusAvatar}
</div> </div>
<DisplayName account={status.get('account')} /> <DisplayName account={status.get('account')} others={otherAccounts} />
</a> </a>
</div> </div>
<StatusContent status={status} onClick={this.handleClick} expanded={!status.get('hidden')} onExpandedToggle={this.handleExpandedToggle} /> <StatusContent status={status} onClick={this.handleClick} expanded={!status.get('hidden')} onExpandedToggle={this.handleExpandedToggle} collapsable />
{media} {media}

View File

@ -43,8 +43,8 @@ const obfuscatedCount = count => {
} }
}; };
@injectIntl export default @injectIntl
export default class StatusActionBar extends ImmutablePureComponent { class StatusActionBar extends ImmutablePureComponent {
static contextTypes = { static contextTypes = {
router: PropTypes.object, router: PropTypes.object,

View File

@ -6,6 +6,8 @@ import { FormattedMessage } from 'react-intl';
import Permalink from './permalink'; import Permalink from './permalink';
import classnames from 'classnames'; import classnames from 'classnames';
const MAX_HEIGHT = 642; // 20px * 32 (+ 2px padding at the top)
export default class StatusContent extends React.PureComponent { export default class StatusContent extends React.PureComponent {
static contextTypes = { static contextTypes = {
@ -17,10 +19,12 @@ export default class StatusContent extends React.PureComponent {
expanded: PropTypes.bool, expanded: PropTypes.bool,
onExpandedToggle: PropTypes.func, onExpandedToggle: PropTypes.func,
onClick: PropTypes.func, onClick: PropTypes.func,
collapsable: PropTypes.bool,
}; };
state = { state = {
hidden: true, hidden: true,
collapsed: null, // `collapsed: null` indicates that an element doesn't need collapsing, while `true` or `false` indicates that it does (and is/isn't).
}; };
_updateStatusLinks () { _updateStatusLinks () {
@ -53,6 +57,16 @@ export default class StatusContent extends React.PureComponent {
link.setAttribute('target', '_blank'); link.setAttribute('target', '_blank');
link.setAttribute('rel', 'noopener'); link.setAttribute('rel', 'noopener');
} }
if (
this.props.collapsable
&& this.props.onClick
&& this.state.collapsed === null
&& node.clientHeight > MAX_HEIGHT
&& this.props.status.get('spoiler_text').length === 0
) {
this.setState({ collapsed: true });
}
} }
componentDidMount () { componentDidMount () {
@ -113,6 +127,11 @@ export default class StatusContent extends React.PureComponent {
} }
} }
handleCollapsedClick = (e) => {
e.preventDefault();
this.setState({ collapsed: !this.state.collapsed });
}
setRef = (c) => { setRef = (c) => {
this.node = c; this.node = c;
} }
@ -132,12 +151,19 @@ export default class StatusContent extends React.PureComponent {
const classNames = classnames('status__content', { const classNames = classnames('status__content', {
'status__content--with-action': this.props.onClick && this.context.router, 'status__content--with-action': this.props.onClick && this.context.router,
'status__content--with-spoiler': status.get('spoiler_text').length > 0, 'status__content--with-spoiler': status.get('spoiler_text').length > 0,
'status__content--collapsed': this.state.collapsed === true,
}); });
if (isRtl(status.get('search_index'))) { if (isRtl(status.get('search_index'))) {
directionStyle.direction = 'rtl'; directionStyle.direction = 'rtl';
} }
const readMoreButton = (
<button className='status__content__read-more-button' onClick={this.props.onClick}>
<FormattedMessage id='status.read_more' defaultMessage='Read more' /><i className='fa fa-fw fa-angle-right' />
</button>
);
if (status.get('spoiler_text').length > 0) { if (status.get('spoiler_text').length > 0) {
let mentionsPlaceholder = ''; let mentionsPlaceholder = '';
@ -167,17 +193,23 @@ export default class StatusContent extends React.PureComponent {
</div> </div>
); );
} else if (this.props.onClick) { } else if (this.props.onClick) {
return ( const output = [
<div <div
ref={this.setRef} ref={this.setRef}
tabIndex='0' tabIndex='0'
className={classNames} className={classNames}
style={directionStyle} style={directionStyle}
dangerouslySetInnerHTML={content}
onMouseDown={this.handleMouseDown} onMouseDown={this.handleMouseDown}
onMouseUp={this.handleMouseUp} onMouseUp={this.handleMouseUp}
dangerouslySetInnerHTML={content} />,
/> ];
);
if (this.state.collapsed) {
output.push(readMoreButton);
}
return output;
} else { } else {
return ( return (
<div <div

View File

@ -8,15 +8,16 @@ const mapStateToProps = state => ({
isModalOpen: state.get('modal').modalType === 'ACTIONS', isModalOpen: state.get('modal').modalType === 'ACTIONS',
dropdownPlacement: state.getIn(['dropdown_menu', 'placement']), dropdownPlacement: state.getIn(['dropdown_menu', 'placement']),
openDropdownId: state.getIn(['dropdown_menu', 'openId']), openDropdownId: state.getIn(['dropdown_menu', 'openId']),
openedViaKeyboard: state.getIn(['dropdown_menu', 'keyboard']),
}); });
const mapDispatchToProps = (dispatch, { status, items }) => ({ const mapDispatchToProps = (dispatch, { status, items }) => ({
onOpen(id, onItemClick, dropdownPlacement) { onOpen(id, onItemClick, dropdownPlacement, keyboard) {
dispatch(isUserTouching() ? openModal('ACTIONS', { dispatch(isUserTouching() ? openModal('ACTIONS', {
status, status,
actions: items, actions: items,
onClick: onItemClick, onClick: onItemClick,
}) : openDropdownMenu(id, dropdownPlacement)); }) : openDropdownMenu(id, dropdownPlacement, keyboard));
}, },
onClose(id) { onClose(id) {
dispatch(closeModal()); dispatch(closeModal());

View File

@ -36,6 +36,8 @@ const messages = defineMessages({
redraftConfirm: { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' }, redraftConfirm: { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' },
redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.' }, redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.' },
blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' }, blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' },
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
}); });
const makeMapStateToProps = () => { const makeMapStateToProps = () => {
@ -51,7 +53,18 @@ const makeMapStateToProps = () => {
const mapDispatchToProps = (dispatch, { intl }) => ({ const mapDispatchToProps = (dispatch, { intl }) => ({
onReply (status, router) { onReply (status, router) {
dispatch(replyCompose(status, router)); dispatch((_, getState) => {
let state = getState();
if (state.getIn(['compose', 'text']).trim().length !== 0) {
dispatch(openModal('CONFIRM', {
message: intl.formatMessage(messages.replyMessage),
confirm: intl.formatMessage(messages.replyConfirm),
onConfirm: () => dispatch(replyCompose(status, router)),
}));
} else {
dispatch(replyCompose(status, router));
}
});
}, },
onModalReblog (status) { onModalReblog (status) {

View File

@ -2,7 +2,7 @@ import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import DropdownMenuContainer from '../../../containers/dropdown_menu_container'; import DropdownMenuContainer from '../../../containers/dropdown_menu_container';
import { Link } from 'react-router-dom'; import { NavLink } from 'react-router-dom';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { me } from '../../../initial_state'; import { me } from '../../../initial_state';
import { shortNumberFormat } from '../../../utils/numbers'; import { shortNumberFormat } from '../../../utils/numbers';
@ -36,8 +36,8 @@ const messages = defineMessages({
unendorse: { id: 'account.unendorse', defaultMessage: 'Don\'t feature on profile' }, unendorse: { id: 'account.unendorse', defaultMessage: 'Don\'t feature on profile' },
}); });
@injectIntl export default @injectIntl
export default class ActionBar extends React.PureComponent { class ActionBar extends React.PureComponent {
static propTypes = { static propTypes = {
account: ImmutablePropTypes.map.isRequired, account: ImmutablePropTypes.map.isRequired,
@ -60,6 +60,13 @@ export default class ActionBar extends React.PureComponent {
}); });
} }
isStatusesPageActive = (match, location) => {
if (!match) {
return false;
}
return !location.pathname.match(/\/(followers|following)\/?$/);
}
render () { render () {
const { account, intl } = this.props; const { account, intl } = this.props;
@ -147,20 +154,20 @@ export default class ActionBar extends React.PureComponent {
<div className='account__action-bar'> <div className='account__action-bar'>
<div className='account__action-bar-links'> <div className='account__action-bar-links'>
<Link className='account__action-bar__tab' to={`/accounts/${account.get('id')}`} title={intl.formatNumber(account.get('statuses_count'))}> <NavLink isActive={this.isStatusesPageActive} activeClassName='active' className='account__action-bar__tab' to={`/accounts/${account.get('id')}`} title={intl.formatNumber(account.get('statuses_count'))}>
<FormattedMessage id='account.posts' defaultMessage='Toots' /> <FormattedMessage id='account.posts' defaultMessage='Toots' />
<strong>{shortNumberFormat(account.get('statuses_count'))}</strong> <strong>{shortNumberFormat(account.get('statuses_count'))}</strong>
</Link> </NavLink>
<Link className='account__action-bar__tab' to={`/accounts/${account.get('id')}/following`} title={intl.formatNumber(account.get('following_count'))}> <NavLink exact activeClassName='active' className='account__action-bar__tab' to={`/accounts/${account.get('id')}/following`} title={intl.formatNumber(account.get('following_count'))}>
<FormattedMessage id='account.follows' defaultMessage='Follows' /> <FormattedMessage id='account.follows' defaultMessage='Follows' />
<strong>{shortNumberFormat(account.get('following_count'))}</strong> <strong>{shortNumberFormat(account.get('following_count'))}</strong>
</Link> </NavLink>
<Link className='account__action-bar__tab' to={`/accounts/${account.get('id')}/followers`} title={intl.formatNumber(account.get('followers_count'))}> <NavLink exact activeClassName='active' className='account__action-bar__tab' to={`/accounts/${account.get('id')}/followers`} title={intl.formatNumber(account.get('followers_count'))}>
<FormattedMessage id='account.followers' defaultMessage='Followers' /> <FormattedMessage id='account.followers' defaultMessage='Followers' />
<strong>{shortNumberFormat(account.get('followers_count'))}</strong> <strong>{shortNumberFormat(account.get('followers_count'))}</strong>
</Link> </NavLink>
</div> </div>
<div className='account__action-bar-dropdown'> <div className='account__action-bar-dropdown'>

View File

@ -15,8 +15,18 @@ const messages = defineMessages({
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' }, requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' },
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' }, edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
linkVerifiedOn: { id: 'account.link_verified_on', defaultMessage: 'Ownership of this link was checked on {date}' },
}); });
const dateFormatOptions = {
month: 'short',
day: 'numeric',
year: 'numeric',
hour12: false,
hour: '2-digit',
minute: '2-digit',
};
class Avatar extends ImmutablePureComponent { class Avatar extends ImmutablePureComponent {
static propTypes = { static propTypes = {
@ -65,8 +75,8 @@ class Avatar extends ImmutablePureComponent {
} }
@injectIntl export default @injectIntl
export default class Header extends ImmutablePureComponent { class Header extends ImmutablePureComponent {
static propTypes = { static propTypes = {
account: ImmutablePropTypes.map, account: ImmutablePropTypes.map,
@ -163,7 +173,10 @@ export default class Header extends ImmutablePureComponent {
{fields.map((pair, i) => ( {fields.map((pair, i) => (
<dl key={i}> <dl key={i}>
<dt dangerouslySetInnerHTML={{ __html: pair.get('name_emojified') }} title={pair.get('name')} /> <dt dangerouslySetInnerHTML={{ __html: pair.get('name_emojified') }} title={pair.get('name')} />
<dd dangerouslySetInnerHTML={{ __html: pair.get('value_emojified') }} title={pair.get('value_plain')} />
<dd className={pair.get('verified_at') && 'verified'} title={pair.get('value_plain')}>
{pair.get('verified_at') && <span title={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(pair.get('verified_at'), dateFormatOptions) })}><i className='fa fa-check verified__mark' /></span>} <span dangerouslySetInnerHTML={{ __html: pair.get('value_emojified') }} />
</dd>
</dl> </dl>
))} ))}
</div> </div>

View File

@ -2,7 +2,7 @@ import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import Permalink from '../../../components/permalink'; import Permalink from '../../../components/permalink';
import { displaySensitiveMedia } from '../../../initial_state'; import { displayMedia } from '../../../initial_state';
export default class MediaItem extends ImmutablePureComponent { export default class MediaItem extends ImmutablePureComponent {
@ -11,7 +11,7 @@ export default class MediaItem extends ImmutablePureComponent {
}; };
state = { state = {
visible: !this.props.media.getIn(['status', 'sensitive']) || displaySensitiveMedia, visible: displayMedia !== 'hide_all' && !this.props.media.getIn(['status', 'sensitive']) || displayMedia === 'show_all',
}; };
handleClick = () => { handleClick = () => {

View File

@ -43,8 +43,8 @@ class LoadMoreMedia extends ImmutablePureComponent {
} }
@connect(mapStateToProps) export default @connect(mapStateToProps)
export default class AccountGallery extends ImmutablePureComponent { class AccountGallery extends ImmutablePureComponent {
static propTypes = { static propTypes = {
params: PropTypes.object.isRequired, params: PropTypes.object.isRequired,

View File

@ -23,8 +23,8 @@ const mapStateToProps = (state, { params: { accountId }, withReplies = false })
}; };
}; };
@connect(mapStateToProps) export default @connect(mapStateToProps)
export default class AccountTimeline extends ImmutablePureComponent { class AccountTimeline extends ImmutablePureComponent {
static propTypes = { static propTypes = {
params: PropTypes.object.isRequired, params: PropTypes.object.isRequired,

View File

@ -20,9 +20,9 @@ const mapStateToProps = state => ({
accountIds: state.getIn(['user_lists', 'blocks', 'items']), accountIds: state.getIn(['user_lists', 'blocks', 'items']),
}); });
@connect(mapStateToProps) export default @connect(mapStateToProps)
@injectIntl @injectIntl
export default class Blocks extends ImmutablePureComponent { class Blocks extends ImmutablePureComponent {
static propTypes = { static propTypes = {
params: PropTypes.object.isRequired, params: PropTypes.object.isRequired,

View File

@ -4,8 +4,8 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import { injectIntl, FormattedMessage } from 'react-intl'; import { injectIntl, FormattedMessage } from 'react-intl';
import SettingToggle from '../../notifications/components/setting_toggle'; import SettingToggle from '../../notifications/components/setting_toggle';
@injectIntl export default @injectIntl
export default class ColumnSettings extends React.PureComponent { class ColumnSettings extends React.PureComponent {
static propTypes = { static propTypes = {
settings: ImmutablePropTypes.map.isRequired, settings: ImmutablePropTypes.map.isRequired,

View File

@ -25,9 +25,9 @@ const mapStateToProps = (state, { onlyMedia, columnId }) => {
}; };
}; };
@connect(mapStateToProps) export default @connect(mapStateToProps)
@injectIntl @injectIntl
export default class CommunityTimeline extends React.PureComponent { class CommunityTimeline extends React.PureComponent {
static contextTypes = { static contextTypes = {
router: PropTypes.object, router: PropTypes.object,

View File

@ -17,8 +17,8 @@ const messages = defineMessages({
filters: { id: 'navigation_bar.filters', defaultMessage: 'Muted words' }, filters: { id: 'navigation_bar.filters', defaultMessage: 'Muted words' },
}); });
@injectIntl export default @injectIntl
export default class ActionBar extends React.PureComponent { class ActionBar extends React.PureComponent {
static propTypes = { static propTypes = {
account: ImmutablePropTypes.map.isRequired, account: ImmutablePropTypes.map.isRequired,

View File

@ -28,8 +28,12 @@ const messages = defineMessages({
publishLoud: { id: 'compose_form.publish_loud', defaultMessage: '{publish}!' }, publishLoud: { id: 'compose_form.publish_loud', defaultMessage: '{publish}!' },
}); });
@injectIntl export default @injectIntl
export default class ComposeForm extends ImmutablePureComponent { class ComposeForm extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object,
};
static propTypes = { static propTypes = {
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
@ -86,7 +90,7 @@ export default class ComposeForm extends ImmutablePureComponent {
return; return;
} }
this.props.onSubmit(); this.props.onSubmit(this.context.router.history);
} }
onSuggestionsClearRequested = () => { onSuggestionsClearRequested = () => {

View File

@ -261,6 +261,7 @@ class EmojiPickerMenu extends React.PureComponent {
skin={skinTone} skin={skinTone}
showPreview={false} showPreview={false}
backgroundImageFn={backgroundImageFn} backgroundImageFn={backgroundImageFn}
autoFocus
emojiTooltip emojiTooltip
/> />
@ -277,8 +278,8 @@ class EmojiPickerMenu extends React.PureComponent {
} }
@injectIntl export default @injectIntl
export default class EmojiPickerDropdown extends React.PureComponent { class EmojiPickerDropdown extends React.PureComponent {
static propTypes = { static propTypes = {
custom_emojis: ImmutablePropTypes.list, custom_emojis: ImmutablePropTypes.list,

View File

@ -149,8 +149,8 @@ class PrivacyDropdownMenu extends React.PureComponent {
} }
@injectIntl export default @injectIntl
export default class PrivacyDropdown extends React.PureComponent { class PrivacyDropdown extends React.PureComponent {
static propTypes = { static propTypes = {
isUserTouching: PropTypes.func, isUserTouching: PropTypes.func,
@ -164,7 +164,7 @@ export default class PrivacyDropdown extends React.PureComponent {
state = { state = {
open: false, open: false,
placement: null, placement: 'bottom',
}; };
handleToggle = ({ target }) => { handleToggle = ({ target }) => {

View File

@ -12,8 +12,8 @@ const messages = defineMessages({
cancel: { id: 'reply_indicator.cancel', defaultMessage: 'Cancel' }, cancel: { id: 'reply_indicator.cancel', defaultMessage: 'Cancel' },
}); });
@injectIntl export default @injectIntl
export default class ReplyIndicator extends ImmutablePureComponent { class ReplyIndicator extends ImmutablePureComponent {
static contextTypes = { static contextTypes = {
router: PropTypes.object, router: PropTypes.object,

View File

@ -43,8 +43,8 @@ class SearchPopout extends React.PureComponent {
} }
@injectIntl export default @injectIntl
export default class Search extends React.PureComponent { class Search extends React.PureComponent {
static propTypes = { static propTypes = {
value: PropTypes.string.isRequired, value: PropTypes.string.isRequired,

View File

@ -11,8 +11,12 @@ const messages = defineMessages({
description: { id: 'upload_form.description', defaultMessage: 'Describe for the visually impaired' }, description: { id: 'upload_form.description', defaultMessage: 'Describe for the visually impaired' },
}); });
@injectIntl export default @injectIntl
export default class Upload extends ImmutablePureComponent { class Upload extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object,
};
static propTypes = { static propTypes = {
media: ImmutablePropTypes.map.isRequired, media: ImmutablePropTypes.map.isRequired,
@ -37,7 +41,7 @@ export default class Upload extends ImmutablePureComponent {
handleSubmit = () => { handleSubmit = () => {
this.handleInputBlur(); this.handleInputBlur();
this.props.onSubmit(); this.props.onSubmit(this.context.router.history);
} }
handleUndoClick = () => { handleUndoClick = () => {

View File

@ -23,9 +23,9 @@ const iconStyle = {
lineHeight: '27px', lineHeight: '27px',
}; };
@connect(makeMapStateToProps) export default @connect(makeMapStateToProps)
@injectIntl @injectIntl
export default class UploadButton extends ImmutablePureComponent { class UploadButton extends ImmutablePureComponent {
static propTypes = { static propTypes = {
disabled: PropTypes.bool, disabled: PropTypes.bool,

View File

@ -34,8 +34,8 @@ const mapDispatchToProps = (dispatch) => ({
dispatch(changeCompose(text)); dispatch(changeCompose(text));
}, },
onSubmit () { onSubmit (router) {
dispatch(submitCompose()); dispatch(submitCompose(router));
}, },
onClearSuggestions () { onClearSuggestions () {

View File

@ -22,8 +22,8 @@ const mapDispatchToProps = dispatch => ({
dispatch(openModal('FOCAL_POINT', { id })); dispatch(openModal('FOCAL_POINT', { id }));
}, },
onSubmit () { onSubmit (router) {
dispatch(submitCompose()); dispatch(submitCompose(router));
}, },
}); });

View File

@ -13,6 +13,7 @@ import spring from 'react-motion/lib/spring';
import SearchResultsContainer from './containers/search_results_container'; import SearchResultsContainer from './containers/search_results_container';
import { changeComposing } from '../../actions/compose'; import { changeComposing } from '../../actions/compose';
import elephantUIPlane from '../../../images/elephant_ui_plane.svg'; import elephantUIPlane from '../../../images/elephant_ui_plane.svg';
import { mascot } from '../../initial_state';
const messages = defineMessages({ const messages = defineMessages({
start: { id: 'getting_started.heading', defaultMessage: 'Getting started' }, start: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
@ -30,9 +31,9 @@ const mapStateToProps = (state, ownProps) => ({
showSearch: ownProps.multiColumn ? state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']) : ownProps.isSearchPage, showSearch: ownProps.multiColumn ? state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']) : ownProps.isSearchPage,
}); });
@connect(mapStateToProps) export default @connect(mapStateToProps)
@injectIntl @injectIntl
export default class Compose extends React.PureComponent { class Compose extends React.PureComponent {
static propTypes = { static propTypes = {
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
@ -107,7 +108,7 @@ export default class Compose extends React.PureComponent {
<ComposeFormContainer /> <ComposeFormContainer />
{multiColumn && ( {multiColumn && (
<div className='drawer__inner__mastodon'> <div className='drawer__inner__mastodon'>
<img alt='' draggable='false' src={elephantUIPlane} /> <img alt='' draggable='false' src={mascot || elephantUIPlane} />
</div> </div>
)} )}
</div>} </div>}

View File

@ -170,7 +170,7 @@ export const urlRegex = (function() {
')' + ')' +
'\\)', '\\)',
'i'); 'i');
// Valid end-of-path chracters (so /foo. does not gobble the period). // Valid end-of-path characters (so /foo. does not gobble the period).
// 1. Allow =&# for empty URL parameters and other URL-join artifacts // 1. Allow =&# for empty URL parameters and other URL-join artifacts
regexen.validUrlPathEndingChars = regexSupplant(/[^#{spaces_group}\(\)\?!\*';:=\,\.\$%\[\]#{pd}~&\|@]|(?:#{validUrlBalancedParens})/i); regexen.validUrlPathEndingChars = regexSupplant(/[^#{spaces_group}\(\)\?!\*';:=\,\.\$%\[\]#{pd}~&\|@]|(?:#{validUrlBalancedParens})/i);
// Allow @ in a url, but only in the middle. Catch things like http://example.com/@user/ // Allow @ in a url, but only in the middle. Catch things like http://example.com/@user/

View File

@ -9,8 +9,8 @@ const messages = defineMessages({
settings: { id: 'home.settings', defaultMessage: 'Column settings' }, settings: { id: 'home.settings', defaultMessage: 'Column settings' },
}); });
@injectIntl export default @injectIntl
export default class ColumnSettings extends React.PureComponent { class ColumnSettings extends React.PureComponent {
static propTypes = { static propTypes = {
settings: ImmutablePropTypes.map.isRequired, settings: ImmutablePropTypes.map.isRequired,

View File

@ -0,0 +1,63 @@
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import StatusContainer from '../../../containers/status_container';
export default class Conversation extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object,
};
static propTypes = {
conversationId: PropTypes.string.isRequired,
accounts: ImmutablePropTypes.list.isRequired,
lastStatusId: PropTypes.string,
unread:PropTypes.bool.isRequired,
onMoveUp: PropTypes.func,
onMoveDown: PropTypes.func,
markRead: PropTypes.func.isRequired,
};
handleClick = () => {
if (!this.context.router) {
return;
}
const { lastStatusId, unread, markRead } = this.props;
if (unread) {
markRead();
}
this.context.router.history.push(`/statuses/${lastStatusId}`);
}
handleHotkeyMoveUp = () => {
this.props.onMoveUp(this.props.conversationId);
}
handleHotkeyMoveDown = () => {
this.props.onMoveDown(this.props.conversationId);
}
render () {
const { accounts, lastStatusId, unread } = this.props;
if (lastStatusId === null) {
return null;
}
return (
<StatusContainer
id={lastStatusId}
unread={unread}
otherAccounts={accounts}
onMoveUp={this.handleHotkeyMoveUp}
onMoveDown={this.handleHotkeyMoveDown}
/>
);
}
}

View File

@ -0,0 +1,68 @@
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import ConversationContainer from '../containers/conversation_container';
import ScrollableList from '../../../components/scrollable_list';
import { debounce } from 'lodash';
export default class ConversationsList extends ImmutablePureComponent {
static propTypes = {
conversationIds: ImmutablePropTypes.list.isRequired,
hasMore: PropTypes.bool,
isLoading: PropTypes.bool,
onLoadMore: PropTypes.func,
shouldUpdateScroll: PropTypes.func,
};
getCurrentIndex = id => this.props.conversationIds.indexOf(id)
handleMoveUp = id => {
const elementIndex = this.getCurrentIndex(id) - 1;
this._selectChild(elementIndex);
}
handleMoveDown = id => {
const elementIndex = this.getCurrentIndex(id) + 1;
this._selectChild(elementIndex);
}
_selectChild (index) {
const element = this.node.node.querySelector(`article:nth-of-type(${index + 1}) .focusable`);
if (element) {
element.focus();
}
}
setRef = c => {
this.node = c;
}
handleLoadOlder = debounce(() => {
const last = this.props.conversationIds.last();
if (last) {
this.props.onLoadMore(last);
}
}, 300, { leading: true })
render () {
const { conversationIds, onLoadMore, ...other } = this.props;
return (
<ScrollableList {...other} onLoadMore={onLoadMore && this.handleLoadOlder} scrollKey='direct' ref={this.setRef}>
{conversationIds.map(item => (
<ConversationContainer
key={item}
conversationId={item}
onMoveUp={this.handleMoveUp}
onMoveDown={this.handleMoveDown}
/>
))}
</ScrollableList>
);
}
}

View File

@ -0,0 +1,19 @@
import { connect } from 'react-redux';
import Conversation from '../components/conversation';
import { markConversationRead } from '../../../actions/conversations';
const mapStateToProps = (state, { conversationId }) => {
const conversation = state.getIn(['conversations', 'items']).find(x => x.get('id') === conversationId);
return {
accounts: conversation.get('accounts').map(accountId => state.getIn(['accounts', accountId], null)),
unread: conversation.get('unread'),
lastStatusId: conversation.get('last_status', null),
};
};
const mapDispatchToProps = (dispatch, { conversationId }) => ({
markRead: () => dispatch(markConversationRead(conversationId)),
});
export default connect(mapStateToProps, mapDispatchToProps)(Conversation);

View File

@ -0,0 +1,15 @@
import { connect } from 'react-redux';
import ConversationsList from '../components/conversations_list';
import { expandConversations } from '../../../actions/conversations';
const mapStateToProps = state => ({
conversationIds: state.getIn(['conversations', 'items']).map(x => x.get('id')),
isLoading: state.getIn(['conversations', 'isLoading'], true),
hasMore: state.getIn(['conversations', 'hasMore'], false),
});
const mapDispatchToProps = dispatch => ({
onLoadMore: maxId => dispatch(expandConversations({ maxId })),
});
export default connect(mapStateToProps, mapDispatchToProps)(ConversationsList);

View File

@ -1,25 +1,21 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import StatusListContainer from '../ui/containers/status_list_container';
import Column from '../../components/column'; import Column from '../../components/column';
import ColumnHeader from '../../components/column_header'; import ColumnHeader from '../../components/column_header';
import { expandDirectTimeline } from '../../actions/timelines'; import { mountConversations, unmountConversations, expandConversations } from '../../actions/conversations';
import { addColumn, removeColumn, moveColumn } from '../../actions/columns'; import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { connectDirectStream } from '../../actions/streaming'; import { connectDirectStream } from '../../actions/streaming';
import ConversationsListContainer from './containers/conversations_list_container';
const messages = defineMessages({ const messages = defineMessages({
title: { id: 'column.direct', defaultMessage: 'Direct messages' }, title: { id: 'column.direct', defaultMessage: 'Direct messages' },
}); });
const mapStateToProps = state => ({ export default @connect()
hasUnread: state.getIn(['timelines', 'direct', 'unread']) > 0,
});
@connect(mapStateToProps)
@injectIntl @injectIntl
export default class DirectTimeline extends React.PureComponent { class DirectTimeline extends React.PureComponent {
static propTypes = { static propTypes = {
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
@ -52,11 +48,14 @@ export default class DirectTimeline extends React.PureComponent {
componentDidMount () { componentDidMount () {
const { dispatch } = this.props; const { dispatch } = this.props;
dispatch(expandDirectTimeline()); dispatch(mountConversations());
dispatch(expandConversations());
this.disconnect = dispatch(connectDirectStream()); this.disconnect = dispatch(connectDirectStream());
} }
componentWillUnmount () { componentWillUnmount () {
this.props.dispatch(unmountConversations());
if (this.disconnect) { if (this.disconnect) {
this.disconnect(); this.disconnect();
this.disconnect = null; this.disconnect = null;
@ -68,11 +67,11 @@ export default class DirectTimeline extends React.PureComponent {
} }
handleLoadMore = maxId => { handleLoadMore = maxId => {
this.props.dispatch(expandDirectTimeline({ maxId })); this.props.dispatch(expandConversations({ maxId }));
} }
render () { render () {
const { intl, shouldUpdateScroll, hasUnread, columnId, multiColumn } = this.props; const { intl, hasUnread, columnId, multiColumn, shouldUpdateScroll } = this.props;
const pinned = !!columnId; const pinned = !!columnId;
return ( return (
@ -88,7 +87,7 @@ export default class DirectTimeline extends React.PureComponent {
multiColumn={multiColumn} multiColumn={multiColumn}
/> />
<StatusListContainer <ConversationsListContainer
trackScroll={!pinned} trackScroll={!pinned}
scrollKey={`direct_timeline-${columnId}`} scrollKey={`direct_timeline-${columnId}`}
timelineId='direct' timelineId='direct'

View File

@ -21,9 +21,9 @@ const mapStateToProps = state => ({
domains: state.getIn(['domain_lists', 'blocks', 'items']), domains: state.getIn(['domain_lists', 'blocks', 'items']),
}); });
@connect(mapStateToProps) export default @connect(mapStateToProps)
@injectIntl @injectIntl
export default class Blocks extends ImmutablePureComponent { class Blocks extends ImmutablePureComponent {
static propTypes = { static propTypes = {
params: PropTypes.object.isRequired, params: PropTypes.object.isRequired,

View File

@ -21,9 +21,9 @@ const mapStateToProps = state => ({
hasMore: !!state.getIn(['status_lists', 'favourites', 'next']), hasMore: !!state.getIn(['status_lists', 'favourites', 'next']),
}); });
@connect(mapStateToProps) export default @connect(mapStateToProps)
@injectIntl @injectIntl
export default class Favourites extends ImmutablePureComponent { class Favourites extends ImmutablePureComponent {
static propTypes = { static propTypes = {
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,

View File

@ -15,8 +15,8 @@ const mapStateToProps = (state, props) => ({
accountIds: state.getIn(['user_lists', 'favourited_by', props.params.statusId]), accountIds: state.getIn(['user_lists', 'favourited_by', props.params.statusId]),
}); });
@connect(mapStateToProps) export default @connect(mapStateToProps)
export default class Favourites extends ImmutablePureComponent { class Favourites extends ImmutablePureComponent {
static propTypes = { static propTypes = {
params: PropTypes.object.isRequired, params: PropTypes.object.isRequired,

View File

@ -13,8 +13,8 @@ const messages = defineMessages({
reject: { id: 'follow_request.reject', defaultMessage: 'Reject' }, reject: { id: 'follow_request.reject', defaultMessage: 'Reject' },
}); });
@injectIntl export default @injectIntl
export default class AccountAuthorize extends ImmutablePureComponent { class AccountAuthorize extends ImmutablePureComponent {
static propTypes = { static propTypes = {
account: ImmutablePropTypes.map.isRequired, account: ImmutablePropTypes.map.isRequired,

View File

@ -20,9 +20,9 @@ const mapStateToProps = state => ({
accountIds: state.getIn(['user_lists', 'follow_requests', 'items']), accountIds: state.getIn(['user_lists', 'follow_requests', 'items']),
}); });
@connect(mapStateToProps) export default @connect(mapStateToProps)
@injectIntl @injectIntl
export default class FollowRequests extends ImmutablePureComponent { class FollowRequests extends ImmutablePureComponent {
static propTypes = { static propTypes = {
params: PropTypes.object.isRequired, params: PropTypes.object.isRequired,

View File

@ -22,8 +22,8 @@ const mapStateToProps = (state, props) => ({
hasMore: !!state.getIn(['user_lists', 'followers', props.params.accountId, 'next']), hasMore: !!state.getIn(['user_lists', 'followers', props.params.accountId, 'next']),
}); });
@connect(mapStateToProps) export default @connect(mapStateToProps)
export default class Followers extends ImmutablePureComponent { class Followers extends ImmutablePureComponent {
static propTypes = { static propTypes = {
params: PropTypes.object.isRequired, params: PropTypes.object.isRequired,

View File

@ -22,8 +22,8 @@ const mapStateToProps = (state, props) => ({
hasMore: !!state.getIn(['user_lists', 'following', props.params.accountId, 'next']), hasMore: !!state.getIn(['user_lists', 'following', props.params.accountId, 'next']),
}); });
@connect(mapStateToProps) export default @connect(mapStateToProps)
export default class Following extends ImmutablePureComponent { class Following extends ImmutablePureComponent {
static propTypes = { static propTypes = {
params: PropTypes.object.isRequired, params: PropTypes.object.isRequired,

Some files were not shown because too many files have changed in this diff Show More