Browse Source

Merge tag 'v2.8.0' of github.com:tootsuite/mastodon

master
Matt Baer 5 years ago
parent
commit
be4037e4a9
100 changed files with 2081 additions and 633 deletions
  1. +5
    -0
      .eslintrc.js
  2. +1
    -1
      .rubocop.yml
  3. +1
    -1
      .ruby-version
  4. +218
    -105
      AUTHORS.md
  5. +169
    -0
      CHANGELOG.md
  6. +3
    -1
      CONTRIBUTING.md
  7. +123
    -85
      Dockerfile
  8. +15
    -15
      Gemfile
  9. +127
    -126
      Gemfile.lock
  10. +3
    -3
      README.md
  11. +12
    -1
      Vagrantfile
  12. +3
    -3
      app/chewy/statuses_index.rb
  13. +9
    -21
      app/controllers/about_controller.rb
  14. +28
    -6
      app/controllers/accounts_controller.rb
  15. +11
    -5
      app/controllers/activitypub/collections_controller.rb
  16. +16
    -3
      app/controllers/activitypub/inboxes_controller.rb
  17. +6
    -0
      app/controllers/activitypub/outboxes_controller.rb
  18. +15
    -2
      app/controllers/admin/accounts_controller.rb
  19. +3
    -0
      app/controllers/admin/custom_emojis_controller.rb
  20. +2
    -1
      app/controllers/admin/dashboard_controller.rb
  21. +1
    -1
      app/controllers/admin/instances_controller.rb
  22. +52
    -0
      app/controllers/admin/pending_accounts_controller.rb
  23. +4
    -0
      app/controllers/admin/reported_statuses_controller.rb
  24. +9
    -65
      app/controllers/admin/settings_controller.rb
  25. +3
    -1
      app/controllers/api/base_controller.rb
  26. +30
    -0
      app/controllers/api/proofs_controller.rb
  27. +5
    -1
      app/controllers/api/v1/accounts/follower_accounts_controller.rb
  28. +5
    -1
      app/controllers/api/v1/accounts/following_accounts_controller.rb
  29. +19
    -0
      app/controllers/api/v1/accounts/identity_proofs_controller.rb
  30. +3
    -2
      app/controllers/api/v1/accounts/search_controller.rb
  31. +14
    -3
      app/controllers/api/v1/accounts/statuses_controller.rb
  32. +5
    -1
      app/controllers/api/v1/accounts_controller.rb
  33. +1
    -1
      app/controllers/api/v1/apps/credentials_controller.rb
  34. +29
    -0
      app/controllers/api/v1/polls/votes_controller.rb
  35. +13
    -0
      app/controllers/api/v1/polls_controller.rb
  36. +12
    -0
      app/controllers/api/v1/preferences_controller.rb
  37. +9
    -17
      app/controllers/api/v1/search_controller.rb
  38. +5
    -1
      app/controllers/api/v1/statuses/reblogs_controller.rb
  39. +16
    -2
      app/controllers/api/v1/statuses_controller.rb
  40. +1
    -1
      app/controllers/api/v1/timelines/tag_controller.rb
  41. +1
    -1
      app/controllers/api/v2/search_controller.rb
  42. +5
    -0
      app/controllers/application_controller.rb
  43. +10
    -5
      app/controllers/auth/registrations_controller.rb
  44. +17
    -3
      app/controllers/concerns/account_controller_concern.rb
  45. +1
    -1
      app/controllers/directories_controller.rb
  46. +9
    -0
      app/controllers/follower_accounts_controller.rb
  47. +9
    -0
      app/controllers/following_accounts_controller.rb
  48. +1
    -1
      app/controllers/home_controller.rb
  49. +5
    -0
      app/controllers/oauth/authorized_applications_controller.rb
  50. +34
    -0
      app/controllers/public_timelines_controller.rb
  51. +104
    -0
      app/controllers/relationships_controller.rb
  52. +16
    -2
      app/controllers/settings/exports_controller.rb
  53. +51
    -0
      app/controllers/settings/featured_tags_controller.rb
  54. +0
    -28
      app/controllers/settings/follower_domains_controller.rb
  55. +63
    -0
      app/controllers/settings/identity_proofs_controller.rb
  56. +2
    -1
      app/controllers/settings/preferences_controller.rb
  57. +1
    -1
      app/controllers/settings/profiles_controller.rb
  58. +1
    -0
      app/controllers/settings/sessions_controller.rb
  59. +1
    -1
      app/controllers/shares_controller.rb
  60. +56
    -1
      app/controllers/statuses_controller.rb
  61. +6
    -12
      app/controllers/tags_controller.rb
  62. +9
    -0
      app/controllers/well_known/keybase_proof_config_controller.rb
  63. +36
    -36
      app/helpers/admin/action_logs_helper.rb
  64. +10
    -0
      app/helpers/admin/dashboard_helper.rb
  65. +4
    -3
      app/helpers/admin/filter_helper.rb
  66. +22
    -1
      app/helpers/application_helper.rb
  67. +18
    -0
      app/helpers/home_helper.rb
  68. +25
    -1
      app/helpers/jsonld_helper.rb
  69. +12
    -4
      app/helpers/settings_helper.rb
  70. +13
    -3
      app/helpers/stream_entries_helper.rb
  71. +1
    -0
      app/javascript/images/logo_transparent_black.svg
  72. BIN
      app/javascript/images/proof_providers/keybase.png
  73. +7
    -2
      app/javascript/mastodon/actions/alerts.js
  74. +87
    -10
      app/javascript/mastodon/actions/compose.js
  75. +5
    -2
      app/javascript/mastodon/actions/conversations.js
  76. +30
    -0
      app/javascript/mastodon/actions/identity_proofs.js
  77. +20
    -7
      app/javascript/mastodon/actions/importer/index.js
  78. +17
    -0
      app/javascript/mastodon/actions/importer/normalizer.js
  79. +3
    -1
      app/javascript/mastodon/actions/notifications.js
  80. +60
    -0
      app/javascript/mastodon/actions/polls.js
  81. +1
    -0
      app/javascript/mastodon/actions/search.js
  82. +5
    -1
      app/javascript/mastodon/actions/statuses.js
  83. +6
    -0
      app/javascript/mastodon/actions/streaming.js
  84. +8
    -0
      app/javascript/mastodon/actions/timelines.js
  85. +6
    -2
      app/javascript/mastodon/api.js
  86. +1
    -1
      app/javascript/mastodon/components/account.js
  87. +3
    -2
      app/javascript/mastodon/components/attachment_list.js
  88. +2
    -1
      app/javascript/mastodon/components/column_back_button.js
  89. +2
    -1
      app/javascript/mastodon/components/column_back_button_slim.js
  90. +8
    -7
      app/javascript/mastodon/components/column_header.js
  91. +16
    -6
      app/javascript/mastodon/components/display_name.js
  92. +1
    -1
      app/javascript/mastodon/components/domain.js
  93. +39
    -0
      app/javascript/mastodon/components/error_boundary.js
  94. +21
    -0
      app/javascript/mastodon/components/icon.js
  95. +5
    -2
      app/javascript/mastodon/components/icon_button.js
  96. +1
    -1
      app/javascript/mastodon/components/intersection_observer_article.js
  97. +2
    -1
      app/javascript/mastodon/components/load_gap.js
  98. +8
    -2
      app/javascript/mastodon/components/media_gallery.js
  99. +140
    -0
      app/javascript/mastodon/components/poll.js
  100. +28
    -2
      app/javascript/mastodon/components/relative_timestamp.js

+ 5
- 0
.eslintrc.js View File

@@ -41,6 +41,11 @@ module.exports = {
'node_modules', 'node_modules',
'\\.(css|scss|json)$', '\\.(css|scss|json)$',
], ],
'import/resolver': {
node: {
paths: ['app/javascript'],
},
},
}, },


rules: { rules: {


+ 1
- 1
.rubocop.yml View File

@@ -80,7 +80,7 @@ Rails/HttpStatus:
Rails/Exit: Rails/Exit:
Exclude: Exclude:
- 'lib/mastodon/*' - 'lib/mastodon/*'
- 'lib/cli'
- 'lib/cli.rb'


Style/ClassAndModuleChildren: Style/ClassAndModuleChildren:
Enabled: false Enabled: false


+ 1
- 1
.ruby-version View File

@@ -1 +1 @@
2.6.0
2.6.1

+ 218
- 105
AUTHORS.md View File

@@ -6,35 +6,35 @@ and provided thanks to the work of the following contributors:


* [Gargron](https://github.com/Gargron) * [Gargron](https://github.com/Gargron)
* [ykzts](https://github.com/ykzts) * [ykzts](https://github.com/ykzts)
* [akihikodaki](https://github.com/akihikodaki)
* [ThibG](https://github.com/ThibG) * [ThibG](https://github.com/ThibG)
* [akihikodaki](https://github.com/akihikodaki)
* [mjankowski](https://github.com/mjankowski) * [mjankowski](https://github.com/mjankowski)
* [dependabot[bot]](https://github.com/apps/dependabot)
* [unarist](https://github.com/unarist) * [unarist](https://github.com/unarist)
* [m4sk1n](https://github.com/m4sk1n) * [m4sk1n](https://github.com/m4sk1n)
* [dependabot[bot]](https://github.com/apps/dependabot)
* [yiskah](https://github.com/yiskah) * [yiskah](https://github.com/yiskah)
* [nolanlawson](https://github.com/nolanlawson) * [nolanlawson](https://github.com/nolanlawson)
* [sorin-davidoi](https://github.com/sorin-davidoi)
* [ysksn](https://github.com/ysksn) * [ysksn](https://github.com/ysksn)
* [sorin-davidoi](https://github.com/sorin-davidoi)
* [abcang](https://github.com/abcang) * [abcang](https://github.com/abcang)
* [lynlynlynx](https://github.com/lynlynlynx) * [lynlynlynx](https://github.com/lynlynlynx)
* [alpaca-tc](https://github.com/alpaca-tc)
* [mayaeh](https://github.com/mayaeh) * [mayaeh](https://github.com/mayaeh)
* [renatolond](https://github.com/renatolond) * [renatolond](https://github.com/renatolond)
* [alpaca-tc](https://github.com/alpaca-tc)
* [nclm](https://github.com/nclm) * [nclm](https://github.com/nclm)
* [ineffyble](https://github.com/ineffyble) * [ineffyble](https://github.com/ineffyble)
* [jeroenpraat](https://github.com/jeroenpraat) * [jeroenpraat](https://github.com/jeroenpraat)
* [blackle](https://github.com/blackle) * [blackle](https://github.com/blackle)
* [Quent-in](https://github.com/Quent-in) * [Quent-in](https://github.com/Quent-in)
* [JantsoP](https://github.com/JantsoP) * [JantsoP](https://github.com/JantsoP)
* [Kjwon15](https://github.com/Kjwon15)
* [mabkenar](https://github.com/mabkenar) * [mabkenar](https://github.com/mabkenar)
* [nullkal](https://github.com/nullkal) * [nullkal](https://github.com/nullkal)
* [yookoala](https://github.com/yookoala) * [yookoala](https://github.com/yookoala)
* [Kjwon15](https://github.com/Kjwon15)
* [shuheiktgw](https://github.com/shuheiktgw) * [shuheiktgw](https://github.com/shuheiktgw)
* [ashfurrow](https://github.com/ashfurrow) * [ashfurrow](https://github.com/ashfurrow)
* [Quenty31](https://github.com/Quenty31)
* [zunda](https://github.com/zunda) * [zunda](https://github.com/zunda)
* [Quenty31](https://github.com/Quenty31)
* [eramdam](https://github.com/eramdam) * [eramdam](https://github.com/eramdam)
* [takayamaki](https://github.com/takayamaki) * [takayamaki](https://github.com/takayamaki)
* [masarakki](https://github.com/masarakki) * [masarakki](https://github.com/masarakki)
@@ -45,30 +45,33 @@ and provided thanks to the work of the following contributors:
* [stephenburgess8](https://github.com/stephenburgess8) * [stephenburgess8](https://github.com/stephenburgess8)
* [Wonderfall](https://github.com/Wonderfall) * [Wonderfall](https://github.com/Wonderfall)
* [matteoaquila](https://github.com/matteoaquila) * [matteoaquila](https://github.com/matteoaquila)
* [rkarabut](https://github.com/rkarabut)
* [yukimochi](https://github.com/yukimochi) * [yukimochi](https://github.com/yukimochi)
* [rkarabut](https://github.com/rkarabut)
* [Artoria2e5](https://github.com/Artoria2e5) * [Artoria2e5](https://github.com/Artoria2e5)
* [nightpool](https://github.com/nightpool)
* [marrus-sh](https://github.com/marrus-sh) * [marrus-sh](https://github.com/marrus-sh)
* [krainboltgreene](https://github.com/krainboltgreene) * [krainboltgreene](https://github.com/krainboltgreene)
* [patf](https://github.com/patf)
* [pfigel](https://github.com/pfigel)
* [Aldarone](https://github.com/Aldarone) * [Aldarone](https://github.com/Aldarone)
* [BoFFire](https://github.com/BoFFire) * [BoFFire](https://github.com/BoFFire)
* [clworld](https://github.com/clworld) * [clworld](https://github.com/clworld)
* [dracos](https://github.com/dracos) * [dracos](https://github.com/dracos)
* [SerCom_KC](mailto:sercom-kc@users.noreply.github.com) * [SerCom_KC](mailto:sercom-kc@users.noreply.github.com)
* [Sylvhem](https://github.com/Sylvhem) * [Sylvhem](https://github.com/Sylvhem)
* [nightpool](https://github.com/nightpool)
* [MasterGroosha](https://github.com/MasterGroosha) * [MasterGroosha](https://github.com/MasterGroosha)
* [JeanGauthier](https://github.com/JeanGauthier) * [JeanGauthier](https://github.com/JeanGauthier)
* [kschaper](https://github.com/kschaper) * [kschaper](https://github.com/kschaper)
* [MaciekBaron](https://github.com/MaciekBaron) * [MaciekBaron](https://github.com/MaciekBaron)
* [MitarashiDango](mailto:mitarashidango@users.noreply.github.com) * [MitarashiDango](mailto:mitarashidango@users.noreply.github.com)
* [beatrix-bitrot](https://github.com/beatrix-bitrot) * [beatrix-bitrot](https://github.com/beatrix-bitrot)
* [Aditoo17](https://github.com/Aditoo17)
* [adbelle](https://github.com/adbelle) * [adbelle](https://github.com/adbelle)
* [evanminto](https://github.com/evanminto) * [evanminto](https://github.com/evanminto)
* [MightyPork](https://github.com/MightyPork) * [MightyPork](https://github.com/MightyPork)
* [yhirano55](https://github.com/yhirano55) * [yhirano55](https://github.com/yhirano55)
* [rinsuki](https://github.com/rinsuki)
* [camponez](https://github.com/camponez) * [camponez](https://github.com/camponez)
* [hinaloe](https://github.com/hinaloe)
* [SerCom-KC](https://github.com/SerCom-KC) * [SerCom-KC](https://github.com/SerCom-KC)
* [aschmitz](https://github.com/aschmitz) * [aschmitz](https://github.com/aschmitz)
* [devkral](https://github.com/devkral) * [devkral](https://github.com/devkral)
@@ -77,6 +80,7 @@ and provided thanks to the work of the following contributors:
* [johnsudaar](https://github.com/johnsudaar) * [johnsudaar](https://github.com/johnsudaar)
* [trebmuh](https://github.com/trebmuh) * [trebmuh](https://github.com/trebmuh)
* [Rakib Hasan](mailto:rmhasan@gmail.com) * [Rakib Hasan](mailto:rmhasan@gmail.com)
* [ashleyhull-versent](https://github.com/ashleyhull-versent)
* [lindwurm](https://github.com/lindwurm) * [lindwurm](https://github.com/lindwurm)
* [victorhck](mailto:victorhck@geeko.site) * [victorhck](mailto:victorhck@geeko.site)
* [voidsatisfaction](https://github.com/voidsatisfaction) * [voidsatisfaction](https://github.com/voidsatisfaction)
@@ -92,20 +96,21 @@ and provided thanks to the work of the following contributors:
* [dunn](https://github.com/dunn) * [dunn](https://github.com/dunn)
* [xqus](https://github.com/xqus) * [xqus](https://github.com/xqus)
* [hugogameiro](https://github.com/hugogameiro) * [hugogameiro](https://github.com/hugogameiro)
* [ariasuni](https://github.com/ariasuni)
* [pfm-eyesightjp](https://github.com/pfm-eyesightjp) * [pfm-eyesightjp](https://github.com/pfm-eyesightjp)
* [fakenine](https://github.com/fakenine) * [fakenine](https://github.com/fakenine)
* [tsuwatch](https://github.com/tsuwatch) * [tsuwatch](https://github.com/tsuwatch)
* [victorhck](https://github.com/victorhck) * [victorhck](https://github.com/victorhck)
* [ashleyhull-versent](https://github.com/ashleyhull-versent)
* [kedamaDQ](https://github.com/kedamaDQ) * [kedamaDQ](https://github.com/kedamaDQ)
* [puckipedia](https://github.com/puckipedia) * [puckipedia](https://github.com/puckipedia)
* [trwnh](https://github.com/trwnh)
* [fvh-P](https://github.com/fvh-P) * [fvh-P](https://github.com/fvh-P)
* [contraexemplo](https://github.com/contraexemplo)
* [Anna e só](mailto:contraexemplos@gmail.com)
* [BenLubar](https://github.com/BenLubar)
* [kazu9su](https://github.com/kazu9su) * [kazu9su](https://github.com/kazu9su)
* [Komic](https://github.com/Komic) * [Komic](https://github.com/Komic)
* [lmorchard](https://github.com/lmorchard) * [lmorchard](https://github.com/lmorchard)
* [diomed](https://github.com/diomed) * [diomed](https://github.com/diomed)
* [ariasuni](https://github.com/ariasuni)
* [Neetshin](mailto:neetshin@neetsh.in) * [Neetshin](mailto:neetshin@neetsh.in)
* [rainyday](https://github.com/rainyday) * [rainyday](https://github.com/rainyday)
* [ProgVal](https://github.com/ProgVal) * [ProgVal](https://github.com/ProgVal)
@@ -114,7 +119,7 @@ and provided thanks to the work of the following contributors:
* [goofy-bz](mailto:goofy@babelzilla.org) * [goofy-bz](mailto:goofy@babelzilla.org)
* [kadiix](https://github.com/kadiix) * [kadiix](https://github.com/kadiix)
* [kodacs](https://github.com/kodacs) * [kodacs](https://github.com/kodacs)
* [rtucker](https://github.com/rtucker)
* [JMendyk](https://github.com/JMendyk)
* [KScl](https://github.com/KScl) * [KScl](https://github.com/KScl)
* [sterdev](https://github.com/sterdev) * [sterdev](https://github.com/sterdev)
* [TheKinrar](https://github.com/TheKinrar) * [TheKinrar](https://github.com/TheKinrar)
@@ -125,16 +130,17 @@ and provided thanks to the work of the following contributors:
* [fhemberger](https://github.com/fhemberger) * [fhemberger](https://github.com/fhemberger)
* [greysteil](https://github.com/greysteil) * [greysteil](https://github.com/greysteil)
* [hensmith](https://github.com/hensmith) * [hensmith](https://github.com/hensmith)
* [hinaloe](https://github.com/hinaloe)
* [d6rkaiz](https://github.com/d6rkaiz) * [d6rkaiz](https://github.com/d6rkaiz)
* [Reverite](https://github.com/Reverite) * [Reverite](https://github.com/Reverite)
* [JMendyk](https://github.com/JMendyk)
* [JohnD28](https://github.com/JohnD28) * [JohnD28](https://github.com/JohnD28)
* [znz](https://github.com/znz) * [znz](https://github.com/znz)
* [marek-lach](https://github.com/marek-lach)
* [Naouak](https://github.com/Naouak) * [Naouak](https://github.com/Naouak)
* [pawelngei](https://github.com/pawelngei) * [pawelngei](https://github.com/pawelngei)
* [rtucker](https://github.com/rtucker)
* [reneklacan](https://github.com/reneklacan) * [reneklacan](https://github.com/reneklacan)
* [ekiru](https://github.com/ekiru) * [ekiru](https://github.com/ekiru)
* [noellabo](https://github.com/noellabo)
* [tcitworld](https://github.com/tcitworld) * [tcitworld](https://github.com/tcitworld)
* [geta6](https://github.com/geta6) * [geta6](https://github.com/geta6)
* [happycoloredbanana](https://github.com/happycoloredbanana) * [happycoloredbanana](https://github.com/happycoloredbanana)
@@ -144,9 +150,8 @@ and provided thanks to the work of the following contributors:
* [noraworld](https://github.com/noraworld) * [noraworld](https://github.com/noraworld)
* [theboss](https://github.com/theboss) * [theboss](https://github.com/theboss)
* [178inaba](https://github.com/178inaba) * [178inaba](https://github.com/178inaba)
* [Aditoo17](https://github.com/Aditoo17)
* [alyssais](https://github.com/alyssais) * [alyssais](https://github.com/alyssais)
* [kodnaplakal](https://github.com/kodnaplakal)
* [hiphref](https://github.com/hiphref)
* [stalker314314](https://github.com/stalker314314) * [stalker314314](https://github.com/stalker314314)
* [huertanix](https://github.com/huertanix) * [huertanix](https://github.com/huertanix)
* [genesixx](https://github.com/genesixx) * [genesixx](https://github.com/genesixx)
@@ -162,11 +167,11 @@ and provided thanks to the work of the following contributors:
* [pierreozoux](https://github.com/pierreozoux) * [pierreozoux](https://github.com/pierreozoux)
* [qguv](https://github.com/qguv) * [qguv](https://github.com/qguv)
* [Ram Lmn](mailto:ramlmn@users.noreply.github.com) * [Ram Lmn](mailto:ramlmn@users.noreply.github.com)
* [sascha-sl](https://github.com/sascha-sl)
* [harukasan](https://github.com/harukasan) * [harukasan](https://github.com/harukasan)
* [stamak](https://github.com/stamak) * [stamak](https://github.com/stamak)
* [noellabo](https://github.com/noellabo)
* [Technowix](mailto:technowix@users.noreply.github.com) * [Technowix](mailto:technowix@users.noreply.github.com)
* [Eychics](https://github.com/Eychics)
* [Zoeille](https://github.com/Zoeille)
* [Thor Harald Johansen](mailto:thj@thj.no) * [Thor Harald Johansen](mailto:thj@thj.no)
* [0x70b1a5](https://github.com/0x70b1a5) * [0x70b1a5](https://github.com/0x70b1a5)
* [gled-rs](https://github.com/gled-rs) * [gled-rs](https://github.com/gled-rs)
@@ -179,21 +184,20 @@ and provided thanks to the work of the following contributors:
* [hoodie](mailto:hoodiekitten@outlook.com) * [hoodie](mailto:hoodiekitten@outlook.com)
* [luzi82](https://github.com/luzi82) * [luzi82](https://github.com/luzi82)
* [duxovni](https://github.com/duxovni) * [duxovni](https://github.com/duxovni)
* [trwnh](https://github.com/trwnh)
* [tmm576](https://github.com/tmm576)
* [unsmell](https://github.com/unsmell) * [unsmell](https://github.com/unsmell)
* [valerauko](https://github.com/valerauko) * [valerauko](https://github.com/valerauko)
* [chriswmartin](https://github.com/chriswmartin) * [chriswmartin](https://github.com/chriswmartin)
* [vahnj](https://github.com/vahnj) * [vahnj](https://github.com/vahnj)
* [ikuradon](https://github.com/ikuradon) * [ikuradon](https://github.com/ikuradon)
* [AndreLewin](https://github.com/AndreLewin) * [AndreLewin](https://github.com/AndreLewin)
* [rinsuki](https://github.com/rinsuki)
* [0xflotus](https://github.com/0xflotus) * [0xflotus](https://github.com/0xflotus)
* [redtachyons](https://github.com/redtachyons) * [redtachyons](https://github.com/redtachyons)
* [thurloat](https://github.com/thurloat) * [thurloat](https://github.com/thurloat)
* [aaribaud](https://github.com/aaribaud) * [aaribaud](https://github.com/aaribaud)
* [pointlessone](https://github.com/pointlessone)
* [Andrew](mailto:andrewlchronister@gmail.com) * [Andrew](mailto:andrewlchronister@gmail.com)
* [estuans](https://github.com/estuans) * [estuans](https://github.com/estuans)
* [BenLubar](https://github.com/BenLubar)
* [dissolve](https://github.com/dissolve) * [dissolve](https://github.com/dissolve)
* [PurpleBooth](https://github.com/PurpleBooth) * [PurpleBooth](https://github.com/PurpleBooth)
* [bradurani](https://github.com/bradurani) * [bradurani](https://github.com/bradurani)
@@ -216,6 +220,7 @@ and provided thanks to the work of the following contributors:
* [ErikXXon](https://github.com/ErikXXon) * [ErikXXon](https://github.com/ErikXXon)
* [ian-kelling](https://github.com/ian-kelling) * [ian-kelling](https://github.com/ian-kelling)
* [immae](https://github.com/immae) * [immae](https://github.com/immae)
* [J0WI](https://github.com/J0WI)
* [foozmeat](https://github.com/foozmeat) * [foozmeat](https://github.com/foozmeat)
* [jasonrhodes](https://github.com/jasonrhodes) * [jasonrhodes](https://github.com/jasonrhodes)
* [Jason Snell](mailto:jason@newrelic.com) * [Jason Snell](mailto:jason@newrelic.com)
@@ -230,6 +235,7 @@ and provided thanks to the work of the following contributors:
* [Lorenz Diener](mailto:halcyon@icosahedron.website) * [Lorenz Diener](mailto:halcyon@icosahedron.website)
* [alimony](https://github.com/alimony) * [alimony](https://github.com/alimony)
* [mig5](https://github.com/mig5) * [mig5](https://github.com/mig5)
* [moritzheiber](https://github.com/moritzheiber)
* [ndarville](https://github.com/ndarville) * [ndarville](https://github.com/ndarville)
* [Abzol](https://github.com/Abzol) * [Abzol](https://github.com/Abzol)
* [pwoolcoc](https://github.com/pwoolcoc) * [pwoolcoc](https://github.com/pwoolcoc)
@@ -238,9 +244,10 @@ and provided thanks to the work of the following contributors:
* [ignisf](https://github.com/ignisf) * [ignisf](https://github.com/ignisf)
* [raymestalez](https://github.com/raymestalez) * [raymestalez](https://github.com/raymestalez)
* [remram44](https://github.com/remram44) * [remram44](https://github.com/remram44)
* [sascha-sl](https://github.com/sascha-sl)
* [sts10](https://github.com/sts10)
* [u1-liquid](https://github.com/u1-liquid) * [u1-liquid](https://github.com/u1-liquid)
* [sim6](https://github.com/sim6) * [sim6](https://github.com/sim6)
* [Sir-Boops](https://github.com/Sir-Boops)
* [stemid](https://github.com/stemid) * [stemid](https://github.com/stemid)
* [sumdog](https://github.com/sumdog) * [sumdog](https://github.com/sumdog)
* [ThomasLeister](https://github.com/ThomasLeister) * [ThomasLeister](https://github.com/ThomasLeister)
@@ -288,6 +295,7 @@ and provided thanks to the work of the following contributors:
* [857b](https://github.com/857b) * [857b](https://github.com/857b)
* [insom](https://github.com/insom) * [insom](https://github.com/insom)
* [tachyons](https://github.com/tachyons) * [tachyons](https://github.com/tachyons)
* [acid-chicken](https://github.com/acid-chicken)
* [Esteth](https://github.com/Esteth) * [Esteth](https://github.com/Esteth)
* [unascribed](https://github.com/unascribed) * [unascribed](https://github.com/unascribed)
* [Aguay-val](https://github.com/Aguay-val) * [Aguay-val](https://github.com/Aguay-val)
@@ -297,7 +305,6 @@ and provided thanks to the work of the following contributors:
* [unleashed](https://github.com/unleashed) * [unleashed](https://github.com/unleashed)
* [alxrcs](https://github.com/alxrcs) * [alxrcs](https://github.com/alxrcs)
* [console-cowboy](https://github.com/console-cowboy) * [console-cowboy](https://github.com/console-cowboy)
* [pointlessone](https://github.com/pointlessone)
* [Alkarex](https://github.com/Alkarex) * [Alkarex](https://github.com/Alkarex)
* [a2](https://github.com/a2) * [a2](https://github.com/a2)
* [0xa](https://github.com/0xa) * [0xa](https://github.com/0xa)
@@ -310,8 +317,11 @@ and provided thanks to the work of the following contributors:
* [Andreas Drop](mailto:andy@remline.de) * [Andreas Drop](mailto:andy@remline.de)
* [andi1984](https://github.com/andi1984) * [andi1984](https://github.com/andi1984)
* [schas002](https://github.com/schas002) * [schas002](https://github.com/schas002)
* [contraexemplo](https://github.com/contraexemplo)
* [abackstrom](https://github.com/abackstrom) * [abackstrom](https://github.com/abackstrom)
* [armandfardeau](https://github.com/armandfardeau)
* [jumbosushi](https://github.com/jumbosushi) * [jumbosushi](https://github.com/jumbosushi)
* [aurelien-reeves](https://github.com/aurelien-reeves)
* [ayumin](https://github.com/ayumin) * [ayumin](https://github.com/ayumin)
* [BaptisteGelez](https://github.com/BaptisteGelez) * [BaptisteGelez](https://github.com/BaptisteGelez)
* [bzg](https://github.com/bzg) * [bzg](https://github.com/bzg)
@@ -329,6 +339,7 @@ and provided thanks to the work of the following contributors:
* [Motoma](https://github.com/Motoma) * [Motoma](https://github.com/Motoma)
* [chriswk](https://github.com/chriswk) * [chriswk](https://github.com/chriswk)
* [csu](https://github.com/csu) * [csu](https://github.com/csu)
* [clarfon](https://github.com/clarfon)
* [kklleemm](https://github.com/kklleemm) * [kklleemm](https://github.com/kklleemm)
* [colindean](https://github.com/colindean) * [colindean](https://github.com/colindean)
* [dachinat](https://github.com/dachinat) * [dachinat](https://github.com/dachinat)
@@ -351,11 +362,13 @@ and provided thanks to the work of the following contributors:
* [eai04191](https://github.com/eai04191) * [eai04191](https://github.com/eai04191)
* [d3vgru](https://github.com/d3vgru) * [d3vgru](https://github.com/d3vgru)
* [Elizafox](https://github.com/Elizafox) * [Elizafox](https://github.com/Elizafox)
* [enewhuis](https://github.com/enewhuis)
* [ericblade](https://github.com/ericblade) * [ericblade](https://github.com/ericblade)
* [mikoim](https://github.com/mikoim) * [mikoim](https://github.com/mikoim)
* [espenronnevik](https://github.com/espenronnevik) * [espenronnevik](https://github.com/espenronnevik)
* [Finariel](https://github.com/Finariel) * [Finariel](https://github.com/Finariel)
* [siuying](https://github.com/siuying) * [siuying](https://github.com/siuying)
* [zoc](https://github.com/zoc)
* [fwenzel](https://github.com/fwenzel) * [fwenzel](https://github.com/fwenzel)
* [GenbuHase](https://github.com/GenbuHase) * [GenbuHase](https://github.com/GenbuHase)
* [hattori6789](https://github.com/hattori6789) * [hattori6789](https://github.com/hattori6789)
@@ -416,6 +429,7 @@ and provided thanks to the work of the following contributors:
* [martymcguire](https://github.com/martymcguire) * [martymcguire](https://github.com/martymcguire)
* [marvinkopf](https://github.com/marvinkopf) * [marvinkopf](https://github.com/marvinkopf)
* [otsune](https://github.com/otsune) * [otsune](https://github.com/otsune)
* [mbugowski](https://github.com/mbugowski)
* [Mathias B](mailto:10813340+mathias-b@users.noreply.github.com) * [Mathias B](mailto:10813340+mathias-b@users.noreply.github.com)
* [matt-auckland](https://github.com/matt-auckland) * [matt-auckland](https://github.com/matt-auckland)
* [webroo](https://github.com/webroo) * [webroo](https://github.com/webroo)
@@ -434,10 +448,10 @@ and provided thanks to the work of the following contributors:
* [premist](https://github.com/premist) * [premist](https://github.com/premist)
* [Mnkai](https://github.com/Mnkai) * [Mnkai](https://github.com/Mnkai)
* [mitchhentges](https://github.com/mitchhentges) * [mitchhentges](https://github.com/mitchhentges)
* [moritzheiber](https://github.com/moritzheiber)
* [mouse-reeve](https://github.com/mouse-reeve) * [mouse-reeve](https://github.com/mouse-reeve)
* [Mozinet-fr](https://github.com/Mozinet-fr) * [Mozinet-fr](https://github.com/Mozinet-fr)
* [lae](https://github.com/lae) * [lae](https://github.com/lae)
* [nosada](https://github.com/nosada)
* [Nanamachi](https://github.com/Nanamachi) * [Nanamachi](https://github.com/Nanamachi)
* [orinthe](https://github.com/orinthe) * [orinthe](https://github.com/orinthe)
* [NecroTechno](https://github.com/NecroTechno) * [NecroTechno](https://github.com/NecroTechno)
@@ -454,21 +468,22 @@ and provided thanks to the work of the following contributors:
* [noppa](https://github.com/noppa) * [noppa](https://github.com/noppa)
* [Otakan951](https://github.com/Otakan951) * [Otakan951](https://github.com/Otakan951)
* [fahy](https://github.com/fahy) * [fahy](https://github.com/fahy)
* [PatrickRWells](https://github.com/PatrickRWells)
* [Pangoraw](https://github.com/Pangoraw)
* [peterkeen](https://github.com/peterkeen)
* [pgate](https://github.com/pgate)
* [retokromer](https://github.com/retokromer)
* [rfwatson](https://github.com/rfwatson)
* [rfreebern](https://github.com/rfreebern)
* [PatrickRWells](mailto:32802366+patrickrwells@users.noreply.github.com)
* [Paul](mailto:naydex.mc+github@gmail.com)
* [Pete Keen](mailto:pete@petekeen.net)
* [Pierre-Morgan Gate](mailto:pgate@users.noreply.github.com)
* [Ratmir Karabut](mailto:rkarabut@sfmodern.ru)
* [Reto Kromer](mailto:retokromer@users.noreply.github.com)
* [Rey Tucker](mailto:git@reytucker.us)
* [Rob Watson](mailto:rfwatson@users.noreply.github.com)
* [Ryan Freebern](mailto:ryan@freebern.org)
* [Ryan Wade](mailto:ryan.wade@protonmail.com) * [Ryan Wade](mailto:ryan.wade@protonmail.com)
* [sylph01](https://github.com/sylph01)
* [S-H-GAMELINKS](https://github.com/S-H-GAMELINKS)
* [staticsafe](https://github.com/staticsafe)
* [snwh](https://github.com/snwh)
* [sts10](https://github.com/sts10)
* [skoji](https://github.com/skoji)
* [ScienJus](https://github.com/ScienJus)
* [Ryo Kajiwara](mailto:kfe-fecn6.prussian@s01.info)
* [S.H](mailto:gamelinks007@gmail.com)
* [Sadiq Saif](mailto:staticsafe@users.noreply.github.com)
* [Sam Hewitt](mailto:hewittsamuel@gmail.com)
* [Satoshi KOJIMA](mailto:skoji@mac.com)
* [ScienJus](mailto:i@scienjus.com)
* [Scott Larkin](mailto:scott@codeclimate.com) * [Scott Larkin](mailto:scott@codeclimate.com)
* [Sebastian Hübner](mailto:imolein@users.noreply.github.com) * [Sebastian Hübner](mailto:imolein@users.noreply.github.com)
* [Sebastian Morr](mailto:sebastian@morr.cc) * [Sebastian Morr](mailto:sebastian@morr.cc)
@@ -480,9 +495,9 @@ and provided thanks to the work of the following contributors:
* [Sho Kusano](mailto:rosylilly@aduca.org) * [Sho Kusano](mailto:rosylilly@aduca.org)
* [Shouko Yu](mailto:imshouko@gmail.com) * [Shouko Yu](mailto:imshouko@gmail.com)
* [Sina Mashek](mailto:sina@mashek.xyz) * [Sina Mashek](mailto:sina@mashek.xyz)
* [Sir-Boops](mailto:admin@boops.me)
* [Soshi Kato](mailto:mail@sossii.com) * [Soshi Kato](mailto:mail@sossii.com)
* [Spanky](mailto:2788886+spankyworks@users.noreply.github.com) * [Spanky](mailto:2788886+spankyworks@users.noreply.github.com)
* [Stanislas](mailto:angristan@pm.me)
* [StefOfficiel](mailto:pichard.stephane@free.fr) * [StefOfficiel](mailto:pichard.stephane@free.fr)
* [Steven Tappert](mailto:admin@dark-it.net) * [Steven Tappert](mailto:admin@dark-it.net)
* [Svetlozar Todorov](mailto:svetlik@users.noreply.github.com) * [Svetlozar Todorov](mailto:svetlik@users.noreply.github.com)
@@ -532,6 +547,7 @@ and provided thanks to the work of the following contributors:
* [fsubal](mailto:fsubal@users.noreply.github.com) * [fsubal](mailto:fsubal@users.noreply.github.com)
* [fusshi-](mailto:dikky1218@users.noreply.github.com) * [fusshi-](mailto:dikky1218@users.noreply.github.com)
* [gentaro](mailto:gentaroooo@gmail.com) * [gentaro](mailto:gentaroooo@gmail.com)
* [gol-cha](mailto:info@mevo.xyz)
* [hakoai](mailto:hk--76@qa2.so-net.ne.jp) * [hakoai](mailto:hk--76@qa2.so-net.ne.jp)
* [haosbvnker](mailto:github@chaosbunker.com) * [haosbvnker](mailto:github@chaosbunker.com)
* [isati](mailto:phil@juchnowi.cz) * [isati](mailto:phil@juchnowi.cz)
@@ -545,16 +561,18 @@ and provided thanks to the work of the following contributors:
* [karlyeurl](mailto:karl.yeurl@gmail.com) * [karlyeurl](mailto:karl.yeurl@gmail.com)
* [kedama](mailto:32974885+kedamadq@users.noreply.github.com) * [kedama](mailto:32974885+kedamadq@users.noreply.github.com)
* [kodai](mailto:shirafuta.kodai@gmail.com) * [kodai](mailto:shirafuta.kodai@gmail.com)
* [koyu](mailto:me@koyu.space)
* [kuro5hin](mailto:rusty@kuro5hin.org) * [kuro5hin](mailto:rusty@kuro5hin.org)
* [luzpaz](mailto:luzpaz@users.noreply.github.com) * [luzpaz](mailto:luzpaz@users.noreply.github.com)
* [maxypy](mailto:maxime@mpigou.fr) * [maxypy](mailto:maxime@mpigou.fr)
* [mhe](mailto:mail@marcus-herrmann.com) * [mhe](mailto:mail@marcus-herrmann.com)
* [mike castleman](mailto:m@mlcastle.net)
* [mimikun](mailto:dzdzble_effort_311@outlook.jp) * [mimikun](mailto:dzdzble_effort_311@outlook.jp)
* [mohemohe](mailto:mohemohe@users.noreply.github.com)
* [mshrtkch](mailto:mshrtkch@users.noreply.github.com) * [mshrtkch](mailto:mshrtkch@users.noreply.github.com)
* [muan](mailto:muan@github.com) * [muan](mailto:muan@github.com)
* [namelessGonbai](mailto:43787036+namelessgonbai@users.noreply.github.com) * [namelessGonbai](mailto:43787036+namelessgonbai@users.noreply.github.com)
* [neetshin](mailto:neetshin@neetsh.in) * [neetshin](mailto:neetshin@neetsh.in)
* [nightpool](mailto:nightpool@users.noreply.github.com)
* [rch850](mailto:rich850@gmail.com) * [rch850](mailto:rich850@gmail.com)
* [roikale](mailto:roikale@users.noreply.github.com) * [roikale](mailto:roikale@users.noreply.github.com)
* [rysiekpl](mailto:rysiek@hackerspace.pl) * [rysiekpl](mailto:rysiek@hackerspace.pl)
@@ -589,243 +607,338 @@ This document is provided for informational purposes only. Since it is only upda


Following people have contributed to translation of Mastodon: Following people have contributed to translation of Mastodon:


- **Albanian**
- Besnik Bleta
- Aditoo
- **Arabic** - **Arabic**
- ButterflyOfFire - ButterflyOfFire
- Aditoo
- Amrz0
- **Asturian** - **Asturian**
- ButterflyOfFire - ButterflyOfFire
- Enol P. - Enol P.
- Aditoo
- **Basque** - **Basque**
- Osoitz
- Aditoo
- Aitzol - Aitzol
- ButterflyOfFire - ButterflyOfFire
- Gorka Azkarate
- Osoitz
- Peru Iparragirre - Peru Iparragirre
- Gorka Azkarate
- **Bengali**
- dxwc
- **Bulgarian** - **Bulgarian**
- ButterflyOfFire - ButterflyOfFire
- Aditoo
- **Catalan** - **Catalan**
- spla
- Aditoo
- ButterflyOfFire - ButterflyOfFire
- Joan Montané - Joan Montané
- Jose Luis - Jose Luis
- spla
- **Chinese (Hong Kong)** - **Chinese (Hong Kong)**
- ButterflyOfFire - ButterflyOfFire
- Luzi Leung - Luzi Leung
- Aditoo
- **Chinese (Simplified)** - **Chinese (Simplified)**
- Allen Zhong - Allen Zhong
- ButterflyOfFire - ButterflyOfFire
- SerCom_KC - SerCom_KC
- martialarts
- Kaitian Xie
- Aditoo
- pan93412
- **Chinese (Traditional)** - **Chinese (Traditional)**
- Aditoo
- ButterflyOfFire - ButterflyOfFire
- James58899 - James58899
- Jeff Huang
- pan93412
- S1ttidoe477 - S1ttidoe477
- SHA265 - SHA265
- Jeff Huang
- **Corsican** - **Corsican**
- Alix D. R. - Alix D. R.
- Aditoo
- ButterflyOfFire - ButterflyOfFire
- **Croatian** - **Croatian**
- ButterflyOfFire - ButterflyOfFire
- Aditoo
- **Czech** - **Czech**
- ButterflyOfFire
- Lorem Ipsum
- Aditoo
- Marek Ľach - Marek Ľach
- **Danish**
- ButterflyOfFire - ButterflyOfFire
- **Danish**
- Einhjeriar
- Rasmus Sæderup - Rasmus Sæderup
- Aditoo
- ButterflyOfFire
- **Dutch** - **Dutch**
- Albakham
- ButterflyOfFire - ButterflyOfFire
- Jelv
- jeroenpraat - jeroenpraat
- rscmbbng - rscmbbng
- Aditoo
- Jelv
- **English** - **English**
- ButterflyOfFire - ButterflyOfFire
- Renato "Lond" Cerqueira - Renato "Lond" Cerqueira
- **English (United Kingdom)**
- Albakham
- **Esperanto** - **Esperanto**
- Aditoo
- ButterflyOfFire - ButterflyOfFire
- Becci Cat
- Jeong Arm - Jeong Arm
- Martin Bodin
- Mélanie Chauvel - Mélanie Chauvel
- Vanege - Vanege
- Martin Bodin
- tuxayo/Victor Grousset - tuxayo/Victor Grousset
- **Finnish** - **Finnish**
- ButterflyOfFire - ButterflyOfFire
- Jonne Arjoranta
- S Heija
- Mikko Poussu
- Taru Luojola - Taru Luojola
- S Heija
- Aditoo
- Jonne Arjoranta
- **French** - **French**
- Alda Marteau-Hardi
- Albakham
- Alix D. R. - Alix D. R.
- Baptiste Jonglez
- ButterflyOfFire - ButterflyOfFire
- Franck Paul
- Jean-Baptiste Holcroft
- Jonathan Chan
- Letiteuf55
- Martin Bodin
- codl
- Leia
- Alda Marteau-Hardi
- Mélanie Chauvel - Mélanie Chauvel
- Olivier Humbert
- Paul Marques Mota - Paul Marques Mota
- Sylvhem
- azenet
- Olivier Humbert
- Aditoo
- Jonathan Chan
- Letiteuf55
- Baptiste Jonglez
- goofy-mdn
- Jean-Baptiste Holcroft
- Technowix - Technowix
- Thibaut Girka
- Martin Bodin
- Théodore - Théodore
- azenet
- codl
- Thibaut Girka
- Franck Paul
- Sylvhem
- **Galician** - **Galician**
- ButterflyOfFire - ButterflyOfFire
- Xose M. - Xose M.
- Aditoo
- manequim - manequim
- **Georgian** - **Georgian**
- ButterflyOfFire - ButterflyOfFire
- Aditoo
- **German** - **German**
- Benedikt Geißler
- Aditoo
- ButterflyOfFire - ButterflyOfFire
- Daniel - Daniel
- Eugen Rochko
- Koyu Berteon
- Patrick Figel
- Weblate Admin
- averageunicorn - averageunicorn
- ePirat
- koyu
- Koyu Berteon
- larsreineke - larsreineke
- koyu
- Austin Jones
- lilo - lilo
- Benedikt Geißler
- ePirat
- Eugen Rochko
- Weblate Admin
- Patrick Figel
- **Greek** - **Greek**
- Dimitris Maroulidis
- Antonis - Antonis
- Aditoo
- ButterflyOfFire - ButterflyOfFire
- Dimitris Maroulidis
- Konstantinos Grevenitis - Konstantinos Grevenitis
- **Hebrew** - **Hebrew**
- ButterflyOfFire - ButterflyOfFire
- Aditoo
- Ira - Ira
- Yaron Shahrabani - Yaron Shahrabani
- **Hungarian** - **Hungarian**
- Adam Paszternak
- ButterflyOfFire - ButterflyOfFire
- Adam Paszternak
- Aditoo
- Tibike Miklós - Tibike Miklós
- **Ido** - **Ido**
- ButterflyOfFire - ButterflyOfFire
- Aditoo
- **Indonesian** - **Indonesian**
- Alfiana Sibuea
- afachri
- ButterflyOfFire - ButterflyOfFire
- Dito Kurnia Pratama - Dito Kurnia Pratama
- Eirworks - Eirworks
- afachri
- Aditoo
- Alfiana Sibuea
- se7entime - se7entime
- **Irish**
- Albakham
- Kevin Houlihan
- **Italian** - **Italian**
- Alessandro Levati - Alessandro Levati
- Albakham
- ButterflyOfFire - ButterflyOfFire
- Marcin Mikołajczak
- Aditoo
- Giuseppe Pignataro - Giuseppe Pignataro
- Stefano - Stefano
- **Japanese** - **Japanese**
- ButterflyOfFire
- Kumasun Morino
- Yamagishi Kazutoshi
- Hinaloe
- 小鳥遊まりあ
- mayaeh - mayaeh
- osapon - osapon
- unarist
- 小鳥遊まりあ
- 森の子リスのミーコの大冒険 - 森の子リスのミーコの大冒険
- **Korean**
- Kumasun Morino
- Yamagishi Kazutoshi
- Aditoo
- ButterflyOfFire - ButterflyOfFire
- Jeong Arm - Jeong Arm
- unarist
- **Kazakh**
- arshat
- Aditoo
- **Korean**
- Aditoo
- Jeong Arm
- ButterflyOfFire
- Minori Hiraoka - Minori Hiraoka
- Yamagishi Kazutoshi - Yamagishi Kazutoshi
- **Lithuanian**
- Sarunas Medeikis
- **Malay** - **Malay**
- ButterflyOfFire
- Muhammad Nur Hidayat (MNH48) - Muhammad Nur Hidayat (MNH48)
- Aditoo
- ButterflyOfFire
- **Norwegian (old code)** - **Norwegian (old code)**
- ButterflyOfFire - ButterflyOfFire
- Espen Rønnevik - Espen Rønnevik
- Aditoo
- Tale - Tale
- **Occitan** - **Occitan**
- Aditoo
- ButterflyOfFire - ButterflyOfFire
- Maxenç
- Quenti2 - Quenti2
- Quentí - Quentí
- Maxenç
- **Persian** - **Persian**
- ButterflyOfFire
- Masoud Abkenar - Masoud Abkenar
- Aditoo
- ButterflyOfFire
- **Polish** - **Polish**
- Aditoo
- Albakham
- ButterflyOfFire - ButterflyOfFire
- Jakub Mendyk
- Stasiek Michalski
- Marcin Mikołajczak - Marcin Mikołajczak
- Jakub Mendyk
- Marek Ľach - Marek Ľach
- Stasiek Michalski
- krkk - krkk
- **Portuguese** - **Portuguese**
- Albakham
- João Pinheiro
- manequim
- Aditoo
- ButterflyOfFire - ButterflyOfFire
- Hugo Gameiro - Hugo Gameiro
- manequim
- **Portuguese (Brazil)** - **Portuguese (Brazil)**
- André Andrade
- Aditoo
- Albakham
- Anna e só - Anna e só
- ButterflyOfFire
- Renato "Lond" Cerqueira - Renato "Lond" Cerqueira
- **Romanian**
- André Andrade
- ButterflyOfFire - ButterflyOfFire
- **Romanian**
- adrianbblk - adrianbblk
- ButterflyOfFire
- Aditoo
- **Russian** - **Russian**
- Andrew Zyabin
- Albakham
- ButterflyOfFire - ButterflyOfFire
- Evgeny Petrov - Evgeny Petrov
- Aditoo
- Павел Гастелло
- Andrew Zyabin
- Yaron Shahrabani - Yaron Shahrabani
- **Serbian** - **Serbian**
- Branko Kokanovic - Branko Kokanovic
- Burekz Finezt - Burekz Finezt
- Aditoo
- ButterflyOfFire - ButterflyOfFire
- **Serbian (latin)** - **Serbian (latin)**
- ButterflyOfFire - ButterflyOfFire
- Aditoo
- **Slovak** - **Slovak**
- Aditoo
- ButterflyOfFire - ButterflyOfFire
- Ivan Pleva - Ivan Pleva
- Lorem Ipsum
- Marek Ľach - Marek Ľach
- Peter - Peter
- **Slovenian** - **Slovenian**
- ButterflyOfFire
- Kristijan Tkalec - Kristijan Tkalec
- Aditoo
- ButterflyOfFire
- **Spanish** - **Spanish**
- Angeles Broullón
- Antón López
- Albakham
- ButterflyOfFire - ButterflyOfFire
- Carlos Mondragon - Carlos Mondragon
- Antón López
- Max Winkler
- Pablo de la Concepción Sanz
- Sergio Soriano
- Angeles Broullón
- Lothar Wolf
- Aditoo
- David Charte - David Charte
- Emmanuel - Emmanuel
- Lothar Wolf
- Pablo de la Concepción Sanz
- **Swedish** - **Swedish**
- ButterflyOfFire - ButterflyOfFire
- Elias Mårtenson
- Isak Holmström - Isak Holmström
- Shellkr - Shellkr
- Aditoo
- Elias Mårtenson
- Stefan Midjich - Stefan Midjich
- Tim Stahel - Tim Stahel
- Jonas Hultén
- **Telugu** - **Telugu**
- avndp
- Ranjith Tellakula
- Aditoo
- ButterflyOfFire - ButterflyOfFire
- Joseph Nuthalapati - Joseph Nuthalapati
- Ranjith Tellakula
- avndp
- **Thai** - **Thai**
- ButterflyOfFire - ButterflyOfFire
- parnikkapore
- Thai Localization
- Aditoo
- **Turkish** - **Turkish**
- Ali Demirtas
- ButterflyOfFire - ButterflyOfFire
- Aditoo
- **Ukrainian** - **Ukrainian**
- alexcleac
- ButterflyOfFire - ButterflyOfFire
- Aditoo
- Ivan Verchenko - Ivan Verchenko
- alexcleac
- **Welsh** - **Welsh**
- ButterflyOfFire
- carl morris
- Jaz-Michael King - Jaz-Michael King
- Kevin Beynon
- Owain Rhys Lewis - Owain Rhys Lewis
- Renato "Lond" Cerqueira
- Rhoslyn Prys - Rhoslyn Prys
- carl morris
- Aditoo
- ButterflyOfFire
- Renato "Lond" Cerqueira
- Albakham
- Kevin Beynon
- **Armenian** - **Armenian**
- Aditoo
- ButterflyOfFire - ButterflyOfFire
- **Latvian** - **Latvian**
- Aditoo
- ButterflyOfFire - ButterflyOfFire
- Maigonis
- **Tamil** - **Tamil**
- Aditoo
- ButterflyOfFire - ButterflyOfFire
- Prasanna Venkadesh - Prasanna Venkadesh

+ 169
- 0
CHANGELOG.md View File

@@ -3,6 +3,175 @@ Changelog


All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.


## [2.8.0] - 2019-04-10
### Added

- Add polls ([Gargron](https://github.com/tootsuite/mastodon/pull/10111), [ThibG](https://github.com/tootsuite/mastodon/pull/10155), [Gargron](https://github.com/tootsuite/mastodon/pull/10184), [ThibG](https://github.com/tootsuite/mastodon/pull/10196), [Gargron](https://github.com/tootsuite/mastodon/pull/10248), [ThibG](https://github.com/tootsuite/mastodon/pull/10255), [ThibG](https://github.com/tootsuite/mastodon/pull/10322), [Gargron](https://github.com/tootsuite/mastodon/pull/10138), [Gargron](https://github.com/tootsuite/mastodon/pull/10139), [Gargron](https://github.com/tootsuite/mastodon/pull/10144), [Gargron](https://github.com/tootsuite/mastodon/pull/10145),[Gargron](https://github.com/tootsuite/mastodon/pull/10146), [Gargron](https://github.com/tootsuite/mastodon/pull/10148), [Gargron](https://github.com/tootsuite/mastodon/pull/10151), [ThibG](https://github.com/tootsuite/mastodon/pull/10150), [Gargron](https://github.com/tootsuite/mastodon/pull/10168), [Gargron](https://github.com/tootsuite/mastodon/pull/10165), [Gargron](https://github.com/tootsuite/mastodon/pull/10172), [Gargron](https://github.com/tootsuite/mastodon/pull/10170), [Gargron](https://github.com/tootsuite/mastodon/pull/10171), [Gargron](https://github.com/tootsuite/mastodon/pull/10186), [Gargron](https://github.com/tootsuite/mastodon/pull/10189), [ThibG](https://github.com/tootsuite/mastodon/pull/10200), [rinsuki](https://github.com/tootsuite/mastodon/pull/10203), [Gargron](https://github.com/tootsuite/mastodon/pull/10213), [Gargron](https://github.com/tootsuite/mastodon/pull/10246), [Gargron](https://github.com/tootsuite/mastodon/pull/10265), [Gargron](https://github.com/tootsuite/mastodon/pull/10261), [ThibG](https://github.com/tootsuite/mastodon/pull/10333), [Gargron](https://github.com/tootsuite/mastodon/pull/10352), [ThibG](https://github.com/tootsuite/mastodon/pull/10140), [ThibG](https://github.com/tootsuite/mastodon/pull/10142), [ThibG](https://github.com/tootsuite/mastodon/pull/10141), [ThibG](https://github.com/tootsuite/mastodon/pull/10162), [ThibG](https://github.com/tootsuite/mastodon/pull/10161), [ThibG](https://github.com/tootsuite/mastodon/pull/10158), [ThibG](https://github.com/tootsuite/mastodon/pull/10156), [ThibG](https://github.com/tootsuite/mastodon/pull/10160), [Gargron](https://github.com/tootsuite/mastodon/pull/10185), [Gargron](https://github.com/tootsuite/mastodon/pull/10188), [ThibG](https://github.com/tootsuite/mastodon/pull/10195), [ThibG](https://github.com/tootsuite/mastodon/pull/10208), [Gargron](https://github.com/tootsuite/mastodon/pull/10187), [ThibG](https://github.com/tootsuite/mastodon/pull/10214), [ThibG](https://github.com/tootsuite/mastodon/pull/10209))
- Add follows & followers managing UI ([Gargron](https://github.com/tootsuite/mastodon/pull/10268), [Gargron](https://github.com/tootsuite/mastodon/pull/10308), [Gargron](https://github.com/tootsuite/mastodon/pull/10404), [Gargron](https://github.com/tootsuite/mastodon/pull/10293))
- Add identity proof integration with Keybase ([Gargron](https://github.com/tootsuite/mastodon/pull/10297), [xgess](https://github.com/tootsuite/mastodon/pull/10375), [Gargron](https://github.com/tootsuite/mastodon/pull/10338), [Gargron](https://github.com/tootsuite/mastodon/pull/10350), [Gargron](https://github.com/tootsuite/mastodon/pull/10414))
- Add option to overwrite imported data instead of merging ([Gargron](https://github.com/tootsuite/mastodon/pull/9962))
- Add featured hashtags to profiles ([Gargron](https://github.com/tootsuite/mastodon/pull/9755), [Gargron](https://github.com/tootsuite/mastodon/pull/10167), [Gargron](https://github.com/tootsuite/mastodon/pull/10249), [ThibG](https://github.com/tootsuite/mastodon/pull/10034))
- Add admission-based registrations mode ([Gargron](https://github.com/tootsuite/mastodon/pull/10250), [ThibG](https://github.com/tootsuite/mastodon/pull/10269), [Gargron](https://github.com/tootsuite/mastodon/pull/10264), [ThibG](https://github.com/tootsuite/mastodon/pull/10321), [Gargron](https://github.com/tootsuite/mastodon/pull/10349), [Gargron](https://github.com/tootsuite/mastodon/pull/10469))
- Add support for WebP uploads ([acid-chicken](https://github.com/tootsuite/mastodon/pull/9879))
- Add "copy link" item to status action bars in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/9983))
- Add list title editing in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/9748))
- Add a "Block & Report" button to the block confirmation dialog in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/10360))
- Add disappointed elephant when the page crashes in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/10275))
- Add ability to upload multiple files at once in web UI ([tmm576](https://github.com/tootsuite/mastodon/pull/9856))
- Add indication when you are not allowed to follow an account in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/10420), [Gargron](https://github.com/tootsuite/mastodon/pull/10491))
- Add validations to admin settings to catch common mistakes ([Gargron](https://github.com/tootsuite/mastodon/pull/10348), [ThibG](https://github.com/tootsuite/mastodon/pull/10354))
- Add `type`, `limit`, `offset`, `min_id`, `max_id`, `account_id` to search API ([Gargron](https://github.com/tootsuite/mastodon/pull/10091))
- Add a preferences API so apps can share basic behaviours ([Gargron](https://github.com/tootsuite/mastodon/pull/10109))
- Add `visibility` param to reblog REST API ([Gargron](https://github.com/tootsuite/mastodon/pull/9851), [ThibG](https://github.com/tootsuite/mastodon/pull/10302))
- Add `allowfullscreen` attribute to OEmbed iframe ([rinsuki](https://github.com/tootsuite/mastodon/pull/10370))
- Add `blocked_by` relationship to the REST API ([Gargron](https://github.com/tootsuite/mastodon/pull/10373))
- Add `tootctl statuses remove` to sweep unreferenced statuses ([Gargron](https://github.com/tootsuite/mastodon/pull/10063))
- Add `tootctl search deploy` to avoid ugly rake task syntax ([Gargron](https://github.com/tootsuite/mastodon/pull/10403))
- Add `tootctl self-destruct` to shut down server gracefully ([Gargron](https://github.com/tootsuite/mastodon/pull/10367))
- Add option to hide application used to toot ([ThibG](https://github.com/tootsuite/mastodon/pull/9897), [rinsuki](https://github.com/tootsuite/mastodon/pull/9994), [hinaloe](https://github.com/tootsuite/mastodon/pull/10086))
- Add `DB_SSLMODE` configuration variable ([sascha-sl](https://github.com/tootsuite/mastodon/pull/10210))
- Add click-to-copy UI to invites page ([Gargron](https://github.com/tootsuite/mastodon/pull/10259))
- Add self-replies fetching ([ThibG](https://github.com/tootsuite/mastodon/pull/10106), [ThibG](https://github.com/tootsuite/mastodon/pull/10128), [ThibG](https://github.com/tootsuite/mastodon/pull/10175), [ThibG](https://github.com/tootsuite/mastodon/pull/10201))
- Add rate limit for media proxy requests ([Gargron](https://github.com/tootsuite/mastodon/pull/10490))
- Add `tootctl emoji purge` ([Gargron](https://github.com/tootsuite/mastodon/pull/10481))
- Add `tootctl accounts approve` ([Gargron](https://github.com/tootsuite/mastodon/pull/10480))
- Add `tootctl accounts reset-relationships` ([noellabo](https://github.com/tootsuite/mastodon/pull/10483))

### Changed

- Change design of landing page ([Gargron](https://github.com/tootsuite/mastodon/pull/10232), [Gargron](https://github.com/tootsuite/mastodon/pull/10260), [ThibG](https://github.com/tootsuite/mastodon/pull/10284), [ThibG](https://github.com/tootsuite/mastodon/pull/10291), [koyuawsmbrtn](https://github.com/tootsuite/mastodon/pull/10356), [Gargron](https://github.com/tootsuite/mastodon/pull/10245))
- Change design of profile column in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/10337), [Aditoo17](https://github.com/tootsuite/mastodon/pull/10387), [ThibG](https://github.com/tootsuite/mastodon/pull/10390), [mayaeh](https://github.com/tootsuite/mastodon/pull/10379), [ThibG](https://github.com/tootsuite/mastodon/pull/10411))
- Change language detector threshold from 140 characters to 4 words ([Gargron](https://github.com/tootsuite/mastodon/pull/10376))
- Change language detector to always kick in for non-latin alphabets ([Gargron](https://github.com/tootsuite/mastodon/pull/10276))
- Change icons of features on admin dashboard ([Gargron](https://github.com/tootsuite/mastodon/pull/10366))
- Change DNS timeouts from 1s to 5s ([ThibG](https://github.com/tootsuite/mastodon/pull/10238))
- Change Docker image to use Ubuntu with jemalloc ([Sir-Boops](https://github.com/tootsuite/mastodon/pull/10100), [BenLubar](https://github.com/tootsuite/mastodon/pull/10212))
- Change public pages to be cacheable by proxies ([BenLubar](https://github.com/tootsuite/mastodon/pull/9059))
- Change the 410 gone response for suspended accounts to be cacheable by proxies ([ThibG](https://github.com/tootsuite/mastodon/pull/10339))
- Change web UI to not not empty timeline of blocked users on block ([ThibG](https://github.com/tootsuite/mastodon/pull/10359))
- Change JSON serializer to remove unused `@context` values ([Gargron](https://github.com/tootsuite/mastodon/pull/10378))
- Change GIFV file size limit to be the same as for other videos ([rinsuki](https://github.com/tootsuite/mastodon/pull/9924))
- Change Webpack to not use @babel/preset-env to compile node_modules ([ykzts](https://github.com/tootsuite/mastodon/pull/10289))
- Change web UI to use new Web Share Target API ([gol-cha](https://github.com/tootsuite/mastodon/pull/9963))
- Change ActivityPub reports to have persistent URIs ([ThibG](https://github.com/tootsuite/mastodon/pull/10303))
- Change `tootctl accounts cull --dry-run` to list accounts that would be deleted ([BenLubar](https://github.com/tootsuite/mastodon/pull/10460))
- Change format of CSV exports of follows and mutes to include extra settings ([ThibG](https://github.com/tootsuite/mastodon/pull/10495), [ThibG](https://github.com/tootsuite/mastodon/pull/10335))
- Change ActivityPub collections to be cacheable by proxies ([ThibG](https://github.com/tootsuite/mastodon/pull/10467))
- Change REST API and public profiles to not return follows/followers for users that have blocked you ([Gargron](https://github.com/tootsuite/mastodon/pull/10491))
- Change the groupings of menu items in settings navigation ([Gargron](https://github.com/tootsuite/mastodon/pull/10533))

### Removed

- Remove zopfli compression to speed up Webpack from 6min to 1min ([nolanlawson](https://github.com/tootsuite/mastodon/pull/10288))
- Remove stats.json generation to speed up Webpack ([nolanlawson](https://github.com/tootsuite/mastodon/pull/10290))

### Fixed

- Fix public timelines being broken by new toots when they are not mounted in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/10131))
- Fix quick filter settings not being saved when selecting a different filter in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/10296))
- Fix remote interaction dialogs being indexed by search engines ([Gargron](https://github.com/tootsuite/mastodon/pull/10240))
- Fix maxed-out invites not showing up as expired in UI ([Gargron](https://github.com/tootsuite/mastodon/pull/10274))
- Fix scrollbar styles on compose textarea ([Gargron](https://github.com/tootsuite/mastodon/pull/10292))
- Fix timeline merge workers being queued for remote users ([Gargron](https://github.com/tootsuite/mastodon/pull/10355))
- Fix alternative relay support regression ([Gargron](https://github.com/tootsuite/mastodon/pull/10398))
- Fix trying to fetch keys of unknown accounts on a self-delete from them ([ThibG](https://github.com/tootsuite/mastodon/pull/10326))
- Fix CAS `:service_validate_url` option ([enewhuis](https://github.com/tootsuite/mastodon/pull/10328))
- Fix race conditions when creating backups ([ThibG](https://github.com/tootsuite/mastodon/pull/10234))
- Fix whitespace not being stripped out of username before validation ([aurelien-reeves](https://github.com/tootsuite/mastodon/pull/10239))
- Fix n+1 query when deleting status ([Gargron](https://github.com/tootsuite/mastodon/pull/10247))
- Fix exiting follows not being rejected when suspending a remote account ([ThibG](https://github.com/tootsuite/mastodon/pull/10230))
- Fix the underlying button element in a disabled icon button not being disabled ([ThibG](https://github.com/tootsuite/mastodon/pull/10194))
- Fix race condition when streaming out deleted statuses ([ThibG](https://github.com/tootsuite/mastodon/pull/10280))
- Fix performance of admin federation UI by caching account counts ([Gargron](https://github.com/tootsuite/mastodon/pull/10374))
- Fix JS error on pages that don't define a CSRF token ([hinaloe](https://github.com/tootsuite/mastodon/pull/10383))
- Fix `tootctl accounts cull` sometimes removing accounts that are temporarily unreachable ([BenLubar](https://github.com/tootsuite/mastodon/pull/10460))

## [2.7.4] - 2019-03-05
### Fixed

- Fix web UI not cleaning up notifications after block ([Gargron](https://github.com/tootsuite/mastodon/pull/10108))
- Fix redundant HTTP requests when resolving private statuses ([ThibG](https://github.com/tootsuite/mastodon/pull/10115))
- Fix performance of account media query ([abcang](https://github.com/tootsuite/mastodon/pull/10121))
- Fix mention processing for unknown accounts ([ThibG](https://github.com/tootsuite/mastodon/pull/10125))
- Fix getting started column not scrolling on short screens ([trwnh](https://github.com/tootsuite/mastodon/pull/10075))
- Fix direct messages pagination in the web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/10126))
- Fix serialization of Announce activities ([ThibG](https://github.com/tootsuite/mastodon/pull/10129))
- Fix home timeline perpetually reloading when empty in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/10130))
- Fix lists export ([ThibG](https://github.com/tootsuite/mastodon/pull/10136))
- Fix edit profile page crash for suspended-then-unsuspended users ([ThibG](https://github.com/tootsuite/mastodon/pull/10178))

## [2.7.3] - 2019-02-23
### Added

- Add domain filter to the admin federation page ([ThibG](https://github.com/tootsuite/mastodon/pull/10071))
- Add quick link from admin account view to block/unblock instance ([ThibG](https://github.com/tootsuite/mastodon/pull/10073))

### Fixed

- Fix video player width not being updated to fit container width ([ThibG](https://github.com/tootsuite/mastodon/pull/10069))
- Fix domain filter being shown in admin page when local filter is active ([ThibG](https://github.com/tootsuite/mastodon/pull/10074))
- Fix crash when conversations have no valid participants ([ThibG](https://github.com/tootsuite/mastodon/pull/10078))
- Fix error when performing admin actions on no statuses ([ThibG](https://github.com/tootsuite/mastodon/pull/10094))

### Changed

- Change custom emojis to randomize stored file name ([hinaloe](https://github.com/tootsuite/mastodon/pull/10090))

## [2.7.2] - 2019-02-17
### Added

- Add support for IPv6 in e-mail validation ([zoc](https://github.com/tootsuite/mastodon/pull/10009))
- Add record of IP address used for signing up ([ThibG](https://github.com/tootsuite/mastodon/pull/10026))
- Add tight rate-limit for API deletions (30 per 30 minutes) ([Gargron](https://github.com/tootsuite/mastodon/pull/10042))
- Add support for embedded `Announce` objects attributed to the same actor ([ThibG](https://github.com/tootsuite/mastodon/pull/9998), [Gargron](https://github.com/tootsuite/mastodon/pull/10065))
- Add spam filter for `Create` and `Announce` activities ([Gargron](https://github.com/tootsuite/mastodon/pull/10005), [Gargron](https://github.com/tootsuite/mastodon/pull/10041), [Gargron](https://github.com/tootsuite/mastodon/pull/10062))
- Add `registrations` attribute to `GET /api/v1/instance` ([Gargron](https://github.com/tootsuite/mastodon/pull/10060))
- Add `vapid_key` to `POST /api/v1/apps` and `GET /api/v1/apps/verify_credentials` ([Gargron](https://github.com/tootsuite/mastodon/pull/10058))

### Fixed

- Fix link color and add link underlines in high-contrast theme ([Gargron](https://github.com/tootsuite/mastodon/pull/9949), [Gargron](https://github.com/tootsuite/mastodon/pull/10028))
- Fix unicode characters in URLs not being linkified ([JMendyk](https://github.com/tootsuite/mastodon/pull/8447), [hinaloe](https://github.com/tootsuite/mastodon/pull/9991))
- Fix URLs linkifier grabbing ending quotation as part of the link ([Gargron](https://github.com/tootsuite/mastodon/pull/9997))
- Fix authorized applications page design ([rinsuki](https://github.com/tootsuite/mastodon/pull/9969))
- Fix custom emojis not showing up in share page emoji picker ([rinsuki](https://github.com/tootsuite/mastodon/pull/9970))
- Fix too liberal application of whitespace in toots ([trwnh](https://github.com/tootsuite/mastodon/pull/9968))
- Fix misleading e-mail hint being displayed in admin view ([ThibG](https://github.com/tootsuite/mastodon/pull/9973))
- Fix tombstones not being cleared out ([abcang](https://github.com/tootsuite/mastodon/pull/9978))
- Fix some timeline jumps ([ThibG](https://github.com/tootsuite/mastodon/pull/9982), [ThibG](https://github.com/tootsuite/mastodon/pull/10001), [rinsuki](https://github.com/tootsuite/mastodon/pull/10046))
- Fix content warning input taking keyboard focus even when hidden ([hinaloe](https://github.com/tootsuite/mastodon/pull/10017))
- Fix hashtags select styling in default and high-contrast themes ([Gargron](https://github.com/tootsuite/mastodon/pull/10029))
- Fix style regressions on landing page ([Gargron](https://github.com/tootsuite/mastodon/pull/10030))
- Fix hashtag column not subscribing to stream on mount ([Gargron](https://github.com/tootsuite/mastodon/pull/10040))
- Fix relay enabling/disabling not resetting inbox availability status ([Gargron](https://github.com/tootsuite/mastodon/pull/10048))
- Fix mutes, blocks, domain blocks and follow requests not paginating ([Gargron](https://github.com/tootsuite/mastodon/pull/10057))
- Fix crash on public hashtag pages when streaming fails ([ThibG](https://github.com/tootsuite/mastodon/pull/10061))

### Changed

- Change icon for unlisted visibility level ([clarcharr](https://github.com/tootsuite/mastodon/pull/9952))
- Change queue of actor deletes from push to pull for non-follower recipients ([ThibG](https://github.com/tootsuite/mastodon/pull/10016))
- Change robots.txt to exclude media proxy URLs ([nightpool](https://github.com/tootsuite/mastodon/pull/10038))
- Change upload description input to allow line breaks ([BenLubar](https://github.com/tootsuite/mastodon/pull/10036))
- Change `dist/mastodon-streaming.service` to recommend running node without intermediary npm command ([nolanlawson](https://github.com/tootsuite/mastodon/pull/10032))
- Change conversations to always show names of other participants ([Gargron](https://github.com/tootsuite/mastodon/pull/10047))
- Change buttons on timeline preview to open the interaction dialog ([Gargron](https://github.com/tootsuite/mastodon/pull/10054))
- Change error graphic to hover-to-play ([Gargron](https://github.com/tootsuite/mastodon/pull/10055))

## [2.7.1] - 2019-01-28
### Fixed

- Fix SSO authentication not working due to missing agreement boolean ([Gargron](https://github.com/tootsuite/mastodon/pull/9915))
- Fix slow fallback of CopyAccountStats migration setting stats to 0 ([Gargron](https://github.com/tootsuite/mastodon/pull/9930))
- Fix wrong command in migration error message ([angristan](https://github.com/tootsuite/mastodon/pull/9877))
- Fix initial value of volume slider in video player and handle volume changes ([ThibG](https://github.com/tootsuite/mastodon/pull/9929))
- Fix missing hotkeys for notifications ([ThibG](https://github.com/tootsuite/mastodon/pull/9927))
- Fix being able to attach unattached media created by other users ([ThibG](https://github.com/tootsuite/mastodon/pull/9921))
- Fix unrescued SSL error during link verification ([renatolond](https://github.com/tootsuite/mastodon/pull/9914))
- Fix Firefox scrollbar color regression ([trwnh](https://github.com/tootsuite/mastodon/pull/9908))
- Fix scheduled status with media immediately creating a status ([ThibG](https://github.com/tootsuite/mastodon/pull/9894))
- Fix missing strong style for landing page description ([Kjwon15](https://github.com/tootsuite/mastodon/pull/9892))

## [2.7.0] - 2019-01-20 ## [2.7.0] - 2019-01-20
### Added ### Added




+ 3
- 1
CONTRIBUTING.md View File

@@ -1,7 +1,7 @@
Contributing Contributing
============ ============


Thank you for considering contributing to Mastodon 🐘
Thank you for considering contributing to Mastodon 🐘


You can contribute in the following ways: You can contribute in the following ways:


@@ -10,6 +10,8 @@ You can contribute in the following ways:
- Contributing code to Mastodon by fixing bugs or implementing features - Contributing code to Mastodon by fixing bugs or implementing features
- Improving the documentation - Improving the documentation


If your contributions are accepted into Mastodon, you can request to be paid through [our OpenCollective](https://opencollective.com/mastodon).

## Bug reports ## Bug reports


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. 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.


+ 123
- 85
Dockerfile View File

@@ -1,90 +1,128 @@
FROM node:8.15-alpine as node
FROM ruby:2.6-alpine3.8

LABEL maintainer="https://github.com/tootsuite/mastodon" \
description="Your self-hosted, globally interconnected microblogging community"

FROM ubuntu:18.04 as build-dep

# Use bash for the shell
SHELL ["bash", "-c"]

# Install Node
ENV NODE_VER="8.15.0"
RUN echo "Etc/UTC" > /etc/localtime && \
apt update && \
apt -y dist-upgrade && \
apt -y install wget make gcc g++ python && \
cd ~ && \
wget https://nodejs.org/download/release/v$NODE_VER/node-v$NODE_VER.tar.gz && \
tar xf node-v$NODE_VER.tar.gz && \
cd node-v$NODE_VER && \
./configure --prefix=/opt/node && \
make -j$(nproc) > /dev/null && \
make install

# Install jemalloc
ENV JE_VER="5.1.0"
RUN apt update && \
apt -y install autoconf && \
cd ~ && \
wget https://github.com/jemalloc/jemalloc/archive/$JE_VER.tar.gz && \
tar xf $JE_VER.tar.gz && \
cd jemalloc-$JE_VER && \
./autogen.sh && \
./configure --prefix=/opt/jemalloc && \
make -j$(nproc) > /dev/null && \
make install_bin install_include install_lib

# Install ruby
ENV RUBY_VER="2.6.1"
ENV CPPFLAGS="-I/opt/jemalloc/include"
ENV LDFLAGS="-L/opt/jemalloc/lib/"
RUN apt update && \
apt -y install build-essential \
bison libyaml-dev libgdbm-dev libreadline-dev \
libncurses5-dev libffi-dev zlib1g-dev libssl-dev && \
cd ~ && \
wget https://cache.ruby-lang.org/pub/ruby/${RUBY_VER%.*}/ruby-$RUBY_VER.tar.gz && \
tar xf ruby-$RUBY_VER.tar.gz && \
cd ruby-$RUBY_VER && \
./configure --prefix=/opt/ruby \
--with-jemalloc \
--with-shared \
--disable-install-doc && \
ln -s /opt/jemalloc/lib/* /usr/lib/ && \
make -j$(nproc) > /dev/null && \
make install

ENV PATH="${PATH}:/opt/ruby/bin:/opt/node/bin"

RUN npm install -g yarn && \
gem install bundler && \
apt update && \
apt -y install git libicu-dev libidn11-dev \
libpq-dev libprotobuf-dev protobuf-compiler

COPY Gemfile* package.json yarn.lock /opt/mastodon/

RUN cd /opt/mastodon && \
bundle install -j$(nproc) --deployment --without development test && \
yarn install --pure-lockfile

FROM ubuntu:18.04

# Copy over all the langs needed for runtime
COPY --from=build-dep /opt/node /opt/node
COPY --from=build-dep /opt/ruby /opt/ruby
COPY --from=build-dep /opt/jemalloc /opt/jemalloc

# Add more PATHs to the PATH
ENV PATH="${PATH}:/opt/ruby/bin:/opt/node/bin:/opt/mastodon/bin"

# Create the mastodon user
ARG UID=991 ARG UID=991
ARG GID=991 ARG GID=991

ENV PATH=/mastodon/bin:$PATH \
RAILS_SERVE_STATIC_FILES=true \
RAILS_ENV=production \
NODE_ENV=production

ARG LIBICONV_VERSION=1.15
ARG LIBICONV_DOWNLOAD_SHA256=ccf536620a45458d26ba83887a983b96827001e92a13847b45e4925cc8913178

EXPOSE 3000 4000

WORKDIR /mastodon

COPY --from=node /usr/local/bin/node /usr/local/bin/node
COPY --from=node /usr/local/lib/node_modules /usr/local/lib/node_modules
COPY --from=node /usr/local/bin/npm /usr/local/bin/npm
COPY --from=node /opt/yarn-* /opt/yarn

RUN apk -U upgrade \
&& apk add -t build-dependencies \
build-base \
icu-dev \
libidn-dev \
libressl \
libtool \
libxml2-dev \
libxslt-dev \
postgresql-dev \
protobuf-dev \
python \
&& apk add \
ca-certificates \
ffmpeg \
file \
git \
icu-libs \
imagemagick \
libidn \
libpq \
libxml2 \
libxslt \
protobuf \
tini \
tzdata \
&& update-ca-certificates \
&& ln -s /opt/yarn/bin/yarn /usr/local/bin/yarn \
&& ln -s /opt/yarn/bin/yarnpkg /usr/local/bin/yarnpkg \
&& mkdir -p /tmp/src /opt \
&& wget -O libiconv.tar.gz "https://ftp.gnu.org/pub/gnu/libiconv/libiconv-$LIBICONV_VERSION.tar.gz" \
&& echo "$LIBICONV_DOWNLOAD_SHA256 *libiconv.tar.gz" | sha256sum -c - \
&& tar -xzf libiconv.tar.gz -C /tmp/src \
&& rm libiconv.tar.gz \
&& cd /tmp/src/libiconv-$LIBICONV_VERSION \
&& ./configure --prefix=/usr/local \
&& make -j$(getconf _NPROCESSORS_ONLN)\
&& make install \
&& libtool --finish /usr/local/lib \
&& cd /mastodon \
&& rm -rf /tmp/* /var/cache/apk/*

COPY Gemfile Gemfile.lock package.json yarn.lock .yarnclean /mastodon/

RUN bundle config build.nokogiri --use-system-libraries --with-iconv-lib=/usr/local/lib --with-iconv-include=/usr/local/include \
&& bundle install -j$(getconf _NPROCESSORS_ONLN) --deployment --without test development \
&& yarn install --pure-lockfile --ignore-engines \
&& yarn cache clean

RUN addgroup -g ${GID} mastodon && adduser -h /mastodon -s /bin/sh -D -G mastodon -u ${UID} mastodon \
&& mkdir -p /mastodon/public/system /mastodon/public/assets /mastodon/public/packs \
&& chown -R mastodon:mastodon /mastodon/public

COPY . /mastodon

RUN chown -R mastodon:mastodon /mastodon

VOLUME /mastodon/public/system

RUN apt update && \
echo "Etc/UTC" > /etc/localtime && \
ln -s /opt/jemalloc/lib/* /usr/lib/ && \
apt -y dist-upgrade && \
apt install -y whois wget && \
addgroup --gid $GID mastodon && \
useradd -m -u $UID -g $GID -d /opt/mastodon mastodon && \
echo "mastodon:`head /dev/urandom | tr -dc A-Za-z0-9 | head -c 24 | mkpasswd -s -m sha-256`" | chpasswd

# Install masto runtime deps
RUN apt -y --no-install-recommends install \
libssl1.1 libpq5 imagemagick ffmpeg \
libicu60 libprotobuf10 libidn11 libyaml-0-2 \
file ca-certificates tzdata libreadline7 && \
apt -y install gcc && \
ln -s /opt/mastodon /mastodon && \
gem install bundler && \
rm -rf /var/cache && \
rm -rf /var/lib/apt

# Add tini
ENV TINI_VERSION="0.18.0"
ENV TINI_SUM="12d20136605531b09a2c2dac02ccee85e1b874eb322ef6baf7561cd93f93c855"
ADD https://github.com/krallin/tini/releases/download/v${TINI_VERSION}/tini /tini
RUN echo "$TINI_SUM tini" | sha256sum -c -
RUN chmod +x /tini

# Copy over masto source, and dependencies from building, and set permissions
COPY --chown=mastodon:mastodon . /opt/mastodon
COPY --from=build-dep --chown=mastodon:mastodon /opt/mastodon /opt/mastodon

# Run masto services in prod mode
ENV RAILS_ENV="production"
ENV NODE_ENV="production"

# Tell rails to serve static files
ENV RAILS_SERVE_STATIC_FILES="true"

# Set the run user
USER mastodon USER mastodon


RUN OTP_SECRET=precompile_placeholder SECRET_KEY_BASE=precompile_placeholder bundle exec rails assets:precompile
# Precompile assets
RUN cd ~ && \
OTP_SECRET=precompile_placeholder SECRET_KEY_BASE=precompile_placeholder rails assets:precompile && \
yarn cache clean


ENTRYPOINT ["/sbin/tini", "--"]
# Set the work dir and the container entry point
WORKDIR /opt/mastodon
ENTRYPOINT ["/tini", "--"]

+ 15
- 15
Gemfile View File

@@ -6,16 +6,16 @@ ruby '>= 2.4.0', '< 2.7.0'
gem 'pkg-config', '~> 1.3' gem 'pkg-config', '~> 1.3'


gem 'puma', '~> 3.12' gem 'puma', '~> 3.12'
gem 'rails', '~> 5.2.2'
gem 'rails', '~> 5.2.3'
gem 'thor', '~> 0.20' gem 'thor', '~> 0.20'


gem 'hamlit-rails', '~> 0.2' gem 'hamlit-rails', '~> 0.2'
gem 'pg', '~> 1.1' gem 'pg', '~> 1.1'
gem 'makara', '~> 0.4' gem 'makara', '~> 0.4'
gem 'pghero', '~> 2.2' gem 'pghero', '~> 2.2'
gem 'dotenv-rails', '~> 2.6'
gem 'dotenv-rails', '~> 2.7'


gem 'aws-sdk-s3', '~> 1.30', require: false
gem 'aws-sdk-s3', '~> 1.36', require: false
gem 'fog-core', '<= 2.1.0' gem 'fog-core', '<= 2.1.0'
gem 'fog-openstack', '~> 0.3', require: false gem 'fog-openstack', '~> 0.3', require: false
gem 'paperclip', '~> 6.0' gem 'paperclip', '~> 6.0'
@@ -23,14 +23,14 @@ gem 'paperclip-av-transcoder', '~> 0.6'
gem 'streamio-ffmpeg', '~> 3.0' gem 'streamio-ffmpeg', '~> 3.0'


gem 'active_model_serializers', '~> 0.10' gem 'active_model_serializers', '~> 0.10'
gem 'addressable', '~> 2.5'
gem 'bootsnap', '~> 1.3', require: false
gem 'addressable', '~> 2.6'
gem 'bootsnap', '~> 1.4', require: false
gem 'browser' gem 'browser'
gem 'charlock_holmes', '~> 0.7.6' gem 'charlock_holmes', '~> 0.7.6'
gem 'iso-639' gem 'iso-639'
gem 'chewy', '~> 5.0' gem 'chewy', '~> 5.0'
gem 'cld3', '~> 3.2.3' gem 'cld3', '~> 3.2.3'
gem 'devise', '~> 4.5'
gem 'devise', '~> 4.6'
gem 'devise-two-factor', '~> 3.0' gem 'devise-two-factor', '~> 3.0'


group :pam_authentication, optional: true do group :pam_authentication, optional: true do
@@ -85,8 +85,8 @@ gem 'strong_migrations', '~> 0.3'
gem 'tty-command', '~> 0.8', require: false gem 'tty-command', '~> 0.8', require: false
gem 'tty-prompt', '~> 0.18', require: false gem 'tty-prompt', '~> 0.18', require: false
gem 'twitter-text', '~> 1.14' gem 'twitter-text', '~> 1.14'
gem 'tzinfo-data', '~> 1.2018'
gem 'webpacker', '~> 3.5'
gem 'tzinfo-data', '~> 1.2019'
gem 'webpacker', '~> 4.0'
gem 'webpush' gem 'webpush'


gem 'json-ld', '~> 3.0' gem 'json-ld', '~> 3.0'
@@ -97,7 +97,7 @@ group :development, :test do
gem 'fabrication', '~> 2.20' gem 'fabrication', '~> 2.20'
gem 'fuubar', '~> 2.3' gem 'fuubar', '~> 2.3'
gem 'i18n-tasks', '~> 0.9', require: false gem 'i18n-tasks', '~> 0.9', require: false
gem 'pry-byebug', '~> 3.6'
gem 'pry-byebug', '~> 3.7'
gem 'pry-rails', '~> 0.3' gem 'pry-rails', '~> 0.3'
gem 'rspec-rails', '~> 3.8' gem 'rspec-rails', '~> 3.8'
end end
@@ -107,19 +107,19 @@ group :production, :test do
end end


group :test do group :test do
gem 'capybara', '~> 3.12'
gem 'capybara', '~> 3.16'
gem 'climate_control', '~> 0.2' gem 'climate_control', '~> 0.2'
gem 'faker', '~> 1.9' gem 'faker', '~> 1.9'
gem 'microformats', '~> 4.0'
gem 'microformats', '~> 4.1'
gem 'rails-controller-testing', '~> 1.0' gem 'rails-controller-testing', '~> 1.0'
gem 'rspec-sidekiq', '~> 3.0' gem 'rspec-sidekiq', '~> 3.0'
gem 'simplecov', '~> 0.16', require: false gem 'simplecov', '~> 0.16', require: false
gem 'webmock', '~> 3.5' gem 'webmock', '~> 3.5'
gem 'parallel_tests', '~> 2.27'
gem 'parallel_tests', '~> 2.28'
end end


group :development do group :development do
gem 'active_record_query_trace', '~> 1.5'
gem 'active_record_query_trace', '~> 1.6'
gem 'annotate', '~> 2.7' gem 'annotate', '~> 2.7'
gem 'better_errors', '~> 2.5' gem 'better_errors', '~> 2.5'
gem 'binding_of_caller', '~> 0.7' gem 'binding_of_caller', '~> 0.7'
@@ -127,8 +127,8 @@ group :development do
gem 'letter_opener', '~> 1.7' gem 'letter_opener', '~> 1.7'
gem 'letter_opener_web', '~> 1.3' gem 'letter_opener_web', '~> 1.3'
gem 'memory_profiler' gem 'memory_profiler'
gem 'rubocop', '~> 0.63', require: false
gem 'brakeman', '~> 4.4', require: false
gem 'rubocop', '~> 0.67', require: false
gem 'brakeman', '~> 4.5', require: false
gem 'bundler-audit', '~> 0.6', require: false gem 'bundler-audit', '~> 0.6', require: false
gem 'scss_lint', '~> 0.57', require: false gem 'scss_lint', '~> 0.57', require: false




+ 127
- 126
Gemfile.lock View File

@@ -15,54 +15,54 @@ GIT
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
actioncable (5.2.2)
actionpack (= 5.2.2)
actioncable (5.2.3)
actionpack (= 5.2.3)
nio4r (~> 2.0) nio4r (~> 2.0)
websocket-driver (>= 0.6.1) websocket-driver (>= 0.6.1)
actionmailer (5.2.2)
actionpack (= 5.2.2)
actionview (= 5.2.2)
activejob (= 5.2.2)
actionmailer (5.2.3)
actionpack (= 5.2.3)
actionview (= 5.2.3)
activejob (= 5.2.3)
mail (~> 2.5, >= 2.5.4) mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
actionpack (5.2.2)
actionview (= 5.2.2)
activesupport (= 5.2.2)
actionpack (5.2.3)
actionview (= 5.2.3)
activesupport (= 5.2.3)
rack (~> 2.0) rack (~> 2.0)
rack-test (>= 0.6.3) rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.2) rails-html-sanitizer (~> 1.0, >= 1.0.2)
actionview (5.2.2)
activesupport (= 5.2.2)
actionview (5.2.3)
activesupport (= 5.2.3)
builder (~> 3.1) builder (~> 3.1)
erubi (~> 1.4) erubi (~> 1.4)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.3) rails-html-sanitizer (~> 1.0, >= 1.0.3)
active_model_serializers (0.10.8)
active_model_serializers (0.10.9)
actionpack (>= 4.1, < 6) actionpack (>= 4.1, < 6)
activemodel (>= 4.1, < 6) activemodel (>= 4.1, < 6)
case_transform (>= 0.2) case_transform (>= 0.2)
jsonapi-renderer (>= 0.1.1.beta1, < 0.3) jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
active_record_query_trace (1.5.4)
activejob (5.2.2)
activesupport (= 5.2.2)
active_record_query_trace (1.6.2)
activejob (5.2.3)
activesupport (= 5.2.3)
globalid (>= 0.3.6) globalid (>= 0.3.6)
activemodel (5.2.2)
activesupport (= 5.2.2)
activerecord (5.2.2)
activemodel (= 5.2.2)
activesupport (= 5.2.2)
activemodel (5.2.3)
activesupport (= 5.2.3)
activerecord (5.2.3)
activemodel (= 5.2.3)
activesupport (= 5.2.3)
arel (>= 9.0) arel (>= 9.0)
activestorage (5.2.2)
actionpack (= 5.2.2)
activerecord (= 5.2.2)
activestorage (5.2.3)
actionpack (= 5.2.3)
activerecord (= 5.2.3)
marcel (~> 0.3.1) marcel (~> 0.3.1)
activesupport (5.2.2)
activesupport (5.2.3)
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 0.7, < 2) i18n (>= 0.7, < 2)
minitest (~> 5.1) minitest (~> 5.1)
tzinfo (~> 1.1) tzinfo (~> 1.1)
addressable (2.5.2)
addressable (2.6.0)
public_suffix (>= 2.0.2, < 4.0) public_suffix (>= 2.0.2, < 4.0)
airbrussh (1.3.0) airbrussh (1.3.0)
sshkit (>= 1.6.1, != 1.7.0) sshkit (>= 1.6.1, != 1.7.0)
@@ -75,32 +75,33 @@ GEM
encryptor (~> 3.0.0) encryptor (~> 3.0.0)
av (0.9.0) av (0.9.0)
cocaine (~> 0.5.3) cocaine (~> 0.5.3)
aws-eventstream (1.0.1)
aws-partitions (1.131.0)
aws-sdk-core (3.45.0)
aws-eventstream (~> 1.0)
aws-eventstream (1.0.2)
aws-partitions (1.147.0)
aws-sdk-core (3.48.3)
aws-eventstream (~> 1.0, >= 1.0.2)
aws-partitions (~> 1.0) aws-partitions (~> 1.0)
aws-sigv4 (~> 1.0)
aws-sigv4 (~> 1.1)
jmespath (~> 1.0) jmespath (~> 1.0)
aws-sdk-kms (1.13.0)
aws-sdk-core (~> 3, >= 3.39.0)
aws-sigv4 (~> 1.0)
aws-sdk-s3 (1.30.1)
aws-sdk-core (~> 3, >= 3.39.0)
aws-sdk-kms (1.16.0)
aws-sdk-core (~> 3, >= 3.48.2)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.36.0)
aws-sdk-core (~> 3, >= 3.48.2)
aws-sdk-kms (~> 1) aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.0) aws-sigv4 (~> 1.0)
aws-sigv4 (1.0.3)
aws-sigv4 (1.1.0)
aws-eventstream (~> 1.0, >= 1.0.2)
bcrypt (3.1.12) bcrypt (3.1.12)
benchmark-ips (2.7.2) benchmark-ips (2.7.2)
better_errors (2.5.0)
better_errors (2.5.1)
coderay (>= 1.0.0) coderay (>= 1.0.0)
erubi (>= 1.0.0) erubi (>= 1.0.0)
rack (>= 0.9.0) rack (>= 0.9.0)
binding_of_caller (0.8.0) binding_of_caller (0.8.0)
debug_inspector (>= 0.0.1) debug_inspector (>= 0.0.1)
bootsnap (1.3.2)
bootsnap (1.4.3)
msgpack (~> 1.0) msgpack (~> 1.0)
brakeman (4.4.0)
brakeman (4.5.0)
browser (2.5.3) browser (2.5.3)
builder (3.2.3) builder (3.2.3)
bullet (5.9.0) bullet (5.9.0)
@@ -109,7 +110,7 @@ GEM
bundler-audit (0.6.1) bundler-audit (0.6.1)
bundler (>= 1.2.0, < 3) bundler (>= 1.2.0, < 3)
thor (~> 0.18) thor (~> 0.18)
byebug (10.0.2)
byebug (11.0.0)
capistrano (3.11.0) capistrano (3.11.0)
airbrussh (>= 1.0.0) airbrussh (>= 1.0.0)
i18n i18n
@@ -126,7 +127,7 @@ GEM
sshkit (~> 1.3) sshkit (~> 1.3)
capistrano-yarn (2.0.2) capistrano-yarn (2.0.2)
capistrano (~> 3.0) capistrano (~> 3.0)
capybara (3.12.0)
capybara (3.16.1)
addressable addressable
mini_mime (>= 0.1.3) mini_mime (>= 0.1.3)
nokogiri (~> 1.8) nokogiri (~> 1.8)
@@ -148,7 +149,7 @@ GEM
cocaine (0.5.8) cocaine (0.5.8)
climate_control (>= 0.0.3, < 1.0) climate_control (>= 0.0.3, < 1.0)
coderay (1.1.2) coderay (1.1.2)
concurrent-ruby (1.1.4)
concurrent-ruby (1.1.5)
connection_pool (2.2.2) connection_pool (2.2.2)
crack (0.4.3) crack (0.4.3)
safe_yaml (~> 1.0.0) safe_yaml (~> 1.0.0)
@@ -164,7 +165,7 @@ GEM
rack (>= 1) rack (>= 1)
rake (> 10, < 13) rake (> 10, < 13)
thor (~> 0.19) thor (~> 0.19)
devise (4.5.0)
devise (4.6.2)
bcrypt (~> 3.0) bcrypt (~> 3.0)
orm_adapter (~> 0.1) orm_adapter (~> 0.1)
railties (>= 4.1.0, < 6.0) railties (>= 4.1.0, < 6.0)
@@ -185,10 +186,10 @@ GEM
unf (>= 0.0.5, < 1.0.0) unf (>= 0.0.5, < 1.0.0)
doorkeeper (5.0.2) doorkeeper (5.0.2)
railties (>= 4.2) railties (>= 4.2)
dotenv (2.6.0)
dotenv-rails (2.6.0)
dotenv (= 2.6.0)
railties (>= 3.2, < 6.0)
dotenv (2.7.2)
dotenv-rails (2.7.2)
dotenv (= 2.7.2)
railties (>= 3.2, < 6.1)
elasticsearch (6.0.2) elasticsearch (6.0.2)
elasticsearch-api (= 6.0.2) elasticsearch-api (= 6.0.2)
elasticsearch-transport (= 6.0.2) elasticsearch-transport (= 6.0.2)
@@ -205,7 +206,7 @@ GEM
tzinfo tzinfo
excon (0.62.0) excon (0.62.0)
fabrication (2.20.1) fabrication (2.20.1)
faker (1.9.1)
faker (1.9.3)
i18n (>= 0.7) i18n (>= 0.7)
faraday (0.15.0) faraday (0.15.0)
multipart-post (>= 1.2, < 3) multipart-post (>= 1.2, < 3)
@@ -232,18 +233,18 @@ GEM
rspec-core (~> 3.0) rspec-core (~> 3.0)
ruby-progressbar (~> 1.4) ruby-progressbar (~> 1.4)
get_process_mem (0.2.3) get_process_mem (0.2.3)
globalid (0.4.1)
globalid (0.4.2)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
goldfinger (2.1.0) goldfinger (2.1.0)
addressable (~> 2.5) addressable (~> 2.5)
http (~> 3.0) http (~> 3.0)
nokogiri (~> 1.8) nokogiri (~> 1.8)
oj (~> 3.0) oj (~> 3.0)
hamlit (2.8.8)
hamlit (2.9.3)
temple (>= 0.8.0) temple (>= 0.8.0)
thor thor
tilt tilt
hamlit-rails (0.2.0)
hamlit-rails (0.2.3)
actionpack (>= 4.0.1) actionpack (>= 4.0.1)
activesupport (>= 4.0.1) activesupport (>= 4.0.1)
hamlit (>= 1.2.0) hamlit (>= 1.2.0)
@@ -253,7 +254,7 @@ GEM
hashdiff (0.3.7) hashdiff (0.3.7)
hashie (3.6.0) hashie (3.6.0)
heapy (0.1.4) heapy (0.1.4)
highline (2.0.0)
highline (2.0.1)
hiredis (0.6.3) hiredis (0.6.3)
hkdf (0.3.0) hkdf (0.3.0)
htmlentities (4.3.4) htmlentities (4.3.4)
@@ -266,12 +267,12 @@ GEM
domain_name (~> 0.5) domain_name (~> 0.5)
http-form_data (2.1.1) http-form_data (2.1.1)
http_accept_language (2.1.1) http_accept_language (2.1.1)
httplog (1.2.0)
httplog (1.2.2)
rack (>= 1.0) rack (>= 1.0)
rainbow (>= 2.0.0) rainbow (>= 2.0.0)
i18n (1.5.2)
i18n (1.6.0)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
i18n-tasks (0.9.28)
i18n-tasks (0.9.29)
activesupport (>= 4.0.2) activesupport (>= 4.0.2)
ast (>= 2.1.0) ast (>= 2.1.0)
erubi erubi
@@ -290,8 +291,8 @@ GEM
json-ld (3.0.2) json-ld (3.0.2)
multi_json (~> 1.12) multi_json (~> 1.12)
rdf (>= 2.2.8, < 4.0) rdf (>= 2.2.8, < 4.0)
json-ld-preloaded (3.0.0)
json-ld (>= 2.2, < 4.0)
json-ld-preloaded (3.0.2)
json-ld (~> 3.0)
multi_json (~> 1.12) multi_json (~> 1.12)
rdf (~> 3.0) rdf (~> 3.0)
jsonapi-renderer (0.2.0) jsonapi-renderer (0.2.0)
@@ -327,25 +328,25 @@ GEM
nokogiri (>= 1.5.9) nokogiri (>= 1.5.9)
mail (2.7.1) mail (2.7.1)
mini_mime (>= 0.1.1) mini_mime (>= 0.1.1)
makara (0.4.0)
makara (0.4.1)
activerecord (>= 3.0.0) activerecord (>= 3.0.0)
marcel (0.3.3) marcel (0.3.3)
mimemagic (~> 0.3.2) mimemagic (~> 0.3.2)
mario-redis-lock (1.2.1) mario-redis-lock (1.2.1)
redis (>= 3.0.5) redis (>= 3.0.5)
memory_profiler (0.9.12)
memory_profiler (0.9.13)
method_source (0.9.2) method_source (0.9.2)
microformats (4.0.7)
json
nokogiri
microformats (4.1.0)
json (~> 2.1)
nokogiri (~> 1.8, >= 1.8.3)
mime-types (3.2.2) mime-types (3.2.2)
mime-types-data (~> 3.2015) mime-types-data (~> 3.2015)
mime-types-data (3.2018.0812) mime-types-data (3.2018.0812)
mimemagic (0.3.2)
mimemagic (0.3.3)
mini_mime (1.0.1) mini_mime (1.0.1)
mini_portile2 (2.4.0) mini_portile2 (2.4.0)
minitest (5.11.3) minitest (5.11.3)
msgpack (1.2.4)
msgpack (1.2.9)
multi_json (1.13.1) multi_json (1.13.1)
multipart-post (2.0.0) multipart-post (2.0.0)
necromancer (0.4.0) necromancer (0.4.0)
@@ -354,7 +355,7 @@ GEM
net-ssh (>= 2.6.5) net-ssh (>= 2.6.5)
net-ssh (5.0.2) net-ssh (5.0.2)
nio4r (2.3.1) nio4r (2.3.1)
nokogiri (1.10.1)
nokogiri (1.10.2)
mini_portile2 (~> 2.4.0) mini_portile2 (~> 2.4.0)
nokogumbo (2.0.0) nokogumbo (2.0.0)
nokogiri (~> 1.8, >= 1.8.4) nokogiri (~> 1.8, >= 1.8.4)
@@ -363,7 +364,7 @@ GEM
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.0.2)
sidekiq (>= 3.5) sidekiq (>= 3.5)
statsd-ruby (~> 1.4, >= 1.4.0) statsd-ruby (~> 1.4, >= 1.4.0)
oj (3.7.7)
oj (3.7.11)
omniauth (1.9.0) omniauth (1.9.0)
hashie (>= 3.4.6, < 3.7.0) hashie (>= 3.4.6, < 3.7.0)
rack (>= 1.6.2, < 3) rack (>= 1.6.2, < 3)
@@ -389,10 +390,10 @@ GEM
paperclip-av-transcoder (0.6.4) paperclip-av-transcoder (0.6.4)
av (~> 0.9.0) av (~> 0.9.0)
paperclip (>= 2.5.2) paperclip (>= 2.5.2)
parallel (1.12.1)
parallel_tests (2.27.1)
parallel (1.17.0)
parallel_tests (2.28.0)
parallel parallel
parser (2.6.0.0)
parser (2.6.2.0)
ast (~> 2.4.0) ast (~> 2.4.0)
pastel (0.7.2) pastel (0.7.2)
equatable (~> 0.5.0) equatable (~> 0.5.0)
@@ -400,8 +401,7 @@ GEM
pg (1.1.4) pg (1.1.4)
pghero (2.2.0) pghero (2.2.0)
activerecord activerecord
pkg-config (1.3.2)
powerpack (0.1.2)
pkg-config (1.3.7)
premailer (1.11.1) premailer (1.11.1)
addressable addressable
css_parser (>= 1.6.0) css_parser (>= 1.6.0)
@@ -413,38 +413,39 @@ GEM
pry (0.12.2) pry (0.12.2)
coderay (~> 1.1.0) coderay (~> 1.1.0)
method_source (~> 0.9.0) method_source (~> 0.9.0)
pry-byebug (3.6.0)
byebug (~> 10.0)
pry-byebug (3.7.0)
byebug (~> 11.0)
pry (~> 0.10) pry (~> 0.10)
pry-rails (0.3.9) pry-rails (0.3.9)
pry (>= 0.10.4) pry (>= 0.10.4)
psych (3.1.0)
public_suffix (3.0.3) public_suffix (3.0.3)
puma (3.12.0)
pundit (2.0.0)
puma (3.12.1)
pundit (2.0.1)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
raabro (1.1.6) raabro (1.1.6)
rack (2.0.6)
rack (2.0.7)
rack-attack (5.4.2) rack-attack (5.4.2)
rack (>= 1.0, < 3) rack (>= 1.0, < 3)
rack-cors (1.0.2)
rack-cors (1.0.3)
rack-protection (2.0.5) rack-protection (2.0.5)
rack rack
rack-proxy (0.6.4)
rack-proxy (0.6.5)
rack rack
rack-test (1.1.0) rack-test (1.1.0)
rack (>= 1.0, < 3) rack (>= 1.0, < 3)
rails (5.2.2)
actioncable (= 5.2.2)
actionmailer (= 5.2.2)
actionpack (= 5.2.2)
actionview (= 5.2.2)
activejob (= 5.2.2)
activemodel (= 5.2.2)
activerecord (= 5.2.2)
activestorage (= 5.2.2)
activesupport (= 5.2.2)
rails (5.2.3)
actioncable (= 5.2.3)
actionmailer (= 5.2.3)
actionpack (= 5.2.3)
actionview (= 5.2.3)
activejob (= 5.2.3)
activemodel (= 5.2.3)
activerecord (= 5.2.3)
activestorage (= 5.2.3)
activesupport (= 5.2.3)
bundler (>= 1.3.0) bundler (>= 1.3.0)
railties (= 5.2.2)
railties (= 5.2.3)
sprockets-rails (>= 2.0.0) sprockets-rails (>= 2.0.0)
rails-controller-testing (1.0.4) rails-controller-testing (1.0.4)
actionpack (>= 5.0.1.x) actionpack (>= 5.0.1.x)
@@ -455,14 +456,14 @@ GEM
nokogiri (>= 1.6) nokogiri (>= 1.6)
rails-html-sanitizer (1.0.4) rails-html-sanitizer (1.0.4)
loofah (~> 2.2, >= 2.2.2) loofah (~> 2.2, >= 2.2.2)
rails-i18n (5.1.2)
rails-i18n (5.1.3)
i18n (>= 0.7, < 2) i18n (>= 0.7, < 2)
railties (>= 5.0, < 6) railties (>= 5.0, < 6)
rails-settings-cached (0.6.6) rails-settings-cached (0.6.6)
rails (>= 4.2.0) rails (>= 4.2.0)
railties (5.2.2)
actionpack (= 5.2.2)
activesupport (= 5.2.2)
railties (5.2.3)
actionpack (= 5.2.3)
activesupport (= 5.2.3)
method_source method_source
rake (>= 0.8.7) rake (>= 0.8.7)
thor (>= 0.19.0, < 2.0) thor (>= 0.19.0, < 2.0)
@@ -498,9 +499,9 @@ GEM
regexp_parser (1.3.0) regexp_parser (1.3.0)
request_store (1.4.1) request_store (1.4.1)
rack (>= 1.4) rack (>= 1.4)
responders (2.4.0)
actionpack (>= 4.2.0, < 5.3)
railties (>= 4.2.0, < 5.3)
responders (2.4.1)
actionpack (>= 4.2.0, < 6.0)
railties (>= 4.2.0, < 6.0)
rotp (2.1.2) rotp (2.1.2)
rpam2 (4.0.2) rpam2 (4.0.2)
rqrcode (0.10.1) rqrcode (0.10.1)
@@ -513,7 +514,7 @@ GEM
rspec-mocks (3.8.0) rspec-mocks (3.8.0)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.8.0) rspec-support (~> 3.8.0)
rspec-rails (3.8.1)
rspec-rails (3.8.2)
actionpack (>= 3.0) actionpack (>= 3.0)
activesupport (>= 3.0) activesupport (>= 3.0)
railties (>= 3.0) railties (>= 3.0)
@@ -525,14 +526,14 @@ GEM
rspec-core (~> 3.0, >= 3.0.0) rspec-core (~> 3.0, >= 3.0.0)
sidekiq (>= 2.4.0) sidekiq (>= 2.4.0)
rspec-support (3.8.0) rspec-support (3.8.0)
rubocop (0.63.0)
rubocop (0.67.1)
jaro_winkler (~> 1.5.1) jaro_winkler (~> 1.5.1)
parallel (~> 1.10) parallel (~> 1.10)
parser (>= 2.5, != 2.5.1.1) parser (>= 2.5, != 2.5.1.1)
powerpack (~> 0.1)
psych (>= 3.1.0)
rainbow (>= 2.2.2, < 4.0) rainbow (>= 2.2.2, < 4.0)
ruby-progressbar (~> 1.7) ruby-progressbar (~> 1.7)
unicode-display_width (~> 1.4.0)
unicode-display_width (>= 1.4.0, < 1.6)
ruby-progressbar (1.10.0) ruby-progressbar (1.10.0)
ruby-saml (1.9.0) ruby-saml (1.9.0)
nokogiri (>= 1.5.10) nokogiri (>= 1.5.10)
@@ -563,9 +564,9 @@ GEM
rufus-scheduler (~> 3.2) rufus-scheduler (~> 3.2)
sidekiq (>= 3) sidekiq (>= 3)
tilt (>= 1.4.0) tilt (>= 1.4.0)
sidekiq-unique-jobs (6.0.8)
sidekiq-unique-jobs (6.0.12)
concurrent-ruby (~> 1.0, >= 1.0.5) concurrent-ruby (~> 1.0, >= 1.0.5)
sidekiq (>= 4.0, < 6.0)
sidekiq (>= 4.0, < 7.0)
thor (~> 0) thor (~> 0)
simple-navigation (4.0.5) simple-navigation (4.0.5)
activesupport (>= 2.3.2) activesupport (>= 2.3.2)
@@ -594,14 +595,14 @@ GEM
multi_json (~> 1.8) multi_json (~> 1.8)
strong_migrations (0.3.1) strong_migrations (0.3.1)
activerecord (>= 3.2.0) activerecord (>= 3.2.0)
temple (0.8.0)
temple (0.8.1)
terminal-table (1.8.0) terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1) unicode-display_width (~> 1.1, >= 1.1.1)
terrapin (0.6.0) terrapin (0.6.0)
climate_control (>= 0.0.3, < 1.0) climate_control (>= 0.0.3, < 1.0)
thor (0.20.3) thor (0.20.3)
thread_safe (0.3.6) thread_safe (0.3.6)
tilt (2.0.8)
tilt (2.0.9)
timers (4.2.0) timers (4.2.0)
tty-color (0.4.3) tty-color (0.4.3)
tty-command (0.8.2) tty-command (0.8.2)
@@ -622,24 +623,24 @@ GEM
unf (~> 0.1.0) unf (~> 0.1.0)
tzinfo (1.2.5) tzinfo (1.2.5)
thread_safe (~> 0.1) thread_safe (~> 0.1)
tzinfo-data (1.2018.9)
tzinfo-data (1.2019.1)
tzinfo (>= 1.0.0) tzinfo (>= 1.0.0)
unf (0.1.4) unf (0.1.4)
unf_ext unf_ext
unf_ext (0.0.7.5) unf_ext (0.0.7.5)
unicode-display_width (1.4.1)
unicode-display_width (1.5.0)
uniform_notifier (1.12.1) uniform_notifier (1.12.1)
warden (1.2.7)
rack (>= 1.0)
warden (1.2.8)
rack (>= 2.0.6)
webmock (3.5.1) webmock (3.5.1)
addressable (>= 2.3.6) addressable (>= 2.3.6)
crack (>= 0.3.2) crack (>= 0.3.2)
hashdiff hashdiff
webpacker (3.5.5)
webpacker (4.0.2)
activesupport (>= 4.2) activesupport (>= 4.2)
rack-proxy (>= 0.6.1) rack-proxy (>= 0.6.1)
railties (>= 4.2) railties (>= 4.2)
webpush (0.3.6)
webpush (0.3.7)
hkdf (~> 0.2) hkdf (~> 0.2)
jwt (~> 2.0) jwt (~> 2.0)
websocket-driver (0.7.0) websocket-driver (0.7.0)
@@ -654,14 +655,14 @@ PLATFORMS


DEPENDENCIES DEPENDENCIES
active_model_serializers (~> 0.10) active_model_serializers (~> 0.10)
active_record_query_trace (~> 1.5)
addressable (~> 2.5)
active_record_query_trace (~> 1.6)
addressable (~> 2.6)
annotate (~> 2.7) annotate (~> 2.7)
aws-sdk-s3 (~> 1.30)
aws-sdk-s3 (~> 1.36)
better_errors (~> 2.5) better_errors (~> 2.5)
binding_of_caller (~> 0.7) binding_of_caller (~> 0.7)
bootsnap (~> 1.3)
brakeman (~> 4.4)
bootsnap (~> 1.4)
brakeman (~> 4.5)
browser browser
bullet (~> 5.9) bullet (~> 5.9)
bundler-audit (~> 0.6) bundler-audit (~> 0.6)
@@ -669,18 +670,18 @@ DEPENDENCIES
capistrano-rails (~> 1.4) capistrano-rails (~> 1.4)
capistrano-rbenv (~> 2.1) capistrano-rbenv (~> 2.1)
capistrano-yarn (~> 2.0) capistrano-yarn (~> 2.0)
capybara (~> 3.12)
capybara (~> 3.16)
charlock_holmes (~> 0.7.6) charlock_holmes (~> 0.7.6)
chewy (~> 5.0) chewy (~> 5.0)
cld3 (~> 3.2.3) cld3 (~> 3.2.3)
climate_control (~> 0.2) climate_control (~> 0.2)
concurrent-ruby concurrent-ruby
derailed_benchmarks derailed_benchmarks
devise (~> 4.5)
devise (~> 4.6)
devise-two-factor (~> 3.0) devise-two-factor (~> 3.0)
devise_pam_authenticatable2 (~> 9.2) devise_pam_authenticatable2 (~> 9.2)
doorkeeper (~> 5.0) doorkeeper (~> 5.0)
dotenv-rails (~> 2.6)
dotenv-rails (~> 2.7)
fabrication (~> 2.20) fabrication (~> 2.20)
faker (~> 1.9) faker (~> 1.9)
fast_blank (~> 1.0) fast_blank (~> 1.0)
@@ -709,7 +710,7 @@ DEPENDENCIES
makara (~> 0.4) makara (~> 0.4)
mario-redis-lock (~> 1.2) mario-redis-lock (~> 1.2)
memory_profiler memory_profiler
microformats (~> 4.0)
microformats (~> 4.1)
mime-types (~> 3.2) mime-types (~> 3.2)
net-ldap (~> 0.10) net-ldap (~> 0.10)
nokogiri (~> 1.10) nokogiri (~> 1.10)
@@ -722,20 +723,20 @@ DEPENDENCIES
ox (~> 2.10) ox (~> 2.10)
paperclip (~> 6.0) paperclip (~> 6.0)
paperclip-av-transcoder (~> 0.6) paperclip-av-transcoder (~> 0.6)
parallel_tests (~> 2.27)
parallel_tests (~> 2.28)
pg (~> 1.1) pg (~> 1.1)
pghero (~> 2.2) pghero (~> 2.2)
pkg-config (~> 1.3) pkg-config (~> 1.3)
posix-spawn! posix-spawn!
premailer-rails premailer-rails
private_address_check (~> 0.5) private_address_check (~> 0.5)
pry-byebug (~> 3.6)
pry-byebug (~> 3.7)
pry-rails (~> 0.3) pry-rails (~> 0.3)
puma (~> 3.12) puma (~> 3.12)
pundit (~> 2.0) pundit (~> 2.0)
rack-attack (~> 5.4) rack-attack (~> 5.4)
rack-cors (~> 1.0) rack-cors (~> 1.0)
rails (~> 5.2.2)
rails (~> 5.2.3)
rails-controller-testing (~> 1.0) rails-controller-testing (~> 1.0)
rails-i18n (~> 5.1) rails-i18n (~> 5.1)
rails-settings-cached (~> 0.6) rails-settings-cached (~> 0.6)
@@ -746,7 +747,7 @@ DEPENDENCIES
rqrcode (~> 0.10) rqrcode (~> 0.10)
rspec-rails (~> 3.8) rspec-rails (~> 3.8)
rspec-sidekiq (~> 3.0) rspec-sidekiq (~> 3.0)
rubocop (~> 0.63)
rubocop (~> 0.67)
sanitize (~> 5.0) sanitize (~> 5.0)
scss_lint (~> 0.57) scss_lint (~> 0.57)
sidekiq (~> 5.2) sidekiq (~> 5.2)
@@ -765,13 +766,13 @@ DEPENDENCIES
tty-command (~> 0.8) tty-command (~> 0.8)
tty-prompt (~> 0.18) tty-prompt (~> 0.18)
twitter-text (~> 1.14) twitter-text (~> 1.14)
tzinfo-data (~> 1.2018)
tzinfo-data (~> 1.2019)
webmock (~> 3.5) webmock (~> 3.5)
webpacker (~> 3.5)
webpacker (~> 4.0)
webpush webpush


RUBY VERSION RUBY VERSION
ruby 2.6.0p0
ruby 2.6.1p33


BUNDLED WITH BUNDLED WITH
1.17.3 1.17.3

+ 3
- 3
README.md View File

@@ -21,7 +21,7 @@ Click below to **learn more** in a video:


[youtube_demo]: https://www.youtube.com/watch?v=IPSbNdBmWKE [youtube_demo]: https://www.youtube.com/watch?v=IPSbNdBmWKE


## Navigation
## Navigation


- [Project homepage 🐘](https://joinmastodon.org) - [Project homepage 🐘](https://joinmastodon.org)
- [Support the development via Patreon][patreon] - [Support the development via Patreon][patreon]
@@ -80,13 +80,13 @@ A **Vagrant** configuration is included for development purposes.


Mastodon is **free, open source software** licensed under **AGPLv3**. Mastodon is **free, open source software** licensed under **AGPLv3**.


You can open issues for bugs you've found or features you think are missing. You can also submit pull requests to this repository, or submit translations using Weblate. To get started, take a look at [CONTRIBUTING.md](CONTRIBUTING.md)
You can open issues for bugs you've found or features you think are missing. You can also submit pull requests to this repository, or submit translations using Weblate. To get started, take a look at [CONTRIBUTING.md](CONTRIBUTING.md). If your contributions are accepted into Mastodon, you can request to be paid through [our OpenCollective](https://opencollective.com/mastodon).


**IRC channel**: #mastodon on irc.freenode.net **IRC channel**: #mastodon on irc.freenode.net


## License ## License


Copyright (C) 2016-2018 Eugen Rochko & other Mastodon contributors (see [AUTHORS.md](AUTHORS.md))
Copyright (C) 2016-2019 Eugen Rochko & other Mastodon contributors (see [AUTHORS.md](AUTHORS.md))


This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.




+ 12
- 1
Vagrantfile View File

@@ -44,7 +44,18 @@ sudo apt-get install \


# Install rvm # Install rvm
read RUBY_VERSION < .ruby-version read RUBY_VERSION < .ruby-version
gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB

gpg_command="gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB"
$($gpg_command)
if [ $? -ne 0 ];then
echo "GPG command failed, This prevented RVM from installing."
echo "Retrying once..." && $($gpg_command)
if [ $? -ne 0 ];then
echo "GPG failed for the second time, please ensure network connectivity."
echo "Exiting..." && exit 1
fi
fi

curl -sSL https://raw.githubusercontent.com/rvm/rvm/stable/binscripts/rvm-installer | bash -s stable --ruby=$RUBY_VERSION curl -sSL https://raw.githubusercontent.com/rvm/rvm/stable/binscripts/rvm-installer | bash -s stable --ruby=$RUBY_VERSION
source /home/vagrant/.rvm/scripts/rvm source /home/vagrant/.rvm/scripts/rvm




+ 3
- 3
app/chewy/statuses_index.rb View File

@@ -31,7 +31,7 @@ class StatusesIndex < Chewy::Index
}, },
} }


define_type ::Status.unscoped.without_reblogs do
define_type ::Status.unscoped.without_reblogs.includes(:media_attachments) do
crutch :mentions do |collection| crutch :mentions do |collection|
data = ::Mention.where(status_id: collection.map(&:id)).pluck(:status_id, :account_id) data = ::Mention.where(status_id: collection.map(&:id)).pluck(:status_id, :account_id)
data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) } data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }
@@ -48,14 +48,14 @@ class StatusesIndex < Chewy::Index
end end


root date_detection: false do root date_detection: false do
field :id, type: 'long'
field :account_id, type: 'long' field :account_id, type: 'long'


field :text, type: 'text', value: ->(status) { [status.spoiler_text, Formatter.instance.plaintext(status)].join("\n\n") } do
field :text, type: 'text', value: ->(status) { [status.spoiler_text, Formatter.instance.plaintext(status)].concat(status.media_attachments.map(&:description)).join("\n\n") } do
field :stemmed, type: 'text', analyzer: 'content' field :stemmed, type: 'text', analyzer: 'content'
end end


field :searchable_by, type: 'long', value: ->(status, crutches) { status.searchable_by(crutches) } field :searchable_by, type: 'long', value: ->(status, crutches) { status.searchable_by(crutches) }
field :created_at, type: 'date'
end end
end end
end end

+ 9
- 21
app/controllers/about_controller.rb View File

@@ -1,26 +1,25 @@
# frozen_string_literal: true # frozen_string_literal: true


class AboutController < ApplicationController class AboutController < ApplicationController
before_action :set_body_classes
layout 'public'

before_action :set_instance_presenter, only: [:show, :more, :terms] before_action :set_instance_presenter, only: [:show, :more, :terms]


def show def show
serializable_resource = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(initial_state_params), serializer: InitialStateSerializer)
@initial_state_json = serializable_resource.to_json
@hide_navbar = true
end end


def more
render layout: 'public'
end
def more; end


def terms
render layout: 'public'
end
def terms; end


private private


def new_user def new_user
User.new.tap(&:build_account)
User.new.tap do |user|
user.build_account
user.build_invite_request
end
end end


helper_method :new_user helper_method :new_user
@@ -28,15 +27,4 @@ class AboutController < ApplicationController
def set_instance_presenter def set_instance_presenter
@instance_presenter = InstancePresenter.new @instance_presenter = InstancePresenter.new
end end

def set_body_classes
@body_classes = 'with-modals'
end

def initial_state_params
{
settings: { known_fediverse: Setting.show_known_fediverse_at_about_page },
token: current_session&.token,
}
end
end end

+ 28
- 6
app/controllers/accounts_controller.rb View File

@@ -10,6 +10,8 @@ class AccountsController < ApplicationController
def show def show
respond_to do |format| respond_to do |format|
format.html do format.html do
mark_cacheable! unless user_signed_in?

@body_classes = 'with-modals' @body_classes = 'with-modals'
@pinned_statuses = [] @pinned_statuses = []
@endorsed_accounts = @account.endorsed_accounts.to_a.sample(4) @endorsed_accounts = @account.endorsed_accounts.to_a.sample(4)
@@ -30,17 +32,21 @@ class AccountsController < ApplicationController
end end


format.atom do format.atom do
mark_cacheable!

@entries = @account.stream_entries.where(hidden: false).with_includes.paginate_by_max_id(PAGE_SIZE, params[:max_id], params[:since_id]) @entries = @account.stream_entries.where(hidden: false).with_includes.paginate_by_max_id(PAGE_SIZE, params[:max_id], params[:since_id])
render xml: OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.feed(@account, @entries.reject { |entry| entry.status.nil? })) render xml: OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.feed(@account, @entries.reject { |entry| entry.status.nil? }))
end end


format.rss do format.rss do
mark_cacheable!

@statuses = cache_collection(default_statuses.without_reblogs.without_replies.limit(PAGE_SIZE), Status) @statuses = cache_collection(default_statuses.without_reblogs.without_replies.limit(PAGE_SIZE), Status)
render xml: RSS::AccountSerializer.render(@account, @statuses) render xml: RSS::AccountSerializer.render(@account, @statuses)
end end


format.json do format.json do
skip_session!
mark_cacheable!


render_cached_json(['activitypub', 'actor', @account], content_type: 'application/activity+json') do render_cached_json(['activitypub', 'actor', @account], content_type: 'application/activity+json') do
ActiveModelSerializers::SerializableResource.new(@account, serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter) ActiveModelSerializers::SerializableResource.new(@account, serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter)
@@ -52,11 +58,12 @@ class AccountsController < ApplicationController
private private


def show_pinned_statuses? def show_pinned_statuses?
[replies_requested?, media_requested?, params[:max_id].present?, params[:min_id].present?].none?
[replies_requested?, media_requested?, tag_requested?, params[:max_id].present?, params[:min_id].present?].none?
end end


def filtered_statuses def filtered_statuses
default_statuses.tap do |statuses| default_statuses.tap do |statuses|
statuses.merge!(hashtag_scope) if tag_requested?
statuses.merge!(only_media_scope) if media_requested? statuses.merge!(only_media_scope) if media_requested?
statuses.merge!(no_replies_scope) unless replies_requested? statuses.merge!(no_replies_scope) unless replies_requested?
end end
@@ -78,12 +85,21 @@ class AccountsController < ApplicationController
Status.without_replies Status.without_replies
end end


def set_account
@account = Account.find_local!(params[:username])
def hashtag_scope
tag = Tag.find_normalized(params[:tag])

if tag
Status.tagged_with(tag.id)
else
Status.none
end
end

def username_param
params[:username]
end end


def older_url def older_url
::Rails.logger.info("older: max_id #{@statuses.last.id}, url #{pagination_url(max_id: @statuses.last.id)}")
pagination_url(max_id: @statuses.last.id) pagination_url(max_id: @statuses.last.id)
end end


@@ -92,7 +108,9 @@ class AccountsController < ApplicationController
end end


def pagination_url(max_id: nil, min_id: nil) def pagination_url(max_id: nil, min_id: nil)
if media_requested?
if tag_requested?
short_account_tag_url(@account, params[:tag], max_id: max_id, min_id: min_id)
elsif media_requested?
short_account_media_url(@account, max_id: max_id, min_id: min_id) short_account_media_url(@account, max_id: max_id, min_id: min_id)
elsif replies_requested? elsif replies_requested?
short_account_with_replies_url(@account, max_id: max_id, min_id: min_id) short_account_with_replies_url(@account, max_id: max_id, min_id: min_id)
@@ -109,6 +127,10 @@ class AccountsController < ApplicationController
request.path.ends_with?('/with_replies') request.path.ends_with?('/with_replies')
end end


def tag_requested?
request.path.ends_with?(Addressable::URI.parse("/tagged/#{params[:tag]}").normalize)
end

def filtered_status_page(params) def filtered_status_page(params)
if params[:min_id].present? if params[:min_id].present?
filtered_statuses.paginate_by_min_id(PAGE_SIZE, params[:min_id]).reverse filtered_statuses.paginate_by_min_id(PAGE_SIZE, params[:min_id]).reverse


+ 11
- 5
app/controllers/activitypub/collections_controller.rb View File

@@ -6,13 +6,19 @@ class ActivityPub::CollectionsController < Api::BaseController
before_action :set_account before_action :set_account
before_action :set_size before_action :set_size
before_action :set_statuses before_action :set_statuses
before_action :set_cache_headers


def show def show
render json: collection_presenter,
serializer: ActivityPub::CollectionSerializer,
adapter: ActivityPub::Adapter,
content_type: 'application/activity+json',
skip_activities: true
skip_session!

render_cached_json(['activitypub', 'collection', @account, params[:id]], content_type: 'application/activity+json') do
ActiveModelSerializers::SerializableResource.new(
collection_presenter,
serializer: ActivityPub::CollectionSerializer,
adapter: ActivityPub::Adapter,
skip_activities: true
)
end
end end


private private


+ 16
- 3
app/controllers/activitypub/inboxes_controller.rb View File

@@ -2,11 +2,14 @@


class ActivityPub::InboxesController < Api::BaseController class ActivityPub::InboxesController < Api::BaseController
include SignatureVerification include SignatureVerification
include JsonLdHelper


before_action :set_account before_action :set_account


def create def create
if signed_request_account
if unknown_deleted_account?
head 202
elsif signed_request_account
upgrade_account upgrade_account
process_payload process_payload
head 202 head 202
@@ -17,12 +20,22 @@ class ActivityPub::InboxesController < Api::BaseController


private private


def unknown_deleted_account?
json = Oj.load(body, mode: :strict)
json['type'] == 'Delete' && json['actor'].present? && json['actor'] == value_or_id(json['object']) && !Account.where(uri: json['actor']).exists?
rescue Oj::ParseError
false
end

def set_account def set_account
@account = Account.find_local!(params[:account_username]) if params[:account_username] @account = Account.find_local!(params[:account_username]) if params[:account_username]
end end


def body def body
@body ||= request.body.read
return @body if defined?(@body)
@body = request.body.read.force_encoding('UTF-8')
request.body.rewind if request.body.respond_to?(:rewind)
@body
end end


def upgrade_account def upgrade_account
@@ -36,6 +49,6 @@ class ActivityPub::InboxesController < Api::BaseController
end end


def process_payload def process_payload
ActivityPub::ProcessingWorker.perform_async(signed_request_account.id, body.force_encoding('UTF-8'), @account&.id)
ActivityPub::ProcessingWorker.perform_async(signed_request_account.id, body, @account&.id)
end end
end end

+ 6
- 0
app/controllers/activitypub/outboxes_controller.rb View File

@@ -7,8 +7,14 @@ class ActivityPub::OutboxesController < Api::BaseController


before_action :set_account before_action :set_account
before_action :set_statuses before_action :set_statuses
before_action :set_cache_headers


def show def show
unless page_requested?
skip_session!
expires_in 1.minute, public: true
end

render json: outbox_presenter, serializer: ActivityPub::OutboxSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json' render json: outbox_presenter, serializer: ActivityPub::OutboxSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
end end




+ 15
- 2
app/controllers/admin/accounts_controller.rb View File

@@ -2,9 +2,9 @@


module Admin module Admin
class AccountsController < BaseController class AccountsController < BaseController
before_action :set_account, only: [:show, :subscribe, :unsubscribe, :redownload, :remove_avatar, :remove_header, :enable, :unsilence, :unsuspend, :memorialize]
before_action :set_account, only: [:show, :subscribe, :unsubscribe, :redownload, :remove_avatar, :remove_header, :enable, :unsilence, :unsuspend, :memorialize, :approve, :reject]
before_action :require_remote_account!, only: [:subscribe, :unsubscribe, :redownload] before_action :require_remote_account!, only: [:subscribe, :unsubscribe, :redownload]
before_action :require_local_account!, only: [:enable, :memorialize]
before_action :require_local_account!, only: [:enable, :memorialize, :approve, :reject]


def index def index
authorize :account, :index? authorize :account, :index?
@@ -45,6 +45,18 @@ module Admin
redirect_to admin_account_path(@account.id) redirect_to admin_account_path(@account.id)
end end


def approve
authorize @account.user, :approve?
@account.user.approve!
redirect_to admin_accounts_path(pending: '1')
end

def reject
authorize @account.user, :reject?
SuspendAccountService.new.call(@account, including_user: true, destroy: true, skip_distribution: true)
redirect_to admin_accounts_path(pending: '1')
end

def unsilence def unsilence
authorize @account, :unsilence? authorize @account, :unsilence?
@account.unsilence! @account.unsilence!
@@ -114,6 +126,7 @@ module Admin
:remote, :remote,
:by_domain, :by_domain,
:active, :active,
:pending,
:silenced, :silenced,
:suspended, :suspended,
:username, :username,


+ 3
- 0
app/controllers/admin/custom_emojis_controller.rb View File

@@ -5,6 +5,9 @@ module Admin
before_action :set_custom_emoji, except: [:index, :new, :create] before_action :set_custom_emoji, except: [:index, :new, :create]
before_action :set_filter_params before_action :set_filter_params


include ObfuscateFilename
obfuscate_filename [:custom_emoji, :image]

def index def index
authorize :custom_emoji, :index? authorize :custom_emoji, :index?
@custom_emojis = filtered_custom_emojis.eager_load(:local_counterpart).page(params[:page]) @custom_emojis = filtered_custom_emojis.eager_load(:local_counterpart).page(params[:page])


+ 2
- 1
app/controllers/admin/dashboard_controller.rb View File

@@ -10,7 +10,7 @@ module Admin
@interactions_week = Redis.current.get("activity:interactions:#{current_week}") || 0 @interactions_week = Redis.current.get("activity:interactions:#{current_week}") || 0
@relay_enabled = Relay.enabled.exists? @relay_enabled = Relay.enabled.exists?
@single_user_mode = Rails.configuration.x.single_user_mode @single_user_mode = Rails.configuration.x.single_user_mode
@registrations_enabled = Setting.open_registrations
@registrations_enabled = Setting.registrations_mode != 'none'
@deletions_enabled = Setting.open_deletion @deletions_enabled = Setting.open_deletion
@invites_enabled = Setting.min_invite_role == 'user' @invites_enabled = Setting.min_invite_role == 'user'
@search_enabled = Chewy.enabled? @search_enabled = Chewy.enabled?
@@ -29,6 +29,7 @@ module Admin
@hidden_service = ENV['ALLOW_ACCESS_TO_HIDDEN_SERVICE'] == 'true' @hidden_service = ENV['ALLOW_ACCESS_TO_HIDDEN_SERVICE'] == 'true'
@trending_hashtags = TrendingTags.get(7) @trending_hashtags = TrendingTags.get(7)
@profile_directory = Setting.profile_directory @profile_directory = Setting.profile_directory
@timeline_preview = Setting.timeline_preview
end end


private private


+ 1
- 1
app/controllers/admin/instances_controller.rb View File

@@ -38,7 +38,7 @@ module Admin
end end


def filter_params def filter_params
params.permit(:limited)
params.permit(:limited, :by_domain)
end end
end end
end end

+ 52
- 0
app/controllers/admin/pending_accounts_controller.rb View File

@@ -0,0 +1,52 @@
# frozen_string_literal: true

module Admin
class PendingAccountsController < BaseController
before_action :set_accounts, only: :index

def index
@form = Form::AccountBatch.new
end

def batch
@form = Form::AccountBatch.new(form_account_batch_params.merge(current_account: current_account, action: action_from_button))
@form.save
rescue ActionController::ParameterMissing
flash[:alert] = I18n.t('admin.accounts.no_account_selected')
ensure
redirect_to admin_pending_accounts_path(current_params)
end

def approve_all
Form::AccountBatch.new(current_account: current_account, account_ids: User.pending.pluck(:account_id), action: 'approve').save
redirect_to admin_pending_accounts_path(current_params)
end

def reject_all
Form::AccountBatch.new(current_account: current_account, account_ids: User.pending.pluck(:account_id), action: 'reject').save
redirect_to admin_pending_accounts_path(current_params)
end

private

def set_accounts
@accounts = Account.joins(:user).merge(User.pending.recent).includes(user: :invite_request).page(params[:page])
end

def form_account_batch_params
params.require(:form_account_batch).permit(:action, account_ids: [])
end

def action_from_button
if params[:approve]
'approve'
elsif params[:reject]
'reject'
end
end

def current_params
params.slice(:page).permit(:page)
end
end
end

+ 4
- 0
app/controllers/admin/reported_statuses_controller.rb View File

@@ -11,6 +11,10 @@ module Admin
flash[:alert] = I18n.t('admin.statuses.failed_to_execute') unless @form.save flash[:alert] = I18n.t('admin.statuses.failed_to_execute') unless @form.save


redirect_to admin_report_path(@report) redirect_to admin_report_path(@report)
rescue ActionController::ParameterMissing
flash[:alert] = I18n.t('admin.statuses.no_status_selected')

redirect_to admin_report_path(@report)
end end


private private


+ 9
- 65
app/controllers/admin/settings_controller.rb View File

@@ -2,85 +2,29 @@


module Admin module Admin
class SettingsController < BaseController class SettingsController < BaseController
ADMIN_SETTINGS = %w(
site_contact_username
site_contact_email
site_title
site_short_description
site_description
site_extended_description
site_terms
open_registrations
closed_registrations_message
open_deletion
timeline_preview
show_staff_badge
bootstrap_timeline_accounts
theme
thumbnail
hero
mascot
min_invite_role
activity_api_enabled
peers_api_enabled
show_known_fediverse_at_about_page
preview_sensitive_media
custom_css
profile_directory
).freeze

BOOLEAN_SETTINGS = %w(
open_registrations
open_deletion
timeline_preview
show_staff_badge
activity_api_enabled
peers_api_enabled
show_known_fediverse_at_about_page
preview_sensitive_media
profile_directory
).freeze

UPLOAD_SETTINGS = %w(
thumbnail
hero
mascot
).freeze

def edit def edit
authorize :settings, :show? authorize :settings, :show?

@admin_settings = Form::AdminSettings.new @admin_settings = Form::AdminSettings.new
end end


def update def update
authorize :settings, :update? authorize :settings, :update?


settings_params.each do |key, value|
if UPLOAD_SETTINGS.include?(key)
upload = SiteUpload.where(var: key).first_or_initialize(var: key)
upload.update(file: value)
else
setting = Setting.where(var: key).first_or_initialize(var: key)
setting.update(value: value_for_update(key, value))
end
end
@admin_settings = Form::AdminSettings.new(settings_params)


flash[:notice] = I18n.t('generic.changes_saved_msg')
redirect_to edit_admin_settings_path
if @admin_settings.save
flash[:notice] = I18n.t('generic.changes_saved_msg')
redirect_to edit_admin_settings_path
else
render :edit
end
end end


private private


def settings_params def settings_params
params.require(:form_admin_settings).permit(ADMIN_SETTINGS)
end

def value_for_update(key, value)
if BOOLEAN_SETTINGS.include?(key)
value == '1'
else
value
end
params.require(:form_admin_settings).permit(*Form::AdminSettings::KEYS)
end end
end end
end end

+ 3
- 1
app/controllers/api/base_controller.rb View File

@@ -73,7 +73,9 @@ class Api::BaseController < ApplicationController
elsif current_user.disabled? elsif current_user.disabled?
render json: { error: 'Your login is currently disabled' }, status: 403 render json: { error: 'Your login is currently disabled' }, status: 403
elsif !current_user.confirmed? elsif !current_user.confirmed?
render json: { error: 'Email confirmation is not completed' }, status: 403
render json: { error: 'Your login is missing a confirmed e-mail address' }, status: 403
elsif !current_user.approved?
render json: { error: 'Your login is currently pending approval' }, status: 403
else else
set_user_activity set_user_activity
end end


+ 30
- 0
app/controllers/api/proofs_controller.rb View File

@@ -0,0 +1,30 @@
# frozen_string_literal: true

class Api::ProofsController < Api::BaseController
before_action :set_account
before_action :set_provider
before_action :check_account_approval
before_action :check_account_suspension

def index
render json: @account, serializer: @provider.serializer_class
end

private

def set_provider
@provider = ProofProvider.find(params[:provider]) || raise(ActiveRecord::RecordNotFound)
end

def set_account
@account = Account.find_local!(params[:username])
end

def check_account_approval
not_found if @account.user_pending?
end

def check_account_suspension
gone if @account.suspended?
end
end

+ 5
- 1
app/controllers/api/v1/accounts/follower_accounts_controller.rb View File

@@ -19,11 +19,15 @@ class Api::V1::Accounts::FollowerAccountsController < Api::BaseController
end end


def load_accounts def load_accounts
return [] if @account.user_hides_network? && current_account.id != @account.id
return [] if hide_results?


default_accounts.merge(paginated_follows).to_a default_accounts.merge(paginated_follows).to_a
end end


def hide_results?
(@account.user_hides_network? && current_account.id != @account.id) || (current_account && @account.blocking?(current_account))
end

def default_accounts def default_accounts
Account.includes(:active_relationships, :account_stat).references(:active_relationships) Account.includes(:active_relationships, :account_stat).references(:active_relationships)
end end


+ 5
- 1
app/controllers/api/v1/accounts/following_accounts_controller.rb View File

@@ -19,11 +19,15 @@ class Api::V1::Accounts::FollowingAccountsController < Api::BaseController
end end


def load_accounts def load_accounts
return [] if @account.user_hides_network? && current_account.id != @account.id
return [] if hide_results?


default_accounts.merge(paginated_follows).to_a default_accounts.merge(paginated_follows).to_a
end end


def hide_results?
(@account.user_hides_network? && current_account.id != @account.id) || (current_account && @account.blocking?(current_account))
end

def default_accounts def default_accounts
Account.includes(:passive_relationships, :account_stat).references(:passive_relationships) Account.includes(:passive_relationships, :account_stat).references(:passive_relationships)
end end


+ 19
- 0
app/controllers/api/v1/accounts/identity_proofs_controller.rb View File

@@ -0,0 +1,19 @@
# frozen_string_literal: true

class Api::V1::Accounts::IdentityProofsController < Api::BaseController
before_action :require_user!
before_action :set_account

respond_to :json

def index
@proofs = @account.identity_proofs.active
render json: @proofs, each_serializer: REST::IdentityProofSerializer
end

private

def set_account
@account = Account.find(params[:account_id])
end
end

+ 3
- 2
app/controllers/api/v1/accounts/search_controller.rb View File

@@ -16,10 +16,11 @@ class Api::V1::Accounts::SearchController < Api::BaseController
def account_search def account_search
AccountSearchService.new.call( AccountSearchService.new.call(
params[:q], params[:q],
limit_param(DEFAULT_ACCOUNTS_LIMIT),
current_account, current_account,
limit: limit_param(DEFAULT_ACCOUNTS_LIMIT),
resolve: truthy_param?(:resolve), resolve: truthy_param?(:resolve),
following: truthy_param?(:following)
following: truthy_param?(:following),
offset: params[:offset]
) )
end end
end end

+ 14
- 3
app/controllers/api/v1/accounts/statuses_controller.rb View File

@@ -33,6 +33,7 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
statuses.merge!(only_media_scope) if truthy_param?(:only_media) statuses.merge!(only_media_scope) if truthy_param?(:only_media)
statuses.merge!(no_replies_scope) if truthy_param?(:exclude_replies) statuses.merge!(no_replies_scope) if truthy_param?(:exclude_replies)
statuses.merge!(no_reblogs_scope) if truthy_param?(:exclude_reblogs) statuses.merge!(no_reblogs_scope) if truthy_param?(:exclude_reblogs)
statuses.merge!(hashtag_scope) if params[:tagged].present?


statuses statuses
end end
@@ -50,9 +51,9 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
# Also, Avoid getting slow by not narrowing down by `statuses.account_id`. # Also, Avoid getting slow by not narrowing down by `statuses.account_id`.
# When narrowing down by `statuses.account_id`, `index_statuses_20180106` will be used # When narrowing down by `statuses.account_id`, `index_statuses_20180106` will be used
# and the table will be joined by `Merge Semi Join`, so the query will be slow. # and the table will be joined by `Merge Semi Join`, so the query will be slow.
Status.joins(:media_attachments).merge(@account.media_attachments).permitted_for(@account, current_account)
.paginate_by_max_id(limit_param(DEFAULT_STATUSES_LIMIT), params[:max_id], params[:since_id])
.reorder(id: :desc).distinct(:id).pluck(:id)
@account.statuses.joins(:media_attachments).merge(@account.media_attachments).permitted_for(@account, current_account)
.paginate_by_max_id(limit_param(DEFAULT_STATUSES_LIMIT), params[:max_id], params[:since_id])
.reorder(id: :desc).distinct(:id).pluck(:id)
end end


def pinned_scope def pinned_scope
@@ -67,6 +68,16 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
Status.without_reblogs Status.without_reblogs
end end


def hashtag_scope
tag = Tag.find_normalized(params[:tagged])

if tag
Status.tagged_with(tag.id)
else
Status.none
end
end

def pagination_params(core_params) def pagination_params(core_params)
params.slice(:limit, :only_media, :exclude_replies).permit(:limit, :only_media, :exclude_replies).merge(core_params) params.slice(:limit, :only_media, :exclude_replies).permit(:limit, :only_media, :exclude_replies).merge(core_params)
end end


+ 5
- 1
app/controllers/api/v1/accounts_controller.rb View File

@@ -80,6 +80,10 @@ class Api::V1::AccountsController < Api::BaseController
end end


def check_enabled_registrations def check_enabled_registrations
forbidden if single_user_mode? || !Setting.open_registrations
forbidden if single_user_mode? || !allowed_registrations?
end

def allowed_registrations?
Setting.registrations_mode != 'none'
end end
end end

+ 1
- 1
app/controllers/api/v1/apps/credentials_controller.rb View File

@@ -6,6 +6,6 @@ class Api::V1::Apps::CredentialsController < Api::BaseController
respond_to :json respond_to :json


def show def show
render json: doorkeeper_token.application, serializer: REST::StatusSerializer::ApplicationSerializer
render json: doorkeeper_token.application, serializer: REST::ApplicationSerializer, fields: %i(name website vapid_key)
end end
end end

+ 29
- 0
app/controllers/api/v1/polls/votes_controller.rb View File

@@ -0,0 +1,29 @@
# frozen_string_literal: true

class Api::V1::Polls::VotesController < Api::BaseController
include Authorization

before_action -> { doorkeeper_authorize! :write, :'write:statuses' }
before_action :require_user!
before_action :set_poll

respond_to :json

def create
VoteService.new.call(current_account, @poll, vote_params[:choices])
render json: @poll, serializer: REST::PollSerializer
end

private

def set_poll
@poll = Poll.attached.find(params[:poll_id])
authorize @poll.status, :show?
rescue Mastodon::NotPermittedError
raise ActiveRecord::RecordNotFound
end

def vote_params
params.permit(choices: [])
end
end

+ 13
- 0
app/controllers/api/v1/polls_controller.rb View File

@@ -0,0 +1,13 @@
# frozen_string_literal: true

class Api::V1::PollsController < Api::BaseController
before_action -> { authorize_if_got_token! :read, :'read:statuses' }, only: :show

respond_to :json

def show
@poll = Poll.attached.find(params[:id])
ActivityPub::FetchRemotePollService.new.call(@poll, current_account) if user_signed_in? && @poll.possibly_stale?
render json: @poll, serializer: REST::PollSerializer, include_results: true
end
end

+ 12
- 0
app/controllers/api/v1/preferences_controller.rb View File

@@ -0,0 +1,12 @@
# frozen_string_literal: true

class Api::V1::PreferencesController < Api::BaseController
before_action -> { doorkeeper_authorize! :read, :'read:accounts' }
before_action :require_user!

respond_to :json

def index
render json: current_account, serializer: REST::PreferencesSerializer
end
end

+ 9
- 17
app/controllers/api/v1/search_controller.rb View File

@@ -3,7 +3,7 @@
class Api::V1::SearchController < Api::BaseController class Api::V1::SearchController < Api::BaseController
include Authorization include Authorization


RESULTS_LIMIT = 5
RESULTS_LIMIT = 20


before_action -> { doorkeeper_authorize! :read, :'read:search' } before_action -> { doorkeeper_authorize! :read, :'read:search' }
before_action :require_user! before_action :require_user!
@@ -11,30 +11,22 @@ class Api::V1::SearchController < Api::BaseController
respond_to :json respond_to :json


def index def index
@search = Search.new(search)
@search = Search.new(search_results)
render json: @search, serializer: REST::SearchSerializer render json: @search, serializer: REST::SearchSerializer
end end


private private


def search
search_results.tap do |search|
search[:statuses].keep_if do |status|
begin
authorize status, :show?
rescue Mastodon::NotPermittedError
false
end
end
end
end

def search_results def search_results
SearchService.new.call( SearchService.new.call(
params[:q], params[:q],
RESULTS_LIMIT,
truthy_param?(:resolve),
current_account
current_account,
limit_param(RESULTS_LIMIT),
search_params.merge(resolve: truthy_param?(:resolve))
) )
end end

def search_params
params.permit(:type, :offset, :min_id, :max_id, :account_id)
end
end end

+ 5
- 1
app/controllers/api/v1/statuses/reblogs_controller.rb View File

@@ -9,7 +9,7 @@ class Api::V1::Statuses::ReblogsController < Api::BaseController
respond_to :json respond_to :json


def create def create
@status = ReblogService.new.call(current_user.account, status_for_reblog)
@status = ReblogService.new.call(current_user.account, status_for_reblog, reblog_params)
render json: @status, serializer: REST::StatusSerializer render json: @status, serializer: REST::StatusSerializer
end end


@@ -32,4 +32,8 @@ class Api::V1::Statuses::ReblogsController < Api::BaseController
def status_for_destroy def status_for_destroy
current_user.account.statuses.where(reblog_of_id: params[:status_id]).first! current_user.account.statuses.where(reblog_of_id: params[:status_id]).first!
end end

def reblog_params
params.permit(:visibility)
end
end end

+ 16
- 2
app/controllers/api/v1/statuses_controller.rb View File

@@ -53,6 +53,7 @@ class Api::V1::StatusesController < Api::BaseController
visibility: status_params[:visibility], visibility: status_params[:visibility],
scheduled_at: status_params[:scheduled_at], scheduled_at: status_params[:scheduled_at],
application: doorkeeper_token.application, application: doorkeeper_token.application,
poll: status_params[:poll],
idempotency: request.headers['Idempotency-Key']) idempotency: request.headers['Idempotency-Key'])


render json: @status, serializer: @status.is_a?(ScheduledStatus) ? REST::ScheduledStatusSerializer : REST::StatusSerializer render json: @status, serializer: @status.is_a?(ScheduledStatus) ? REST::ScheduledStatusSerializer : REST::StatusSerializer
@@ -73,12 +74,25 @@ class Api::V1::StatusesController < Api::BaseController
@status = Status.find(params[:id]) @status = Status.find(params[:id])
authorize @status, :show? authorize @status, :show?
rescue Mastodon::NotPermittedError rescue Mastodon::NotPermittedError
# Reraise in order to get a 404 instead of a 403 error code
raise ActiveRecord::RecordNotFound raise ActiveRecord::RecordNotFound
end end


def status_params def status_params
params.permit(:status, :in_reply_to_id, :sensitive, :spoiler_text, :visibility, :scheduled_at, media_ids: [])
params.permit(
:status,
:in_reply_to_id,
:sensitive,
:spoiler_text,
:visibility,
:scheduled_at,
media_ids: [],
poll: [
:multiple,
:hide_totals,
:expires_in,
options: [],
]
)
end end


def pagination_params(core_params) def pagination_params(core_params)


+ 1
- 1
app/controllers/api/v1/timelines/tag_controller.rb View File

@@ -14,7 +14,7 @@ class Api::V1::Timelines::TagController < Api::BaseController
private private


def load_tag def load_tag
@tag = Tag.find_by(name: params[:id].downcase)
@tag = Tag.find_normalized(params[:id])
end end


def load_statuses def load_statuses


+ 1
- 1
app/controllers/api/v2/search_controller.rb View File

@@ -2,7 +2,7 @@


class Api::V2::SearchController < Api::V1::SearchController class Api::V2::SearchController < Api::V1::SearchController
def index def index
@search = Search.new(search)
@search = Search.new(search_results)
render json: @search, serializer: REST::V2::SearchSerializer render json: @search, serializer: REST::V2::SearchSerializer
end end
end end

+ 5
- 0
app/controllers/application_controller.rb View File

@@ -151,6 +151,11 @@ class ApplicationController < ActionController::Base
response.headers['Vary'] = 'Accept' response.headers['Vary'] = 'Accept'
end end


def mark_cacheable!
skip_session!
expires_in 0, public: true
end

def skip_session! def skip_session!
request.session_options[:skip] = true request.session_options[:skip] = true
end end


+ 10
- 5
app/controllers/auth/registrations_controller.rb View File

@@ -10,6 +10,10 @@ class Auth::RegistrationsController < Devise::RegistrationsController
before_action :set_instance_presenter, only: [:new, :create, :update] before_action :set_instance_presenter, only: [:new, :create, :update]
before_action :set_body_classes, only: [:new, :create, :edit, :update] before_action :set_body_classes, only: [:new, :create, :edit, :update]


def new
super(&:build_invite_request)
end

def destroy def destroy
not_found not_found
end end
@@ -24,16 +28,17 @@ class Auth::RegistrationsController < Devise::RegistrationsController
def build_resource(hash = nil) def build_resource(hash = nil)
super(hash) super(hash)


resource.locale = I18n.locale
resource.invite_code = params[:invite_code] if resource.invite_code.blank?
resource.agreement = true
resource.locale = I18n.locale
resource.invite_code = params[:invite_code] if resource.invite_code.blank?
resource.agreement = true
resource.current_sign_in_ip = request.remote_ip


resource.build_account if resource.account.nil? resource.build_account if resource.account.nil?
end end


def configure_sign_up_params def configure_sign_up_params
devise_parameter_sanitizer.permit(:sign_up) do |u| devise_parameter_sanitizer.permit(:sign_up) do |u|
u.permit({ account_attributes: [:username] }, :email, :password, :password_confirmation, :invite_code)
u.permit({ account_attributes: [:username], invite_request_attributes: [:text] }, :email, :password, :password_confirmation, :invite_code)
end end
end end


@@ -64,7 +69,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
end end


def allowed_registrations? def allowed_registrations?
Setting.open_registrations || @invite&.valid_for_use?
Setting.registrations_mode != 'none' || @invite&.valid_for_use?
end end


def invite_code def invite_code


+ 17
- 3
app/controllers/concerns/account_controller_concern.rb View File

@@ -7,16 +7,18 @@ module AccountControllerConcern


included do included do
layout 'public' layout 'public'

before_action :set_account before_action :set_account
before_action :check_account_approval
before_action :check_account_suspension
before_action :set_instance_presenter before_action :set_instance_presenter
before_action :set_link_headers before_action :set_link_headers
before_action :check_account_suspension
end end


private private


def set_account def set_account
@account = Account.find_local!(params[:account_username])
@account = Account.find_local!(username_param)
end end


def set_instance_presenter def set_instance_presenter
@@ -33,6 +35,10 @@ module AccountControllerConcern
) )
end end


def username_param
params[:account_username]
end

def webfinger_account_link def webfinger_account_link
[ [
webfinger_account_url, webfinger_account_url,
@@ -58,7 +64,15 @@ module AccountControllerConcern
webfinger_url(resource: @account.to_webfinger_s) webfinger_url(resource: @account.to_webfinger_s)
end end


def check_account_approval
not_found if @account.user_pending?
end

def check_account_suspension def check_account_suspension
gone if @account.suspended?
if @account.suspended?
skip_session!
expires_in(3.minutes, public: true)
gone
end
end end
end end

+ 1
- 1
app/controllers/directories_controller.rb View File

@@ -32,7 +32,7 @@ class DirectoriesController < ApplicationController
end end


def set_accounts def set_accounts
@accounts = Account.discoverable.page(params[:page]).per(40).tap do |query|
@accounts = Account.discoverable.by_recent_status.page(params[:page]).per(40).tap do |query|
query.merge!(Account.tagged_with(@tag.id)) if @tag query.merge!(Account.tagged_with(@tag.id)) if @tag
end end
end end


+ 9
- 0
app/controllers/follower_accounts_controller.rb View File

@@ -3,9 +3,13 @@
class FollowerAccountsController < ApplicationController class FollowerAccountsController < ApplicationController
include AccountControllerConcern include AccountControllerConcern


before_action :set_cache_headers

def index def index
respond_to do |format| respond_to do |format|
format.html do format.html do
mark_cacheable! unless user_signed_in?

next if @account.user_hides_network? next if @account.user_hides_network?


follows follows
@@ -15,6 +19,11 @@ class FollowerAccountsController < ApplicationController
format.json do format.json do
raise Mastodon::NotPermittedError if params[:page].present? && @account.user_hides_network? raise Mastodon::NotPermittedError if params[:page].present? && @account.user_hides_network?


if params[:page].blank?
skip_session!
expires_in 3.minutes, public: true
end

render json: collection_presenter, render json: collection_presenter,
serializer: ActivityPub::CollectionSerializer, serializer: ActivityPub::CollectionSerializer,
adapter: ActivityPub::Adapter, adapter: ActivityPub::Adapter,


+ 9
- 0
app/controllers/following_accounts_controller.rb View File

@@ -3,9 +3,13 @@
class FollowingAccountsController < ApplicationController class FollowingAccountsController < ApplicationController
include AccountControllerConcern include AccountControllerConcern


before_action :set_cache_headers

def index def index
respond_to do |format| respond_to do |format|
format.html do format.html do
mark_cacheable! unless user_signed_in?

next if @account.user_hides_network? next if @account.user_hides_network?


follows follows
@@ -15,6 +19,11 @@ class FollowingAccountsController < ApplicationController
format.json do format.json do
raise Mastodon::NotPermittedError if params[:page].present? && @account.user_hides_network? raise Mastodon::NotPermittedError if params[:page].present? && @account.user_hides_network?


if params[:page].blank?
skip_session!
expires_in 3.minutes, public: true
end

render json: collection_presenter, render json: collection_presenter,
serializer: ActivityPub::CollectionSerializer, serializer: ActivityPub::CollectionSerializer,
adapter: ActivityPub::Adapter, adapter: ActivityPub::Adapter,


+ 1
- 1
app/controllers/home_controller.rb View File

@@ -50,7 +50,7 @@ class HomeController < ApplicationController
push_subscription: current_account.user.web_push_subscription(current_session), push_subscription: current_account.user.web_push_subscription(current_session),
current_account: current_account, current_account: current_account,
token: current_session.token, token: current_session.token,
admin: Account.find_local(Setting.site_contact_username),
admin: Account.find_local(Setting.site_contact_username.strip.gsub(/\A@/, '')),
} }
end end




+ 5
- 0
app/controllers/oauth/authorized_applications_controller.rb View File

@@ -5,6 +5,7 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio


before_action :store_current_location before_action :store_current_location
before_action :authenticate_resource_owner! before_action :authenticate_resource_owner!
before_action :set_body_classes


include Localized include Localized


@@ -15,6 +16,10 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio


private private


def set_body_classes
@body_classes = 'admin'
end

def store_current_location def store_current_location
store_location_for(:user, request.url) store_location_for(:user, request.url)
end end


+ 34
- 0
app/controllers/public_timelines_controller.rb View File

@@ -0,0 +1,34 @@
# frozen_string_literal: true

class PublicTimelinesController < ApplicationController
layout 'public'

before_action :check_enabled
before_action :set_body_classes
before_action :set_instance_presenter

def show
respond_to do |format|
format.html do
@initial_state_json = ActiveModelSerializers::SerializableResource.new(
InitialStatePresenter.new(settings: { known_fediverse: Setting.show_known_fediverse_at_about_page }, token: current_session&.token),
serializer: InitialStateSerializer
).to_json
end
end
end

private

def check_enabled
raise ActiveRecord::RecordNotFound unless Setting.timeline_preview
end

def set_body_classes
@body_classes = 'with-modals'
end

def set_instance_presenter
@instance_presenter = InstancePresenter.new
end
end

+ 104
- 0
app/controllers/relationships_controller.rb View File

@@ -0,0 +1,104 @@
# frozen_string_literal: true

class RelationshipsController < ApplicationController
layout 'admin'

before_action :authenticate_user!
before_action :set_accounts, only: :show
before_action :set_body_classes

helper_method :following_relationship?, :followed_by_relationship?, :mutual_relationship?

def show
@form = Form::AccountBatch.new
end

def update
@form = Form::AccountBatch.new(form_account_batch_params.merge(current_account: current_account, action: action_from_button))
@form.save
rescue ActionController::ParameterMissing
# Do nothing
ensure
redirect_to relationships_path(current_params)
end

private

def set_accounts
@accounts = relationships_scope.page(params[:page]).per(40)
end

def relationships_scope
scope = begin
if following_relationship?
current_account.following.eager_load(:account_stat).reorder(nil)
else
current_account.followers.eager_load(:account_stat).reorder(nil)
end
end

scope.merge!(Follow.recent) if params[:order].blank? || params[:order] == 'recent'
scope.merge!(Account.by_recent_status) if params[:order] == 'active'
scope.merge!(mutual_relationship_scope) if mutual_relationship?
scope.merge!(moved_account_scope) if params[:status] == 'moved'
scope.merge!(primary_account_scope) if params[:status] == 'primary'
scope.merge!(by_domain_scope) if params[:by_domain].present?
scope.merge!(dormant_account_scope) if params[:activity] == 'dormant'

scope
end

def mutual_relationship_scope
Account.where(id: current_account.following)
end

def moved_account_scope
Account.where.not(moved_to_account_id: nil)
end

def primary_account_scope
Account.where(moved_to_account_id: nil)
end

def dormant_account_scope
AccountStat.where(last_status_at: nil).or(AccountStat.where(AccountStat.arel_table[:last_status_at].lt(1.month.ago)))
end

def by_domain_scope
Account.where(domain: params[:by_domain])
end

def form_account_batch_params
params.require(:form_account_batch).permit(:action, account_ids: [])
end

def following_relationship?
params[:relationship].blank? || params[:relationship] == 'following'
end

def mutual_relationship?
params[:relationship] == 'mutual'
end

def followed_by_relationship?
params[:relationship] == 'followed_by'
end

def current_params
params.slice(:page, :status, :relationship, :by_domain, :activity, :order).permit(:page, :status, :relationship, :by_domain, :activity, :order)
end

def action_from_button
if params[:unfollow]
'unfollow'
elsif params[:remove_from_followers]
'remove_from_followers'
elsif params[:block_domains]
'block_domains'
end
end

def set_body_classes
@body_classes = 'admin'
end
end

+ 16
- 2
app/controllers/settings/exports_controller.rb View File

@@ -13,11 +13,25 @@ class Settings::ExportsController < Settings::BaseController
end end


def create def create
authorize :backup, :create?
raise Mastodon::NotPermittedError unless user_signed_in?

backup = nil

RedisLock.acquire(lock_options) do |lock|
if lock.acquired?
authorize :backup, :create?
backup = current_user.backups.create!
else
raise Mastodon::RaceConditionError
end
end


backup = current_user.backups.create!
BackupWorker.perform_async(backup.id) BackupWorker.perform_async(backup.id)


redirect_to settings_export_path redirect_to settings_export_path
end end

def lock_options
{ redis: Redis.current, key: "backup:#{current_user.id}" }
end
end end

+ 51
- 0
app/controllers/settings/featured_tags_controller.rb View File

@@ -0,0 +1,51 @@
# frozen_string_literal: true

class Settings::FeaturedTagsController < Settings::BaseController
layout 'admin'

before_action :authenticate_user!
before_action :set_featured_tags, only: :index
before_action :set_featured_tag, except: [:index, :create]
before_action :set_most_used_tags, only: :index

def index
@featured_tag = FeaturedTag.new
end

def create
@featured_tag = current_account.featured_tags.new(featured_tag_params)
@featured_tag.reset_data

if @featured_tag.save
redirect_to settings_featured_tags_path
else
set_featured_tags
set_most_used_tags

render :index
end
end

def destroy
@featured_tag.destroy!
redirect_to settings_featured_tags_path
end

private

def set_featured_tag
@featured_tag = current_account.featured_tags.find(params[:id])
end

def set_featured_tags
@featured_tags = current_account.featured_tags.order(statuses_count: :desc).reject(&:new_record?)
end

def set_most_used_tags
@most_used_tags = Tag.most_used(current_account).where.not(id: @featured_tags.map(&:id)).limit(10)
end

def featured_tag_params
params.require(:featured_tag).permit(:name)
end
end

+ 0
- 28
app/controllers/settings/follower_domains_controller.rb View File

@@ -1,28 +0,0 @@
# frozen_string_literal: true

class Settings::FollowerDomainsController < Settings::BaseController
layout 'admin'

before_action :authenticate_user!

def show
@account = current_account
@domains = current_account.followers.reorder(Arel.sql('MIN(follows.id) DESC')).group('accounts.domain').select('accounts.domain, count(accounts.id) as accounts_from_domain').page(params[:page]).per(10)
end

def update
domains = bulk_params[:select] || []

AfterAccountDomainBlockWorker.push_bulk(domains) do |domain|
[current_account.id, domain]
end

redirect_to settings_follower_domains_path, notice: I18n.t('followers.success', count: domains.size)
end

private

def bulk_params
params.permit(select: [])
end
end

+ 63
- 0
app/controllers/settings/identity_proofs_controller.rb View File

@@ -0,0 +1,63 @@
# frozen_string_literal: true

class Settings::IdentityProofsController < Settings::BaseController
layout 'admin'

before_action :authenticate_user!
before_action :check_required_params, only: :new

def index
@proofs = AccountIdentityProof.where(account: current_account).order(provider: :asc, provider_username: :asc)
@proofs.each(&:refresh!)
end

def new
@proof = current_account.identity_proofs.new(
token: params[:token],
provider: params[:provider],
provider_username: params[:provider_username]
)

if current_account.username.casecmp(params[:username]).zero?
render layout: 'auth'
else
flash[:alert] = I18n.t('identity_proofs.errors.wrong_user', proving: params[:username], current: current_account.username)
redirect_to settings_identity_proofs_path
end
end

def create
@proof = current_account.identity_proofs.where(provider: resource_params[:provider], provider_username: resource_params[:provider_username]).first_or_initialize(resource_params)
@proof.token = resource_params[:token]

if @proof.save
PostStatusService.new.call(current_user.account, text: post_params[:status_text]) if publish_proof?
redirect_to @proof.on_success_path(params[:user_agent])
else
flash[:alert] = I18n.t('identity_proofs.errors.failed', provider: @proof.provider.capitalize)
redirect_to settings_identity_proofs_path
end
end

private

def check_required_params
redirect_to settings_identity_proofs_path unless [:provider, :provider_username, :username, :token].all? { |k| params[k].present? }
end

def resource_params
params.require(:account_identity_proof).permit(:provider, :provider_username, :token)
end

def publish_proof?
ActiveModel::Type::Boolean.new.cast(post_params[:post_status])
end

def post_params
params.require(:account_identity_proof).permit(:post_status, :status_text)
end

def set_body_classes
@body_classes = ''
end
end

+ 2
- 1
app/controllers/settings/preferences_controller.rb View File

@@ -48,7 +48,8 @@ class Settings::PreferencesController < Settings::BaseController
:setting_theme, :setting_theme,
:setting_hide_network, :setting_hide_network,
:setting_aggregate_reblogs, :setting_aggregate_reblogs,
notification_emails: %i(follow follow_request reblog favourite mention digest report),
:setting_show_application,
notification_emails: %i(follow follow_request reblog favourite mention digest report pending_account),
interactions: %i(must_be_follower must_be_following) interactions: %i(must_be_follower must_be_following)
) )
end end


+ 1
- 1
app/controllers/settings/profiles_controller.rb View File

@@ -32,6 +32,6 @@ class Settings::ProfilesController < Settings::BaseController
end end


def set_account def set_account
@account = current_user.account
@account = current_account
end end
end end

+ 1
- 0
app/controllers/settings/sessions_controller.rb View File

@@ -1,6 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true


class Settings::SessionsController < Settings::BaseController class Settings::SessionsController < Settings::BaseController
before_action :authenticate_user!
before_action :set_session, only: :destroy before_action :set_session, only: :destroy


def destroy def destroy


+ 1
- 1
app/controllers/shares_controller.rb View File

@@ -21,7 +21,7 @@ class SharesController < ApplicationController
push_subscription: current_account.user.web_push_subscription(current_session), push_subscription: current_account.user.web_push_subscription(current_session),
current_account: current_account, current_account: current_account,
token: current_session.token, token: current_session.token,
admin: Account.find_local(Setting.site_contact_username),
admin: Account.find_local(Setting.site_contact_username.strip.gsub(/\A@/, '')),
text: text, text: text,
} }
end end


+ 56
- 1
app/controllers/statuses_controller.rb View File

@@ -18,6 +18,7 @@ class StatusesController < ApplicationController
before_action :redirect_to_original, only: [:show] before_action :redirect_to_original, only: [:show]
before_action :set_referrer_policy_header, only: [:show] before_action :set_referrer_policy_header, only: [:show]
before_action :set_cache_headers before_action :set_cache_headers
before_action :set_replies, only: [:replies]


content_security_policy only: :embed do |p| content_security_policy only: :embed do |p|
p.frame_ancestors(false) p.frame_ancestors(false)
@@ -26,6 +27,8 @@ class StatusesController < ApplicationController
def show def show
respond_to do |format| respond_to do |format|
format.html do format.html do
mark_cacheable! unless user_signed_in?

@body_classes = 'with-modals' @body_classes = 'with-modals'


set_ancestors set_ancestors
@@ -35,7 +38,7 @@ class StatusesController < ApplicationController
end end


format.json do format.json do
skip_session! unless @stream_entry.hidden?
mark_cacheable! unless @stream_entry.hidden?


render_cached_json(['activitypub', 'note', @status], content_type: 'application/activity+json', public: !@stream_entry.hidden?) do render_cached_json(['activitypub', 'note', @status], content_type: 'application/activity+json', public: !@stream_entry.hidden?) do
ActiveModelSerializers::SerializableResource.new(@status, serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter) ActiveModelSerializers::SerializableResource.new(@status, serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter)
@@ -63,8 +66,37 @@ class StatusesController < ApplicationController
render 'stream_entries/embed', layout: 'embedded' render 'stream_entries/embed', layout: 'embedded'
end end


def replies
skip_session!

render json: replies_collection_presenter,
serializer: ActivityPub::CollectionSerializer,
adapter: ActivityPub::Adapter,
content_type: 'application/activity+json',
skip_activities: true
end

private private


def replies_collection_presenter
page = ActivityPub::CollectionPresenter.new(
id: replies_account_status_url(@account, @status, page_params),
type: :unordered,
part_of: replies_account_status_url(@account, @status),
next: next_page,
items: @replies.map { |status| status.local ? status : status.id }
)
if page_requested?
page
else
ActivityPub::CollectionPresenter.new(
id: replies_account_status_url(@account, @status),
type: :unordered,
first: page
)
end
end

def create_descendant_thread(starting_depth, statuses) def create_descendant_thread(starting_depth, statuses)
depth = starting_depth + statuses.size depth = starting_depth + statuses.size
if depth < DESCENDANTS_DEPTH_LIMIT if depth < DESCENDANTS_DEPTH_LIMIT
@@ -174,4 +206,27 @@ class StatusesController < ApplicationController
return if @status.public_visibility? || @status.unlisted_visibility? return if @status.public_visibility? || @status.unlisted_visibility?
response.headers['Referrer-Policy'] = 'origin' response.headers['Referrer-Policy'] = 'origin'
end end

def page_requested?
params[:page] == 'true'
end

def set_replies
@replies = page_params[:other_accounts] ? Status.where.not(account_id: @account.id) : @account.statuses
@replies = @replies.where(in_reply_to_id: @status.id, visibility: [:public, :unlisted])
@replies = @replies.paginate_by_min_id(DESCENDANTS_LIMIT, params[:min_id])
end

def next_page
last_reply = @replies.last
return if last_reply.nil?
same_account = last_reply.account_id == @account.id
return unless same_account || @replies.size == DESCENDANTS_LIMIT
same_account = false unless @replies.size == DESCENDANTS_LIMIT
replies_account_status_url(@account, @status, page: true, min_id: last_reply.id, other_accounts: !same_account)
end

def page_params
{ page: true, other_accounts: params[:other_accounts], min_id: params[:min_id] }.compact
end
end end

+ 6
- 12
app/controllers/tags_controller.rb View File

@@ -9,12 +9,14 @@ class TagsController < ApplicationController
before_action :set_instance_presenter before_action :set_instance_presenter


def show def show
@tag = Tag.find_by!(name: params[:id].downcase)
@tag = Tag.find_normalized!(params[:id])


respond_to do |format| respond_to do |format|
format.html do format.html do
serializable_resource = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(initial_state_params), serializer: InitialStateSerializer)
@initial_state_json = serializable_resource.to_json
@initial_state_json = ActiveModelSerializers::SerializableResource.new(
InitialStatePresenter.new(settings: {}, token: current_session&.token),
serializer: InitialStateSerializer
).to_json
end end


format.rss do format.rss do
@@ -25,8 +27,7 @@ class TagsController < ApplicationController
end end


format.json do format.json do
@statuses = HashtagQueryService.new.call(@tag, params.slice(:any, :all, :none), current_account, params[:local])
.paginate_by_max_id(PAGE_SIZE, params[:max_id])
@statuses = HashtagQueryService.new.call(@tag, params.slice(:any, :all, :none), current_account, params[:local]).paginate_by_max_id(PAGE_SIZE, params[:max_id])
@statuses = cache_collection(@statuses, Status) @statuses = cache_collection(@statuses, Status)


render json: collection_presenter, render json: collection_presenter,
@@ -55,11 +56,4 @@ class TagsController < ApplicationController
items: @statuses.map { |s| ActivityPub::TagManager.instance.uri_for(s) } items: @statuses.map { |s| ActivityPub::TagManager.instance.uri_for(s) }
) )
end end

def initial_state_params
{
settings: {},
token: current_session&.token,
}
end
end end

+ 9
- 0
app/controllers/well_known/keybase_proof_config_controller.rb View File

@@ -0,0 +1,9 @@
# frozen_string_literal: true

module WellKnown
class KeybaseProofConfigController < ActionController::Base
def show
render json: {}, serializer: ProofProvider::Keybase::ConfigSerializer
end
end
end

+ 36
- 36
app/helpers/admin/action_logs_helper.rb View File

@@ -9,42 +9,6 @@ module Admin::ActionLogsHelper
end end
end end


def linkable_log_target(record)
case record.class.name
when 'Account'
link_to record.acct, admin_account_path(record.id)
when 'User'
link_to record.account.acct, admin_account_path(record.account_id)
when 'CustomEmoji'
record.shortcode
when 'Report'
link_to "##{record.id}", admin_report_path(record)
when 'DomainBlock', 'EmailDomainBlock'
link_to record.domain, "https://#{record.domain}"
when 'Status'
link_to record.account.acct, TagManager.instance.url_for(record)
when 'AccountWarning'
link_to record.target_account.acct, admin_account_path(record.target_account_id)
end
end

def log_target_from_history(type, attributes)
case type
when 'CustomEmoji'
attributes['shortcode']
when 'DomainBlock', 'EmailDomainBlock'
link_to attributes['domain'], "https://#{attributes['domain']}"
when 'Status'
tmp_status = Status.new(attributes.except('reblogs_count', 'favourites_count'))

if tmp_status.account
link_to tmp_status.account&.acct || "##{tmp_status.account_id}", admin_account_path(tmp_status.account_id)
else
I18n.t('admin.action_logs.deleted_status')
end
end
end

def relevant_log_changes(log) def relevant_log_changes(log)
if log.target_type == 'CustomEmoji' && [:enable, :disable, :destroy].include?(log.action) if log.target_type == 'CustomEmoji' && [:enable, :disable, :destroy].include?(log.action)
log.recorded_changes.slice('domain') log.recorded_changes.slice('domain')
@@ -111,4 +75,40 @@ module Admin::ActionLogsHelper
def opposite_verbs?(log) def opposite_verbs?(log)
%w(DomainBlock EmailDomainBlock AccountWarning).include?(log.target_type) %w(DomainBlock EmailDomainBlock AccountWarning).include?(log.target_type)
end end

def linkable_log_target(record)
case record.class.name
when 'Account'
link_to record.acct, admin_account_path(record.id)
when 'User'
link_to record.account.acct, admin_account_path(record.account_id)
when 'CustomEmoji'
record.shortcode
when 'Report'
link_to "##{record.id}", admin_report_path(record)
when 'DomainBlock', 'EmailDomainBlock'
link_to record.domain, "https://#{record.domain}"
when 'Status'
link_to record.account.acct, TagManager.instance.url_for(record)
when 'AccountWarning'
link_to record.target_account.acct, admin_account_path(record.target_account_id)
end
end

def log_target_from_history(type, attributes)
case type
when 'CustomEmoji'
attributes['shortcode']
when 'DomainBlock', 'EmailDomainBlock'
link_to attributes['domain'], "https://#{attributes['domain']}"
when 'Status'
tmp_status = Status.new(attributes.except('reblogs_count', 'favourites_count'))

if tmp_status.account
link_to tmp_status.account&.acct || "##{tmp_status.account_id}", admin_account_path(tmp_status.account_id)
else
I18n.t('admin.action_logs.deleted_status')
end
end
end
end end

+ 10
- 0
app/helpers/admin/dashboard_helper.rb View File

@@ -0,0 +1,10 @@
# frozen_string_literal: true

module Admin::DashboardHelper
def feature_hint(feature, enabled)
indicator = safe_join([enabled ? t('simple_form.yes') : t('simple_form.no'), fa_icon('power-off fw')], ' ')
class_names = enabled ? 'pull-right positive-hint' : 'pull-right neutral-hint'

safe_join([feature, content_tag(:span, indicator, class: class_names)])
end
end

+ 4
- 3
app/helpers/admin/filter_helper.rb View File

@@ -1,14 +1,15 @@
# frozen_string_literal: true # frozen_string_literal: true


module Admin::FilterHelper module Admin::FilterHelper
ACCOUNT_FILTERS = %i(local remote by_domain active silenced suspended username display_name email ip staff).freeze
ACCOUNT_FILTERS = %i(local remote by_domain active pending silenced suspended username display_name email ip staff).freeze
REPORT_FILTERS = %i(resolved account_id target_account_id).freeze REPORT_FILTERS = %i(resolved account_id target_account_id).freeze
INVITE_FILTER = %i(available expired).freeze INVITE_FILTER = %i(available expired).freeze
CUSTOM_EMOJI_FILTERS = %i(local remote by_domain shortcode).freeze CUSTOM_EMOJI_FILTERS = %i(local remote by_domain shortcode).freeze
TAGS_FILTERS = %i(hidden).freeze TAGS_FILTERS = %i(hidden).freeze
INSTANCES_FILTERS = %i(limited).freeze
INSTANCES_FILTERS = %i(limited by_domain).freeze
FOLLOWERS_FILTERS = %i(relationship status by_domain activity order).freeze


FILTERS = ACCOUNT_FILTERS + REPORT_FILTERS + INVITE_FILTER + CUSTOM_EMOJI_FILTERS + TAGS_FILTERS + INSTANCES_FILTERS
FILTERS = ACCOUNT_FILTERS + REPORT_FILTERS + INVITE_FILTER + CUSTOM_EMOJI_FILTERS + TAGS_FILTERS + INSTANCES_FILTERS + FOLLOWERS_FILTERS


def filter_link_to(text, link_to_params, link_class_params = link_to_params) def filter_link_to(text, link_to_params, link_class_params = link_to_params)
new_url = filtered_url_for(link_to_params) new_url = filtered_url_for(link_to_params)


+ 22
- 1
app/helpers/application_helper.rb View File

@@ -20,7 +20,23 @@ module ApplicationHelper
end end


def open_registrations? def open_registrations?
Setting.open_registrations
Setting.registrations_mode == 'open'
end

def approved_registrations?
Setting.registrations_mode == 'approved'
end

def closed_registrations?
Setting.registrations_mode == 'none'
end

def available_sign_up_path
if closed_registrations?
'https://joinmastodon.org/#getting-started'
else
new_user_registration_path
end
end end


def open_deletion? def open_deletion?
@@ -101,4 +117,9 @@ module ApplicationHelper
def storage_host? def storage_host?
ENV['S3_ALIAS_HOST'].present? || ENV['S3_CLOUDFRONT_HOST'].present? ENV['S3_ALIAS_HOST'].present? || ENV['S3_CLOUDFRONT_HOST'].present?
end end

def quote_wrap(text, line_width: 80, break_sequence: "\n")
text = word_wrap(text, line_width: line_width - 2, break_sequence: break_sequence)
text.split("\n").map { |line| '> ' + line }.join("\n")
end
end end

+ 18
- 0
app/helpers/home_helper.rb View File

@@ -56,4 +56,22 @@ module HomeHelper
'emojify' 'emojify'
end end
end end

def optional_link_to(condition, path, options = {}, &block)
if condition
link_to(path, options, &block)
else
content_tag(:div, &block)
end
end

def sign_up_message
if closed_registrations?
t('auth.registration_closed', instance: site_hostname)
elsif open_registrations?
t('auth.register')
elsif approved_registrations?
t('auth.apply_for_account')
end
end
end end

+ 25
- 1
app/helpers/jsonld_helper.rb View File

@@ -47,6 +47,15 @@ module JsonLdHelper
!uri.start_with?('http://', 'https://') !uri.start_with?('http://', 'https://')
end end


def invalid_origin?(url)
return true if unsupported_uri_scheme?(url)

needle = Addressable::URI.parse(url).host
haystack = Addressable::URI.parse(@account.uri).host

!haystack.casecmp(needle).zero?
end

def canonicalize(json) def canonicalize(json)
graph = RDF::Graph.new << JSON::LD::API.toRdf(json, documentLoader: method(:load_jsonld_context)) graph = RDF::Graph.new << JSON::LD::API.toRdf(json, documentLoader: method(:load_jsonld_context))
graph.dump(:normalize) graph.dump(:normalize)
@@ -63,12 +72,19 @@ module JsonLdHelper
json.present? && json['id'] == uri ? json : nil json.present? && json['id'] == uri ? json : nil
end end


def fetch_resource_without_id_validation(uri, on_behalf_of = nil)
def fetch_resource_without_id_validation(uri, on_behalf_of = nil, raise_on_temporary_error = false)
build_request(uri, on_behalf_of).perform do |response| build_request(uri, on_behalf_of).perform do |response|
unless response_successful?(response) || response_error_unsalvageable?(response) || !raise_on_temporary_error
raise Mastodon::UnexpectedResponseError, response
end
return body_to_json(response.body_with_limit) if response.code == 200 return body_to_json(response.body_with_limit) if response.code == 200
end end
# If request failed, retry without doing it on behalf of a user # If request failed, retry without doing it on behalf of a user
return if on_behalf_of.nil?
build_request(uri).perform do |response| build_request(uri).perform do |response|
unless response_successful?(response) || response_error_unsalvageable?(response) || !raise_on_temporary_error
raise Mastodon::UnexpectedResponseError, response
end
response.code == 200 ? body_to_json(response.body_with_limit) : nil response.code == 200 ? body_to_json(response.body_with_limit) : nil
end end
end end
@@ -91,6 +107,14 @@ module JsonLdHelper


private private


def response_successful?(response)
(200...300).cover?(response.code)
end

def response_error_unsalvageable?(response)
(400...500).cover?(response.code) && response.code != 429
end

def build_request(uri, on_behalf_of = nil) def build_request(uri, on_behalf_of = nil)
request = Request.new(:get, uri) request = Request.new(:get, uri)
request.on_behalf_of(on_behalf_of) if on_behalf_of request.on_behalf_of(on_behalf_of) if on_behalf_of


+ 12
- 4
app/helpers/settings_helper.rb View File

@@ -4,8 +4,9 @@ module SettingsHelper
HUMAN_LOCALES = { HUMAN_LOCALES = {
en: 'English', en: 'English',
ar: 'العربية', ar: 'العربية',
ast: 'l\'asturianu',
ast: 'Asturianu',
bg: 'Български', bg: 'Български',
bn: 'বাংলা',
ca: 'Català', ca: 'Català',
co: 'Corsu', co: 'Corsu',
cs: 'Čeština', cs: 'Čeština',
@@ -19,8 +20,10 @@ module SettingsHelper
fa: 'فارسی', fa: 'فارسی',
fi: 'Suomi', fi: 'Suomi',
fr: 'Français', fr: 'Français',
ga: 'Gaeilge',
gl: 'Galego', gl: 'Galego',
he: 'עברית', he: 'עברית',
hi: 'हिन्दी',
hr: 'Hrvatski', hr: 'Hrvatski',
hu: 'Magyar', hu: 'Magyar',
hy: 'Հայերեն', hy: 'Հայերեն',
@@ -29,24 +32,29 @@ module SettingsHelper
it: 'Italiano', it: 'Italiano',
ja: '日本語', ja: '日本語',
ka: 'ქართული', ka: 'ქართული',
kk: 'Қазақша',
ko: '한국어', ko: '한국어',
lt: 'Lietuvių',
lv: 'Latviešu',
ml: 'മലയാളം', ml: 'മലയാളം',
ms: 'Bahasa Melayu',
nl: 'Nederlands', nl: 'Nederlands',
no: 'Norsk', no: 'Norsk',
oc: 'Occitan', oc: 'Occitan',
pl: 'Polszczyzna',
pl: 'Polski',
pt: 'Português', pt: 'Português',
'pt-BR': 'Português do Brasil', 'pt-BR': 'Português do Brasil',
ro: 'Limba română',
ro: 'Română',
ru: 'Русский', ru: 'Русский',
sk: 'Slovenčina', sk: 'Slovenčina',
sl: 'Slovenščina', sl: 'Slovenščina',
sq: 'Shqip',
sr: 'Српски', sr: 'Српски',
'sr-Latn': 'Srpski (latinica)', 'sr-Latn': 'Srpski (latinica)',
sv: 'Svenska', sv: 'Svenska',
ta: 'தமிழ்', ta: 'தமிழ்',
te: 'తెలుగు', te: 'తెలుగు',
th: 'ภาษาไทย',
th: 'ไทย',
tr: 'Türkçe', tr: 'Türkçe',
uk: 'Українська', uk: 'Українська',
zh: '中文', zh: '中文',


+ 13
- 3
app/helpers/stream_entries_helper.rb View File

@@ -23,7 +23,7 @@ module StreamEntriesHelper
safe_join([render(file: Rails.root.join('app', 'javascript', 'images', 'logo.svg')), t('accounts.unfollow')]) safe_join([render(file: Rails.root.join('app', 'javascript', 'images', 'logo.svg')), t('accounts.unfollow')])
end end
elsif !(account.memorial? || account.moved?) elsif !(account.memorial? || account.moved?)
link_to account_follow_path(account), class: 'button logo-button', data: { method: :post } do
link_to account_follow_path(account), class: "button logo-button#{account.blocking?(current_account) ? ' disabled' : ''}", data: { method: :post } do
safe_join([render(file: Rails.root.join('app', 'javascript', 'images', 'logo.svg')), t('accounts.follow')]) safe_join([render(file: Rails.root.join('app', 'javascript', 'images', 'logo.svg')), t('accounts.follow')])
end end
end end
@@ -104,9 +104,19 @@ module StreamEntriesHelper
I18n.t('statuses.content_warning', warning: status.spoiler_text) I18n.t('statuses.content_warning', warning: status.spoiler_text)
end end


def poll_summary(status)
return unless status.preloadable_poll
status.preloadable_poll.options.map { |o| "[ ] #{o}" }.join("\n")
end

def status_description(status) def status_description(status)
components = [[media_summary(status), status_text_summary(status)].reject(&:blank?).join(' · ')] components = [[media_summary(status), status_text_summary(status)].reject(&:blank?).join(' · ')]
components << status.text if status.spoiler_text.blank?

if status.spoiler_text.blank?
components << status.text
components << poll_summary(status)
end

components.reject(&:blank?).join("\n\n") components.reject(&:blank?).join("\n\n")
end end


@@ -170,7 +180,7 @@ module StreamEntriesHelper
when 'public' when 'public'
fa_icon 'globe fw' fa_icon 'globe fw'
when 'unlisted' when 'unlisted'
fa_icon 'unlock-alt fw'
fa_icon 'unlock fw'
when 'private' when 'private'
fa_icon 'lock fw' fa_icon 'lock fw'
when 'direct' when 'direct'


+ 1
- 0
app/javascript/images/logo_transparent_black.svg View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 216.4144 232.00976"><path d="M107.86523 0C78.203984.2425 49.672422 3.4535937 33.044922 11.089844c0 0-32.97656262 14.752031-32.97656262 65.082031 0 11.525-.224375 25.306175.140625 39.919925 1.19750002 49.22 9.02375002 97.72843 54.53124962 109.77343 20.9825 5.55375 38.99711 6.71547 53.505856 5.91797 26.31125-1.45875 41.08203-9.38867 41.08203-9.38867l-.86914-19.08984s-18.80171 5.92758-39.91796 5.20508c-20.921254-.7175-43.006879-2.25516-46.390629-27.94141-.3125-2.25625-.46875-4.66938-.46875-7.20313 0 0 20.536953 5.0204 46.564449 6.21289 15.915.73001 30.8393-.93343 45.99805-2.74218 29.07-3.47125 54.38125-21.3818 57.5625-37.74805 5.0125-25.78125 4.59961-62.916015 4.59961-62.916015 0-50.33-32.97461-65.082031-32.97461-65.082031C166.80539 3.4535938 138.255.2425 108.59375 0h-.72852zM74.296875 39.326172c12.355 0 21.710234 4.749297 27.896485 14.248047l6.01367 10.080078 6.01563-10.080078c6.185-9.49875 15.54023-14.248047 27.89648-14.248047 10.6775 0 19.28156 3.753672 25.85156 11.076172 6.36875 7.3225 9.53907 17.218828 9.53907 29.673828v60.941408h-24.14454V81.869141c0-12.46875-5.24453-18.798829-15.73828-18.798829-11.6025 0-17.41797 7.508516-17.41797 22.353516v32.375002H96.207031V85.423828c0-14.845-5.815468-22.353515-17.417969-22.353516-10.49375 0-15.740234 6.330079-15.740234 18.798829v59.148439H38.904297V80.076172c0-12.455 3.171016-22.351328 9.541015-29.673828 6.568751-7.3225 15.172813-11.076172 25.851563-11.076172z" fill="#000"/></svg>

BIN
app/javascript/images/proof_providers/keybase.png View File

Before After
Width: 200  |  Height: 200  |  Size: 12 KiB

+ 7
- 2
app/javascript/mastodon/actions/alerts.js View File

@@ -22,7 +22,7 @@ export function clearAlert() {
}; };
}; };


export function showAlert(title, message) {
export function showAlert(title = messages.unexpectedTitle, message = messages.unexpectedMessage) {
return { return {
type: ALERT_SHOW, type: ALERT_SHOW,
title, title,
@@ -34,6 +34,11 @@ export function showAlertForError(error) {
if (error.response) { if (error.response) {
const { data, status, statusText } = error.response; const { data, status, statusText } = error.response;


if (status === 404 || status === 410) {
// Skip these errors as they are reflected in the UI
return {};
}

let message = statusText; let message = statusText;
let title = `${status}`; let title = `${status}`;


@@ -44,6 +49,6 @@ export function showAlertForError(error) {
return showAlert(title, message); return showAlert(title, message);
} else { } else {
console.error(error); console.error(error);
return showAlert(messages.unexpectedTitle, messages.unexpectedMessage);
return showAlert();
} }
} }

+ 87
- 10
app/javascript/mastodon/actions/compose.js View File

@@ -8,6 +8,8 @@ import resizeImage from '../utils/resize_image';
import { importFetchedAccounts } from './importer'; import { importFetchedAccounts } from './importer';
import { updateTimeline } from './timelines'; import { updateTimeline } from './timelines';
import { showAlertForError } from './alerts'; import { showAlertForError } from './alerts';
import { showAlert } from './alerts';
import { defineMessages } from 'react-intl';


let cancelFetchComposeSuggestionsAccounts; let cancelFetchComposeSuggestionsAccounts;


@@ -49,6 +51,18 @@ export const COMPOSE_UPLOAD_CHANGE_REQUEST = 'COMPOSE_UPLOAD_UPDATE_REQUEST'
export const COMPOSE_UPLOAD_CHANGE_SUCCESS = 'COMPOSE_UPLOAD_UPDATE_SUCCESS'; export const COMPOSE_UPLOAD_CHANGE_SUCCESS = 'COMPOSE_UPLOAD_UPDATE_SUCCESS';
export const COMPOSE_UPLOAD_CHANGE_FAIL = 'COMPOSE_UPLOAD_UPDATE_FAIL'; export const COMPOSE_UPLOAD_CHANGE_FAIL = 'COMPOSE_UPLOAD_UPDATE_FAIL';


export const COMPOSE_POLL_ADD = 'COMPOSE_POLL_ADD';
export const COMPOSE_POLL_REMOVE = 'COMPOSE_POLL_REMOVE';
export const COMPOSE_POLL_OPTION_ADD = 'COMPOSE_POLL_OPTION_ADD';
export const COMPOSE_POLL_OPTION_CHANGE = 'COMPOSE_POLL_OPTION_CHANGE';
export const COMPOSE_POLL_OPTION_REMOVE = 'COMPOSE_POLL_OPTION_REMOVE';
export const COMPOSE_POLL_SETTINGS_CHANGE = 'COMPOSE_POLL_SETTINGS_CHANGE';

const messages = defineMessages({
uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' },
uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' },
});

export function changeCompose(text) { export function changeCompose(text) {
return { return {
type: COMPOSE_CHANGE, type: COMPOSE_CHANGE,
@@ -125,6 +139,7 @@ export function submitCompose(routerHistory) {
sensitive: getState().getIn(['compose', 'sensitive']), sensitive: getState().getIn(['compose', 'sensitive']),
spoiler_text: getState().getIn(['compose', 'spoiler_text'], ''), spoiler_text: getState().getIn(['compose', 'spoiler_text'], ''),
visibility: getState().getIn(['compose', 'privacy']), visibility: getState().getIn(['compose', 'privacy']),
poll: getState().getIn(['compose', 'poll'], null),
}, { }, {
headers: { headers: {
'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']), 'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']),
@@ -143,7 +158,9 @@ export function submitCompose(routerHistory) {
// into the columns // into the columns


const insertIfOnline = timelineId => { const insertIfOnline = timelineId => {
if (getState().getIn(['timelines', timelineId, 'items', 0]) !== null) {
const timeline = getState().getIn(['timelines', timelineId]);

if (timeline && timeline.get('items').size > 0 && timeline.getIn(['items', 0]) !== null && timeline.get('online')) {
dispatch(updateTimeline(timelineId, { ...response.data })); dispatch(updateTimeline(timelineId, { ...response.data }));
} }
}; };
@@ -184,20 +201,38 @@ export function submitComposeFail(error) {


export function uploadCompose(files) { export function uploadCompose(files) {
return function (dispatch, getState) { return function (dispatch, getState) {
if (getState().getIn(['compose', 'media_attachments']).size > 3) {
const uploadLimit = 4;
const media = getState().getIn(['compose', 'media_attachments']);
const total = Array.from(files).reduce((a, v) => a + v.size, 0);
const progress = new Array(files.length).fill(0);

if (files.length + media.size > uploadLimit) {
dispatch(showAlert(undefined, messages.uploadErrorLimit));
return;
}

if (getState().getIn(['compose', 'poll'])) {
dispatch(showAlert(undefined, messages.uploadErrorPoll));
return; return;
} }


dispatch(uploadComposeRequest()); dispatch(uploadComposeRequest());


resizeImage(files[0]).then(file => {
const data = new FormData();
data.append('file', file);
for (const [i, f] of Array.from(files).entries()) {
if (media.size + i > 3) break;

resizeImage(f).then(file => {
const data = new FormData();
data.append('file', file);


return api(getState).post('/api/v1/media', data, {
onUploadProgress: ({ loaded, total }) => dispatch(uploadComposeProgress(loaded, total)),
}).then(({ data }) => dispatch(uploadComposeSuccess(data)));
}).catch(error => dispatch(uploadComposeFail(error)));
return api(getState).post('/api/v1/media', data, {
onUploadProgress: function({ loaded }){
progress[i] = loaded;
dispatch(uploadComposeProgress(progress.reduce((a, v) => a + v, 0), total));
},
}).then(({ data }) => dispatch(uploadComposeSuccess(data)));
}).catch(error => dispatch(uploadComposeFail(error)));
};
}; };
}; };


@@ -466,4 +501,46 @@ export function changeComposing(value) {
type: COMPOSE_COMPOSING_CHANGE, type: COMPOSE_COMPOSING_CHANGE,
value, value,
}; };
}
};

export function addPoll() {
return {
type: COMPOSE_POLL_ADD,
};
};

export function removePoll() {
return {
type: COMPOSE_POLL_REMOVE,
};
};

export function addPollOption(title) {
return {
type: COMPOSE_POLL_OPTION_ADD,
title,
};
};

export function changePollOption(index, title) {
return {
type: COMPOSE_POLL_OPTION_CHANGE,
index,
title,
};
};

export function removePollOption(index) {
return {
type: COMPOSE_POLL_OPTION_REMOVE,
index,
};
};

export function changePollSettings(expiresIn, isMultiple) {
return {
type: COMPOSE_POLL_SETTINGS_CHANGE,
expiresIn,
isMultiple,
};
};

+ 5
- 2
app/javascript/mastodon/actions/conversations.js View File

@@ -41,13 +41,15 @@ export const expandConversations = ({ maxId } = {}) => (dispatch, getState) => {
params.since_id = getState().getIn(['conversations', 'items', 0, 'last_status']); params.since_id = getState().getIn(['conversations', 'items', 0, 'last_status']);
} }


const isLoadingRecent = !!params.since_id;

api(getState).get('/api/v1/conversations', { params }) api(getState).get('/api/v1/conversations', { params })
.then(response => { .then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next'); const next = getLinks(response).refs.find(link => link.rel === 'next');


dispatch(importFetchedAccounts(response.data.reduce((aggr, item) => aggr.concat(item.accounts), []))); dispatch(importFetchedAccounts(response.data.reduce((aggr, item) => aggr.concat(item.accounts), [])));
dispatch(importFetchedStatuses(response.data.map(item => item.last_status).filter(x => !!x))); dispatch(importFetchedStatuses(response.data.map(item => item.last_status).filter(x => !!x)));
dispatch(expandConversationsSuccess(response.data, next ? next.uri : null));
dispatch(expandConversationsSuccess(response.data, next ? next.uri : null, isLoadingRecent));
}) })
.catch(err => dispatch(expandConversationsFail(err))); .catch(err => dispatch(expandConversationsFail(err)));
}; };
@@ -56,10 +58,11 @@ export const expandConversationsRequest = () => ({
type: CONVERSATIONS_FETCH_REQUEST, type: CONVERSATIONS_FETCH_REQUEST,
}); });


export const expandConversationsSuccess = (conversations, next) => ({
export const expandConversationsSuccess = (conversations, next, isLoadingRecent) => ({
type: CONVERSATIONS_FETCH_SUCCESS, type: CONVERSATIONS_FETCH_SUCCESS,
conversations, conversations,
next, next,
isLoadingRecent,
}); });


export const expandConversationsFail = error => ({ export const expandConversationsFail = error => ({


+ 30
- 0
app/javascript/mastodon/actions/identity_proofs.js View File

@@ -0,0 +1,30 @@
import api from '../api';

export const IDENTITY_PROOFS_ACCOUNT_FETCH_REQUEST = 'IDENTITY_PROOFS_ACCOUNT_FETCH_REQUEST';
export const IDENTITY_PROOFS_ACCOUNT_FETCH_SUCCESS = 'IDENTITY_PROOFS_ACCOUNT_FETCH_SUCCESS';
export const IDENTITY_PROOFS_ACCOUNT_FETCH_FAIL = 'IDENTITY_PROOFS_ACCOUNT_FETCH_FAIL';

export const fetchAccountIdentityProofs = accountId => (dispatch, getState) => {
dispatch(fetchAccountIdentityProofsRequest(accountId));

api(getState).get(`/api/v1/accounts/${accountId}/identity_proofs`)
.then(({ data }) => dispatch(fetchAccountIdentityProofsSuccess(accountId, data)))
.catch(err => dispatch(fetchAccountIdentityProofsFail(accountId, err)));
};

export const fetchAccountIdentityProofsRequest = id => ({
type: IDENTITY_PROOFS_ACCOUNT_FETCH_REQUEST,
id,
});

export const fetchAccountIdentityProofsSuccess = (accountId, identity_proofs) => ({
type: IDENTITY_PROOFS_ACCOUNT_FETCH_SUCCESS,
accountId,
identity_proofs,
});

export const fetchAccountIdentityProofsFail = (accountId, err) => ({
type: IDENTITY_PROOFS_ACCOUNT_FETCH_FAIL,
accountId,
err,
});

+ 20
- 7
app/javascript/mastodon/actions/importer/index.js View File

@@ -1,11 +1,10 @@
// import { autoPlayGif } from '../../initial_state';
// import { putAccounts, putStatuses } from '../../storage/modifier';
import { normalizeAccount, normalizeStatus } from './normalizer';
import { normalizeAccount, normalizeStatus, normalizePoll } from './normalizer';


export const ACCOUNT_IMPORT = 'ACCOUNT_IMPORT';
export const ACCOUNT_IMPORT = 'ACCOUNT_IMPORT';
export const ACCOUNTS_IMPORT = 'ACCOUNTS_IMPORT'; export const ACCOUNTS_IMPORT = 'ACCOUNTS_IMPORT';
export const STATUS_IMPORT = 'STATUS_IMPORT';
export const STATUS_IMPORT = 'STATUS_IMPORT';
export const STATUSES_IMPORT = 'STATUSES_IMPORT'; export const STATUSES_IMPORT = 'STATUSES_IMPORT';
export const POLLS_IMPORT = 'POLLS_IMPORT';


function pushUnique(array, object) { function pushUnique(array, object) {
if (array.every(element => element.id !== object.id)) { if (array.every(element => element.id !== object.id)) {
@@ -29,6 +28,10 @@ export function importStatuses(statuses) {
return { type: STATUSES_IMPORT, statuses }; return { type: STATUSES_IMPORT, statuses };
} }


export function importPolls(polls) {
return { type: POLLS_IMPORT, polls };
}

export function importFetchedAccount(account) { export function importFetchedAccount(account) {
return importFetchedAccounts([account]); return importFetchedAccounts([account]);
} }
@@ -45,7 +48,6 @@ export function importFetchedAccounts(accounts) {
} }


accounts.forEach(processAccount); accounts.forEach(processAccount);
//putAccounts(normalAccounts, !autoPlayGif);


return importAccounts(normalAccounts); return importAccounts(normalAccounts);
} }
@@ -58,6 +60,7 @@ export function importFetchedStatuses(statuses) {
return (dispatch, getState) => { return (dispatch, getState) => {
const accounts = []; const accounts = [];
const normalStatuses = []; const normalStatuses = [];
const polls = [];


function processStatus(status) { function processStatus(status) {
pushUnique(normalStatuses, normalizeStatus(status, getState().getIn(['statuses', status.id]))); pushUnique(normalStatuses, normalizeStatus(status, getState().getIn(['statuses', status.id])));
@@ -66,12 +69,22 @@ export function importFetchedStatuses(statuses) {
if (status.reblog && status.reblog.id) { if (status.reblog && status.reblog.id) {
processStatus(status.reblog); processStatus(status.reblog);
} }

if (status.poll && status.poll.id) {
pushUnique(polls, normalizePoll(status.poll));
}
} }


statuses.forEach(processStatus); statuses.forEach(processStatus);
//putStatuses(normalStatuses);


dispatch(importPolls(polls));
dispatch(importFetchedAccounts(accounts)); dispatch(importFetchedAccounts(accounts));
dispatch(importStatuses(normalStatuses)); dispatch(importStatuses(normalStatuses));
}; };
} }

export function importFetchedPoll(poll) {
return dispatch => {
dispatch(importPolls([normalizePoll(poll)]));
};
}

+ 17
- 0
app/javascript/mastodon/actions/importer/normalizer.js View File

@@ -43,6 +43,10 @@ export function normalizeStatus(status, normalOldStatus) {
normalStatus.reblog = status.reblog.id; normalStatus.reblog = status.reblog.id;
} }


if (status.poll && status.poll.id) {
normalStatus.poll = status.poll.id;
}

// Only calculate these values when status first encountered // Only calculate these values when status first encountered
// Otherwise keep the ones already in the reducer // Otherwise keep the ones already in the reducer
if (normalOldStatus) { if (normalOldStatus) {
@@ -63,3 +67,16 @@ export function normalizeStatus(status, normalOldStatus) {


return normalStatus; return normalStatus;
} }

export function normalizePoll(poll) {
const normalPoll = { ...poll };

const emojiMap = makeEmojiMap(normalPoll);

normalPoll.options = poll.options.map(option => ({
...option,
title_emojified: emojify(escapeTextContentForBrowser(option.title), emojiMap),
}));

return normalPoll;
}

+ 3
- 1
app/javascript/mastodon/actions/notifications.js View File

@@ -7,6 +7,7 @@ import {
importFetchedStatus, importFetchedStatus,
importFetchedStatuses, importFetchedStatuses,
} from './importer'; } from './importer';
import { saveSettings } from './settings';
import { defineMessages } from 'react-intl'; import { defineMessages } from 'react-intl';
import { List as ImmutableList } from 'immutable'; import { List as ImmutableList } from 'immutable';
import { unescapeHTML } from '../utils/html'; import { unescapeHTML } from '../utils/html';
@@ -92,7 +93,7 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
const excludeTypesFromSettings = state => state.getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS(); const excludeTypesFromSettings = state => state.getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS();


const excludeTypesFromFilter = filter => { const excludeTypesFromFilter = filter => {
const allTypes = ImmutableList(['follow', 'favourite', 'reblog', 'mention']);
const allTypes = ImmutableList(['follow', 'favourite', 'reblog', 'mention', 'poll']);
return allTypes.filterNot(item => item === filter).toJS(); return allTypes.filterNot(item => item === filter).toJS();
}; };


@@ -187,5 +188,6 @@ export function setFilter (filterType) {
value: filterType, value: filterType,
}); });
dispatch(expandNotifications()); dispatch(expandNotifications());
dispatch(saveSettings());
}; };
}; };

+ 60
- 0
app/javascript/mastodon/actions/polls.js View File

@@ -0,0 +1,60 @@
import api from '../api';
import { importFetchedPoll } from './importer';

export const POLL_VOTE_REQUEST = 'POLL_VOTE_REQUEST';
export const POLL_VOTE_SUCCESS = 'POLL_VOTE_SUCCESS';
export const POLL_VOTE_FAIL = 'POLL_VOTE_FAIL';

export const POLL_FETCH_REQUEST = 'POLL_FETCH_REQUEST';
export const POLL_FETCH_SUCCESS = 'POLL_FETCH_SUCCESS';
export const POLL_FETCH_FAIL = 'POLL_FETCH_FAIL';

export const vote = (pollId, choices) => (dispatch, getState) => {
dispatch(voteRequest());

api(getState).post(`/api/v1/polls/${pollId}/votes`, { choices })
.then(({ data }) => {
dispatch(importFetchedPoll(data));
dispatch(voteSuccess(data));
})
.catch(err => dispatch(voteFail(err)));
};

export const fetchPoll = pollId => (dispatch, getState) => {
dispatch(fetchPollRequest());

api(getState).get(`/api/v1/polls/${pollId}`)
.then(({ data }) => {
dispatch(importFetchedPoll(data));
dispatch(fetchPollSuccess(data));
})
.catch(err => dispatch(fetchPollFail(err)));
};

export const voteRequest = () => ({
type: POLL_VOTE_REQUEST,
});

export const voteSuccess = poll => ({
type: POLL_VOTE_SUCCESS,
poll,
});

export const voteFail = error => ({
type: POLL_VOTE_FAIL,
error,
});

export const fetchPollRequest = () => ({
type: POLL_FETCH_REQUEST,
});

export const fetchPollSuccess = poll => ({
type: POLL_FETCH_SUCCESS,
poll,
});

export const fetchPollFail = error => ({
type: POLL_FETCH_FAIL,
error,
});

+ 1
- 0
app/javascript/mastodon/actions/search.js View File

@@ -37,6 +37,7 @@ export function submitSearch() {
params: { params: {
q: value, q: value,
resolve: true, resolve: true,
limit: 5,
}, },
}).then(response => { }).then(response => {
if (response.data.accounts) { if (response.data.accounts) {


+ 5
- 1
app/javascript/mastodon/actions/statuses.js View File

@@ -140,7 +140,11 @@ export function redraft(status) {


export function deleteStatus(id, router, withRedraft = false) { export function deleteStatus(id, router, withRedraft = false) {
return (dispatch, getState) => { return (dispatch, getState) => {
const status = getState().getIn(['statuses', id]);
let status = getState().getIn(['statuses', id]);

if (status.get('poll')) {
status = status.set('poll', getState().getIn(['polls', status.get('poll')]));
}


dispatch(deleteStatusRequest(id)); dispatch(deleteStatusRequest(id));




+ 6
- 0
app/javascript/mastodon/actions/streaming.js View File

@@ -3,6 +3,7 @@ import {
updateTimeline, updateTimeline,
deleteFromTimelines, deleteFromTimelines,
expandHomeTimeline, expandHomeTimeline,
connectTimeline,
disconnectTimeline, disconnectTimeline,
} from './timelines'; } from './timelines';
import { updateNotifications, expandNotifications } from './notifications'; import { updateNotifications, expandNotifications } from './notifications';
@@ -16,7 +17,12 @@ export function connectTimelineStream (timelineId, path, pollingRefresh = null,


return connectStream (path, pollingRefresh, (dispatch, getState) => { return connectStream (path, pollingRefresh, (dispatch, getState) => {
const locale = getState().getIn(['meta', 'locale']); const locale = getState().getIn(['meta', 'locale']);

return { return {
onConnect() {
dispatch(connectTimeline(timelineId));
},

onDisconnect() { onDisconnect() {
dispatch(disconnectTimeline(timelineId)); dispatch(disconnectTimeline(timelineId));
}, },


+ 8
- 0
app/javascript/mastodon/actions/timelines.js View File

@@ -12,6 +12,7 @@ export const TIMELINE_EXPAND_FAIL = 'TIMELINE_EXPAND_FAIL';


export const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP'; export const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP';


export const TIMELINE_CONNECT = 'TIMELINE_CONNECT';
export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT'; export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT';


export function updateTimeline(timeline, status, accept) { export function updateTimeline(timeline, status, accept) {
@@ -143,6 +144,13 @@ export function scrollTopTimeline(timeline, top) {
}; };
}; };


export function connectTimeline(timeline) {
return {
type: TIMELINE_CONNECT,
timeline,
};
};

export function disconnectTimeline(timeline) { export function disconnectTimeline(timeline) {
return { return {
type: TIMELINE_DISCONNECT, type: TIMELINE_DISCONNECT,


+ 6
- 2
app/javascript/mastodon/api.js View File

@@ -13,10 +13,14 @@ export const getLinks = response => {
}; };


let csrfHeader = {}; let csrfHeader = {};

function setCSRFHeader() { function setCSRFHeader() {
const csrfToken = document.querySelector('meta[name=csrf-token]').content;
csrfHeader['X-CSRF-Token'] = csrfToken;
const csrfToken = document.querySelector('meta[name=csrf-token]');
if (csrfToken) {
csrfHeader['X-CSRF-Token'] = csrfToken.content;
}
} }

ready(setCSRFHeader); ready(setCSRFHeader);


export default getState => axios.create({ export default getState => axios.create({


+ 1
- 1
app/javascript/mastodon/components/account.js View File

@@ -88,7 +88,7 @@ class Account extends ImmutablePureComponent {
if (requested) { if (requested) {
buttons = <IconButton disabled icon='hourglass' title={intl.formatMessage(messages.requested)} />; buttons = <IconButton disabled icon='hourglass' title={intl.formatMessage(messages.requested)} />;
} else if (blocking) { } else if (blocking) {
buttons = <IconButton active icon='unlock-alt' title={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.handleBlock} />;
buttons = <IconButton active icon='unlock' title={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.handleBlock} />;
} else if (muting) { } else if (muting) {
let hidingNotificationsButton; let hidingNotificationsButton;
if (account.getIn(['relationship', 'muting_notifications'])) { if (account.getIn(['relationship', 'muting_notifications'])) {


+ 3
- 2
app/javascript/mastodon/components/attachment_list.js View File

@@ -2,6 +2,7 @@ import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import Icon from 'mastodon/components/icon';


const filename = url => url.split('/').pop().split('#')[0].split('?')[0]; const filename = url => url.split('/').pop().split('#')[0].split('?')[0];


@@ -24,7 +25,7 @@ export default class AttachmentList extends ImmutablePureComponent {


return ( return (
<li key={attachment.get('id')}> <li key={attachment.get('id')}>
<a href={displayUrl} target='_blank' rel='noopener'><i className='fa fa-link' /> {filename(displayUrl)}</a>
<a href={displayUrl} target='_blank' rel='noopener'><Icon id='link' /> {filename(displayUrl)}</a>
</li> </li>
); );
})} })}
@@ -36,7 +37,7 @@ export default class AttachmentList extends ImmutablePureComponent {
return ( return (
<div className='attachment-list'> <div className='attachment-list'>
<div className='attachment-list__icon'> <div className='attachment-list__icon'>
<i className='fa fa-link' />
<Icon id='link' />
</div> </div>


<ul className='attachment-list__list'> <ul className='attachment-list__list'>


+ 2
- 1
app/javascript/mastodon/components/column_back_button.js View File

@@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Icon from 'mastodon/components/icon';


export default class ColumnBackButton extends React.PureComponent { export default class ColumnBackButton extends React.PureComponent {


@@ -19,7 +20,7 @@ export default class ColumnBackButton extends React.PureComponent {
render () { render () {
return ( return (
<button onClick={this.handleClick} className='column-back-button'> <button onClick={this.handleClick} className='column-back-button'>
<i className='fa fa-fw fa-chevron-left column-back-button__icon' />
<Icon id='chevron-left' className='column-back-button__icon' fixedWidth />
<FormattedMessage id='column_back_button.label' defaultMessage='Back' /> <FormattedMessage id='column_back_button.label' defaultMessage='Back' />
</button> </button>
); );


+ 2
- 1
app/javascript/mastodon/components/column_back_button_slim.js View File

@@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import ColumnBackButton from './column_back_button'; import ColumnBackButton from './column_back_button';
import Icon from 'mastodon/components/icon';


export default class ColumnBackButtonSlim extends ColumnBackButton { export default class ColumnBackButtonSlim extends ColumnBackButton {


@@ -8,7 +9,7 @@ export default class ColumnBackButtonSlim extends ColumnBackButton {
return ( return (
<div className='column-back-button--slim'> <div className='column-back-button--slim'>
<div role='button' tabIndex='0' onClick={this.handleClick} className='column-back-button column-back-button--slim-button'> <div role='button' tabIndex='0' onClick={this.handleClick} className='column-back-button column-back-button--slim-button'>
<i className='fa fa-fw fa-chevron-left column-back-button__icon' />
<Icon id='chevron-left' className='column-back-button__icon' fixedWidth />
<FormattedMessage id='column_back_button.label' defaultMessage='Back' /> <FormattedMessage id='column_back_button.label' defaultMessage='Back' />
</div> </div>
</div> </div>


+ 8
- 7
app/javascript/mastodon/components/column_header.js View File

@@ -2,6 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classNames from 'classnames'; import classNames from 'classnames';
import { FormattedMessage, injectIntl, defineMessages } from 'react-intl'; import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
import Icon from 'mastodon/components/icon';


const messages = defineMessages({ const messages = defineMessages({
show: { id: 'column_header.show_settings', defaultMessage: 'Show settings' }, show: { id: 'column_header.show_settings', defaultMessage: 'Show settings' },
@@ -109,22 +110,22 @@ class ColumnHeader extends React.PureComponent {
} }


if (multiColumn && pinned) { if (multiColumn && pinned) {
pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={this.handlePin}><i className='fa fa fa-times' /> <FormattedMessage id='column_header.unpin' defaultMessage='Unpin' /></button>;
pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={this.handlePin}><Icon id='times' /> <FormattedMessage id='column_header.unpin' defaultMessage='Unpin' /></button>;


moveButtons = ( moveButtons = (
<div key='move-buttons' className='column-header__setting-arrows'> <div key='move-buttons' className='column-header__setting-arrows'>
<button title={formatMessage(messages.moveLeft)} aria-label={formatMessage(messages.moveLeft)} className='text-btn column-header__setting-btn' onClick={this.handleMoveLeft}><i className='fa fa-chevron-left' /></button>
<button title={formatMessage(messages.moveRight)} aria-label={formatMessage(messages.moveRight)} className='text-btn column-header__setting-btn' onClick={this.handleMoveRight}><i className='fa fa-chevron-right' /></button>
<button title={formatMessage(messages.moveLeft)} aria-label={formatMessage(messages.moveLeft)} className='text-btn column-header__setting-btn' onClick={this.handleMoveLeft}><Icon id='chevron-left' /></button>
<button title={formatMessage(messages.moveRight)} aria-label={formatMessage(messages.moveRight)} className='text-btn column-header__setting-btn' onClick={this.handleMoveRight}><Icon id='chevron-right' /></button>
</div> </div>
); );
} else if (multiColumn) { } else if (multiColumn) {
pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={this.handlePin}><i className='fa fa fa-plus' /> <FormattedMessage id='column_header.pin' defaultMessage='Pin' /></button>;
pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={this.handlePin}><Icon id='plus' /> <FormattedMessage id='column_header.pin' defaultMessage='Pin' /></button>;
} }


if (!pinned && (multiColumn || showBackButton)) { if (!pinned && (multiColumn || showBackButton)) {
backButton = ( backButton = (
<button onClick={this.handleBackClick} className='column-header__back-button'> <button onClick={this.handleBackClick} className='column-header__back-button'>
<i className='fa fa-fw fa-chevron-left column-back-button__icon' />
<Icon id='chevron-left' className='column-back-button__icon' fixedWidth />
<FormattedMessage id='column_back_button.label' defaultMessage='Back' /> <FormattedMessage id='column_back_button.label' defaultMessage='Back' />
</button> </button>
); );
@@ -140,7 +141,7 @@ class ColumnHeader extends React.PureComponent {
} }


if (children || multiColumn) { if (children || multiColumn) {
collapseButton = <button className={collapsibleButtonClassName} title={formatMessage(collapsed ? messages.show : messages.hide)} aria-label={formatMessage(collapsed ? messages.show : messages.hide)} aria-pressed={collapsed ? 'false' : 'true'} onClick={this.handleToggleClick}><i className='fa fa-sliders' /></button>;
collapseButton = <button className={collapsibleButtonClassName} title={formatMessage(collapsed ? messages.show : messages.hide)} aria-label={formatMessage(collapsed ? messages.show : messages.hide)} aria-pressed={collapsed ? 'false' : 'true'} onClick={this.handleToggleClick}><Icon id='sliders' /></button>;
} }


const hasTitle = icon && title; const hasTitle = icon && title;
@@ -150,7 +151,7 @@ class ColumnHeader extends React.PureComponent {
<h1 className={buttonClassName}> <h1 className={buttonClassName}>
{hasTitle && ( {hasTitle && (
<button onClick={this.handleTitleClick}> <button onClick={this.handleTitleClick}>
<i className={`fa fa-fw fa-${icon} column-header__icon`} />
<Icon id={icon} fixedWidth className='column-header__icon' />
{title} {title}
</button> </button>
)} )}


+ 16
- 6
app/javascript/mastodon/components/display_name.js View File

@@ -11,26 +11,36 @@ export default class DisplayName extends React.PureComponent {
}; };


render () { render () {
const { account, others, localDomain } = this.props;
const displayNameHtml = { __html: account.get('display_name_html') };
const { others, localDomain } = this.props;


let suffix;
let displayName, suffix, account;


if (others && others.size > 1) { if (others && others.size > 1) {
suffix = `+${others.size}`;
displayName = others.take(2).map(a => <bdi key={a.get('id')}><strong className='display-name__html' dangerouslySetInnerHTML={{ __html: a.get('display_name_html') }} /></bdi>).reduce((prev, cur) => [prev, ', ', cur]);

if (others.size - 2 > 0) {
suffix = `+${others.size - 2}`;
}
} else { } else {
if (others && others.size > 0) {
account = others.first();
} else {
account = this.props.account;
}

let acct = account.get('acct'); let acct = account.get('acct');


if (acct.indexOf('@') === -1 && localDomain) { if (acct.indexOf('@') === -1 && localDomain) {
acct = `${acct}@${localDomain}`; acct = `${acct}@${localDomain}`;
} }


suffix = <span className='display-name__account'>@{acct}</span>;
displayName = <bdi><strong className='display-name__html' dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }} /></bdi>;
suffix = <span className='display-name__account'>@{acct}</span>;
} }


return ( return (
<span className='display-name'> <span className='display-name'>
<bdi><strong className='display-name__html' dangerouslySetInnerHTML={displayNameHtml} /></bdi> {suffix}
{displayName} {suffix}
</span> </span>
); );
} }


+ 1
- 1
app/javascript/mastodon/components/domain.js View File

@@ -32,7 +32,7 @@ class Account extends ImmutablePureComponent {
</span> </span>


<div className='domain__buttons'> <div className='domain__buttons'>
<IconButton active icon='unlock-alt' title={intl.formatMessage(messages.unblockDomain, { domain })} onClick={this.handleDomainUnblock} />
<IconButton active icon='unlock' title={intl.formatMessage(messages.unblockDomain, { domain })} onClick={this.handleDomainUnblock} />
</div> </div>
</div> </div>
</div> </div>


+ 39
- 0
app/javascript/mastodon/components/error_boundary.js View File

@@ -0,0 +1,39 @@
import React from 'react';
import PropTypes from 'prop-types';
import illustration from '../../images/elephant_ui_disappointed.svg';

export default class ErrorBoundary extends React.PureComponent {

static propTypes = {
children: PropTypes.node,
};

state = {
hasError: false,
stackTrace: undefined,
componentStack: undefined,
}

componentDidCatch(error, info) {
this.setState({
hasError: true,
stackTrace: error.stack,
componentStack: info && info.componentStack,
});
}

render() {
const { hasError } = this.state;

if (!hasError) {
return this.props.children;
}

return (
<div>
<img src={illustration} alt='' />
</div>
);
}

}

+ 21
- 0
app/javascript/mastodon/components/icon.js View File

@@ -0,0 +1,21 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';

export default class Icon extends React.PureComponent {

static propTypes = {
id: PropTypes.string.isRequired,
className: PropTypes.string,
fixedWidth: PropTypes.bool,
};

render () {
const { id, className, fixedWidth, ...other } = this.props;

return (
<i role='img' className={classNames('fa', `fa-${id}`, className, { 'fa-fw': fixedWidth })} {...other} />
);
}

}

+ 5
- 2
app/javascript/mastodon/components/icon_button.js View File

@@ -3,6 +3,7 @@ import Motion from '../features/ui/util/optional_motion';
import spring from 'react-motion/lib/spring'; import spring from 'react-motion/lib/spring';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classNames from 'classnames'; import classNames from 'classnames';
import Icon from 'mastodon/components/icon';


export default class IconButton extends React.PureComponent { export default class IconButton extends React.PureComponent {


@@ -85,8 +86,9 @@ export default class IconButton extends React.PureComponent {
onClick={this.handleClick} onClick={this.handleClick}
style={style} style={style}
tabIndex={tabIndex} tabIndex={tabIndex}
disabled={disabled}
> >
<i className={`fa fa-fw fa-${icon}`} aria-hidden='true' />
<Icon id={icon} fixedWidth aria-hidden='true' />
</button> </button>
); );
} }
@@ -103,8 +105,9 @@ export default class IconButton extends React.PureComponent {
onClick={this.handleClick} onClick={this.handleClick}
style={style} style={style}
tabIndex={tabIndex} tabIndex={tabIndex}
disabled={disabled}
> >
<i style={{ transform: `rotate(${rotate}deg)` }} className={`fa fa-fw fa-${icon}`} aria-hidden='true' />
<Icon id={icon} style={{ transform: `rotate(${rotate}deg)` }} fixedWidth aria-hidden='true' />
</button> </button>
)} )}
</Motion> </Motion>


+ 1
- 1
app/javascript/mastodon/components/intersection_observer_article.js View File

@@ -65,7 +65,7 @@ export default class IntersectionObserverArticle extends React.Component {
} }


updateStateAfterIntersection = (prevState) => { updateStateAfterIntersection = (prevState) => {
if (prevState.isIntersecting && !this.entry.isIntersecting) {
if (prevState.isIntersecting !== false && !this.entry.isIntersecting) {
scheduleIdleTask(this.hideIfNotIntersecting); scheduleIdleTask(this.hideIfNotIntersecting);
} }
return { return {


+ 2
- 1
app/javascript/mastodon/components/load_gap.js View File

@@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { injectIntl, defineMessages } from 'react-intl'; import { injectIntl, defineMessages } from 'react-intl';
import Icon from 'mastodon/components/icon';


const messages = defineMessages({ const messages = defineMessages({
load_more: { id: 'status.load_more', defaultMessage: 'Load more' }, load_more: { id: 'status.load_more', defaultMessage: 'Load more' },
@@ -25,7 +26,7 @@ class LoadGap extends React.PureComponent {


return ( return (
<button className='load-more load-gap' disabled={disabled} onClick={this.handleClick} aria-label={intl.formatMessage(messages.load_more)}> <button className='load-more load-gap' disabled={disabled} onClick={this.handleClick} aria-label={intl.formatMessage(messages.load_more)}>
<i className='fa fa-ellipsis-h' />
<Icon id='ellipsis-h' />
</button> </button>
); );
} }


+ 8
- 2
app/javascript/mastodon/components/media_gallery.js View File

@@ -194,6 +194,8 @@ class MediaGallery extends React.PureComponent {
height: PropTypes.number.isRequired, height: PropTypes.number.isRequired,
onOpenMedia: PropTypes.func.isRequired, onOpenMedia: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
defaultWidth: PropTypes.number,
cacheWidth: PropTypes.func,
}; };


static defaultProps = { static defaultProps = {
@@ -202,6 +204,7 @@ class MediaGallery extends React.PureComponent {


state = { state = {
visible: displayMedia !== 'hide_all' && !this.props.sensitive || displayMedia === 'show_all', visible: displayMedia !== 'hide_all' && !this.props.sensitive || displayMedia === 'show_all',
width: this.props.defaultWidth,
}; };


componentWillReceiveProps (nextProps) { componentWillReceiveProps (nextProps) {
@@ -221,6 +224,7 @@ class MediaGallery extends React.PureComponent {
handleRef = (node) => { handleRef = (node) => {
if (node /*&& this.isStandaloneEligible()*/) { if (node /*&& this.isStandaloneEligible()*/) {
// offsetWidth triggers a layout, so only calculate when we need to // offsetWidth triggers a layout, so only calculate when we need to
if (this.props.cacheWidth) this.props.cacheWidth(node.offsetWidth);
this.setState({ this.setState({
width: node.offsetWidth, width: node.offsetWidth,
}); });
@@ -233,8 +237,10 @@ class MediaGallery extends React.PureComponent {
} }


render () { render () {
const { media, intl, sensitive, height } = this.props;
const { width, visible } = this.state;
const { media, intl, sensitive, height, defaultWidth } = this.props;
const { visible } = this.state;

const width = this.state.width || defaultWidth;


let children; let children;




+ 140
- 0
app/javascript/mastodon/components/poll.js View File

@@ -0,0 +1,140 @@
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { vote, fetchPoll } from 'mastodon/actions/polls';
import Motion from 'mastodon/features/ui/util/optional_motion';
import spring from 'react-motion/lib/spring';
import escapeTextContentForBrowser from 'escape-html';
import emojify from 'mastodon/features/emoji/emoji';
import RelativeTimestamp from './relative_timestamp';

const messages = defineMessages({
closed: { id: 'poll.closed', defaultMessage: 'Closed' },
});

const makeEmojiMap = record => record.get('emojis').reduce((obj, emoji) => {
obj[`:${emoji.get('shortcode')}:`] = emoji.toJS();
return obj;
}, {});

export default @injectIntl
class Poll extends ImmutablePureComponent {

static propTypes = {
poll: ImmutablePropTypes.map,
intl: PropTypes.object.isRequired,
dispatch: PropTypes.func,
disabled: PropTypes.bool,
};

state = {
selected: {},
};

handleOptionChange = e => {
const { target: { value } } = e;

if (this.props.poll.get('multiple')) {
const tmp = { ...this.state.selected };
if (tmp[value]) {
delete tmp[value];
} else {
tmp[value] = true;
}
this.setState({ selected: tmp });
} else {
const tmp = {};
tmp[value] = true;
this.setState({ selected: tmp });
}
};

handleVote = () => {
if (this.props.disabled) {
return;
}

this.props.dispatch(vote(this.props.poll.get('id'), Object.keys(this.state.selected)));
};

handleRefresh = () => {
if (this.props.disabled) {
return;
}

this.props.dispatch(fetchPoll(this.props.poll.get('id')));
};

renderOption (option, optionIndex) {
const { poll, disabled } = this.props;
const percent = poll.get('votes_count') === 0 ? 0 : (option.get('votes_count') / poll.get('votes_count')) * 100;
const leading = poll.get('options').filterNot(other => other.get('title') === option.get('title')).every(other => option.get('votes_count') > other.get('votes_count'));
const active = !!this.state.selected[`${optionIndex}`];
const showResults = poll.get('voted') || poll.get('expired');

let titleEmojified = option.get('title_emojified');
if (!titleEmojified) {
const emojiMap = makeEmojiMap(poll);
titleEmojified = emojify(escapeTextContentForBrowser(option.get('title')), emojiMap);
}

return (
<li key={option.get('title')}>
{showResults && (
<Motion defaultStyle={{ width: 0 }} style={{ width: spring(percent, { stiffness: 180, damping: 12 }) }}>
{({ width }) =>
<span className={classNames('poll__chart', { leading })} style={{ width: `${width}%` }} />
}
</Motion>
)}

<label className={classNames('poll__text', { selectable: !showResults })}>
<input
name='vote-options'
type={poll.get('multiple') ? 'checkbox' : 'radio'}
value={optionIndex}
checked={active}
onChange={this.handleOptionChange}
disabled={disabled}
/>

{!showResults && <span className={classNames('poll__input', { checkbox: poll.get('multiple'), active })} />}
{showResults && <span className='poll__number'>{Math.round(percent)}%</span>}

<span dangerouslySetInnerHTML={{ __html: titleEmojified }} />
</label>
</li>
);
}

render () {
const { poll, intl } = this.props;

if (!poll) {
return null;
}

const timeRemaining = poll.get('expired') ? intl.formatMessage(messages.closed) : <RelativeTimestamp timestamp={poll.get('expires_at')} futureDate />;
const showResults = poll.get('voted') || poll.get('expired');
const disabled = this.props.disabled || Object.entries(this.state.selected).every(item => !item);

return (
<div className='poll'>
<ul>
{poll.get('options').map((option, i) => this.renderOption(option, i))}
</ul>

<div className='poll__footer'>
{!showResults && <button className='button button-secondary' disabled={disabled} onClick={this.handleVote}><FormattedMessage id='poll.vote' defaultMessage='Vote' /></button>}
{showResults && !this.props.disabled && <span><button className='poll__link' onClick={this.handleRefresh}><FormattedMessage id='poll.refresh' defaultMessage='Refresh' /></button> · </span>}
<FormattedMessage id='poll.total_votes' defaultMessage='{count, plural, one {# vote} other {# votes}}' values={{ count: poll.get('votes_count') }} />
{poll.get('expires_at') && <span> · {timeRemaining}</span>}
</div>
</div>
);
}

}

+ 28
- 2
app/javascript/mastodon/components/relative_timestamp.js View File

@@ -8,6 +8,11 @@ const messages = defineMessages({
minutes: { id: 'relative_time.minutes', defaultMessage: '{number}m' }, minutes: { id: 'relative_time.minutes', defaultMessage: '{number}m' },
hours: { id: 'relative_time.hours', defaultMessage: '{number}h' }, hours: { id: 'relative_time.hours', defaultMessage: '{number}h' },
days: { id: 'relative_time.days', defaultMessage: '{number}d' }, days: { id: 'relative_time.days', defaultMessage: '{number}d' },
moments_remaining: { id: 'time_remaining.moments', defaultMessage: 'Moments remaining' },
seconds_remaining: { id: 'time_remaining.seconds', defaultMessage: '{number, plural, one {# second} other {# seconds}} left' },
minutes_remaining: { id: 'time_remaining.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}} left' },
hours_remaining: { id: 'time_remaining.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}} left' },
days_remaining: { id: 'time_remaining.days', defaultMessage: '{number, plural, one {# day} other {# days}} left' },
}); });


const dateFormatOptions = { const dateFormatOptions = {
@@ -86,6 +91,26 @@ export const timeAgoString = (intl, date, now, year) => {
return relativeTime; return relativeTime;
}; };


const timeRemainingString = (intl, date, now) => {
const delta = date.getTime() - now;

let relativeTime;

if (delta < 10 * SECOND) {
relativeTime = intl.formatMessage(messages.moments_remaining);
} else if (delta < MINUTE) {
relativeTime = intl.formatMessage(messages.seconds_remaining, { number: Math.floor(delta / SECOND) });
} else if (delta < HOUR) {
relativeTime = intl.formatMessage(messages.minutes_remaining, { number: Math.floor(delta / MINUTE) });
} else if (delta < DAY) {
relativeTime = intl.formatMessage(messages.hours_remaining, { number: Math.floor(delta / HOUR) });
} else {
relativeTime = intl.formatMessage(messages.days_remaining, { number: Math.floor(delta / DAY) });
}

return relativeTime;
};

export default @injectIntl export default @injectIntl
class RelativeTimestamp extends React.Component { class RelativeTimestamp extends React.Component {


@@ -93,6 +118,7 @@ class RelativeTimestamp extends React.Component {
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
timestamp: PropTypes.string.isRequired, timestamp: PropTypes.string.isRequired,
year: PropTypes.number.isRequired, year: PropTypes.number.isRequired,
futureDate: PropTypes.bool,
}; };


state = { state = {
@@ -145,10 +171,10 @@ class RelativeTimestamp extends React.Component {
} }


render () { render () {
const { timestamp, intl, year } = this.props;
const { timestamp, intl, year, futureDate } = this.props;


const date = new Date(timestamp); const date = new Date(timestamp);
const relativeTime = timeAgoString(intl, date, this.state.now, year);
const relativeTime = futureDate ? timeRemainingString(intl, date, this.state.now) : timeAgoString(intl, date, this.state.now, year);


return ( return (
<time dateTime={timestamp} title={intl.formatDate(date, dateFormatOptions)}> <time dateTime={timestamp} title={intl.formatDate(date, dateFormatOptions)}>


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

Loading…
Cancel
Save