diff --git a/.babelrc b/.babelrc deleted file mode 100644 index 190b5038c..000000000 --- a/.babelrc +++ /dev/null @@ -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" - ] - } - } -} diff --git a/.circleci/config.yml b/.circleci/config.yml index 20688b8e9..1161e4076 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -13,6 +13,9 @@ aliases: ALLOW_NOPAM: true CONTINUOUS_INTEGRATION: true DISABLE_SIMPLECOV: true + PAM_ENABLED: true + PAM_DEFAULT_SERVICE: pam_test + PAM_CONTROLLED_SERVICE: pam_test_controlled working_directory: ~/projects/mastodon/ - &attach_workspace @@ -172,6 +175,8 @@ jobs: - *attach_workspace - run: bundle exec i18n-tasks check-normalized - run: bundle exec i18n-tasks unused + - run: bundle exec i18n-tasks missing -t plural + - run: bundle exec i18n-tasks check-consistent-interpolations workflows: version: 2 diff --git a/.env.test b/.env.test index 726351c5e..fa4e1d91f 100644 --- a/.env.test +++ b/.env.test @@ -3,7 +3,3 @@ NODE_ENV=test # Federation LOCAL_DOMAIN=cb6e6126.ngrok.io LOCAL_HTTPS=true -# test pam authentication -PAM_ENABLED=true -PAM_DEFAULT_SERVICE=pam_test -PAM_CONTROLLED_SERVICE=pam_test_controlled diff --git a/.env.vagrant b/.env.vagrant index 04b889d00..f3b54f6e3 100644 --- a/.env.vagrant +++ b/.env.vagrant @@ -1,2 +1,2 @@ VAGRANT=true -LOCAL_DOMAIN=mastodon.dev \ No newline at end of file +LOCAL_DOMAIN=mastodon.local diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 602530db0..f49964bd9 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,12 +1,27 @@ --- name: Bug Report -about: Create a report to help us improve +about: If something isn't working as expected --- -[Issue text goes here]. + -* * * * + -- [ ] I searched or browsed the repo’s other issues to ensure this is not a duplicate. -- [ ] 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). +### Expected behaviour + + + +### Actual behaviour + + + +### Steps to reproduce the problem + + + +### Specifications + + + + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 46602fd2c..3890729e2 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,11 +1,17 @@ --- name: Feature Request -about: Suggest an idea for this project +about: I have a suggestion --- -[Issue text goes here]. + -* * * * + -- [ ] I searched or browsed the repo’s other issues to ensure this is not a duplicate. +### Pitch + + + +### Motivation + + diff --git a/.github/ISSUE_TEMPLATE/support.md b/.github/ISSUE_TEMPLATE/support.md new file mode 100644 index 000000000..7fbc86ff1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/support.md @@ -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 diff --git a/.rubocop.yml b/.rubocop.yml index 4ba5903bd..59e8a757a 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -77,6 +77,11 @@ Rails/SkipsModelValidations: Rails/HttpStatus: Enabled: false +Rails/Exit: + Exclude: + - 'lib/mastodon/*' + - 'lib/cli' + Style/ClassAndModuleChildren: Enabled: false diff --git a/.ruby-version b/.ruby-version index 73462a5a1..aedc15bb0 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.5.1 +2.5.3 diff --git a/AUTHORS.md b/AUTHORS.md index c4bbb6014..b81b6d245 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -3,76 +3,85 @@ and provided thanks to the work of the following contributors: * [Gargron](https://github.com/Gargron) * [ykzts](https://github.com/ykzts) -* [mjankowski](https://github.com/mjankowski) * [akihikodaki](https://github.com/akihikodaki) +* [mjankowski](https://github.com/mjankowski) +* [ThibG](https://github.com/ThibG) * [unarist](https://github.com/unarist) -* [yiskah](https://github.com/yiskah) * [m4sk1n](https://github.com/m4sk1n) +* [yiskah](https://github.com/yiskah) * [nolanlawson](https://github.com/nolanlawson) * [sorin-davidoi](https://github.com/sorin-davidoi) * [abcang](https://github.com/abcang) -* [ThibG](https://github.com/ThibG) * [lynlynlynx](https://github.com/lynlynlynx) +* [dependabot[bot]](https://github.com/apps/dependabot) * [alpaca-tc](https://github.com/alpaca-tc) * [nclm](https://github.com/nclm) * [ineffyble](https://github.com/ineffyble) +* [renatolond](https://github.com/renatolond) * [jeroenpraat](https://github.com/jeroenpraat) +* [mayaeh](https://github.com/mayaeh) * [blackle](https://github.com/blackle) * [Quent-in](https://github.com/Quent-in) * [JantsoP](https://github.com/JantsoP) * [nullkal](https://github.com/nullkal) * [yookoala](https://github.com/yookoala) +* [mabkenar](https://github.com/mabkenar) * [ysksn](https://github.com/ysksn) +* [shuheiktgw](https://github.com/shuheiktgw) * [ashfurrow](https://github.com/ashfurrow) -* [eramdam](https://github.com/eramdam) -* [mayaeh](https://github.com/mayaeh) +* [Kjwon15](https://github.com/Kjwon15) * [zunda](https://github.com/zunda) -* [ticky](https://github.com/ticky) +* [eramdam](https://github.com/eramdam) * [masarakki](https://github.com/masarakki) +* [takayamaki](https://github.com/takayamaki) +* [ticky](https://github.com/ticky) +* [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) * [matteoaquila](https://github.com/matteoaquila) * [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) +* [Artoria2e5](https://github.com/Artoria2e5) * [marrus-sh](https://github.com/marrus-sh) * [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) * [clworld](https://github.com/clworld) -* [danhunsaker](https://github.com/danhunsaker) -* [patf](https://github.com/patf) -* [Quenty31](https://github.com/Quenty31) -* [MitarashiDango](https://github.com/MitarashiDango) -* [Aldarone](https://github.com/Aldarone) +* [dracos](https://github.com/dracos) +* [SerCom_KC](mailto:sercom-kc@users.noreply.github.com) +* [Sylvhem](https://github.com/Sylvhem) +* [nightpool](https://github.com/nightpool) +* [MasterGroosha](https://github.com/MasterGroosha) * [JeanGauthier](https://github.com/JeanGauthier) * [kschaper](https://github.com/kschaper) -* [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) * [evanminto](https://github.com/evanminto) -* [mabkenar](https://github.com/mabkenar) * [MightyPork](https://github.com/MightyPork) -* [beatrix-bitrot](https://github.com/beatrix-bitrot) * [yhirano55](https://github.com/yhirano55) * [camponez](https://github.com/camponez) +* [SerCom-KC](https://github.com/SerCom-KC) * [aschmitz](https://github.com/aschmitz) +* [devkral](https://github.com/devkral) * [fpiesche](https://github.com/fpiesche) * [gandaro](https://github.com/gandaro) * [johnsudaar](https://github.com/johnsudaar) * [trebmuh](https://github.com/trebmuh) -* [Sylvhem](https://github.com/Sylvhem) +* [Rakib Hasan](mailto:rmhasan@gmail.com) * [lindwurm](https://github.com/lindwurm) +* [victorhck](mailto:victorhck@geeko.site) * [voidsatisfaction](https://github.com/voidsatisfaction) -* [neetshin](https://github.com/neetshin) -* [valentin2105](https://github.com/valentin2105) * [hikari-no-yume](https://github.com/hikari-no-yume) -* [Angristan](https://github.com/Angristan) +* [angristan](https://github.com/angristan) * [seefood](https://github.com/seefood) * [jackjennings](https://github.com/jackjennings) -* [hcmiya](https://github.com/hcmiya) -* [nightpool](https://github.com/nightpool) -* [salvadorpla](https://github.com/salvadorpla) +* [spla](mailto:spla@mastodont.cat) * [expenses](https://github.com/expenses) * [walf443](https://github.com/walf443) * [JoelQ](https://github.com/JoelQ) @@ -84,74 +93,93 @@ and provided thanks to the work of the following contributors: * [tsuwatch](https://github.com/tsuwatch) * [victorhck](https://github.com/victorhck) * [puckipedia](https://github.com/puckipedia) +* [fvh-P](https://github.com/fvh-P) * [contraexemplo](https://github.com/contraexemplo) +* [hugogameiro](https://github.com/hugogameiro) * [kazu9su](https://github.com/kazu9su) * [Komic](https://github.com/Komic) * [diomed](https://github.com/diomed) +* [ariasuni](https://github.com/ariasuni) +* [Neetshin](mailto:neetshin@neetsh.in) * [rainyday](https://github.com/rainyday) +* [ProgVal](https://github.com/ProgVal) +* [valentin2105](https://github.com/valentin2105) +* [yuntan](https://github.com/yuntan) +* [ashleyhull-versent](https://github.com/ashleyhull-versent) +* [goofy-bz](mailto:goofy@babelzilla.org) * [kadiix](https://github.com/kadiix) * [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) * [TheKinrar](https://github.com/TheKinrar) * [AA4ch1](https://github.com/AA4ch1) * [alexgleason](https://github.com/alexgleason) * [cpytel](https://github.com/cpytel) * [northerner](https://github.com/northerner) +* [fhemberger](https://github.com/fhemberger) +* [greysteil](https://github.com/greysteil) * [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) * [znz](https://github.com/znz) * [Naouak](https://github.com/Naouak) -* [rtucker](https://github.com/rtucker) * [reneklacan](https://github.com/reneklacan) -* [KScl](https://github.com/KScl) -* [SerCom-KC](https://github.com/SerCom-KC) +* [ekiru](https://github.com/ekiru) * [tcitworld](https://github.com/tcitworld) * [geta6](https://github.com/geta6) -* [goofy-bz](https://github.com/goofy-bz) * [happycoloredbanana](https://github.com/happycoloredbanana) +* [kedamaDQ](https://github.com/kedamaDQ) * [leopku](https://github.com/leopku) * [SansPseudoFix](https://github.com/SansPseudoFix) * [tomfhowe](https://github.com/tomfhowe) * [noraworld](https://github.com/noraworld) -* [fvh-P](https://github.com/fvh-P) +* [theboss](https://github.com/theboss) * [178inaba](https://github.com/178inaba) -* [devkral](https://github.com/devkral) * [alyssais](https://github.com/alyssais) * [kodnaplakal](https://github.com/kodnaplakal) * [stalker314314](https://github.com/stalker314314) * [huertanix](https://github.com/huertanix) * [genesixx](https://github.com/genesixx) -* [fhemberger](https://github.com/fhemberger) * [halkeye](https://github.com/halkeye) +* [hinaloe](https://github.com/hinaloe) * [treby](https://github.com/treby) -* [d6rkaiz](https://github.com/d6rkaiz) +* [Reverite](https://github.com/Reverite) * [jpdevries](https://github.com/jpdevries) -* [rndm-stranger](https://github.com/rndm-stranger) +* [H-C-F](https://github.com/H-C-F) +* [Kurtis Rainbolt-Greene](mailto:me@kurtisrainboltgreene.name) * [saper](https://github.com/saper) * [nevillepark](https://github.com/nevillepark) * [ornithocoder](https://github.com/ornithocoder) * [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) * [stamak](https://github.com/stamak) +* [Technowix](mailto:technowix@users.noreply.github.com) * [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) * [gled-rs](https://github.com/gled-rs) +* [Valentin_NC](mailto:valentin.ouvrard@nautile.sarl) * [R0ckweb](https://github.com/R0ckweb) +* [caasi](https://github.com/caasi) * [esetomo](https://github.com/esetomo) * [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) * [chriswmartin](https://github.com/chriswmartin) * [vahnj](https://github.com/vahnj) * [ikuradon](https://github.com/ikuradon) * [AndreLewin](https://github.com/AndreLewin) +* [rinsuki](https://github.com/rinsuki) * [redtachyons](https://github.com/redtachyons) * [thurloat](https://github.com/thurloat) * [aaribaud](https://github.com/aaribaud) +* [Andrew](mailto:andrewlchronister@gmail.com) * [estuans](https://github.com/estuans) * [dissolve](https://github.com/dissolve) * [PurpleBooth](https://github.com/PurpleBooth) @@ -165,36 +193,46 @@ and provided thanks to the work of the following contributors: * [farlistener](https://github.com/farlistener) * [DavidLibeau](https://github.com/DavidLibeau) * [SirCmpwn](https://github.com/SirCmpwn) -* [MasterGroosha](https://github.com/MasterGroosha) * [Fjoerfoks](https://github.com/Fjoerfoks) * [fmauNeko](https://github.com/fmauNeko) * [gloaec](https://github.com/gloaec) -* [greysteil](https://github.com/greysteil) +* [Gomasy](https://github.com/Gomasy) * [unstabler](https://github.com/unstabler) * [potato4d](https://github.com/potato4d) * [h-izumi](https://github.com/h-izumi) * [ErikXXon](https://github.com/ErikXXon) * [ian-kelling](https://github.com/ian-kelling) +* [immae](https://github.com/immae) * [foozmeat](https://github.com/foozmeat) * [jasonrhodes](https://github.com/jasonrhodes) -* [asm](https://github.com/asm) +* [Jason Snell](mailto:jason@newrelic.com) * [jviide](https://github.com/jviide) * [crakaC](https://github.com/crakaC) * [tkbky](https://github.com/tkbky) +* [Kaylee](mailto:kaylee@codethat.sucks) * [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) * [mig5](https://github.com/mig5) * [ndarville](https://github.com/ndarville) * [Abzol](https://github.com/Abzol) +* [pwoolcoc](https://github.com/pwoolcoc) * [xPaw](https://github.com/xPaw) +* [petzah](https://github.com/petzah) +* [ignisf](https://github.com/ignisf) * [raymestalez](https://github.com/raymestalez) +* [sascha-sl](https://github.com/sascha-sl) +* [u1-liquid](https://github.com/u1-liquid) * [sim6](https://github.com/sim6) -* [ekiru](https://github.com/ekiru) -* [Technowix](https://github.com/Technowix) +* [stemid](https://github.com/stemid) * [ThomasLeister](https://github.com/ThomasLeister) * [mcat-ee](https://github.com/mcat-ee) * [tototoshi](https://github.com/tototoshi) +* [TrashMacNugget](https://github.com/TrashMacNugget) * [VirtuBox](https://github.com/VirtuBox) +* [Vladyslav](mailto:vaden@tuta.io) * [kaniini](https://github.com/kaniini) * [vayan](https://github.com/vayan) * [yannicka](https://github.com/yannicka) @@ -202,7 +240,12 @@ and provided thanks to the work of the following contributors: * [zacanger](https://github.com/zacanger) * [amazedkoumei](https://github.com/amazedkoumei) * [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) +* [cpsdqs](https://github.com/cpsdqs) * [barzamin](https://github.com/barzamin) * [fhalna](https://github.com/fhalna) * [haoyayoi](https://github.com/haoyayoi) @@ -215,42 +258,55 @@ and provided thanks to the work of the following contributors: * [oliverkeeble](https://github.com/oliverkeeble) * [pinfort](https://github.com/pinfort) * [rbaumert](https://github.com/rbaumert) +* [rhoio](https://github.com/rhoio) +* [trwnh](https://github.com/trwnh) * [usagi-f](https://github.com/usagi-f) * [vidarlee](https://github.com/vidarlee) * [vjackson725](https://github.com/vjackson725) * [wxcafe](https://github.com/wxcafe) -* [rinsuki](https://github.com/rinsuki) +* [新都心(Neet Shin)](mailto:nucx@dio-vox.com) * [cygnan](https://github.com/cygnan) * [Awea](https://github.com/Awea) * [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) * [857b](https://github.com/857b) +* [insom](https://github.com/insom) +* [Aditoo17](https://github.com/Aditoo17) * [unascribed](https://github.com/unascribed) * [Aguay-val](https://github.com/Aguay-val) +* [Akihiko Odaki](mailto:nekomanma@pixiv.co.jp) * [knu](https://github.com/knu) +* [h3poteto](https://github.com/h3poteto) +* [unleashed](https://github.com/unleashed) * [alxrcs](https://github.com/alxrcs) * [console-cowboy](https://github.com/console-cowboy) * [pointlessone](https://github.com/pointlessone) * [a2](https://github.com/a2) * [0xa](https://github.com/0xa) +* [palindromordnilap](https://github.com/palindromordnilap) * [virtualpain](https://github.com/virtualpain) * [sapphirus](https://github.com/sapphirus) * [amandavisconti](https://github.com/amandavisconti) * [ameliavoncat](https://github.com/ameliavoncat) * [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) +* [abackstrom](https://github.com/abackstrom) * [jumbosushi](https://github.com/jumbosushi) * [ayumin](https://github.com/ayumin) * [BaptisteGelez](https://github.com/BaptisteGelez) * [bzg](https://github.com/bzg) +* [BenLubar](https://github.com/BenLubar) * [benediktg](https://github.com/benediktg) * [blakebarnett](https://github.com/blakebarnett) * [bradj](https://github.com/bradj) * [brycied00d](https://github.com/brycied00d) * [carlosjs23](https://github.com/carlosjs23) * [cgxxx](https://github.com/cgxxx) +* [kibitan](https://github.com/kibitan) * [chrisheninger](https://github.com/chrisheninger) * [chris-martin](https://github.com/chris-martin) * [DoubleMalt](https://github.com/DoubleMalt) @@ -259,22 +315,33 @@ and provided thanks to the work of the following contributors: * [chriswk](https://github.com/chriswk) * [csu](https://github.com/csu) * [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) * [daprice](https://github.com/daprice) * [dar5hak](https://github.com/dar5hak) * [kant](https://github.com/kant) +* [maxolasersquad](https://github.com/maxolasersquad) * [singingwolfboy](https://github.com/singingwolfboy) * [davidcelis](https://github.com/davidcelis) +* [davefp](https://github.com/davefp) * [yipdw](https://github.com/yipdw) * [debanshuk](https://github.com/debanshuk) +* [Derek Lewis](mailto:derekcecillewis@gmail.com) * [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) * [Elizafox](https://github.com/Elizafox) * [ericblade](https://github.com/ericblade) * [mikoim](https://github.com/mikoim) +* [espenronnevik](https://github.com/espenronnevik) +* [Finariel](https://github.com/Finariel) * [siuying](https://github.com/siuying) +* [GenbuHase](https://github.com/GenbuHase) * [hattori6789](https://github.com/hattori6789) * [algernon](https://github.com/algernon) * [Fastbyte01](https://github.com/Fastbyte01) @@ -283,52 +350,62 @@ and provided thanks to the work of the following contributors: * [Fiaxhs](https://github.com/Fiaxhs) * [reedcourty](https://github.com/reedcourty) * [anneau](https://github.com/anneau) +* [lanodan](https://github.com/lanodan) +* [Harmon758](https://github.com/Harmon758) * [HellPie](https://github.com/HellPie) * [Habu-Kagumba](https://github.com/Habu-Kagumba) -* [hinaloe](https://github.com/hinaloe) * [suzukaze](https://github.com/suzukaze) * [Hiromi-Kai](https://github.com/Hiromi-Kai) +* [hishamhm](https://github.com/hishamhm) * [musashino205](https://github.com/musashino205) * [iwaim](https://github.com/iwaim) * [valrus](https://github.com/valrus) * [IMcD23](https://github.com/IMcD23) * [yi0713](https://github.com/yi0713) -* [immae](https://github.com/immae) * [iblech](https://github.com/iblech) +* [usbsnowcrash](https://github.com/usbsnowcrash) * [jack-michaud](https://github.com/jack-michaud) * [Floppy](https://github.com/Floppy) * [loomchild](https://github.com/loomchild) -* [docjkl](https://github.com/docjkl) +* [jenkr55](https://github.com/jenkr55) +* [press5](https://github.com/press5) * [TrollDecker](https://github.com/TrollDecker) * [jmontane](https://github.com/jmontane) * [jonathanklee](https://github.com/jonathanklee) * [jguerder](https://github.com/jguerder) * [Jehops](https://github.com/Jehops) * [joshuap](https://github.com/joshuap) +* [YuleZ](https://github.com/YuleZ) * [Tiwy57](https://github.com/Tiwy57) * [xuv](https://github.com/xuv) * [Jnsll](https://github.com/Jnsll) * [j0k3r](https://github.com/j0k3r) * [KEINOS](https://github.com/KEINOS) * [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) -* [connyduck](https://github.com/connyduck) * [k0ta0uchi](https://github.com/k0ta0uchi) * [KrzysiekJ](https://github.com/KrzysiekJ) * [leowzukw](https://github.com/leowzukw) * [lmorchard](https://github.com/lmorchard) +* [Tak](https://github.com/Tak) * [cacheflow](https://github.com/cacheflow) * [ldidry](https://github.com/ldidry) * [jemus42](https://github.com/jemus42) * [lfuelling](https://github.com/lfuelling) * [Grabacr07](https://github.com/Grabacr07) * [mistermantas](https://github.com/mistermantas) +* [mareklach](https://github.com/mareklach) * [wirehack7](https://github.com/wirehack7) +* [martymcguire](https://github.com/martymcguire) * [marvinkopf](https://github.com/marvinkopf) * [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) +* [webroo](https://github.com/webroo) +* [matthiasbeyer](https://github.com/matthiasbeyer) * [mattjmattj](https://github.com/mattjmattj) * [mtparet](https://github.com/mtparet) * [maximeborges](https://github.com/maximeborges) @@ -336,16 +413,20 @@ and provided thanks to the work of the following contributors: * [michaeljdeeb](https://github.com/michaeljdeeb) * [Themimitoof](https://github.com/Themimitoof) * [cyweo](https://github.com/cyweo) -* [M1dgard](https://github.com/M1dgard) +* [Midgard](mailto:m1dgard@users.noreply.github.com) * [mike-burns](https://github.com/mike-burns) * [verymilan](https://github.com/verymilan) * [milmazz](https://github.com/milmazz) +* [premist](https://github.com/premist) * [Mnkai](https://github.com/Mnkai) * [mitchhentges](https://github.com/mitchhentges) * [moritzheiber](https://github.com/moritzheiber) * [mouse-reeve](https://github.com/mouse-reeve) +* [Mozinet-fr](https://github.com/Mozinet-fr) * [lae](https://github.com/lae) * [Nanamachi](https://github.com/Nanamachi) +* [orinthe](https://github.com/orinthe) +* [Dar13](https://github.com/Dar13) * [ngerakines](https://github.com/ngerakines) * [vonneudeck](https://github.com/vonneudeck) * [Ninetailed](https://github.com/Ninetailed) @@ -355,18 +436,23 @@ and provided thanks to the work of the following contributors: * [norayr](https://github.com/norayr) * [joyeusenoelle](https://github.com/joyeusenoelle) * [OlivierNicole](https://github.com/OlivierNicole) +* [noppa](https://github.com/noppa) * [Otakan951](https://github.com/Otakan951) * [fahy](https://github.com/fahy) +* [PatrickRWells](https://github.com/PatrickRWells) * [Pangoraw](https://github.com/Pangoraw) -* [pwoolcoc](https://github.com/pwoolcoc) * [peterkeen](https://github.com/peterkeen) -* [petzah](https://github.com/petzah) -* [ignisf](https://github.com/ignisf) +* [pgate](https://github.com/pgate) +* [remram44](https://github.com/remram44) +* [retokromer](https://github.com/retokromer) * [rfwatson](https://github.com/rfwatson) * [rfreebern](https://github.com/rfreebern) +* [Ryan Wade](mailto:ryan.wade@protonmail.com) * [sylph01](https://github.com/sylph01) +* [S-H-GAMELINKS](https://github.com/S-H-GAMELINKS) * [staticsafe](https://github.com/staticsafe) * [snwh](https://github.com/snwh) +* [sts10](https://github.com/sts10) * [skoji](https://github.com/skoji) * [ScienJus](https://github.com/ScienJus) * [larkinscott](https://github.com/larkinscott) @@ -378,73 +464,103 @@ and provided thanks to the work of the following contributors: * [ernix](https://github.com/ernix) * [rosylilly](https://github.com/rosylilly) * [shouko](https://github.com/shouko) +* [Sina Mashek](mailto:sina@mashek.xyz) * [sossii](https://github.com/sossii) -* [StefOfficiel](https://github.com/StefOfficiel) -* [svetlik](https://github.com/svetlik) -* [dereckson](https://github.com/dereckson) -* [theboss](https://github.com/theboss) -* [takp](https://github.com/takp) -* [tkusano](https://github.com/tkusano) -* [TheInventrix](https://github.com/TheInventrix) -* [shug0](https://github.com/shug0) -* [Fortyseven](https://github.com/Fortyseven) -* [tobypinder](https://github.com/tobypinder) -* [tomosm](https://github.com/tomosm) -* [TomoyaShibata](https://github.com/TomoyaShibata) -* [TrashMacNugget](https://github.com/TrashMacNugget) -* [treyssatvincent](https://github.com/treyssatvincent) -* [optikfluffel](https://github.com/optikfluffel) -* [vmincev](https://github.com/vmincev) -* [waldyrious](https://github.com/waldyrious) -* [tahnok](https://github.com/tahnok) -* [YDrogen](https://github.com/YDrogen) -* [YOSHIOKAEiichiro](https://github.com/YOSHIOKAEiichiro) -* [S-YOU](https://github.com/S-YOU) -* [YaQ00](https://github.com/YaQ00) -* [yanakend](https://github.com/yanakend) -* [orzFly](https://github.com/orzFly) -* [chansuke](https://github.com/chansuke) -* [yuntan](https://github.com/yuntan) -* [LogicalDash](https://github.com/LogicalDash) -* [ZiiX](https://github.com/ZiiX) -* [benklop](https://github.com/benklop) -* [caasi](https://github.com/caasi) -* [caesarologia](https://github.com/caesarologia) -* [chrolis](https://github.com/chrolis) -* [cormojs](https://github.com/cormojs) -* [cpsdqs](https://github.com/cpsdqs) -* [d0p1s4m4](https://github.com/d0p1s4m4) -* [evilny0](https://github.com/evilny0) -* [febrezo](https://github.com/febrezo) -* [fsubal](https://github.com/fsubal) -* [dikky1218](https://github.com/dikky1218) -* [gentarok](https://github.com/gentarok) -* [hakoai](https://github.com/hakoai) -* [chaosbunker](https://github.com/chaosbunker) -* [isati](https://github.com/isati) -* [jkap](https://github.com/jkap) -* [jirayudech](https://github.com/jirayudech) -* [jukper](https://github.com/jukper) -* [karlyeurl](https://github.com/karlyeurl) -* [kedamaDQ](https://github.com/kedamaDQ) -* [kuro5hin](https://github.com/kuro5hin) -* [maxypy](https://github.com/maxypy) -* [marcus-herrmann](https://github.com/marcus-herrmann) -* [mshrtkch](https://github.com/mshrtkch) -* [muan](https://github.com/muan) -* [rch850](https://github.com/rch850) -* [roikale](https://github.com/roikale) -* [rysiekpl](https://github.com/rysiekpl) -* [saturday06](https://github.com/saturday06) -* [scriptjunkie](https://github.com/scriptjunkie) -* [seekr](https://github.com/seekr) -* [syui](https://github.com/syui) -* [tackeyy](https://github.com/tackeyy) -* [tmyt](https://github.com/tmyt) -* [utam0k](https://github.com/utam0k) -* [vpzomtrrfrt](https://github.com/vpzomtrrfrt) -* [walfie](https://github.com/walfie) -* [y-temp4](https://github.com/y-temp4) -* [ymmtmdk](https://github.com/ymmtmdk) +* [Spanky](mailto:2788886+spankyworks@users.noreply.github.com) +* [StefOfficiel](mailto:pichard.stephane@free.fr) +* [Svetlozar Todorov](mailto:svetlik@users.noreply.github.com) +* [Sébastien Santoro](mailto:dereckson@espace-win.org) +* [Tad Thorley](mailto:phaedryx@users.noreply.github.com) +* [Takayoshi Nishida](mailto:takayoshi.nishida@gmail.com) +* [Takayuki KUSANO](mailto:github@tkusano.jp) +* [TakesxiSximada](mailto:takesxi.sximada@gmail.com) +* [TheInventrix](mailto:theinventrix@users.noreply.github.com) +* [Thomas Alberola](mailto:thomas@needacoffee.fr) +* [Toby Deshane](mailto:fortyseven@users.noreply.github.com) +* [Toby Pinder](mailto:gigitrix@gmail.com) +* [Tomonori Murakami](mailto:crosslife777@gmail.com) +* [TomoyaShibata](mailto:wind.of.hometown@gmail.com) +* [Treyssat-Vincent Nino](mailto:treyssatvincent@users.noreply.github.com) +* [Udo Kramer](mailto:optik@fluffel.io) +* [Una](mailto:una@unascribed.com) +* [Ushitora Anqou](mailto:ushitora_anqou@yahoo.co.jp) +* [Valentin Lorentz](mailto:progval+git@progval.net) +* [Vladimir Mincev](mailto:vladimir@canicinteractive.com) +* [Waldir Pimenta](mailto:waldyrious@gmail.com) +* [Wesley Ellis](mailto:tahnok@gmail.com) +* [Wiktor](mailto:wiktor@metacode.biz) +* [Wonderfall](mailto:wonderfall@schrodinger.io) +* [YDrogen](mailto:ydrogen45@gmail.com) +* [YMHuang](mailto:ymhuang@fmbase.tw) +* [YOSHIOKA Eiichiro](mailto:yoshioka.eiichiro@gmail.com) +* [YOU](mailto:stackexchange.you@gmail.com) +* [YaQ](mailto:i_k_o_m_a_7@yahoo.co.jp) +* [Yanaken](mailto:yanakend@gmail.com) +* [Yann Klis](mailto:yann.klis@gmail.com) +* [Yeechan Lu](mailto:wz.bluesnow@gmail.com) +* [Yusuke Abe](mailto:moonset20@gmail.com) +* [Zachary Spector](mailto:logicaldash@gmail.com) +* [ZiiX](mailto:ziix@users.noreply.github.com) +* [asria-jp](mailto:is@alicematic.com) +* [ava](mailto:vladooku@users.noreply.github.com) +* [benklop](mailto:benklop@gmail.com) +* [bsky](mailto:git@imbsky.net) +* [caesarologia](mailto:lopesgemelli.1@gmail.com) +* [cbayerlein](mailto:c.bayerlein@gmail.com) +* [chrolis](mailto:chrolis@users.noreply.github.com) +* [cormo](mailto:cormorant2+github@gmail.com) +* [d0p1](mailto:dopi-sama@hush.com) +* [evilny0](mailto:evilny0@moomoocamp.net) +* [febrezo](mailto:felixbrezo@gmail.com) +* [fsubal](mailto:fsubal@users.noreply.github.com) +* [fusshi-](mailto:dikky1218@users.noreply.github.com) +* [gentaro](mailto:gentaroooo@gmail.com) +* [hakoai](mailto:hk--76@qa2.so-net.ne.jp) +* [haosbvnker](mailto:github@chaosbunker.com) +* [isati](mailto:phil@juchnowi.cz) +* [jacob](mailto:jacobherringtondeveloper@gmail.com) +* [jenn kaplan](mailto:me@jkap.io) +* [jirayudech](mailto:jirayudech@gmail.com) +* [jooops](mailto:joops@autistici.org) +* [jukper](mailto:jukkaperanto@gmail.com) +* [jumoru](mailto:jumoru@mailbox.org) +* [karlyeurl](mailto:karl.yeurl@gmail.com) +* [kedama](mailto:32974885+kedamadq@users.noreply.github.com) +* [kuro5hin](mailto:rusty@kuro5hin.org) +* [luzpaz](mailto:luzpaz@users.noreply.github.com) +* [maxypy](mailto:maxime@mpigou.fr) +* [mhe](mailto:mail@marcus-herrmann.com) +* [mimikun](mailto:dzdzble_effort_311@outlook.jp) +* [mshrtkch](mailto:mshrtkch@users.noreply.github.com) +* [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. diff --git a/Aptfile b/Aptfile index 60d24f8b3..0a01fa24b 100644 --- a/Aptfile +++ b/Aptfile @@ -10,3 +10,20 @@ libxdamage1 libxfixes3 protobuf-compiler 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 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..cb8bf3272 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,134 @@ +Changelog +========= + +All notable changes to this project will be documented in this file. + +## [2.6.1] - 2018-10-30 +### Fixed + +- Fix resolving resources by URL not working due to a regression in #9132 (#9171) +- Fix reducer error in web UI when a conversation has no last status (#9173) + +## [2.6.0] - 2018-10-30 +### 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, #9093, #9150) +- 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 `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) +- Add suggested follows to search screen in mobile layout (#9010) +- Add CORS header to `/.well-known/*` routes (#9083) +- Add `card` attribute to statuses returned from REST API (#9120) +- Add in-stream link preview (#9120) +- Add support for ActivityPub `Page` objects (#9121) + +### 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) +- Change docker-compose default to persist volumes in current directory (#9055) +- Change character counters on edit profile page to input length limit (#9100) +- Change notification filtering to always let through messages from staff (#9152) +- Change "hide boosts from user" function also hiding notifications about boosts (#9147) +- Change CSS `detailed-status__wrapper` class actually wrap the detailed status (#8547) + +### Deprecated + +- `GET /api/v1/timelines/direct` → `GET /api/v1/conversations` (#8832) +- `POST /api/v1/notifications/dismiss` → `POST /api/v1/notifications/:id/dismiss` (#8905) +- `GET /api/v1/statuses/:id/card` → `card` attributed included in status (#9120) + +### 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, #9145, #9146) +- 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) +- Fix og:url on status pages (#9047) +- Fix upload option buttons only being visible on hover (#9074) +- Fix tootctl not returning exit code 1 on wrong arguments (#9094) +- Fix preview cards for appearing for profiles mentioned in toot (#6934, #9158) +- Fix local accounts sometimes being duplicated as faux-remote (#9109) +- Fix emoji search when the shortcode has multiple separators (#9124) +- Fix dropdowns sometimes being partially obscured by other elements (#9126) +- Fix cache not updating when reply/boost/favourite counters or media sensitivity update (#9119) +- Fix empty display name precedence over username in web UI (#9163) +- Fix td instead of th in sessions table header (#9162) +- Fix handling of content types with profile (#9132) + +## [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) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d0f75f2e4..b55729a9b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 -2. By working on the back-end application -3. By working on the front-end application +You can contribute in the following ways: -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 -- No spelling mistakes -- 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/) +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). diff --git a/Dockerfile b/Dockerfile index b85d05047..9c53b4145 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ -FROM node:8.11.3-alpine as node -FROM ruby:2.4.4-alpine3.6 +FROM node:8.12.0-alpine as node +FROM ruby:2.4.5-alpine3.8 LABEL maintainer="https://github.com/tootsuite/mastodon" \ description="Your self-hosted, globally interconnected microblogging community" diff --git a/Gemfile b/Gemfile index bdac00b48..bf23015e6 100644 --- a/Gemfile +++ b/Gemfile @@ -5,36 +5,36 @@ ruby '>= 2.3.0', '< 2.6.0' gem 'pkg-config', '~> 1.3' -gem 'puma', '~> 3.11' +gem 'puma', '~> 3.12' gem 'rails', '~> 5.2.1' gem 'thor', '~> 0.20' gem 'hamlit-rails', '~> 0.2' -gem 'pg', '~> 1.0' +gem 'pg', '~> 1.1' gem 'makara', '~> 0.4' -gem 'pghero', '~> 2.1' -gem 'dotenv-rails', '~> 2.2', '< 2.3' +gem 'pghero', '~> 2.2' +gem 'dotenv-rails', '~> 2.5' -gem 'aws-sdk-s3', '~> 1.9', require: false -gem 'fog-core', '~> 1.45' -gem 'fog-openstack', '~> 0.1', require: false +gem 'aws-sdk-s3', '~> 1.23', require: false +gem 'fog-core', '<= 2.1.0' +gem 'fog-openstack', '~> 0.3', require: false gem 'paperclip', '~> 6.0' gem 'paperclip-av-transcoder', '~> 0.6' gem 'streamio-ffmpeg', '~> 3.0' gem 'active_model_serializers', '~> 0.10' gem 'addressable', '~> 2.5' -gem 'bootsnap', '~> 1.3' +gem 'bootsnap', '~> 1.3', require: false gem 'browser' gem 'charlock_holmes', '~> 0.7.6' gem 'iso-639' gem 'chewy', '~> 5.0' gem 'cld3', '~> 3.2.0' -gem 'devise', '~> 4.4' +gem 'devise', '~> 4.5' gem 'devise-two-factor', '~> 3.0' group :pam_authentication, optional: true do - gem 'devise_pam_authenticatable2', '~> 9.1' + gem 'devise_pam_authenticatable2', '~> 9.2' end gem 'net-ldap', '~> 0.10' @@ -49,45 +49,44 @@ gem 'goldfinger', '~> 2.1' gem 'hiredis', '~> 0.6' gem 'redis-namespace', '~> 1.5' gem 'htmlentities', '~> 4.3' -gem 'http', '~> 3.2' +gem 'http', '~> 3.3' gem 'http_accept_language', '~> 2.1' 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 'kaminari', '~> 1.1' 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 'nsa', '~> 0.2' -gem 'oj', '~> 3.5' +gem 'oj', '~> 3.7' 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 'pundit', '~> 1.1' +gem 'pundit', '~> 2.0' gem 'premailer-rails' -gem 'rack-attack', '~> 5.2' +gem 'rack-attack', '~> 5.4' gem 'rack-cors', '~> 1.0', require: 'rack/cors' gem 'rails-i18n', '~> 5.1' gem 'rails-settings-cached', '~> 0.6' gem 'redis', '~> 4.0', require: ['redis', 'redis/connection/hiredis'] gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock' gem 'rqrcode', '~> 0.10' -gem 'ruby-progressbar', '~> 1.4' -gem 'sanitize', '~> 4.6' -gem 'sidekiq', '~> 5.1' -gem 'sidekiq-scheduler', '~> 2.2' +gem 'sanitize', '~> 5.0' +gem 'sidekiq', '~> 5.2' +gem 'sidekiq-scheduler', '~> 3.0' gem 'sidekiq-unique-jobs', '~> 5.0' gem 'sidekiq-bulk', '~>0.1.1' gem 'simple-navigation', '~> 4.0' gem 'simple_form', '~> 4.0' gem 'sprockets-rails', '~> 3.2', require: 'sprockets/railtie' gem 'stoplight', '~> 2.1.3' -gem 'strong_migrations', '~> 0.2' +gem 'strong_migrations', '~> 0.3' 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 'tzinfo-data', '~> 1.2018' -gem 'webpacker', '~> 3.4' +gem 'webpacker', '~> 3.5' gem 'webpush' gem 'json-ld', '~> 2.2' @@ -95,45 +94,45 @@ gem 'rdf-normalize', '~> 0.3' group :development, :test do gem 'fabrication', '~> 2.20' - gem 'fuubar', '~> 2.2' + gem 'fuubar', '~> 2.3' gem 'i18n-tasks', '~> 0.9', require: false gem 'pry-byebug', '~> 3.6' gem 'pry-rails', '~> 0.3' - gem 'rspec-rails', '~> 3.7' + gem 'rspec-rails', '~> 3.8' end group :production, :test do - gem 'private_address_check', '~> 0.4.1' + gem 'private_address_check', '~> 0.5' end group :test do - gem 'capybara', '~> 2.18' + gem 'capybara', '~> 3.10' gem 'climate_control', '~> 0.2' - gem 'faker', '~> 1.8' + gem 'faker', '~> 1.9' gem 'microformats', '~> 4.0' gem 'rails-controller-testing', '~> 1.0' gem 'rspec-sidekiq', '~> 3.0' gem 'simplecov', '~> 0.16', require: false - gem 'webmock', '~> 3.3' - gem 'parallel_tests', '~> 2.21' + gem 'webmock', '~> 3.4' + gem 'parallel_tests', '~> 2.26' end group :development do gem 'active_record_query_trace', '~> 1.5' gem 'annotate', '~> 2.7' - gem 'better_errors', '~> 2.4' + gem 'better_errors', '~> 2.5' gem 'binding_of_caller', '~> 0.7' gem 'bullet', '~> 5.7' gem 'letter_opener', '~> 1.4' gem 'letter_opener_web', '~> 1.3' gem 'memory_profiler' - gem 'rubocop', '~> 0.55', require: false - gem 'brakeman', '~> 4.2', require: false + gem 'rubocop', '~> 0.60', require: false + gem 'brakeman', '~> 4.3', require: false gem 'bundler-audit', '~> 0.6', require: false gem 'scss_lint', '~> 0.57', require: false - gem 'capistrano', '~> 3.10' - gem 'capistrano-rails', '~> 1.3' + gem 'capistrano', '~> 3.11' + gem 'capistrano-rails', '~> 1.4' gem 'capistrano-rbenv', '~> 2.1' gem 'capistrano-yarn', '~> 2.0' diff --git a/Gemfile.lock b/Gemfile.lock index 502d08efe..91a2e8281 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -66,7 +66,7 @@ GEM public_suffix (>= 2.0.2, < 4.0) airbrussh (1.3.0) sshkit (>= 1.6.1, != 1.7.0) - annotate (2.7.3) + annotate (2.7.4) activerecord (>= 3.2, < 6.0) rake (>= 10.4, < 13.0) arel (9.0.0) @@ -75,40 +75,42 @@ GEM encryptor (~> 3.0.0) av (0.9.0) cocaine (~> 0.5.3) - aws-partitions (1.80.0) - aws-sdk-core (3.19.0) + aws-eventstream (1.0.1) + aws-partitions (1.106.0) + aws-sdk-core (3.35.0) + aws-eventstream (~> 1.0) aws-partitions (~> 1.0) aws-sigv4 (~> 1.0) jmespath (~> 1.0) - aws-sdk-kms (1.5.0) - aws-sdk-core (~> 3) + aws-sdk-kms (1.11.0) + aws-sdk-core (~> 3, >= 3.26.0) aws-sigv4 (~> 1.0) - aws-sdk-s3 (1.9.1) - aws-sdk-core (~> 3) + aws-sdk-s3 (1.23.0) + aws-sdk-core (~> 3, >= 3.26.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.0) - aws-sigv4 (1.0.2) + aws-sigv4 (1.0.3) bcrypt (3.1.12) benchmark-ips (2.7.2) - better_errors (2.4.0) + better_errors (2.5.0) coderay (>= 1.0.0) erubi (>= 1.0.0) rack (>= 0.9.0) binding_of_caller (0.8.0) debug_inspector (>= 0.0.1) - bootsnap (1.3.0) + bootsnap (1.3.2) msgpack (~> 1.0) - brakeman (4.2.1) + brakeman (4.3.1) browser (2.5.3) builder (3.2.3) - bullet (5.7.5) + bullet (5.7.6) activesupport (>= 3.0.0) uniform_notifier (~> 1.11.0) bundler-audit (0.6.0) bundler (~> 1.2) thor (~> 0.18) byebug (10.0.2) - capistrano (3.10.2) + capistrano (3.11.0) airbrussh (>= 1.0.0) i18n rake (>= 10.0.0) @@ -116,21 +118,22 @@ GEM capistrano-bundler (1.3.0) capistrano (~> 3.1) sshkit (~> 1.2) - capistrano-rails (1.3.1) + capistrano-rails (1.4.0) capistrano (~> 3.1) capistrano-bundler (~> 1.1) - capistrano-rbenv (2.1.3) + capistrano-rbenv (2.1.4) capistrano (~> 3.1) sshkit (~> 1.3) capistrano-yarn (2.0.2) capistrano (~> 3.0) - capybara (2.18.0) + capybara (3.10.0) addressable mini_mime (>= 0.1.3) - nokogiri (>= 1.3.3) - rack (>= 1.0.0) - rack-test (>= 0.5.4) - xpath (>= 2.0, < 4.0) + nokogiri (~> 1.8) + rack (>= 1.6.0) + rack-test (>= 0.6.3) + regexp_parser (~> 1.2) + xpath (~> 3.2) case_transform (0.2) activesupport charlock_holmes (0.7.6) @@ -145,16 +148,15 @@ GEM cocaine (0.5.8) climate_control (>= 0.0.3, < 1.0) coderay (1.1.2) - colorize (0.8.1) concurrent-ruby (1.0.5) - connection_pool (2.2.1) + connection_pool (2.2.2) crack (0.4.3) safe_yaml (~> 1.0.0) crass (1.0.4) css_parser (1.6.0) addressable debug_inspector (0.0.3) - derailed_benchmarks (1.3.4) + derailed_benchmarks (1.3.5) benchmark-ips (~> 2) get_process_mem (~> 0) heapy (~> 0) @@ -162,7 +164,7 @@ GEM rack (>= 1) rake (> 10, < 13) thor (~> 0.19) - devise (4.4.3) + devise (4.5.0) bcrypt (~> 3.0) orm_adapter (~> 0.1) railties (>= 4.1.0, < 6.0) @@ -174,22 +176,19 @@ GEM devise (~> 4.0) railties (< 5.3) rotp (~> 2.0) - devise_pam_authenticatable2 (9.1.1) + devise_pam_authenticatable2 (9.2.0) devise (>= 4.0.0) rpam2 (~> 4.0) diff-lcs (1.3) docile (1.3.0) domain_name (0.5.20180417) unf (>= 0.0.5, < 1.0.0) - doorkeeper (5.0.0) + doorkeeper (5.0.2) railties (>= 4.2) - dotenv (2.2.2) - dotenv-rails (2.2.2) - dotenv (= 2.2.2) + dotenv (2.5.0) + dotenv-rails (2.5.0) + dotenv (= 2.5.0) railties (>= 3.2, < 6.0) - easy_translate (0.5.1) - thread - thread_safe elasticsearch (6.0.2) elasticsearch-api (= 6.0.2) elasticsearch-transport (= 6.0.2) @@ -202,33 +201,37 @@ GEM encryptor (3.0.0) equatable (0.5.0) erubi (1.7.1) - et-orbi (1.1.0) + et-orbi (1.1.6) tzinfo excon (0.62.0) fabrication (2.20.1) - faker (1.8.7) + faker (1.9.1) i18n (>= 0.7) faraday (0.15.0) multipart-post (>= 1.2, < 3) fast_blank (1.0.0) - fastimage (2.1.1) - ffi (1.9.23) - fog-core (1.45.0) + fastimage (2.1.4) + ffi (1.9.25) + fog-core (2.1.0) builder excon (~> 0.58) formatador (~> 0.2) - fog-json (1.0.2) - fog-core (~> 1.0) + mime-types + fog-json (1.2.0) + fog-core multi_json (~> 1.10) - fog-openstack (0.1.25) - fog-core (~> 1.40) + fog-openstack (0.3.7) + fog-core (>= 1.45, <= 2.1.0) fog-json (>= 1.0) ipaddress (>= 0.8) 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) ruby-progressbar (~> 1.4) - get_process_mem (0.2.1) + get_process_mem (0.2.2) globalid (0.4.1) activesupport (>= 4.2.0) goldfinger (2.1.0) @@ -249,39 +252,40 @@ GEM concurrent-ruby (~> 1.0) hashdiff (0.3.7) hashie (3.5.7) - heapy (0.1.3) - highline (1.7.10) + heapy (0.1.4) + highline (2.0.0) hiredis (0.6.1) - hitimes (1.2.6) + hitimes (1.3.0) hkdf (0.3.0) htmlentities (4.3.4) - http (3.2.0) + http (3.3.0) addressable (~> 2.3) http-cookie (~> 1.0) http-form_data (~> 2.0) http_parser.rb (~> 0.6.0) http-cookie (1.0.3) domain_name (~> 0.5) - http-form_data (2.1.0) + http-form_data (2.1.1) http_accept_language (2.1.1) - httplog (1.0.2) - colorize (~> 0.8) + httplog (1.1.1) rack (>= 1.0) - i18n (1.1.0) + rainbow (>= 2.0.0) + i18n (1.1.1) concurrent-ruby (~> 1.0) - i18n-tasks (0.9.21) + i18n-tasks (0.9.28) activesupport (>= 4.0.2) ast (>= 2.1.0) - easy_translate (>= 0.5.1) erubi - highline (>= 1.7.3) + highline (>= 2.0.0) i18n parser (>= 2.2.3.0) + rails-i18n rainbow (>= 2.2.2, < 4.0) terminal-table (>= 1.5.1) idn-ruby (0.1.0) ipaddress (0.8.3) iso-639 (0.2.8) + jaro_winkler (1.5.1) jmespath (1.4.0) json (2.1.0) json-ld (2.2.1) @@ -315,7 +319,7 @@ GEM activesupport (>= 4) railties (>= 4) request_store (~> 1.0) - loofah (2.2.2) + loofah (2.2.3) crass (~> 1.0.2) nokogiri (>= 1.5.9) mail (2.7.0) @@ -326,16 +330,16 @@ GEM mimemagic (~> 0.3.2) mario-redis-lock (1.2.1) redis (>= 3.0.5) - memory_profiler (0.9.10) + memory_profiler (0.9.12) method_source (0.9.0) microformats (4.0.7) json nokogiri - mime-types (3.1) + mime-types (3.2.2) mime-types-data (~> 3.2015) - mime-types-data (3.2016.0521) + mime-types-data (3.2018.0812) mimemagic (0.3.2) - mini_mime (1.0.0) + mini_mime (1.0.1) mini_portile2 (2.3.0) minitest (5.11.3) msgpack (1.2.4) @@ -345,18 +349,18 @@ GEM net-ldap (0.16.1) net-scp (1.2.1) net-ssh (>= 2.6.5) - net-ssh (4.2.0) + net-ssh (5.0.2) nio4r (2.3.1) - nokogiri (1.8.4) + nokogiri (1.8.5) mini_portile2 (~> 2.3.0) - nokogumbo (1.5.0) - nokogiri + nokogumbo (2.0.0) + nokogiri (~> 1.8, >= 1.8.4) nsa (0.2.4) activesupport (>= 4.2, < 6) concurrent-ruby (~> 1.0.0) sidekiq (>= 3.5.0) statsd-ruby (~> 1.2.0) - oj (3.5.1) + oj (3.7.0) omniauth (1.8.1) hashie (>= 3.4.6, < 3.6.0) rack (>= 1.6.2, < 3) @@ -364,7 +368,7 @@ GEM addressable (~> 2.3) nokogiri (~> 1.5) omniauth (~> 1.2) - omniauth-saml (1.10.0) + omniauth-saml (1.10.1) omniauth (~> 1.3, >= 1.3.2) ruby-saml (~> 1.7) orm_adapter (0.5.0) @@ -372,7 +376,7 @@ GEM addressable (~> 2.5) http (~> 3.0) nokogiri (~> 1.8) - ox (2.9.2) + ox (2.10.0) paperclip (6.0.0) activemodel (>= 4.2.0) activesupport (>= 4.2.0) @@ -383,18 +387,18 @@ GEM av (~> 0.9.0) paperclip (>= 2.5.2) parallel (1.12.1) - parallel_tests (2.21.3) + parallel_tests (2.26.0) parallel - parser (2.5.1.0) + parser (2.5.3.0) ast (~> 2.4.0) pastel (0.7.2) equatable (~> 0.5.0) tty-color (~> 0.4.0) - pg (1.0.0) - pghero (2.1.0) + pg (1.1.3) + pghero (2.2.0) activerecord - pkg-config (1.3.0) - powerpack (0.1.1) + pkg-config (1.3.1) + powerpack (0.1.2) premailer (1.11.1) addressable css_parser (>= 1.6.0) @@ -402,7 +406,7 @@ GEM premailer-rails (1.10.2) actionmailer (>= 3, < 6) premailer (~> 1.7, >= 1.7.9) - private_address_check (0.4.1) + private_address_check (0.5.0) pry (0.11.3) coderay (~> 1.1.0) method_source (~> 0.9.0) @@ -411,15 +415,16 @@ GEM pry (~> 0.10) pry-rails (0.3.6) pry (>= 0.10.4) - public_suffix (3.0.2) - puma (3.11.4) - pundit (1.1.0) + public_suffix (3.0.3) + puma (3.12.0) + pundit (2.0.0) activesupport (>= 3.0.0) + raabro (1.1.6) rack (2.0.5) - rack-attack (5.2.0) - rack + rack-attack (5.4.1) + rack (>= 1.0, < 3) rack-cors (1.0.2) - rack-protection (2.0.1) + rack-protection (2.0.4) rack rack-proxy (0.6.4) rack @@ -447,7 +452,7 @@ GEM nokogiri (>= 1.6) rails-html-sanitizer (1.0.4) loofah (~> 2.2, >= 2.2.2) - rails-i18n (5.1.1) + rails-i18n (5.1.2) i18n (>= 0.7, < 2) railties (>= 5.0, < 6) rails-settings-cached (0.6.6) @@ -468,7 +473,7 @@ GEM link_header (~> 0.0, >= 0.0.8) rdf-normalize (0.3.3) rdf (>= 2.2, < 4.0) - redis (4.0.1) + redis (4.0.2) redis-actionpack (5.0.2) actionpack (>= 4.0, < 6) redis-rack (>= 1, < 3) @@ -487,6 +492,7 @@ GEM redis-store (>= 1.2, < 2) redis-store (1.5.0) redis (>= 2.2, < 5) + regexp_parser (1.2.0) request_store (1.4.1) rack (>= 1.4) responders (2.4.0) @@ -496,60 +502,60 @@ GEM rpam2 (4.0.2) rqrcode (0.10.1) chunky_png (~> 1.0) - rspec-core (3.7.1) - rspec-support (~> 3.7.0) - rspec-expectations (3.7.0) + rspec-core (3.8.0) + rspec-support (~> 3.8.0) + rspec-expectations (3.8.2) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.7.0) - rspec-mocks (3.7.0) + rspec-support (~> 3.8.0) + rspec-mocks (3.8.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.7.0) - rspec-rails (3.7.2) + rspec-support (~> 3.8.0) + rspec-rails (3.8.1) actionpack (>= 3.0) activesupport (>= 3.0) railties (>= 3.0) - rspec-core (~> 3.7.0) - rspec-expectations (~> 3.7.0) - rspec-mocks (~> 3.7.0) - rspec-support (~> 3.7.0) + rspec-core (~> 3.8.0) + rspec-expectations (~> 3.8.0) + rspec-mocks (~> 3.8.0) + rspec-support (~> 3.8.0) rspec-sidekiq (3.0.3) rspec-core (~> 3.0, >= 3.0.0) sidekiq (>= 2.4.0) - rspec-support (3.7.1) - rubocop (0.55.0) + rspec-support (3.8.0) + rubocop (0.60.0) + jaro_winkler (~> 1.5.1) parallel (~> 1.10) - parser (>= 2.5) + parser (>= 2.5, != 2.5.1.1) powerpack (~> 0.1) rainbow (>= 2.2.2, < 4.0) ruby-progressbar (~> 1.7) - unicode-display_width (~> 1.0, >= 1.0.1) - ruby-progressbar (1.9.0) - ruby-saml (1.7.2) + unicode-display_width (~> 1.4.0) + ruby-progressbar (1.10.0) + ruby-saml (1.9.0) nokogiri (>= 1.5.10) - rufus-scheduler (3.4.2) - et-orbi (~> 1.0) + rufus-scheduler (3.5.2) + fugit (~> 1.1, >= 1.1.5) safe_yaml (1.0.4) - sanitize (4.6.4) + sanitize (5.0.0) crass (~> 1.0.2) - nokogiri (>= 1.4.4) - nokogumbo (~> 1.4) - sass (3.5.6) + nokogiri (>= 1.8.0) + nokogumbo (~> 2.0) + sass (3.6.0) sass-listen (~> 4.0.0) sass-listen (4.0.0) rb-fsevent (~> 0.9, >= 0.9.4) rb-inotify (~> 0.9, >= 0.9.7) - scss_lint (0.57.0) + scss_lint (0.57.1) rake (>= 0.9, < 13) - sass (~> 3.5.5) - sidekiq (5.1.3) - concurrent-ruby (~> 1.0) - connection_pool (~> 2.2, >= 2.2.0) + sass (~> 3.5, >= 3.5.5) + sidekiq (5.2.2) + connection_pool (~> 2.2, >= 2.2.2) rack-protection (>= 1.5.0) redis (>= 3.3.5, < 5) sidekiq-bulk (0.1.1) activesupport sidekiq - sidekiq-scheduler (2.2.1) + sidekiq-scheduler (3.0.0) redis (>= 3, < 5) rufus-scheduler (~> 3.2) sidekiq (>= 3) @@ -559,9 +565,9 @@ GEM thor (~> 0) simple-navigation (4.0.5) activesupport (>= 2.3.2) - simple_form (4.0.0) - actionpack (> 4) - activemodel (> 4) + simple_form (4.0.1) + actionpack (>= 5.0) + activemodel (>= 5.0) simplecov (0.16.1) docile (~> 1.1) json (>= 1.8, < 3) @@ -574,15 +580,15 @@ GEM actionpack (>= 4.0) activesupport (>= 4.0) sprockets (>= 3.0.0) - sshkit (1.16.0) + sshkit (1.17.0) net-scp (>= 1.1.2) net-ssh (>= 2.8.0) - stackprof (0.2.11) + stackprof (0.2.12) statsd-ruby (1.2.1) stoplight (2.1.3) streamio-ffmpeg (3.0.2) multi_json (~> 1.8) - strong_migrations (0.2.2) + strong_migrations (0.3.1) activerecord (>= 3.2.0) temple (0.8.0) terminal-table (1.8.0) @@ -590,55 +596,54 @@ GEM terrapin (0.6.0) climate_control (>= 0.0.3, < 1.0) thor (0.20.0) - thread (0.2.2) thread_safe (0.3.6) tilt (2.0.8) timers (4.1.2) hitimes - tty-color (0.4.2) - tty-command (0.8.0) + tty-color (0.4.3) + tty-command (0.8.2) pastel (~> 0.7.0) - tty-cursor (0.5.0) - tty-prompt (0.16.0) + tty-cursor (0.6.0) + tty-prompt (0.17.1) necromancer (~> 0.4.0) pastel (~> 0.7.0) timers (~> 4.0) - tty-cursor (~> 0.5.0) - tty-reader (~> 0.2.0) - tty-reader (0.2.0) - tty-cursor (~> 0.5.0) + tty-cursor (~> 0.6.0) + tty-reader (~> 0.4.0) + tty-reader (0.4.0) + tty-cursor (~> 0.6.0) tty-screen (~> 0.6.4) wisper (~> 2.0.0) - tty-screen (0.6.4) + tty-screen (0.6.5) twitter-text (1.14.7) unf (~> 0.1.0) tzinfo (1.2.5) thread_safe (~> 0.1) - tzinfo-data (1.2018.4) + tzinfo-data (1.2018.7) tzinfo (>= 1.0.0) unf (0.1.4) unf_ext unf_ext (0.0.7.5) - unicode-display_width (1.3.2) + unicode-display_width (1.4.0) uniform_notifier (1.11.0) warden (1.2.7) rack (>= 1.0) - webmock (3.3.0) + webmock (3.4.2) addressable (>= 2.3.6) crack (>= 0.3.2) hashdiff - webpacker (3.4.3) + webpacker (3.5.5) activesupport (>= 4.2) rack-proxy (>= 0.6.1) railties (>= 4.2) - webpush (0.3.3) + webpush (0.3.4) hkdf (~> 0.2) jwt (~> 2.0) websocket-driver (0.7.0) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.3) wisper (2.0.0) - xpath (3.0.0) + xpath (3.2.0) nokogiri (~> 1.8) PLATFORMS @@ -649,44 +654,44 @@ DEPENDENCIES active_record_query_trace (~> 1.5) addressable (~> 2.5) annotate (~> 2.7) - aws-sdk-s3 (~> 1.9) - better_errors (~> 2.4) + aws-sdk-s3 (~> 1.23) + better_errors (~> 2.5) binding_of_caller (~> 0.7) bootsnap (~> 1.3) - brakeman (~> 4.2) + brakeman (~> 4.3) browser bullet (~> 5.7) bundler-audit (~> 0.6) - capistrano (~> 3.10) - capistrano-rails (~> 1.3) + capistrano (~> 3.11) + capistrano-rails (~> 1.4) capistrano-rbenv (~> 2.1) capistrano-yarn (~> 2.0) - capybara (~> 2.18) + capybara (~> 3.10) charlock_holmes (~> 0.7.6) chewy (~> 5.0) cld3 (~> 3.2.0) climate_control (~> 0.2) derailed_benchmarks - devise (~> 4.4) + devise (~> 4.5) devise-two-factor (~> 3.0) - devise_pam_authenticatable2 (~> 9.1) + devise_pam_authenticatable2 (~> 9.2) doorkeeper (~> 5.0) - dotenv-rails (~> 2.2, < 2.3) + dotenv-rails (~> 2.5) fabrication (~> 2.20) - faker (~> 1.8) + faker (~> 1.9) fast_blank (~> 1.0) fastimage - fog-core (~> 1.45) - fog-openstack (~> 0.1) - fuubar (~> 2.2) + fog-core (<= 2.1.0) + fog-openstack (~> 0.3) + fuubar (~> 2.3) goldfinger (~> 2.1) hamlit-rails (~> 0.2) hiredis (~> 0.6) htmlentities (~> 4.3) - http (~> 3.2) + http (~> 3.3) http_accept_language (~> 2.1) http_parser.rb (~> 0.6)! - httplog (~> 1.0) + httplog (~> 1.1) i18n-tasks (~> 0.9) idn-ruby iso-639 @@ -700,30 +705,30 @@ DEPENDENCIES mario-redis-lock (~> 1.2) memory_profiler microformats (~> 4.0) - mime-types (~> 3.1) + mime-types (~> 3.2) net-ldap (~> 0.10) nokogiri (~> 1.8) nsa (~> 0.2) - oj (~> 3.5) + oj (~> 3.7) omniauth (~> 1.2) omniauth-cas (~> 1.1) omniauth-saml (~> 1.10) ostatus2 (~> 2.0) - ox (~> 2.9) + ox (~> 2.10) paperclip (~> 6.0) paperclip-av-transcoder (~> 0.6) - parallel_tests (~> 2.21) - pg (~> 1.0) - pghero (~> 2.1) + parallel_tests (~> 2.26) + pg (~> 1.1) + pghero (~> 2.2) pkg-config (~> 1.3) posix-spawn! premailer-rails - private_address_check (~> 0.4.1) + private_address_check (~> 0.5) pry-byebug (~> 3.6) pry-rails (~> 0.3) - puma (~> 3.11) - pundit (~> 1.1) - rack-attack (~> 5.2) + puma (~> 3.12) + pundit (~> 2.0) + rack-attack (~> 5.4) rack-cors (~> 1.0) rails (~> 5.2.1) rails-controller-testing (~> 1.0) @@ -734,15 +739,14 @@ DEPENDENCIES redis-namespace (~> 1.5) redis-rails (~> 5.0) rqrcode (~> 0.10) - rspec-rails (~> 3.7) + rspec-rails (~> 3.8) rspec-sidekiq (~> 3.0) - rubocop (~> 0.55) - ruby-progressbar (~> 1.4) - sanitize (~> 4.6) + rubocop (~> 0.60) + sanitize (~> 5.0) scss_lint (~> 0.57) - sidekiq (~> 5.1) + sidekiq (~> 5.2) sidekiq-bulk (~> 0.1.1) - sidekiq-scheduler (~> 2.2) + sidekiq-scheduler (~> 3.0) sidekiq-unique-jobs (~> 5.0) simple-navigation (~> 4.0) simple_form (~> 4.0) @@ -751,18 +755,18 @@ DEPENDENCIES stackprof stoplight (~> 2.1.3) streamio-ffmpeg (~> 3.0) - strong_migrations (~> 0.2) + strong_migrations (~> 0.3) thor (~> 0.20) tty-command (~> 0.8) - tty-prompt (~> 0.16) + tty-prompt (~> 0.17) twitter-text (~> 1.14) tzinfo-data (~> 1.2018) - webmock (~> 3.3) - webpacker (~> 3.4) + webmock (~> 3.4) + webpacker (~> 3.5) webpush RUBY VERSION - ruby 2.5.0p0 + ruby 2.5.3p105 BUNDLED WITH - 1.16.3 + 1.16.6 diff --git a/README.md b/README.md index c6496ec5f..6c4918e6f 100644 --- a/README.md +++ b/README.md @@ -1,98 +1,95 @@ ![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] [![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] +[![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 [code_climate]: https://codeclimate.com/github/tootsuite/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 -**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 ---- - -## 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 + + **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! -**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** 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** -Mastodon acts as an OAuth2 provider so 3rd party apps can use the API - -**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. +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! ## 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 -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 ## 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 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 . - ---- - -## Extra credits - -The elephant friend illustrations are created by [Dopatwo](https://mastodon.social/@dopatwo) diff --git a/Vagrantfile b/Vagrantfile index 57fec9e02..5e25efb80 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -85,6 +85,9 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| config.vm.provider :virtualbox do |vb| vb.name = "mastodon" 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. # https://github.com/mitchellh/vagrant/issues/1172 @@ -97,19 +100,22 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| end - config.vm.hostname = "mastodon.dev" - # 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: # $ vagrant plugin install vagrant-hostsupdater + config.vm.hostname = "mastodon.local" + if defined?(VagrantPlugins::HostsUpdater) config.vm.network :private_network, ip: "192.168.42.42", nictype: "virtio" config.hostsupdater.remove_on_suspend = false end 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 config.vm.synced_folder ".", "/vagrant" end diff --git a/app/controllers/activitypub/inboxes_controller.rb b/app/controllers/activitypub/inboxes_controller.rb index af51e32d5..8f5e1887e 100644 --- a/app/controllers/activitypub/inboxes_controller.rb +++ b/app/controllers/activitypub/inboxes_controller.rb @@ -36,6 +36,6 @@ class ActivityPub::InboxesController < Api::BaseController end def process_payload - ActivityPub::ProcessingWorker.perform_async(signed_request_account.id, body.force_encoding('UTF-8')) + ActivityPub::ProcessingWorker.perform_async(signed_request_account.id, body.force_encoding('UTF-8'), @account&.id) end end diff --git a/app/controllers/admin/accounts_controller.rb b/app/controllers/admin/accounts_controller.rb index e7ca6b907..5d57fe361 100644 --- a/app/controllers/admin/accounts_controller.rb +++ b/app/controllers/admin/accounts_controller.rb @@ -95,7 +95,7 @@ module Admin :remote, :by_domain, :silenced, - :recent, + :alphabetic, :suspended, :username, :display_name, diff --git a/app/controllers/admin/base_controller.rb b/app/controllers/admin/base_controller.rb index 7fb69d578..8593b582a 100644 --- a/app/controllers/admin/base_controller.rb +++ b/app/controllers/admin/base_controller.rb @@ -5,8 +5,15 @@ module Admin include Authorization include AccountableConcern - before_action :require_staff! - layout 'admin' + + before_action :require_staff! + before_action :set_body_classes + + private + + def set_body_classes + @body_classes = 'admin' + end end end diff --git a/app/controllers/admin/domain_blocks_controller.rb b/app/controllers/admin/domain_blocks_controller.rb index 64de2cbf0..90c70275a 100644 --- a/app/controllers/admin/domain_blocks_controller.rb +++ b/app/controllers/admin/domain_blocks_controller.rb @@ -46,7 +46,7 @@ module Admin end 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 def retroactive_unblock? diff --git a/app/controllers/admin/reports_controller.rb b/app/controllers/admin/reports_controller.rb index 5d7f43e00..e97ddb9b6 100644 --- a/app/controllers/admin/reports_controller.rb +++ b/app/controllers/admin/reports_controller.rb @@ -44,6 +44,14 @@ module Admin when 'resolve' @report.resolve!(current_account) 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' @report.resolve!(current_account) @report.target_account.update!(silenced: true) @@ -55,6 +63,7 @@ module Admin else raise ActiveRecord::RecordNotFound end + @report.reload end diff --git a/app/controllers/admin/settings_controller.rb b/app/controllers/admin/settings_controller.rb index 7d38f76ae..d9f261489 100644 --- a/app/controllers/admin/settings_controller.rb +++ b/app/controllers/admin/settings_controller.rb @@ -19,6 +19,7 @@ module Admin theme thumbnail hero + mascot min_invite_role activity_api_enabled peers_api_enabled @@ -41,6 +42,7 @@ module Admin UPLOAD_SETTINGS = %w( thumbnail hero + mascot ).freeze def edit diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb index 0b3735087..ac8de5fc0 100644 --- a/app/controllers/api/base_controller.rb +++ b/app/controllers/api/base_controller.rb @@ -53,8 +53,8 @@ class Api::BaseController < ApplicationController [params[:limit].to_i.abs, default_limit * 2].min end - def truthy_param?(key) - ActiveModel::Type::Boolean.new.cast(params[key]) + def params_slice(*keys) + params.slice(*keys).permit(*keys) end def current_resource_owner diff --git a/app/controllers/api/v1/accounts/statuses_controller.rb b/app/controllers/api/v1/accounts/statuses_controller.rb index 06fa6c762..b68a8805f 100644 --- a/app/controllers/api/v1/accounts/statuses_controller.rb +++ b/app/controllers/api/v1/accounts/statuses_controller.rb @@ -28,10 +28,9 @@ class Api::V1::Accounts::StatusesController < Api::BaseController def 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), - params[:max_id], - params[:since_id] + params_slice(:max_id, :since_id, :min_id) ) statuses.merge!(only_media_scope) if truthy_param?(:only_media) @@ -82,7 +81,7 @@ class Api::V1::Accounts::StatusesController < Api::BaseController def prev_path 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 diff --git a/app/controllers/api/v1/conversations_controller.rb b/app/controllers/api/v1/conversations_controller.rb new file mode 100644 index 000000000..b19f27ebf --- /dev/null +++ b/app/controllers/api/v1/conversations_controller.rb @@ -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 diff --git a/app/controllers/api/v1/favourites_controller.rb b/app/controllers/api/v1/favourites_controller.rb index ab5204355..db827f9d4 100644 --- a/app/controllers/api/v1/favourites_controller.rb +++ b/app/controllers/api/v1/favourites_controller.rb @@ -26,10 +26,9 @@ class Api::V1::FavouritesController < Api::BaseController end def results - @_results ||= account_favourites.paginate_by_max_id( + @_results ||= account_favourites.paginate_by_id( limit_param(DEFAULT_STATUSES_LIMIT), - params[:max_id], - params[:since_id] + params_slice(:max_id, :since_id, :min_id) ) end @@ -49,7 +48,7 @@ class Api::V1::FavouritesController < Api::BaseController def prev_path 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 diff --git a/app/controllers/api/v1/instances_controller.rb b/app/controllers/api/v1/instances_controller.rb index 1c6971c18..5686e8d7c 100644 --- a/app/controllers/api/v1/instances_controller.rb +++ b/app/controllers/api/v1/instances_controller.rb @@ -4,6 +4,8 @@ class Api::V1::InstancesController < Api::BaseController respond_to :json 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 diff --git a/app/controllers/api/v1/notifications_controller.rb b/app/controllers/api/v1/notifications_controller.rb index 593c8f9a9..e2dec62af 100644 --- a/app/controllers/api/v1/notifications_controller.rb +++ b/app/controllers/api/v1/notifications_controller.rb @@ -37,10 +37,9 @@ class Api::V1::NotificationsController < Api::BaseController end def paginated_notifications - browserable_account_notifications.paginate_by_max_id( + browserable_account_notifications.paginate_by_id( limit_param(DEFAULT_NOTIFICATIONS_LIMIT), - params[:max_id], - params[:since_id] + params_slice(:max_id, :since_id, :min_id) ) end @@ -64,7 +63,7 @@ class Api::V1::NotificationsController < Api::BaseController def prev_path 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 diff --git a/app/controllers/api/v1/reports_controller.rb b/app/controllers/api/v1/reports_controller.rb index a954101cb..e182a9c6c 100644 --- a/app/controllers/api/v1/reports_controller.rb +++ b/app/controllers/api/v1/reports_controller.rb @@ -1,17 +1,11 @@ # frozen_string_literal: true 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 :require_user! respond_to :json - def index - @reports = current_account.reports - render json: @reports, each_serializer: REST::ReportSerializer - end - def create @report = ReportService.new.call( current_account, @@ -27,7 +21,7 @@ class Api::V1::ReportsController < Api::BaseController private def reported_status_ids - Status.find(status_ids).pluck(:id) + reported_account.statuses.find(status_ids).pluck(:id) end def status_ids diff --git a/app/controllers/api/v1/timelines/home_controller.rb b/app/controllers/api/v1/timelines/home_controller.rb index 4412aaaa3..fcd0757f1 100644 --- a/app/controllers/api/v1/timelines/home_controller.rb +++ b/app/controllers/api/v1/timelines/home_controller.rb @@ -30,7 +30,8 @@ class Api::V1::Timelines::HomeController < Api::BaseController account_home_feed.get( limit_param(DEFAULT_STATUSES_LIMIT), params[:max_id], - params[:since_id] + params[:since_id], + params[:min_id] ) end @@ -51,7 +52,7 @@ class Api::V1::Timelines::HomeController < Api::BaseController end 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 def pagination_max_id diff --git a/app/controllers/api/v1/timelines/list_controller.rb b/app/controllers/api/v1/timelines/list_controller.rb index cfc5f3b5e..a15eae468 100644 --- a/app/controllers/api/v1/timelines/list_controller.rb +++ b/app/controllers/api/v1/timelines/list_controller.rb @@ -32,7 +32,8 @@ class Api::V1::Timelines::ListController < Api::BaseController list_feed.get( limit_param(DEFAULT_STATUSES_LIMIT), params[:max_id], - params[:since_id] + params[:since_id], + params[:min_id] ) end @@ -53,7 +54,7 @@ class Api::V1::Timelines::ListController < Api::BaseController end 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 def pagination_max_id diff --git a/app/controllers/api/v1/timelines/public_controller.rb b/app/controllers/api/v1/timelines/public_controller.rb index 13fe015b7..aabe24324 100644 --- a/app/controllers/api/v1/timelines/public_controller.rb +++ b/app/controllers/api/v1/timelines/public_controller.rb @@ -21,10 +21,9 @@ class Api::V1::Timelines::PublicController < Api::BaseController end def public_statuses - statuses = public_timeline_statuses.paginate_by_max_id( + statuses = public_timeline_statuses.paginate_by_id( limit_param(DEFAULT_STATUSES_LIMIT), - params[:max_id], - params[:since_id] + params_slice(:max_id, :since_id, :min_id) ) if truthy_param?(:only_media) @@ -53,7 +52,7 @@ class Api::V1::Timelines::PublicController < Api::BaseController end 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 def pagination_max_id diff --git a/app/controllers/api/v1/timelines/tag_controller.rb b/app/controllers/api/v1/timelines/tag_controller.rb index 7de49a5ed..cf58d5cf4 100644 --- a/app/controllers/api/v1/timelines/tag_controller.rb +++ b/app/controllers/api/v1/timelines/tag_controller.rb @@ -29,10 +29,9 @@ class Api::V1::Timelines::TagController < Api::BaseController if @tag.nil? [] else - statuses = tag_timeline_statuses.paginate_by_max_id( + statuses = tag_timeline_statuses.paginate_by_id( limit_param(DEFAULT_STATUSES_LIMIT), - params[:max_id], - params[:since_id] + params_slice(:max_id, :since_id, :min_id) ) if truthy_param?(:only_media) @@ -62,7 +61,7 @@ class Api::V1::Timelines::TagController < Api::BaseController end 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 def pagination_max_id diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index d266fa1bd..7bb14aa4c 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -58,6 +58,10 @@ class ApplicationController < ActionController::Base protected + def truthy_param?(key) + ActiveModel::Type::Boolean.new.cast(params[key]) + end + def forbidden respond_with_error(403) end @@ -122,6 +126,7 @@ class ApplicationController < ActionController::Base def respond_with_error(code) respond_to do |format| format.any { head code } + format.html do set_locale render "errors/#{code}", layout: 'error', status: code diff --git a/app/controllers/auth/registrations_controller.rb b/app/controllers/auth/registrations_controller.rb index a19f4e62e..088832be3 100644 --- a/app/controllers/auth/registrations_controller.rb +++ b/app/controllers/auth/registrations_controller.rb @@ -8,7 +8,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController before_action :configure_sign_up_params, only: [:create] before_action :set_sessions, only: [:edit, :update] before_action :set_instance_presenter, only: [:new, :create, :update] - before_action :set_body_classes, only: [:new, :create] + before_action :set_body_classes, only: [:new, :create, :edit, :update] def destroy not_found @@ -81,7 +81,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController end def set_body_classes - @body_classes = 'lighter' + @body_classes = %w(edit update).include?(action_name) ? 'admin' : 'lighter' end def set_invite diff --git a/app/controllers/auth/sessions_controller.rb b/app/controllers/auth/sessions_controller.rb index 62b4a6377..fb8615c31 100644 --- a/app/controllers/auth/sessions_controller.rb +++ b/app/controllers/auth/sessions_controller.rb @@ -27,8 +27,10 @@ class Auth::SessionsController < Devise::SessionsController end def destroy + tmp_stored_location = stored_location_for(:user) super flash.delete(:notice) + store_location_for(:user, tmp_stored_location) if continue_after? end protected @@ -121,4 +123,8 @@ class Auth::SessionsController < Devise::SessionsController end paths end + + def continue_after? + truthy_param?(:continue) + end end diff --git a/app/controllers/concerns/signature_verification.rb b/app/controllers/concerns/signature_verification.rb index 4d77fa432..e5d5e2ca6 100644 --- a/app/controllers/concerns/signature_verification.rb +++ b/app/controllers/concerns/signature_verification.rb @@ -22,6 +22,12 @@ module SignatureVerification return 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'] signature_params = {} @@ -76,7 +82,7 @@ module SignatureVerification def build_signed_string(signed_headers) 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 "#{Request::REQUEST_TARGET}: #{request.method.downcase} #{request.path}" elsif signed_header == 'digest' @@ -89,12 +95,12 @@ module SignatureVerification def matches_time_window? begin - time_sent = DateTime.httpdate(request.headers['Date']) + time_sent = Time.httpdate(request.headers['Date']) rescue ArgumentError return false end - (Time.now.utc - time_sent).abs <= 30 + (Time.now.utc - time_sent).abs <= 12.hours end def body_digest diff --git a/app/controllers/filters_controller.rb b/app/controllers/filters_controller.rb index 175dbab07..d2e0fb739 100644 --- a/app/controllers/filters_controller.rb +++ b/app/controllers/filters_controller.rb @@ -7,6 +7,7 @@ class FiltersController < ApplicationController before_action :set_filters, only: :index before_action :set_filter, only: [:edit, :update, :destroy] + before_action :set_body_classes def index @filters = current_account.custom_filters @@ -54,4 +55,8 @@ class FiltersController < ApplicationController def resource_params params.require(:custom_filter).permit(:phrase, :expires_in, :irreversible, :whole_word, context: []) end + + def set_body_classes + @body_classes = 'admin' + end end diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb index 3aaa2776f..fdb3a0962 100644 --- a/app/controllers/invites_controller.rb +++ b/app/controllers/invites_controller.rb @@ -6,6 +6,7 @@ class InvitesController < ApplicationController layout 'admin' before_action :authenticate_user! + before_action :set_body_classes def index authorize :invite, :create? @@ -44,4 +45,8 @@ class InvitesController < ApplicationController def resource_params params.require(:invite).permit(:max_uses, :expires_in, :autofollow) end + + def set_body_classes + @body_classes = 'admin' + end end diff --git a/app/controllers/oauth/authorizations_controller.rb b/app/controllers/oauth/authorizations_controller.rb index e9cdf9fa8..cebbdc4d0 100644 --- a/app/controllers/oauth/authorizations_controller.rb +++ b/app/controllers/oauth/authorizations_controller.rb @@ -13,4 +13,18 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController def store_current_location store_location_for(:user, request.url) 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 diff --git a/app/controllers/settings/applications_controller.rb b/app/controllers/settings/applications_controller.rb index 2a4962311..a1a2c57fa 100644 --- a/app/controllers/settings/applications_controller.rb +++ b/app/controllers/settings/applications_controller.rb @@ -6,6 +6,7 @@ class Settings::ApplicationsController < ApplicationController before_action :authenticate_user! before_action :set_application, only: [:show, :update, :destroy, :regenerate] before_action :prepare_scopes, only: [:create, :update] + before_action :set_body_classes def index @applications = current_user.applications.order(id: :desc).page(params[:page]) @@ -69,4 +70,8 @@ class Settings::ApplicationsController < ApplicationController scopes = params.fetch(:doorkeeper_application, {}).fetch(:scopes, nil) params[:doorkeeper_application][:scopes] = scopes.join(' ') if scopes.is_a? Array end + + def set_body_classes + @body_classes = 'admin' + end end diff --git a/app/controllers/settings/deletes_controller.rb b/app/controllers/settings/deletes_controller.rb index 80002b995..97f3946c8 100644 --- a/app/controllers/settings/deletes_controller.rb +++ b/app/controllers/settings/deletes_controller.rb @@ -5,6 +5,7 @@ class Settings::DeletesController < ApplicationController before_action :check_enabled_deletion before_action :authenticate_user! + before_action :set_body_classes def show @confirmation = Form::DeleteConfirmation.new @@ -29,4 +30,8 @@ class Settings::DeletesController < ApplicationController def delete_params params.require(:form_delete_confirmation).permit(:password) end + + def set_body_classes + @body_classes = 'admin' + end end diff --git a/app/controllers/settings/exports_controller.rb b/app/controllers/settings/exports_controller.rb index 869e11d3b..3a2334ef0 100644 --- a/app/controllers/settings/exports_controller.rb +++ b/app/controllers/settings/exports_controller.rb @@ -6,6 +6,7 @@ class Settings::ExportsController < ApplicationController layout 'admin' before_action :authenticate_user! + before_action :set_body_classes def show @export = Export.new(current_account) @@ -20,4 +21,10 @@ class Settings::ExportsController < ApplicationController redirect_to settings_export_path end + + private + + def set_body_classes + @body_classes = 'admin' + end end diff --git a/app/controllers/settings/follower_domains_controller.rb b/app/controllers/settings/follower_domains_controller.rb index a128bd136..9c39e66bb 100644 --- a/app/controllers/settings/follower_domains_controller.rb +++ b/app/controllers/settings/follower_domains_controller.rb @@ -4,6 +4,7 @@ class Settings::FollowerDomainsController < ApplicationController layout 'admin' before_action :authenticate_user! + before_action :set_body_classes def show @account = current_account @@ -25,4 +26,8 @@ class Settings::FollowerDomainsController < ApplicationController def bulk_params params.permit(select: []) end + + def set_body_classes + @body_classes = 'admin' + end end diff --git a/app/controllers/settings/imports_controller.rb b/app/controllers/settings/imports_controller.rb index 0db13d1ca..e9548ce62 100644 --- a/app/controllers/settings/imports_controller.rb +++ b/app/controllers/settings/imports_controller.rb @@ -5,6 +5,7 @@ class Settings::ImportsController < ApplicationController before_action :authenticate_user! before_action :set_account + before_action :set_body_classes def show @import = Import.new @@ -31,4 +32,8 @@ class Settings::ImportsController < ApplicationController def import_params params.require(:import).permit(:data, :type) end + + def set_body_classes + @body_classes = 'admin' + end end diff --git a/app/controllers/settings/migrations_controller.rb b/app/controllers/settings/migrations_controller.rb index bc6436b87..bd4f9c87a 100644 --- a/app/controllers/settings/migrations_controller.rb +++ b/app/controllers/settings/migrations_controller.rb @@ -4,6 +4,7 @@ class Settings::MigrationsController < ApplicationController layout 'admin' before_action :authenticate_user! + before_action :set_body_classes def show @migration = Form::Migration.new(account: current_account.moved_to_account) @@ -31,4 +32,8 @@ class Settings::MigrationsController < ApplicationController current_account.moved_to_account_id != @migration.account&.id && current_account.id != @migration.account&.id end + + def set_body_classes + @body_classes = 'admin' + end end diff --git a/app/controllers/settings/notifications_controller.rb b/app/controllers/settings/notifications_controller.rb index fe45c17b2..d0754296c 100644 --- a/app/controllers/settings/notifications_controller.rb +++ b/app/controllers/settings/notifications_controller.rb @@ -4,6 +4,7 @@ class Settings::NotificationsController < ApplicationController layout 'admin' before_action :authenticate_user! + before_action :set_body_classes def show; end @@ -29,4 +30,8 @@ class Settings::NotificationsController < ApplicationController interactions: %i(must_be_follower must_be_following must_be_following_dm) ) end + + def set_body_classes + @body_classes = 'admin' + end end diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb index b475c722d..7bb5fb112 100644 --- a/app/controllers/settings/preferences_controller.rb +++ b/app/controllers/settings/preferences_controller.rb @@ -4,6 +4,7 @@ class Settings::PreferencesController < ApplicationController layout 'admin' before_action :authenticate_user! + before_action :set_body_classes def show; end @@ -40,7 +41,8 @@ class Settings::PreferencesController < ApplicationController :setting_boost_modal, :setting_delete_modal, :setting_auto_play_gif, - :setting_display_sensitive_media, + :setting_display_media, + :setting_expand_spoilers, :setting_reduce_motion, :setting_system_font_ui, :setting_noindex, @@ -50,4 +52,8 @@ class Settings::PreferencesController < ApplicationController interactions: %i(must_be_follower must_be_following) ) end + + def set_body_classes + @body_classes = 'admin' + end end diff --git a/app/controllers/settings/profiles_controller.rb b/app/controllers/settings/profiles_controller.rb index fe265c81d..5b3bfd71f 100644 --- a/app/controllers/settings/profiles_controller.rb +++ b/app/controllers/settings/profiles_controller.rb @@ -7,6 +7,7 @@ class Settings::ProfilesController < ApplicationController before_action :authenticate_user! before_action :set_account + before_action :set_body_classes obfuscate_filename [:account, :avatar] obfuscate_filename [:account, :header] @@ -34,4 +35,8 @@ class Settings::ProfilesController < ApplicationController def set_account @account = current_user.account end + + def set_body_classes + @body_classes = 'admin' + end end diff --git a/app/controllers/settings/sessions_controller.rb b/app/controllers/settings/sessions_controller.rb index 0da1b027b..74cebc07b 100644 --- a/app/controllers/settings/sessions_controller.rb +++ b/app/controllers/settings/sessions_controller.rb @@ -2,6 +2,7 @@ class Settings::SessionsController < ApplicationController before_action :set_session, only: :destroy + before_action :set_body_classes def destroy @session.destroy! @@ -14,4 +15,8 @@ class Settings::SessionsController < ApplicationController def set_session @session = current_user.session_activations.find(params[:id]) end + + def set_body_classes + @body_classes = 'admin' + end end diff --git a/app/controllers/settings/two_factor_authentication/confirmations_controller.rb b/app/controllers/settings/two_factor_authentication/confirmations_controller.rb index 8d534960d..ee567c2a7 100644 --- a/app/controllers/settings/two_factor_authentication/confirmations_controller.rb +++ b/app/controllers/settings/two_factor_authentication/confirmations_controller.rb @@ -7,6 +7,7 @@ module Settings before_action :authenticate_user! before_action :ensure_otp_secret + before_action :set_body_classes def new prepare_two_factor_form @@ -43,6 +44,10 @@ module Settings def ensure_otp_secret redirect_to settings_two_factor_authentication_path unless current_user.otp_secret end + + def set_body_classes + @body_classes = 'admin' + end end end end diff --git a/app/controllers/settings/two_factor_authentication/recovery_codes_controller.rb b/app/controllers/settings/two_factor_authentication/recovery_codes_controller.rb index e591e9502..bfb103620 100644 --- a/app/controllers/settings/two_factor_authentication/recovery_codes_controller.rb +++ b/app/controllers/settings/two_factor_authentication/recovery_codes_controller.rb @@ -6,6 +6,7 @@ module Settings layout 'admin' before_action :authenticate_user! + before_action :set_body_classes def create @recovery_codes = current_user.generate_otp_backup_codes! @@ -13,6 +14,12 @@ module Settings flash[:notice] = I18n.t('two_factor_authentication.recovery_codes_regenerated') render :index end + + private + + def set_body_classes + @body_classes = 'admin' + end end end end diff --git a/app/controllers/settings/two_factor_authentications_controller.rb b/app/controllers/settings/two_factor_authentications_controller.rb index 863cc7351..e4d8aed41 100644 --- a/app/controllers/settings/two_factor_authentications_controller.rb +++ b/app/controllers/settings/two_factor_authentications_controller.rb @@ -6,6 +6,7 @@ module Settings before_action :authenticate_user! before_action :verify_otp_required, only: [:create] + before_action :set_body_classes def show @confirmation = Form::TwoFactorConfirmation.new @@ -43,5 +44,9 @@ module Settings current_user.validate_and_consume_otp!(confirmation_params[:code]) || current_user.invalidate_otp_backup_code!(confirmation_params[:code]) end + + def set_body_classes + @body_classes = 'admin' + end end end diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb index d4ad3df60..0f3fe198f 100644 --- a/app/controllers/statuses_controller.rb +++ b/app/controllers/statuses_controller.rb @@ -19,6 +19,10 @@ class StatusesController < ApplicationController before_action :set_referrer_policy_header, only: [:show] before_action :set_cache_headers + content_security_policy only: :embed do |p| + p.frame_ancestors(false) + end + def show respond_to do |format| format.html do diff --git a/app/helpers/admin/account_moderation_notes_helper.rb b/app/helpers/admin/account_moderation_notes_helper.rb index 25586fbba..4d8f0352e 100644 --- a/app/helpers/admin/account_moderation_notes_helper.rb +++ b/app/helpers/admin/account_moderation_notes_helper.rb @@ -4,7 +4,7 @@ module Admin::AccountModerationNotesHelper def admin_account_link_to(account) 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([ image_tag(account.avatar.url, width: 15, height: 15, alt: display_name(account), class: 'avatar'), content_tag(:span, account.acct, class: 'username'), @@ -15,7 +15,7 @@ module Admin::AccountModerationNotesHelper def admin_account_inline_link_to(account) 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') end end diff --git a/app/helpers/admin/filter_helper.rb b/app/helpers/admin/filter_helper.rb index 359c43d0e..60e5142e3 100644 --- a/app/helpers/admin/filter_helper.rb +++ b/app/helpers/admin/filter_helper.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true 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 INVITE_FILTER = %i(available expired).freeze CUSTOM_EMOJI_FILTERS = %i(local remote by_domain shortcode).freeze diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 758f864d7..c33975cac 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -7,8 +7,8 @@ module ApplicationHelper follow ).freeze - def active_nav_class(path) - current_page?(path) ? 'active' : '' + def active_nav_class(*paths) + paths.any? { |path| current_page?(path) } ? 'active' : '' end def active_link_to(label, path, **options) @@ -81,4 +81,20 @@ module ApplicationHelper output << 'rtl' if locale_direction == 'rtl' output.reject(&:blank?).join(' ') 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 diff --git a/app/helpers/home_helper.rb b/app/helpers/home_helper.rb index f5b501235..ba7c443c2 100644 --- a/app/helpers/home_helper.rb +++ b/app/helpers/home_helper.rb @@ -7,13 +7,13 @@ module HomeHelper } 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__wrapper') do section = if account.nil? content_tag(:div, class: 'account__display-name') 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 + content_tag(:span, class: 'display-name') do content_tag(:strong, t('about.contact_missing')) + @@ -21,9 +21,9 @@ module HomeHelper end end 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', 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 + content_tag(:span, class: 'display-name') do content_tag(:bdi) do @@ -48,4 +48,12 @@ module HomeHelper '1+' end end + + def custom_field_classes(field) + if field.verified? + 'verified' + else + 'emojify' + end + end end diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb index fc03fcf82..ed873ceed 100644 --- a/app/helpers/settings_helper.rb +++ b/app/helpers/settings_helper.rb @@ -8,6 +8,7 @@ module SettingsHelper bg: 'Български', ca: 'Català', co: 'Corsu', + cs: 'Čeština', cy: 'Cymraeg', da: 'Dansk', de: 'Deutsch', diff --git a/app/javascript/mastodon/actions/cards.js b/app/javascript/mastodon/actions/cards.js deleted file mode 100644 index baf04833a..000000000 --- a/app/javascript/mastodon/actions/cards.js +++ /dev/null @@ -1,52 +0,0 @@ -import api from '../api'; - -export const STATUS_CARD_FETCH_REQUEST = 'STATUS_CARD_FETCH_REQUEST'; -export const STATUS_CARD_FETCH_SUCCESS = 'STATUS_CARD_FETCH_SUCCESS'; -export const STATUS_CARD_FETCH_FAIL = 'STATUS_CARD_FETCH_FAIL'; - -export function fetchStatusCard(id) { - return (dispatch, getState) => { - if (getState().getIn(['cards', id], null) !== null) { - return; - } - - dispatch(fetchStatusCardRequest(id)); - - api(getState).get(`/api/v1/statuses/${id}/card`).then(response => { - if (!response.data.url) { - return; - } - - dispatch(fetchStatusCardSuccess(id, response.data)); - }).catch(error => { - dispatch(fetchStatusCardFail(id, error)); - }); - }; -}; - -export function fetchStatusCardRequest(id) { - return { - type: STATUS_CARD_FETCH_REQUEST, - id, - skipLoading: true, - }; -}; - -export function fetchStatusCardSuccess(id, card) { - return { - type: STATUS_CARD_FETCH_SUCCESS, - id, - card, - skipLoading: true, - }; -}; - -export function fetchStatusCardFail(id, error) { - return { - type: STATUS_CARD_FETCH_FAIL, - id, - error, - skipLoading: true, - skipAlert: true, - }; -}; diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js index 6d975cd1e..86d83122f 100644 --- a/app/javascript/mastodon/actions/compose.js +++ b/app/javascript/mastodon/actions/compose.js @@ -56,7 +56,7 @@ export function changeCompose(text) { }; }; -export function replyCompose(status, router) { +export function replyCompose(status, routerHistory) { return (dispatch, getState) => { dispatch({ type: COMPOSE_REPLY, @@ -64,7 +64,7 @@ export function replyCompose(status, router) { }); if (!getState().getIn(['compose', 'mounted'])) { - router.push('/statuses/new'); + routerHistory.push('/statuses/new'); } }; }; @@ -81,7 +81,7 @@ export function resetCompose() { }; }; -export function mentionCompose(account, router) { +export function mentionCompose(account, routerHistory) { return (dispatch, getState) => { dispatch({ type: COMPOSE_MENTION, @@ -89,12 +89,12 @@ export function mentionCompose(account, router) { }); 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) => { dispatch({ type: COMPOSE_DIRECT, @@ -102,12 +102,12 @@ export function directCompose(account, router) { }); if (!getState().getIn(['compose', 'mounted'])) { - router.push('/statuses/new'); + routerHistory.push('/statuses/new'); } }; }; -export function submitCompose() { +export function submitCompose(routerHistory) { return function (dispatch, getState) { const status = getState().getIn(['compose', 'text'], ''); const media = getState().getIn(['compose', 'media_attachments']); @@ -133,21 +133,24 @@ export function submitCompose() { dispatch(insertIntoTagHistory(response.data.tags, status)); 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) { dispatch(updateTimeline(timelineId, { ...response.data })); } }; - insertIfOnline('home'); + if (response.data.visibility === 'direct' && getState().getIn(['conversations', 'mounted']) <= 0 && routerHistory) { + routerHistory.push('/timelines/direct'); + } else if (response.data.visibility !== 'direct') { + insertIfOnline('home'); + } if (response.data.in_reply_to_id === null && response.data.visibility === 'public') { insertIfOnline('community'); insertIfOnline('public'); - } else if (response.data.visibility === 'direct') { - insertIfOnline('direct'); } }).catch(function (error) { dispatch(submitComposeFail(error)); diff --git a/app/javascript/mastodon/actions/conversations.js b/app/javascript/mastodon/actions/conversations.js new file mode 100644 index 000000000..aefd2fef7 --- /dev/null +++ b/app/javascript/mastodon/actions/conversations.js @@ -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, + }); +}; diff --git a/app/javascript/mastodon/actions/dropdown_menu.js b/app/javascript/mastodon/actions/dropdown_menu.js index 217ba4e74..14f2939c7 100644 --- a/app/javascript/mastodon/actions/dropdown_menu.js +++ b/app/javascript/mastodon/actions/dropdown_menu.js @@ -1,8 +1,8 @@ export const DROPDOWN_MENU_OPEN = 'DROPDOWN_MENU_OPEN'; export const DROPDOWN_MENU_CLOSE = 'DROPDOWN_MENU_CLOSE'; -export function openDropdownMenu(id, placement) { - return { type: DROPDOWN_MENU_OPEN, id, placement }; +export function openDropdownMenu(id, placement, keyboard) { + return { type: DROPDOWN_MENU_OPEN, id, placement, keyboard }; } export function closeDropdownMenu(id) { diff --git a/app/javascript/mastodon/actions/importer/normalizer.js b/app/javascript/mastodon/actions/importer/normalizer.js index 10a39e050..34a4150fa 100644 --- a/app/javascript/mastodon/actions/importer/normalizer.js +++ b/app/javascript/mastodon/actions/importer/normalizer.js @@ -1,6 +1,7 @@ import escapeTextContentForBrowser from 'escape-html'; import emojify from '../../features/emoji/emoji'; import { unescapeHTML } from '../../utils/html'; +import { expandSpoilers } from '../../initial_state'; const domParser = new DOMParser(); @@ -13,7 +14,7 @@ export function normalizeAccount(account) { account = { ...account }; const emojiMap = makeEmojiMap(account); - const displayName = account.display_name.length === 0 ? account.username : account.display_name; + const displayName = account.display_name.trim().length === 0 ? account.username : account.display_name; account.display_name_html = emojify(escapeTextContentForBrowser(displayName), emojiMap); account.note_emojified = emojify(account.note, emojiMap); @@ -57,7 +58,7 @@ export function normalizeStatus(status, normalOldStatus) { normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent; normalStatus.contentHtml = emojify(normalStatus.content, 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; diff --git a/app/javascript/mastodon/actions/statuses.js b/app/javascript/mastodon/actions/statuses.js index 8d5e72bec..e7c89b4ba 100644 --- a/app/javascript/mastodon/actions/statuses.js +++ b/app/javascript/mastodon/actions/statuses.js @@ -3,7 +3,6 @@ import openDB from '../storage/db'; import { evictStatus } from '../storage/modifier'; import { deleteFromTimelines } from './timelines'; -import { fetchStatusCard } from './cards'; import { importFetchedStatus, importFetchedStatuses, importAccount, importStatus } from './importer'; export const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST'; @@ -86,7 +85,6 @@ export function fetchStatus(id) { const skipLoading = getState().getIn(['statuses', id], null) !== null; dispatch(fetchContext(id)); - dispatch(fetchStatusCard(id)); if (skipLoading) { return; diff --git a/app/javascript/mastodon/actions/streaming.js b/app/javascript/mastodon/actions/streaming.js index 32fc67e67..8cf055540 100644 --- a/app/javascript/mastodon/actions/streaming.js +++ b/app/javascript/mastodon/actions/streaming.js @@ -6,6 +6,7 @@ import { disconnectTimeline, } from './timelines'; import { updateNotifications, expandNotifications } from './notifications'; +import { updateConversations } from './conversations'; import { fetchFilters } from './filters'; import { getLocale } from '../locales'; @@ -31,6 +32,9 @@ export function connectTimelineStream (timelineId, path, pollingRefresh = null) case 'notification': dispatch(updateNotifications(JSON.parse(data.payload), messages, locale)); break; + case 'conversation': + dispatch(updateConversations(JSON.parse(data.payload))); + break; case 'filters_changed': dispatch(fetchFilters()); break; diff --git a/app/javascript/mastodon/actions/suggestions.js b/app/javascript/mastodon/actions/suggestions.js new file mode 100644 index 000000000..b15bd916b --- /dev/null +++ b/app/javascript/mastodon/actions/suggestions.js @@ -0,0 +1,52 @@ +import api from '../api'; +import { importFetchedAccounts } from './importer'; + +export const SUGGESTIONS_FETCH_REQUEST = 'SUGGESTIONS_FETCH_REQUEST'; +export const SUGGESTIONS_FETCH_SUCCESS = 'SUGGESTIONS_FETCH_SUCCESS'; +export const SUGGESTIONS_FETCH_FAIL = 'SUGGESTIONS_FETCH_FAIL'; + +export const SUGGESTIONS_DISMISS = 'SUGGESTIONS_DISMISS'; + +export function fetchSuggestions() { + return (dispatch, getState) => { + dispatch(fetchSuggestionsRequest()); + + api(getState).get('/api/v1/suggestions').then(response => { + dispatch(importFetchedAccounts(response.data)); + dispatch(fetchSuggestionsSuccess(response.data)); + }).catch(error => dispatch(fetchSuggestionsFail(error))); + }; +}; + +export function fetchSuggestionsRequest() { + return { + type: SUGGESTIONS_FETCH_REQUEST, + skipLoading: true, + }; +}; + +export function fetchSuggestionsSuccess(accounts) { + return { + type: SUGGESTIONS_FETCH_SUCCESS, + accounts, + skipLoading: true, + }; +}; + +export function fetchSuggestionsFail(error) { + return { + type: SUGGESTIONS_FETCH_FAIL, + error, + skipLoading: true, + skipAlert: true, + }; +}; + +export const dismissSuggestion = accountId => (dispatch, getState) => { + dispatch({ + type: SUGGESTIONS_DISMISS, + id: accountId, + }); + + api(getState).delete(`/api/v1/suggestions/${accountId}`); +}; diff --git a/app/javascript/mastodon/actions/timelines.js b/app/javascript/mastodon/actions/timelines.js index e8fd441e1..c4fc6448c 100644 --- a/app/javascript/mastodon/actions/timelines.js +++ b/app/javascript/mastodon/actions/timelines.js @@ -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 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 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 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 }); diff --git a/app/javascript/mastodon/components/__tests__/__snapshots__/autosuggest_emoji-test.js.snap b/app/javascript/mastodon/components/__tests__/__snapshots__/autosuggest_emoji-test.js.snap new file mode 100644 index 000000000..1c3727848 --- /dev/null +++ b/app/javascript/mastodon/components/__tests__/__snapshots__/autosuggest_emoji-test.js.snap @@ -0,0 +1,27 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders emoji with custom url 1`] = ` +
+ foobar + :foobar: +
+`; + +exports[` renders native emoji 1`] = ` +
+ 💙 + :foobar: +
+`; diff --git a/app/javascript/mastodon/components/__tests__/__snapshots__/button-test.js.snap b/app/javascript/mastodon/components/__tests__/__snapshots__/button-test.js.snap index c3f018d90..5c04e0979 100644 --- a/app/javascript/mastodon/components/__tests__/__snapshots__/button-test.js.snap +++ b/app/javascript/mastodon/components/__tests__/__snapshots__/button-test.js.snap @@ -3,7 +3,6 @@ exports[` + ); + if (status.get('spoiler_text').length > 0) { let mentionsPlaceholder = ''; @@ -167,17 +193,24 @@ export default class StatusContent extends React.PureComponent { ); } else if (this.props.onClick) { - return ( + const output = [
- ); + />, + ]; + + if (this.state.collapsed) { + output.push(readMoreButton); + } + + return output; } else { return (
({ isModalOpen: state.get('modal').modalType === 'ACTIONS', dropdownPlacement: state.getIn(['dropdown_menu', 'placement']), openDropdownId: state.getIn(['dropdown_menu', 'openId']), + openedViaKeyboard: state.getIn(['dropdown_menu', 'keyboard']), }); const mapDispatchToProps = (dispatch, { status, items }) => ({ - onOpen(id, onItemClick, dropdownPlacement) { + onOpen(id, onItemClick, dropdownPlacement, keyboard) { dispatch(isUserTouching() ? openModal('ACTIONS', { status, actions: items, onClick: onItemClick, - }) : openDropdownMenu(id, dropdownPlacement)); + }) : openDropdownMenu(id, dropdownPlacement, keyboard)); }, onClose(id) { dispatch(closeModal()); diff --git a/app/javascript/mastodon/containers/status_container.js b/app/javascript/mastodon/containers/status_container.js index bbc0d5e96..b3555c76e 100644 --- a/app/javascript/mastodon/containers/status_container.js +++ b/app/javascript/mastodon/containers/status_container.js @@ -36,6 +36,8 @@ const messages = defineMessages({ 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.' }, 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 = () => { @@ -51,7 +53,18 @@ const makeMapStateToProps = () => { const mapDispatchToProps = (dispatch, { intl }) => ({ 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) { diff --git a/app/javascript/mastodon/features/account/components/action_bar.js b/app/javascript/mastodon/features/account/components/action_bar.js index bc6f86628..c9ae2df98 100644 --- a/app/javascript/mastodon/features/account/components/action_bar.js +++ b/app/javascript/mastodon/features/account/components/action_bar.js @@ -2,7 +2,7 @@ import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; 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 { me } from '../../../initial_state'; import { shortNumberFormat } from '../../../utils/numbers'; @@ -36,8 +36,8 @@ const messages = defineMessages({ unendorse: { id: 'account.unendorse', defaultMessage: 'Don\'t feature on profile' }, }); -@injectIntl -export default class ActionBar extends React.PureComponent { +export default @injectIntl +class ActionBar extends React.PureComponent { static propTypes = { 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 () { const { account, intl } = this.props; @@ -147,20 +154,20 @@ export default class ActionBar extends React.PureComponent {
- + {shortNumberFormat(account.get('statuses_count'))} - + - + {shortNumberFormat(account.get('following_count'))} - + - + {shortNumberFormat(account.get('followers_count'))} - +
diff --git a/app/javascript/mastodon/features/account/components/header.js b/app/javascript/mastodon/features/account/components/header.js index eb9236aeb..11ae58905 100644 --- a/app/javascript/mastodon/features/account/components/header.js +++ b/app/javascript/mastodon/features/account/components/header.js @@ -15,8 +15,18 @@ const messages = defineMessages({ requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' }, unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, 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 { static propTypes = { @@ -65,8 +75,8 @@ class Avatar extends ImmutablePureComponent { } -@injectIntl -export default class Header extends ImmutablePureComponent { +export default @injectIntl +class Header extends ImmutablePureComponent { static propTypes = { account: ImmutablePropTypes.map, @@ -163,7 +173,10 @@ export default class Header extends ImmutablePureComponent { {fields.map((pair, i) => (
-
+ +
+ {pair.get('verified_at') && } +
))}
diff --git a/app/javascript/mastodon/features/account_gallery/components/media_item.js b/app/javascript/mastodon/features/account_gallery/components/media_item.js index f7a802dc7..7c330c430 100644 --- a/app/javascript/mastodon/features/account_gallery/components/media_item.js +++ b/app/javascript/mastodon/features/account_gallery/components/media_item.js @@ -2,7 +2,7 @@ import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; import Permalink from '../../../components/permalink'; -import { displaySensitiveMedia } from '../../../initial_state'; +import { displayMedia } from '../../../initial_state'; export default class MediaItem extends ImmutablePureComponent { @@ -11,7 +11,7 @@ export default class MediaItem extends ImmutablePureComponent { }; state = { - visible: !this.props.media.getIn(['status', 'sensitive']) || displaySensitiveMedia, + visible: displayMedia !== 'hide_all' && !this.props.media.getIn(['status', 'sensitive']) || displayMedia === 'show_all', }; handleClick = () => { diff --git a/app/javascript/mastodon/features/account_gallery/index.js b/app/javascript/mastodon/features/account_gallery/index.js index a6c464aff..32cb5ebdc 100644 --- a/app/javascript/mastodon/features/account_gallery/index.js +++ b/app/javascript/mastodon/features/account_gallery/index.js @@ -43,8 +43,8 @@ class LoadMoreMedia extends ImmutablePureComponent { } -@connect(mapStateToProps) -export default class AccountGallery extends ImmutablePureComponent { +export default @connect(mapStateToProps) +class AccountGallery extends ImmutablePureComponent { static propTypes = { params: PropTypes.object.isRequired, diff --git a/app/javascript/mastodon/features/account_timeline/index.js b/app/javascript/mastodon/features/account_timeline/index.js index 934513cd7..6055af51d 100644 --- a/app/javascript/mastodon/features/account_timeline/index.js +++ b/app/javascript/mastodon/features/account_timeline/index.js @@ -23,8 +23,8 @@ const mapStateToProps = (state, { params: { accountId }, withReplies = false }) }; }; -@connect(mapStateToProps) -export default class AccountTimeline extends ImmutablePureComponent { +export default @connect(mapStateToProps) +class AccountTimeline extends ImmutablePureComponent { static propTypes = { params: PropTypes.object.isRequired, diff --git a/app/javascript/mastodon/features/blocks/index.js b/app/javascript/mastodon/features/blocks/index.js index 68661a37c..ca7ce6f8e 100644 --- a/app/javascript/mastodon/features/blocks/index.js +++ b/app/javascript/mastodon/features/blocks/index.js @@ -20,9 +20,9 @@ const mapStateToProps = state => ({ accountIds: state.getIn(['user_lists', 'blocks', 'items']), }); -@connect(mapStateToProps) +export default @connect(mapStateToProps) @injectIntl -export default class Blocks extends ImmutablePureComponent { +class Blocks extends ImmutablePureComponent { static propTypes = { params: PropTypes.object.isRequired, diff --git a/app/javascript/mastodon/features/community_timeline/components/column_settings.js b/app/javascript/mastodon/features/community_timeline/components/column_settings.js index f4325f58d..8250190a7 100644 --- a/app/javascript/mastodon/features/community_timeline/components/column_settings.js +++ b/app/javascript/mastodon/features/community_timeline/components/column_settings.js @@ -4,8 +4,8 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import { injectIntl, FormattedMessage } from 'react-intl'; import SettingToggle from '../../notifications/components/setting_toggle'; -@injectIntl -export default class ColumnSettings extends React.PureComponent { +export default @injectIntl +class ColumnSettings extends React.PureComponent { static propTypes = { settings: ImmutablePropTypes.map.isRequired, diff --git a/app/javascript/mastodon/features/community_timeline/index.js b/app/javascript/mastodon/features/community_timeline/index.js index 48d2b3f68..7d26c98b0 100644 --- a/app/javascript/mastodon/features/community_timeline/index.js +++ b/app/javascript/mastodon/features/community_timeline/index.js @@ -25,9 +25,9 @@ const mapStateToProps = (state, { onlyMedia, columnId }) => { }; }; -@connect(mapStateToProps) +export default @connect(mapStateToProps) @injectIntl -export default class CommunityTimeline extends React.PureComponent { +class CommunityTimeline extends React.PureComponent { static contextTypes = { router: PropTypes.object, diff --git a/app/javascript/mastodon/features/compose/components/action_bar.js b/app/javascript/mastodon/features/compose/components/action_bar.js index daee18552..95d6eeb06 100644 --- a/app/javascript/mastodon/features/compose/components/action_bar.js +++ b/app/javascript/mastodon/features/compose/components/action_bar.js @@ -17,8 +17,8 @@ const messages = defineMessages({ filters: { id: 'navigation_bar.filters', defaultMessage: 'Muted words' }, }); -@injectIntl -export default class ActionBar extends React.PureComponent { +export default @injectIntl +class ActionBar extends React.PureComponent { static propTypes = { account: ImmutablePropTypes.map.isRequired, diff --git a/app/javascript/mastodon/features/compose/components/compose_form.js b/app/javascript/mastodon/features/compose/components/compose_form.js index 6eb01123e..0625ab223 100644 --- a/app/javascript/mastodon/features/compose/components/compose_form.js +++ b/app/javascript/mastodon/features/compose/components/compose_form.js @@ -27,8 +27,12 @@ const messages = defineMessages({ publishLoud: { id: 'compose_form.publish_loud', defaultMessage: '{publish}!' }, }); -@injectIntl -export default class ComposeForm extends ImmutablePureComponent { +export default @injectIntl +class ComposeForm extends ImmutablePureComponent { + + static contextTypes = { + router: PropTypes.object, + }; static propTypes = { intl: PropTypes.object.isRequired, @@ -84,7 +88,7 @@ export default class ComposeForm extends ImmutablePureComponent { return; } - this.props.onSubmit(); + this.props.onSubmit(this.context.router ? this.context.router.history : null); } onSuggestionsClearRequested = () => { diff --git a/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js index 4fc32db8a..c1429c756 100644 --- a/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js +++ b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js @@ -261,6 +261,7 @@ class EmojiPickerMenu extends React.PureComponent { skin={skinTone} showPreview={false} backgroundImageFn={backgroundImageFn} + autoFocus emojiTooltip /> @@ -277,8 +278,8 @@ class EmojiPickerMenu extends React.PureComponent { } -@injectIntl -export default class EmojiPickerDropdown extends React.PureComponent { +export default @injectIntl +class EmojiPickerDropdown extends React.PureComponent { static propTypes = { custom_emojis: ImmutablePropTypes.list, diff --git a/app/javascript/mastodon/features/compose/components/privacy_dropdown.js b/app/javascript/mastodon/features/compose/components/privacy_dropdown.js index e19778fd2..5698765d9 100644 --- a/app/javascript/mastodon/features/compose/components/privacy_dropdown.js +++ b/app/javascript/mastodon/features/compose/components/privacy_dropdown.js @@ -149,8 +149,8 @@ class PrivacyDropdownMenu extends React.PureComponent { } -@injectIntl -export default class PrivacyDropdown extends React.PureComponent { +export default @injectIntl +class PrivacyDropdown extends React.PureComponent { static propTypes = { isUserTouching: PropTypes.func, @@ -164,7 +164,7 @@ export default class PrivacyDropdown extends React.PureComponent { state = { open: false, - placement: null, + placement: 'bottom', }; handleToggle = ({ target }) => { diff --git a/app/javascript/mastodon/features/compose/components/reply_indicator.js b/app/javascript/mastodon/features/compose/components/reply_indicator.js index 6f358a98b..142223f3d 100644 --- a/app/javascript/mastodon/features/compose/components/reply_indicator.js +++ b/app/javascript/mastodon/features/compose/components/reply_indicator.js @@ -12,8 +12,8 @@ const messages = defineMessages({ cancel: { id: 'reply_indicator.cancel', defaultMessage: 'Cancel' }, }); -@injectIntl -export default class ReplyIndicator extends ImmutablePureComponent { +export default @injectIntl +class ReplyIndicator extends ImmutablePureComponent { static contextTypes = { router: PropTypes.object, diff --git a/app/javascript/mastodon/features/compose/components/search.js b/app/javascript/mastodon/features/compose/components/search.js index 71c0a203f..d203d8780 100644 --- a/app/javascript/mastodon/features/compose/components/search.js +++ b/app/javascript/mastodon/features/compose/components/search.js @@ -43,8 +43,8 @@ class SearchPopout extends React.PureComponent { } -@injectIntl -export default class Search extends React.PureComponent { +export default @injectIntl +class Search extends React.PureComponent { static propTypes = { value: PropTypes.string.isRequired, diff --git a/app/javascript/mastodon/features/compose/components/search_results.js b/app/javascript/mastodon/features/compose/components/search_results.js index c351b84bb..f0ddf6d71 100644 --- a/app/javascript/mastodon/features/compose/components/search_results.js +++ b/app/javascript/mastodon/features/compose/components/search_results.js @@ -1,19 +1,56 @@ import React from 'react'; +import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import { FormattedMessage } from 'react-intl'; +import { FormattedMessage, defineMessages, injectIntl } from 'react-intl'; import AccountContainer from '../../../containers/account_container'; import StatusContainer from '../../../containers/status_container'; import ImmutablePureComponent from 'react-immutable-pure-component'; import Hashtag from '../../../components/hashtag'; -export default class SearchResults extends ImmutablePureComponent { +const messages = defineMessages({ + dismissSuggestion: { id: 'suggestions.dismiss', defaultMessage: 'Dismiss suggestion' }, +}); + +export default @injectIntl +class SearchResults extends ImmutablePureComponent { static propTypes = { results: ImmutablePropTypes.map.isRequired, + suggestions: ImmutablePropTypes.list.isRequired, + fetchSuggestions: PropTypes.func.isRequired, + dismissSuggestion: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, }; + componentDidMount () { + this.props.fetchSuggestions(); + } + render () { - const { results } = this.props; + const { intl, results, suggestions, dismissSuggestion } = this.props; + + if (results.isEmpty() && !suggestions.isEmpty()) { + return ( +
+
+
+ + +
+ + {suggestions && suggestions.map(accountId => ( + + ))} +
+
+ ); + } let accounts, statuses, hashtags; let count = 0; diff --git a/app/javascript/mastodon/features/compose/components/upload.js b/app/javascript/mastodon/features/compose/components/upload.js index 3d09217dc..a1e99dcbb 100644 --- a/app/javascript/mastodon/features/compose/components/upload.js +++ b/app/javascript/mastodon/features/compose/components/upload.js @@ -11,8 +11,12 @@ const messages = defineMessages({ description: { id: 'upload_form.description', defaultMessage: 'Describe for the visually impaired' }, }); -@injectIntl -export default class Upload extends ImmutablePureComponent { +export default @injectIntl +class Upload extends ImmutablePureComponent { + + static contextTypes = { + router: PropTypes.object, + }; static propTypes = { media: ImmutablePropTypes.map.isRequired, @@ -37,14 +41,16 @@ export default class Upload extends ImmutablePureComponent { handleSubmit = () => { this.handleInputBlur(); - this.props.onSubmit(); + this.props.onSubmit(this.context.router.history); } - handleUndoClick = () => { + handleUndoClick = e => { + e.stopPropagation(); this.props.onUndo(this.props.media.get('id')); } - handleFocalPointClick = () => { + handleFocalPointClick = e => { + e.stopPropagation(); this.props.onOpenFocalPoint(this.props.media.get('id')); } @@ -64,6 +70,10 @@ export default class Upload extends ImmutablePureComponent { this.setState({ focused: true }); } + handleClick = () => { + this.setState({ focused: true }); + } + handleInputBlur = () => { const { dirtyDescription } = this.state; @@ -84,7 +94,7 @@ export default class Upload extends ImmutablePureComponent { const y = ((focusY / -2) + .5) * 100; return ( -
+
{({ scale }) => (
diff --git a/app/javascript/mastodon/features/compose/components/upload_button.js b/app/javascript/mastodon/features/compose/components/upload_button.js index 8b9609138..b6fe770ea 100644 --- a/app/javascript/mastodon/features/compose/components/upload_button.js +++ b/app/javascript/mastodon/features/compose/components/upload_button.js @@ -23,9 +23,9 @@ const iconStyle = { lineHeight: '27px', }; -@connect(makeMapStateToProps) +export default @connect(makeMapStateToProps) @injectIntl -export default class UploadButton extends ImmutablePureComponent { +class UploadButton extends ImmutablePureComponent { static propTypes = { disabled: PropTypes.bool, diff --git a/app/javascript/mastodon/features/compose/containers/compose_form_container.js b/app/javascript/mastodon/features/compose/containers/compose_form_container.js index 3822dd711..5d7fb8852 100644 --- a/app/javascript/mastodon/features/compose/containers/compose_form_container.js +++ b/app/javascript/mastodon/features/compose/containers/compose_form_container.js @@ -33,8 +33,8 @@ const mapDispatchToProps = (dispatch) => ({ dispatch(changeCompose(text)); }, - onSubmit () { - dispatch(submitCompose()); + onSubmit (router) { + dispatch(submitCompose(router)); }, onClearSuggestions () { diff --git a/app/javascript/mastodon/features/compose/containers/search_results_container.js b/app/javascript/mastodon/features/compose/containers/search_results_container.js index 16d95d417..f9637861a 100644 --- a/app/javascript/mastodon/features/compose/containers/search_results_container.js +++ b/app/javascript/mastodon/features/compose/containers/search_results_container.js @@ -1,8 +1,15 @@ import { connect } from 'react-redux'; import SearchResults from '../components/search_results'; +import { fetchSuggestions, dismissSuggestion } from '../../../actions/suggestions'; const mapStateToProps = state => ({ results: state.getIn(['search', 'results']), + suggestions: state.getIn(['suggestions', 'items']), }); -export default connect(mapStateToProps)(SearchResults); +const mapDispatchToProps = dispatch => ({ + fetchSuggestions: () => dispatch(fetchSuggestions()), + dismissSuggestion: account => dispatch(dismissSuggestion(account.get('id'))), +}); + +export default connect(mapStateToProps, mapDispatchToProps)(SearchResults); diff --git a/app/javascript/mastodon/features/compose/containers/upload_container.js b/app/javascript/mastodon/features/compose/containers/upload_container.js index 9f3aab4bc..b6d81f03a 100644 --- a/app/javascript/mastodon/features/compose/containers/upload_container.js +++ b/app/javascript/mastodon/features/compose/containers/upload_container.js @@ -22,8 +22,8 @@ const mapDispatchToProps = dispatch => ({ dispatch(openModal('FOCAL_POINT', { id })); }, - onSubmit () { - dispatch(submitCompose()); + onSubmit (router) { + dispatch(submitCompose(router)); }, }); diff --git a/app/javascript/mastodon/features/compose/index.js b/app/javascript/mastodon/features/compose/index.js index b7394a39e..d76cd76e6 100644 --- a/app/javascript/mastodon/features/compose/index.js +++ b/app/javascript/mastodon/features/compose/index.js @@ -13,6 +13,7 @@ import spring from 'react-motion/lib/spring'; import SearchResultsContainer from './containers/search_results_container'; import { changeComposing } from '../../actions/compose'; import elephantUIPlane from '../../../images/elephant_ui_plane.svg'; +import { mascot } from '../../initial_state'; const messages = defineMessages({ 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, }); -@connect(mapStateToProps) +export default @connect(mapStateToProps) @injectIntl -export default class Compose extends React.PureComponent { +class Compose extends React.PureComponent { static propTypes = { dispatch: PropTypes.func.isRequired, @@ -107,7 +108,7 @@ export default class Compose extends React.PureComponent { {multiColumn && (
- +
)}
} diff --git a/app/javascript/mastodon/features/compose/util/url_regex.js b/app/javascript/mastodon/features/compose/util/url_regex.js index d5e39e0d5..7f1e17620 100644 --- a/app/javascript/mastodon/features/compose/util/url_regex.js +++ b/app/javascript/mastodon/features/compose/util/url_regex.js @@ -170,7 +170,7 @@ export const urlRegex = (function() { ')' + '\\)', '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 regexen.validUrlPathEndingChars = regexSupplant(/[^#{spaces_group}\(\)\?!\*';:=\,\.\$%\[\]#{pd}~&\|@]|(?:#{validUrlBalancedParens})/i); // Allow @ in a url, but only in the middle. Catch things like http://example.com/@user/ diff --git a/app/javascript/mastodon/features/direct_timeline/components/column_settings.js b/app/javascript/mastodon/features/direct_timeline/components/column_settings.js index a992b27bb..b629c128d 100644 --- a/app/javascript/mastodon/features/direct_timeline/components/column_settings.js +++ b/app/javascript/mastodon/features/direct_timeline/components/column_settings.js @@ -9,8 +9,8 @@ const messages = defineMessages({ settings: { id: 'home.settings', defaultMessage: 'Column settings' }, }); -@injectIntl -export default class ColumnSettings extends React.PureComponent { +export default @injectIntl +class ColumnSettings extends React.PureComponent { static propTypes = { settings: ImmutablePropTypes.map.isRequired, diff --git a/app/javascript/mastodon/features/direct_timeline/components/conversation.js b/app/javascript/mastodon/features/direct_timeline/components/conversation.js new file mode 100644 index 000000000..ffcd6d281 --- /dev/null +++ b/app/javascript/mastodon/features/direct_timeline/components/conversation.js @@ -0,0 +1,64 @@ +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 ( + + ); + } + +} diff --git a/app/javascript/mastodon/features/direct_timeline/components/conversations_list.js b/app/javascript/mastodon/features/direct_timeline/components/conversations_list.js new file mode 100644 index 000000000..635c03c1d --- /dev/null +++ b/app/javascript/mastodon/features/direct_timeline/components/conversations_list.js @@ -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 = { + conversations: ImmutablePropTypes.list.isRequired, + hasMore: PropTypes.bool, + isLoading: PropTypes.bool, + onLoadMore: PropTypes.func, + shouldUpdateScroll: PropTypes.func, + }; + + getCurrentIndex = id => this.props.conversations.findIndex(x => x.get('id') === 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.conversations.last(); + + if (last && last.get('last_status')) { + this.props.onLoadMore(last.get('last_status')); + } + }, 300, { leading: true }) + + render () { + const { conversations, onLoadMore, ...other } = this.props; + + return ( + + {conversations.map(item => ( + + ))} + + ); + } + +} diff --git a/app/javascript/mastodon/features/direct_timeline/containers/conversation_container.js b/app/javascript/mastodon/features/direct_timeline/containers/conversation_container.js new file mode 100644 index 000000000..bd6f6bfb0 --- /dev/null +++ b/app/javascript/mastodon/features/direct_timeline/containers/conversation_container.js @@ -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); diff --git a/app/javascript/mastodon/features/direct_timeline/containers/conversations_list_container.js b/app/javascript/mastodon/features/direct_timeline/containers/conversations_list_container.js new file mode 100644 index 000000000..57e17d96f --- /dev/null +++ b/app/javascript/mastodon/features/direct_timeline/containers/conversations_list_container.js @@ -0,0 +1,15 @@ +import { connect } from 'react-redux'; +import ConversationsList from '../components/conversations_list'; +import { expandConversations } from '../../../actions/conversations'; + +const mapStateToProps = state => ({ + conversations: state.getIn(['conversations', 'items']), + 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); diff --git a/app/javascript/mastodon/features/direct_timeline/index.js b/app/javascript/mastodon/features/direct_timeline/index.js index dd289ce56..d202f3bfd 100644 --- a/app/javascript/mastodon/features/direct_timeline/index.js +++ b/app/javascript/mastodon/features/direct_timeline/index.js @@ -1,25 +1,21 @@ import React from 'react'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; -import StatusListContainer from '../ui/containers/status_list_container'; import Column from '../../components/column'; 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 { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { connectDirectStream } from '../../actions/streaming'; +import ConversationsListContainer from './containers/conversations_list_container'; const messages = defineMessages({ title: { id: 'column.direct', defaultMessage: 'Direct messages' }, }); -const mapStateToProps = state => ({ - hasUnread: state.getIn(['timelines', 'direct', 'unread']) > 0, -}); - -@connect(mapStateToProps) +export default @connect() @injectIntl -export default class DirectTimeline extends React.PureComponent { +class DirectTimeline extends React.PureComponent { static propTypes = { dispatch: PropTypes.func.isRequired, @@ -52,11 +48,14 @@ export default class DirectTimeline extends React.PureComponent { componentDidMount () { const { dispatch } = this.props; - dispatch(expandDirectTimeline()); + dispatch(mountConversations()); + dispatch(expandConversations()); this.disconnect = dispatch(connectDirectStream()); } componentWillUnmount () { + this.props.dispatch(unmountConversations()); + if (this.disconnect) { this.disconnect(); this.disconnect = null; @@ -68,11 +67,11 @@ export default class DirectTimeline extends React.PureComponent { } handleLoadMore = maxId => { - this.props.dispatch(expandDirectTimeline({ maxId })); + this.props.dispatch(expandConversations({ maxId })); } render () { - const { intl, shouldUpdateScroll, hasUnread, columnId, multiColumn } = this.props; + const { intl, hasUnread, columnId, multiColumn, shouldUpdateScroll } = this.props; const pinned = !!columnId; return ( @@ -88,7 +87,7 @@ export default class DirectTimeline extends React.PureComponent { multiColumn={multiColumn} /> - ({ domains: state.getIn(['domain_lists', 'blocks', 'items']), }); -@connect(mapStateToProps) +export default @connect(mapStateToProps) @injectIntl -export default class Blocks extends ImmutablePureComponent { +class Blocks extends ImmutablePureComponent { static propTypes = { params: PropTypes.object.isRequired, diff --git a/app/javascript/mastodon/features/emoji/emoji_mart_search_light.js b/app/javascript/mastodon/features/emoji/emoji_mart_search_light.js index 36351ec02..164fdcc0b 100644 --- a/app/javascript/mastodon/features/emoji/emoji_mart_search_light.js +++ b/app/javascript/mastodon/features/emoji/emoji_mart_search_light.js @@ -2,7 +2,7 @@ // https://github.com/missive/emoji-mart/blob/5f2ffcc/src/utils/emoji-index.js import data from './emoji_mart_data_light'; -import { getData, getSanitizedData, intersect } from './emoji_utils'; +import { getData, getSanitizedData, uniq, intersect } from './emoji_utils'; let originalPool = {}; let index = {}; @@ -103,7 +103,7 @@ function search(value, { emojisToShowFilter, maxResults, include, exclude, custo } } - allResults = values.map((value) => { + const searchValue = (value) => { let aPool = pool, aIndex = index, length = 0; @@ -150,15 +150,23 @@ function search(value, { emojisToShowFilter, maxResults, include, exclude, custo } return aIndex.results; - }).filter(a => a); + }; - if (allResults.length > 1) { - results = intersect.apply(null, allResults); - } else if (allResults.length) { - results = allResults[0]; + if (values.length > 1) { + results = searchValue(value); } else { results = []; } + + allResults = values.map(searchValue).filter(a => a); + + if (allResults.length > 1) { + allResults = intersect.apply(null, allResults); + } else if (allResults.length) { + allResults = allResults[0]; + } + + results = uniq(results.concat(allResults)); } if (results) { diff --git a/app/javascript/mastodon/features/favourited_statuses/index.js b/app/javascript/mastodon/features/favourited_statuses/index.js index 499530cd9..fa9401b90 100644 --- a/app/javascript/mastodon/features/favourited_statuses/index.js +++ b/app/javascript/mastodon/features/favourited_statuses/index.js @@ -21,9 +21,9 @@ const mapStateToProps = state => ({ hasMore: !!state.getIn(['status_lists', 'favourites', 'next']), }); -@connect(mapStateToProps) +export default @connect(mapStateToProps) @injectIntl -export default class Favourites extends ImmutablePureComponent { +class Favourites extends ImmutablePureComponent { static propTypes = { dispatch: PropTypes.func.isRequired, diff --git a/app/javascript/mastodon/features/favourites/index.js b/app/javascript/mastodon/features/favourites/index.js index 74a683ccc..d1ac229a2 100644 --- a/app/javascript/mastodon/features/favourites/index.js +++ b/app/javascript/mastodon/features/favourites/index.js @@ -15,8 +15,8 @@ const mapStateToProps = (state, props) => ({ accountIds: state.getIn(['user_lists', 'favourited_by', props.params.statusId]), }); -@connect(mapStateToProps) -export default class Favourites extends ImmutablePureComponent { +export default @connect(mapStateToProps) +class Favourites extends ImmutablePureComponent { static propTypes = { params: PropTypes.object.isRequired, diff --git a/app/javascript/mastodon/features/follow_requests/components/account_authorize.js b/app/javascript/mastodon/features/follow_requests/components/account_authorize.js index 4fc5638d9..a3b524db1 100644 --- a/app/javascript/mastodon/features/follow_requests/components/account_authorize.js +++ b/app/javascript/mastodon/features/follow_requests/components/account_authorize.js @@ -13,8 +13,8 @@ const messages = defineMessages({ reject: { id: 'follow_request.reject', defaultMessage: 'Reject' }, }); -@injectIntl -export default class AccountAuthorize extends ImmutablePureComponent { +export default @injectIntl +class AccountAuthorize extends ImmutablePureComponent { static propTypes = { account: ImmutablePropTypes.map.isRequired, diff --git a/app/javascript/mastodon/features/follow_requests/index.js b/app/javascript/mastodon/features/follow_requests/index.js index cb574e08d..56ae8764b 100644 --- a/app/javascript/mastodon/features/follow_requests/index.js +++ b/app/javascript/mastodon/features/follow_requests/index.js @@ -20,9 +20,9 @@ const mapStateToProps = state => ({ accountIds: state.getIn(['user_lists', 'follow_requests', 'items']), }); -@connect(mapStateToProps) +export default @connect(mapStateToProps) @injectIntl -export default class FollowRequests extends ImmutablePureComponent { +class FollowRequests extends ImmutablePureComponent { static propTypes = { params: PropTypes.object.isRequired, diff --git a/app/javascript/mastodon/features/followers/index.js b/app/javascript/mastodon/features/followers/index.js index 97d59cc4a..b9ca7f3dd 100644 --- a/app/javascript/mastodon/features/followers/index.js +++ b/app/javascript/mastodon/features/followers/index.js @@ -22,8 +22,8 @@ const mapStateToProps = (state, props) => ({ hasMore: !!state.getIn(['user_lists', 'followers', props.params.accountId, 'next']), }); -@connect(mapStateToProps) -export default class Followers extends ImmutablePureComponent { +export default @connect(mapStateToProps) +class Followers extends ImmutablePureComponent { static propTypes = { params: PropTypes.object.isRequired, diff --git a/app/javascript/mastodon/features/following/index.js b/app/javascript/mastodon/features/following/index.js index 17bc7ec6e..b3e160240 100644 --- a/app/javascript/mastodon/features/following/index.js +++ b/app/javascript/mastodon/features/following/index.js @@ -22,8 +22,8 @@ const mapStateToProps = (state, props) => ({ hasMore: !!state.getIn(['user_lists', 'following', props.params.accountId, 'next']), }); -@connect(mapStateToProps) -export default class Following extends ImmutablePureComponent { +export default @connect(mapStateToProps) +class Following extends ImmutablePureComponent { static propTypes = { params: PropTypes.object.isRequired, diff --git a/app/javascript/mastodon/features/getting_started/index.js b/app/javascript/mastodon/features/getting_started/index.js index 97c726f62..0fd9badac 100644 --- a/app/javascript/mastodon/features/getting_started/index.js +++ b/app/javascript/mastodon/features/getting_started/index.js @@ -53,9 +53,9 @@ const badgeDisplay = (number, limit) => { } }; -@connect(mapStateToProps, mapDispatchToProps) +export default @connect(mapStateToProps, mapDispatchToProps) @injectIntl -export default class GettingStarted extends ImmutablePureComponent { +class GettingStarted extends ImmutablePureComponent { static propTypes = { intl: PropTypes.object.isRequired, @@ -144,7 +144,7 @@ export default class GettingStarted extends ImmutablePureComponent {
  • ·
  • ·
  • ·
  • -
  • ·
  • +
  • ·
  • diff --git a/app/javascript/mastodon/features/hashtag_timeline/index.js b/app/javascript/mastodon/features/hashtag_timeline/index.js index b67486f07..63efdf1bd 100644 --- a/app/javascript/mastodon/features/hashtag_timeline/index.js +++ b/app/javascript/mastodon/features/hashtag_timeline/index.js @@ -13,8 +13,8 @@ const mapStateToProps = (state, props) => ({ hasUnread: state.getIn(['timelines', `hashtag:${props.params.id}`, 'unread']) > 0, }); -@connect(mapStateToProps) -export default class HashtagTimeline extends React.PureComponent { +export default @connect(mapStateToProps) +class HashtagTimeline extends React.PureComponent { static propTypes = { params: PropTypes.object.isRequired, diff --git a/app/javascript/mastodon/features/home_timeline/components/column_settings.js b/app/javascript/mastodon/features/home_timeline/components/column_settings.js index 932ac2049..455e21881 100644 --- a/app/javascript/mastodon/features/home_timeline/components/column_settings.js +++ b/app/javascript/mastodon/features/home_timeline/components/column_settings.js @@ -4,8 +4,8 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import { injectIntl, FormattedMessage } from 'react-intl'; import SettingToggle from '../../notifications/components/setting_toggle'; -@injectIntl -export default class ColumnSettings extends React.PureComponent { +export default @injectIntl +class ColumnSettings extends React.PureComponent { static propTypes = { settings: ImmutablePropTypes.map.isRequired, diff --git a/app/javascript/mastodon/features/home_timeline/index.js b/app/javascript/mastodon/features/home_timeline/index.js index 12dab0e44..3ffa7a681 100644 --- a/app/javascript/mastodon/features/home_timeline/index.js +++ b/app/javascript/mastodon/features/home_timeline/index.js @@ -19,9 +19,9 @@ const mapStateToProps = state => ({ isPartial: state.getIn(['timelines', 'home', 'items', 0], null) === null, }); -@connect(mapStateToProps) +export default @connect(mapStateToProps) @injectIntl -export default class HomeTimeline extends React.PureComponent { +class HomeTimeline extends React.PureComponent { static propTypes = { dispatch: PropTypes.func.isRequired, diff --git a/app/javascript/mastodon/features/keyboard_shortcuts/index.js b/app/javascript/mastodon/features/keyboard_shortcuts/index.js index 2d86953c6..ab1ac511e 100644 --- a/app/javascript/mastodon/features/keyboard_shortcuts/index.js +++ b/app/javascript/mastodon/features/keyboard_shortcuts/index.js @@ -9,8 +9,8 @@ const messages = defineMessages({ heading: { id: 'keyboard_shortcuts.heading', defaultMessage: 'Keyboard Shortcuts' }, }); -@injectIntl -export default class KeyboardShortcuts extends ImmutablePureComponent { +export default @injectIntl +class KeyboardShortcuts extends ImmutablePureComponent { static propTypes = { intl: PropTypes.object.isRequired, diff --git a/app/javascript/mastodon/features/list_editor/components/account.js b/app/javascript/mastodon/features/list_editor/components/account.js index c78c58e24..48085af43 100644 --- a/app/javascript/mastodon/features/list_editor/components/account.js +++ b/app/javascript/mastodon/features/list_editor/components/account.js @@ -31,9 +31,9 @@ const mapDispatchToProps = (dispatch, { accountId }) => ({ onAdd: () => dispatch(addToListEditor(accountId)), }); -@connect(makeMapStateToProps, mapDispatchToProps) +export default @connect(makeMapStateToProps, mapDispatchToProps) @injectIntl -export default class Account extends ImmutablePureComponent { +class Account extends ImmutablePureComponent { static propTypes = { account: ImmutablePropTypes.map.isRequired, diff --git a/app/javascript/mastodon/features/list_editor/components/search.js b/app/javascript/mastodon/features/list_editor/components/search.js index 45c4d0f2e..f7617fe58 100644 --- a/app/javascript/mastodon/features/list_editor/components/search.js +++ b/app/javascript/mastodon/features/list_editor/components/search.js @@ -19,9 +19,9 @@ const mapDispatchToProps = dispatch => ({ onChange: value => dispatch(changeListSuggestions(value)), }); -@connect(mapStateToProps, mapDispatchToProps) +export default @connect(mapStateToProps, mapDispatchToProps) @injectIntl -export default class Search extends React.PureComponent { +class Search extends React.PureComponent { static propTypes = { intl: PropTypes.object.isRequired, diff --git a/app/javascript/mastodon/features/list_editor/index.js b/app/javascript/mastodon/features/list_editor/index.js index 65f7420de..aab0cdd0c 100644 --- a/app/javascript/mastodon/features/list_editor/index.js +++ b/app/javascript/mastodon/features/list_editor/index.js @@ -22,9 +22,9 @@ const mapDispatchToProps = dispatch => ({ onReset: () => dispatch(resetListEditor()), }); -@connect(mapStateToProps, mapDispatchToProps) +export default @connect(mapStateToProps, mapDispatchToProps) @injectIntl -export default class ListEditor extends ImmutablePureComponent { +class ListEditor extends ImmutablePureComponent { static propTypes = { listId: PropTypes.string.isRequired, diff --git a/app/javascript/mastodon/features/list_timeline/index.js b/app/javascript/mastodon/features/list_timeline/index.js index a655451b1..5b047ace4 100644 --- a/app/javascript/mastodon/features/list_timeline/index.js +++ b/app/javascript/mastodon/features/list_timeline/index.js @@ -25,9 +25,9 @@ const mapStateToProps = (state, props) => ({ hasUnread: state.getIn(['timelines', `list:${props.params.id}`, 'unread']) > 0, }); -@connect(mapStateToProps) +export default @connect(mapStateToProps) @injectIntl -export default class ListTimeline extends React.PureComponent { +class ListTimeline extends React.PureComponent { static contextTypes = { router: PropTypes.object, diff --git a/app/javascript/mastodon/features/lists/components/new_list_form.js b/app/javascript/mastodon/features/lists/components/new_list_form.js index eed6efc25..739246640 100644 --- a/app/javascript/mastodon/features/lists/components/new_list_form.js +++ b/app/javascript/mastodon/features/lists/components/new_list_form.js @@ -20,9 +20,9 @@ const mapDispatchToProps = dispatch => ({ onSubmit: () => dispatch(submitListEditor(true)), }); -@connect(mapStateToProps, mapDispatchToProps) +export default @connect(mapStateToProps, mapDispatchToProps) @injectIntl -export default class NewListForm extends React.PureComponent { +class NewListForm extends React.PureComponent { static propTypes = { value: PropTypes.string.isRequired, diff --git a/app/javascript/mastodon/features/lists/index.js b/app/javascript/mastodon/features/lists/index.js index 127347730..24f40c16b 100644 --- a/app/javascript/mastodon/features/lists/index.js +++ b/app/javascript/mastodon/features/lists/index.js @@ -31,9 +31,9 @@ const mapStateToProps = state => ({ lists: getOrderedLists(state), }); -@connect(mapStateToProps) +export default @connect(mapStateToProps) @injectIntl -export default class Lists extends ImmutablePureComponent { +class Lists extends ImmutablePureComponent { static propTypes = { params: PropTypes.object.isRequired, diff --git a/app/javascript/mastodon/features/mutes/index.js b/app/javascript/mastodon/features/mutes/index.js index 7bf9c1464..f979ef72f 100644 --- a/app/javascript/mastodon/features/mutes/index.js +++ b/app/javascript/mastodon/features/mutes/index.js @@ -20,9 +20,9 @@ const mapStateToProps = state => ({ accountIds: state.getIn(['user_lists', 'mutes', 'items']), }); -@connect(mapStateToProps) +export default @connect(mapStateToProps) @injectIntl -export default class Mutes extends ImmutablePureComponent { +class Mutes extends ImmutablePureComponent { static propTypes = { params: PropTypes.object.isRequired, diff --git a/app/javascript/mastodon/features/notifications/components/column_settings.js b/app/javascript/mastodon/features/notifications/components/column_settings.js index d9638aaf3..fcdf5c6e6 100644 --- a/app/javascript/mastodon/features/notifications/components/column_settings.js +++ b/app/javascript/mastodon/features/notifications/components/column_settings.js @@ -27,7 +27,6 @@ export default class ColumnSettings extends React.PureComponent { const showPushSettings = pushSettings.get('browserSupport') && pushSettings.get('isSubscribed'); const pushStr = showPushSettings && ; - const pushMeta = showPushSettings && ; return (
    @@ -40,7 +39,7 @@ export default class ColumnSettings extends React.PureComponent {
    - {showPushSettings && } + {showPushSettings && }
    @@ -51,7 +50,7 @@ export default class ColumnSettings extends React.PureComponent {
    - {showPushSettings && } + {showPushSettings && }
    @@ -62,7 +61,7 @@ export default class ColumnSettings extends React.PureComponent {
    - {showPushSettings && } + {showPushSettings && }
    @@ -73,7 +72,7 @@ export default class ColumnSettings extends React.PureComponent {
    - {showPushSettings && } + {showPushSettings && }
    diff --git a/app/javascript/mastodon/features/notifications/components/notification.js b/app/javascript/mastodon/features/notifications/components/notification.js index 07fec84b2..8df6830c5 100644 --- a/app/javascript/mastodon/features/notifications/components/notification.js +++ b/app/javascript/mastodon/features/notifications/components/notification.js @@ -16,8 +16,8 @@ const notificationForScreenReader = (intl, message, timestamp) => { return output.join(', '); }; -@injectIntl -export default class Notification extends ImmutablePureComponent { +export default @injectIntl +class Notification extends ImmutablePureComponent { static contextTypes = { router: PropTypes.object, @@ -85,8 +85,9 @@ export default class Notification extends ImmutablePureComponent {
    - - + + +