Merge branch 'main' of https://github.com/glitch-soc/mastodon
commit
2fab999373
|
@ -0,0 +1,7 @@
|
||||||
|
[production]
|
||||||
|
defaults
|
||||||
|
not IE 11
|
||||||
|
not dead
|
||||||
|
|
||||||
|
[development]
|
||||||
|
supports es6-module
|
|
@ -11,7 +11,8 @@
|
||||||
"extensions": [
|
"extensions": [
|
||||||
"EditorConfig.EditorConfig",
|
"EditorConfig.EditorConfig",
|
||||||
"dbaeumer.vscode-eslint",
|
"dbaeumer.vscode-eslint",
|
||||||
"rebornix.Ruby"
|
"rebornix.Ruby",
|
||||||
|
"webben.browserslist"
|
||||||
],
|
],
|
||||||
|
|
||||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||||
|
|
99
CHANGELOG.md
99
CHANGELOG.md
|
@ -3,6 +3,105 @@ Changelog
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## [3.5.3] - 2022-05-26
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **Add language dropdown to compose form in web UI** ([Gargron](https://github.com/mastodon/mastodon/pull/18420), [ykzts](https://github.com/mastodon/mastodon/pull/18460))
|
||||||
|
- **Add warning for limited accounts in web UI** ([Gargron](https://github.com/mastodon/mastodon/pull/18344))
|
||||||
|
- Add `limited` attribute to accounts in REST API ([Gargron](https://github.com/mastodon/mastodon/pull/18344))
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **Change RSS feeds** ([Gargron](https://github.com/mastodon/mastodon/pull/18356), [tribela](https://github.com/mastodon/mastodon/pull/18406))
|
||||||
|
- Titles are now date and time of post
|
||||||
|
- Bodies now render all content faithfully, including polls and emojis
|
||||||
|
- All media attachments are included with Media RSS
|
||||||
|
- Change "dangerous" to "sensitive" in privacy policy and web UI ([Gargron](https://github.com/mastodon/mastodon/pull/18515))
|
||||||
|
- Change unconfirmed accounts to not be visible in REST API ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17530))
|
||||||
|
- Change `tootctl search deploy` to improve performance ([Gargron](https://github.com/mastodon/mastodon/pull/18463), [Gargron](https://github.com/mastodon/mastodon/pull/18514))
|
||||||
|
- Change search indexing to use batches to minimize resource usage ([Gargron](https://github.com/mastodon/mastodon/pull/18451))
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix follower and other counters being able to go negative ([Gargron](https://github.com/mastodon/mastodon/pull/18517))
|
||||||
|
- Fix unnecessary query on when creating a status ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17901))
|
||||||
|
- Fix warning an account outside of a report closing all reports for that account ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18387))
|
||||||
|
- Fix error when resolving a link that redirects to a local post ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18314))
|
||||||
|
- Fix preferred posting language returning unusable value in REST API ([Gargron](https://github.com/mastodon/mastodon/pull/18428))
|
||||||
|
- Fix race condition error when external status is reblogged ([ykzts](https://github.com/mastodon/mastodon/pull/18424))
|
||||||
|
- Fix missing string for appeal validation error ([Gargron](https://github.com/mastodon/mastodon/pull/18410))
|
||||||
|
- Fix block/mute lists showing a follow button in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18364))
|
||||||
|
- Fix Redis configuration not being changed by `mastodon:setup` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18383))
|
||||||
|
- Fix streaming notifications not using quick filter logic in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18316))
|
||||||
|
- Fix ambiguous wording on appeal actions in admin UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18328))
|
||||||
|
- Fix floating action button obscuring last element in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18332))
|
||||||
|
- Fix account warnings not being recorded in audit log ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18338))
|
||||||
|
- Fix leftover icons for direct visibility statuses ([Steffo99](https://github.com/mastodon/mastodon/pull/18305))
|
||||||
|
- Fix link verification requiring case sensitivity on links ([sgolemon](https://github.com/mastodon/mastodon/pull/18320))
|
||||||
|
- Fix embeds not setting their height correctly ([rinsuki](https://github.com/mastodon/mastodon/pull/18301))
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Fix concurrent unfollowing decrementing follower count more than once ([Gargron](https://github.com/mastodon/mastodon/pull/18527))
|
||||||
|
- Fix being able to appeal a strike unlimited times ([Gargron](https://github.com/mastodon/mastodon/pull/18529))
|
||||||
|
- Fix being able to report otherwise inaccessible statuses ([Gargron](https://github.com/mastodon/mastodon/pull/18528))
|
||||||
|
- Fix empty votes arbitrarily increasing voters count in polls ([Gargron](https://github.com/mastodon/mastodon/pull/18526))
|
||||||
|
- Fix moderator identity leak when approving appeal of sensitive marked statuses ([Gargron](https://github.com/mastodon/mastodon/pull/18525))
|
||||||
|
- Fix suspended users being able to access APIs that don't require a user ([Gargron](https://github.com/mastodon/mastodon/pull/18524))
|
||||||
|
- Fix confirmation redirect to app without `Location` header ([Gargron](https://github.com/mastodon/mastodon/pull/18523))
|
||||||
|
|
||||||
|
## [3.5.2] - 2022-05-04
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Add warning on direct messages screen in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/18289))
|
||||||
|
- We already had a warning when composing a direct message, it has now been reworded to be more clear
|
||||||
|
- Same warning is now displayed when viewing sent and received direct messages
|
||||||
|
- Add ability to set approval-based registration through tootctl ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18248))
|
||||||
|
- Add pre-filling of domain from search filter in domain allow/block admin UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18172))
|
||||||
|
|
||||||
|
## Changed
|
||||||
|
|
||||||
|
- Change name of “Direct” visibility to “Mentioned people only” in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/18146), [Gargron](https://github.com/mastodon/mastodon/pull/18289), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/18291))
|
||||||
|
- Change trending posts to only show one post from each account ([Gargron](https://github.com/mastodon/mastodon/pull/18181))
|
||||||
|
- Change half-life of trending posts from 6 hours to 2 hours ([Gargron](https://github.com/mastodon/mastodon/pull/18182))
|
||||||
|
- Change full-text search feature to also include polls you have voted in ([tribela](https://github.com/mastodon/mastodon/pull/18070))
|
||||||
|
- Change Redis from using one connection per process, to using a connection pool ([Gargron](https://github.com/mastodon/mastodon/pull/18135), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/18160), [Gargron](https://github.com/mastodon/mastodon/pull/18171))
|
||||||
|
- Different threads no longer have to wait on a mutex over a single connection
|
||||||
|
- However, this does increase the number of Redis connections by a fair amount
|
||||||
|
- We are planning to optimize Redis use so that the pool can be made smaller in the future
|
||||||
|
|
||||||
|
## Removed
|
||||||
|
|
||||||
|
- Remove IP matching from e-mail domain blocks ([Gargron](https://github.com/mastodon/mastodon/pull/18190))
|
||||||
|
- The IPs of the blocked e-mail domain or its MX records are no longer checked
|
||||||
|
- Previously it was too easy to block e-mail providers by mistake
|
||||||
|
|
||||||
|
## Fixed
|
||||||
|
|
||||||
|
- Fix compatibility with Friendica's pinned posts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18254), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/18260))
|
||||||
|
- Fix error when looking up handle with surrounding spaces in REST API ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18225))
|
||||||
|
- Fix double render error when authorizing interaction ([Gargron](https://github.com/mastodon/mastodon/pull/18203))
|
||||||
|
- Fix error when a post references an invalid media attachment ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18211))
|
||||||
|
- Fix error when trying to revoke OAuth token without supplying a token ([Gargron](https://github.com/mastodon/mastodon/pull/18205))
|
||||||
|
- Fix error caused by missing subject in Webfinger response ([Gargron](https://github.com/mastodon/mastodon/pull/18204))
|
||||||
|
- Fix error on attempting to delete an account moderation note ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18196))
|
||||||
|
- Fix light-mode emoji borders in web UI ([Gaelan](https://github.com/mastodon/mastodon/pull/18131))
|
||||||
|
- Fix being able to scroll away from the loading bar in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/18170))
|
||||||
|
- Fix error when a bookmark or favorite has been reported and deleted ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18174))
|
||||||
|
- Fix being offered empty “Server rules violation” report option in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18165))
|
||||||
|
- Fix temporary network errors preventing from authorizing interactions with remote accounts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18161))
|
||||||
|
- Fix incorrect link in "new trending tags" email ([cdzombak](https://github.com/mastodon/mastodon/pull/18156))
|
||||||
|
- Fix missing indexes on some foreign keys ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18157))
|
||||||
|
- Fix n+1 query on feed merge and populate operations ([Gargron](https://github.com/mastodon/mastodon/pull/18111))
|
||||||
|
- Fix feed unmerge worker being exceptionally slow in some conditions ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18110))
|
||||||
|
- Fix PeerTube videos appearing with an erroneous “Edited at” marker ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18100))
|
||||||
|
- Fix instance actor being created incorrectly when running through migrations ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18109))
|
||||||
|
- Fix web push notifications containing HTML entities ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18071))
|
||||||
|
- Fix inconsistent parsing of `TRUSTED_PROXY_IP` ([ykzts](https://github.com/mastodon/mastodon/pull/18051))
|
||||||
|
- Fix error when fetching pinned posts ([tribela](https://github.com/mastodon/mastodon/pull/18030))
|
||||||
|
- Fix wrong optimization in feed populate operation ([dogelover911](https://github.com/mastodon/mastodon/pull/18009))
|
||||||
|
- Fix error in alias settings page ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18004))
|
||||||
|
|
||||||
## [3.5.1] - 2022-04-08
|
## [3.5.1] - 2022-04-08
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
|
20
Gemfile
20
Gemfile
|
@ -1,13 +1,13 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
source 'https://rubygems.org'
|
source 'https://rubygems.org'
|
||||||
ruby '>= 2.5.0', '< 3.1.0'
|
ruby '>= 2.6.0', '< 3.1.0'
|
||||||
|
|
||||||
gem 'pkg-config', '~> 1.4'
|
gem 'pkg-config', '~> 1.4'
|
||||||
gem 'rexml', '~> 3.2'
|
gem 'rexml', '~> 3.2'
|
||||||
|
|
||||||
gem 'puma', '~> 5.6'
|
gem 'puma', '~> 5.6'
|
||||||
gem 'rails', '~> 6.1.5'
|
gem 'rails', '~> 6.1.6'
|
||||||
gem 'sprockets', '~> 3.7.2'
|
gem 'sprockets', '~> 3.7.2'
|
||||||
gem 'thor', '~> 1.2'
|
gem 'thor', '~> 1.2'
|
||||||
gem 'rack', '~> 2.2.3'
|
gem 'rack', '~> 2.2.3'
|
||||||
|
@ -18,7 +18,7 @@ gem 'makara', '~> 0.5'
|
||||||
gem 'pghero', '~> 2.8'
|
gem 'pghero', '~> 2.8'
|
||||||
gem 'dotenv-rails', '~> 2.7'
|
gem 'dotenv-rails', '~> 2.7'
|
||||||
|
|
||||||
gem 'aws-sdk-s3', '~> 1.113', require: false
|
gem 'aws-sdk-s3', '~> 1.114', require: false
|
||||||
gem 'fog-core', '<= 2.1.0'
|
gem 'fog-core', '<= 2.1.0'
|
||||||
gem 'fog-openstack', '~> 0.3', require: false
|
gem 'fog-openstack', '~> 0.3', require: false
|
||||||
gem 'kt-paperclip', '~> 7.1'
|
gem 'kt-paperclip', '~> 7.1'
|
||||||
|
@ -26,7 +26,7 @@ gem 'blurhash', '~> 0.1'
|
||||||
|
|
||||||
gem 'active_model_serializers', '~> 0.10'
|
gem 'active_model_serializers', '~> 0.10'
|
||||||
gem 'addressable', '~> 2.8'
|
gem 'addressable', '~> 2.8'
|
||||||
gem 'bootsnap', '~> 1.10.3', require: false
|
gem 'bootsnap', '~> 1.11.1', require: false
|
||||||
gem 'browser'
|
gem 'browser'
|
||||||
gem 'charlock_holmes', '~> 0.7.7'
|
gem 'charlock_holmes', '~> 0.7.7'
|
||||||
gem 'chewy', '~> 7.2'
|
gem 'chewy', '~> 7.2'
|
||||||
|
@ -79,13 +79,13 @@ gem 'ruby-progressbar', '~> 1.11'
|
||||||
gem 'sanitize', '~> 6.0'
|
gem 'sanitize', '~> 6.0'
|
||||||
gem 'scenic', '~> 1.6'
|
gem 'scenic', '~> 1.6'
|
||||||
gem 'sidekiq', '~> 6.4'
|
gem 'sidekiq', '~> 6.4'
|
||||||
gem 'sidekiq-scheduler', '~> 3.1'
|
gem 'sidekiq-scheduler', '~> 4.0'
|
||||||
gem 'sidekiq-unique-jobs', '~> 7.1'
|
gem 'sidekiq-unique-jobs', '~> 7.1'
|
||||||
gem 'sidekiq-bulk', '~>0.2.0'
|
gem 'sidekiq-bulk', '~> 0.2.0'
|
||||||
gem 'simple-navigation', '~> 4.3'
|
gem 'simple-navigation', '~> 4.3'
|
||||||
gem 'simple_form', '~> 5.1'
|
gem 'simple_form', '~> 5.1'
|
||||||
gem 'sprockets-rails', '~> 3.4', require: 'sprockets/railtie'
|
gem 'sprockets-rails', '~> 3.4', require: 'sprockets/railtie'
|
||||||
gem 'stoplight', '~> 2.2.1'
|
gem 'stoplight', '~> 3.0.0'
|
||||||
gem 'strong_migrations', '~> 0.7'
|
gem 'strong_migrations', '~> 0.7'
|
||||||
gem 'tty-prompt', '~> 0.23', require: false
|
gem 'tty-prompt', '~> 0.23', require: false
|
||||||
gem 'twitter-text', '~> 3.1.0'
|
gem 'twitter-text', '~> 3.1.0'
|
||||||
|
@ -114,9 +114,9 @@ group :production, :test do
|
||||||
end
|
end
|
||||||
|
|
||||||
group :test do
|
group :test do
|
||||||
gem 'capybara', '~> 3.36'
|
gem 'capybara', '~> 3.37'
|
||||||
gem 'climate_control', '~> 0.2'
|
gem 'climate_control', '~> 0.2'
|
||||||
gem 'faker', '~> 2.20'
|
gem 'faker', '~> 2.21'
|
||||||
gem 'microformats', '~> 4.2'
|
gem 'microformats', '~> 4.2'
|
||||||
gem 'rails-controller-testing', '~> 1.0'
|
gem 'rails-controller-testing', '~> 1.0'
|
||||||
gem 'rspec-sidekiq', '~> 3.1'
|
gem 'rspec-sidekiq', '~> 3.1'
|
||||||
|
@ -134,7 +134,7 @@ group :development do
|
||||||
gem 'letter_opener', '~> 1.8'
|
gem 'letter_opener', '~> 1.8'
|
||||||
gem 'letter_opener_web', '~> 2.0'
|
gem 'letter_opener_web', '~> 2.0'
|
||||||
gem 'memory_profiler'
|
gem 'memory_profiler'
|
||||||
gem 'rubocop', '~> 1.26', require: false
|
gem 'rubocop', '~> 1.29', require: false
|
||||||
gem 'rubocop-rails', '~> 2.14', require: false
|
gem 'rubocop-rails', '~> 2.14', require: false
|
||||||
gem 'brakeman', '~> 5.2', require: false
|
gem 'brakeman', '~> 5.2', require: false
|
||||||
gem 'bundler-audit', '~> 0.9', require: false
|
gem 'bundler-audit', '~> 0.9', require: false
|
||||||
|
|
195
Gemfile.lock
195
Gemfile.lock
|
@ -1,40 +1,40 @@
|
||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
actioncable (6.1.5)
|
actioncable (6.1.6)
|
||||||
actionpack (= 6.1.5)
|
actionpack (= 6.1.6)
|
||||||
activesupport (= 6.1.5)
|
activesupport (= 6.1.6)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
websocket-driver (>= 0.6.1)
|
websocket-driver (>= 0.6.1)
|
||||||
actionmailbox (6.1.5)
|
actionmailbox (6.1.6)
|
||||||
actionpack (= 6.1.5)
|
actionpack (= 6.1.6)
|
||||||
activejob (= 6.1.5)
|
activejob (= 6.1.6)
|
||||||
activerecord (= 6.1.5)
|
activerecord (= 6.1.6)
|
||||||
activestorage (= 6.1.5)
|
activestorage (= 6.1.6)
|
||||||
activesupport (= 6.1.5)
|
activesupport (= 6.1.6)
|
||||||
mail (>= 2.7.1)
|
mail (>= 2.7.1)
|
||||||
actionmailer (6.1.5)
|
actionmailer (6.1.6)
|
||||||
actionpack (= 6.1.5)
|
actionpack (= 6.1.6)
|
||||||
actionview (= 6.1.5)
|
actionview (= 6.1.6)
|
||||||
activejob (= 6.1.5)
|
activejob (= 6.1.6)
|
||||||
activesupport (= 6.1.5)
|
activesupport (= 6.1.6)
|
||||||
mail (~> 2.5, >= 2.5.4)
|
mail (~> 2.5, >= 2.5.4)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
actionpack (6.1.5)
|
actionpack (6.1.6)
|
||||||
actionview (= 6.1.5)
|
actionview (= 6.1.6)
|
||||||
activesupport (= 6.1.5)
|
activesupport (= 6.1.6)
|
||||||
rack (~> 2.0, >= 2.0.9)
|
rack (~> 2.0, >= 2.0.9)
|
||||||
rack-test (>= 0.6.3)
|
rack-test (>= 0.6.3)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
||||||
actiontext (6.1.5)
|
actiontext (6.1.6)
|
||||||
actionpack (= 6.1.5)
|
actionpack (= 6.1.6)
|
||||||
activerecord (= 6.1.5)
|
activerecord (= 6.1.6)
|
||||||
activestorage (= 6.1.5)
|
activestorage (= 6.1.6)
|
||||||
activesupport (= 6.1.5)
|
activesupport (= 6.1.6)
|
||||||
nokogiri (>= 1.8.5)
|
nokogiri (>= 1.8.5)
|
||||||
actionview (6.1.5)
|
actionview (6.1.6)
|
||||||
activesupport (= 6.1.5)
|
activesupport (= 6.1.6)
|
||||||
builder (~> 3.1)
|
builder (~> 3.1)
|
||||||
erubi (~> 1.4)
|
erubi (~> 1.4)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
|
@ -45,22 +45,22 @@ GEM
|
||||||
case_transform (>= 0.2)
|
case_transform (>= 0.2)
|
||||||
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
|
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
|
||||||
active_record_query_trace (1.8)
|
active_record_query_trace (1.8)
|
||||||
activejob (6.1.5)
|
activejob (6.1.6)
|
||||||
activesupport (= 6.1.5)
|
activesupport (= 6.1.6)
|
||||||
globalid (>= 0.3.6)
|
globalid (>= 0.3.6)
|
||||||
activemodel (6.1.5)
|
activemodel (6.1.6)
|
||||||
activesupport (= 6.1.5)
|
activesupport (= 6.1.6)
|
||||||
activerecord (6.1.5)
|
activerecord (6.1.6)
|
||||||
activemodel (= 6.1.5)
|
activemodel (= 6.1.6)
|
||||||
activesupport (= 6.1.5)
|
activesupport (= 6.1.6)
|
||||||
activestorage (6.1.5)
|
activestorage (6.1.6)
|
||||||
actionpack (= 6.1.5)
|
actionpack (= 6.1.6)
|
||||||
activejob (= 6.1.5)
|
activejob (= 6.1.6)
|
||||||
activerecord (= 6.1.5)
|
activerecord (= 6.1.6)
|
||||||
activesupport (= 6.1.5)
|
activesupport (= 6.1.6)
|
||||||
marcel (~> 1.0)
|
marcel (~> 1.0)
|
||||||
mini_mime (>= 1.1.0)
|
mini_mime (>= 1.1.0)
|
||||||
activesupport (6.1.5)
|
activesupport (6.1.6)
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||||
i18n (>= 1.6, < 2)
|
i18n (>= 1.6, < 2)
|
||||||
minitest (>= 5.1)
|
minitest (>= 5.1)
|
||||||
|
@ -81,20 +81,20 @@ GEM
|
||||||
attr_required (1.0.1)
|
attr_required (1.0.1)
|
||||||
awrence (1.1.1)
|
awrence (1.1.1)
|
||||||
aws-eventstream (1.2.0)
|
aws-eventstream (1.2.0)
|
||||||
aws-partitions (1.558.0)
|
aws-partitions (1.587.0)
|
||||||
aws-sdk-core (3.127.0)
|
aws-sdk-core (3.130.2)
|
||||||
aws-eventstream (~> 1, >= 1.0.2)
|
aws-eventstream (~> 1, >= 1.0.2)
|
||||||
aws-partitions (~> 1, >= 1.525.0)
|
aws-partitions (~> 1, >= 1.525.0)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.1)
|
||||||
jmespath (~> 1.0)
|
jmespath (~> 1.0)
|
||||||
aws-sdk-kms (1.55.0)
|
aws-sdk-kms (1.56.0)
|
||||||
aws-sdk-core (~> 3, >= 3.127.0)
|
aws-sdk-core (~> 3, >= 3.127.0)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.1)
|
||||||
aws-sdk-s3 (1.113.0)
|
aws-sdk-s3 (1.114.0)
|
||||||
aws-sdk-core (~> 3, >= 3.127.0)
|
aws-sdk-core (~> 3, >= 3.127.0)
|
||||||
aws-sdk-kms (~> 1)
|
aws-sdk-kms (~> 1)
|
||||||
aws-sigv4 (~> 1.4)
|
aws-sigv4 (~> 1.4)
|
||||||
aws-sigv4 (1.4.0)
|
aws-sigv4 (1.5.0)
|
||||||
aws-eventstream (~> 1, >= 1.0.2)
|
aws-eventstream (~> 1, >= 1.0.2)
|
||||||
bcrypt (3.1.17)
|
bcrypt (3.1.17)
|
||||||
better_errors (2.9.1)
|
better_errors (2.9.1)
|
||||||
|
@ -114,9 +114,9 @@ GEM
|
||||||
debug_inspector (>= 0.0.1)
|
debug_inspector (>= 0.0.1)
|
||||||
blurhash (0.1.6)
|
blurhash (0.1.6)
|
||||||
ffi (~> 1.14)
|
ffi (~> 1.14)
|
||||||
bootsnap (1.10.3)
|
bootsnap (1.11.1)
|
||||||
msgpack (~> 1.2)
|
msgpack (~> 1.2)
|
||||||
brakeman (5.2.1)
|
brakeman (5.2.3)
|
||||||
browser (4.2.0)
|
browser (4.2.0)
|
||||||
brpoplpush-redis_script (0.1.2)
|
brpoplpush-redis_script (0.1.2)
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.5)
|
concurrent-ruby (~> 1.0, >= 1.0.5)
|
||||||
|
@ -144,7 +144,7 @@ GEM
|
||||||
sshkit (~> 1.3)
|
sshkit (~> 1.3)
|
||||||
capistrano-yarn (2.0.2)
|
capistrano-yarn (2.0.2)
|
||||||
capistrano (~> 3.0)
|
capistrano (~> 3.0)
|
||||||
capybara (3.36.0)
|
capybara (3.37.1)
|
||||||
addressable
|
addressable
|
||||||
matrix
|
matrix
|
||||||
mini_mime (>= 0.1.3)
|
mini_mime (>= 0.1.3)
|
||||||
|
@ -203,7 +203,6 @@ GEM
|
||||||
dotenv-rails (2.7.6)
|
dotenv-rails (2.7.6)
|
||||||
dotenv (= 2.7.6)
|
dotenv (= 2.7.6)
|
||||||
railties (>= 3.2)
|
railties (>= 3.2)
|
||||||
e2mmap (0.1.0)
|
|
||||||
ed25519 (1.3.0)
|
ed25519 (1.3.0)
|
||||||
elasticsearch (7.13.3)
|
elasticsearch (7.13.3)
|
||||||
elasticsearch-api (= 7.13.3)
|
elasticsearch-api (= 7.13.3)
|
||||||
|
@ -216,11 +215,11 @@ GEM
|
||||||
multi_json
|
multi_json
|
||||||
encryptor (3.0.0)
|
encryptor (3.0.0)
|
||||||
erubi (1.10.0)
|
erubi (1.10.0)
|
||||||
et-orbi (1.2.6)
|
et-orbi (1.2.7)
|
||||||
tzinfo
|
tzinfo
|
||||||
excon (0.76.0)
|
excon (0.76.0)
|
||||||
fabrication (2.28.0)
|
fabrication (2.28.0)
|
||||||
faker (2.20.0)
|
faker (2.21.0)
|
||||||
i18n (>= 1.8.11, < 2)
|
i18n (>= 1.8.11, < 2)
|
||||||
faraday (1.9.3)
|
faraday (1.9.3)
|
||||||
faraday-em_http (~> 1.0)
|
faraday-em_http (~> 1.0)
|
||||||
|
@ -264,8 +263,8 @@ GEM
|
||||||
fog-json (>= 1.0)
|
fog-json (>= 1.0)
|
||||||
ipaddress (>= 0.8)
|
ipaddress (>= 0.8)
|
||||||
formatador (0.2.5)
|
formatador (0.2.5)
|
||||||
fugit (1.5.2)
|
fugit (1.5.3)
|
||||||
et-orbi (~> 1.1, >= 1.1.8)
|
et-orbi (~> 1, >= 1.2.7)
|
||||||
raabro (~> 1.4)
|
raabro (~> 1.4)
|
||||||
fuubar (2.5.1)
|
fuubar (2.5.1)
|
||||||
rspec-core (~> 3.0)
|
rspec-core (~> 3.0)
|
||||||
|
@ -309,7 +308,7 @@ GEM
|
||||||
rainbow (>= 2.0.0)
|
rainbow (>= 2.0.0)
|
||||||
i18n (1.10.0)
|
i18n (1.10.0)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
i18n-tasks (1.0.8)
|
i18n-tasks (1.0.10)
|
||||||
activesupport (>= 4.0.2)
|
activesupport (>= 4.0.2)
|
||||||
ast (>= 2.1.0)
|
ast (>= 2.1.0)
|
||||||
better_html (~> 1.0)
|
better_html (~> 1.0)
|
||||||
|
@ -322,7 +321,7 @@ GEM
|
||||||
terminal-table (>= 1.5.1)
|
terminal-table (>= 1.5.1)
|
||||||
idn-ruby (0.1.4)
|
idn-ruby (0.1.4)
|
||||||
ipaddress (0.8.3)
|
ipaddress (0.8.3)
|
||||||
jmespath (1.6.0)
|
jmespath (1.6.1)
|
||||||
json (2.5.1)
|
json (2.5.1)
|
||||||
json-canonicalization (0.3.0)
|
json-canonicalization (0.3.0)
|
||||||
json-jwt (1.13.0)
|
json-jwt (1.13.0)
|
||||||
|
@ -377,7 +376,7 @@ GEM
|
||||||
activesupport (>= 4)
|
activesupport (>= 4)
|
||||||
railties (>= 4)
|
railties (>= 4)
|
||||||
request_store (~> 1.0)
|
request_store (~> 1.0)
|
||||||
loofah (2.16.0)
|
loofah (2.18.0)
|
||||||
crass (~> 1.0.2)
|
crass (~> 1.0.2)
|
||||||
nokogiri (>= 1.5.9)
|
nokogiri (>= 1.5.9)
|
||||||
mail (2.7.1)
|
mail (2.7.1)
|
||||||
|
@ -399,7 +398,7 @@ GEM
|
||||||
mini_mime (1.1.2)
|
mini_mime (1.1.2)
|
||||||
mini_portile2 (2.8.0)
|
mini_portile2 (2.8.0)
|
||||||
minitest (5.15.0)
|
minitest (5.15.0)
|
||||||
msgpack (1.4.4)
|
msgpack (1.5.1)
|
||||||
multi_json (1.15.0)
|
multi_json (1.15.0)
|
||||||
multipart-post (2.1.1)
|
multipart-post (2.1.1)
|
||||||
net-ldap (0.17.0)
|
net-ldap (0.17.0)
|
||||||
|
@ -407,7 +406,7 @@ GEM
|
||||||
net-ssh (>= 2.6.5, < 7.0.0)
|
net-ssh (>= 2.6.5, < 7.0.0)
|
||||||
net-ssh (6.1.0)
|
net-ssh (6.1.0)
|
||||||
nio4r (2.5.8)
|
nio4r (2.5.8)
|
||||||
nokogiri (1.13.3)
|
nokogiri (1.13.6)
|
||||||
mini_portile2 (~> 2.8.0)
|
mini_portile2 (~> 2.8.0)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nsa (0.2.8)
|
nsa (0.2.8)
|
||||||
|
@ -444,13 +443,13 @@ GEM
|
||||||
orm_adapter (0.5.0)
|
orm_adapter (0.5.0)
|
||||||
ox (2.14.11)
|
ox (2.14.11)
|
||||||
parallel (1.22.1)
|
parallel (1.22.1)
|
||||||
parser (3.1.1.0)
|
parser (3.1.2.0)
|
||||||
ast (~> 2.4.1)
|
ast (~> 2.4.1)
|
||||||
parslet (2.0.0)
|
parslet (2.0.0)
|
||||||
pastel (0.8.0)
|
pastel (0.8.0)
|
||||||
tty-color (~> 0.5)
|
tty-color (~> 0.5)
|
||||||
pg (1.3.5)
|
pg (1.3.5)
|
||||||
pghero (2.8.2)
|
pghero (2.8.3)
|
||||||
activerecord (>= 5)
|
activerecord (>= 5)
|
||||||
pkg-config (1.4.7)
|
pkg-config (1.4.7)
|
||||||
posix-spawn (0.3.15)
|
posix-spawn (0.3.15)
|
||||||
|
@ -470,7 +469,7 @@ GEM
|
||||||
pry (~> 0.13.0)
|
pry (~> 0.13.0)
|
||||||
pry-rails (0.3.9)
|
pry-rails (0.3.9)
|
||||||
pry (>= 0.10.4)
|
pry (>= 0.10.4)
|
||||||
public_suffix (4.0.6)
|
public_suffix (4.0.7)
|
||||||
puma (5.6.4)
|
puma (5.6.4)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
pundit (2.2.0)
|
pundit (2.2.0)
|
||||||
|
@ -478,7 +477,7 @@ GEM
|
||||||
raabro (1.4.0)
|
raabro (1.4.0)
|
||||||
racc (1.6.0)
|
racc (1.6.0)
|
||||||
rack (2.2.3)
|
rack (2.2.3)
|
||||||
rack-attack (6.6.0)
|
rack-attack (6.6.1)
|
||||||
rack (>= 1.0, < 3)
|
rack (>= 1.0, < 3)
|
||||||
rack-cors (1.1.1)
|
rack-cors (1.1.1)
|
||||||
rack (>= 2.0.0)
|
rack (>= 2.0.0)
|
||||||
|
@ -492,20 +491,20 @@ GEM
|
||||||
rack
|
rack
|
||||||
rack-test (1.1.0)
|
rack-test (1.1.0)
|
||||||
rack (>= 1.0, < 3)
|
rack (>= 1.0, < 3)
|
||||||
rails (6.1.5)
|
rails (6.1.6)
|
||||||
actioncable (= 6.1.5)
|
actioncable (= 6.1.6)
|
||||||
actionmailbox (= 6.1.5)
|
actionmailbox (= 6.1.6)
|
||||||
actionmailer (= 6.1.5)
|
actionmailer (= 6.1.6)
|
||||||
actionpack (= 6.1.5)
|
actionpack (= 6.1.6)
|
||||||
actiontext (= 6.1.5)
|
actiontext (= 6.1.6)
|
||||||
actionview (= 6.1.5)
|
actionview (= 6.1.6)
|
||||||
activejob (= 6.1.5)
|
activejob (= 6.1.6)
|
||||||
activemodel (= 6.1.5)
|
activemodel (= 6.1.6)
|
||||||
activerecord (= 6.1.5)
|
activerecord (= 6.1.6)
|
||||||
activestorage (= 6.1.5)
|
activestorage (= 6.1.6)
|
||||||
activesupport (= 6.1.5)
|
activesupport (= 6.1.6)
|
||||||
bundler (>= 1.15.0)
|
bundler (>= 1.15.0)
|
||||||
railties (= 6.1.5)
|
railties (= 6.1.6)
|
||||||
sprockets-rails (>= 2.0.0)
|
sprockets-rails (>= 2.0.0)
|
||||||
rails-controller-testing (1.0.5)
|
rails-controller-testing (1.0.5)
|
||||||
actionpack (>= 5.0.1.rc1)
|
actionpack (>= 5.0.1.rc1)
|
||||||
|
@ -521,9 +520,9 @@ GEM
|
||||||
railties (>= 6.0.0, < 7)
|
railties (>= 6.0.0, < 7)
|
||||||
rails-settings-cached (0.6.6)
|
rails-settings-cached (0.6.6)
|
||||||
rails (>= 4.2.0)
|
rails (>= 4.2.0)
|
||||||
railties (6.1.5)
|
railties (6.1.6)
|
||||||
actionpack (= 6.1.5)
|
actionpack (= 6.1.6)
|
||||||
activesupport (= 6.1.5)
|
activesupport (= 6.1.6)
|
||||||
method_source
|
method_source
|
||||||
rake (>= 12.2)
|
rake (>= 12.2)
|
||||||
thor (~> 1.0)
|
thor (~> 1.0)
|
||||||
|
@ -537,7 +536,7 @@ GEM
|
||||||
redis (4.5.1)
|
redis (4.5.1)
|
||||||
redis-namespace (1.8.2)
|
redis-namespace (1.8.2)
|
||||||
redis (>= 3.0.4)
|
redis (>= 3.0.4)
|
||||||
regexp_parser (2.2.1)
|
regexp_parser (2.4.0)
|
||||||
request_store (1.5.1)
|
request_store (1.5.1)
|
||||||
rack (>= 1.4)
|
rack (>= 1.4)
|
||||||
responders (3.0.1)
|
responders (3.0.1)
|
||||||
|
@ -555,10 +554,10 @@ GEM
|
||||||
rspec-expectations (3.11.0)
|
rspec-expectations (3.11.0)
|
||||||
diff-lcs (>= 1.2.0, < 2.0)
|
diff-lcs (>= 1.2.0, < 2.0)
|
||||||
rspec-support (~> 3.11.0)
|
rspec-support (~> 3.11.0)
|
||||||
rspec-mocks (3.11.0)
|
rspec-mocks (3.11.1)
|
||||||
diff-lcs (>= 1.2.0, < 2.0)
|
diff-lcs (>= 1.2.0, < 2.0)
|
||||||
rspec-support (~> 3.11.0)
|
rspec-support (~> 3.11.0)
|
||||||
rspec-rails (5.1.1)
|
rspec-rails (5.1.2)
|
||||||
actionpack (>= 5.2)
|
actionpack (>= 5.2)
|
||||||
activesupport (>= 5.2)
|
activesupport (>= 5.2)
|
||||||
railties (>= 5.2)
|
railties (>= 5.2)
|
||||||
|
@ -572,16 +571,16 @@ GEM
|
||||||
rspec-support (3.11.0)
|
rspec-support (3.11.0)
|
||||||
rspec_junit_formatter (0.5.1)
|
rspec_junit_formatter (0.5.1)
|
||||||
rspec-core (>= 2, < 4, != 2.12.0)
|
rspec-core (>= 2, < 4, != 2.12.0)
|
||||||
rubocop (1.26.1)
|
rubocop (1.29.1)
|
||||||
parallel (~> 1.10)
|
parallel (~> 1.10)
|
||||||
parser (>= 3.1.0.0)
|
parser (>= 3.1.0.0)
|
||||||
rainbow (>= 2.2.2, < 4.0)
|
rainbow (>= 2.2.2, < 4.0)
|
||||||
regexp_parser (>= 1.8, < 3.0)
|
regexp_parser (>= 1.8, < 3.0)
|
||||||
rexml
|
rexml (>= 3.2.5, < 4.0)
|
||||||
rubocop-ast (>= 1.16.0, < 2.0)
|
rubocop-ast (>= 1.17.0, < 2.0)
|
||||||
ruby-progressbar (~> 1.7)
|
ruby-progressbar (~> 1.7)
|
||||||
unicode-display_width (>= 1.4.0, < 3.0)
|
unicode-display_width (>= 1.4.0, < 3.0)
|
||||||
rubocop-ast (1.16.0)
|
rubocop-ast (1.18.0)
|
||||||
parser (>= 3.1.1.0)
|
parser (>= 3.1.1.0)
|
||||||
rubocop-rails (2.14.2)
|
rubocop-rails (2.14.2)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
|
@ -604,20 +603,18 @@ GEM
|
||||||
railties (>= 4.0.0)
|
railties (>= 4.0.0)
|
||||||
securecompare (1.0.0)
|
securecompare (1.0.0)
|
||||||
semantic_range (3.0.0)
|
semantic_range (3.0.0)
|
||||||
sidekiq (6.4.1)
|
sidekiq (6.4.2)
|
||||||
connection_pool (>= 2.2.2)
|
connection_pool (>= 2.2.2)
|
||||||
rack (~> 2.0)
|
rack (~> 2.0)
|
||||||
redis (>= 4.2.0)
|
redis (>= 4.2.0)
|
||||||
sidekiq-bulk (0.2.0)
|
sidekiq-bulk (0.2.0)
|
||||||
sidekiq
|
sidekiq
|
||||||
sidekiq-scheduler (3.1.1)
|
sidekiq-scheduler (4.0.0)
|
||||||
e2mmap
|
redis (>= 4.2.0)
|
||||||
redis (>= 3, < 5)
|
|
||||||
rufus-scheduler (~> 3.2)
|
rufus-scheduler (~> 3.2)
|
||||||
sidekiq (>= 3)
|
sidekiq (>= 4)
|
||||||
thwait
|
|
||||||
tilt (>= 1.4.0)
|
tilt (>= 1.4.0)
|
||||||
sidekiq-unique-jobs (7.1.16)
|
sidekiq-unique-jobs (7.1.22)
|
||||||
brpoplpush-redis_script (> 0.1.1, <= 2.0.0)
|
brpoplpush-redis_script (> 0.1.1, <= 2.0.0)
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.5)
|
concurrent-ruby (~> 1.0, >= 1.0.5)
|
||||||
sidekiq (>= 5.0, < 8.0)
|
sidekiq (>= 5.0, < 8.0)
|
||||||
|
@ -646,7 +643,7 @@ GEM
|
||||||
net-ssh (>= 2.8.0)
|
net-ssh (>= 2.8.0)
|
||||||
stackprof (0.2.19)
|
stackprof (0.2.19)
|
||||||
statsd-ruby (1.5.0)
|
statsd-ruby (1.5.0)
|
||||||
stoplight (2.2.1)
|
stoplight (3.0.0)
|
||||||
strong_migrations (0.7.9)
|
strong_migrations (0.7.9)
|
||||||
activerecord (>= 5)
|
activerecord (>= 5)
|
||||||
swd (1.3.0)
|
swd (1.3.0)
|
||||||
|
@ -659,8 +656,6 @@ GEM
|
||||||
terrapin (0.6.0)
|
terrapin (0.6.0)
|
||||||
climate_control (>= 0.0.3, < 1.0)
|
climate_control (>= 0.0.3, < 1.0)
|
||||||
thor (1.2.1)
|
thor (1.2.1)
|
||||||
thwait (0.2.0)
|
|
||||||
e2mmap
|
|
||||||
tilt (2.0.10)
|
tilt (2.0.10)
|
||||||
tpm-key_attestation (0.9.0)
|
tpm-key_attestation (0.9.0)
|
||||||
bindata (~> 2.4)
|
bindata (~> 2.4)
|
||||||
|
@ -737,11 +732,11 @@ DEPENDENCIES
|
||||||
active_record_query_trace (~> 1.8)
|
active_record_query_trace (~> 1.8)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
annotate (~> 3.2)
|
annotate (~> 3.2)
|
||||||
aws-sdk-s3 (~> 1.113)
|
aws-sdk-s3 (~> 1.114)
|
||||||
better_errors (~> 2.9)
|
better_errors (~> 2.9)
|
||||||
binding_of_caller (~> 1.0)
|
binding_of_caller (~> 1.0)
|
||||||
blurhash (~> 0.1)
|
blurhash (~> 0.1)
|
||||||
bootsnap (~> 1.10.3)
|
bootsnap (~> 1.11.1)
|
||||||
brakeman (~> 5.2)
|
brakeman (~> 5.2)
|
||||||
browser
|
browser
|
||||||
bullet (~> 7.0)
|
bullet (~> 7.0)
|
||||||
|
@ -750,7 +745,7 @@ DEPENDENCIES
|
||||||
capistrano-rails (~> 1.6)
|
capistrano-rails (~> 1.6)
|
||||||
capistrano-rbenv (~> 2.2)
|
capistrano-rbenv (~> 2.2)
|
||||||
capistrano-yarn (~> 2.0)
|
capistrano-yarn (~> 2.0)
|
||||||
capybara (~> 3.36)
|
capybara (~> 3.37)
|
||||||
charlock_holmes (~> 0.7.7)
|
charlock_holmes (~> 0.7.7)
|
||||||
chewy (~> 7.2)
|
chewy (~> 7.2)
|
||||||
climate_control (~> 0.2)
|
climate_control (~> 0.2)
|
||||||
|
@ -765,7 +760,7 @@ DEPENDENCIES
|
||||||
dotenv-rails (~> 2.7)
|
dotenv-rails (~> 2.7)
|
||||||
ed25519 (~> 1.3)
|
ed25519 (~> 1.3)
|
||||||
fabrication (~> 2.28)
|
fabrication (~> 2.28)
|
||||||
faker (~> 2.20)
|
faker (~> 2.21)
|
||||||
fast_blank (~> 1.0)
|
fast_blank (~> 1.0)
|
||||||
fastimage
|
fastimage
|
||||||
fog-core (<= 2.1.0)
|
fog-core (<= 2.1.0)
|
||||||
|
@ -817,7 +812,7 @@ DEPENDENCIES
|
||||||
rack (~> 2.2.3)
|
rack (~> 2.2.3)
|
||||||
rack-attack (~> 6.6)
|
rack-attack (~> 6.6)
|
||||||
rack-cors (~> 1.1)
|
rack-cors (~> 1.1)
|
||||||
rails (~> 6.1.5)
|
rails (~> 6.1.6)
|
||||||
rails-controller-testing (~> 1.0)
|
rails-controller-testing (~> 1.0)
|
||||||
rails-i18n (~> 6.0)
|
rails-i18n (~> 6.0)
|
||||||
rails-settings-cached (~> 0.6)
|
rails-settings-cached (~> 0.6)
|
||||||
|
@ -830,14 +825,14 @@ DEPENDENCIES
|
||||||
rspec-rails (~> 5.1)
|
rspec-rails (~> 5.1)
|
||||||
rspec-sidekiq (~> 3.1)
|
rspec-sidekiq (~> 3.1)
|
||||||
rspec_junit_formatter (~> 0.5)
|
rspec_junit_formatter (~> 0.5)
|
||||||
rubocop (~> 1.26)
|
rubocop (~> 1.29)
|
||||||
rubocop-rails (~> 2.14)
|
rubocop-rails (~> 2.14)
|
||||||
ruby-progressbar (~> 1.11)
|
ruby-progressbar (~> 1.11)
|
||||||
sanitize (~> 6.0)
|
sanitize (~> 6.0)
|
||||||
scenic (~> 1.6)
|
scenic (~> 1.6)
|
||||||
sidekiq (~> 6.4)
|
sidekiq (~> 6.4)
|
||||||
sidekiq-bulk (~> 0.2.0)
|
sidekiq-bulk (~> 0.2.0)
|
||||||
sidekiq-scheduler (~> 3.1)
|
sidekiq-scheduler (~> 4.0)
|
||||||
sidekiq-unique-jobs (~> 7.1)
|
sidekiq-unique-jobs (~> 7.1)
|
||||||
simple-navigation (~> 4.3)
|
simple-navigation (~> 4.3)
|
||||||
simple_form (~> 5.1)
|
simple_form (~> 5.1)
|
||||||
|
@ -845,7 +840,7 @@ DEPENDENCIES
|
||||||
sprockets (~> 3.7.2)
|
sprockets (~> 3.7.2)
|
||||||
sprockets-rails (~> 3.4)
|
sprockets-rails (~> 3.4)
|
||||||
stackprof
|
stackprof
|
||||||
stoplight (~> 2.2.1)
|
stoplight (~> 3.0.0)
|
||||||
strong_migrations (~> 0.7)
|
strong_migrations (~> 0.7)
|
||||||
thor (~> 1.2)
|
thor (~> 1.2)
|
||||||
tty-prompt (~> 0.23)
|
tty-prompt (~> 0.23)
|
||||||
|
|
|
@ -14,7 +14,7 @@ A "vulnerability in Mastodon" is a vulnerability in the code distributed through
|
||||||
| ------- | ------------------ |
|
| ------- | ------------------ |
|
||||||
| 3.5.x | Yes |
|
| 3.5.x | Yes |
|
||||||
| 3.4.x | Yes |
|
| 3.4.x | Yes |
|
||||||
| 3.3.x | Yes |
|
| 3.3.x | No |
|
||||||
| < 3.3 | No |
|
| < 3.3 | No |
|
||||||
|
|
||||||
[bug-bounty]: https://app.intigriti.com/programs/mastodon/mastodonio/detail
|
[bug-bounty]: https://app.intigriti.com/programs/mastodon/mastodonio/detail
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class AccountsIndex < Chewy::Index
|
class AccountsIndex < Chewy::Index
|
||||||
settings index: { refresh_interval: '5m' }, analysis: {
|
settings index: { refresh_interval: '30s' }, analysis: {
|
||||||
analyzer: {
|
analyzer: {
|
||||||
content: {
|
content: {
|
||||||
tokenizer: 'whitespace',
|
tokenizer: 'whitespace',
|
||||||
|
@ -23,7 +23,7 @@ class AccountsIndex < Chewy::Index
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
index_scope ::Account.searchable.includes(:account_stat), delete_if: ->(account) { account.destroyed? || !account.searchable? }
|
index_scope ::Account.searchable.includes(:account_stat)
|
||||||
|
|
||||||
root date_detection: false do
|
root date_detection: false do
|
||||||
field :id, type: 'long'
|
field :id, type: 'long'
|
||||||
|
@ -36,8 +36,8 @@ class AccountsIndex < Chewy::Index
|
||||||
field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'content'
|
field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'content'
|
||||||
end
|
end
|
||||||
|
|
||||||
field :following_count, type: 'long', value: ->(account) { account.following.local.count }
|
field :following_count, type: 'long', value: ->(account) { account.following_count }
|
||||||
field :followers_count, type: 'long', value: ->(account) { account.followers.local.count }
|
field :followers_count, type: 'long', value: ->(account) { account.followers_count }
|
||||||
field :last_status_at, type: 'date', value: ->(account) { account.last_status_at || account.created_at }
|
field :last_status_at, type: 'date', value: ->(account) { account.last_status_at || account.created_at }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
class StatusesIndex < Chewy::Index
|
class StatusesIndex < Chewy::Index
|
||||||
include FormattingHelper
|
include FormattingHelper
|
||||||
|
|
||||||
settings index: { refresh_interval: '15m' }, analysis: {
|
settings index: { refresh_interval: '30s' }, analysis: {
|
||||||
filter: {
|
filter: {
|
||||||
english_stop: {
|
english_stop: {
|
||||||
type: 'stop',
|
type: 'stop',
|
||||||
|
@ -33,6 +33,8 @@ class StatusesIndex < Chewy::Index
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# We do not use delete_if option here because it would call a method that we
|
||||||
|
# expect to be called with crutches without crutches, causing n+1 queries
|
||||||
index_scope ::Status.unscoped.kept.without_reblogs.includes(:media_attachments, :preloadable_poll)
|
index_scope ::Status.unscoped.kept.without_reblogs.includes(:media_attachments, :preloadable_poll)
|
||||||
|
|
||||||
crutch :mentions do |collection|
|
crutch :mentions do |collection|
|
||||||
|
@ -55,6 +57,11 @@ class StatusesIndex < Chewy::Index
|
||||||
data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }
|
data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
crutch :votes do |collection|
|
||||||
|
data = ::PollVote.joins(:poll).where(poll: { status_id: collection.map(&:id) }).where(account: Account.local).pluck(:status_id, :account_id)
|
||||||
|
data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }
|
||||||
|
end
|
||||||
|
|
||||||
root date_detection: false do
|
root date_detection: false do
|
||||||
field :id, type: 'long'
|
field :id, type: 'long'
|
||||||
field :account_id, type: 'long'
|
field :account_id, type: 'long'
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class TagsIndex < Chewy::Index
|
class TagsIndex < Chewy::Index
|
||||||
settings index: { refresh_interval: '15m' }, analysis: {
|
settings index: { refresh_interval: '30s' }, analysis: {
|
||||||
analyzer: {
|
analyzer: {
|
||||||
content: {
|
content: {
|
||||||
tokenizer: 'keyword',
|
tokenizer: 'keyword',
|
||||||
|
@ -23,7 +23,11 @@ class TagsIndex < Chewy::Index
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
index_scope ::Tag.listable, delete_if: ->(tag) { tag.destroyed? || !tag.listable? }
|
index_scope ::Tag.listable
|
||||||
|
|
||||||
|
crutch :time_period do
|
||||||
|
7.days.ago.to_date..0.days.ago.to_date
|
||||||
|
end
|
||||||
|
|
||||||
root date_detection: false do
|
root date_detection: false do
|
||||||
field :name, type: 'text', analyzer: 'content' do
|
field :name, type: 'text', analyzer: 'content' do
|
||||||
|
@ -31,7 +35,7 @@ class TagsIndex < Chewy::Index
|
||||||
end
|
end
|
||||||
|
|
||||||
field :reviewed, type: 'boolean', value: ->(tag) { tag.reviewed? }
|
field :reviewed, type: 'boolean', value: ->(tag) { tag.reviewed? }
|
||||||
field :usage, type: 'long', value: ->(tag) { tag.history.reduce(0) { |total, day| total + day.accounts } }
|
field :usage, type: 'long', value: ->(tag, crutches) { tag.history.aggregate(crutches.time_period).accounts }
|
||||||
field :last_status_at, type: 'date', value: ->(tag) { tag.last_status_at || tag.created_at }
|
field :last_status_at, type: 'date', value: ->(tag) { tag.last_status_at || tag.created_at }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -45,7 +45,6 @@ class AccountsController < ApplicationController
|
||||||
limit = params[:limit].present? ? [params[:limit].to_i, PAGE_SIZE_MAX].min : PAGE_SIZE
|
limit = params[:limit].present? ? [params[:limit].to_i, PAGE_SIZE_MAX].min : PAGE_SIZE
|
||||||
@statuses = filtered_statuses.without_reblogs.limit(limit)
|
@statuses = filtered_statuses.without_reblogs.limit(limit)
|
||||||
@statuses = cache_collection(@statuses, Status)
|
@statuses = cache_collection(@statuses, Status)
|
||||||
render xml: RSS::AccountSerializer.render(@account, @statuses, params[:tag])
|
|
||||||
end
|
end
|
||||||
|
|
||||||
format.json do
|
format.json do
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
class ActivityPub::BaseController < Api::BaseController
|
class ActivityPub::BaseController < Api::BaseController
|
||||||
skip_before_action :require_authenticated_user!
|
skip_before_action :require_authenticated_user!
|
||||||
|
skip_before_action :require_not_suspended!
|
||||||
skip_around_action :set_locale
|
skip_around_action :set_locale
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
module Admin
|
module Admin
|
||||||
class DashboardController < BaseController
|
class DashboardController < BaseController
|
||||||
|
include Redisable
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@system_checks = Admin::SystemCheck.perform
|
@system_checks = Admin::SystemCheck.perform
|
||||||
@time_period = (29.days.ago.to_date...Time.now.utc.to_date)
|
@time_period = (29.days.ago.to_date...Time.now.utc.to_date)
|
||||||
|
@ -15,10 +17,10 @@ module Admin
|
||||||
|
|
||||||
def redis_info
|
def redis_info
|
||||||
@redis_info ||= begin
|
@redis_info ||= begin
|
||||||
if Redis.current.is_a?(Redis::Namespace)
|
if redis.is_a?(Redis::Namespace)
|
||||||
Redis.current.redis.info
|
redis.redis.info
|
||||||
else
|
else
|
||||||
Redis.current.info
|
redis.info
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,6 +4,17 @@ module Admin
|
||||||
class DomainBlocksController < BaseController
|
class DomainBlocksController < BaseController
|
||||||
before_action :set_domain_block, only: [:show, :destroy, :edit, :update]
|
before_action :set_domain_block, only: [:show, :destroy, :edit, :update]
|
||||||
|
|
||||||
|
def batch
|
||||||
|
@form = Form::DomainBlockBatch.new(form_domain_block_batch_params.merge(current_account: current_account, action: action_from_button))
|
||||||
|
@form.save
|
||||||
|
rescue ActionController::ParameterMissing
|
||||||
|
flash[:alert] = I18n.t('admin.email_domain_blocks.no_domain_block_selected')
|
||||||
|
rescue Mastodon::NotPermittedError
|
||||||
|
flash[:alert] = I18n.t('admin.domain_blocks.created_msg')
|
||||||
|
else
|
||||||
|
redirect_to admin_instances_path(limited: '1'), notice: I18n.t('admin.domain_blocks.created_msg')
|
||||||
|
end
|
||||||
|
|
||||||
def new
|
def new
|
||||||
authorize :domain_block, :create?
|
authorize :domain_block, :create?
|
||||||
@domain_block = DomainBlock.new(domain: params[:_domain])
|
@domain_block = DomainBlock.new(domain: params[:_domain])
|
||||||
|
@ -76,5 +87,15 @@ module Admin
|
||||||
def resource_params
|
def resource_params
|
||||||
params.require(:domain_block).permit(:domain, :severity, :reject_media, :reject_reports, :private_comment, :public_comment, :obfuscate)
|
params.require(:domain_block).permit(:domain, :severity, :reject_media, :reject_reports, :private_comment, :public_comment, :obfuscate)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def form_domain_block_batch_params
|
||||||
|
params.require(:form_domain_block_batch).permit(domain_blocks_attributes: [:enabled, :domain, :severity, :reject_media, :reject_reports, :private_comment, :public_comment, :obfuscate])
|
||||||
|
end
|
||||||
|
|
||||||
|
def action_from_button
|
||||||
|
if params[:save]
|
||||||
|
'save'
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,60 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'csv'
|
||||||
|
|
||||||
|
module Admin
|
||||||
|
class ExportDomainAllowsController < BaseController
|
||||||
|
include AdminExportControllerConcern
|
||||||
|
|
||||||
|
before_action :set_dummy_import!, only: [:new]
|
||||||
|
|
||||||
|
ROWS_PROCESSING_LIMIT = 20_000
|
||||||
|
|
||||||
|
def new
|
||||||
|
authorize :domain_allow, :create?
|
||||||
|
end
|
||||||
|
|
||||||
|
def export
|
||||||
|
authorize :instance, :index?
|
||||||
|
send_export_file
|
||||||
|
end
|
||||||
|
|
||||||
|
def import
|
||||||
|
authorize :domain_allow, :create?
|
||||||
|
begin
|
||||||
|
@import = Admin::Import.new(import_params)
|
||||||
|
parse_import_data!(export_headers)
|
||||||
|
|
||||||
|
@data.take(ROWS_PROCESSING_LIMIT).each do |row|
|
||||||
|
domain = row['#domain'].strip
|
||||||
|
next if DomainAllow.allowed?(domain)
|
||||||
|
|
||||||
|
domain_allow = DomainAllow.new(domain: domain)
|
||||||
|
log_action :create, domain_allow if domain_allow.save
|
||||||
|
end
|
||||||
|
flash[:notice] = I18n.t('admin.domain_allows.created_msg')
|
||||||
|
rescue ActionController::ParameterMissing
|
||||||
|
flash[:error] = I18n.t('admin.export_domain_allows.no_file')
|
||||||
|
end
|
||||||
|
redirect_to admin_instances_path
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def export_filename
|
||||||
|
'domain_allows.csv'
|
||||||
|
end
|
||||||
|
|
||||||
|
def export_headers
|
||||||
|
%w(#domain)
|
||||||
|
end
|
||||||
|
|
||||||
|
def export_data
|
||||||
|
CSV.generate(headers: export_headers, write_headers: true) do |content|
|
||||||
|
DomainAllow.allowed_domains.each do |instance|
|
||||||
|
content << [instance.domain]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,71 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'csv'
|
||||||
|
|
||||||
|
module Admin
|
||||||
|
class ExportDomainBlocksController < BaseController
|
||||||
|
include AdminExportControllerConcern
|
||||||
|
|
||||||
|
before_action :set_dummy_import!, only: [:new]
|
||||||
|
|
||||||
|
ROWS_PROCESSING_LIMIT = 20_000
|
||||||
|
|
||||||
|
def new
|
||||||
|
authorize :domain_block, :create?
|
||||||
|
end
|
||||||
|
|
||||||
|
def export
|
||||||
|
authorize :instance, :index?
|
||||||
|
send_export_file
|
||||||
|
end
|
||||||
|
|
||||||
|
def import
|
||||||
|
authorize :domain_block, :create?
|
||||||
|
|
||||||
|
@import = Admin::Import.new(import_params)
|
||||||
|
parse_import_data!(export_headers)
|
||||||
|
|
||||||
|
@global_private_comment = I18n.t('admin.export_domain_blocks.import.private_comment_template', source: @import.data_file_name, date: I18n.l(Time.now.utc))
|
||||||
|
|
||||||
|
@form = Form::DomainBlockBatch.new
|
||||||
|
@domain_blocks = @data.take(ROWS_PROCESSING_LIMIT).filter_map do |row|
|
||||||
|
domain = row['#domain'].strip
|
||||||
|
next if DomainBlock.rule_for(domain).present?
|
||||||
|
|
||||||
|
domain_block = DomainBlock.new(domain: domain,
|
||||||
|
severity: row['#severity'].strip,
|
||||||
|
reject_media: row['#reject_media'].strip,
|
||||||
|
reject_reports: row['#reject_reports'].strip,
|
||||||
|
private_comment: @global_private_comment,
|
||||||
|
public_comment: row['#public_comment']&.strip,
|
||||||
|
obfuscate: row['#obfuscate'].strip)
|
||||||
|
|
||||||
|
domain_block if domain_block.valid?
|
||||||
|
end
|
||||||
|
|
||||||
|
@warning_domains = Instance.where(domain: @domain_blocks.map(&:domain)).where('EXISTS (SELECT 1 FROM follows JOIN accounts ON follows.account_id = accounts.id OR follows.target_account_id = accounts.id WHERE accounts.domain = instances.domain)').pluck(:domain)
|
||||||
|
rescue ActionController::ParameterMissing
|
||||||
|
flash.now[:alert] = I18n.t('admin.export_domain_blocks.no_file')
|
||||||
|
set_dummy_import!
|
||||||
|
render :new
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def export_filename
|
||||||
|
'domain_blocks.csv'
|
||||||
|
end
|
||||||
|
|
||||||
|
def export_headers
|
||||||
|
%w(#domain #severity #reject_media #reject_reports #public_comment #obfuscate)
|
||||||
|
end
|
||||||
|
|
||||||
|
def export_data
|
||||||
|
CSV.generate(headers: export_headers, write_headers: true) do |content|
|
||||||
|
DomainBlock.with_user_facing_limitations.each do |instance|
|
||||||
|
content << [instance.domain, instance.severity, instance.reject_media, instance.reject_reports, instance.public_comment, instance.obfuscate]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -11,6 +11,7 @@ class Api::BaseController < ApplicationController
|
||||||
skip_before_action :require_functional!, unless: :whitelist_mode?
|
skip_before_action :require_functional!, unless: :whitelist_mode?
|
||||||
|
|
||||||
before_action :require_authenticated_user!, if: :disallow_unauthenticated_api_access?
|
before_action :require_authenticated_user!, if: :disallow_unauthenticated_api_access?
|
||||||
|
before_action :require_not_suspended!
|
||||||
before_action :set_cache_headers
|
before_action :set_cache_headers
|
||||||
|
|
||||||
protect_from_forgery with: :null_session
|
protect_from_forgery with: :null_session
|
||||||
|
@ -97,6 +98,10 @@ class Api::BaseController < ApplicationController
|
||||||
render json: { error: 'This method requires an authenticated user' }, status: 401 unless current_user
|
render json: { error: 'This method requires an authenticated user' }, status: 401 unless current_user
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def require_not_suspended!
|
||||||
|
render json: { error: 'Your login is currently disabled' }, status: 403 if current_user&.account&.suspended?
|
||||||
|
end
|
||||||
|
|
||||||
def require_user!
|
def require_user!
|
||||||
if !current_user
|
if !current_user
|
||||||
render json: { error: 'This method requires an authenticated user' }, status: 422
|
render json: { error: 'This method requires an authenticated user' }, status: 422
|
||||||
|
|
|
@ -12,5 +12,7 @@ class Api::V1::Accounts::LookupController < Api::BaseController
|
||||||
|
|
||||||
def set_account
|
def set_account
|
||||||
@account = ResolveAccountService.new.call(params[:acct], skip_webfinger: true) || raise(ActiveRecord::RecordNotFound)
|
@account = ResolveAccountService.new.call(params[:acct], skip_webfinger: true) || raise(ActiveRecord::RecordNotFound)
|
||||||
|
rescue Addressable::URI::InvalidURIError
|
||||||
|
raise(ActiveRecord::RecordNotFound)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -9,6 +9,8 @@ class Api::V1::AccountsController < Api::BaseController
|
||||||
|
|
||||||
before_action :require_user!, except: [:show, :create]
|
before_action :require_user!, except: [:show, :create]
|
||||||
before_action :set_account, except: [:create]
|
before_action :set_account, except: [:create]
|
||||||
|
before_action :check_account_approval, except: [:create]
|
||||||
|
before_action :check_account_confirmation, except: [:create]
|
||||||
before_action :check_enabled_registrations, only: [:create]
|
before_action :check_enabled_registrations, only: [:create]
|
||||||
|
|
||||||
skip_before_action :require_authenticated_user!, only: :create
|
skip_before_action :require_authenticated_user!, only: :create
|
||||||
|
@ -74,6 +76,14 @@ class Api::V1::AccountsController < Api::BaseController
|
||||||
@account = Account.find(params[:id])
|
@account = Account.find(params[:id])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def check_account_approval
|
||||||
|
raise(ActiveRecord::RecordNotFound) if @account.local? && @account.user_pending?
|
||||||
|
end
|
||||||
|
|
||||||
|
def check_account_confirmation
|
||||||
|
raise(ActiveRecord::RecordNotFound) if @account.local? && !@account.user_confirmed?
|
||||||
|
end
|
||||||
|
|
||||||
def relationships(**options)
|
def relationships(**options)
|
||||||
AccountRelationshipsPresenter.new([@account.id], current_user.account_id, **options)
|
AccountRelationshipsPresenter.new([@account.id], current_user.account_id, **options)
|
||||||
end
|
end
|
||||||
|
|
|
@ -21,7 +21,7 @@ class Api::V1::BookmarksController < Api::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def results
|
def results
|
||||||
@_results ||= account_bookmarks.eager_load(:status).to_a_paginated_by_id(
|
@_results ||= account_bookmarks.joins(:status).eager_load(:status).to_a_paginated_by_id(
|
||||||
limit_param(DEFAULT_STATUSES_LIMIT),
|
limit_param(DEFAULT_STATUSES_LIMIT),
|
||||||
params_slice(:max_id, :since_id, :min_id)
|
params_slice(:max_id, :since_id, :min_id)
|
||||||
)
|
)
|
||||||
|
|
|
@ -21,7 +21,7 @@ class Api::V1::FavouritesController < Api::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def results
|
def results
|
||||||
@_results ||= account_favourites.eager_load(:status).to_a_paginated_by_id(
|
@_results ||= account_favourites.joins(:status).eager_load(:status).to_a_paginated_by_id(
|
||||||
limit_param(DEFAULT_STATUSES_LIMIT),
|
limit_param(DEFAULT_STATUSES_LIMIT),
|
||||||
params_slice(:max_id, :since_id, :min_id)
|
params_slice(:max_id, :since_id, :min_id)
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Api::V1::NotificationsController < Api::BaseController
|
class Api::V1::NotificationsController < Api::BaseController
|
||||||
before_action -> { doorkeeper_authorize! :read, :'read:notifications' }, except: [:clear, :dismiss]
|
before_action -> { doorkeeper_authorize! :read, :'read:notifications' }, except: [:clear, :dismiss, :destroy, :destroy_multiple]
|
||||||
before_action -> { doorkeeper_authorize! :write, :'write:notifications' }, only: [:clear, :dismiss]
|
before_action -> { doorkeeper_authorize! :write, :'write:notifications' }, only: [:clear, :dismiss, :destroy, :destroy_multiple]
|
||||||
before_action :require_user!
|
before_action :require_user!
|
||||||
after_action :insert_pagination_headers, only: :index
|
after_action :insert_pagination_headers, only: :index
|
||||||
|
|
||||||
|
|
|
@ -89,7 +89,7 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController
|
||||||
|
|
||||||
def after_confirmation_path_for(_resource_name, user)
|
def after_confirmation_path_for(_resource_name, user)
|
||||||
if user.created_by_application && truthy_param?(:redirect_to_app)
|
if user.created_by_application && truthy_param?(:redirect_to_app)
|
||||||
user.created_by_application.redirect_uri
|
user.created_by_application.confirmation_redirect_uri
|
||||||
else
|
else
|
||||||
super
|
super
|
||||||
end
|
end
|
||||||
|
|
|
@ -14,7 +14,7 @@ class AuthorizeInteractionsController < ApplicationController
|
||||||
if @resource.is_a?(Account)
|
if @resource.is_a?(Account)
|
||||||
render :show
|
render :show
|
||||||
elsif @resource.is_a?(Status)
|
elsif @resource.is_a?(Status)
|
||||||
redirect_to web_url("statuses/#{@resource.id}")
|
redirect_to web_url("@#{@resource.account.pretty_acct}/#{@resource.id}")
|
||||||
else
|
else
|
||||||
render :error
|
render :error
|
||||||
end
|
end
|
||||||
|
@ -26,15 +26,17 @@ class AuthorizeInteractionsController < ApplicationController
|
||||||
else
|
else
|
||||||
render :error
|
render :error
|
||||||
end
|
end
|
||||||
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
rescue ActiveRecord::RecordNotFound
|
||||||
render :error
|
render :error
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_resource
|
def set_resource
|
||||||
@resource = located_resource || render(:error)
|
@resource = located_resource
|
||||||
authorize(@resource, :show?) if @resource.is_a?(Status)
|
authorize(@resource, :show?) if @resource.is_a?(Status)
|
||||||
|
rescue Mastodon::NotPermittedError
|
||||||
|
not_found
|
||||||
end
|
end
|
||||||
|
|
||||||
def located_resource
|
def located_resource
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module AdminExportControllerConcern
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def send_export_file
|
||||||
|
respond_to do |format|
|
||||||
|
format.csv { send_data export_data, filename: export_filename }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def export_data
|
||||||
|
raise 'Override in controller'
|
||||||
|
end
|
||||||
|
|
||||||
|
def export_filename
|
||||||
|
raise 'Override in controller'
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_dummy_import!
|
||||||
|
@import = Admin::Import.new
|
||||||
|
end
|
||||||
|
|
||||||
|
def import_params
|
||||||
|
params.require(:admin_import).permit(:data)
|
||||||
|
end
|
||||||
|
|
||||||
|
def import_data
|
||||||
|
Paperclip.io_adapters.for(@import.data).read
|
||||||
|
end
|
||||||
|
|
||||||
|
def parse_import_data!(default_headers)
|
||||||
|
data = CSV.parse(import_data, headers: true)
|
||||||
|
data = CSV.parse(import_data, headers: default_headers) unless data.headers&.first&.strip&.include?(default_headers[0])
|
||||||
|
@data = data.reject(&:blank?)
|
||||||
|
end
|
||||||
|
end
|
|
@ -22,7 +22,10 @@ class FollowingAccountsController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
format.json do
|
format.json do
|
||||||
raise Mastodon::NotPermittedError if page_requested? && @account.hide_collections?
|
if page_requested? && @account.hide_collections?
|
||||||
|
forbidden
|
||||||
|
next
|
||||||
|
end
|
||||||
|
|
||||||
expires_in(page_requested? ? 0 : 3.minutes, public: public_fetch_mode?)
|
expires_in(page_requested? ? 0 : 3.minutes, public: public_fetch_mode?)
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
class MediaProxyController < ApplicationController
|
class MediaProxyController < ApplicationController
|
||||||
include RoutingHelper
|
include RoutingHelper
|
||||||
include Authorization
|
include Authorization
|
||||||
|
include Redisable
|
||||||
|
include Lockable
|
||||||
|
|
||||||
skip_before_action :store_current_location
|
skip_before_action :store_current_location
|
||||||
skip_before_action :require_functional!
|
skip_before_action :require_functional!
|
||||||
|
@ -15,14 +17,10 @@ class MediaProxyController < ApplicationController
|
||||||
rescue_from HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError, with: :internal_server_error
|
rescue_from HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError, with: :internal_server_error
|
||||||
|
|
||||||
def show
|
def show
|
||||||
RedisLock.acquire(lock_options) do |lock|
|
with_lock("media_download:#{params[:id]}") do
|
||||||
if lock.acquired?
|
@media_attachment = MediaAttachment.remote.attached.find(params[:id])
|
||||||
@media_attachment = MediaAttachment.remote.attached.find(params[:id])
|
authorize @media_attachment.status, :show?
|
||||||
authorize @media_attachment.status, :show?
|
redownload! if @media_attachment.needs_redownload? && !reject_media?
|
||||||
redownload! if @media_attachment.needs_redownload? && !reject_media?
|
|
||||||
else
|
|
||||||
raise Mastodon::RaceConditionError
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
redirect_to full_asset_url(@media_attachment.file.url(version))
|
redirect_to full_asset_url(@media_attachment.file.url(version))
|
||||||
|
@ -44,10 +42,6 @@ class MediaProxyController < ApplicationController
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def lock_options
|
|
||||||
{ redis: Redis.current, key: "media_download:#{params[:id]}", autorelease: 15.minutes.seconds }
|
|
||||||
end
|
|
||||||
|
|
||||||
def reject_media?
|
def reject_media?
|
||||||
DomainBlock.reject_media?(@media_attachment.account.domain)
|
DomainBlock.reject_media?(@media_attachment.account.domain)
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,7 +2,8 @@
|
||||||
|
|
||||||
class Oauth::TokensController < Doorkeeper::TokensController
|
class Oauth::TokensController < Doorkeeper::TokensController
|
||||||
def revoke
|
def revoke
|
||||||
unsubscribe_for_token if authorized? && token.accessible?
|
unsubscribe_for_token if token.present? && authorized? && token.accessible?
|
||||||
|
|
||||||
super
|
super
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
class Settings::ExportsController < Settings::BaseController
|
class Settings::ExportsController < Settings::BaseController
|
||||||
include Authorization
|
include Authorization
|
||||||
|
include Redisable
|
||||||
|
include Lockable
|
||||||
|
|
||||||
skip_before_action :require_functional!
|
skip_before_action :require_functional!
|
||||||
|
|
||||||
|
@ -13,21 +15,13 @@ class Settings::ExportsController < Settings::BaseController
|
||||||
def create
|
def create
|
||||||
backup = nil
|
backup = nil
|
||||||
|
|
||||||
RedisLock.acquire(lock_options) do |lock|
|
with_lock("backup:#{current_user.id}") do
|
||||||
if lock.acquired?
|
authorize :backup, :create?
|
||||||
authorize :backup, :create?
|
backup = current_user.backups.create!
|
||||||
backup = current_user.backups.create!
|
|
||||||
else
|
|
||||||
raise Mastodon::RaceConditionError
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
BackupWorker.perform_async(backup.id)
|
BackupWorker.perform_async(backup.id)
|
||||||
|
|
||||||
redirect_to settings_export_path
|
redirect_to settings_export_path
|
||||||
end
|
end
|
||||||
|
|
||||||
def lock_options
|
|
||||||
{ redis: Redis.current, key: "backup:#{current_user.id}" }
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -27,7 +27,6 @@ class TagsController < ApplicationController
|
||||||
|
|
||||||
format.rss do
|
format.rss do
|
||||||
expires_in 0, public: true
|
expires_in 0, public: true
|
||||||
render xml: RSS::TagSerializer.render(@tag, @statuses)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
format.json do
|
format.json do
|
||||||
|
|
|
@ -132,7 +132,7 @@ module ApplicationHelper
|
||||||
elsif status.private_visibility? || status.limited_visibility?
|
elsif status.private_visibility? || status.limited_visibility?
|
||||||
fa_icon('lock', title: I18n.t('statuses.visibilities.private'))
|
fa_icon('lock', title: I18n.t('statuses.visibilities.private'))
|
||||||
elsif status.direct_visibility?
|
elsif status.direct_visibility?
|
||||||
fa_icon('envelope', title: I18n.t('statuses.visibilities.direct'))
|
fa_icon('at', title: I18n.t('statuses.visibilities.direct'))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -244,7 +244,7 @@ module ApplicationHelper
|
||||||
end.values
|
end.values
|
||||||
end
|
end
|
||||||
|
|
||||||
def prerender_custom_emojis(html, custom_emojis)
|
def prerender_custom_emojis(html, custom_emojis, other_options = {})
|
||||||
EmojiFormatter.new(html, custom_emojis, animate: prefers_autoplay?).to_s
|
EmojiFormatter.new(html, custom_emojis, other_options.merge(animate: prefers_autoplay?)).to_s
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -18,6 +18,32 @@ module FormattingHelper
|
||||||
html_aware_format(status.text, status.local?, preloaded_accounts: [status.account] + (status.respond_to?(:active_mentions) ? status.active_mentions.map(&:account) : []), content_type: status.content_type)
|
html_aware_format(status.text, status.local?, preloaded_accounts: [status.account] + (status.respond_to?(:active_mentions) ? status.active_mentions.map(&:account) : []), content_type: status.content_type)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def rss_status_content_format(status)
|
||||||
|
html = status_content_format(status)
|
||||||
|
|
||||||
|
before_html = begin
|
||||||
|
if status.spoiler_text?
|
||||||
|
"<p><strong>#{I18n.t('rss.content_warning', locale: available_locale_or_nil(status.language) || I18n.default_locale)}</strong> #{h(status.spoiler_text)}</p><hr />"
|
||||||
|
else
|
||||||
|
''
|
||||||
|
end
|
||||||
|
end.html_safe # rubocop:disable Rails/OutputSafety
|
||||||
|
|
||||||
|
after_html = begin
|
||||||
|
if status.preloadable_poll
|
||||||
|
"<p>#{status.preloadable_poll.options.map { |o| "<input type=#{status.preloadable_poll.multiple? ? 'checkbox' : 'radio'} disabled /> #{h(o)}" }.join('<br />')}</p>"
|
||||||
|
else
|
||||||
|
''
|
||||||
|
end
|
||||||
|
end.html_safe # rubocop:disable Rails/OutputSafety
|
||||||
|
|
||||||
|
prerender_custom_emojis(
|
||||||
|
safe_join([before_html, html, after_html]),
|
||||||
|
status.emojis,
|
||||||
|
style: 'width: 1.1em; height: 1.1em; object-fit: contain; vertical-align: middle; margin: -.2ex .15em .2ex'
|
||||||
|
).to_str
|
||||||
|
end
|
||||||
|
|
||||||
def account_bio_format(account)
|
def account_bio_format(account)
|
||||||
html_aware_format(account.note, account.local?)
|
html_aware_format(account.note, account.local?)
|
||||||
end
|
end
|
||||||
|
|
|
@ -254,4 +254,8 @@ module LanguagesHelper
|
||||||
def valid_locale?(locale)
|
def valid_locale?(locale)
|
||||||
locale.present? && SUPPORTED_LOCALES.key?(locale.to_sym)
|
locale.present? && SUPPORTED_LOCALES.key?(locale.to_sym)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def available_locale_or_nil(locale_name)
|
||||||
|
locale_name.to_sym if locale_name.present? && I18n.available_locales.include?(locale_name.to_sym)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,2 +0,0 @@
|
||||||
module Settings::KeywordMutesHelper
|
|
||||||
end
|
|
|
@ -101,7 +101,7 @@ module StatusesHelper
|
||||||
when 'private'
|
when 'private'
|
||||||
fa_icon 'lock fw'
|
fa_icon 'lock fw'
|
||||||
when 'direct'
|
when 'direct'
|
||||||
fa_icon 'envelope fw'
|
fa_icon 'at fw'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -101,4 +101,20 @@ ready(() => {
|
||||||
|
|
||||||
const registrationMode = document.getElementById('form_admin_settings_registrations_mode');
|
const registrationMode = document.getElementById('form_admin_settings_registrations_mode');
|
||||||
if (registrationMode) onChangeRegistrationMode(registrationMode);
|
if (registrationMode) onChangeRegistrationMode(registrationMode);
|
||||||
|
|
||||||
|
const checkAllElement = document.querySelector('#batch_checkbox_all');
|
||||||
|
if (checkAllElement) {
|
||||||
|
checkAllElement.checked = [].every.call(document.querySelectorAll(batchCheckboxClassName), (content) => content.checked);
|
||||||
|
checkAllElement.indeterminate = !checkAllElement.checked && [].some.call(document.querySelectorAll(batchCheckboxClassName), (content) => content.checked);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelector('a#add-instance-button')?.addEventListener('click', (e) => {
|
||||||
|
const domain = document.getElementById('by_domain')?.value;
|
||||||
|
|
||||||
|
if (domain) {
|
||||||
|
const url = new URL(event.target.href);
|
||||||
|
url.searchParams.set('_domain', domain);
|
||||||
|
e.target.href = url;
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -88,6 +88,8 @@ export const PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_CHANGE = 'PINNED_ACCOUNTS_EDITOR
|
||||||
export const PINNED_ACCOUNTS_EDITOR_RESET = 'PINNED_ACCOUNTS_EDITOR_RESET';
|
export const PINNED_ACCOUNTS_EDITOR_RESET = 'PINNED_ACCOUNTS_EDITOR_RESET';
|
||||||
|
|
||||||
|
|
||||||
|
export const ACCOUNT_REVEAL = 'ACCOUNT_REVEAL';
|
||||||
|
|
||||||
export function fetchAccount(id) {
|
export function fetchAccount(id) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
dispatch(fetchRelationships([id]));
|
dispatch(fetchRelationships([id]));
|
||||||
|
@ -798,6 +800,11 @@ export function unpinAccountFail(error) {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const revealAccount = id => ({
|
||||||
|
type: ACCOUNT_REVEAL,
|
||||||
|
id,
|
||||||
|
});
|
||||||
|
|
||||||
export function fetchPinnedAccounts() {
|
export function fetchPinnedAccounts() {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
dispatch(fetchPinnedAccountsRequest());
|
dispatch(fetchPinnedAccountsRequest());
|
||||||
|
|
|
@ -48,12 +48,13 @@ export const COMPOSE_MOUNT = 'COMPOSE_MOUNT';
|
||||||
export const COMPOSE_UNMOUNT = 'COMPOSE_UNMOUNT';
|
export const COMPOSE_UNMOUNT = 'COMPOSE_UNMOUNT';
|
||||||
|
|
||||||
export const COMPOSE_ADVANCED_OPTIONS_CHANGE = 'COMPOSE_ADVANCED_OPTIONS_CHANGE';
|
export const COMPOSE_ADVANCED_OPTIONS_CHANGE = 'COMPOSE_ADVANCED_OPTIONS_CHANGE';
|
||||||
export const COMPOSE_SENSITIVITY_CHANGE = 'COMPOSE_SENSITIVITY_CHANGE';
|
export const COMPOSE_SENSITIVITY_CHANGE = 'COMPOSE_SENSITIVITY_CHANGE';
|
||||||
export const COMPOSE_SPOILERNESS_CHANGE = 'COMPOSE_SPOILERNESS_CHANGE';
|
export const COMPOSE_SPOILERNESS_CHANGE = 'COMPOSE_SPOILERNESS_CHANGE';
|
||||||
export const COMPOSE_SPOILER_TEXT_CHANGE = 'COMPOSE_SPOILER_TEXT_CHANGE';
|
export const COMPOSE_SPOILER_TEXT_CHANGE = 'COMPOSE_SPOILER_TEXT_CHANGE';
|
||||||
export const COMPOSE_VISIBILITY_CHANGE = 'COMPOSE_VISIBILITY_CHANGE';
|
export const COMPOSE_VISIBILITY_CHANGE = 'COMPOSE_VISIBILITY_CHANGE';
|
||||||
export const COMPOSE_LISTABILITY_CHANGE = 'COMPOSE_LISTABILITY_CHANGE';
|
export const COMPOSE_LISTABILITY_CHANGE = 'COMPOSE_LISTABILITY_CHANGE';
|
||||||
export const COMPOSE_CONTENT_TYPE_CHANGE = 'COMPOSE_CONTENT_TYPE_CHANGE';
|
export const COMPOSE_CONTENT_TYPE_CHANGE = 'COMPOSE_CONTENT_TYPE_CHANGE';
|
||||||
|
export const COMPOSE_LANGUAGE_CHANGE = 'COMPOSE_LANGUAGE_CHANGE';
|
||||||
|
|
||||||
export const COMPOSE_EMOJI_INSERT = 'COMPOSE_EMOJI_INSERT';
|
export const COMPOSE_EMOJI_INSERT = 'COMPOSE_EMOJI_INSERT';
|
||||||
|
|
||||||
|
@ -189,6 +190,7 @@ export function submitCompose(routerHistory) {
|
||||||
spoiler_text: spoilerText,
|
spoiler_text: spoilerText,
|
||||||
visibility: getState().getIn(['compose', 'privacy']),
|
visibility: getState().getIn(['compose', 'privacy']),
|
||||||
poll: getState().getIn(['compose', 'poll'], null),
|
poll: getState().getIn(['compose', 'poll'], null),
|
||||||
|
language: getState().getIn(['compose', 'language']),
|
||||||
},
|
},
|
||||||
headers: {
|
headers: {
|
||||||
'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']),
|
'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']),
|
||||||
|
@ -675,6 +677,11 @@ export function changeComposeSensitivity() {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const changeComposeLanguage = language => ({
|
||||||
|
type: COMPOSE_LANGUAGE_CHANGE,
|
||||||
|
language,
|
||||||
|
});
|
||||||
|
|
||||||
export function changeComposeSpoilerness() {
|
export function changeComposeSpoilerness() {
|
||||||
return {
|
return {
|
||||||
type: COMPOSE_SPOILERNESS_CHANGE,
|
type: COMPOSE_SPOILERNESS_CHANGE,
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import escapeTextContentForBrowser from 'escape-html';
|
import escapeTextContentForBrowser from 'escape-html';
|
||||||
import emojify from 'flavours/glitch/util/emoji';
|
import emojify from 'flavours/glitch/util/emoji';
|
||||||
import { unescapeHTML } from 'flavours/glitch/util/html';
|
import { unescapeHTML } from 'flavours/glitch/util/html';
|
||||||
import { expandSpoilers } from 'flavours/glitch/util/initial_state';
|
|
||||||
|
|
||||||
const domParser = new DOMParser();
|
const domParser = new DOMParser();
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { saveSettings } from './settings';
|
||||||
|
|
||||||
|
export const LANGUAGE_USE = 'LANGUAGE_USE';
|
||||||
|
|
||||||
|
export const useLanguage = language => dispatch => {
|
||||||
|
dispatch({
|
||||||
|
type: LANGUAGE_USE,
|
||||||
|
language,
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatch(saveSettings());
|
||||||
|
};
|
|
@ -1,4 +1,46 @@
|
||||||
|
import { expandSpoilers, disableSwiping } from 'flavours/glitch/util/initial_state';
|
||||||
|
import { openModal } from './modal';
|
||||||
|
|
||||||
export const LOCAL_SETTING_CHANGE = 'LOCAL_SETTING_CHANGE';
|
export const LOCAL_SETTING_CHANGE = 'LOCAL_SETTING_CHANGE';
|
||||||
|
export const LOCAL_SETTING_DELETE = 'LOCAL_SETTING_DELETE';
|
||||||
|
|
||||||
|
export function checkDeprecatedLocalSettings() {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const local_auto_unfold = getState().getIn(['local_settings', 'content_warnings', 'auto_unfold']);
|
||||||
|
const local_swipe_to_change_columns = getState().getIn(['local_settings', 'swipe_to_change_columns']);
|
||||||
|
let changed_settings = [];
|
||||||
|
|
||||||
|
if (local_auto_unfold !== null && local_auto_unfold !== undefined) {
|
||||||
|
if (local_auto_unfold === expandSpoilers) {
|
||||||
|
dispatch(deleteLocalSetting(['content_warnings', 'auto_unfold']));
|
||||||
|
} else {
|
||||||
|
changed_settings.push('user_setting_expand_spoilers');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (local_swipe_to_change_columns !== null && local_swipe_to_change_columns !== undefined) {
|
||||||
|
if (local_swipe_to_change_columns === !disableSwiping) {
|
||||||
|
dispatch(deleteLocalSetting(['swipe_to_change_columns']));
|
||||||
|
} else {
|
||||||
|
changed_settings.push('user_setting_disable_swiping');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changed_settings.length > 0) {
|
||||||
|
dispatch(openModal('DEPRECATED_SETTINGS', {
|
||||||
|
settings: changed_settings,
|
||||||
|
onConfirm: () => dispatch(clearDeprecatedLocalSettings()),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function clearDeprecatedLocalSettings() {
|
||||||
|
return (dispatch) => {
|
||||||
|
dispatch(deleteLocalSetting(['content_warnings', 'auto_unfold']));
|
||||||
|
dispatch(deleteLocalSetting(['swipe_to_change_columns']));
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export function changeLocalSetting(key, value) {
|
export function changeLocalSetting(key, value) {
|
||||||
return dispatch => {
|
return dispatch => {
|
||||||
|
@ -12,6 +54,17 @@ export function changeLocalSetting(key, value) {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function deleteLocalSetting(key) {
|
||||||
|
return dispatch => {
|
||||||
|
dispatch({
|
||||||
|
type: LOCAL_SETTING_DELETE,
|
||||||
|
key,
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatch(saveLocalSettings());
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
// __TODO :__
|
// __TODO :__
|
||||||
// Right now `saveLocalSettings()` doesn't keep track of which user
|
// Right now `saveLocalSettings()` doesn't keep track of which user
|
||||||
// is currently signed in, but it might be better to give each user
|
// is currently signed in, but it might be better to give each user
|
||||||
|
|
|
@ -70,7 +70,8 @@ export const loadPending = () => ({
|
||||||
|
|
||||||
export function updateNotifications(notification, intlMessages, intlLocale) {
|
export function updateNotifications(notification, intlMessages, intlLocale) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
const showInColumn = getState().getIn(['settings', 'notifications', 'shows', notification.type], true);
|
const activeFilter = getState().getIn(['settings', 'notifications', 'quickFilter', 'active']);
|
||||||
|
const showInColumn = activeFilter === 'all' ? getState().getIn(['settings', 'notifications', 'shows', notification.type], true) : activeFilter === notification.type;
|
||||||
const showAlert = getState().getIn(['settings', 'notifications', 'alerts', notification.type], true);
|
const showAlert = getState().getIn(['settings', 'notifications', 'alerts', notification.type], true);
|
||||||
const playSound = getState().getIn(['settings', 'notifications', 'sounds', notification.type], true);
|
const playSound = getState().getIn(['settings', 'notifications', 'sounds', notification.type], true);
|
||||||
const filters = getFiltersRegex(getState(), { contextType: 'notifications' });
|
const filters = getFiltersRegex(getState(), { contextType: 'notifications' });
|
||||||
|
|
|
@ -16,8 +16,10 @@ const messages = defineMessages({
|
||||||
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' },
|
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' },
|
||||||
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
|
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
|
||||||
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
|
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
|
||||||
mute_notifications: { id: 'account.mute_notifications', defaultMessage: 'You are not currently muting notifications from @{name}. Click to mute notifications' },
|
mute_notifications: { id: 'account.mute_notifications', defaultMessage: 'Mute notifications from @{name}' },
|
||||||
unmute_notifications: { id: 'account.unmute_notifications', defaultMessage: 'You are currently muting notifications from @{name}. Click to unmute notifications' },
|
unmute_notifications: { id: 'account.unmute_notifications', defaultMessage: 'Unmute notifications from @{name}' },
|
||||||
|
mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
|
||||||
|
block: { id: 'account.block', defaultMessage: 'Block @{name}' },
|
||||||
});
|
});
|
||||||
|
|
||||||
export default @injectIntl
|
export default @injectIntl
|
||||||
|
@ -34,6 +36,7 @@ class Account extends ImmutablePureComponent {
|
||||||
small: PropTypes.bool,
|
small: PropTypes.bool,
|
||||||
actionIcon: PropTypes.string,
|
actionIcon: PropTypes.string,
|
||||||
actionTitle: PropTypes.string,
|
actionTitle: PropTypes.string,
|
||||||
|
defaultAction: PropTypes.string,
|
||||||
onActionClick: PropTypes.func,
|
onActionClick: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -70,6 +73,7 @@ class Account extends ImmutablePureComponent {
|
||||||
onActionClick,
|
onActionClick,
|
||||||
actionIcon,
|
actionIcon,
|
||||||
actionTitle,
|
actionTitle,
|
||||||
|
defaultAction,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
if (!account) {
|
if (!account) {
|
||||||
|
@ -114,6 +118,10 @@ class Account extends ImmutablePureComponent {
|
||||||
{hidingNotificationsButton}
|
{hidingNotificationsButton}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
|
} else if (defaultAction === 'mute') {
|
||||||
|
buttons = <IconButton icon='volume-off' title={intl.formatMessage(messages.mute, { name: account.get('username') })} onClick={this.handleMute} />;
|
||||||
|
} else if (defaultAction === 'block') {
|
||||||
|
buttons = <IconButton icon='lock' title={intl.formatMessage(messages.block, { name: account.get('username') })} onClick={this.handleBlock} />;
|
||||||
} else if (!account.get('moved') || following) {
|
} else if (!account.get('moved') || following) {
|
||||||
buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following} />;
|
buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following} />;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import classNames from 'classnames';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import { autoPlayGif } from 'flavours/glitch/util/initial_state';
|
import { autoPlayGif } from 'flavours/glitch/util/initial_state';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
export default class Avatar extends React.PureComponent {
|
export default class Avatar extends React.PureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
account: ImmutablePropTypes.map.isRequired,
|
account: ImmutablePropTypes.map,
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
size: PropTypes.number.isRequired,
|
size: PropTypes.number.isRequired,
|
||||||
style: PropTypes.object,
|
style: PropTypes.object,
|
||||||
|
@ -45,11 +45,6 @@ export default class Avatar extends React.PureComponent {
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const { hovering } = this.state;
|
const { hovering } = this.state;
|
||||||
|
|
||||||
const src = account.get('avatar');
|
|
||||||
const staticSrc = account.get('avatar_static');
|
|
||||||
|
|
||||||
const computedClass = classNames('account__avatar', { 'account__avatar-inline': inline }, className);
|
|
||||||
|
|
||||||
const style = {
|
const style = {
|
||||||
...this.props.style,
|
...this.props.style,
|
||||||
width: `${size}px`,
|
width: `${size}px`,
|
||||||
|
@ -57,19 +52,24 @@ export default class Avatar extends React.PureComponent {
|
||||||
backgroundSize: `${size}px ${size}px`,
|
backgroundSize: `${size}px ${size}px`,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (hovering || animate) {
|
if (account) {
|
||||||
style.backgroundImage = `url(${src})`;
|
const src = account.get('avatar');
|
||||||
} else {
|
const staticSrc = account.get('avatar_static');
|
||||||
style.backgroundImage = `url(${staticSrc})`;
|
|
||||||
|
if (hovering || animate) {
|
||||||
|
style.backgroundImage = `url(${src})`;
|
||||||
|
} else {
|
||||||
|
style.backgroundImage = `url(${staticSrc})`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={computedClass}
|
className={classNames('account__avatar', { 'account__avatar-inline': inline }, className)}
|
||||||
onMouseEnter={this.handleMouseEnter}
|
onMouseEnter={this.handleMouseEnter}
|
||||||
onMouseLeave={this.handleMouseLeave}
|
onMouseLeave={this.handleMouseLeave}
|
||||||
style={style}
|
style={style}
|
||||||
data-avatar-of={`@${account.get('acct')}`}
|
data-avatar-of={account && `@${account.get('acct')}`}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,7 @@ export function counterRenderer(counterType, isBold = true) {
|
||||||
return (displayNumber, pluralReady) => (
|
return (displayNumber, pluralReady) => (
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='account.statuses_counter'
|
id='account.statuses_counter'
|
||||||
defaultMessage='{count, plural, one {{counter} Toot} other {{counter} Toots}}'
|
defaultMessage='{count, plural, one {{counter} Post} other {{counter} Posts}}'
|
||||||
values={{
|
values={{
|
||||||
count: pluralReady,
|
count: pluralReady,
|
||||||
counter: renderCounter(displayNumber),
|
counter: renderCounter(displayNumber),
|
||||||
|
|
|
@ -7,7 +7,7 @@ import Motion from 'flavours/glitch/util/optional_motion';
|
||||||
import spring from 'react-motion/lib/spring';
|
import spring from 'react-motion/lib/spring';
|
||||||
import { supportsPassiveEvents } from 'detect-passive-events';
|
import { supportsPassiveEvents } from 'detect-passive-events';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { CircularProgress } from 'mastodon/components/loading_indicator';
|
import { CircularProgress } from 'flavours/glitch/components/loading_indicator';
|
||||||
|
|
||||||
const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
|
const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
|
||||||
let id = 0;
|
let id = 0;
|
||||||
|
|
|
@ -55,6 +55,10 @@ export default class ModalRoot extends React.PureComponent {
|
||||||
window.addEventListener('keyup', this.handleKeyUp, false);
|
window.addEventListener('keyup', this.handleKeyUp, false);
|
||||||
window.addEventListener('keydown', this.handleKeyDown, false);
|
window.addEventListener('keydown', this.handleKeyDown, false);
|
||||||
this.history = this.context.router ? this.context.router.history : createBrowserHistory();
|
this.history = this.context.router ? this.context.router.history : createBrowserHistory();
|
||||||
|
|
||||||
|
if (this.props.children) {
|
||||||
|
this._handleModalOpen();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps (nextProps) {
|
componentWillReceiveProps (nextProps) {
|
||||||
|
|
|
@ -100,6 +100,7 @@ class Status extends ImmutablePureComponent {
|
||||||
scrollKey: PropTypes.string,
|
scrollKey: PropTypes.string,
|
||||||
deployPictureInPicture: PropTypes.func,
|
deployPictureInPicture: PropTypes.func,
|
||||||
usingPiP: PropTypes.bool,
|
usingPiP: PropTypes.bool,
|
||||||
|
settings: ImmutablePropTypes.map.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
|
@ -581,10 +582,7 @@ class Status extends ImmutablePureComponent {
|
||||||
// backgrounds for collapsed statuses are enabled.
|
// backgrounds for collapsed statuses are enabled.
|
||||||
|
|
||||||
attachments = status.get('media_attachments');
|
attachments = status.get('media_attachments');
|
||||||
if (status.get('poll')) {
|
|
||||||
media.push(<PollContainer pollId={status.get('poll')} />);
|
|
||||||
mediaIcons.push('tasks');
|
|
||||||
}
|
|
||||||
if (usingPiP) {
|
if (usingPiP) {
|
||||||
media.push(<PictureInPicturePlaceholder width={this.props.cachedMediaWidth} />);
|
media.push(<PictureInPicturePlaceholder width={this.props.cachedMediaWidth} />);
|
||||||
mediaIcons.push('video-camera');
|
mediaIcons.push('video-camera');
|
||||||
|
@ -684,6 +682,11 @@ class Status extends ImmutablePureComponent {
|
||||||
mediaIcons.push('link');
|
mediaIcons.push('link');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (status.get('poll')) {
|
||||||
|
media.push(<PollContainer pollId={status.get('poll')} />);
|
||||||
|
mediaIcons.push('tasks');
|
||||||
|
}
|
||||||
|
|
||||||
// Here we prepare extra data-* attributes for CSS selectors.
|
// Here we prepare extra data-* attributes for CSS selectors.
|
||||||
// Users can use those for theming, hiding avatars etc via UserStyle
|
// Users can use those for theming, hiding avatars etc via UserStyle
|
||||||
const selectorAttribs = {
|
const selectorAttribs = {
|
||||||
|
@ -753,6 +756,7 @@ class Status extends ImmutablePureComponent {
|
||||||
collapsed={isCollapsed}
|
collapsed={isCollapsed}
|
||||||
setCollapsed={setCollapsed}
|
setCollapsed={setCollapsed}
|
||||||
directMessage={!!otherAccounts}
|
directMessage={!!otherAccounts}
|
||||||
|
settings={settings.get('status_icons')}
|
||||||
/>
|
/>
|
||||||
</header>
|
</header>
|
||||||
<StatusContent
|
<StatusContent
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { defineMessages, injectIntl } from 'react-intl';
|
||||||
import IconButton from './icon_button';
|
import IconButton from './icon_button';
|
||||||
import VisibilityIcon from './status_visibility_icon';
|
import VisibilityIcon from './status_visibility_icon';
|
||||||
import Icon from 'flavours/glitch/components/icon';
|
import Icon from 'flavours/glitch/components/icon';
|
||||||
|
import { languages } from 'flavours/glitch/util/initial_state';
|
||||||
|
|
||||||
// Messages for use with internationalization stuff.
|
// Messages for use with internationalization stuff.
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
|
@ -22,6 +23,23 @@ const messages = defineMessages({
|
||||||
localOnly: { id: 'status.local_only', defaultMessage: 'Only visible from your instance' },
|
localOnly: { id: 'status.local_only', defaultMessage: 'Only visible from your instance' },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const LanguageIcon = ({ language }) => {
|
||||||
|
if (!languages) return null;
|
||||||
|
|
||||||
|
const lang = languages.find((lang) => lang[0] === language);
|
||||||
|
if (!lang) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className='text-icon' title={`${lang[2]} (${lang[1]})`} aria-hidden='true'>
|
||||||
|
{lang[0].toUpperCase()}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
LanguageIcon.propTypes = {
|
||||||
|
language: PropTypes.string.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
export default @injectIntl
|
export default @injectIntl
|
||||||
class StatusIcons extends React.PureComponent {
|
class StatusIcons extends React.PureComponent {
|
||||||
|
|
||||||
|
@ -33,6 +51,7 @@ class StatusIcons extends React.PureComponent {
|
||||||
directMessage: PropTypes.bool,
|
directMessage: PropTypes.bool,
|
||||||
setCollapsed: PropTypes.func.isRequired,
|
setCollapsed: PropTypes.func.isRequired,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
|
settings: ImmutablePropTypes.map.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handles clicks on collapsed button
|
// Handles clicks on collapsed button
|
||||||
|
@ -82,12 +101,14 @@ class StatusIcons extends React.PureComponent {
|
||||||
collapsible,
|
collapsible,
|
||||||
collapsed,
|
collapsed,
|
||||||
directMessage,
|
directMessage,
|
||||||
|
settings,
|
||||||
intl,
|
intl,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='status__info__icons'>
|
<div className='status__info__icons'>
|
||||||
{status.get('in_reply_to_id', null) !== null ? (
|
{settings.get('language') && status.get('language') && <LanguageIcon language={status.get('language')} />}
|
||||||
|
{settings.get('reply') && status.get('in_reply_to_id', null) !== null ? (
|
||||||
<Icon
|
<Icon
|
||||||
className='status__reply-icon'
|
className='status__reply-icon'
|
||||||
fixedWidth
|
fixedWidth
|
||||||
|
@ -96,16 +117,16 @@ class StatusIcons extends React.PureComponent {
|
||||||
title={intl.formatMessage(messages.inReplyTo)}
|
title={intl.formatMessage(messages.inReplyTo)}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{status.get('local_only') &&
|
{settings.get('local_only') && status.get('local_only') &&
|
||||||
<Icon
|
<Icon
|
||||||
fixedWidth
|
fixedWidth
|
||||||
id='home'
|
id='home'
|
||||||
aria-hidden='true'
|
aria-hidden='true'
|
||||||
title={intl.formatMessage(messages.localOnly)}
|
title={intl.formatMessage(messages.localOnly)}
|
||||||
/>}
|
/>}
|
||||||
{ !!mediaIcons && mediaIcons.map(icon => this.renderIcon(icon)) }
|
{settings.get('media') && !!mediaIcons && mediaIcons.map(icon => this.renderIcon(icon))}
|
||||||
{!directMessage && <VisibilityIcon visibility={status.get('visibility')} />}
|
{settings.get('visibility') && !directMessage && <VisibilityIcon visibility={status.get('visibility')} />}
|
||||||
{collapsible ? (
|
{collapsible && (
|
||||||
<IconButton
|
<IconButton
|
||||||
className='status__collapse-button'
|
className='status__collapse-button'
|
||||||
animate
|
animate
|
||||||
|
@ -118,7 +139,7 @@ class StatusIcons extends React.PureComponent {
|
||||||
icon='angle-double-up'
|
icon='angle-double-up'
|
||||||
onClick={this.handleCollapsedClick}
|
onClick={this.handleCollapsedClick}
|
||||||
/>
|
/>
|
||||||
) : null}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,7 +38,7 @@ export default class StatusPrepend extends React.PureComponent {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'featured':
|
case 'featured':
|
||||||
return (
|
return (
|
||||||
<FormattedMessage id='status.pinned' defaultMessage='Pinned toot' />
|
<FormattedMessage id='status.pinned' defaultMessage='Pinned post' />
|
||||||
);
|
);
|
||||||
case 'reblogged_by':
|
case 'reblogged_by':
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -9,7 +9,7 @@ const messages = defineMessages({
|
||||||
public: { id: 'privacy.public.short', defaultMessage: 'Public' },
|
public: { id: 'privacy.public.short', defaultMessage: 'Public' },
|
||||||
unlisted: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' },
|
unlisted: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' },
|
||||||
private: { id: 'privacy.private.short', defaultMessage: 'Followers-only' },
|
private: { id: 'privacy.private.short', defaultMessage: 'Followers-only' },
|
||||||
direct: { id: 'privacy.direct.short', defaultMessage: 'Direct' },
|
direct: { id: 'privacy.direct.short', defaultMessage: 'Mentioned people only' },
|
||||||
});
|
});
|
||||||
|
|
||||||
export default @injectIntl
|
export default @injectIntl
|
||||||
|
|
|
@ -8,6 +8,7 @@ import UI from 'flavours/glitch/features/ui';
|
||||||
import { fetchCustomEmojis } from 'flavours/glitch/actions/custom_emojis';
|
import { fetchCustomEmojis } from 'flavours/glitch/actions/custom_emojis';
|
||||||
import { hydrateStore } from 'flavours/glitch/actions/store';
|
import { hydrateStore } from 'flavours/glitch/actions/store';
|
||||||
import { connectUserStream } from 'flavours/glitch/actions/streaming';
|
import { connectUserStream } from 'flavours/glitch/actions/streaming';
|
||||||
|
import { checkDeprecatedLocalSettings } from 'flavours/glitch/actions/local_settings';
|
||||||
import { IntlProvider, addLocaleData } from 'react-intl';
|
import { IntlProvider, addLocaleData } from 'react-intl';
|
||||||
import { getLocale } from 'locales';
|
import { getLocale } from 'locales';
|
||||||
import initialState from 'flavours/glitch/util/initial_state';
|
import initialState from 'flavours/glitch/util/initial_state';
|
||||||
|
@ -20,6 +21,9 @@ export const store = configureStore();
|
||||||
const hydrateAction = hydrateStore(initialState);
|
const hydrateAction = hydrateStore(initialState);
|
||||||
store.dispatch(hydrateAction);
|
store.dispatch(hydrateAction);
|
||||||
|
|
||||||
|
// check for deprecated local settings
|
||||||
|
store.dispatch(checkDeprecatedLocalSettings());
|
||||||
|
|
||||||
// load custom emojis
|
// load custom emojis
|
||||||
store.dispatch(fetchCustomEmojis());
|
store.dispatch(fetchCustomEmojis());
|
||||||
|
|
||||||
|
|
|
@ -37,7 +37,7 @@ const messages = defineMessages({
|
||||||
showReblogs: { id: 'account.show_reblogs', defaultMessage: 'Show boosts from @{name}' },
|
showReblogs: { id: 'account.show_reblogs', defaultMessage: 'Show boosts from @{name}' },
|
||||||
enableNotifications: { id: 'account.enable_notifications', defaultMessage: 'Notify me when @{name} posts' },
|
enableNotifications: { id: 'account.enable_notifications', defaultMessage: 'Notify me when @{name} posts' },
|
||||||
disableNotifications: { id: 'account.disable_notifications', defaultMessage: 'Stop notifying me when @{name} posts' },
|
disableNotifications: { id: 'account.disable_notifications', defaultMessage: 'Stop notifying me when @{name} posts' },
|
||||||
pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned toots' },
|
pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned posts' },
|
||||||
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
|
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
|
||||||
follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
|
follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
|
||||||
favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' },
|
favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' },
|
||||||
|
@ -82,6 +82,7 @@ class Header extends ImmutablePureComponent {
|
||||||
onEditAccountNote: PropTypes.func.isRequired,
|
onEditAccountNote: PropTypes.func.isRequired,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
domain: PropTypes.string.isRequired,
|
domain: PropTypes.string.isRequired,
|
||||||
|
hidden: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
openEditProfile = () => {
|
openEditProfile = () => {
|
||||||
|
@ -115,7 +116,7 @@ class Header extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { account, intl, domain, identity_proofs } = this.props;
|
const { account, hidden, intl, domain } = this.props;
|
||||||
|
|
||||||
if (!account) {
|
if (!account) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -270,23 +271,29 @@ class Header extends ImmutablePureComponent {
|
||||||
{info}
|
{info}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<img src={autoPlayGif ? account.get('header') : account.get('header_static')} alt='' className='parallax' />
|
{!(suspended || hidden) && <img src={autoPlayGif ? account.get('header') : account.get('header_static')} alt='' className='parallax' />}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='account__header__bar'>
|
<div className='account__header__bar'>
|
||||||
<div className='account__header__tabs'>
|
<div className='account__header__tabs'>
|
||||||
<a className='avatar' href={account.get('url')} rel='noopener noreferrer' target='_blank'>
|
<a className='avatar' href={account.get('url')} rel='noopener noreferrer' target='_blank'>
|
||||||
<Avatar account={account} size={90} />
|
<Avatar account={suspended || hidden ? undefined : account} size={90} />
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div className='spacer' />
|
<div className='spacer' />
|
||||||
|
|
||||||
<div className='account__header__tabs__buttons'>
|
{!suspended && (
|
||||||
{actionBtn}
|
<div className='account__header__tabs__buttons'>
|
||||||
{bellBtn}
|
{!hidden && (
|
||||||
|
<React.Fragment>
|
||||||
|
{actionBtn}
|
||||||
|
{bellBtn}
|
||||||
|
</React.Fragment>
|
||||||
|
)}
|
||||||
|
|
||||||
<DropdownMenuContainer items={menu} icon='ellipsis-v' size={24} direction='right' />
|
<DropdownMenuContainer items={menu} icon='ellipsis-v' size={24} direction='right' />
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='account__header__tabs__name'>
|
<div className='account__header__tabs__name'>
|
||||||
|
@ -298,23 +305,11 @@ class Header extends ImmutablePureComponent {
|
||||||
|
|
||||||
<AccountNoteContainer account={account} />
|
<AccountNoteContainer account={account} />
|
||||||
|
|
||||||
{!suspended && (
|
{!(suspended || hidden) && (
|
||||||
<div className='account__header__extra'>
|
<div className='account__header__extra'>
|
||||||
<div className='account__header__bio'>
|
<div className='account__header__bio'>
|
||||||
{ (fields.size > 0 || identity_proofs.size > 0) && (
|
{ fields.size > 0 && (
|
||||||
<div className='account__header__fields'>
|
<div className='account__header__fields'>
|
||||||
{identity_proofs.map((proof, i) => (
|
|
||||||
<dl key={i}>
|
|
||||||
<dt dangerouslySetInnerHTML={{ __html: proof.get('provider') }} className='translate' />
|
|
||||||
|
|
||||||
<dd className='verified'>
|
|
||||||
<a href={proof.get('proof_url')} target='_blank' rel='noopener noreferrer'><span title={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(proof.get('updated_at'), dateFormatOptions) })}>
|
|
||||||
<Icon id='check' className='verified__mark' />
|
|
||||||
</span></a>
|
|
||||||
<a href={proof.get('profile_url')} target='_blank' rel='noopener noreferrer'><span dangerouslySetInnerHTML={{ __html: ' '+proof.get('provider_username') }} className='translate' /></a>
|
|
||||||
</dd>
|
|
||||||
</dl>
|
|
||||||
))}
|
|
||||||
{fields.map((pair, i) => (
|
{fields.map((pair, i) => (
|
||||||
<dl key={i}>
|
<dl key={i}>
|
||||||
<dt dangerouslySetInnerHTML={{ __html: pair.get('name_emojified') }} title={pair.get('name')} />
|
<dt dangerouslySetInnerHTML={{ __html: pair.get('name_emojified') }} title={pair.get('name')} />
|
||||||
|
|
|
@ -12,7 +12,6 @@ export default class Header extends ImmutablePureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
account: ImmutablePropTypes.map,
|
account: ImmutablePropTypes.map,
|
||||||
identity_proofs: ImmutablePropTypes.list,
|
|
||||||
onFollow: PropTypes.func.isRequired,
|
onFollow: PropTypes.func.isRequired,
|
||||||
onBlock: PropTypes.func.isRequired,
|
onBlock: PropTypes.func.isRequired,
|
||||||
onMention: PropTypes.func.isRequired,
|
onMention: PropTypes.func.isRequired,
|
||||||
|
@ -26,6 +25,7 @@ export default class Header extends ImmutablePureComponent {
|
||||||
onAddToList: PropTypes.func.isRequired,
|
onAddToList: PropTypes.func.isRequired,
|
||||||
hideTabs: PropTypes.bool,
|
hideTabs: PropTypes.bool,
|
||||||
domain: PropTypes.string.isRequired,
|
domain: PropTypes.string.isRequired,
|
||||||
|
hidden: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
static contextTypes = {
|
static contextTypes = {
|
||||||
|
@ -93,7 +93,7 @@ export default class Header extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { account, hideTabs, identity_proofs } = this.props;
|
const { account, hidden, hideTabs } = this.props;
|
||||||
|
|
||||||
if (account === null) {
|
if (account === null) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -101,11 +101,10 @@ export default class Header extends ImmutablePureComponent {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='account-timeline__header'>
|
<div className='account-timeline__header'>
|
||||||
{account.get('moved') && <MovedNote from={account} to={account.get('moved')} />}
|
{(!hidden && account.get('moved')) && <MovedNote from={account} to={account.get('moved')} />}
|
||||||
|
|
||||||
<InnerHeader
|
<InnerHeader
|
||||||
account={account}
|
account={account}
|
||||||
identity_proofs={identity_proofs}
|
|
||||||
onFollow={this.handleFollow}
|
onFollow={this.handleFollow}
|
||||||
onBlock={this.handleBlock}
|
onBlock={this.handleBlock}
|
||||||
onMention={this.handleMention}
|
onMention={this.handleMention}
|
||||||
|
@ -120,16 +119,17 @@ export default class Header extends ImmutablePureComponent {
|
||||||
onAddToList={this.handleAddToList}
|
onAddToList={this.handleAddToList}
|
||||||
onEditAccountNote={this.handleEditAccountNote}
|
onEditAccountNote={this.handleEditAccountNote}
|
||||||
domain={this.props.domain}
|
domain={this.props.domain}
|
||||||
|
hidden={hidden}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ActionBar
|
<ActionBar
|
||||||
account={account}
|
account={account}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{!hideTabs && (
|
{!(hideTabs || hidden) && (
|
||||||
<div className='account__section-headline'>
|
<div className='account__section-headline'>
|
||||||
<NavLink exact to={`/@${account.get('acct')}`}><FormattedMessage id='account.posts' defaultMessage='Toots' /></NavLink>
|
<NavLink exact to={`/@${account.get('acct')}`}><FormattedMessage id='account.posts' defaultMessage='Posts' /></NavLink>
|
||||||
<NavLink exact to={`/@${account.get('acct')}/with_replies`}><FormattedMessage id='account.posts_with_replies' defaultMessage='Toots with replies' /></NavLink>
|
<NavLink exact to={`/@${account.get('acct')}/with_replies`}><FormattedMessage id='account.posts_with_replies' defaultMessage='Posts with replies' /></NavLink>
|
||||||
<NavLink exact to={`/@${account.get('acct')}/media`}><FormattedMessage id='account.media' defaultMessage='Media' /></NavLink>
|
<NavLink exact to={`/@${account.get('acct')}/media`}><FormattedMessage id='account.media' defaultMessage='Media' /></NavLink>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { revealAccount } from 'flavours/glitch/actions/accounts';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
import Button from 'flavours/glitch/components/button';
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch, { accountId }) => ({
|
||||||
|
|
||||||
|
reveal () {
|
||||||
|
dispatch(revealAccount(accountId));
|
||||||
|
},
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
export default @connect(() => {}, mapDispatchToProps)
|
||||||
|
class LimitedAccountHint extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
accountId: PropTypes.string.isRequired,
|
||||||
|
reveal: PropTypes.func,
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { reveal } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='limited-account-hint'>
|
||||||
|
<p><FormattedMessage id='limited_account_hint.title' defaultMessage='This profile has been hidden by the moderators of your server.' /></p>
|
||||||
|
<Button onClick={reveal}><FormattedMessage id='limited_account_hint.action' defaultMessage='Show profile anyway' /></Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { makeGetAccount } from 'flavours/glitch/selectors';
|
import { makeGetAccount, getAccountHidden } from 'flavours/glitch/selectors';
|
||||||
import Header from '../components/header';
|
import Header from '../components/header';
|
||||||
import {
|
import {
|
||||||
followAccount,
|
followAccount,
|
||||||
|
@ -22,7 +22,6 @@ import { blockDomain, unblockDomain } from 'flavours/glitch/actions/domain_block
|
||||||
import { initEditAccountNote } from 'flavours/glitch/actions/account_notes';
|
import { initEditAccountNote } from 'flavours/glitch/actions/account_notes';
|
||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
import { unfollowModal } from 'flavours/glitch/util/initial_state';
|
import { unfollowModal } from 'flavours/glitch/util/initial_state';
|
||||||
import { List as ImmutableList } from 'immutable';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
|
unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
|
||||||
|
@ -35,7 +34,7 @@ const makeMapStateToProps = () => {
|
||||||
const mapStateToProps = (state, { accountId }) => ({
|
const mapStateToProps = (state, { accountId }) => ({
|
||||||
account: getAccount(state, accountId),
|
account: getAccount(state, accountId),
|
||||||
domain: state.getIn(['meta', 'domain']),
|
domain: state.getIn(['meta', 'domain']),
|
||||||
identity_proofs: state.getIn(['identity_proofs', accountId], ImmutableList()),
|
hidden: getAccountHidden(state, accountId),
|
||||||
});
|
});
|
||||||
|
|
||||||
return mapStateToProps;
|
return mapStateToProps;
|
||||||
|
|
|
@ -13,9 +13,10 @@ import ColumnBackButton from 'flavours/glitch/components/column_back_button';
|
||||||
import { List as ImmutableList } from 'immutable';
|
import { List as ImmutableList } from 'immutable';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
import { fetchAccountIdentityProofs } from '../../actions/identity_proofs';
|
|
||||||
import MissingIndicator from 'flavours/glitch/components/missing_indicator';
|
import MissingIndicator from 'flavours/glitch/components/missing_indicator';
|
||||||
import TimelineHint from 'flavours/glitch/components/timeline_hint';
|
import TimelineHint from 'flavours/glitch/components/timeline_hint';
|
||||||
|
import LimitedAccountHint from './components/limited_account_hint';
|
||||||
|
import { getAccountHidden } from 'flavours/glitch/selectors';
|
||||||
|
|
||||||
const emptyList = ImmutableList();
|
const emptyList = ImmutableList();
|
||||||
|
|
||||||
|
@ -40,11 +41,12 @@ const mapStateToProps = (state, { params: { acct, id }, withReplies = false }) =
|
||||||
isLoading: state.getIn(['timelines', `account:${path}`, 'isLoading']),
|
isLoading: state.getIn(['timelines', `account:${path}`, 'isLoading']),
|
||||||
hasMore: state.getIn(['timelines', `account:${path}`, 'hasMore']),
|
hasMore: state.getIn(['timelines', `account:${path}`, 'hasMore']),
|
||||||
suspended: state.getIn(['accounts', accountId, 'suspended'], false),
|
suspended: state.getIn(['accounts', accountId, 'suspended'], false),
|
||||||
|
hidden: getAccountHidden(state, accountId),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const RemoteHint = ({ url }) => (
|
const RemoteHint = ({ url }) => (
|
||||||
<TimelineHint url={url} resource={<FormattedMessage id='timeline_hint.resources.statuses' defaultMessage='Older toots' />} />
|
<TimelineHint url={url} resource={<FormattedMessage id='timeline_hint.resources.statuses' defaultMessage='Older posts' />} />
|
||||||
);
|
);
|
||||||
|
|
||||||
RemoteHint.propTypes = {
|
RemoteHint.propTypes = {
|
||||||
|
@ -68,6 +70,7 @@ class AccountTimeline extends ImmutablePureComponent {
|
||||||
withReplies: PropTypes.bool,
|
withReplies: PropTypes.bool,
|
||||||
isAccount: PropTypes.bool,
|
isAccount: PropTypes.bool,
|
||||||
suspended: PropTypes.bool,
|
suspended: PropTypes.bool,
|
||||||
|
hidden: PropTypes.bool,
|
||||||
remote: PropTypes.bool,
|
remote: PropTypes.bool,
|
||||||
remoteUrl: PropTypes.string,
|
remoteUrl: PropTypes.string,
|
||||||
multiColumn: PropTypes.bool,
|
multiColumn: PropTypes.bool,
|
||||||
|
@ -77,7 +80,7 @@ class AccountTimeline extends ImmutablePureComponent {
|
||||||
const { accountId, withReplies, dispatch } = this.props;
|
const { accountId, withReplies, dispatch } = this.props;
|
||||||
|
|
||||||
dispatch(fetchAccount(accountId));
|
dispatch(fetchAccount(accountId));
|
||||||
dispatch(fetchAccountIdentityProofs(accountId));
|
|
||||||
if (!withReplies) {
|
if (!withReplies) {
|
||||||
dispatch(expandAccountFeaturedTimeline(accountId));
|
dispatch(expandAccountFeaturedTimeline(accountId));
|
||||||
}
|
}
|
||||||
|
@ -109,10 +112,11 @@ class AccountTimeline extends ImmutablePureComponent {
|
||||||
|
|
||||||
if ((nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) || nextProps.withReplies !== this.props.withReplies) {
|
if ((nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) || nextProps.withReplies !== this.props.withReplies) {
|
||||||
dispatch(fetchAccount(nextProps.params.accountId));
|
dispatch(fetchAccount(nextProps.params.accountId));
|
||||||
dispatch(fetchAccountIdentityProofs(nextProps.params.accountId));
|
|
||||||
if (!nextProps.withReplies) {
|
if (!nextProps.withReplies) {
|
||||||
dispatch(expandAccountFeaturedTimeline(nextProps.params.accountId));
|
dispatch(expandAccountFeaturedTimeline(nextProps.params.accountId));
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(expandAccountTimeline(nextProps.params.accountId, { withReplies: nextProps.params.withReplies }));
|
dispatch(expandAccountTimeline(nextProps.params.accountId, { withReplies: nextProps.params.withReplies }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -130,7 +134,7 @@ class AccountTimeline extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { statusIds, featuredStatusIds, isLoading, hasMore, suspended, isAccount, multiColumn, remote, remoteUrl } = this.props;
|
const { accountId, statusIds, featuredStatusIds, isLoading, hasMore, suspended, isAccount, hidden, multiColumn, remote, remoteUrl } = this.props;
|
||||||
|
|
||||||
if (!isAccount) {
|
if (!isAccount) {
|
||||||
return (
|
return (
|
||||||
|
@ -151,12 +155,16 @@ class AccountTimeline extends ImmutablePureComponent {
|
||||||
|
|
||||||
let emptyMessage;
|
let emptyMessage;
|
||||||
|
|
||||||
|
const forceEmptyState = suspended || hidden;
|
||||||
|
|
||||||
if (suspended) {
|
if (suspended) {
|
||||||
emptyMessage = <FormattedMessage id='empty_column.account_suspended' defaultMessage='Account suspended' />;
|
emptyMessage = <FormattedMessage id='empty_column.account_suspended' defaultMessage='Account suspended' />;
|
||||||
|
} else if (hidden) {
|
||||||
|
emptyMessage = <LimitedAccountHint accountId={accountId} />;
|
||||||
} else if (remote && statusIds.isEmpty()) {
|
} else if (remote && statusIds.isEmpty()) {
|
||||||
emptyMessage = <RemoteHint url={remoteUrl} />;
|
emptyMessage = <RemoteHint url={remoteUrl} />;
|
||||||
} else {
|
} else {
|
||||||
emptyMessage = <FormattedMessage id='empty_column.account_timeline' defaultMessage='No toots here!' />;
|
emptyMessage = <FormattedMessage id='empty_column.account_timeline' defaultMessage='No posts found' />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const remoteMessage = remote ? <RemoteHint url={remoteUrl} /> : null;
|
const remoteMessage = remote ? <RemoteHint url={remoteUrl} /> : null;
|
||||||
|
@ -166,14 +174,14 @@ class AccountTimeline extends ImmutablePureComponent {
|
||||||
<ProfileColumnHeader onClick={this.handleHeaderClick} multiColumn={multiColumn} />
|
<ProfileColumnHeader onClick={this.handleHeaderClick} multiColumn={multiColumn} />
|
||||||
|
|
||||||
<StatusList
|
<StatusList
|
||||||
prepend={<HeaderContainer accountId={this.props.accountId} />}
|
prepend={<HeaderContainer accountId={this.props.accountId} hideTabs={forceEmptyState} />}
|
||||||
alwaysPrepend
|
alwaysPrepend
|
||||||
append={remoteMessage}
|
append={remoteMessage}
|
||||||
scrollKey='account_timeline'
|
scrollKey='account_timeline'
|
||||||
statusIds={suspended ? emptyList : statusIds}
|
statusIds={forceEmptyState ? emptyList : statusIds}
|
||||||
featuredStatusIds={featuredStatusIds}
|
featuredStatusIds={featuredStatusIds}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
hasMore={hasMore}
|
hasMore={!forceEmptyState && hasMore}
|
||||||
onLoadMore={this.handleLoadMore}
|
onLoadMore={this.handleLoadMore}
|
||||||
emptyMessage={emptyMessage}
|
emptyMessage={emptyMessage}
|
||||||
bindToDocument={!multiColumn}
|
bindToDocument={!multiColumn}
|
||||||
|
|
|
@ -69,7 +69,7 @@ class Blocks extends ImmutablePureComponent {
|
||||||
bindToDocument={!multiColumn}
|
bindToDocument={!multiColumn}
|
||||||
>
|
>
|
||||||
{accountIds.map(id =>
|
{accountIds.map(id =>
|
||||||
<AccountContainer key={id} id={id} />,
|
<AccountContainer key={id} id={id} defaultAction='block' />,
|
||||||
)}
|
)}
|
||||||
</ScrollableList>
|
</ScrollableList>
|
||||||
</Column>
|
</Column>
|
||||||
|
|
|
@ -70,7 +70,7 @@ class Bookmarks extends ImmutablePureComponent {
|
||||||
const { intl, statusIds, columnId, multiColumn, hasMore, isLoading } = this.props;
|
const { intl, statusIds, columnId, multiColumn, hasMore, isLoading } = this.props;
|
||||||
const pinned = !!columnId;
|
const pinned = !!columnId;
|
||||||
|
|
||||||
const emptyMessage = <FormattedMessage id='empty_column.bookmarked_statuses' defaultMessage="You don't have any bookmarked toots yet. When you bookmark one, it will show up here." />;
|
const emptyMessage = <FormattedMessage id='empty_column.bookmarked_statuses' defaultMessage="You don't have any bookmarked posts yet. When you bookmark one, it will show up here." />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column bindToDocument={!multiColumn} ref={this.setRef} name='bookmarks'>
|
<Column bindToDocument={!multiColumn} ref={this.setRef} name='bookmarks'>
|
||||||
|
|
|
@ -0,0 +1,332 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { injectIntl, defineMessages } from 'react-intl';
|
||||||
|
import TextIconButton from './text_icon_button';
|
||||||
|
import Overlay from 'react-overlays/lib/Overlay';
|
||||||
|
import Motion from 'flavours/glitch/util/optional_motion';
|
||||||
|
import spring from 'react-motion/lib/spring';
|
||||||
|
import { supportsPassiveEvents } from 'detect-passive-events';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { languages as preloadedLanguages } from 'flavours/glitch/util/initial_state';
|
||||||
|
import fuzzysort from 'fuzzysort';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
changeLanguage: { id: 'compose.language.change', defaultMessage: 'Change language' },
|
||||||
|
search: { id: 'compose.language.search', defaultMessage: 'Search languages...' },
|
||||||
|
clear: { id: 'emoji_button.clear', defaultMessage: 'Clear' },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Copied from emoji-mart for consistency with emoji picker and since
|
||||||
|
// they don't export the icons in the package
|
||||||
|
const icons = {
|
||||||
|
loupe: (
|
||||||
|
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' width='13' height='13'>
|
||||||
|
<path d='M12.9 14.32a8 8 0 1 1 1.41-1.41l5.35 5.33-1.42 1.42-5.33-5.34zM8 14A6 6 0 1 0 8 2a6 6 0 0 0 0 12z' />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
|
||||||
|
delete: (
|
||||||
|
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' width='13' height='13'>
|
||||||
|
<path d='M10 8.586L2.929 1.515 1.515 2.929 8.586 10l-7.071 7.071 1.414 1.414L10 11.414l7.071 7.071 1.414-1.414L11.414 10l7.071-7.071-1.414-1.414L10 8.586z' />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
|
||||||
|
|
||||||
|
class LanguageDropdownMenu extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
style: PropTypes.object,
|
||||||
|
value: PropTypes.string.isRequired,
|
||||||
|
frequentlyUsedLanguages: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||||
|
placement: PropTypes.string.isRequired,
|
||||||
|
onClose: PropTypes.func.isRequired,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
languages: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.string)),
|
||||||
|
intl: PropTypes.object,
|
||||||
|
};
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
languages: preloadedLanguages,
|
||||||
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
mounted: false,
|
||||||
|
searchValue: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
handleDocumentClick = e => {
|
||||||
|
if (this.node && !this.node.contains(e.target)) {
|
||||||
|
this.props.onClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
document.addEventListener('click', this.handleDocumentClick, false);
|
||||||
|
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
||||||
|
this.setState({ mounted: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount () {
|
||||||
|
document.removeEventListener('click', this.handleDocumentClick, false);
|
||||||
|
document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
setRef = c => {
|
||||||
|
this.node = c;
|
||||||
|
}
|
||||||
|
|
||||||
|
setListRef = c => {
|
||||||
|
this.listNode = c;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSearchChange = ({ target }) => {
|
||||||
|
this.setState({ searchValue: target.value });
|
||||||
|
}
|
||||||
|
|
||||||
|
search () {
|
||||||
|
const { languages, value, frequentlyUsedLanguages } = this.props;
|
||||||
|
const { searchValue } = this.state;
|
||||||
|
|
||||||
|
if (searchValue === '') {
|
||||||
|
return [...languages].sort((a, b) => {
|
||||||
|
// Push current selection to the top of the list
|
||||||
|
|
||||||
|
if (a[0] === value) {
|
||||||
|
return -1;
|
||||||
|
} else if (b[0] === value) {
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
// Sort according to frequently used languages
|
||||||
|
|
||||||
|
const indexOfA = frequentlyUsedLanguages.indexOf(a[0]);
|
||||||
|
const indexOfB = frequentlyUsedLanguages.indexOf(b[0]);
|
||||||
|
|
||||||
|
return ((indexOfA > -1 ? indexOfA : Infinity) - (indexOfB > -1 ? indexOfB : Infinity));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return fuzzysort.go(searchValue, languages, {
|
||||||
|
keys: ['0', '1', '2'],
|
||||||
|
limit: 5,
|
||||||
|
threshold: -10000,
|
||||||
|
}).map(result => result.obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
frequentlyUsed () {
|
||||||
|
const { languages, value } = this.props;
|
||||||
|
const current = languages.find(lang => lang[0] === value);
|
||||||
|
const results = [];
|
||||||
|
|
||||||
|
if (current) {
|
||||||
|
results.push(current);
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClick = e => {
|
||||||
|
const value = e.currentTarget.getAttribute('data-index');
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
this.props.onClose();
|
||||||
|
this.props.onChange(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleKeyDown = e => {
|
||||||
|
const { onClose } = this.props;
|
||||||
|
const index = Array.from(this.listNode.childNodes).findIndex(node => node === e.currentTarget);
|
||||||
|
|
||||||
|
let element = null;
|
||||||
|
|
||||||
|
switch(e.key) {
|
||||||
|
case 'Escape':
|
||||||
|
onClose();
|
||||||
|
break;
|
||||||
|
case 'Enter':
|
||||||
|
this.handleClick(e);
|
||||||
|
break;
|
||||||
|
case 'ArrowDown':
|
||||||
|
element = this.listNode.childNodes[index + 1] || this.listNode.firstChild;
|
||||||
|
break;
|
||||||
|
case 'ArrowUp':
|
||||||
|
element = this.listNode.childNodes[index - 1] || this.listNode.lastChild;
|
||||||
|
break;
|
||||||
|
case 'Tab':
|
||||||
|
if (e.shiftKey) {
|
||||||
|
element = this.listNode.childNodes[index - 1] || this.listNode.lastChild;
|
||||||
|
} else {
|
||||||
|
element = this.listNode.childNodes[index + 1] || this.listNode.firstChild;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'Home':
|
||||||
|
element = this.listNode.firstChild;
|
||||||
|
break;
|
||||||
|
case 'End':
|
||||||
|
element = this.listNode.lastChild;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element) {
|
||||||
|
element.focus();
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSearchKeyDown = e => {
|
||||||
|
const { onChange, onClose } = this.props;
|
||||||
|
const { searchValue } = this.state;
|
||||||
|
|
||||||
|
let element = null;
|
||||||
|
|
||||||
|
switch(e.key) {
|
||||||
|
case 'Tab':
|
||||||
|
case 'ArrowDown':
|
||||||
|
element = this.listNode.firstChild;
|
||||||
|
|
||||||
|
if (element) {
|
||||||
|
element.focus();
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
case 'Enter':
|
||||||
|
element = this.listNode.firstChild;
|
||||||
|
|
||||||
|
if (element) {
|
||||||
|
onChange(element.getAttribute('data-index'));
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'Escape':
|
||||||
|
if (searchValue !== '') {
|
||||||
|
e.preventDefault();
|
||||||
|
this.handleClear();
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClear = () => {
|
||||||
|
this.setState({ searchValue: '' });
|
||||||
|
}
|
||||||
|
|
||||||
|
renderItem = lang => {
|
||||||
|
const { value } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={lang[0]} role='option' tabIndex='0' data-index={lang[0]} className={classNames('language-dropdown__dropdown__results__item', { active: lang[0] === value })} aria-selected={lang[0] === value} onClick={this.handleClick} onKeyDown={this.handleKeyDown}>
|
||||||
|
<span className='language-dropdown__dropdown__results__item__native-name'>{lang[2]}</span> <span className='language-dropdown__dropdown__results__item__common-name'>({lang[1]})</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { style, placement, intl } = this.props;
|
||||||
|
const { mounted, searchValue } = this.state;
|
||||||
|
const isSearching = searchValue !== '';
|
||||||
|
const results = this.search();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Motion defaultStyle={{ opacity: 0, scaleX: 0.85, scaleY: 0.75 }} style={{ opacity: spring(1, { damping: 35, stiffness: 400 }), scaleX: spring(1, { damping: 35, stiffness: 400 }), scaleY: spring(1, { damping: 35, stiffness: 400 }) }}>
|
||||||
|
{({ opacity, scaleX, scaleY }) => (
|
||||||
|
// It should not be transformed when mounting because the resulting
|
||||||
|
// size will be used to determine the coordinate of the menu by
|
||||||
|
// react-overlays
|
||||||
|
<div className={`language-dropdown__dropdown ${placement}`} style={{ ...style, opacity: opacity, transform: mounted ? `scale(${scaleX}, ${scaleY})` : null }} ref={this.setRef}>
|
||||||
|
<div className='emoji-mart-search'>
|
||||||
|
<input type='search' value={searchValue} onChange={this.handleSearchChange} onKeyDown={this.handleSearchKeyDown} placeholder={intl.formatMessage(messages.search)} autoFocus />
|
||||||
|
<button className='emoji-mart-search-icon' disabled={!isSearching} aria-label={intl.formatMessage(messages.clear)} onClick={this.handleClear}>{!isSearching ? icons.loupe : icons.delete}</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='language-dropdown__dropdown__results emoji-mart-scroll' role='listbox' ref={this.setListRef}>
|
||||||
|
{results.map(this.renderItem)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Motion>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default @injectIntl
|
||||||
|
class LanguageDropdown extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
value: PropTypes.string,
|
||||||
|
frequentlyUsedLanguages: PropTypes.arrayOf(PropTypes.string),
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
|
onChange: PropTypes.func,
|
||||||
|
onClose: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
open: false,
|
||||||
|
placement: 'bottom',
|
||||||
|
};
|
||||||
|
|
||||||
|
handleToggle = ({ target }) => {
|
||||||
|
const { top } = target.getBoundingClientRect();
|
||||||
|
|
||||||
|
if (this.state.open && this.activeElement) {
|
||||||
|
this.activeElement.focus({ preventScroll: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({ placement: top * 2 < innerHeight ? 'bottom' : 'top' });
|
||||||
|
this.setState({ open: !this.state.open });
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClose = () => {
|
||||||
|
const { value, onClose } = this.props;
|
||||||
|
|
||||||
|
if (this.state.open && this.activeElement) {
|
||||||
|
this.activeElement.focus({ preventScroll: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({ open: false });
|
||||||
|
onClose(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleChange = value => {
|
||||||
|
const { onChange } = this.props;
|
||||||
|
onChange(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { value, intl, frequentlyUsedLanguages } = this.props;
|
||||||
|
const { open, placement } = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classNames('privacy-dropdown', { active: open })}>
|
||||||
|
<div className='privacy-dropdown__value'>
|
||||||
|
<TextIconButton
|
||||||
|
className='privacy-dropdown__value-icon'
|
||||||
|
label={value && value.toUpperCase()}
|
||||||
|
title={intl.formatMessage(messages.changeLanguage)}
|
||||||
|
active={open}
|
||||||
|
onClick={this.handleToggle}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Overlay show={open} placement={placement} target={this}>
|
||||||
|
<LanguageDropdownMenu
|
||||||
|
value={value}
|
||||||
|
frequentlyUsedLanguages={frequentlyUsedLanguages}
|
||||||
|
onClose={this.handleClose}
|
||||||
|
onChange={this.handleChange}
|
||||||
|
placement={placement}
|
||||||
|
intl={intl}
|
||||||
|
/>
|
||||||
|
</Overlay>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -12,6 +12,7 @@ import IconButton from 'flavours/glitch/components/icon_button';
|
||||||
import TextIconButton from './text_icon_button';
|
import TextIconButton from './text_icon_button';
|
||||||
import Dropdown from './dropdown';
|
import Dropdown from './dropdown';
|
||||||
import PrivacyDropdown from './privacy_dropdown';
|
import PrivacyDropdown from './privacy_dropdown';
|
||||||
|
import LanguageDropdown from '../containers/language_dropdown_container';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
|
||||||
// Utils.
|
// Utils.
|
||||||
|
@ -306,6 +307,7 @@ class ComposerOptions extends ImmutablePureComponent {
|
||||||
title={formatMessage(messages.spoiler)}
|
title={formatMessage(messages.spoiler)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
<LanguageDropdown />
|
||||||
<Dropdown
|
<Dropdown
|
||||||
active={advancedOptions && advancedOptions.some(value => !!value)}
|
active={advancedOptions && advancedOptions.some(value => !!value)}
|
||||||
disabled={disabled || isEditing}
|
disabled={disabled || isEditing}
|
||||||
|
|
|
@ -6,12 +6,12 @@ import Dropdown from './dropdown';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
|
public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
|
||||||
public_long: { id: 'privacy.public.long', defaultMessage: 'Visible for all, shown in public timelines' },
|
public_long: { id: 'privacy.public.long', defaultMessage: 'Visible for all' },
|
||||||
unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' },
|
unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' },
|
||||||
unlisted_long: { id: 'privacy.unlisted.long', defaultMessage: 'Visible for all, but not in public timelines' },
|
unlisted_long: { id: 'privacy.unlisted.long', defaultMessage: 'Visible for all, but opted-out of discovery features' },
|
||||||
private_short: { id: 'privacy.private.short', defaultMessage: 'Followers-only' },
|
private_short: { id: 'privacy.private.short', defaultMessage: 'Followers only' },
|
||||||
private_long: { id: 'privacy.private.long', defaultMessage: 'Visible for followers only' },
|
private_long: { id: 'privacy.private.long', defaultMessage: 'Visible for followers only' },
|
||||||
direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' },
|
direct_short: { id: 'privacy.direct.short', defaultMessage: 'Only people I mention' },
|
||||||
direct_long: { id: 'privacy.direct.long', defaultMessage: 'Visible for mentioned users only' },
|
direct_long: { id: 'privacy.direct.long', defaultMessage: 'Visible for mentioned users only' },
|
||||||
change_privacy: { id: 'privacy.change', defaultMessage: 'Adjust status privacy' },
|
change_privacy: { id: 'privacy.change', defaultMessage: 'Adjust status privacy' },
|
||||||
});
|
});
|
||||||
|
|
|
@ -48,6 +48,9 @@ class SearchResults extends ImmutablePureComponent {
|
||||||
render () {
|
render () {
|
||||||
const { intl, results, suggestions, dismissSuggestion, searchTerm } = this.props;
|
const { intl, results, suggestions, dismissSuggestion, searchTerm } = this.props;
|
||||||
|
|
||||||
|
let accounts, statuses, hashtags;
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
if (searchTerm === '' && !suggestions.isEmpty()) {
|
if (searchTerm === '' && !suggestions.isEmpty()) {
|
||||||
return (
|
return (
|
||||||
<div className='drawer--results'>
|
<div className='drawer--results'>
|
||||||
|
@ -72,18 +75,15 @@ class SearchResults extends ImmutablePureComponent {
|
||||||
} else if(results.get('statuses') && results.get('statuses').size === 0 && !searchEnabled && !(searchTerm.startsWith('@') || searchTerm.startsWith('#') || searchTerm.includes(' '))) {
|
} else if(results.get('statuses') && results.get('statuses').size === 0 && !searchEnabled && !(searchTerm.startsWith('@') || searchTerm.startsWith('#') || searchTerm.includes(' '))) {
|
||||||
statuses = (
|
statuses = (
|
||||||
<section className='search-results__section'>
|
<section className='search-results__section'>
|
||||||
<h5><Icon id='quote-right' fixedWidth /><FormattedMessage id='search_results.statuses' defaultMessage='Toots' /></h5>
|
<h5><Icon id='quote-right' fixedWidth /><FormattedMessage id='search_results.statuses' defaultMessage='Posts' /></h5>
|
||||||
|
|
||||||
<div className='search-results__info'>
|
<div className='search-results__info'>
|
||||||
<FormattedMessage id='search_results.statuses_fts_disabled' defaultMessage='Searching toots by their content is not enabled on this Mastodon server.' />
|
<FormattedMessage id='search_results.statuses_fts_disabled' defaultMessage='Searching posts by their content is not enabled on this Mastodon server.' />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let accounts, statuses, hashtags;
|
|
||||||
let count = 0;
|
|
||||||
|
|
||||||
if (results.get('accounts') && results.get('accounts').size > 0) {
|
if (results.get('accounts') && results.get('accounts').size > 0) {
|
||||||
count += results.get('accounts').size;
|
count += results.get('accounts').size;
|
||||||
accounts = (
|
accounts = (
|
||||||
|
@ -101,7 +101,7 @@ class SearchResults extends ImmutablePureComponent {
|
||||||
count += results.get('statuses').size;
|
count += results.get('statuses').size;
|
||||||
statuses = (
|
statuses = (
|
||||||
<section className='search-results__section'>
|
<section className='search-results__section'>
|
||||||
<h5><Icon id='quote-right' fixedWidth /><FormattedMessage id='search_results.statuses' defaultMessage='Toots' /></h5>
|
<h5><Icon id='quote-right' fixedWidth /><FormattedMessage id='search_results.statuses' defaultMessage='Posts' /></h5>
|
||||||
|
|
||||||
{results.get('statuses').map(statusId => <StatusContainer id={statusId} key={statusId}/>)}
|
{results.get('statuses').map(statusId => <StatusContainer id={statusId} key={statusId}/>)}
|
||||||
|
|
||||||
|
|
|
@ -17,11 +17,6 @@ export default class TextIconButton extends React.PureComponent {
|
||||||
ariaControls: PropTypes.string,
|
ariaControls: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
handleClick = (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
this.props.onClick();
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { label, title, active, ariaControls } = this.props;
|
const { label, title, active, ariaControls } = this.props;
|
||||||
|
|
||||||
|
@ -31,7 +26,7 @@ export default class TextIconButton extends React.PureComponent {
|
||||||
aria-label={title}
|
aria-label={title}
|
||||||
className={`text-icon-button ${active ? 'active' : ''}`}
|
className={`text-icon-button ${active ? 'active' : ''}`}
|
||||||
aria-expanded={active}
|
aria-expanded={active}
|
||||||
onClick={this.handleClick}
|
onClick={this.props.onClick}
|
||||||
aria-controls={ariaControls}
|
aria-controls={ariaControls}
|
||||||
style={iconStyle}
|
style={iconStyle}
|
||||||
>
|
>
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import LanguageDropdown from '../components/language_dropdown';
|
||||||
|
import { changeComposeLanguage } from 'flavours/glitch/actions/compose';
|
||||||
|
import { useLanguage } from 'flavours/glitch/actions/languages';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { Map as ImmutableMap } from 'immutable';
|
||||||
|
|
||||||
|
const getFrequentlyUsedLanguages = createSelector([
|
||||||
|
state => state.getIn(['settings', 'frequentlyUsedLanguages'], ImmutableMap()),
|
||||||
|
], languageCounters => (
|
||||||
|
languageCounters.keySeq()
|
||||||
|
.sort((a, b) => languageCounters.get(a) - languageCounters.get(b))
|
||||||
|
.reverse()
|
||||||
|
.toArray()
|
||||||
|
));
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
frequentlyUsedLanguages: getFrequentlyUsedLanguages(state),
|
||||||
|
value: state.getIn(['compose', 'language']),
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
|
||||||
|
onChange (value) {
|
||||||
|
dispatch(changeComposeLanguage(value));
|
||||||
|
},
|
||||||
|
|
||||||
|
onClose (value) {
|
||||||
|
dispatch(useLanguage(value));
|
||||||
|
},
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(LanguageDropdown);
|
|
@ -43,13 +43,13 @@ const WarningWrapper = ({ needsLockWarning, hashtagWarning, directMessageWarning
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hashtagWarning) {
|
if (hashtagWarning) {
|
||||||
return <Warning message={<FormattedMessage id='compose_form.hashtag_warning' defaultMessage="This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag." />} />;
|
return <Warning message={<FormattedMessage id='compose_form.hashtag_warning' defaultMessage="This post won't be listed under any hashtag as it is unlisted. Only public posts can be searched by hashtag." />} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (directMessageWarning) {
|
if (directMessageWarning) {
|
||||||
const message = (
|
const message = (
|
||||||
<span>
|
<span>
|
||||||
<FormattedMessage id='compose_form.direct_message_warning' defaultMessage='This toot will only be sent to all the mentioned users.' /> {!!termsLink && <a href='/terms' target='_blank'><FormattedMessage id='compose_form.direct_message_warning_learn_more' defaultMessage='Learn more' /></a>}
|
<FormattedMessage id='compose_form.encryption_warning' defaultMessage='Posts on Mastodon are not end-to-end encrypted. Do not share any dangerous information over Mastodon.' /> {!!termsLink && <a href={termsLink} target='_blank'><FormattedMessage id='compose_form.direct_message_warning_learn_more' defaultMessage='Learn more' /></a>}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@ import { cycleElefriendCompose } from 'flavours/glitch/actions/compose';
|
||||||
import HeaderContainer from './containers/header_container';
|
import HeaderContainer from './containers/header_container';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
compose: { id: 'navigation_bar.compose', defaultMessage: 'Compose new toot' },
|
compose: { id: 'navigation_bar.compose', defaultMessage: 'Compose new post' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapStateToProps = (state, ownProps) => ({
|
const mapStateToProps = (state, ownProps) => ({
|
||||||
|
|
|
@ -2,6 +2,7 @@ import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
|
import SettingToggle from 'flavours/glitch/features/notifications/components/setting_toggle';
|
||||||
import SettingText from '../../../components/setting_text';
|
import SettingText from '../../../components/setting_text';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
|
@ -23,6 +24,12 @@ class ColumnSettings extends React.PureComponent {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
<span className='column-settings__section'><FormattedMessage id='home.column_settings.basic' defaultMessage='Basic' /></span>
|
||||||
|
|
||||||
|
<div className='column-settings__row'>
|
||||||
|
<SettingToggle settings={settings} settingPath={['conversations']} onChange={onChange} label={<FormattedMessage id='direct.group_by_conversations' defaultMessage='Group by conversation' />} />
|
||||||
|
</div>
|
||||||
|
|
||||||
<span className='column-settings__section'><FormattedMessage id='home.column_settings.advanced' defaultMessage='Advanced' /></span>
|
<span className='column-settings__section'><FormattedMessage id='home.column_settings.advanced' defaultMessage='Advanced' /></span>
|
||||||
|
|
||||||
<div className='column-settings__row'>
|
<div className='column-settings__row'>
|
||||||
|
|
|
@ -10,7 +10,6 @@ import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/col
|
||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
import ColumnSettingsContainer from './containers/column_settings_container';
|
import ColumnSettingsContainer from './containers/column_settings_container';
|
||||||
import { connectDirectStream } from 'flavours/glitch/actions/streaming';
|
import { connectDirectStream } from 'flavours/glitch/actions/streaming';
|
||||||
import { changeSetting } from 'flavours/glitch/actions/settings';
|
|
||||||
import ConversationsListContainer from './containers/conversations_list_container';
|
import ConversationsListContainer from './containers/conversations_list_container';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
|
@ -99,14 +98,6 @@ class DirectTimeline extends React.PureComponent {
|
||||||
this.props.dispatch(expandConversations({ maxId }));
|
this.props.dispatch(expandConversations({ maxId }));
|
||||||
}
|
}
|
||||||
|
|
||||||
handleTimelineClick = () => {
|
|
||||||
this.props.dispatch(changeSetting(['direct', 'conversations'], false));
|
|
||||||
}
|
|
||||||
|
|
||||||
handleConversationsClick = () => {
|
|
||||||
this.props.dispatch(changeSetting(['direct', 'conversations'], true));
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { intl, hasUnread, columnId, multiColumn, conversationsMode } = this.props;
|
const { intl, hasUnread, columnId, multiColumn, conversationsMode } = this.props;
|
||||||
const pinned = !!columnId;
|
const pinned = !!columnId;
|
||||||
|
@ -119,6 +110,7 @@ class DirectTimeline extends React.PureComponent {
|
||||||
scrollKey={`direct_timeline-${columnId}`}
|
scrollKey={`direct_timeline-${columnId}`}
|
||||||
timelineId='direct'
|
timelineId='direct'
|
||||||
onLoadMore={this.handleLoadMore}
|
onLoadMore={this.handleLoadMore}
|
||||||
|
prepend={<div className='follow_requests-unlocked_explanation'><span><FormattedMessage id='compose_form.encryption_warning' defaultMessage='Posts on Mastodon are not end-to-end encrypted. Do not share any dangerous information over Mastodon.' /> <a href='/terms' target='_blank'><FormattedMessage id='compose_form.direct_message_warning_learn_more' defaultMessage='Learn more' /></a></span></div>}
|
||||||
emptyMessage={<FormattedMessage id='empty_column.direct' defaultMessage="You don't have any direct messages yet. When you send or receive one, it will show up here." />}
|
emptyMessage={<FormattedMessage id='empty_column.direct' defaultMessage="You don't have any direct messages yet. When you send or receive one, it will show up here." />}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -129,6 +121,7 @@ class DirectTimeline extends React.PureComponent {
|
||||||
scrollKey={`direct_timeline-${columnId}`}
|
scrollKey={`direct_timeline-${columnId}`}
|
||||||
timelineId='direct'
|
timelineId='direct'
|
||||||
onLoadMore={this.handleLoadMoreTimeline}
|
onLoadMore={this.handleLoadMoreTimeline}
|
||||||
|
prepend={<div className='follow_requests-unlocked_explanation'><span><FormattedMessage id='compose_form.encryption_warning' defaultMessage='Posts on Mastodon are not end-to-end encrypted. Do not share any dangerous information over Mastodon.' /> <a href='/terms' target='_blank'><FormattedMessage id='compose_form.direct_message_warning_learn_more' defaultMessage='Learn more' /></a></span></div>}
|
||||||
emptyMessage={<FormattedMessage id='empty_column.direct' defaultMessage="You don't have any direct messages yet. When you send or receive one, it will show up here." />}
|
emptyMessage={<FormattedMessage id='empty_column.direct' defaultMessage="You don't have any direct messages yet. When you send or receive one, it will show up here." />}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -149,27 +142,6 @@ class DirectTimeline extends React.PureComponent {
|
||||||
<ColumnSettingsContainer />
|
<ColumnSettingsContainer />
|
||||||
</ColumnHeader>
|
</ColumnHeader>
|
||||||
|
|
||||||
<div className='notification__filter-bar'>
|
|
||||||
<button
|
|
||||||
className={conversationsMode ? 'active' : ''}
|
|
||||||
onClick={this.handleConversationsClick}
|
|
||||||
>
|
|
||||||
<FormattedMessage
|
|
||||||
id='direct.conversations_mode'
|
|
||||||
defaultMessage='Conversations'
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className={conversationsMode ? '' : 'active'}
|
|
||||||
onClick={this.handleTimelineClick}
|
|
||||||
>
|
|
||||||
<FormattedMessage
|
|
||||||
id='direct.timeline_mode'
|
|
||||||
defaultMessage='Timeline'
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{contents}
|
{contents}
|
||||||
</Column>
|
</Column>
|
||||||
);
|
);
|
||||||
|
|
|
@ -191,7 +191,7 @@ class AccountCard extends ImmutablePureComponent {
|
||||||
<div className='account-card__counters__item'>
|
<div className='account-card__counters__item'>
|
||||||
<ShortNumber value={account.get('statuses_count')} />
|
<ShortNumber value={account.get('statuses_count')} />
|
||||||
<small>
|
<small>
|
||||||
<FormattedMessage id='account.posts' defaultMessage='Toots' />
|
<FormattedMessage id='account.posts' defaultMessage='Posts' />
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -70,7 +70,7 @@ class Favourites extends ImmutablePureComponent {
|
||||||
const { intl, statusIds, columnId, multiColumn, hasMore, isLoading } = this.props;
|
const { intl, statusIds, columnId, multiColumn, hasMore, isLoading } = this.props;
|
||||||
const pinned = !!columnId;
|
const pinned = !!columnId;
|
||||||
|
|
||||||
const emptyMessage = <FormattedMessage id='empty_column.favourited_statuses' defaultMessage="You don't have any favourite toots yet. When you favourite one, it will show up here." />;
|
const emptyMessage = <FormattedMessage id='empty_column.favourited_statuses' defaultMessage="You don't have any favourite posts yet. When you favourite one, it will show up here." />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column bindToDocument={!multiColumn} ref={this.setRef} name='favourites' label={intl.formatMessage(messages.heading)}>
|
<Column bindToDocument={!multiColumn} ref={this.setRef} name='favourites' label={intl.formatMessage(messages.heading)}>
|
||||||
|
|
|
@ -68,7 +68,7 @@ class Favourites extends ImmutablePureComponent {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const emptyMessage = <FormattedMessage id='empty_column.favourites' defaultMessage='No one has favourited this toot yet. When someone does, they will show up here.' />;
|
const emptyMessage = <FormattedMessage id='empty_column.favourites' defaultMessage='No one has favourited this post yet. When someone does, they will show up here.' />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column ref={this.setRef}>
|
<Column ref={this.setRef}>
|
||||||
|
|
|
@ -19,6 +19,8 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import MissingIndicator from 'flavours/glitch/components/missing_indicator';
|
import MissingIndicator from 'flavours/glitch/components/missing_indicator';
|
||||||
import ScrollableList from 'flavours/glitch/components/scrollable_list';
|
import ScrollableList from 'flavours/glitch/components/scrollable_list';
|
||||||
import TimelineHint from 'flavours/glitch/components/timeline_hint';
|
import TimelineHint from 'flavours/glitch/components/timeline_hint';
|
||||||
|
import LimitedAccountHint from '../account_timeline/components/limited_account_hint';
|
||||||
|
import { getAccountHidden } from 'flavours/glitch/selectors';
|
||||||
|
|
||||||
const mapStateToProps = (state, { params: { acct, id } }) => {
|
const mapStateToProps = (state, { params: { acct, id } }) => {
|
||||||
const accountId = id || state.getIn(['accounts_map', acct]);
|
const accountId = id || state.getIn(['accounts_map', acct]);
|
||||||
|
@ -37,6 +39,8 @@ const mapStateToProps = (state, { params: { acct, id } }) => {
|
||||||
accountIds: state.getIn(['user_lists', 'followers', accountId, 'items']),
|
accountIds: state.getIn(['user_lists', 'followers', accountId, 'items']),
|
||||||
hasMore: !!state.getIn(['user_lists', 'followers', accountId, 'next']),
|
hasMore: !!state.getIn(['user_lists', 'followers', accountId, 'next']),
|
||||||
isLoading: state.getIn(['user_lists', 'followers', accountId, 'isLoading'], true),
|
isLoading: state.getIn(['user_lists', 'followers', accountId, 'isLoading'], true),
|
||||||
|
suspended: state.getIn(['accounts', accountId, 'suspended'], false),
|
||||||
|
hidden: getAccountHidden(state, accountId),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -62,6 +66,8 @@ class Followers extends ImmutablePureComponent {
|
||||||
hasMore: PropTypes.bool,
|
hasMore: PropTypes.bool,
|
||||||
isLoading: PropTypes.bool,
|
isLoading: PropTypes.bool,
|
||||||
isAccount: PropTypes.bool,
|
isAccount: PropTypes.bool,
|
||||||
|
suspended: PropTypes.bool,
|
||||||
|
hidden: PropTypes.bool,
|
||||||
remote: PropTypes.bool,
|
remote: PropTypes.bool,
|
||||||
remoteUrl: PropTypes.string,
|
remoteUrl: PropTypes.string,
|
||||||
multiColumn: PropTypes.bool,
|
multiColumn: PropTypes.bool,
|
||||||
|
@ -107,7 +113,7 @@ class Followers extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { accountIds, hasMore, isAccount, multiColumn, isLoading, remote, remoteUrl } = this.props;
|
const { accountId, accountIds, hasMore, isAccount, multiColumn, isLoading, suspended, hidden, remote, remoteUrl } = this.props;
|
||||||
|
|
||||||
if (!isAccount) {
|
if (!isAccount) {
|
||||||
return (
|
return (
|
||||||
|
@ -127,7 +133,13 @@ class Followers extends ImmutablePureComponent {
|
||||||
|
|
||||||
let emptyMessage;
|
let emptyMessage;
|
||||||
|
|
||||||
if (remote && accountIds.isEmpty()) {
|
const forceEmptyState = suspended || hidden;
|
||||||
|
|
||||||
|
if (suspended) {
|
||||||
|
emptyMessage = <FormattedMessage id='empty_column.account_suspended' defaultMessage='Account suspended' />;
|
||||||
|
} else if (hidden) {
|
||||||
|
emptyMessage = <LimitedAccountHint accountId={accountId} />;
|
||||||
|
} else if (remote && accountIds.isEmpty()) {
|
||||||
emptyMessage = <RemoteHint url={remoteUrl} />;
|
emptyMessage = <RemoteHint url={remoteUrl} />;
|
||||||
} else {
|
} else {
|
||||||
emptyMessage = <FormattedMessage id='account.followers.empty' defaultMessage='No one follows this user yet.' />;
|
emptyMessage = <FormattedMessage id='account.followers.empty' defaultMessage='No one follows this user yet.' />;
|
||||||
|
@ -141,7 +153,7 @@ class Followers extends ImmutablePureComponent {
|
||||||
|
|
||||||
<ScrollableList
|
<ScrollableList
|
||||||
scrollKey='followers'
|
scrollKey='followers'
|
||||||
hasMore={hasMore}
|
hasMore={!forceEmptyState && hasMore}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
onLoadMore={this.handleLoadMore}
|
onLoadMore={this.handleLoadMore}
|
||||||
prepend={<HeaderContainer accountId={this.props.accountId} hideTabs />}
|
prepend={<HeaderContainer accountId={this.props.accountId} hideTabs />}
|
||||||
|
|
|
@ -19,6 +19,8 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import MissingIndicator from 'flavours/glitch/components/missing_indicator';
|
import MissingIndicator from 'flavours/glitch/components/missing_indicator';
|
||||||
import ScrollableList from 'flavours/glitch/components/scrollable_list';
|
import ScrollableList from 'flavours/glitch/components/scrollable_list';
|
||||||
import TimelineHint from 'flavours/glitch/components/timeline_hint';
|
import TimelineHint from 'flavours/glitch/components/timeline_hint';
|
||||||
|
import LimitedAccountHint from '../account_timeline/components/limited_account_hint';
|
||||||
|
import { getAccountHidden } from 'flavours/glitch/selectors';
|
||||||
|
|
||||||
const mapStateToProps = (state, { params: { acct, id } }) => {
|
const mapStateToProps = (state, { params: { acct, id } }) => {
|
||||||
const accountId = id || state.getIn(['accounts_map', acct]);
|
const accountId = id || state.getIn(['accounts_map', acct]);
|
||||||
|
@ -37,6 +39,8 @@ const mapStateToProps = (state, { params: { acct, id } }) => {
|
||||||
accountIds: state.getIn(['user_lists', 'following', accountId, 'items']),
|
accountIds: state.getIn(['user_lists', 'following', accountId, 'items']),
|
||||||
hasMore: !!state.getIn(['user_lists', 'following', accountId, 'next']),
|
hasMore: !!state.getIn(['user_lists', 'following', accountId, 'next']),
|
||||||
isLoading: state.getIn(['user_lists', 'following', accountId, 'isLoading'], true),
|
isLoading: state.getIn(['user_lists', 'following', accountId, 'isLoading'], true),
|
||||||
|
suspended: state.getIn(['accounts', accountId, 'suspended'], false),
|
||||||
|
hidden: getAccountHidden(state, accountId),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -62,6 +66,8 @@ class Following extends ImmutablePureComponent {
|
||||||
hasMore: PropTypes.bool,
|
hasMore: PropTypes.bool,
|
||||||
isLoading: PropTypes.bool,
|
isLoading: PropTypes.bool,
|
||||||
isAccount: PropTypes.bool,
|
isAccount: PropTypes.bool,
|
||||||
|
suspended: PropTypes.bool,
|
||||||
|
hidden: PropTypes.bool,
|
||||||
remote: PropTypes.bool,
|
remote: PropTypes.bool,
|
||||||
remoteUrl: PropTypes.string,
|
remoteUrl: PropTypes.string,
|
||||||
multiColumn: PropTypes.bool,
|
multiColumn: PropTypes.bool,
|
||||||
|
@ -107,7 +113,7 @@ class Following extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { accountIds, hasMore, isAccount, multiColumn, isLoading, remote, remoteUrl } = this.props;
|
const { accountId, accountIds, hasMore, isAccount, multiColumn, isLoading, suspended, hidden, remote, remoteUrl } = this.props;
|
||||||
|
|
||||||
if (!isAccount) {
|
if (!isAccount) {
|
||||||
return (
|
return (
|
||||||
|
@ -127,7 +133,13 @@ class Following extends ImmutablePureComponent {
|
||||||
|
|
||||||
let emptyMessage;
|
let emptyMessage;
|
||||||
|
|
||||||
if (remote && accountIds.isEmpty()) {
|
const forceEmptyState = suspended || hidden;
|
||||||
|
|
||||||
|
if (suspended) {
|
||||||
|
emptyMessage = <FormattedMessage id='empty_column.account_suspended' defaultMessage='Account suspended' />;
|
||||||
|
} else if (hidden) {
|
||||||
|
emptyMessage = <LimitedAccountHint accountId={accountId} />;
|
||||||
|
} else if (remote && accountIds.isEmpty()) {
|
||||||
emptyMessage = <RemoteHint url={remoteUrl} />;
|
emptyMessage = <RemoteHint url={remoteUrl} />;
|
||||||
} else {
|
} else {
|
||||||
emptyMessage = <FormattedMessage id='account.follows.empty' defaultMessage="This user doesn't follow anyone yet." />;
|
emptyMessage = <FormattedMessage id='account.follows.empty' defaultMessage="This user doesn't follow anyone yet." />;
|
||||||
|
@ -141,7 +153,7 @@ class Following extends ImmutablePureComponent {
|
||||||
|
|
||||||
<ScrollableList
|
<ScrollableList
|
||||||
scrollKey='following'
|
scrollKey='following'
|
||||||
hasMore={hasMore}
|
hasMore={!forceEmptyState && hasMore}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
onLoadMore={this.handleLoadMore}
|
onLoadMore={this.handleLoadMore}
|
||||||
prepend={<HeaderContainer accountId={this.props.accountId} hideTabs />}
|
prepend={<HeaderContainer accountId={this.props.accountId} hideTabs />}
|
||||||
|
|
|
@ -6,7 +6,7 @@ import PropTypes from 'prop-types';
|
||||||
import IconButton from 'flavours/glitch/components/icon_button';
|
import IconButton from 'flavours/glitch/components/icon_button';
|
||||||
import Icon from 'flavours/glitch/components/icon';
|
import Icon from 'flavours/glitch/components/icon';
|
||||||
import { defineMessages, injectIntl, FormattedMessage, FormattedDate } from 'react-intl';
|
import { defineMessages, injectIntl, FormattedMessage, FormattedDate } from 'react-intl';
|
||||||
import { autoPlayGif, reduceMotion } from 'flavours/glitch/util/initial_state';
|
import { autoPlayGif, reduceMotion, disableSwiping } from 'flavours/glitch/util/initial_state';
|
||||||
import elephantUIPlane from 'mastodon/../images/elephant_ui_plane.svg';
|
import elephantUIPlane from 'mastodon/../images/elephant_ui_plane.svg';
|
||||||
import { mascot } from 'flavours/glitch/util/initial_state';
|
import { mascot } from 'flavours/glitch/util/initial_state';
|
||||||
import unicodeMapping from 'flavours/glitch/util/emoji/emoji_unicode_mapping_light';
|
import unicodeMapping from 'flavours/glitch/util/emoji/emoji_unicode_mapping_light';
|
||||||
|
@ -430,6 +430,7 @@ class Announcements extends ImmutablePureComponent {
|
||||||
removeReaction={this.props.removeReaction}
|
removeReaction={this.props.removeReaction}
|
||||||
intl={intl}
|
intl={intl}
|
||||||
selected={index === idx}
|
selected={index === idx}
|
||||||
|
disabled={disableSwiping}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</ReactSwipeableViews>
|
</ReactSwipeableViews>
|
||||||
|
|
|
@ -18,7 +18,7 @@ const messages = defineMessages({
|
||||||
mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' },
|
mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' },
|
||||||
info: { id: 'navigation_bar.info', defaultMessage: 'Extended information' },
|
info: { id: 'navigation_bar.info', defaultMessage: 'Extended information' },
|
||||||
show_me_around: { id: 'getting_started.onboarding', defaultMessage: 'Show me around' },
|
show_me_around: { id: 'getting_started.onboarding', defaultMessage: 'Show me around' },
|
||||||
pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned toots' },
|
pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned posts' },
|
||||||
info: { id: 'navigation_bar.info', defaultMessage: 'Extended information' },
|
info: { id: 'navigation_bar.info', defaultMessage: 'Extended information' },
|
||||||
keyboard_shortcuts: { id: 'navigation_bar.keyboard_shortcuts', defaultMessage: 'Keyboard shortcuts' },
|
keyboard_shortcuts: { id: 'navigation_bar.keyboard_shortcuts', defaultMessage: 'Keyboard shortcuts' },
|
||||||
featured_users: { id: 'navigation_bar.featured_users', defaultMessage: 'Featured users' },
|
featured_users: { id: 'navigation_bar.featured_users', defaultMessage: 'Featured users' },
|
||||||
|
|
|
@ -103,7 +103,7 @@ class KeyboardShortcuts extends ImmutablePureComponent {
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><kbd>alt</kbd>+<kbd>n</kbd></td>
|
<td><kbd>alt</kbd>+<kbd>n</kbd></td>
|
||||||
<td><FormattedMessage id='keyboard_shortcuts.toot' defaultMessage='to start a brand new toot' /></td>
|
<td><FormattedMessage id='keyboard_shortcuts.toot' defaultMessage='to start a brand new post' /></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><kbd>alt</kbd>+<kbd>x</kbd></td>
|
<td><kbd>alt</kbd>+<kbd>x</kbd></td>
|
||||||
|
|
|
@ -0,0 +1,83 @@
|
||||||
|
// Package imports
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||||
|
|
||||||
|
export default class LocalSettingsPageItem extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
children: PropTypes.node.isRequired,
|
||||||
|
id: PropTypes.string.isRequired,
|
||||||
|
options: PropTypes.arrayOf(PropTypes.shape({
|
||||||
|
value: PropTypes.string.isRequired,
|
||||||
|
message: PropTypes.string.isRequired,
|
||||||
|
hint: PropTypes.string,
|
||||||
|
})),
|
||||||
|
value: PropTypes.any,
|
||||||
|
placeholder: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { id, options, children, placeholder, value } = this.props;
|
||||||
|
|
||||||
|
if (options && options.length > 0) {
|
||||||
|
const currentValue = value;
|
||||||
|
const optionElems = options && options.length > 0 && options.map((opt) => {
|
||||||
|
let optionId = `${id}--${opt.value}`;
|
||||||
|
return (
|
||||||
|
<label htmlFor={optionId}>
|
||||||
|
<input
|
||||||
|
type='radio'
|
||||||
|
name={id}
|
||||||
|
id={optionId}
|
||||||
|
value={opt.value}
|
||||||
|
checked={currentValue === opt.value}
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
{opt.message}
|
||||||
|
{opt.hint && <span className='hint'>{opt.hint}</span>}
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<div className='glitch local-settings__page__item radio_buttons'>
|
||||||
|
<fieldset>
|
||||||
|
<legend>{children}</legend>
|
||||||
|
{optionElems}
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if (placeholder) {
|
||||||
|
return (
|
||||||
|
<div className='glitch local-settings__page__item string'>
|
||||||
|
<label htmlFor={id}>
|
||||||
|
<p>{children}</p>
|
||||||
|
<p>
|
||||||
|
<input
|
||||||
|
id={id}
|
||||||
|
type='text'
|
||||||
|
value={value}
|
||||||
|
placeholder={placeholder}
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else return (
|
||||||
|
<div className='glitch local-settings__page__item boolean'>
|
||||||
|
<label htmlFor={id}>
|
||||||
|
<input
|
||||||
|
id={id}
|
||||||
|
type='checkbox'
|
||||||
|
checked={value}
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
{children}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -5,7 +5,10 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
|
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
|
||||||
|
|
||||||
// Our imports
|
// Our imports
|
||||||
|
import { expandSpoilers, disableSwiping } from 'flavours/glitch/util/initial_state';
|
||||||
|
import { preferenceLink } from 'flavours/glitch/util/backend_links';
|
||||||
import LocalSettingsPageItem from './item';
|
import LocalSettingsPageItem from './item';
|
||||||
|
import DeprecatedLocalSettingsPageItem from './deprecated_item';
|
||||||
|
|
||||||
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||||
|
|
||||||
|
@ -114,6 +117,50 @@ class LocalSettingsPage extends React.PureComponent {
|
||||||
<span className='hint'><FormattedMessage id='settings.notifications.favicon_badge.hint' defaultMessage="Add a badge for unread notifications to the favicon" /></span>
|
<span className='hint'><FormattedMessage id='settings.notifications.favicon_badge.hint' defaultMessage="Add a badge for unread notifications to the favicon" /></span>
|
||||||
</LocalSettingsPageItem>
|
</LocalSettingsPageItem>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2><FormattedMessage id='settings.status_icons' defaultMessage='Toot icons' /></h2>
|
||||||
|
<LocalSettingsPageItem
|
||||||
|
settings={settings}
|
||||||
|
item={['status_icons', 'language']}
|
||||||
|
id='mastodon-settings--status-icons-language'
|
||||||
|
onChange={onChange}
|
||||||
|
>
|
||||||
|
<FormattedMessage id='settings.status_icons_language' defaultMessage='Language indicator' />
|
||||||
|
</LocalSettingsPageItem>
|
||||||
|
<LocalSettingsPageItem
|
||||||
|
settings={settings}
|
||||||
|
item={['status_icons', 'reply']}
|
||||||
|
id='mastodon-settings--status-icons-reply'
|
||||||
|
onChange={onChange}
|
||||||
|
>
|
||||||
|
<FormattedMessage id='settings.status_icons_reply' defaultMessage='Reply indicator' />
|
||||||
|
</LocalSettingsPageItem>
|
||||||
|
<LocalSettingsPageItem
|
||||||
|
settings={settings}
|
||||||
|
item={['status_icons', 'local_only']}
|
||||||
|
id='mastodon-settings--status-icons-local_only'
|
||||||
|
onChange={onChange}
|
||||||
|
>
|
||||||
|
<FormattedMessage id='settings.status_icons_local_only' defaultMessage='Local-only indicator' />
|
||||||
|
</LocalSettingsPageItem>
|
||||||
|
<LocalSettingsPageItem
|
||||||
|
settings={settings}
|
||||||
|
item={['status_icons', 'media']}
|
||||||
|
id='mastodon-settings--status-icons-media'
|
||||||
|
onChange={onChange}
|
||||||
|
>
|
||||||
|
<FormattedMessage id='settings.status_icons_media' defaultMessage='Media and poll indicators' />
|
||||||
|
</LocalSettingsPageItem>
|
||||||
|
<LocalSettingsPageItem
|
||||||
|
settings={settings}
|
||||||
|
item={['status_icons', 'visibility']}
|
||||||
|
id='mastodon-settings--status-icons-visibility'
|
||||||
|
onChange={onChange}
|
||||||
|
>
|
||||||
|
<FormattedMessage id='settings.status_icons_visibility' defaultMessage='Toot privacy indicator' />
|
||||||
|
</LocalSettingsPageItem>
|
||||||
|
</section>
|
||||||
<section>
|
<section>
|
||||||
<h2><FormattedMessage id='settings.layout_opts' defaultMessage='Layout options' /></h2>
|
<h2><FormattedMessage id='settings.layout_opts' defaultMessage='Layout options' /></h2>
|
||||||
<LocalSettingsPageItem
|
<LocalSettingsPageItem
|
||||||
|
@ -146,14 +193,28 @@ class LocalSettingsPage extends React.PureComponent {
|
||||||
>
|
>
|
||||||
<FormattedMessage id='settings.navbar_under' defaultMessage='Navbar at the bottom (Mobile only)' />
|
<FormattedMessage id='settings.navbar_under' defaultMessage='Navbar at the bottom (Mobile only)' />
|
||||||
</LocalSettingsPageItem>
|
</LocalSettingsPageItem>
|
||||||
<LocalSettingsPageItem
|
<DeprecatedLocalSettingsPageItem
|
||||||
settings={settings}
|
|
||||||
item={['swipe_to_change_columns']}
|
|
||||||
id='mastodon-settings--swipe_to_change_columns'
|
id='mastodon-settings--swipe_to_change_columns'
|
||||||
onChange={onChange}
|
value={!disableSwiping}
|
||||||
>
|
>
|
||||||
<FormattedMessage id='settings.swipe_to_change_columns' defaultMessage='Allow swiping to change columns (Mobile only)' />
|
<FormattedMessage id='settings.swipe_to_change_columns' defaultMessage='Allow swiping to change columns (Mobile only)' />
|
||||||
</LocalSettingsPageItem>
|
<span className='hint'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='settings.deprecated_setting'
|
||||||
|
defaultMessage="This setting is now controlled from Mastodon's {settings_page_link}"
|
||||||
|
values={{
|
||||||
|
settings_page_link: (
|
||||||
|
<a href={preferenceLink('user_setting_disable_swiping')}>
|
||||||
|
<FormattedMessage
|
||||||
|
id='settings.shared_settings_link'
|
||||||
|
defaultMessage='user preferences'
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</DeprecatedLocalSettingsPageItem>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
|
@ -242,21 +303,35 @@ class LocalSettingsPage extends React.PureComponent {
|
||||||
({ intl, onChange, settings }) => (
|
({ intl, onChange, settings }) => (
|
||||||
<div className='glitch local-settings__page content_warnings'>
|
<div className='glitch local-settings__page content_warnings'>
|
||||||
<h1><FormattedMessage id='settings.content_warnings' defaultMessage='Content warnings' /></h1>
|
<h1><FormattedMessage id='settings.content_warnings' defaultMessage='Content warnings' /></h1>
|
||||||
<LocalSettingsPageItem
|
<DeprecatedLocalSettingsPageItem
|
||||||
settings={settings}
|
|
||||||
item={['content_warnings', 'auto_unfold']}
|
|
||||||
id='mastodon-settings--content_warnings-auto_unfold'
|
id='mastodon-settings--content_warnings-auto_unfold'
|
||||||
onChange={onChange}
|
value={expandSpoilers}
|
||||||
>
|
>
|
||||||
<FormattedMessage id='settings.enable_content_warnings_auto_unfold' defaultMessage='Automatically unfold content-warnings' />
|
<FormattedMessage id='settings.enable_content_warnings_auto_unfold' defaultMessage='Automatically unfold content-warnings' />
|
||||||
</LocalSettingsPageItem>
|
<span className='hint'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='settings.deprecated_setting'
|
||||||
|
defaultMessage="This setting is now controlled from Mastodon's {settings_page_link}"
|
||||||
|
values={{
|
||||||
|
settings_page_link: (
|
||||||
|
<a href={preferenceLink('user_setting_expand_spoilers')}>
|
||||||
|
<FormattedMessage
|
||||||
|
id='settings.shared_settings_link'
|
||||||
|
defaultMessage='user preferences'
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</DeprecatedLocalSettingsPageItem>
|
||||||
<LocalSettingsPageItem
|
<LocalSettingsPageItem
|
||||||
settings={settings}
|
settings={settings}
|
||||||
item={['content_warnings', 'filter']}
|
item={['content_warnings', 'filter']}
|
||||||
id='mastodon-settings--content_warnings-auto_unfold'
|
id='mastodon-settings--content_warnings-auto_unfold'
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
dependsOn={[['content_warnings', 'auto_unfold']]}
|
|
||||||
placeholder={intl.formatMessage(messages.regexp)}
|
placeholder={intl.formatMessage(messages.regexp)}
|
||||||
|
disabled={!expandSpoilers}
|
||||||
>
|
>
|
||||||
<FormattedMessage id='settings.content_warnings_filter' defaultMessage='Content warnings to not automatically unfold:' />
|
<FormattedMessage id='settings.content_warnings_filter' defaultMessage='Content warnings to not automatically unfold:' />
|
||||||
</LocalSettingsPageItem>
|
</LocalSettingsPageItem>
|
||||||
|
|
|
@ -21,6 +21,7 @@ export default class LocalSettingsPageItem extends React.PureComponent {
|
||||||
})),
|
})),
|
||||||
settings: ImmutablePropTypes.map.isRequired,
|
settings: ImmutablePropTypes.map.isRequired,
|
||||||
placeholder: PropTypes.string,
|
placeholder: PropTypes.string,
|
||||||
|
disabled: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
handleChange = e => {
|
handleChange = e => {
|
||||||
|
@ -33,8 +34,8 @@ export default class LocalSettingsPageItem extends React.PureComponent {
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { handleChange } = this;
|
const { handleChange } = this;
|
||||||
const { settings, item, id, options, children, dependsOn, dependsOnNot, placeholder } = this.props;
|
const { settings, item, id, options, children, dependsOn, dependsOnNot, placeholder, disabled } = this.props;
|
||||||
let enabled = true;
|
let enabled = !disabled;
|
||||||
|
|
||||||
if (dependsOn) {
|
if (dependsOn) {
|
||||||
for (let i = 0; i < dependsOn.length; i++) {
|
for (let i = 0; i < dependsOn.length; i++) {
|
||||||
|
|
|
@ -69,7 +69,7 @@ class Mutes extends ImmutablePureComponent {
|
||||||
bindToDocument={!multiColumn}
|
bindToDocument={!multiColumn}
|
||||||
>
|
>
|
||||||
{accountIds.map(id =>
|
{accountIds.map(id =>
|
||||||
<AccountContainer key={id} id={id} />,
|
<AccountContainer key={id} id={id} defaultAction='mute' />,
|
||||||
)}
|
)}
|
||||||
</ScrollableList>
|
</ScrollableList>
|
||||||
</Column>
|
</Column>
|
||||||
|
|
|
@ -146,7 +146,7 @@ export default class ColumnSettings extends React.PureComponent {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div role='group' aria-labelledby='notifications-status'>
|
<div role='group' aria-labelledby='notifications-status'>
|
||||||
<span id='notifications-status' className='column-settings__section'><FormattedMessage id='notifications.column_settings.status' defaultMessage='New toots:' /></span>
|
<span id='notifications-status' className='column-settings__section'><FormattedMessage id='notifications.column_settings.status' defaultMessage='New posts:' /></span>
|
||||||
|
|
||||||
<div className='column-settings__pillbar'>
|
<div className='column-settings__pillbar'>
|
||||||
<PillBarButton disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'status']} onChange={onChange} label={alertStr} />
|
<PillBarButton disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'status']} onChange={onChange} label={alertStr} />
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { defineMessages, injectIntl } from 'react-intl';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
heading: { id: 'column.pins', defaultMessage: 'Pinned toot' },
|
heading: { id: 'column.pins', defaultMessage: 'Pinned post' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
|
|
|
@ -68,7 +68,7 @@ class Reblogs extends ImmutablePureComponent {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const emptyMessage = <FormattedMessage id='status.reblogs.empty' defaultMessage='No one has boosted this toot yet. When someone does, they will show up here.' />;
|
const emptyMessage = <FormattedMessage id='status.reblogs.empty' defaultMessage='No one has boosted this post yet. When someone does, they will show up here.' />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column ref={this.setRef}>
|
<Column ref={this.setRef}>
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
import Button from 'flavours/glitch/components/button';
|
import Button from 'flavours/glitch/components/button';
|
||||||
import Option from './components/option';
|
import Option from './components/option';
|
||||||
|
@ -17,11 +19,17 @@ const messages = defineMessages({
|
||||||
account: { id: 'report.category.title_account', defaultMessage: 'profile' },
|
account: { id: 'report.category.title_account', defaultMessage: 'profile' },
|
||||||
});
|
});
|
||||||
|
|
||||||
export default @injectIntl
|
const mapStateToProps = state => ({
|
||||||
|
rules: state.get('rules'),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default @connect(mapStateToProps)
|
||||||
|
@injectIntl
|
||||||
class Category extends React.PureComponent {
|
class Category extends React.PureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
onNextStep: PropTypes.func.isRequired,
|
onNextStep: PropTypes.func.isRequired,
|
||||||
|
rules: ImmutablePropTypes.list,
|
||||||
category: PropTypes.string,
|
category: PropTypes.string,
|
||||||
onChangeCategory: PropTypes.func.isRequired,
|
onChangeCategory: PropTypes.func.isRequired,
|
||||||
startedFrom: PropTypes.oneOf(['status', 'account']),
|
startedFrom: PropTypes.oneOf(['status', 'account']),
|
||||||
|
@ -53,13 +61,15 @@ class Category extends React.PureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { category, startedFrom, intl } = this.props;
|
const { category, startedFrom, rules, intl } = this.props;
|
||||||
|
|
||||||
const options = [
|
const options = rules.size > 0 ? [
|
||||||
'dislike',
|
|
||||||
'spam',
|
'spam',
|
||||||
'violation',
|
'violation',
|
||||||
'other',
|
'other',
|
||||||
|
] : [
|
||||||
|
'spam',
|
||||||
|
'other',
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -8,7 +8,7 @@ import {
|
||||||
unfollowAccount,
|
unfollowAccount,
|
||||||
muteAccount,
|
muteAccount,
|
||||||
blockAccount,
|
blockAccount,
|
||||||
} from 'mastodon/actions/accounts';
|
} from 'flavours/glitch/actions/accounts';
|
||||||
|
|
||||||
const mapStateToProps = () => ({});
|
const mapStateToProps = () => ({});
|
||||||
|
|
||||||
|
|
|
@ -134,10 +134,6 @@ class DetailedStatus extends ImmutablePureComponent {
|
||||||
outerStyle.height = `${this.state.height}px`;
|
outerStyle.height = `${this.state.height}px`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status.get('poll')) {
|
|
||||||
media.push(<PollContainer pollId={status.get('poll')} />);
|
|
||||||
mediaIcons.push('tasks');
|
|
||||||
}
|
|
||||||
if (usingPiP) {
|
if (usingPiP) {
|
||||||
media.push(<PictureInPicturePlaceholder />);
|
media.push(<PictureInPicturePlaceholder />);
|
||||||
mediaIcons.push('video-camera');
|
mediaIcons.push('video-camera');
|
||||||
|
@ -202,6 +198,11 @@ class DetailedStatus extends ImmutablePureComponent {
|
||||||
mediaIcons.push('link');
|
mediaIcons.push('link');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (status.get('poll')) {
|
||||||
|
media.push(<PollContainer pollId={status.get('poll')} />);
|
||||||
|
mediaIcons.push('tasks');
|
||||||
|
}
|
||||||
|
|
||||||
if (status.get('application')) {
|
if (status.get('application')) {
|
||||||
applicationLink = <React.Fragment> · <a className='detailed-status__application' href={status.getIn(['application', 'website'])} target='_blank' rel='noopener noreferrer'>{status.getIn(['application', 'name'])}</a></React.Fragment>;
|
applicationLink = <React.Fragment> · <a className='detailed-status__application' href={status.getIn(['application', 'website'])} target='_blank' rel='noopener noreferrer'>{status.getIn(['application', 'name'])}</a></React.Fragment>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,14 +14,11 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import PrivacyDropdown from 'flavours/glitch/features/compose/components/privacy_dropdown';
|
import PrivacyDropdown from 'flavours/glitch/features/compose/components/privacy_dropdown';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { changeBoostPrivacy } from 'flavours/glitch/actions/boosts';
|
import { changeBoostPrivacy } from 'flavours/glitch/actions/boosts';
|
||||||
|
import VisibilityIcon from 'flavours/glitch/components/status_visibility_icon';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
cancel_reblog: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
|
cancel_reblog: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
|
||||||
reblog: { id: 'status.reblog', defaultMessage: 'Boost' },
|
reblog: { id: 'status.reblog', defaultMessage: 'Boost' },
|
||||||
public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
|
|
||||||
unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' },
|
|
||||||
private_short: { id: 'privacy.private.short', defaultMessage: 'Followers-only' },
|
|
||||||
direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapStateToProps = state => {
|
const mapStateToProps = state => {
|
||||||
|
@ -85,15 +82,6 @@ class BoostModal extends ImmutablePureComponent {
|
||||||
const { status, missingMediaDescription, privacy, intl } = this.props;
|
const { status, missingMediaDescription, privacy, intl } = this.props;
|
||||||
const buttonText = status.get('reblogged') ? messages.cancel_reblog : messages.reblog;
|
const buttonText = status.get('reblogged') ? messages.cancel_reblog : messages.reblog;
|
||||||
|
|
||||||
const visibilityIconInfo = {
|
|
||||||
'public': { icon: 'globe', text: intl.formatMessage(messages.public_short) },
|
|
||||||
'unlisted': { icon: 'unlock', text: intl.formatMessage(messages.unlisted_short) },
|
|
||||||
'private': { icon: 'lock', text: intl.formatMessage(messages.private_short) },
|
|
||||||
'direct': { icon: 'envelope', text: intl.formatMessage(messages.direct_short) },
|
|
||||||
};
|
|
||||||
|
|
||||||
const visibilityIcon = visibilityIconInfo[status.get('visibility')];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='modal-root__modal boost-modal'>
|
<div className='modal-root__modal boost-modal'>
|
||||||
<div className='boost-modal__container'>
|
<div className='boost-modal__container'>
|
||||||
|
@ -101,7 +89,7 @@ class BoostModal extends ImmutablePureComponent {
|
||||||
<div className='boost-modal__status-header'>
|
<div className='boost-modal__status-header'>
|
||||||
<div className='boost-modal__status-time'>
|
<div className='boost-modal__status-time'>
|
||||||
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener noreferrer'>
|
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener noreferrer'>
|
||||||
<span className='status__visibility-icon'><Icon id={visibilityIcon.icon} title={visibilityIcon.text} /></span>
|
<VisibilityIcon visibility={status.get('visibility')} />
|
||||||
<RelativeTimestamp timestamp={status.get('created_at')} /></a>
|
<RelativeTimestamp timestamp={status.get('created_at')} /></a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,8 @@ import ReactSwipeableViews from 'react-swipeable-views';
|
||||||
import TabsBar, { links, getIndex, getLink } from './tabs_bar';
|
import TabsBar, { links, getIndex, getLink } from './tabs_bar';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { disableSwiping } from 'flavours/glitch/util/initial_state';
|
||||||
|
|
||||||
import BundleContainer from '../containers/bundle_container';
|
import BundleContainer from '../containers/bundle_container';
|
||||||
import ColumnLoading from './column_loading';
|
import ColumnLoading from './column_loading';
|
||||||
import DrawerLoading from './drawer_loading';
|
import DrawerLoading from './drawer_loading';
|
||||||
|
@ -63,7 +65,6 @@ class ColumnsArea extends ImmutablePureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
columns: ImmutablePropTypes.list.isRequired,
|
columns: ImmutablePropTypes.list.isRequired,
|
||||||
swipeToChangeColumns: PropTypes.bool,
|
|
||||||
singleColumn: PropTypes.bool,
|
singleColumn: PropTypes.bool,
|
||||||
children: PropTypes.node,
|
children: PropTypes.node,
|
||||||
navbarUnder: PropTypes.bool,
|
navbarUnder: PropTypes.bool,
|
||||||
|
@ -210,7 +211,7 @@ class ColumnsArea extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { columns, children, singleColumn, swipeToChangeColumns, intl, navbarUnder, openSettings } = this.props;
|
const { columns, children, singleColumn, intl, navbarUnder, openSettings } = this.props;
|
||||||
const { shouldAnimate, renderComposePanel } = this.state;
|
const { shouldAnimate, renderComposePanel } = this.state;
|
||||||
|
|
||||||
const columnIndex = getIndex(this.context.router.history.location.pathname);
|
const columnIndex = getIndex(this.context.router.history.location.pathname);
|
||||||
|
@ -219,7 +220,7 @@ class ColumnsArea extends ImmutablePureComponent {
|
||||||
const floatingActionButton = shouldHideFAB(this.context.router.history.location.pathname) ? null : <Link key='floating-action-button' to='/publish' className='floating-action-button' aria-label={intl.formatMessage(messages.publish)}><Icon id='pencil' /></Link>;
|
const floatingActionButton = shouldHideFAB(this.context.router.history.location.pathname) ? null : <Link key='floating-action-button' to='/publish' className='floating-action-button' aria-label={intl.formatMessage(messages.publish)}><Icon id='pencil' /></Link>;
|
||||||
|
|
||||||
const content = columnIndex !== -1 ? (
|
const content = columnIndex !== -1 ? (
|
||||||
<ReactSwipeableViews key='content' hysteresis={0.2} threshold={15} index={columnIndex} onChangeIndex={this.handleSwipe} onTransitionEnd={this.handleAnimationEnd} animateTransitions={shouldAnimate} springConfig={{ duration: '400ms', delay: '0s', easeFunction: 'ease' }} style={{ height: '100%' }} disabled={!swipeToChangeColumns}>
|
<ReactSwipeableViews key='content' hysteresis={0.2} threshold={15} index={columnIndex} onChangeIndex={this.handleSwipe} onTransitionEnd={this.handleAnimationEnd} animateTransitions={shouldAnimate} springConfig={{ duration: '400ms', delay: '0s', easeFunction: 'ease' }} style={{ height: '100%' }} disabled={disableSwiping}>
|
||||||
{links.map(this.renderView)}
|
{links.map(this.renderView)}
|
||||||
</ReactSwipeableViews>
|
</ReactSwipeableViews>
|
||||||
) : (
|
) : (
|
||||||
|
@ -234,7 +235,7 @@ class ColumnsArea extends ImmutablePureComponent {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='columns-area__panels__main'>
|
<div className={`columns-area__panels__main ${floatingActionButton && 'with-fab'}`}>
|
||||||
{!navbarUnder && <TabsBar key='tabs' />}
|
{!navbarUnder && <TabsBar key='tabs' />}
|
||||||
{content}
|
{content}
|
||||||
{navbarUnder && <TabsBar key='tabs' />}
|
{navbarUnder && <TabsBar key='tabs' />}
|
||||||
|
|
|
@ -0,0 +1,86 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
|
import { preferenceLink } from 'flavours/glitch/util/backend_links';
|
||||||
|
import Button from 'flavours/glitch/components/button';
|
||||||
|
import Icon from 'flavours/glitch/components/icon';
|
||||||
|
import illustration from 'flavours/glitch/images/logo_warn_glitch.svg';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
discardChanges: { id: 'confirmations.deprecated_settings.confirm', defaultMessage: 'Use Mastodon preferences' },
|
||||||
|
user_setting_expand_spoilers: { id: 'settings.enable_content_warnings_auto_unfold', defaultMessage: 'Automatically unfold content-warnings' },
|
||||||
|
user_setting_disable_swiping: { id: 'settings.swipe_to_change_columns', defaultMessage: 'Allow swiping to change columns (Mobile only)' },
|
||||||
|
});
|
||||||
|
|
||||||
|
export default @injectIntl
|
||||||
|
class DeprecatedSettingsModal extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
settings: ImmutablePropTypes.list.isRequired,
|
||||||
|
onClose: PropTypes.func.isRequired,
|
||||||
|
onConfirm: PropTypes.func.isRequired,
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.button.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClick = () => {
|
||||||
|
this.props.onConfirm();
|
||||||
|
this.props.onClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
setRef = (c) => {
|
||||||
|
this.button = c;
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { settings, intl } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='modal-root__modal confirmation-modal'>
|
||||||
|
<div className='confirmation-modal__container'>
|
||||||
|
|
||||||
|
<img src={illustration} className='modal-warning' alt='' />
|
||||||
|
|
||||||
|
<FormattedMessage
|
||||||
|
id='confirmations.deprecated_settings.message'
|
||||||
|
defaultMessage='Some of the glitch-soc device-specific {app_settings} you are using have been replaced by Mastodon {preferences} and will be overriden:'
|
||||||
|
values={{
|
||||||
|
app_settings: (
|
||||||
|
<strong className='deprecated-settings-label'>
|
||||||
|
<Icon id='cogs' /> <FormattedMessage id='navigation_bar.app_settings' defaultMessage='App settings' />
|
||||||
|
</strong>
|
||||||
|
),
|
||||||
|
preferences: (
|
||||||
|
<strong className='deprecated-settings-label'>
|
||||||
|
<Icon id='cog' /> <FormattedMessage id='navigation_bar.preferences' defaultMessage='Preferences' />
|
||||||
|
</strong>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className='deprecated-settings-info'>
|
||||||
|
<ul>
|
||||||
|
{ settings.map((setting_name) => (
|
||||||
|
<li>
|
||||||
|
<a href={preferenceLink(setting_name)}><FormattedMessage {...messages[setting_name]} /></a>
|
||||||
|
</li>
|
||||||
|
)) }
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className='confirmation-modal__action-bar'>
|
||||||
|
<div />
|
||||||
|
<Button text={intl.formatMessage(messages.discardChanges)} onClick={this.handleClick} ref={this.setRef} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -11,13 +11,10 @@ import AttachmentList from 'flavours/glitch/components/attachment_list';
|
||||||
import Icon from 'flavours/glitch/components/icon';
|
import Icon from 'flavours/glitch/components/icon';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import VisibilityIcon from 'flavours/glitch/components/status_visibility_icon';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
favourite: { id: 'status.favourite', defaultMessage: 'Favourite' },
|
favourite: { id: 'status.favourite', defaultMessage: 'Favourite' },
|
||||||
public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
|
|
||||||
unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' },
|
|
||||||
private_short: { id: 'privacy.private.short', defaultMessage: 'Followers-only' },
|
|
||||||
direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default @injectIntl
|
export default @injectIntl
|
||||||
|
@ -60,15 +57,6 @@ class FavouriteModal extends ImmutablePureComponent {
|
||||||
render () {
|
render () {
|
||||||
const { status, intl } = this.props;
|
const { status, intl } = this.props;
|
||||||
|
|
||||||
const visibilityIconInfo = {
|
|
||||||
'public': { icon: 'globe', text: intl.formatMessage(messages.public_short) },
|
|
||||||
'unlisted': { icon: 'unlock', text: intl.formatMessage(messages.unlisted_short) },
|
|
||||||
'private': { icon: 'lock', text: intl.formatMessage(messages.private_short) },
|
|
||||||
'direct': { icon: 'envelope', text: intl.formatMessage(messages.direct_short) },
|
|
||||||
};
|
|
||||||
|
|
||||||
const visibilityIcon = visibilityIconInfo[status.get('visibility')];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='modal-root__modal favourite-modal'>
|
<div className='modal-root__modal favourite-modal'>
|
||||||
<div className='favourite-modal__container'>
|
<div className='favourite-modal__container'>
|
||||||
|
@ -76,7 +64,7 @@ class FavouriteModal extends ImmutablePureComponent {
|
||||||
<div className='favourite-modal__status-header'>
|
<div className='favourite-modal__status-header'>
|
||||||
<div className='favourite-modal__status-time'>
|
<div className='favourite-modal__status-time'>
|
||||||
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener noreferrer'>
|
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener noreferrer'>
|
||||||
<span className='status__visibility-icon'><Icon id={visibilityIcon.icon} title={visibilityIcon.text} /></span>
|
<VisibilityIcon visibility={status.get('visibility')} />
|
||||||
<RelativeTimestamp timestamp={status.get('created_at')} />
|
<RelativeTimestamp timestamp={status.get('created_at')} />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -12,6 +12,7 @@ import Icon from 'flavours/glitch/components/icon';
|
||||||
import GIFV from 'flavours/glitch/components/gifv';
|
import GIFV from 'flavours/glitch/components/gifv';
|
||||||
import Footer from 'flavours/glitch/features/picture_in_picture/components/footer';
|
import Footer from 'flavours/glitch/features/picture_in_picture/components/footer';
|
||||||
import { getAverageFromBlurhash } from 'flavours/glitch/blurhash';
|
import { getAverageFromBlurhash } from 'flavours/glitch/blurhash';
|
||||||
|
import { disableSwiping } from 'flavours/glitch/util/initial_state';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
close: { id: 'lightbox.close', defaultMessage: 'Close' },
|
close: { id: 'lightbox.close', defaultMessage: 'Close' },
|
||||||
|
@ -227,6 +228,7 @@ class MediaModal extends ImmutablePureComponent {
|
||||||
onChangeIndex={this.handleSwipe}
|
onChangeIndex={this.handleSwipe}
|
||||||
onTransitionEnd={this.handleTransitionEnd}
|
onTransitionEnd={this.handleTransitionEnd}
|
||||||
index={index}
|
index={index}
|
||||||
|
disabled={disableSwiping}
|
||||||
>
|
>
|
||||||
{content}
|
{content}
|
||||||
</ReactSwipeableViews>
|
</ReactSwipeableViews>
|
||||||
|
|
|
@ -14,6 +14,7 @@ import AudioModal from './audio_modal';
|
||||||
import DoodleModal from './doodle_modal';
|
import DoodleModal from './doodle_modal';
|
||||||
import ConfirmationModal from './confirmation_modal';
|
import ConfirmationModal from './confirmation_modal';
|
||||||
import FocalPointModal from './focal_point_modal';
|
import FocalPointModal from './focal_point_modal';
|
||||||
|
import DeprecatedSettingsModal from './deprecated_settings_modal';
|
||||||
import {
|
import {
|
||||||
OnboardingModal,
|
OnboardingModal,
|
||||||
MuteModal,
|
MuteModal,
|
||||||
|
@ -40,6 +41,7 @@ const MODAL_COMPONENTS = {
|
||||||
'BLOCK': BlockModal,
|
'BLOCK': BlockModal,
|
||||||
'REPORT': ReportModal,
|
'REPORT': ReportModal,
|
||||||
'SETTINGS': SettingsModal,
|
'SETTINGS': SettingsModal,
|
||||||
|
'DEPRECATED_SETTINGS': () => Promise.resolve({ default: DeprecatedSettingsModal }),
|
||||||
'ACTIONS': () => Promise.resolve({ default: ActionsModal }),
|
'ACTIONS': () => Promise.resolve({ default: ActionsModal }),
|
||||||
'EMBED': EmbedModal,
|
'EMBED': EmbedModal,
|
||||||
'LIST_EDITOR': ListEditor,
|
'LIST_EDITOR': ListEditor,
|
||||||
|
|
|
@ -4,7 +4,6 @@ import { openModal } from 'flavours/glitch/actions/modal';
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
columns: state.getIn(['settings', 'columns']),
|
columns: state.getIn(['settings', 'columns']),
|
||||||
swipeToChangeColumns: state.getIn(['local_settings', 'swipe_to_change_columns']),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
|
|
@ -11,6 +11,7 @@ import { uploadCompose, resetCompose, changeComposeSpoilerness } from 'flavours/
|
||||||
import { expandHomeTimeline } from 'flavours/glitch/actions/timelines';
|
import { expandHomeTimeline } from 'flavours/glitch/actions/timelines';
|
||||||
import { expandNotifications, notificationsSetVisibility } from 'flavours/glitch/actions/notifications';
|
import { expandNotifications, notificationsSetVisibility } from 'flavours/glitch/actions/notifications';
|
||||||
import { fetchFilters } from 'flavours/glitch/actions/filters';
|
import { fetchFilters } from 'flavours/glitch/actions/filters';
|
||||||
|
import { fetchRules } from 'flavours/glitch/actions/rules';
|
||||||
import { clearHeight } from 'flavours/glitch/actions/height_cache';
|
import { clearHeight } from 'flavours/glitch/actions/height_cache';
|
||||||
import { synchronouslySubmitMarkers, submitMarkers, fetchMarkers } from 'flavours/glitch/actions/markers';
|
import { synchronouslySubmitMarkers, submitMarkers, fetchMarkers } from 'flavours/glitch/actions/markers';
|
||||||
import { WrappedSwitch, WrappedRoute } from 'flavours/glitch/util/react_router_helpers';
|
import { WrappedSwitch, WrappedRoute } from 'flavours/glitch/util/react_router_helpers';
|
||||||
|
@ -402,6 +403,7 @@ class UI extends React.Component {
|
||||||
this.props.dispatch(expandHomeTimeline());
|
this.props.dispatch(expandHomeTimeline());
|
||||||
this.props.dispatch(expandNotifications());
|
this.props.dispatch(expandNotifications());
|
||||||
setTimeout(() => this.props.dispatch(fetchFilters()), 500);
|
setTimeout(() => this.props.dispatch(fetchFilters()), 500);
|
||||||
|
setTimeout(() => this.props.dispatch(fetchRules()), 3000);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 216.41507 232.00976"
|
||||||
|
version="1.1"
|
||||||
|
id="svg6"
|
||||||
|
sodipodi:docname="logo_warn_glitch.svg"
|
||||||
|
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<defs
|
||||||
|
id="defs10" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview8"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="1.7951831"
|
||||||
|
inkscape:cx="-30.916067"
|
||||||
|
inkscape:cy="90.241493"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1011"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="32"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg6" />
|
||||||
|
<g
|
||||||
|
id="g2025">
|
||||||
|
<path
|
||||||
|
d="M211.80683 139.0875c-3.1825 16.36625-28.4925 34.2775-57.5625 37.74875-15.16 1.80875-30.0825 3.47125-45.99875 2.74125-26.0275-1.1925-46.565-6.2125-46.565-6.2125 0 2.53375.15625 4.94625.46875 7.2025 3.38375 25.68625 25.47 27.225 46.3925 27.9425 21.115.7225 39.91625-5.20625 39.91625-5.20625l.86875 19.09s-14.77 7.93125-41.08125 9.39c-14.50875.7975-32.52375-.365-53.50625-5.91875C9.23183 213.82 1.40558 165.31125.20808 116.09125c-.36375-14.61375-.14-28.39375-.14-39.91875 0-50.33 32.97625-65.0825 32.97625-65.0825C49.67058 3.45375 78.20308.2425 107.86433 0h.72875c29.66125.2425 58.21125 3.45375 74.8375 11.09 0 0 32.97625 14.7525 32.97625 65.0825 0 0 .4125 37.13375-4.6 62.915"
|
||||||
|
fill="#3088d4"
|
||||||
|
id="path2" />
|
||||||
|
<path
|
||||||
|
d="m 124.52893,137.75645 c 0,9.01375 -7.30875,16.32125 -16.3225,16.32125 -9.01375,0 -16.32125,-7.3075 -16.32125,-16.32125 0,-9.01375 7.3075,-16.3225 16.32125,-16.3225 9.01375,0 16.3225,7.30875 16.3225,16.3225"
|
||||||
|
fill="#ffffff"
|
||||||
|
id="path4"
|
||||||
|
sodipodi:nodetypes="csssc" />
|
||||||
|
<path
|
||||||
|
id="path1121"
|
||||||
|
d="m 108.20703,25.453125 c -9.013749,0 -16.322264,7.308516 -16.322264,16.322266 0,5.31808 2.555126,37.386806 6.492187,67.763669 4.100497,4.20028 15.890147,3.77063 19.660157,-0.01 3.9367,-30.375272 6.49219,-62.4364 6.49219,-67.753909 0,-9.01375 -7.30852,-16.322266 -16.32227,-16.322266 z"
|
||||||
|
style="fill:#ffffff"
|
||||||
|
sodipodi:nodetypes="ssccsss" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.4 KiB |
|
@ -1,4 +1,5 @@
|
||||||
import { ACCOUNT_IMPORT, ACCOUNTS_IMPORT } from '../actions/importer';
|
import { ACCOUNT_IMPORT, ACCOUNTS_IMPORT } from 'flavours/glitch/actions/importer';
|
||||||
|
import { ACCOUNT_REVEAL } from 'flavours/glitch/actions/accounts';
|
||||||
import { Map as ImmutableMap, fromJS } from 'immutable';
|
import { Map as ImmutableMap, fromJS } from 'immutable';
|
||||||
|
|
||||||
const initialState = ImmutableMap();
|
const initialState = ImmutableMap();
|
||||||
|
@ -10,6 +11,8 @@ const normalizeAccount = (state, account) => {
|
||||||
delete account.following_count;
|
delete account.following_count;
|
||||||
delete account.statuses_count;
|
delete account.statuses_count;
|
||||||
|
|
||||||
|
account.hidden = state.getIn([account.id, 'hidden']) === false ? false : account.limited;
|
||||||
|
|
||||||
return state.set(account.id, fromJS(account));
|
return state.set(account.id, fromJS(account));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -27,6 +30,8 @@ export default function accounts(state = initialState, action) {
|
||||||
return normalizeAccount(state, action.account);
|
return normalizeAccount(state, action.account);
|
||||||
case ACCOUNTS_IMPORT:
|
case ACCOUNTS_IMPORT:
|
||||||
return normalizeAccounts(state, action.accounts);
|
return normalizeAccounts(state, action.accounts);
|
||||||
|
case ACCOUNT_REVEAL:
|
||||||
|
return state.setIn([action.id, 'hidden'], false);
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,7 @@ import {
|
||||||
COMPOSE_SPOILERNESS_CHANGE,
|
COMPOSE_SPOILERNESS_CHANGE,
|
||||||
COMPOSE_SPOILER_TEXT_CHANGE,
|
COMPOSE_SPOILER_TEXT_CHANGE,
|
||||||
COMPOSE_VISIBILITY_CHANGE,
|
COMPOSE_VISIBILITY_CHANGE,
|
||||||
|
COMPOSE_LANGUAGE_CHANGE,
|
||||||
COMPOSE_CONTENT_TYPE_CHANGE,
|
COMPOSE_CONTENT_TYPE_CHANGE,
|
||||||
COMPOSE_EMOJI_INSERT,
|
COMPOSE_EMOJI_INSERT,
|
||||||
COMPOSE_UPLOAD_CHANGE_REQUEST,
|
COMPOSE_UPLOAD_CHANGE_REQUEST,
|
||||||
|
@ -100,6 +101,7 @@ const initialState = ImmutableMap({
|
||||||
}),
|
}),
|
||||||
default_privacy: 'public',
|
default_privacy: 'public',
|
||||||
default_sensitive: false,
|
default_sensitive: false,
|
||||||
|
default_language: 'en',
|
||||||
resetFileKey: Math.floor((Math.random() * 0x10000)),
|
resetFileKey: Math.floor((Math.random() * 0x10000)),
|
||||||
idempotencyKey: null,
|
idempotencyKey: null,
|
||||||
tagHistory: ImmutableList(),
|
tagHistory: ImmutableList(),
|
||||||
|
@ -175,7 +177,8 @@ function clearAll(state) {
|
||||||
map => map.mergeWith(overwrite, state.get('default_advanced_options'))
|
map => map.mergeWith(overwrite, state.get('default_advanced_options'))
|
||||||
);
|
);
|
||||||
map.set('privacy', state.get('default_privacy'));
|
map.set('privacy', state.get('default_privacy'));
|
||||||
map.set('sensitive', false);
|
map.set('sensitive', state.get('default_sensitive'));
|
||||||
|
map.set('language', state.get('default_language'));
|
||||||
map.update('media_attachments', list => list.clear());
|
map.update('media_attachments', list => list.clear());
|
||||||
map.set('poll', null);
|
map.set('poll', null);
|
||||||
map.set('idempotencyKey', uuid());
|
map.set('idempotencyKey', uuid());
|
||||||
|
@ -557,6 +560,7 @@ export default function compose(state = initialState, action) {
|
||||||
map.set('caretPosition', null);
|
map.set('caretPosition', null);
|
||||||
map.set('idempotencyKey', uuid());
|
map.set('idempotencyKey', uuid());
|
||||||
map.set('sensitive', action.status.get('sensitive'));
|
map.set('sensitive', action.status.get('sensitive'));
|
||||||
|
map.set('language', action.status.get('language'));
|
||||||
map.update(
|
map.update(
|
||||||
'advanced_options',
|
'advanced_options',
|
||||||
map => map.merge(new ImmutableMap({ do_not_federate }))
|
map => map.merge(new ImmutableMap({ do_not_federate }))
|
||||||
|
@ -589,6 +593,7 @@ export default function compose(state = initialState, action) {
|
||||||
map.set('caretPosition', null);
|
map.set('caretPosition', null);
|
||||||
map.set('idempotencyKey', uuid());
|
map.set('idempotencyKey', uuid());
|
||||||
map.set('sensitive', action.status.get('sensitive'));
|
map.set('sensitive', action.status.get('sensitive'));
|
||||||
|
map.set('language', action.status.get('language'));
|
||||||
|
|
||||||
if (action.spoiler_text.length > 0) {
|
if (action.spoiler_text.length > 0) {
|
||||||
map.set('spoiler', true);
|
map.set('spoiler', true);
|
||||||
|
@ -618,6 +623,8 @@ export default function compose(state = initialState, action) {
|
||||||
return state.updateIn(['poll', 'options'], options => options.delete(action.index));
|
return state.updateIn(['poll', 'options'], options => options.delete(action.index));
|
||||||
case COMPOSE_POLL_SETTINGS_CHANGE:
|
case COMPOSE_POLL_SETTINGS_CHANGE:
|
||||||
return state.update('poll', poll => poll.set('expires_in', action.expiresIn).set('multiple', action.isMultiple));
|
return state.update('poll', poll => poll.set('expires_in', action.expiresIn).set('multiple', action.isMultiple));
|
||||||
|
case COMPOSE_LANGUAGE_CHANGE:
|
||||||
|
return state.set('language', action.language);
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,25 +0,0 @@
|
||||||
import { Map as ImmutableMap, fromJS } from 'immutable';
|
|
||||||
import {
|
|
||||||
IDENTITY_PROOFS_ACCOUNT_FETCH_REQUEST,
|
|
||||||
IDENTITY_PROOFS_ACCOUNT_FETCH_SUCCESS,
|
|
||||||
IDENTITY_PROOFS_ACCOUNT_FETCH_FAIL,
|
|
||||||
} from '../actions/identity_proofs';
|
|
||||||
|
|
||||||
const initialState = ImmutableMap();
|
|
||||||
|
|
||||||
export default function identityProofsReducer(state = initialState, action) {
|
|
||||||
switch(action.type) {
|
|
||||||
case IDENTITY_PROOFS_ACCOUNT_FETCH_REQUEST:
|
|
||||||
return state.set('isLoading', true);
|
|
||||||
case IDENTITY_PROOFS_ACCOUNT_FETCH_FAIL:
|
|
||||||
return state.set('isLoading', false);
|
|
||||||
case IDENTITY_PROOFS_ACCOUNT_FETCH_SUCCESS:
|
|
||||||
return state.update(identity_proofs => identity_proofs.withMutations(map => {
|
|
||||||
map.set('isLoading', false);
|
|
||||||
map.set('loaded', true);
|
|
||||||
map.set(action.accountId, fromJS(action.identity_proofs));
|
|
||||||
}));
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -34,7 +34,6 @@ import conversations from './conversations';
|
||||||
import suggestions from './suggestions';
|
import suggestions from './suggestions';
|
||||||
import pinnedAccountsEditor from './pinned_accounts_editor';
|
import pinnedAccountsEditor from './pinned_accounts_editor';
|
||||||
import polls from './polls';
|
import polls from './polls';
|
||||||
import identity_proofs from './identity_proofs';
|
|
||||||
import trends from './trends';
|
import trends from './trends';
|
||||||
import announcements from './announcements';
|
import announcements from './announcements';
|
||||||
import markers from './markers';
|
import markers from './markers';
|
||||||
|
@ -73,7 +72,6 @@ const reducers = {
|
||||||
notifications,
|
notifications,
|
||||||
height_cache,
|
height_cache,
|
||||||
custom_emojis,
|
custom_emojis,
|
||||||
identity_proofs,
|
|
||||||
lists,
|
lists,
|
||||||
listEditor,
|
listEditor,
|
||||||
listAdder,
|
listAdder,
|
||||||
|
|
|
@ -3,13 +3,12 @@ import { Map as ImmutableMap } from 'immutable';
|
||||||
|
|
||||||
// Our imports.
|
// Our imports.
|
||||||
import { STORE_HYDRATE } from 'flavours/glitch/actions/store';
|
import { STORE_HYDRATE } from 'flavours/glitch/actions/store';
|
||||||
import { LOCAL_SETTING_CHANGE } from 'flavours/glitch/actions/local_settings';
|
import { LOCAL_SETTING_CHANGE, LOCAL_SETTING_DELETE } from 'flavours/glitch/actions/local_settings';
|
||||||
|
|
||||||
const initialState = ImmutableMap({
|
const initialState = ImmutableMap({
|
||||||
layout : 'auto',
|
layout : 'auto',
|
||||||
stretch : true,
|
stretch : true,
|
||||||
navbar_under : false,
|
navbar_under : false,
|
||||||
swipe_to_change_columns: true,
|
|
||||||
side_arm : 'none',
|
side_arm : 'none',
|
||||||
side_arm_reply_mode : 'keep',
|
side_arm_reply_mode : 'keep',
|
||||||
show_reply_count : false,
|
show_reply_count : false,
|
||||||
|
@ -26,7 +25,6 @@ const initialState = ImmutableMap({
|
||||||
tag_misleading_links: true,
|
tag_misleading_links: true,
|
||||||
rewrite_mentions: 'no',
|
rewrite_mentions: 'no',
|
||||||
content_warnings : ImmutableMap({
|
content_warnings : ImmutableMap({
|
||||||
auto_unfold : false,
|
|
||||||
filter : null,
|
filter : null,
|
||||||
}),
|
}),
|
||||||
collapsed : ImmutableMap({
|
collapsed : ImmutableMap({
|
||||||
|
@ -56,6 +54,13 @@ const initialState = ImmutableMap({
|
||||||
favicon_badge : false,
|
favicon_badge : false,
|
||||||
tab_badge : true,
|
tab_badge : true,
|
||||||
}),
|
}),
|
||||||
|
status_icons : ImmutableMap({
|
||||||
|
language: true,
|
||||||
|
reply: true,
|
||||||
|
local_only: true,
|
||||||
|
media: true,
|
||||||
|
visibility: true,
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const hydrate = (state, localSettings) => state.mergeDeep(localSettings);
|
const hydrate = (state, localSettings) => state.mergeDeep(localSettings);
|
||||||
|
@ -66,6 +71,8 @@ export default function localSettings(state = initialState, action) {
|
||||||
return hydrate(state, action.state.get('local_settings'));
|
return hydrate(state, action.state.get('local_settings'));
|
||||||
case LOCAL_SETTING_CHANGE:
|
case LOCAL_SETTING_CHANGE:
|
||||||
return state.setIn(action.key, action.value);
|
return state.setIn(action.key, action.value);
|
||||||
|
case LOCAL_SETTING_DELETE:
|
||||||
|
return state.deleteIn(action.key);
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue