This commit is contained in:
Kay Faraday 2022-05-06 01:24:07 +00:00
commit a0c818309d
492 changed files with 8777 additions and 5850 deletions

View File

@ -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',

View File

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

16
Gemfile
View File

@ -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,7 +79,7 @@ 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', '~> 3.2'
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'
@ -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

View File

@ -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)
@ -208,10 +216,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 +264,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 +286,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 +309,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 +322,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 +372,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 +399,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 +407,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 +429,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 +442,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)
@ -461,18 +471,18 @@ GEM
pry-rails (0.3.9) pry-rails (0.3.9)
pry (>= 0.10.4) pry (>= 0.10.4)
public_suffix (4.0.6) public_suffix (4.0.6)
puma (5.6.2) puma (5.6.4)
nio4r (~> 2.0) nio4r (~> 2.0)
pundit (2.2.0) pundit (2.2.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
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 +492,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 +521,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 +537,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 +555,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 +572,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 +610,14 @@ 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 (3.2.0)
e2mmap e2mmap
redis (>= 3, < 5) redis (>= 3, < 5)
rufus-scheduler (~> 3.2) rufus-scheduler (~> 3.2)
sidekiq (>= 3) sidekiq (>= 3)
thwait 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 +633,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)
@ -638,7 +649,7 @@ GEM
stoplight (2.2.1) stoplight (2.2.1)
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)
@ -694,7 +705,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 +741,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 +764,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 +779,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 +788,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 +830,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 (~> 3.2)
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)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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)
on_authentication_success(user, :sign_in_token)
else
on_authentication_failure(user, :sign_in_token, :invalid_sign_in_token)
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

View File

@ -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?)

View File

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

View File

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

View File

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

View File

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

View File

@ -19,8 +19,11 @@ module ApplicationHelper
# is looked up from the locales definition, and rails-i18n comes with # is looked up from the locales definition, and rails-i18n comes with
# values that don't seem to make much sense for many languages, so # values that don't seem to make much sense for many languages, so
# override these values with a default of 3 digits of precision. # override these values with a default of 3 digits of precision.
options[:precision] = 3 options = options.merge(
options[:strip_insignificant_zeros] = true precision: 3,
strip_insignificant_zeros: true,
significant: true
)
number_to_human(number, **options) number_to_human(number, **options)
end end

View File

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

View File

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

View File

@ -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,
}; };
}; };

View File

@ -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),

View File

@ -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 = {

View File

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

View File

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

View File

@ -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' },

View File

@ -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>
)} )}

View File

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

View File

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

View File

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

View File

@ -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}/>)}

View File

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

View File

@ -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) => ({

View File

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

View File

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

View File

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

View File

@ -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)}>

View File

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

View File

@ -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' },

View File

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

View File

@ -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} />

View File

@ -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 => ({

View File

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

View File

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

View File

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

View File

@ -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>;
} }

View File

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

View File

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

View File

@ -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 () {

View File

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

View File

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

View File

@ -579,7 +579,7 @@
} }
& > span { & > span {
max-width: 400px; max-width: 500px;
} }
a { a {

View File

@ -206,7 +206,12 @@
sub { sub {
font-size: smaller; font-size: smaller;
text-align: sub; vertical-align: sub;
}
sup {
font-size: smaller;
vertical-align: super;
} }
ul, ol { ul, ol {

View File

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

View File

@ -125,7 +125,7 @@
sub { sub {
font-size: smaller; font-size: smaller;
text-align: sub; vertical-align: sub;
} }
sup { sup {

View File

@ -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) {

Binary file not shown.

After

Width:  |  Height:  |  Size: 622 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 418 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 488 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 390 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 452 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 589 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 457 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 650 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 498 B

View File

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

View File

@ -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,
}; };
}; };

View File

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

View File

@ -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),

View File

@ -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') {

View File

@ -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' },

View File

@ -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>
)} )}

View File

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

View File

@ -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)}>

View File

@ -6,7 +6,7 @@ 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' },

View File

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

View File

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

View File

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

View File

@ -26,7 +26,7 @@ const messages = defineMessages({
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' },
}); });

View File

@ -76,7 +76,7 @@ class DirectTimeline extends React.PureComponent {
return ( return (
<Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.title)}> <Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.title)}>
<ColumnHeader <ColumnHeader
icon='envelope' icon='at'
active={hasUnread} active={hasUnread}
title={intl.formatMessage(messages.title)} title={intl.formatMessage(messages.title)}
onPin={this.handlePin} onPin={this.handlePin}
@ -91,6 +91,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." />}
/> />
</Column> </Column>

View File

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

View File

@ -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) {

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