Merge remote-tracking branch 'glitch-soc/main'
# Conflicts: # app/controllers/concerns/sign_in_token_authentication_concern.rb # app/javascript/mastodon/features/compose/components/action_bar.js # app/javascript/mastodon/features/compose/index.js
@ -79,6 +79,11 @@ module.exports = {
|
|||||||
'no-irregular-whitespace': 'error',
|
'no-irregular-whitespace': 'error',
|
||||||
'no-mixed-spaces-and-tabs': 'warn',
|
'no-mixed-spaces-and-tabs': 'warn',
|
||||||
'no-nested-ternary': 'warn',
|
'no-nested-ternary': 'warn',
|
||||||
|
'no-restricted-properties': [
|
||||||
|
'error',
|
||||||
|
{ property: 'substring', message: 'Use .slice instead of .substring.' },
|
||||||
|
{ property: 'substr', message: 'Use .slice instead of .substr.' },
|
||||||
|
],
|
||||||
'no-trailing-spaces': 'warn',
|
'no-trailing-spaces': 'warn',
|
||||||
'no-undef': 'error',
|
'no-undef': 'error',
|
||||||
'no-unreachable': 'error',
|
'no-unreachable': 'error',
|
||||||
|
95
CHANGELOG.md
@ -3,6 +3,101 @@ 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.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
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Add pagination for trending statuses in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/17976))
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Change e-mail notifications to only be sent when recipient is offline ([Gargron](https://github.com/mastodon/mastodon/pull/17984))
|
||||||
|
- Send e-mails for mentions and follows by default again
|
||||||
|
- But only when recipient does not have push notifications through an app
|
||||||
|
- Change `website` attribute to be nullable on `Application` entity in REST API ([rinsuki](https://github.com/mastodon/mastodon/pull/17962))
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
- Remove sign-in token authentication, instead send e-mail about new sign-in ([Gargron](https://github.com/mastodon/mastodon/pull/17970))
|
||||||
|
- You no longer need to enter a security code sent through e-mail
|
||||||
|
- Instead you get an e-mail about a new sign-in from an unfamiliar IP address
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix error resposes for `from` search prefix ([single-right-quote](https://github.com/mastodon/mastodon/pull/17963))
|
||||||
|
- Fix dangling language-specific trends ([Gargron](https://github.com/mastodon/mastodon/pull/17997))
|
||||||
|
- Fix extremely rare race condition when deleting a status or account ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17994))
|
||||||
|
- Fix trends returning less results per page when filtered in REST API ([Gargron](https://github.com/mastodon/mastodon/pull/17996))
|
||||||
|
- Fix pagination header on empty trends responses in REST API ([Gargron](https://github.com/mastodon/mastodon/pull/17986))
|
||||||
|
- Fix cookies secure flag being set when served over Tor ([Gargron](https://github.com/mastodon/mastodon/pull/17992))
|
||||||
|
- Fix migration error handling ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17991))
|
||||||
|
- Fix error when re-running some migrations if they get interrupted at the wrong moment ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17989))
|
||||||
|
- Fix potentially missing statuses when reconnecting to streaming API in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17981), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17987), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17980))
|
||||||
|
- Fix error when sending warning emails with custom text ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17983))
|
||||||
|
- Fix unset `SMTP_RETURN_PATH` environment variable causing e-mail not to send ([Gargron](https://github.com/mastodon/mastodon/pull/17982))
|
||||||
|
- Fix possible duplicate statuses in timelines in some edge cases in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17971))
|
||||||
|
- Fix spurious edits and require incoming edits to be explicitly marked as such ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17918))
|
||||||
|
- Fix error when encountering invalid pinned statuses ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17964))
|
||||||
|
- Fix inconsistency in error handling when removing a status ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17974))
|
||||||
|
- Fix admin API unconditionally requiring CSRF token ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17975))
|
||||||
|
- Fix trending tags endpoint missing `offset` param in REST API ([Gargron](https://github.com/mastodon/mastodon/pull/17973))
|
||||||
|
- Fix unusual number formatting in some locales ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17929))
|
||||||
|
- Fix `S3_FORCE_SINGLE_REQUEST` environment variable not working ([HolgerHuo](https://github.com/mastodon/mastodon/pull/17922))
|
||||||
|
- Fix failure to build assets with OpenSSL 3 ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17930))
|
||||||
|
- Fix PWA manifest using outdated routes ([HolgerHuo](https://github.com/mastodon/mastodon/pull/17921))
|
||||||
|
- Fix error when indexing statuses into Elasticsearch ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17912))
|
||||||
|
|
||||||
## [3.5.0] - 2022-03-30
|
## [3.5.0] - 2022-03-30
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
18
Gemfile
@ -1,7 +1,7 @@
|
|||||||
# 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'
|
||||||
@ -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'
|
||||||
@ -40,7 +40,7 @@ end
|
|||||||
gem 'net-ldap', '~> 0.17'
|
gem 'net-ldap', '~> 0.17'
|
||||||
gem 'omniauth-cas', '~> 2.0'
|
gem 'omniauth-cas', '~> 2.0'
|
||||||
gem 'omniauth-saml', '~> 1.10'
|
gem 'omniauth-saml', '~> 1.10'
|
||||||
gem 'gitlab-omniauth-openid-connect', '~>0.5.0', require: 'omniauth_openid_connect'
|
gem 'gitlab-omniauth-openid-connect', '~>0.9.1', require: 'omniauth_openid_connect'
|
||||||
gem 'omniauth', '~> 1.9'
|
gem 'omniauth', '~> 1.9'
|
||||||
gem 'omniauth-rails_csrf_protection', '~> 0.1'
|
gem 'omniauth-rails_csrf_protection', '~> 0.1'
|
||||||
|
|
||||||
@ -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'
|
||||||
@ -101,9 +101,9 @@ gem 'rdf-normalize', '~> 0.5'
|
|||||||
gem 'redcarpet', '~> 3.5'
|
gem 'redcarpet', '~> 3.5'
|
||||||
|
|
||||||
group :development, :test do
|
group :development, :test do
|
||||||
gem 'fabrication', '~> 2.27'
|
gem 'fabrication', '~> 2.28'
|
||||||
gem 'fuubar', '~> 2.5'
|
gem 'fuubar', '~> 2.5'
|
||||||
gem 'i18n-tasks', '~> 0.9', require: false
|
gem 'i18n-tasks', '~> 1.0', require: false
|
||||||
gem 'pry-byebug', '~> 3.9'
|
gem 'pry-byebug', '~> 3.9'
|
||||||
gem 'pry-rails', '~> 0.3'
|
gem 'pry-rails', '~> 0.3'
|
||||||
gem 'rspec-rails', '~> 5.1'
|
gem 'rspec-rails', '~> 5.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.28', 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
|
||||||
@ -148,7 +148,7 @@ group :development do
|
|||||||
end
|
end
|
||||||
|
|
||||||
group :production do
|
group :production do
|
||||||
gem 'lograge', '~> 0.11'
|
gem 'lograge', '~> 0.12'
|
||||||
end
|
end
|
||||||
|
|
||||||
gem 'concurrent-ruby', require: false
|
gem 'concurrent-ruby', require: false
|
||||||
|
218
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.5.1)
|
||||||
actionpack (= 6.1.5)
|
actionpack (= 6.1.5.1)
|
||||||
activesupport (= 6.1.5)
|
activesupport (= 6.1.5.1)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
websocket-driver (>= 0.6.1)
|
websocket-driver (>= 0.6.1)
|
||||||
actionmailbox (6.1.5)
|
actionmailbox (6.1.5.1)
|
||||||
actionpack (= 6.1.5)
|
actionpack (= 6.1.5.1)
|
||||||
activejob (= 6.1.5)
|
activejob (= 6.1.5.1)
|
||||||
activerecord (= 6.1.5)
|
activerecord (= 6.1.5.1)
|
||||||
activestorage (= 6.1.5)
|
activestorage (= 6.1.5.1)
|
||||||
activesupport (= 6.1.5)
|
activesupport (= 6.1.5.1)
|
||||||
mail (>= 2.7.1)
|
mail (>= 2.7.1)
|
||||||
actionmailer (6.1.5)
|
actionmailer (6.1.5.1)
|
||||||
actionpack (= 6.1.5)
|
actionpack (= 6.1.5.1)
|
||||||
actionview (= 6.1.5)
|
actionview (= 6.1.5.1)
|
||||||
activejob (= 6.1.5)
|
activejob (= 6.1.5.1)
|
||||||
activesupport (= 6.1.5)
|
activesupport (= 6.1.5.1)
|
||||||
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.5.1)
|
||||||
actionview (= 6.1.5)
|
actionview (= 6.1.5.1)
|
||||||
activesupport (= 6.1.5)
|
activesupport (= 6.1.5.1)
|
||||||
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.5.1)
|
||||||
actionpack (= 6.1.5)
|
actionpack (= 6.1.5.1)
|
||||||
activerecord (= 6.1.5)
|
activerecord (= 6.1.5.1)
|
||||||
activestorage (= 6.1.5)
|
activestorage (= 6.1.5.1)
|
||||||
activesupport (= 6.1.5)
|
activesupport (= 6.1.5.1)
|
||||||
nokogiri (>= 1.8.5)
|
nokogiri (>= 1.8.5)
|
||||||
actionview (6.1.5)
|
actionview (6.1.5.1)
|
||||||
activesupport (= 6.1.5)
|
activesupport (= 6.1.5.1)
|
||||||
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.5.1)
|
||||||
activesupport (= 6.1.5)
|
activesupport (= 6.1.5.1)
|
||||||
globalid (>= 0.3.6)
|
globalid (>= 0.3.6)
|
||||||
activemodel (6.1.5)
|
activemodel (6.1.5.1)
|
||||||
activesupport (= 6.1.5)
|
activesupport (= 6.1.5.1)
|
||||||
activerecord (6.1.5)
|
activerecord (6.1.5.1)
|
||||||
activemodel (= 6.1.5)
|
activemodel (= 6.1.5.1)
|
||||||
activesupport (= 6.1.5)
|
activesupport (= 6.1.5.1)
|
||||||
activestorage (6.1.5)
|
activestorage (6.1.5.1)
|
||||||
actionpack (= 6.1.5)
|
actionpack (= 6.1.5.1)
|
||||||
activejob (= 6.1.5)
|
activejob (= 6.1.5.1)
|
||||||
activerecord (= 6.1.5)
|
activerecord (= 6.1.5.1)
|
||||||
activesupport (= 6.1.5)
|
activesupport (= 6.1.5.1)
|
||||||
marcel (~> 1.0)
|
marcel (~> 1.0)
|
||||||
mini_mime (>= 1.1.0)
|
mini_mime (>= 1.1.0)
|
||||||
activesupport (6.1.5)
|
activesupport (6.1.5.1)
|
||||||
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,34 +81,42 @@ 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.582.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.113.2)
|
||||||
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)
|
||||||
coderay (>= 1.0.0)
|
coderay (>= 1.0.0)
|
||||||
erubi (>= 1.0.0)
|
erubi (>= 1.0.0)
|
||||||
rack (>= 0.9.0)
|
rack (>= 0.9.0)
|
||||||
|
better_html (1.0.16)
|
||||||
|
actionview (>= 4.0)
|
||||||
|
activesupport (>= 4.0)
|
||||||
|
ast (~> 2.0)
|
||||||
|
erubi (~> 1.4)
|
||||||
|
html_tokenizer (~> 0.0.6)
|
||||||
|
parser (>= 2.4)
|
||||||
|
smart_properties
|
||||||
bindata (2.4.10)
|
bindata (2.4.10)
|
||||||
binding_of_caller (1.0.0)
|
binding_of_caller (1.0.0)
|
||||||
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)
|
||||||
@ -195,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)
|
||||||
@ -208,10 +215,10 @@ 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.27.0)
|
fabrication (2.28.0)
|
||||||
faker (2.20.0)
|
faker (2.20.0)
|
||||||
i18n (>= 1.8.11, < 2)
|
i18n (>= 1.8.11, < 2)
|
||||||
faraday (1.9.3)
|
faraday (1.9.3)
|
||||||
@ -256,13 +263,13 @@ 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)
|
||||||
ruby-progressbar (~> 1.4)
|
ruby-progressbar (~> 1.4)
|
||||||
gitlab-omniauth-openid-connect (0.5.0)
|
gitlab-omniauth-openid-connect (0.9.1)
|
||||||
addressable (~> 2.7)
|
addressable (~> 2.7)
|
||||||
omniauth (~> 1.9)
|
omniauth (~> 1.9)
|
||||||
openid_connect (~> 1.2)
|
openid_connect (~> 1.2)
|
||||||
@ -278,12 +285,13 @@ GEM
|
|||||||
hamlit (>= 1.2.0)
|
hamlit (>= 1.2.0)
|
||||||
railties (>= 4.0.1)
|
railties (>= 4.0.1)
|
||||||
hashdiff (1.0.1)
|
hashdiff (1.0.1)
|
||||||
hashie (4.1.0)
|
hashie (5.0.0)
|
||||||
hcaptcha (7.1.0)
|
hcaptcha (7.1.0)
|
||||||
json
|
json
|
||||||
highline (2.0.3)
|
highline (2.0.3)
|
||||||
hiredis (0.6.3)
|
hiredis (0.6.3)
|
||||||
hkdf (0.3.0)
|
hkdf (0.3.0)
|
||||||
|
html_tokenizer (0.0.7)
|
||||||
htmlentities (4.3.4)
|
htmlentities (4.3.4)
|
||||||
http (5.0.4)
|
http (5.0.4)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
@ -300,9 +308,10 @@ 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 (0.9.37)
|
i18n-tasks (1.0.9)
|
||||||
activesupport (>= 4.0.2)
|
activesupport (>= 4.0.2)
|
||||||
ast (>= 2.1.0)
|
ast (>= 2.1.0)
|
||||||
|
better_html (~> 1.0)
|
||||||
erubi
|
erubi
|
||||||
highline (>= 2.0.0)
|
highline (>= 2.0.0)
|
||||||
i18n
|
i18n
|
||||||
@ -312,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)
|
||||||
@ -362,12 +371,12 @@ GEM
|
|||||||
llhttp-ffi (0.4.0)
|
llhttp-ffi (0.4.0)
|
||||||
ffi-compiler (~> 1.0)
|
ffi-compiler (~> 1.0)
|
||||||
rake (~> 13.0)
|
rake (~> 13.0)
|
||||||
lograge (0.11.2)
|
lograge (0.12.0)
|
||||||
actionpack (>= 4)
|
actionpack (>= 4)
|
||||||
activesupport (>= 4)
|
activesupport (>= 4)
|
||||||
railties (>= 4)
|
railties (>= 4)
|
||||||
request_store (~> 1.0)
|
request_store (~> 1.0)
|
||||||
loofah (2.15.0)
|
loofah (2.17.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)
|
||||||
@ -389,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)
|
||||||
@ -397,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.4)
|
||||||
mini_portile2 (~> 2.8.0)
|
mini_portile2 (~> 2.8.0)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nsa (0.2.8)
|
nsa (0.2.8)
|
||||||
@ -419,7 +428,7 @@ GEM
|
|||||||
omniauth-saml (1.10.3)
|
omniauth-saml (1.10.3)
|
||||||
omniauth (~> 1.3, >= 1.3.2)
|
omniauth (~> 1.3, >= 1.3.2)
|
||||||
ruby-saml (~> 1.9)
|
ruby-saml (~> 1.9)
|
||||||
openid_connect (1.2.0)
|
openid_connect (1.3.0)
|
||||||
activemodel
|
activemodel
|
||||||
attr_required (>= 1.0.0)
|
attr_required (>= 1.0.0)
|
||||||
json-jwt (>= 1.5.0)
|
json-jwt (>= 1.5.0)
|
||||||
@ -432,15 +441,15 @@ GEM
|
|||||||
openssl (2.2.0)
|
openssl (2.2.0)
|
||||||
openssl-signature_algorithm (0.4.0)
|
openssl-signature_algorithm (0.4.0)
|
||||||
orm_adapter (0.5.0)
|
orm_adapter (0.5.0)
|
||||||
ox (2.14.10)
|
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.4)
|
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)
|
||||||
@ -468,11 +477,11 @@ 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)
|
||||||
rack-oauth2 (1.16.0)
|
rack-oauth2 (1.19.0)
|
||||||
activesupport
|
activesupport
|
||||||
attr_required
|
attr_required
|
||||||
httpclient
|
httpclient
|
||||||
@ -482,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.5.1)
|
||||||
actioncable (= 6.1.5)
|
actioncable (= 6.1.5.1)
|
||||||
actionmailbox (= 6.1.5)
|
actionmailbox (= 6.1.5.1)
|
||||||
actionmailer (= 6.1.5)
|
actionmailer (= 6.1.5.1)
|
||||||
actionpack (= 6.1.5)
|
actionpack (= 6.1.5.1)
|
||||||
actiontext (= 6.1.5)
|
actiontext (= 6.1.5.1)
|
||||||
actionview (= 6.1.5)
|
actionview (= 6.1.5.1)
|
||||||
activejob (= 6.1.5)
|
activejob (= 6.1.5.1)
|
||||||
activemodel (= 6.1.5)
|
activemodel (= 6.1.5.1)
|
||||||
activerecord (= 6.1.5)
|
activerecord (= 6.1.5.1)
|
||||||
activestorage (= 6.1.5)
|
activestorage (= 6.1.5.1)
|
||||||
activesupport (= 6.1.5)
|
activesupport (= 6.1.5.1)
|
||||||
bundler (>= 1.15.0)
|
bundler (>= 1.15.0)
|
||||||
railties (= 6.1.5)
|
railties (= 6.1.5.1)
|
||||||
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)
|
||||||
@ -511,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.5.1)
|
||||||
actionpack (= 6.1.5)
|
actionpack (= 6.1.5.1)
|
||||||
activesupport (= 6.1.5)
|
activesupport (= 6.1.5.1)
|
||||||
method_source
|
method_source
|
||||||
rake (>= 12.2)
|
rake (>= 12.2)
|
||||||
thor (~> 1.0)
|
thor (~> 1.0)
|
||||||
@ -527,8 +536,8 @@ 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.3.1)
|
||||||
request_store (1.5.0)
|
request_store (1.5.1)
|
||||||
rack (>= 1.4)
|
rack (>= 1.4)
|
||||||
responders (3.0.1)
|
responders (3.0.1)
|
||||||
actionpack (>= 5.0)
|
actionpack (>= 5.0)
|
||||||
@ -545,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)
|
||||||
@ -562,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.28.2)
|
||||||
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
|
||||||
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.17.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)
|
||||||
@ -600,14 +609,12 @@ GEM
|
|||||||
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.15)
|
sidekiq-unique-jobs (7.1.21)
|
||||||
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)
|
||||||
@ -623,6 +630,7 @@ GEM
|
|||||||
simplecov_json_formatter (~> 0.1)
|
simplecov_json_formatter (~> 0.1)
|
||||||
simplecov-html (0.12.3)
|
simplecov-html (0.12.3)
|
||||||
simplecov_json_formatter (0.1.2)
|
simplecov_json_formatter (0.1.2)
|
||||||
|
smart_properties (1.17.0)
|
||||||
sprockets (3.7.2)
|
sprockets (3.7.2)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
rack (> 1, < 3)
|
rack (> 1, < 3)
|
||||||
@ -635,10 +643,10 @@ 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.2.0)
|
swd (1.3.0)
|
||||||
activesupport (>= 3)
|
activesupport (>= 3)
|
||||||
attr_required (>= 0.0.5)
|
attr_required (>= 0.0.5)
|
||||||
httpclient (>= 2.4)
|
httpclient (>= 2.4)
|
||||||
@ -648,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)
|
||||||
@ -694,7 +700,7 @@ GEM
|
|||||||
safety_net_attestation (~> 0.4.0)
|
safety_net_attestation (~> 0.4.0)
|
||||||
securecompare (~> 1.0)
|
securecompare (~> 1.0)
|
||||||
tpm-key_attestation (~> 0.9.0)
|
tpm-key_attestation (~> 0.9.0)
|
||||||
webfinger (1.1.0)
|
webfinger (1.2.0)
|
||||||
activesupport
|
activesupport
|
||||||
httpclient (>= 2.4)
|
httpclient (>= 2.4)
|
||||||
webmock (3.14.0)
|
webmock (3.14.0)
|
||||||
@ -730,7 +736,7 @@ DEPENDENCIES
|
|||||||
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)
|
||||||
@ -753,14 +759,14 @@ DEPENDENCIES
|
|||||||
doorkeeper (~> 5.5)
|
doorkeeper (~> 5.5)
|
||||||
dotenv-rails (~> 2.7)
|
dotenv-rails (~> 2.7)
|
||||||
ed25519 (~> 1.3)
|
ed25519 (~> 1.3)
|
||||||
fabrication (~> 2.27)
|
fabrication (~> 2.28)
|
||||||
faker (~> 2.20)
|
faker (~> 2.20)
|
||||||
fast_blank (~> 1.0)
|
fast_blank (~> 1.0)
|
||||||
fastimage
|
fastimage
|
||||||
fog-core (<= 2.1.0)
|
fog-core (<= 2.1.0)
|
||||||
fog-openstack (~> 0.3)
|
fog-openstack (~> 0.3)
|
||||||
fuubar (~> 2.5)
|
fuubar (~> 2.5)
|
||||||
gitlab-omniauth-openid-connect (~> 0.5.0)
|
gitlab-omniauth-openid-connect (~> 0.9.1)
|
||||||
hamlit-rails (~> 0.2)
|
hamlit-rails (~> 0.2)
|
||||||
hcaptcha (~> 7.1)
|
hcaptcha (~> 7.1)
|
||||||
hiredis (~> 0.6)
|
hiredis (~> 0.6)
|
||||||
@ -768,7 +774,7 @@ DEPENDENCIES
|
|||||||
http (~> 5.0)
|
http (~> 5.0)
|
||||||
http_accept_language (~> 2.1)
|
http_accept_language (~> 2.1)
|
||||||
httplog (~> 1.5.0)
|
httplog (~> 1.5.0)
|
||||||
i18n-tasks (~> 0.9)
|
i18n-tasks (~> 1.0)
|
||||||
idn-ruby
|
idn-ruby
|
||||||
json-ld
|
json-ld
|
||||||
json-ld-preloaded (~> 3.2)
|
json-ld-preloaded (~> 3.2)
|
||||||
@ -777,7 +783,7 @@ DEPENDENCIES
|
|||||||
letter_opener (~> 1.8)
|
letter_opener (~> 1.8)
|
||||||
letter_opener_web (~> 2.0)
|
letter_opener_web (~> 2.0)
|
||||||
link_header (~> 0.0)
|
link_header (~> 0.0)
|
||||||
lograge (~> 0.11)
|
lograge (~> 0.12)
|
||||||
makara (~> 0.5)
|
makara (~> 0.5)
|
||||||
mario-redis-lock (~> 1.2)
|
mario-redis-lock (~> 1.2)
|
||||||
memory_profiler
|
memory_profiler
|
||||||
@ -819,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.28)
|
||||||
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)
|
||||||
@ -834,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)
|
||||||
|
@ -12,6 +12,7 @@ A "vulnerability in Mastodon" is a vulnerability in the code distributed through
|
|||||||
|
|
||||||
| Version | Supported |
|
| Version | Supported |
|
||||||
| ------- | ------------------ |
|
| ------- | ------------------ |
|
||||||
|
| 3.5.x | Yes |
|
||||||
| 3.4.x | Yes |
|
| 3.4.x | Yes |
|
||||||
| 3.3.x | Yes |
|
| 3.3.x | Yes |
|
||||||
| < 3.3 | No |
|
| < 3.3 | No |
|
||||||
|
@ -55,6 +55,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'
|
||||||
|
@ -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
|
||||||
|
@ -1,27 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module Admin
|
|
||||||
class SignInTokenAuthenticationsController < BaseController
|
|
||||||
before_action :set_target_user
|
|
||||||
|
|
||||||
def create
|
|
||||||
authorize @user, :enable_sign_in_token_auth?
|
|
||||||
@user.update(skip_sign_in_token: false)
|
|
||||||
log_action :enable_sign_in_token_auth, @user
|
|
||||||
redirect_to admin_account_path(@user.account_id)
|
|
||||||
end
|
|
||||||
|
|
||||||
def destroy
|
|
||||||
authorize @user, :disable_sign_in_token_auth?
|
|
||||||
@user.update(skip_sign_in_token: true)
|
|
||||||
log_action :disable_sign_in_token_auth, @user
|
|
||||||
redirect_to admin_account_path(@user.account_id)
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def set_target_user
|
|
||||||
@user = User.find(params[:user_id])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
@ -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
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Api::V1::Admin::AccountActionsController < Api::BaseController
|
class Api::V1::Admin::AccountActionsController < Api::BaseController
|
||||||
protect_from_forgery with: :exception
|
|
||||||
|
|
||||||
before_action -> { authorize_if_got_token! :'admin:write', :'admin:write:accounts' }
|
before_action -> { authorize_if_got_token! :'admin:write', :'admin:write:accounts' }
|
||||||
before_action :require_staff!
|
before_action :require_staff!
|
||||||
before_action :set_account
|
before_action :set_account
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Api::V1::Admin::AccountsController < Api::BaseController
|
class Api::V1::Admin::AccountsController < Api::BaseController
|
||||||
protect_from_forgery with: :exception
|
|
||||||
|
|
||||||
include Authorization
|
include Authorization
|
||||||
include AccountableConcern
|
include AccountableConcern
|
||||||
|
|
||||||
@ -67,8 +65,9 @@ class Api::V1::Admin::AccountsController < Api::BaseController
|
|||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
authorize @account, :destroy?
|
authorize @account, :destroy?
|
||||||
|
json = render_to_body json: @account, serializer: REST::Admin::AccountSerializer
|
||||||
Admin::AccountDeletionWorker.perform_async(@account.id)
|
Admin::AccountDeletionWorker.perform_async(@account.id)
|
||||||
render json: @account, serializer: REST::Admin::AccountSerializer
|
render json: json
|
||||||
end
|
end
|
||||||
|
|
||||||
def unsensitive
|
def unsensitive
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Api::V1::Admin::DimensionsController < Api::BaseController
|
class Api::V1::Admin::DimensionsController < Api::BaseController
|
||||||
protect_from_forgery with: :exception
|
|
||||||
|
|
||||||
before_action -> { authorize_if_got_token! :'admin:read' }
|
before_action -> { authorize_if_got_token! :'admin:read' }
|
||||||
before_action :require_staff!
|
before_action :require_staff!
|
||||||
before_action :set_dimensions
|
before_action :set_dimensions
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Api::V1::Admin::MeasuresController < Api::BaseController
|
class Api::V1::Admin::MeasuresController < Api::BaseController
|
||||||
protect_from_forgery with: :exception
|
|
||||||
|
|
||||||
before_action -> { authorize_if_got_token! :'admin:read' }
|
before_action -> { authorize_if_got_token! :'admin:read' }
|
||||||
before_action :require_staff!
|
before_action :require_staff!
|
||||||
before_action :set_measures
|
before_action :set_measures
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Api::V1::Admin::ReportsController < Api::BaseController
|
class Api::V1::Admin::ReportsController < Api::BaseController
|
||||||
protect_from_forgery with: :exception
|
|
||||||
|
|
||||||
include Authorization
|
include Authorization
|
||||||
include AccountableConcern
|
include AccountableConcern
|
||||||
|
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Api::V1::Admin::RetentionController < Api::BaseController
|
class Api::V1::Admin::RetentionController < Api::BaseController
|
||||||
protect_from_forgery with: :exception
|
|
||||||
|
|
||||||
before_action -> { authorize_if_got_token! :'admin:read' }
|
before_action -> { authorize_if_got_token! :'admin:read' }
|
||||||
before_action :require_staff!
|
before_action :require_staff!
|
||||||
before_action :set_cohorts
|
before_action :set_cohorts
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Api::V1::Admin::Trends::LinksController < Api::BaseController
|
class Api::V1::Admin::Trends::LinksController < Api::BaseController
|
||||||
protect_from_forgery with: :exception
|
|
||||||
|
|
||||||
before_action -> { authorize_if_got_token! :'admin:read' }
|
before_action -> { authorize_if_got_token! :'admin:read' }
|
||||||
before_action :require_staff!
|
before_action :require_staff!
|
||||||
before_action :set_links
|
before_action :set_links
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Api::V1::Admin::Trends::StatusesController < Api::BaseController
|
class Api::V1::Admin::Trends::StatusesController < Api::BaseController
|
||||||
protect_from_forgery with: :exception
|
|
||||||
|
|
||||||
before_action -> { authorize_if_got_token! :'admin:read' }
|
before_action -> { authorize_if_got_token! :'admin:read' }
|
||||||
before_action :require_staff!
|
before_action :require_staff!
|
||||||
before_action :set_statuses
|
before_action :set_statuses
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Api::V1::Admin::Trends::TagsController < Api::BaseController
|
class Api::V1::Admin::Trends::TagsController < Api::BaseController
|
||||||
protect_from_forgery with: :exception
|
|
||||||
|
|
||||||
before_action -> { authorize_if_got_token! :'admin:read' }
|
before_action -> { authorize_if_got_token! :'admin:read' }
|
||||||
before_action :require_staff!
|
before_action :require_staff!
|
||||||
before_action :set_tags
|
before_action :set_tags
|
||||||
|
@ -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)
|
||||||
)
|
)
|
||||||
|
@ -79,10 +79,12 @@ class Api::V1::StatusesController < Api::BaseController
|
|||||||
authorize @status, :destroy?
|
authorize @status, :destroy?
|
||||||
|
|
||||||
@status.discard
|
@status.discard
|
||||||
RemovalWorker.perform_async(@status.id, { 'redraft' => true })
|
|
||||||
@status.account.statuses_count = @status.account.statuses_count - 1
|
@status.account.statuses_count = @status.account.statuses_count - 1
|
||||||
|
json = render_to_body json: @status, serializer: REST::StatusSerializer, source_requested: true
|
||||||
|
|
||||||
render json: @status, serializer: REST::StatusSerializer, source_requested: true
|
RemovalWorker.perform_async(@status.id, { 'redraft' => true })
|
||||||
|
|
||||||
|
render json: json
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
@ -36,13 +36,17 @@ class Api::V1::Trends::LinksController < Api::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def next_path
|
def next_path
|
||||||
api_v1_trends_links_url pagination_params(offset: offset_param + limit_param(DEFAULT_LINKS_LIMIT))
|
api_v1_trends_links_url pagination_params(offset: offset_param + limit_param(DEFAULT_LINKS_LIMIT)) if records_continue?
|
||||||
end
|
end
|
||||||
|
|
||||||
def prev_path
|
def prev_path
|
||||||
api_v1_trends_links_url pagination_params(offset: offset_param - limit_param(DEFAULT_LINKS_LIMIT)) if offset_param > limit_param(DEFAULT_LINKS_LIMIT)
|
api_v1_trends_links_url pagination_params(offset: offset_param - limit_param(DEFAULT_LINKS_LIMIT)) if offset_param > limit_param(DEFAULT_LINKS_LIMIT)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def records_continue?
|
||||||
|
@links.size == limit_param(DEFAULT_LINKS_LIMIT)
|
||||||
|
end
|
||||||
|
|
||||||
def offset_param
|
def offset_param
|
||||||
params[:offset].to_i
|
params[:offset].to_i
|
||||||
end
|
end
|
||||||
|
@ -36,7 +36,7 @@ class Api::V1::Trends::StatusesController < Api::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def next_path
|
def next_path
|
||||||
api_v1_trends_statuses_url pagination_params(offset: offset_param + limit_param(DEFAULT_STATUSES_LIMIT))
|
api_v1_trends_statuses_url pagination_params(offset: offset_param + limit_param(DEFAULT_STATUSES_LIMIT)) if records_continue?
|
||||||
end
|
end
|
||||||
|
|
||||||
def prev_path
|
def prev_path
|
||||||
@ -46,4 +46,8 @@ class Api::V1::Trends::StatusesController < Api::BaseController
|
|||||||
def offset_param
|
def offset_param
|
||||||
params[:offset].to_i
|
params[:offset].to_i
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def records_continue?
|
||||||
|
@statuses.size == limit_param(DEFAULT_STATUSES_LIMIT)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -16,7 +16,7 @@ class Api::V1::Trends::TagsController < Api::BaseController
|
|||||||
def set_tags
|
def set_tags
|
||||||
@tags = begin
|
@tags = begin
|
||||||
if Setting.trends
|
if Setting.trends
|
||||||
Trends.tags.query.allowed.limit(limit_param(DEFAULT_TAGS_LIMIT))
|
Trends.tags.query.allowed.offset(offset_param).limit(limit_param(DEFAULT_TAGS_LIMIT))
|
||||||
else
|
else
|
||||||
[]
|
[]
|
||||||
end
|
end
|
||||||
@ -32,7 +32,7 @@ class Api::V1::Trends::TagsController < Api::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def next_path
|
def next_path
|
||||||
api_v1_trends_tags_url pagination_params(offset: offset_param + limit_param(DEFAULT_TAGS_LIMIT))
|
api_v1_trends_tags_url pagination_params(offset: offset_param + limit_param(DEFAULT_TAGS_LIMIT)) if records_continue?
|
||||||
end
|
end
|
||||||
|
|
||||||
def prev_path
|
def prev_path
|
||||||
@ -42,4 +42,8 @@ class Api::V1::Trends::TagsController < Api::BaseController
|
|||||||
def offset_param
|
def offset_param
|
||||||
params[:offset].to_i
|
params[:offset].to_i
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def records_continue?
|
||||||
|
@tags.size == limit_param(DEFAULT_TAGS_LIMIT)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -11,6 +11,10 @@ class Api::V2::SearchController < Api::BaseController
|
|||||||
def index
|
def index
|
||||||
@search = Search.new(search_results)
|
@search = Search.new(search_results)
|
||||||
render json: @search, serializer: REST::SearchSerializer
|
render json: @search, serializer: REST::SearchSerializer
|
||||||
|
rescue Mastodon::SyntaxError
|
||||||
|
unprocessable_entity
|
||||||
|
rescue ActiveRecord::RecordNotFound
|
||||||
|
not_found
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
@ -10,7 +10,6 @@ class Auth::SessionsController < Devise::SessionsController
|
|||||||
prepend_before_action :set_pack
|
prepend_before_action :set_pack
|
||||||
|
|
||||||
include TwoFactorAuthenticationConcern
|
include TwoFactorAuthenticationConcern
|
||||||
include SignInTokenAuthenticationConcern
|
|
||||||
|
|
||||||
before_action :set_instance_presenter, only: [:new]
|
before_action :set_instance_presenter, only: [:new]
|
||||||
before_action :set_body_classes
|
before_action :set_body_classes
|
||||||
@ -68,7 +67,7 @@ class Auth::SessionsController < Devise::SessionsController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def user_params
|
def user_params
|
||||||
params.require(:user).permit(:email, :password, :otp_attempt, :sign_in_token_attempt, credential: {})
|
params.require(:user).permit(:email, :password, :otp_attempt, credential: {})
|
||||||
end
|
end
|
||||||
|
|
||||||
def after_sign_in_path_for(resource)
|
def after_sign_in_path_for(resource)
|
||||||
@ -148,6 +147,12 @@ class Auth::SessionsController < Devise::SessionsController
|
|||||||
ip: request.remote_ip,
|
ip: request.remote_ip,
|
||||||
user_agent: request.user_agent
|
user_agent: request.user_agent
|
||||||
)
|
)
|
||||||
|
|
||||||
|
UserMailer.suspicious_sign_in(user, request.remote_ip, request.user_agent, Time.now.utc).deliver_later! if suspicious_sign_in?(user)
|
||||||
|
end
|
||||||
|
|
||||||
|
def suspicious_sign_in?(user)
|
||||||
|
SuspiciousSignInDetector.new(user).suspicious?(request)
|
||||||
end
|
end
|
||||||
|
|
||||||
def on_authentication_failure(user, security_measure, failure_reason)
|
def on_authentication_failure(user, security_measure, failure_reason)
|
||||||
|
@ -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
|
||||||
|
@ -1,57 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module SignInTokenAuthenticationConcern
|
|
||||||
extend ActiveSupport::Concern
|
|
||||||
|
|
||||||
included do
|
|
||||||
prepend_before_action :authenticate_with_sign_in_token, if: :sign_in_token_required?, only: [:create]
|
|
||||||
end
|
|
||||||
|
|
||||||
def sign_in_token_required?
|
|
||||||
find_user&.suspicious_sign_in?(request.remote_ip)
|
|
||||||
end
|
|
||||||
|
|
||||||
def valid_sign_in_token_attempt?(user)
|
|
||||||
Devise.secure_compare(user.sign_in_token, user_params[:sign_in_token_attempt])
|
|
||||||
end
|
|
||||||
|
|
||||||
def authenticate_with_sign_in_token
|
|
||||||
if user_params[:email].present?
|
|
||||||
user = self.resource = find_user_from_params
|
|
||||||
prompt_for_sign_in_token(user) if user&.external_or_valid_password?(user_params[:password])
|
|
||||||
elsif session[:attempt_user_id]
|
|
||||||
user = self.resource = User.find_by(id: session[:attempt_user_id])
|
|
||||||
return if user.nil?
|
|
||||||
|
|
||||||
if session[:attempt_user_updated_at] != user.updated_at.to_s
|
|
||||||
restart_session
|
|
||||||
elsif user_params.key?(:sign_in_token_attempt)
|
|
||||||
authenticate_with_sign_in_token_attempt(user)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def authenticate_with_sign_in_token_attempt(user)
|
|
||||||
if valid_sign_in_token_attempt?(user)
|
|
||||||
clear_attempt_from_session
|
|
||||||
sign_in(user)
|
|
||||||
else
|
|
||||||
flash.now[:alert] = I18n.t('users.invalid_sign_in_token')
|
|
||||||
prompt_for_sign_in_token(user)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def prompt_for_sign_in_token(user)
|
|
||||||
if user.sign_in_token_expired?
|
|
||||||
user.generate_sign_in_token && user.save
|
|
||||||
UserMailer.sign_in_token(user, request.remote_ip, request.user_agent, Time.now.utc.to_s).deliver_later!
|
|
||||||
end
|
|
||||||
|
|
||||||
set_attempt_session(user)
|
|
||||||
use_pack 'auth'
|
|
||||||
|
|
||||||
@body_classes = 'lighter'
|
|
||||||
|
|
||||||
set_locale { render :sign_in_token }
|
|
||||||
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,7 @@
|
|||||||
class MediaProxyController < ApplicationController
|
class MediaProxyController < ApplicationController
|
||||||
include RoutingHelper
|
include RoutingHelper
|
||||||
include Authorization
|
include Authorization
|
||||||
|
include Redisable
|
||||||
|
|
||||||
skip_before_action :store_current_location
|
skip_before_action :store_current_location
|
||||||
skip_before_action :require_functional!
|
skip_before_action :require_functional!
|
||||||
@ -45,7 +46,7 @@ class MediaProxyController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def lock_options
|
def lock_options
|
||||||
{ redis: Redis.current, key: "media_download:#{params[:id]}", autorelease: 15.minutes.seconds }
|
{ redis: redis, key: "media_download:#{params[:id]}", autorelease: 15.minutes.seconds }
|
||||||
end
|
end
|
||||||
|
|
||||||
def reject_media?
|
def reject_media?
|
||||||
|
@ -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,7 @@
|
|||||||
|
|
||||||
class Settings::ExportsController < Settings::BaseController
|
class Settings::ExportsController < Settings::BaseController
|
||||||
include Authorization
|
include Authorization
|
||||||
|
include Redisable
|
||||||
|
|
||||||
skip_before_action :require_functional!
|
skip_before_action :require_functional!
|
||||||
|
|
||||||
@ -28,6 +29,6 @@ class Settings::ExportsController < Settings::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def lock_options
|
def lock_options
|
||||||
{ redis: Redis.current, key: "backup:#{current_user.id}" }
|
{ redis: redis, key: "backup:#{current_user.id}" }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -57,7 +57,8 @@ class Settings::PreferencesController < Settings::BaseController
|
|||||||
:setting_use_pending_items,
|
:setting_use_pending_items,
|
||||||
:setting_trends,
|
:setting_trends,
|
||||||
:setting_crop_images,
|
:setting_crop_images,
|
||||||
notification_emails: %i(follow follow_request reblog favourite mention digest report pending_account trending_tag trending_link trending_status),
|
:setting_always_send_emails,
|
||||||
|
notification_emails: %i(follow follow_request reblog favourite mention digest report pending_account trending_tag trending_link trending_status appeal),
|
||||||
interactions: %i(must_be_follower must_be_following must_be_following_dm)
|
interactions: %i(must_be_follower must_be_following must_be_following_dm)
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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,14 @@ 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);
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -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' });
|
||||||
|
@ -7,6 +7,10 @@ import {
|
|||||||
expandHomeTimeline,
|
expandHomeTimeline,
|
||||||
connectTimeline,
|
connectTimeline,
|
||||||
disconnectTimeline,
|
disconnectTimeline,
|
||||||
|
fillHomeTimelineGaps,
|
||||||
|
fillPublicTimelineGaps,
|
||||||
|
fillCommunityTimelineGaps,
|
||||||
|
fillListTimelineGaps,
|
||||||
} from './timelines';
|
} from './timelines';
|
||||||
import { updateNotifications, expandNotifications } from './notifications';
|
import { updateNotifications, expandNotifications } from './notifications';
|
||||||
import { updateConversations } from './conversations';
|
import { updateConversations } from './conversations';
|
||||||
@ -35,6 +39,7 @@ const randomUpTo = max =>
|
|||||||
* @param {Object.<string, string>} params
|
* @param {Object.<string, string>} params
|
||||||
* @param {Object} options
|
* @param {Object} options
|
||||||
* @param {function(Function, Function): void} [options.fallback]
|
* @param {function(Function, Function): void} [options.fallback]
|
||||||
|
* @param {function(): void} [options.fillGaps]
|
||||||
* @param {function(object): boolean} [options.accept]
|
* @param {function(object): boolean} [options.accept]
|
||||||
* @return {function(): void}
|
* @return {function(): void}
|
||||||
*/
|
*/
|
||||||
@ -61,6 +66,10 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
|
|||||||
clearTimeout(pollingId);
|
clearTimeout(pollingId);
|
||||||
pollingId = null;
|
pollingId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options.fillGaps) {
|
||||||
|
dispatch(options.fillGaps());
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onDisconnect() {
|
onDisconnect() {
|
||||||
@ -119,7 +128,7 @@ const refreshHomeTimelineAndNotification = (dispatch, done) => {
|
|||||||
* @return {function(): void}
|
* @return {function(): void}
|
||||||
*/
|
*/
|
||||||
export const connectUserStream = () =>
|
export const connectUserStream = () =>
|
||||||
connectTimelineStream('home', 'user', {}, { fallback: refreshHomeTimelineAndNotification });
|
connectTimelineStream('home', 'user', {}, { fallback: refreshHomeTimelineAndNotification, fillGaps: fillHomeTimelineGaps });
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Object} options
|
* @param {Object} options
|
||||||
@ -127,7 +136,7 @@ export const connectUserStream = () =>
|
|||||||
* @return {function(): void}
|
* @return {function(): void}
|
||||||
*/
|
*/
|
||||||
export const connectCommunityStream = ({ onlyMedia } = {}) =>
|
export const connectCommunityStream = ({ onlyMedia } = {}) =>
|
||||||
connectTimelineStream(`community${onlyMedia ? ':media' : ''}`, `public:local${onlyMedia ? ':media' : ''}`);
|
connectTimelineStream(`community${onlyMedia ? ':media' : ''}`, `public:local${onlyMedia ? ':media' : ''}`, {}, { fillGaps: () => (fillCommunityTimelineGaps({ onlyMedia })) });
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Object} options
|
* @param {Object} options
|
||||||
@ -137,7 +146,7 @@ export const connectCommunityStream = ({ onlyMedia } = {}) =>
|
|||||||
* @return {function(): void}
|
* @return {function(): void}
|
||||||
*/
|
*/
|
||||||
export const connectPublicStream = ({ onlyMedia, onlyRemote, allowLocalOnly } = {}) =>
|
export const connectPublicStream = ({ onlyMedia, onlyRemote, allowLocalOnly } = {}) =>
|
||||||
connectTimelineStream(`public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`, `public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`);
|
connectTimelineStream(`public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`, `public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`, {}, { fillGaps: () => fillPublicTimelineGaps({ onlyMedia, onlyRemote, allowLocalOnly }) });
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} columnId
|
* @param {string} columnId
|
||||||
@ -160,4 +169,4 @@ export const connectDirectStream = () =>
|
|||||||
* @return {function(): void}
|
* @return {function(): void}
|
||||||
*/
|
*/
|
||||||
export const connectListStream = listId =>
|
export const connectListStream = listId =>
|
||||||
connectTimelineStream(`list:${listId}`, 'list', { list: listId });
|
connectTimelineStream(`list:${listId}`, 'list', { list: listId }, { fillGaps: () => fillListTimelineGaps(listId) });
|
||||||
|
@ -138,6 +138,22 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function fillTimelineGaps(timelineId, path, params = {}, done = noOp) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const timeline = getState().getIn(['timelines', timelineId], ImmutableMap());
|
||||||
|
const items = timeline.get('items');
|
||||||
|
const nullIndexes = items.map((statusId, index) => statusId === null ? index : null);
|
||||||
|
const gaps = nullIndexes.map(index => index > 0 ? items.get(index - 1) : null);
|
||||||
|
|
||||||
|
// Only expand at most two gaps to avoid doing too many requests
|
||||||
|
done = gaps.take(2).reduce((done, maxId) => {
|
||||||
|
return (() => dispatch(expandTimeline(timelineId, path, { ...params, maxId }, done)));
|
||||||
|
}, done);
|
||||||
|
|
||||||
|
done();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export const expandHomeTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId }, done);
|
export const expandHomeTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId }, done);
|
||||||
export const expandPublicTimeline = ({ maxId, onlyMedia, onlyRemote, allowLocalOnly } = {}, done = noOp) => expandTimeline(`public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, allow_local_only: !!allowLocalOnly, max_id: maxId, only_media: !!onlyMedia }, done);
|
export const expandPublicTimeline = ({ maxId, onlyMedia, onlyRemote, allowLocalOnly } = {}, done = noOp) => expandTimeline(`public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, allow_local_only: !!allowLocalOnly, max_id: maxId, only_media: !!onlyMedia }, done);
|
||||||
export const expandCommunityTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia }, done);
|
export const expandCommunityTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia }, done);
|
||||||
@ -156,6 +172,11 @@ export const expandHashtagTimeline = (hashtag, { maxId, tags, local } =
|
|||||||
}, done);
|
}, done);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const fillHomeTimelineGaps = (done = noOp) => fillTimelineGaps('home', '/api/v1/timelines/home', {}, done);
|
||||||
|
export const fillPublicTimelineGaps = ({ onlyMedia, onlyRemote, allowLocalOnly } = {}, done = noOp) => fillTimelineGaps(`public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, only_media: !!onlyMedia, allow_local_only: !!allowLocalOnly }, done);
|
||||||
|
export const fillCommunityTimelineGaps = ({ onlyMedia } = {}, done = noOp) => fillTimelineGaps(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, only_media: !!onlyMedia }, done);
|
||||||
|
export const fillListTimelineGaps = (id, done = noOp) => fillTimelineGaps(`list:${id}`, `/api/v1/timelines/list/${id}`, {}, done);
|
||||||
|
|
||||||
export function expandTimelineRequest(timeline, isLoadingMore) {
|
export function expandTimelineRequest(timeline, isLoadingMore) {
|
||||||
return {
|
return {
|
||||||
type: TIMELINE_EXPAND_REQUEST,
|
type: TIMELINE_EXPAND_REQUEST,
|
||||||
@ -199,6 +220,7 @@ export function connectTimeline(timeline) {
|
|||||||
return {
|
return {
|
||||||
type: TIMELINE_CONNECT,
|
type: TIMELINE_CONNECT,
|
||||||
timeline,
|
timeline,
|
||||||
|
usePendingItems: preferPendingItems,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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),
|
||||||
|
@ -581,10 +581,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 +681,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 = {
|
||||||
|
@ -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
|
||||||
|
@ -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' },
|
||||||
|
@ -128,8 +128,8 @@ export default class Header extends ImmutablePureComponent {
|
|||||||
|
|
||||||
{!hideTabs && (
|
{!hideTabs && (
|
||||||
<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>
|
||||||
)}
|
)}
|
||||||
|
@ -44,7 +44,7 @@ const mapStateToProps = (state, { params: { acct, id }, withReplies = false }) =
|
|||||||
};
|
};
|
||||||
|
|
||||||
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 = {
|
||||||
@ -156,7 +156,7 @@ class AccountTimeline extends ImmutablePureComponent {
|
|||||||
} 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;
|
||||||
|
@ -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'>
|
||||||
|
@ -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' },
|
||||||
});
|
});
|
||||||
|
@ -72,10 +72,10 @@ 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>
|
||||||
);
|
);
|
||||||
@ -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}/>)}
|
||||||
|
|
||||||
|
@ -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}>
|
||||||
|
@ -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>
|
||||||
|
@ -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 (
|
||||||
|
@ -24,7 +24,7 @@ const trim = (text, len) => {
|
|||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
return text.substring(0, cut) + (text.length > len ? '…' : '');
|
return text.slice(0, cut) + (text.length > len ? '…' : '');
|
||||||
};
|
};
|
||||||
|
|
||||||
const domParser = new DOMParser();
|
const domParser = new DOMParser();
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
@ -234,7 +234,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' />}
|
||||||
|
@ -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>
|
||||||
|
@ -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 () {
|
||||||
|
@ -90,7 +90,7 @@ export const fileNameFromURL = str => {
|
|||||||
const pathname = url.pathname;
|
const pathname = url.pathname;
|
||||||
const index = pathname.lastIndexOf('/');
|
const index = pathname.lastIndexOf('/');
|
||||||
|
|
||||||
return pathname.substring(index + 1);
|
return pathname.slice(index + 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default @injectIntl
|
export default @injectIntl
|
||||||
|
@ -16,7 +16,7 @@ import {
|
|||||||
ACCOUNT_MUTE_SUCCESS,
|
ACCOUNT_MUTE_SUCCESS,
|
||||||
ACCOUNT_UNFOLLOW_SUCCESS,
|
ACCOUNT_UNFOLLOW_SUCCESS,
|
||||||
} from 'flavours/glitch/actions/accounts';
|
} from 'flavours/glitch/actions/accounts';
|
||||||
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
|
import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable';
|
||||||
import compareId from 'flavours/glitch/util/compare_id';
|
import compareId from 'flavours/glitch/util/compare_id';
|
||||||
|
|
||||||
const initialState = ImmutableMap();
|
const initialState = ImmutableMap();
|
||||||
@ -32,6 +32,13 @@ const initialTimeline = ImmutableMap({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, isLoadingRecent, usePendingItems) => {
|
const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, isLoadingRecent, usePendingItems) => {
|
||||||
|
// This method is pretty tricky because:
|
||||||
|
// - existing items in the timeline might be out of order
|
||||||
|
// - the existing timeline may have gaps, most often explicitly noted with a `null` item
|
||||||
|
// - ideally, we don't want it to reorder existing items of the timeline
|
||||||
|
// - `statuses` may include items that are already included in the timeline
|
||||||
|
// - this function can be called either to fill in a gap, or load newer items
|
||||||
|
|
||||||
return state.update(timeline, initialTimeline, map => map.withMutations(mMap => {
|
return state.update(timeline, initialTimeline, map => map.withMutations(mMap => {
|
||||||
mMap.set('isLoading', false);
|
mMap.set('isLoading', false);
|
||||||
mMap.set('isPartial', isPartial);
|
mMap.set('isPartial', isPartial);
|
||||||
@ -45,15 +52,43 @@ const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, is
|
|||||||
|
|
||||||
mMap.update(usePendingItems ? 'pendingItems' : 'items', ImmutableList(), oldIds => {
|
mMap.update(usePendingItems ? 'pendingItems' : 'items', ImmutableList(), oldIds => {
|
||||||
const newIds = statuses.map(status => status.get('id'));
|
const newIds = statuses.map(status => status.get('id'));
|
||||||
const lastIndex = oldIds.findLastIndex(id => id !== null && compareId(id, newIds.last()) >= 0) + 1;
|
|
||||||
const firstIndex = oldIds.take(lastIndex).findLastIndex(id => id !== null && compareId(id, newIds.first()) > 0);
|
|
||||||
|
|
||||||
if (firstIndex < 0) {
|
// Now this gets tricky, as we don't necessarily know for sure where the gap to fill is
|
||||||
return (isPartial ? newIds.unshift(null) : newIds).concat(oldIds.skip(lastIndex));
|
// and some items in the timeline may not be properly ordered.
|
||||||
|
|
||||||
|
// However, we know that `newIds.last()` is the oldest item that was requested and that
|
||||||
|
// there is no “hole” between `newIds.last()` and `newIds.first()`.
|
||||||
|
|
||||||
|
// First, find the furthest (if properly sorted, oldest) item in the timeline that is
|
||||||
|
// newer than the oldest fetched one, as it's most likely that it delimits the gap.
|
||||||
|
// Start the gap *after* that item.
|
||||||
|
const lastIndex = oldIds.findLastIndex(id => id !== null && compareId(id, newIds.last()) >= 0) + 1;
|
||||||
|
|
||||||
|
// Then, try to find the furthest (if properly sorted, oldest) item in the timeline that
|
||||||
|
// is newer than the most recent fetched one, as it delimits a section comprised of only
|
||||||
|
// items older or within `newIds` (or that were deleted from the server, so should be removed
|
||||||
|
// anyway).
|
||||||
|
// Stop the gap *after* that item.
|
||||||
|
const firstIndex = oldIds.take(lastIndex).findLastIndex(id => id !== null && compareId(id, newIds.first()) > 0) + 1;
|
||||||
|
|
||||||
|
let insertedIds = ImmutableOrderedSet(newIds).withMutations(insertedIds => {
|
||||||
|
// It is possible, though unlikely, that the slice we are replacing contains items older
|
||||||
|
// than the elements we got from the API. Get them and add them back at the back of the
|
||||||
|
// slice.
|
||||||
|
const olderIds = oldIds.slice(firstIndex, lastIndex).filter(id => id !== null && compareId(id, newIds.last()) < 0);
|
||||||
|
insertedIds.union(olderIds);
|
||||||
|
|
||||||
|
// Make sure we aren't inserting duplicates
|
||||||
|
insertedIds.subtract(oldIds.take(firstIndex), oldIds.skip(lastIndex));
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
// Finally, insert a gap marker if the data is marked as partial by the server
|
||||||
|
if (isPartial && (firstIndex === 0 || oldIds.get(firstIndex - 1) !== null)) {
|
||||||
|
insertedIds = insertedIds.unshift(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
return oldIds.take(firstIndex + 1).concat(
|
return oldIds.take(firstIndex).concat(
|
||||||
isPartial && oldIds.get(firstIndex) !== null ? newIds.unshift(null) : newIds,
|
insertedIds,
|
||||||
oldIds.skip(lastIndex),
|
oldIds.skip(lastIndex),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -142,6 +177,17 @@ const updateTop = (state, timeline, top) => {
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const reconnectTimeline = (state, usePendingItems) => {
|
||||||
|
if (state.get('online')) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
return state.withMutations(mMap => {
|
||||||
|
mMap.update(usePendingItems ? 'pendingItems' : 'items', items => items.first() ? items.unshift(null) : items);
|
||||||
|
mMap.set('online', true);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export default function timelines(state = initialState, action) {
|
export default function timelines(state = initialState, action) {
|
||||||
switch(action.type) {
|
switch(action.type) {
|
||||||
case TIMELINE_LOAD_PENDING:
|
case TIMELINE_LOAD_PENDING:
|
||||||
@ -167,7 +213,7 @@ export default function timelines(state = initialState, action) {
|
|||||||
case TIMELINE_SCROLL_TOP:
|
case TIMELINE_SCROLL_TOP:
|
||||||
return updateTop(state, action.timeline, action.top);
|
return updateTop(state, action.timeline, action.top);
|
||||||
case TIMELINE_CONNECT:
|
case TIMELINE_CONNECT:
|
||||||
return state.update(action.timeline, initialTimeline, map => map.set('online', true));
|
return state.update(action.timeline, initialTimeline, map => reconnectTimeline(map, action.usePendingItems));
|
||||||
case TIMELINE_DISCONNECT:
|
case TIMELINE_DISCONNECT:
|
||||||
return state.update(
|
return state.update(
|
||||||
action.timeline,
|
action.timeline,
|
||||||
|
@ -579,7 +579,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
& > span {
|
& > span {
|
||||||
max-width: 400px;
|
max-width: 500px;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
|
@ -1566,7 +1566,7 @@ button.icon-button.active i.fa-retweet {
|
|||||||
.loading-bar {
|
.loading-bar {
|
||||||
background-color: $ui-highlight-color;
|
background-color: $ui-highlight-color;
|
||||||
height: 3px;
|
height: 3px;
|
||||||
position: absolute;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
|
@ -233,6 +233,10 @@
|
|||||||
.columns-area__panels__pane--compositional {
|
.columns-area__panels__pane--compositional {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.with-fab .scrollable .item-list:last-child {
|
||||||
|
padding-bottom: 5.25rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 600px + (285px * 1) + (10px * 1)) {
|
@media screen and (min-width: 600px + (285px * 1) + (10px * 1)) {
|
||||||
|
@ -124,7 +124,7 @@ function search(value, { emojisToShowFilter, maxResults, include, exclude, custo
|
|||||||
for (let id in aPool) {
|
for (let id in aPool) {
|
||||||
let emoji = aPool[id],
|
let emoji = aPool[id],
|
||||||
{ search } = emoji,
|
{ search } = emoji,
|
||||||
sub = value.substr(0, length),
|
sub = value.slice(0, length),
|
||||||
subIndex = search.indexOf(sub);
|
subIndex = search.indexOf(sub);
|
||||||
|
|
||||||
if (subIndex !== -1) {
|
if (subIndex !== -1) {
|
||||||
|
BIN
app/javascript/images/alert_badge.png
Normal file
After Width: | Height: | Size: 622 B |
BIN
app/javascript/images/icon_bookmarks.png
Normal file
After Width: | Height: | Size: 418 B |
BIN
app/javascript/images/icon_developers.png
Normal file
After Width: | Height: | Size: 488 B |
BIN
app/javascript/images/icon_direct.png
Normal file
After Width: | Height: | Size: 390 B |
BIN
app/javascript/images/icon_docs.png
Normal file
After Width: | Height: | Size: 452 B |
BIN
app/javascript/images/icon_domain_blocks.png
Normal file
After Width: | Height: | Size: 589 B |
BIN
app/javascript/images/icon_invite.png
Normal file
After Width: | Height: | Size: 457 B |
BIN
app/javascript/images/icon_mobile_apps.png
Normal file
After Width: | Height: | Size: 650 B |
BIN
app/javascript/images/icon_notifications.png
Normal file
After Width: | Height: | Size: 282 B |
BIN
app/javascript/images/icon_profile_directory.png
Normal file
After Width: | Height: | Size: 340 B |
BIN
app/javascript/images/icon_tos.png
Normal file
After Width: | Height: | Size: 498 B |
@ -58,7 +58,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' });
|
||||||
|
@ -7,6 +7,10 @@ import {
|
|||||||
expandHomeTimeline,
|
expandHomeTimeline,
|
||||||
connectTimeline,
|
connectTimeline,
|
||||||
disconnectTimeline,
|
disconnectTimeline,
|
||||||
|
fillHomeTimelineGaps,
|
||||||
|
fillPublicTimelineGaps,
|
||||||
|
fillCommunityTimelineGaps,
|
||||||
|
fillListTimelineGaps,
|
||||||
} from './timelines';
|
} from './timelines';
|
||||||
import { updateNotifications, expandNotifications } from './notifications';
|
import { updateNotifications, expandNotifications } from './notifications';
|
||||||
import { updateConversations } from './conversations';
|
import { updateConversations } from './conversations';
|
||||||
@ -35,6 +39,7 @@ const randomUpTo = max =>
|
|||||||
* @param {Object.<string, string>} params
|
* @param {Object.<string, string>} params
|
||||||
* @param {Object} options
|
* @param {Object} options
|
||||||
* @param {function(Function, Function): void} [options.fallback]
|
* @param {function(Function, Function): void} [options.fallback]
|
||||||
|
* @param {function(): void} [options.fillGaps]
|
||||||
* @param {function(object): boolean} [options.accept]
|
* @param {function(object): boolean} [options.accept]
|
||||||
* @return {function(): void}
|
* @return {function(): void}
|
||||||
*/
|
*/
|
||||||
@ -61,6 +66,10 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
|
|||||||
clearTimeout(pollingId);
|
clearTimeout(pollingId);
|
||||||
pollingId = null;
|
pollingId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options.fillGaps) {
|
||||||
|
dispatch(options.fillGaps());
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onDisconnect() {
|
onDisconnect() {
|
||||||
@ -119,7 +128,7 @@ const refreshHomeTimelineAndNotification = (dispatch, done) => {
|
|||||||
* @return {function(): void}
|
* @return {function(): void}
|
||||||
*/
|
*/
|
||||||
export const connectUserStream = () =>
|
export const connectUserStream = () =>
|
||||||
connectTimelineStream('home', 'user', {}, { fallback: refreshHomeTimelineAndNotification });
|
connectTimelineStream('home', 'user', {}, { fallback: refreshHomeTimelineAndNotification, fillGaps: fillHomeTimelineGaps });
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Object} options
|
* @param {Object} options
|
||||||
@ -127,7 +136,7 @@ export const connectUserStream = () =>
|
|||||||
* @return {function(): void}
|
* @return {function(): void}
|
||||||
*/
|
*/
|
||||||
export const connectCommunityStream = ({ onlyMedia } = {}) =>
|
export const connectCommunityStream = ({ onlyMedia } = {}) =>
|
||||||
connectTimelineStream(`community${onlyMedia ? ':media' : ''}`, `public:local${onlyMedia ? ':media' : ''}`);
|
connectTimelineStream(`community${onlyMedia ? ':media' : ''}`, `public:local${onlyMedia ? ':media' : ''}`, {}, { fillGaps: () => (fillCommunityTimelineGaps({ onlyMedia })) });
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Object} options
|
* @param {Object} options
|
||||||
@ -136,7 +145,7 @@ export const connectCommunityStream = ({ onlyMedia } = {}) =>
|
|||||||
* @return {function(): void}
|
* @return {function(): void}
|
||||||
*/
|
*/
|
||||||
export const connectPublicStream = ({ onlyMedia, onlyRemote } = {}) =>
|
export const connectPublicStream = ({ onlyMedia, onlyRemote } = {}) =>
|
||||||
connectTimelineStream(`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, `public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`);
|
connectTimelineStream(`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, `public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, {}, { fillGaps: () => fillPublicTimelineGaps({ onlyMedia, onlyRemote }) });
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} columnId
|
* @param {string} columnId
|
||||||
@ -159,4 +168,4 @@ export const connectDirectStream = () =>
|
|||||||
* @return {function(): void}
|
* @return {function(): void}
|
||||||
*/
|
*/
|
||||||
export const connectListStream = listId =>
|
export const connectListStream = listId =>
|
||||||
connectTimelineStream(`list:${listId}`, 'list', { list: listId });
|
connectTimelineStream(`list:${listId}`, 'list', { list: listId }, { fillGaps: () => fillListTimelineGaps(listId) });
|
||||||
|
@ -124,6 +124,22 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function fillTimelineGaps(timelineId, path, params = {}, done = noOp) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const timeline = getState().getIn(['timelines', timelineId], ImmutableMap());
|
||||||
|
const items = timeline.get('items');
|
||||||
|
const nullIndexes = items.map((statusId, index) => statusId === null ? index : null);
|
||||||
|
const gaps = nullIndexes.map(index => index > 0 ? items.get(index - 1) : null);
|
||||||
|
|
||||||
|
// Only expand at most two gaps to avoid doing too many requests
|
||||||
|
done = gaps.take(2).reduce((done, maxId) => {
|
||||||
|
return (() => dispatch(expandTimeline(timelineId, path, { ...params, maxId }, done)));
|
||||||
|
}, done);
|
||||||
|
|
||||||
|
done();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export const expandHomeTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId }, done);
|
export const expandHomeTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId }, done);
|
||||||
export const expandPublicTimeline = ({ maxId, onlyMedia, onlyRemote } = {}, done = noOp) => expandTimeline(`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, max_id: maxId, only_media: !!onlyMedia }, done);
|
export const expandPublicTimeline = ({ maxId, onlyMedia, onlyRemote } = {}, done = noOp) => expandTimeline(`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, max_id: maxId, only_media: !!onlyMedia }, done);
|
||||||
export const expandCommunityTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia }, done);
|
export const expandCommunityTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia }, done);
|
||||||
@ -141,6 +157,11 @@ export const expandHashtagTimeline = (hashtag, { maxId, tags, local } =
|
|||||||
}, done);
|
}, done);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const fillHomeTimelineGaps = (done = noOp) => fillTimelineGaps('home', '/api/v1/timelines/home', {}, done);
|
||||||
|
export const fillPublicTimelineGaps = ({ onlyMedia, onlyRemote } = {}, done = noOp) => fillTimelineGaps(`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, only_media: !!onlyMedia }, done);
|
||||||
|
export const fillCommunityTimelineGaps = ({ onlyMedia } = {}, done = noOp) => fillTimelineGaps(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, only_media: !!onlyMedia }, done);
|
||||||
|
export const fillListTimelineGaps = (id, done = noOp) => fillTimelineGaps(`list:${id}`, `/api/v1/timelines/list/${id}`, {}, done);
|
||||||
|
|
||||||
export function expandTimelineRequest(timeline, isLoadingMore) {
|
export function expandTimelineRequest(timeline, isLoadingMore) {
|
||||||
return {
|
return {
|
||||||
type: TIMELINE_EXPAND_REQUEST,
|
type: TIMELINE_EXPAND_REQUEST,
|
||||||
@ -184,6 +205,7 @@ export function connectTimeline(timeline) {
|
|||||||
return {
|
return {
|
||||||
type: TIMELINE_CONNECT,
|
type: TIMELINE_CONNECT,
|
||||||
timeline,
|
timeline,
|
||||||
|
usePendingItems: preferPendingItems,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import api from '../api';
|
import api, { getLinks } from '../api';
|
||||||
import { importFetchedStatuses } from './importer';
|
import { importFetchedStatuses } from './importer';
|
||||||
|
|
||||||
export const TRENDS_TAGS_FETCH_REQUEST = 'TRENDS_TAGS_FETCH_REQUEST';
|
export const TRENDS_TAGS_FETCH_REQUEST = 'TRENDS_TAGS_FETCH_REQUEST';
|
||||||
@ -13,6 +13,10 @@ export const TRENDS_STATUSES_FETCH_REQUEST = 'TRENDS_STATUSES_FETCH_REQUEST';
|
|||||||
export const TRENDS_STATUSES_FETCH_SUCCESS = 'TRENDS_STATUSES_FETCH_SUCCESS';
|
export const TRENDS_STATUSES_FETCH_SUCCESS = 'TRENDS_STATUSES_FETCH_SUCCESS';
|
||||||
export const TRENDS_STATUSES_FETCH_FAIL = 'TRENDS_STATUSES_FETCH_FAIL';
|
export const TRENDS_STATUSES_FETCH_FAIL = 'TRENDS_STATUSES_FETCH_FAIL';
|
||||||
|
|
||||||
|
export const TRENDS_STATUSES_EXPAND_REQUEST = 'TRENDS_STATUSES_EXPAND_REQUEST';
|
||||||
|
export const TRENDS_STATUSES_EXPAND_SUCCESS = 'TRENDS_STATUSES_EXPAND_SUCCESS';
|
||||||
|
export const TRENDS_STATUSES_EXPAND_FAIL = 'TRENDS_STATUSES_EXPAND_FAIL';
|
||||||
|
|
||||||
export const fetchTrendingHashtags = () => (dispatch, getState) => {
|
export const fetchTrendingHashtags = () => (dispatch, getState) => {
|
||||||
dispatch(fetchTrendingHashtagsRequest());
|
dispatch(fetchTrendingHashtagsRequest());
|
||||||
|
|
||||||
@ -68,11 +72,16 @@ export const fetchTrendingLinksFail = error => ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const fetchTrendingStatuses = () => (dispatch, getState) => {
|
export const fetchTrendingStatuses = () => (dispatch, getState) => {
|
||||||
|
if (getState().getIn(['status_lists', 'trending', 'isLoading'])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
dispatch(fetchTrendingStatusesRequest());
|
dispatch(fetchTrendingStatusesRequest());
|
||||||
|
|
||||||
api(getState).get('/api/v1/trends/statuses').then(({ data }) => {
|
api(getState).get('/api/v1/trends/statuses').then(response => {
|
||||||
dispatch(importFetchedStatuses(data));
|
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||||
dispatch(fetchTrendingStatusesSuccess(data));
|
dispatch(importFetchedStatuses(response.data));
|
||||||
|
dispatch(fetchTrendingStatusesSuccess(response.data, next ? next.uri : null));
|
||||||
}).catch(err => dispatch(fetchTrendingStatusesFail(err)));
|
}).catch(err => dispatch(fetchTrendingStatusesFail(err)));
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -81,9 +90,10 @@ export const fetchTrendingStatusesRequest = () => ({
|
|||||||
skipLoading: true,
|
skipLoading: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const fetchTrendingStatusesSuccess = statuses => ({
|
export const fetchTrendingStatusesSuccess = (statuses, next) => ({
|
||||||
type: TRENDS_STATUSES_FETCH_SUCCESS,
|
type: TRENDS_STATUSES_FETCH_SUCCESS,
|
||||||
statuses,
|
statuses,
|
||||||
|
next,
|
||||||
skipLoading: true,
|
skipLoading: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -93,3 +103,37 @@ export const fetchTrendingStatusesFail = error => ({
|
|||||||
skipLoading: true,
|
skipLoading: true,
|
||||||
skipAlert: true,
|
skipAlert: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
export const expandTrendingStatuses = () => (dispatch, getState) => {
|
||||||
|
const url = getState().getIn(['status_lists', 'trending', 'next'], null);
|
||||||
|
|
||||||
|
if (url === null || getState().getIn(['status_lists', 'trending', 'isLoading'])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(expandTrendingStatusesRequest());
|
||||||
|
|
||||||
|
api(getState).get(url).then(response => {
|
||||||
|
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||||
|
dispatch(importFetchedStatuses(response.data));
|
||||||
|
dispatch(expandTrendingStatusesSuccess(response.data, next ? next.uri : null));
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch(expandTrendingStatusesFail(error));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const expandTrendingStatusesRequest = () => ({
|
||||||
|
type: TRENDS_STATUSES_EXPAND_REQUEST,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const expandTrendingStatusesSuccess = (statuses, next) => ({
|
||||||
|
type: TRENDS_STATUSES_EXPAND_SUCCESS,
|
||||||
|
statuses,
|
||||||
|
next,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const expandTrendingStatusesFail = error => ({
|
||||||
|
type: TRENDS_STATUSES_EXPAND_FAIL,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
@ -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),
|
||||||
|
@ -56,7 +56,7 @@ const messages = defineMessages({
|
|||||||
public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
|
public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
|
||||||
unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' },
|
unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' },
|
||||||
private_short: { id: 'privacy.private.short', defaultMessage: 'Followers-only' },
|
private_short: { id: 'privacy.private.short', defaultMessage: 'Followers-only' },
|
||||||
direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' },
|
direct_short: { id: 'privacy.direct.short', defaultMessage: 'Mentioned people only' },
|
||||||
edited: { id: 'status.edited', defaultMessage: 'Edited {date}' },
|
edited: { id: 'status.edited', defaultMessage: 'Edited {date}' },
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -349,7 +349,7 @@ class Status extends ImmutablePureComponent {
|
|||||||
prepend = (
|
prepend = (
|
||||||
<div className='status__prepend'>
|
<div className='status__prepend'>
|
||||||
<div className='status__prepend-icon-wrapper'><Icon id='thumb-tack' className='status__prepend-icon' fixedWidth /></div>
|
<div className='status__prepend-icon-wrapper'><Icon id='thumb-tack' className='status__prepend-icon' fixedWidth /></div>
|
||||||
<FormattedMessage id='status.pinned' defaultMessage='Pinned toot' />
|
<FormattedMessage id='status.pinned' defaultMessage='Pinned post' />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') {
|
} else if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') {
|
||||||
@ -468,7 +468,7 @@ class Status extends ImmutablePureComponent {
|
|||||||
'public': { icon: 'globe', text: intl.formatMessage(messages.public_short) },
|
'public': { icon: 'globe', text: intl.formatMessage(messages.public_short) },
|
||||||
'unlisted': { icon: 'unlock', text: intl.formatMessage(messages.unlisted_short) },
|
'unlisted': { icon: 'unlock', text: intl.formatMessage(messages.unlisted_short) },
|
||||||
'private': { icon: 'lock', text: intl.formatMessage(messages.private_short) },
|
'private': { icon: 'lock', text: intl.formatMessage(messages.private_short) },
|
||||||
'direct': { icon: 'envelope', text: intl.formatMessage(messages.direct_short) },
|
'direct': { icon: 'at', text: intl.formatMessage(messages.direct_short) },
|
||||||
};
|
};
|
||||||
|
|
||||||
const visibilityIcon = visibilityIconInfo[status.get('visibility')];
|
const visibilityIcon = visibilityIconInfo[status.get('visibility')];
|
||||||
|
@ -38,7 +38,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' },
|
||||||
|
@ -121,8 +121,8 @@ export default class Header extends ImmutablePureComponent {
|
|||||||
|
|
||||||
{!hideTabs && (
|
{!hideTabs && (
|
||||||
<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 and replies' /></NavLink>
|
<NavLink exact to={`/@${account.get('acct')}/with_replies`}><FormattedMessage id='account.posts_with_replies' defaultMessage='Posts and 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>
|
||||||
)}
|
)}
|
||||||
|
@ -45,7 +45,7 @@ const mapStateToProps = (state, { params: { acct, id }, withReplies = false }) =
|
|||||||
};
|
};
|
||||||
|
|
||||||
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 = {
|
||||||
@ -156,7 +156,7 @@ class AccountTimeline extends ImmutablePureComponent {
|
|||||||
} 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;
|
||||||
|
@ -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} label={intl.formatMessage(messages.heading)}>
|
<Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.heading)}>
|
||||||
|
@ -5,18 +5,18 @@ import DropdownMenuContainer from '../../../containers/dropdown_menu_container';
|
|||||||
import { defineMessages, injectIntl } from 'react-intl';
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
edit_profile : { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
|
edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
|
||||||
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' },
|
||||||
lists : { id: 'navigation_bar.lists', defaultMessage: 'Lists' },
|
lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' },
|
||||||
blocks : { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' },
|
blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' },
|
||||||
domain_blocks : { id: 'navigation_bar.domain_blocks', defaultMessage: 'Hidden domains' },
|
domain_blocks: { id: 'navigation_bar.domain_blocks', defaultMessage: 'Hidden domains' },
|
||||||
mutes : { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' },
|
mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' },
|
||||||
filters : { id: 'navigation_bar.filters', defaultMessage: 'Muted words' },
|
filters: { id: 'navigation_bar.filters', defaultMessage: 'Muted words' },
|
||||||
logout : { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
|
logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
|
||||||
bookmarks : { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' },
|
bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' },
|
||||||
});
|
});
|
||||||
|
|
||||||
export default @injectIntl
|
export default @injectIntl
|
||||||
@ -30,7 +30,7 @@ class ActionBar extends React.PureComponent {
|
|||||||
|
|
||||||
handleLogout = () => {
|
handleLogout = () => {
|
||||||
this.props.onLogout();
|
this.props.onLogout();
|
||||||
};
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { intl } = this.props;
|
const { intl } = this.props;
|
||||||
|
@ -11,12 +11,12 @@ import Icon from 'mastodon/components/icon';
|
|||||||
|
|
||||||
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: 'Mentioned people only' },
|
||||||
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' },
|
||||||
});
|
});
|
||||||
@ -242,7 +242,7 @@ class PrivacyDropdown extends React.PureComponent {
|
|||||||
|
|
||||||
if (!this.props.noDirect) {
|
if (!this.props.noDirect) {
|
||||||
this.options.push(
|
this.options.push(
|
||||||
{ icon: 'envelope', value: 'direct', text: formatMessage(messages.direct_short), meta: formatMessage(messages.direct_long) },
|
{ icon: 'at', value: 'direct', text: formatMessage(messages.direct_short), meta: formatMessage(messages.direct_long) },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -91,7 +91,7 @@ class SearchResults extends ImmutablePureComponent {
|
|||||||
count += results.get('statuses').size;
|
count += results.get('statuses').size;
|
||||||
statuses = (
|
statuses = (
|
||||||
<div className='search-results__section'>
|
<div 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 key={statusId} id={statusId} />)}
|
{results.get('statuses').map(statusId => <StatusContainer key={statusId} id={statusId} />)}
|
||||||
|
|
||||||
@ -101,10 +101,10 @@ 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 = (
|
||||||
<div className='search-results__section'>
|
<div 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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -42,13 +42,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.' /> <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.' /> <a href='/terms' target='_blank'><FormattedMessage id='compose_form.direct_message_warning_learn_more' defaultMessage='Learn more' /></a>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -19,20 +19,20 @@ import Icon from 'mastodon/components/icon';
|
|||||||
import { logOut } from 'mastodon/utils/log_out';
|
import { logOut } from 'mastodon/utils/log_out';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
start : { id: 'getting_started.heading', defaultMessage: 'Getting started' },
|
start: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
|
||||||
home_timeline: { id: 'tabs_bar.home', defaultMessage: 'Home' },
|
home_timeline: { id: 'tabs_bar.home', defaultMessage: 'Home' },
|
||||||
notifications: { id: 'tabs_bar.notifications', defaultMessage: 'Notifications' },
|
notifications: { id: 'tabs_bar.notifications', defaultMessage: 'Notifications' },
|
||||||
public : { id: 'navigation_bar.public_timeline', defaultMessage: 'Federated timeline' },
|
public: { id: 'navigation_bar.public_timeline', defaultMessage: 'Federated timeline' },
|
||||||
community : { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' },
|
community: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' },
|
||||||
preferences : { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
|
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
|
||||||
logout : { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
|
logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
|
||||||
compose : { id: 'navigation_bar.compose', defaultMessage: 'Compose new toot' },
|
compose: { id: 'navigation_bar.compose', defaultMessage: 'Compose new post' },
|
||||||
logoutMessage: { id: 'confirmations.logout.message', defaultMessage: 'Are you sure you want to log out?' },
|
logoutMessage: { id: 'confirmations.logout.message', defaultMessage: 'Are you sure you want to log out?' },
|
||||||
logoutConfirm: { id: 'confirmations.logout.confirm', defaultMessage: 'Log out' },
|
logoutConfirm: { id: 'confirmations.logout.confirm', defaultMessage: 'Log out' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapStateToProps = (state, ownProps) => ({
|
const mapStateToProps = (state, ownProps) => ({
|
||||||
columns : state.getIn(['settings', 'columns']),
|
columns: state.getIn(['settings', 'columns']),
|
||||||
showSearch: ownProps.multiColumn ? state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']) : ownProps.isSearchPage,
|
showSearch: ownProps.multiColumn ? state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']) : ownProps.isSearchPage,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -41,12 +41,12 @@ export default @connect(mapStateToProps)
|
|||||||
class Compose extends React.PureComponent {
|
class Compose extends React.PureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
dispatch : PropTypes.func.isRequired,
|
dispatch: PropTypes.func.isRequired,
|
||||||
columns : ImmutablePropTypes.list.isRequired,
|
columns: ImmutablePropTypes.list.isRequired,
|
||||||
multiColumn : PropTypes.bool,
|
multiColumn: PropTypes.bool,
|
||||||
showSearch : PropTypes.bool,
|
showSearch: PropTypes.bool,
|
||||||
isSearchPage: PropTypes.bool,
|
isSearchPage: PropTypes.bool,
|
||||||
intl : PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
|