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',
'\\.(css|scss|json)$',
],
'import/resolver': {
node: {
paths: ['app/javascript'],
},
},
},

rules: {


+ 1
- 1
.rubocop.yml View File

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

Style/ClassAndModuleChildren:
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)
* [ykzts](https://github.com/ykzts)
* [akihikodaki](https://github.com/akihikodaki)
* [ThibG](https://github.com/ThibG)
* [akihikodaki](https://github.com/akihikodaki)
* [mjankowski](https://github.com/mjankowski)
* [dependabot[bot]](https://github.com/apps/dependabot)
* [unarist](https://github.com/unarist)
* [m4sk1n](https://github.com/m4sk1n)
* [dependabot[bot]](https://github.com/apps/dependabot)
* [yiskah](https://github.com/yiskah)
* [nolanlawson](https://github.com/nolanlawson)
* [sorin-davidoi](https://github.com/sorin-davidoi)
* [ysksn](https://github.com/ysksn)
* [sorin-davidoi](https://github.com/sorin-davidoi)
* [abcang](https://github.com/abcang)
* [lynlynlynx](https://github.com/lynlynlynx)
* [alpaca-tc](https://github.com/alpaca-tc)
* [mayaeh](https://github.com/mayaeh)
* [renatolond](https://github.com/renatolond)
* [alpaca-tc](https://github.com/alpaca-tc)
* [nclm](https://github.com/nclm)
* [ineffyble](https://github.com/ineffyble)
* [jeroenpraat](https://github.com/jeroenpraat)
* [blackle](https://github.com/blackle)
* [Quent-in](https://github.com/Quent-in)
* [JantsoP](https://github.com/JantsoP)
* [Kjwon15](https://github.com/Kjwon15)
* [mabkenar](https://github.com/mabkenar)
* [nullkal](https://github.com/nullkal)
* [yookoala](https://github.com/yookoala)
* [Kjwon15](https://github.com/Kjwon15)
* [shuheiktgw](https://github.com/shuheiktgw)
* [ashfurrow](https://github.com/ashfurrow)
* [Quenty31](https://github.com/Quenty31)
* [zunda](https://github.com/zunda)
* [Quenty31](https://github.com/Quenty31)
* [eramdam](https://github.com/eramdam)
* [takayamaki](https://github.com/takayamaki)
* [masarakki](https://github.com/masarakki)
@@ -45,30 +45,33 @@ and provided thanks to the work of the following contributors:
* [stephenburgess8](https://github.com/stephenburgess8)
* [Wonderfall](https://github.com/Wonderfall)
* [matteoaquila](https://github.com/matteoaquila)
* [rkarabut](https://github.com/rkarabut)
* [yukimochi](https://github.com/yukimochi)
* [rkarabut](https://github.com/rkarabut)
* [Artoria2e5](https://github.com/Artoria2e5)
* [nightpool](https://github.com/nightpool)
* [marrus-sh](https://github.com/marrus-sh)
* [krainboltgreene](https://github.com/krainboltgreene)
* [patf](https://github.com/patf)
* [pfigel](https://github.com/pfigel)
* [Aldarone](https://github.com/Aldarone)
* [BoFFire](https://github.com/BoFFire)
* [clworld](https://github.com/clworld)
* [dracos](https://github.com/dracos)
* [SerCom_KC](mailto:sercom-kc@users.noreply.github.com)
* [Sylvhem](https://github.com/Sylvhem)
* [nightpool](https://github.com/nightpool)
* [MasterGroosha](https://github.com/MasterGroosha)
* [JeanGauthier](https://github.com/JeanGauthier)
* [kschaper](https://github.com/kschaper)
* [MaciekBaron](https://github.com/MaciekBaron)
* [MitarashiDango](mailto:mitarashidango@users.noreply.github.com)
* [beatrix-bitrot](https://github.com/beatrix-bitrot)
* [Aditoo17](https://github.com/Aditoo17)
* [adbelle](https://github.com/adbelle)
* [evanminto](https://github.com/evanminto)
* [MightyPork](https://github.com/MightyPork)
* [yhirano55](https://github.com/yhirano55)
* [rinsuki](https://github.com/rinsuki)
* [camponez](https://github.com/camponez)
* [hinaloe](https://github.com/hinaloe)
* [SerCom-KC](https://github.com/SerCom-KC)
* [aschmitz](https://github.com/aschmitz)
* [devkral](https://github.com/devkral)
@@ -77,6 +80,7 @@ and provided thanks to the work of the following contributors:
* [johnsudaar](https://github.com/johnsudaar)
* [trebmuh](https://github.com/trebmuh)
* [Rakib Hasan](mailto:rmhasan@gmail.com)
* [ashleyhull-versent](https://github.com/ashleyhull-versent)
* [lindwurm](https://github.com/lindwurm)
* [victorhck](mailto:victorhck@geeko.site)
* [voidsatisfaction](https://github.com/voidsatisfaction)
@@ -92,20 +96,21 @@ and provided thanks to the work of the following contributors:
* [dunn](https://github.com/dunn)
* [xqus](https://github.com/xqus)
* [hugogameiro](https://github.com/hugogameiro)
* [ariasuni](https://github.com/ariasuni)
* [pfm-eyesightjp](https://github.com/pfm-eyesightjp)
* [fakenine](https://github.com/fakenine)
* [tsuwatch](https://github.com/tsuwatch)
* [victorhck](https://github.com/victorhck)
* [ashleyhull-versent](https://github.com/ashleyhull-versent)
* [kedamaDQ](https://github.com/kedamaDQ)
* [puckipedia](https://github.com/puckipedia)
* [trwnh](https://github.com/trwnh)
* [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)
* [Komic](https://github.com/Komic)
* [lmorchard](https://github.com/lmorchard)
* [diomed](https://github.com/diomed)
* [ariasuni](https://github.com/ariasuni)
* [Neetshin](mailto:neetshin@neetsh.in)
* [rainyday](https://github.com/rainyday)
* [ProgVal](https://github.com/ProgVal)
@@ -114,7 +119,7 @@ and provided thanks to the work of the following contributors:
* [goofy-bz](mailto:goofy@babelzilla.org)
* [kadiix](https://github.com/kadiix)
* [kodacs](https://github.com/kodacs)
* [rtucker](https://github.com/rtucker)
* [JMendyk](https://github.com/JMendyk)
* [KScl](https://github.com/KScl)
* [sterdev](https://github.com/sterdev)
* [TheKinrar](https://github.com/TheKinrar)
@@ -125,16 +130,17 @@ and provided thanks to the work of the following contributors:
* [fhemberger](https://github.com/fhemberger)
* [greysteil](https://github.com/greysteil)
* [hensmith](https://github.com/hensmith)
* [hinaloe](https://github.com/hinaloe)
* [d6rkaiz](https://github.com/d6rkaiz)
* [Reverite](https://github.com/Reverite)
* [JMendyk](https://github.com/JMendyk)
* [JohnD28](https://github.com/JohnD28)
* [znz](https://github.com/znz)
* [marek-lach](https://github.com/marek-lach)
* [Naouak](https://github.com/Naouak)
* [pawelngei](https://github.com/pawelngei)
* [rtucker](https://github.com/rtucker)
* [reneklacan](https://github.com/reneklacan)
* [ekiru](https://github.com/ekiru)
* [noellabo](https://github.com/noellabo)
* [tcitworld](https://github.com/tcitworld)
* [geta6](https://github.com/geta6)
* [happycoloredbanana](https://github.com/happycoloredbanana)
@@ -144,9 +150,8 @@ and provided thanks to the work of the following contributors:
* [noraworld](https://github.com/noraworld)
* [theboss](https://github.com/theboss)
* [178inaba](https://github.com/178inaba)
* [Aditoo17](https://github.com/Aditoo17)
* [alyssais](https://github.com/alyssais)
* [kodnaplakal](https://github.com/kodnaplakal)
* [hiphref](https://github.com/hiphref)
* [stalker314314](https://github.com/stalker314314)
* [huertanix](https://github.com/huertanix)
* [genesixx](https://github.com/genesixx)
@@ -162,11 +167,11 @@ and provided thanks to the work of the following contributors:
* [pierreozoux](https://github.com/pierreozoux)
* [qguv](https://github.com/qguv)
* [Ram Lmn](mailto:ramlmn@users.noreply.github.com)
* [sascha-sl](https://github.com/sascha-sl)
* [harukasan](https://github.com/harukasan)
* [stamak](https://github.com/stamak)
* [noellabo](https://github.com/noellabo)
* [Technowix](mailto:technowix@users.noreply.github.com)
* [Eychics](https://github.com/Eychics)
* [Zoeille](https://github.com/Zoeille)
* [Thor Harald Johansen](mailto:thj@thj.no)
* [0x70b1a5](https://github.com/0x70b1a5)
* [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)
* [luzi82](https://github.com/luzi82)
* [duxovni](https://github.com/duxovni)
* [trwnh](https://github.com/trwnh)
* [tmm576](https://github.com/tmm576)
* [unsmell](https://github.com/unsmell)
* [valerauko](https://github.com/valerauko)
* [chriswmartin](https://github.com/chriswmartin)
* [vahnj](https://github.com/vahnj)
* [ikuradon](https://github.com/ikuradon)
* [AndreLewin](https://github.com/AndreLewin)
* [rinsuki](https://github.com/rinsuki)
* [0xflotus](https://github.com/0xflotus)
* [redtachyons](https://github.com/redtachyons)
* [thurloat](https://github.com/thurloat)
* [aaribaud](https://github.com/aaribaud)
* [pointlessone](https://github.com/pointlessone)
* [Andrew](mailto:andrewlchronister@gmail.com)
* [estuans](https://github.com/estuans)
* [BenLubar](https://github.com/BenLubar)
* [dissolve](https://github.com/dissolve)
* [PurpleBooth](https://github.com/PurpleBooth)
* [bradurani](https://github.com/bradurani)
@@ -216,6 +220,7 @@ and provided thanks to the work of the following contributors:
* [ErikXXon](https://github.com/ErikXXon)
* [ian-kelling](https://github.com/ian-kelling)
* [immae](https://github.com/immae)
* [J0WI](https://github.com/J0WI)
* [foozmeat](https://github.com/foozmeat)
* [jasonrhodes](https://github.com/jasonrhodes)
* [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)
* [alimony](https://github.com/alimony)
* [mig5](https://github.com/mig5)
* [moritzheiber](https://github.com/moritzheiber)
* [ndarville](https://github.com/ndarville)
* [Abzol](https://github.com/Abzol)
* [pwoolcoc](https://github.com/pwoolcoc)
@@ -238,9 +244,10 @@ and provided thanks to the work of the following contributors:
* [ignisf](https://github.com/ignisf)
* [raymestalez](https://github.com/raymestalez)
* [remram44](https://github.com/remram44)
* [sascha-sl](https://github.com/sascha-sl)
* [sts10](https://github.com/sts10)
* [u1-liquid](https://github.com/u1-liquid)
* [sim6](https://github.com/sim6)
* [Sir-Boops](https://github.com/Sir-Boops)
* [stemid](https://github.com/stemid)
* [sumdog](https://github.com/sumdog)
* [ThomasLeister](https://github.com/ThomasLeister)
@@ -288,6 +295,7 @@ and provided thanks to the work of the following contributors:
* [857b](https://github.com/857b)
* [insom](https://github.com/insom)
* [tachyons](https://github.com/tachyons)
* [acid-chicken](https://github.com/acid-chicken)
* [Esteth](https://github.com/Esteth)
* [unascribed](https://github.com/unascribed)
* [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)
* [alxrcs](https://github.com/alxrcs)
* [console-cowboy](https://github.com/console-cowboy)
* [pointlessone](https://github.com/pointlessone)
* [Alkarex](https://github.com/Alkarex)
* [a2](https://github.com/a2)
* [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)
* [andi1984](https://github.com/andi1984)
* [schas002](https://github.com/schas002)
* [contraexemplo](https://github.com/contraexemplo)
* [abackstrom](https://github.com/abackstrom)
* [armandfardeau](https://github.com/armandfardeau)
* [jumbosushi](https://github.com/jumbosushi)
* [aurelien-reeves](https://github.com/aurelien-reeves)
* [ayumin](https://github.com/ayumin)
* [BaptisteGelez](https://github.com/BaptisteGelez)
* [bzg](https://github.com/bzg)
@@ -329,6 +339,7 @@ and provided thanks to the work of the following contributors:
* [Motoma](https://github.com/Motoma)
* [chriswk](https://github.com/chriswk)
* [csu](https://github.com/csu)
* [clarfon](https://github.com/clarfon)
* [kklleemm](https://github.com/kklleemm)
* [colindean](https://github.com/colindean)
* [dachinat](https://github.com/dachinat)
@@ -351,11 +362,13 @@ and provided thanks to the work of the following contributors:
* [eai04191](https://github.com/eai04191)
* [d3vgru](https://github.com/d3vgru)
* [Elizafox](https://github.com/Elizafox)
* [enewhuis](https://github.com/enewhuis)
* [ericblade](https://github.com/ericblade)
* [mikoim](https://github.com/mikoim)
* [espenronnevik](https://github.com/espenronnevik)
* [Finariel](https://github.com/Finariel)
* [siuying](https://github.com/siuying)
* [zoc](https://github.com/zoc)
* [fwenzel](https://github.com/fwenzel)
* [GenbuHase](https://github.com/GenbuHase)
* [hattori6789](https://github.com/hattori6789)
@@ -416,6 +429,7 @@ and provided thanks to the work of the following contributors:
* [martymcguire](https://github.com/martymcguire)
* [marvinkopf](https://github.com/marvinkopf)
* [otsune](https://github.com/otsune)
* [mbugowski](https://github.com/mbugowski)
* [Mathias B](mailto:10813340+mathias-b@users.noreply.github.com)
* [matt-auckland](https://github.com/matt-auckland)
* [webroo](https://github.com/webroo)
@@ -434,10 +448,10 @@ and provided thanks to the work of the following contributors:
* [premist](https://github.com/premist)
* [Mnkai](https://github.com/Mnkai)
* [mitchhentges](https://github.com/mitchhentges)
* [moritzheiber](https://github.com/moritzheiber)
* [mouse-reeve](https://github.com/mouse-reeve)
* [Mozinet-fr](https://github.com/Mozinet-fr)
* [lae](https://github.com/lae)
* [nosada](https://github.com/nosada)
* [Nanamachi](https://github.com/Nanamachi)
* [orinthe](https://github.com/orinthe)
* [NecroTechno](https://github.com/NecroTechno)
@@ -454,21 +468,22 @@ and provided thanks to the work of the following contributors:
* [noppa](https://github.com/noppa)
* [Otakan951](https://github.com/Otakan951)
* [fahy](https://github.com/fahy)
* [PatrickRWells](https://github.com/PatrickRWells)
* [Pangoraw](https://github.com/Pangoraw)
* [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)
* [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)
* [Sebastian Hübner](mailto:imolein@users.noreply.github.com)
* [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)
* [Shouko Yu](mailto:imshouko@gmail.com)
* [Sina Mashek](mailto:sina@mashek.xyz)
* [Sir-Boops](mailto:admin@boops.me)
* [Soshi Kato](mailto:mail@sossii.com)
* [Spanky](mailto:2788886+spankyworks@users.noreply.github.com)
* [Stanislas](mailto:angristan@pm.me)
* [StefOfficiel](mailto:pichard.stephane@free.fr)
* [Steven Tappert](mailto:admin@dark-it.net)
* [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)
* [fusshi-](mailto:dikky1218@users.noreply.github.com)
* [gentaro](mailto:gentaroooo@gmail.com)
* [gol-cha](mailto:info@mevo.xyz)
* [hakoai](mailto:hk--76@qa2.so-net.ne.jp)
* [haosbvnker](mailto:github@chaosbunker.com)
* [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)
* [kedama](mailto:32974885+kedamadq@users.noreply.github.com)
* [kodai](mailto:shirafuta.kodai@gmail.com)
* [koyu](mailto:me@koyu.space)
* [kuro5hin](mailto:rusty@kuro5hin.org)
* [luzpaz](mailto:luzpaz@users.noreply.github.com)
* [maxypy](mailto:maxime@mpigou.fr)
* [mhe](mailto:mail@marcus-herrmann.com)
* [mike castleman](mailto:m@mlcastle.net)
* [mimikun](mailto:dzdzble_effort_311@outlook.jp)
* [mohemohe](mailto:mohemohe@users.noreply.github.com)
* [mshrtkch](mailto:mshrtkch@users.noreply.github.com)
* [muan](mailto:muan@github.com)
* [namelessGonbai](mailto:43787036+namelessgonbai@users.noreply.github.com)
* [neetshin](mailto:neetshin@neetsh.in)
* [nightpool](mailto:nightpool@users.noreply.github.com)
* [rch850](mailto:rich850@gmail.com)
* [roikale](mailto:roikale@users.noreply.github.com)
* [rysiekpl](mailto:rysiek@hackerspace.pl)
@@ -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:

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

## [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
### Added



+ 3
- 1
CONTRIBUTING.md View File

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

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

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

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 'puma', '~> 3.12'
gem 'rails', '~> 5.2.2'
gem 'rails', '~> 5.2.3'
gem 'thor', '~> 0.20'

gem 'hamlit-rails', '~> 0.2'
gem 'pg', '~> 1.1'
gem 'makara', '~> 0.4'
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-openstack', '~> 0.3', require: false
gem 'paperclip', '~> 6.0'
@@ -23,14 +23,14 @@ gem 'paperclip-av-transcoder', '~> 0.6'
gem 'streamio-ffmpeg', '~> 3.0'

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

group :pam_authentication, optional: true do
@@ -85,8 +85,8 @@ gem 'strong_migrations', '~> 0.3'
gem 'tty-command', '~> 0.8', require: false
gem 'tty-prompt', '~> 0.18', require: false
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 'json-ld', '~> 3.0'
@@ -97,7 +97,7 @@ group :development, :test do
gem 'fabrication', '~> 2.20'
gem 'fuubar', '~> 2.3'
gem 'i18n-tasks', '~> 0.9', require: false
gem 'pry-byebug', '~> 3.6'
gem 'pry-byebug', '~> 3.7'
gem 'pry-rails', '~> 0.3'
gem 'rspec-rails', '~> 3.8'
end
@@ -107,19 +107,19 @@ group :production, :test do
end

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

group :development do
gem 'active_record_query_trace', '~> 1.5'
gem 'active_record_query_trace', '~> 1.6'
gem 'annotate', '~> 2.7'
gem 'better_errors', '~> 2.5'
gem 'binding_of_caller', '~> 0.7'
@@ -127,8 +127,8 @@ group :development do
gem 'letter_opener', '~> 1.7'
gem 'letter_opener_web', '~> 1.3'
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 'scss_lint', '~> 0.57', require: false



+ 127
- 126
Gemfile.lock View File

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

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

RUBY VERSION
ruby 2.6.0p0
ruby 2.6.1p33

BUNDLED WITH
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

## Navigation
## Navigation

- [Project homepage 🐘](https://joinmastodon.org)
- [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**.

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

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



+ 12
- 1
Vagrantfile View File

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

# Install rvm
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
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|
data = ::Mention.where(status_id: collection.map(&:id)).pluck(:status_id, :account_id)
data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }
@@ -48,14 +48,14 @@ class StatusesIndex < Chewy::Index
end

root date_detection: false do
field :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'
end

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

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

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

class AboutController < ApplicationController
before_action :set_body_classes
layout 'public'

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

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

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

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

private

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

helper_method :new_user
@@ -28,15 +27,4 @@ class AboutController < ApplicationController
def set_instance_presenter
@instance_presenter = InstancePresenter.new
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

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

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

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

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])
render xml: OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.feed(@account, @entries.reject { |entry| entry.status.nil? }))
end

format.rss do
mark_cacheable!

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

format.json do
skip_session!
mark_cacheable!

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

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

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

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)
end

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

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)
elsif replies_requested?
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')
end

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

def filtered_status_page(params)
if params[:min_id].present?
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_size
before_action :set_statuses
before_action :set_cache_headers

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

private


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

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

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

before_action :set_account

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

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
@account = Account.find_local!(params[:account_username]) if params[:account_username]
end

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

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

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

+ 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_statuses
before_action :set_cache_headers

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'
end



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

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

module Admin
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_local_account!, only: [:enable, :memorialize]
before_action :require_local_account!, only: [:enable, :memorialize, :approve, :reject]

def index
authorize :account, :index?
@@ -45,6 +45,18 @@ module Admin
redirect_to admin_account_path(@account.id)
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
authorize @account, :unsilence?
@account.unsilence!
@@ -114,6 +126,7 @@ module Admin
:remote,
:by_domain,
:active,
:pending,
:silenced,
:suspended,
: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_filter_params

include ObfuscateFilename
obfuscate_filename [:custom_emoji, :image]

def index
authorize :custom_emoji, :index?
@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
@relay_enabled = Relay.enabled.exists?
@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
@invites_enabled = Setting.min_invite_role == 'user'
@search_enabled = Chewy.enabled?
@@ -29,6 +29,7 @@ module Admin
@hidden_service = ENV['ALLOW_ACCESS_TO_HIDDEN_SERVICE'] == 'true'
@trending_hashtags = TrendingTags.get(7)
@profile_directory = Setting.profile_directory
@timeline_preview = Setting.timeline_preview
end

private


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

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

def filter_params
params.permit(:limited)
params.permit(:limited, :by_domain)
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

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

private


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

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

module Admin
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
authorize :settings, :show?

@admin_settings = Form::AdminSettings.new
end

def 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

private

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

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

@@ -73,7 +73,9 @@ class Api::BaseController < ApplicationController
elsif current_user.disabled?
render json: { error: 'Your login is currently disabled' }, status: 403
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
set_user_activity
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

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
end

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

def default_accounts
Account.includes(:active_relationships, :account_stat).references(:active_relationships)
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

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
end

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

def default_accounts
Account.includes(:passive_relationships, :account_stat).references(:passive_relationships)
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
AccountSearchService.new.call(
params[:q],
limit_param(DEFAULT_ACCOUNTS_LIMIT),
current_account,
limit: limit_param(DEFAULT_ACCOUNTS_LIMIT),
resolve: truthy_param?(:resolve),
following: truthy_param?(:following)
following: truthy_param?(:following),
offset: params[:offset]
)
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!(no_replies_scope) if truthy_param?(:exclude_replies)
statuses.merge!(no_reblogs_scope) if truthy_param?(:exclude_reblogs)
statuses.merge!(hashtag_scope) if params[:tagged].present?

statuses
end
@@ -50,9 +51,9 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
# 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
# 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

def pinned_scope
@@ -67,6 +68,16 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
Status.without_reblogs
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)
params.slice(:limit, :only_media, :exclude_replies).permit(:limit, :only_media, :exclude_replies).merge(core_params)
end


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

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

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

+ 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

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

+ 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
include Authorization

RESULTS_LIMIT = 5
RESULTS_LIMIT = 20

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

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

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
SearchService.new.call(
params[:q],
RESULTS_LIMIT,
truthy_param?(:resolve),
current_account
current_account,
limit_param(RESULTS_LIMIT),
search_params.merge(resolve: truthy_param?(:resolve))
)
end

def search_params
params.permit(:type, :offset, :min_id, :max_id, :account_id)
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

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
end

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

def reblog_params
params.permit(:visibility)
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],
scheduled_at: status_params[:scheduled_at],
application: doorkeeper_token.application,
poll: status_params[:poll],
idempotency: request.headers['Idempotency-Key'])

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])
authorize @status, :show?
rescue Mastodon::NotPermittedError
# Reraise in order to get a 404 instead of a 403 error code
raise ActiveRecord::RecordNotFound
end

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

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

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

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
def index
@search = Search.new(search)
@search = Search.new(search_results)
render json: @search, serializer: REST::V2::SearchSerializer
end
end

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

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

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

def skip_session!
request.session_options[:skip] = true
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_body_classes, only: [:new, :create, :edit, :update]

def new
super(&:build_invite_request)
end

def destroy
not_found
end
@@ -24,16 +28,17 @@ class Auth::RegistrationsController < Devise::RegistrationsController
def build_resource(hash = nil)
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?
end

def configure_sign_up_params
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

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

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

def invite_code


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

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

included do
layout 'public'

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

private

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

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

def username_param
params[:account_username]
end

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

def check_account_approval
not_found if @account.user_pending?
end

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

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

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

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


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

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

before_action :set_cache_headers

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

next if @account.user_hides_network?

follows
@@ -15,6 +19,11 @@ class FollowerAccountsController < ApplicationController
format.json do
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,
serializer: ActivityPub::CollectionSerializer,
adapter: ActivityPub::Adapter,


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

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

before_action :set_cache_headers

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

next if @account.user_hides_network?

follows
@@ -15,6 +19,11 @@ class FollowingAccountsController < ApplicationController
format.json do
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,
serializer: ActivityPub::CollectionSerializer,
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),
current_account: current_account,
token: current_session.token,
admin: Account.find_local(Setting.site_contact_username),
admin: Account.find_local(Setting.site_contact_username.strip.gsub(/\A@/, '')),
}
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 :authenticate_resource_owner!
before_action :set_body_classes

include Localized

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

private

def set_body_classes
@body_classes = 'admin'
end

def store_current_location
store_location_for(:user, request.url)
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

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)

redirect_to settings_export_path
end

def lock_options
{ redis: Redis.current, key: "backup:#{current_user.id}" }
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_hide_network,
: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)
)
end


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

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

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

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

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

class Settings::SessionsController < Settings::BaseController
before_action :authenticate_user!
before_action :set_session, only: :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),
current_account: current_account,
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,
}
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 :set_referrer_policy_header, only: [:show]
before_action :set_cache_headers
before_action :set_replies, only: [:replies]

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

@body_classes = 'with-modals'

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

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
ActiveModelSerializers::SerializableResource.new(@status, serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter)
@@ -63,8 +66,37 @@ class StatusesController < ApplicationController
render 'stream_entries/embed', layout: 'embedded'
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

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)
depth = starting_depth + statuses.size
if depth < DESCENDANTS_DEPTH_LIMIT
@@ -174,4 +206,27 @@ class StatusesController < ApplicationController
return if @status.public_visibility? || @status.unlisted_visibility?
response.headers['Referrer-Policy'] = 'origin'
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

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

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

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

respond_to do |format|
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

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

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)

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

def initial_state_params
{
settings: {},
token: current_session&.token,
}
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

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

+ 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

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
INVITE_FILTER = %i(available expired).freeze
CUSTOM_EMOJI_FILTERS = %i(local remote by_domain shortcode).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)
new_url = filtered_url_for(link_to_params)


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

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

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

def open_deletion?
@@ -101,4 +117,9 @@ module ApplicationHelper
def storage_host?
ENV['S3_ALIAS_HOST'].present? || ENV['S3_CLOUDFRONT_HOST'].present?
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

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

@@ -56,4 +56,22 @@ module HomeHelper
'emojify'
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

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

@@ -47,6 +47,15 @@ module JsonLdHelper
!uri.start_with?('http://', 'https://')
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)
graph = RDF::Graph.new << JSON::LD::API.toRdf(json, documentLoader: method(:load_jsonld_context))
graph.dump(:normalize)
@@ -63,12 +72,19 @@ module JsonLdHelper
json.present? && json['id'] == uri ? json : nil
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|
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
end
# If request failed, retry without doing it on behalf of a user
return if on_behalf_of.nil?
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
end
end
@@ -91,6 +107,14 @@ module JsonLdHelper

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

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

def status_description(status)
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")
end

@@ -170,7 +180,7 @@ module StreamEntriesHelper
when 'public'
fa_icon 'globe fw'
when 'unlisted'
fa_icon 'unlock-alt fw'
fa_icon 'unlock fw'
when 'private'
fa_icon 'lock fw'
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 {
type: ALERT_SHOW,
title,
@@ -34,6 +34,11 @@ export function showAlertForError(error) {
if (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 title = `${status}`;

@@ -44,6 +49,6 @@ export function showAlertForError(error) {
return showAlert(title, message);
} else {
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 { updateTimeline } from './timelines';
import { showAlertForError } from './alerts';
import { showAlert } from './alerts';
import { defineMessages } from 'react-intl';

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

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 }));
}
};
@@ -184,20 +201,38 @@ export function submitComposeFail(error) {

export function uploadCompose(files) {
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;
}

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,
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']);
}

const isLoadingRecent = !!params.since_id;

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

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

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

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 STATUS_IMPORT = 'STATUS_IMPORT';
export const STATUS_IMPORT = 'STATUS_IMPORT';
export const STATUSES_IMPORT = 'STATUSES_IMPORT';
export const POLLS_IMPORT = 'POLLS_IMPORT';

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

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

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

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

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

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

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

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

dispatch(importPolls(polls));
dispatch(importFetchedAccounts(accounts));
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;
}

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

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

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,
importFetchedStatuses,
} from './importer';
import { saveSettings } from './settings';
import { defineMessages } from 'react-intl';
import { List as ImmutableList } from 'immutable';
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 excludeTypesFromFilter = filter => {
const allTypes = ImmutableList(['follow', 'favourite', 'reblog', 'mention']);
const allTypes = ImmutableList(['follow', 'favourite', 'reblog', 'mention', 'poll']);
return allTypes.filterNot(item => item === filter).toJS();
};

@@ -187,5 +188,6 @@ export function setFilter (filterType) {
value: filterType,
});
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: {
q: value,
resolve: true,
limit: 5,
},
}).then(response => {
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) {
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));



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

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

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

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

onDisconnect() {
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_CONNECT = 'TIMELINE_CONNECT';
export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT';

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) {
return {
type: TIMELINE_DISCONNECT,


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

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

let csrfHeader = {};

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);

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) {
buttons = <IconButton disabled icon='hourglass' title={intl.formatMessage(messages.requested)} />;
} 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) {
let hidingNotificationsButton;
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 PropTypes from 'prop-types';
import ImmutablePureComponent from 'react-immutable-pure-component';
import Icon from 'mastodon/components/icon';

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

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

return (
<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>
);
})}
@@ -36,7 +37,7 @@ export default class AttachmentList extends ImmutablePureComponent {
return (
<div className='attachment-list'>
<div className='attachment-list__icon'>
<i className='fa fa-link' />
<Icon id='link' />
</div>

<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 { FormattedMessage } from 'react-intl';
import PropTypes from 'prop-types';
import Icon from 'mastodon/components/icon';

export default class ColumnBackButton extends React.PureComponent {

@@ -19,7 +20,7 @@ export default class ColumnBackButton extends React.PureComponent {
render () {
return (
<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' />
</button>
);


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

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

export default class ColumnBackButtonSlim extends ColumnBackButton {

@@ -8,7 +9,7 @@ export default class ColumnBackButtonSlim extends ColumnBackButton {
return (
<div className='column-back-button--slim'>
<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' />
</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 classNames from 'classnames';
import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
import Icon from 'mastodon/components/icon';

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

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 = (
<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>
);
} 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)) {
backButton = (
<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' />
</button>
);
@@ -140,7 +141,7 @@ class ColumnHeader extends React.PureComponent {
}

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


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

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

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) {
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 {
if (others && others.size > 0) {
account = others.first();
} else {
account = this.props.account;
}

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

if (acct.indexOf('@') === -1 && 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 (
<span className='display-name'>
<bdi><strong className='display-name__html' dangerouslySetInnerHTML={displayNameHtml} /></bdi> {suffix}
{displayName} {suffix}
</span>
);
}


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

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

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


+ 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 PropTypes from 'prop-types';
import classNames from 'classnames';
import Icon from 'mastodon/components/icon';

export default class IconButton extends React.PureComponent {

@@ -85,8 +86,9 @@ export default class IconButton extends React.PureComponent {
onClick={this.handleClick}
style={style}
tabIndex={tabIndex}
disabled={disabled}
>
<i className={`fa fa-fw fa-${icon}`} aria-hidden='true' />
<Icon id={icon} fixedWidth aria-hidden='true' />
</button>
);
}
@@ -103,8 +105,9 @@ export default class IconButton extends React.PureComponent {
onClick={this.handleClick}
style={style}
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>
)}
</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) => {
if (prevState.isIntersecting && !this.entry.isIntersecting) {
if (prevState.isIntersecting !== false && !this.entry.isIntersecting) {
scheduleIdleTask(this.hideIfNotIntersecting);
}
return {


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

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

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

return (
<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>
);
}


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

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

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

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

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

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;



+ 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' },
hours: { id: 'relative_time.hours', defaultMessage: '{number}h' },
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 = {
@@ -86,6 +91,26 @@ export const timeAgoString = (intl, date, now, year) => {
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
class RelativeTimestamp extends React.Component {

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

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

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

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 (
<time dateTime={timestamp} title={intl.formatDate(date, dateFormatOptions)}>


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

Loading…
Cancel
Save