Merge commit '6268188543
' into kb_migration
This commit is contained in:
commit
850c4dfb3c
133 changed files with 1644 additions and 947 deletions
|
@ -1,5 +1,5 @@
|
||||||
# For details, see https://github.com/devcontainers/images/tree/main/src/ruby
|
# For details, see https://github.com/devcontainers/images/tree/main/src/ruby
|
||||||
FROM mcr.microsoft.com/devcontainers/ruby:0-3.2-bullseye
|
FROM mcr.microsoft.com/devcontainers/ruby:1-3.2-bullseye
|
||||||
|
|
||||||
# Install Rails
|
# Install Rails
|
||||||
# RUN gem install rails webdrivers
|
# RUN gem install rails webdrivers
|
||||||
|
|
|
@ -437,45 +437,6 @@ RSpec/SubjectStub:
|
||||||
- 'spec/services/unallow_domain_service_spec.rb'
|
- 'spec/services/unallow_domain_service_spec.rb'
|
||||||
- 'spec/validators/blacklisted_email_validator_spec.rb'
|
- 'spec/validators/blacklisted_email_validator_spec.rb'
|
||||||
|
|
||||||
# Configuration parameters: IgnoreNameless, IgnoreSymbolicNames.
|
|
||||||
RSpec/VerifiedDoubles:
|
|
||||||
Exclude:
|
|
||||||
- 'spec/controllers/admin/change_emails_controller_spec.rb'
|
|
||||||
- 'spec/controllers/admin/confirmations_controller_spec.rb'
|
|
||||||
- 'spec/controllers/admin/disputes/appeals_controller_spec.rb'
|
|
||||||
- 'spec/controllers/admin/domain_allows_controller_spec.rb'
|
|
||||||
- 'spec/controllers/admin/domain_blocks_controller_spec.rb'
|
|
||||||
- 'spec/controllers/api/v1/reports_controller_spec.rb'
|
|
||||||
- 'spec/controllers/api/web/embeds_controller_spec.rb'
|
|
||||||
- 'spec/controllers/auth/sessions_controller_spec.rb'
|
|
||||||
- 'spec/controllers/disputes/appeals_controller_spec.rb'
|
|
||||||
- 'spec/helpers/statuses_helper_spec.rb'
|
|
||||||
- 'spec/lib/suspicious_sign_in_detector_spec.rb'
|
|
||||||
- 'spec/models/account/field_spec.rb'
|
|
||||||
- 'spec/models/session_activation_spec.rb'
|
|
||||||
- 'spec/models/setting_spec.rb'
|
|
||||||
- 'spec/services/account_search_service_spec.rb'
|
|
||||||
- 'spec/services/post_status_service_spec.rb'
|
|
||||||
- 'spec/services/search_service_spec.rb'
|
|
||||||
- 'spec/validators/blacklisted_email_validator_spec.rb'
|
|
||||||
- 'spec/validators/disallowed_hashtags_validator_spec.rb'
|
|
||||||
- 'spec/validators/email_mx_validator_spec.rb'
|
|
||||||
- 'spec/validators/follow_limit_validator_spec.rb'
|
|
||||||
- 'spec/validators/note_length_validator_spec.rb'
|
|
||||||
- 'spec/validators/poll_validator_spec.rb'
|
|
||||||
- 'spec/validators/status_length_validator_spec.rb'
|
|
||||||
- 'spec/validators/status_pin_validator_spec.rb'
|
|
||||||
- 'spec/validators/unique_username_validator_spec.rb'
|
|
||||||
- 'spec/validators/unreserved_username_validator_spec.rb'
|
|
||||||
- 'spec/validators/url_validator_spec.rb'
|
|
||||||
- 'spec/views/statuses/show.html.haml_spec.rb'
|
|
||||||
- 'spec/workers/activitypub/processing_worker_spec.rb'
|
|
||||||
- 'spec/workers/admin/domain_purge_worker_spec.rb'
|
|
||||||
- 'spec/workers/domain_block_worker_spec.rb'
|
|
||||||
- 'spec/workers/domain_clear_media_worker_spec.rb'
|
|
||||||
- 'spec/workers/feed_insert_worker_spec.rb'
|
|
||||||
- 'spec/workers/regeneration_worker_spec.rb'
|
|
||||||
|
|
||||||
# This cop supports unsafe autocorrection (--autocorrect-all).
|
# This cop supports unsafe autocorrection (--autocorrect-all).
|
||||||
Rails/ApplicationController:
|
Rails/ApplicationController:
|
||||||
Exclude:
|
Exclude:
|
||||||
|
|
2
Gemfile
2
Gemfile
|
@ -3,8 +3,6 @@
|
||||||
source 'https://rubygems.org'
|
source 'https://rubygems.org'
|
||||||
ruby '>= 3.0.0'
|
ruby '>= 3.0.0'
|
||||||
|
|
||||||
gem 'pkg-config', '~> 1.5'
|
|
||||||
|
|
||||||
gem 'puma', '~> 6.3'
|
gem 'puma', '~> 6.3'
|
||||||
gem 'rails', '~> 6.1.7'
|
gem 'rails', '~> 6.1.7'
|
||||||
gem 'sprockets', '~> 3.7.2'
|
gem 'sprockets', '~> 3.7.2'
|
||||||
|
|
112
Gemfile.lock
112
Gemfile.lock
|
@ -18,40 +18,40 @@ GIT
|
||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
actioncable (6.1.7.3)
|
actioncable (6.1.7.4)
|
||||||
actionpack (= 6.1.7.3)
|
actionpack (= 6.1.7.4)
|
||||||
activesupport (= 6.1.7.3)
|
activesupport (= 6.1.7.4)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
websocket-driver (>= 0.6.1)
|
websocket-driver (>= 0.6.1)
|
||||||
actionmailbox (6.1.7.3)
|
actionmailbox (6.1.7.4)
|
||||||
actionpack (= 6.1.7.3)
|
actionpack (= 6.1.7.4)
|
||||||
activejob (= 6.1.7.3)
|
activejob (= 6.1.7.4)
|
||||||
activerecord (= 6.1.7.3)
|
activerecord (= 6.1.7.4)
|
||||||
activestorage (= 6.1.7.3)
|
activestorage (= 6.1.7.4)
|
||||||
activesupport (= 6.1.7.3)
|
activesupport (= 6.1.7.4)
|
||||||
mail (>= 2.7.1)
|
mail (>= 2.7.1)
|
||||||
actionmailer (6.1.7.3)
|
actionmailer (6.1.7.4)
|
||||||
actionpack (= 6.1.7.3)
|
actionpack (= 6.1.7.4)
|
||||||
actionview (= 6.1.7.3)
|
actionview (= 6.1.7.4)
|
||||||
activejob (= 6.1.7.3)
|
activejob (= 6.1.7.4)
|
||||||
activesupport (= 6.1.7.3)
|
activesupport (= 6.1.7.4)
|
||||||
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.7.3)
|
actionpack (6.1.7.4)
|
||||||
actionview (= 6.1.7.3)
|
actionview (= 6.1.7.4)
|
||||||
activesupport (= 6.1.7.3)
|
activesupport (= 6.1.7.4)
|
||||||
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.7.3)
|
actiontext (6.1.7.4)
|
||||||
actionpack (= 6.1.7.3)
|
actionpack (= 6.1.7.4)
|
||||||
activerecord (= 6.1.7.3)
|
activerecord (= 6.1.7.4)
|
||||||
activestorage (= 6.1.7.3)
|
activestorage (= 6.1.7.4)
|
||||||
activesupport (= 6.1.7.3)
|
activesupport (= 6.1.7.4)
|
||||||
nokogiri (>= 1.8.5)
|
nokogiri (>= 1.8.5)
|
||||||
actionview (6.1.7.3)
|
actionview (6.1.7.4)
|
||||||
activesupport (= 6.1.7.3)
|
activesupport (= 6.1.7.4)
|
||||||
builder (~> 3.1)
|
builder (~> 3.1)
|
||||||
erubi (~> 1.4)
|
erubi (~> 1.4)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
|
@ -61,22 +61,22 @@ GEM
|
||||||
activemodel (>= 4.1, < 7.1)
|
activemodel (>= 4.1, < 7.1)
|
||||||
case_transform (>= 0.2)
|
case_transform (>= 0.2)
|
||||||
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
|
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
|
||||||
activejob (6.1.7.3)
|
activejob (6.1.7.4)
|
||||||
activesupport (= 6.1.7.3)
|
activesupport (= 6.1.7.4)
|
||||||
globalid (>= 0.3.6)
|
globalid (>= 0.3.6)
|
||||||
activemodel (6.1.7.3)
|
activemodel (6.1.7.4)
|
||||||
activesupport (= 6.1.7.3)
|
activesupport (= 6.1.7.4)
|
||||||
activerecord (6.1.7.3)
|
activerecord (6.1.7.4)
|
||||||
activemodel (= 6.1.7.3)
|
activemodel (= 6.1.7.4)
|
||||||
activesupport (= 6.1.7.3)
|
activesupport (= 6.1.7.4)
|
||||||
activestorage (6.1.7.3)
|
activestorage (6.1.7.4)
|
||||||
actionpack (= 6.1.7.3)
|
actionpack (= 6.1.7.4)
|
||||||
activejob (= 6.1.7.3)
|
activejob (= 6.1.7.4)
|
||||||
activerecord (= 6.1.7.3)
|
activerecord (= 6.1.7.4)
|
||||||
activesupport (= 6.1.7.3)
|
activesupport (= 6.1.7.4)
|
||||||
marcel (~> 1.0)
|
marcel (~> 1.0)
|
||||||
mini_mime (>= 1.1.0)
|
mini_mime (>= 1.1.0)
|
||||||
activesupport (6.1.7.3)
|
activesupport (6.1.7.4)
|
||||||
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)
|
||||||
|
@ -412,7 +412,7 @@ GEM
|
||||||
mime-types-data (3.2023.0218.1)
|
mime-types-data (3.2023.0218.1)
|
||||||
mini_mime (1.1.2)
|
mini_mime (1.1.2)
|
||||||
mini_portile2 (2.8.2)
|
mini_portile2 (2.8.2)
|
||||||
minitest (5.18.0)
|
minitest (5.18.1)
|
||||||
msgpack (1.7.1)
|
msgpack (1.7.1)
|
||||||
multi_json (1.15.0)
|
multi_json (1.15.0)
|
||||||
multipart-post (2.3.0)
|
multipart-post (2.3.0)
|
||||||
|
@ -478,7 +478,6 @@ GEM
|
||||||
pg (1.5.3)
|
pg (1.5.3)
|
||||||
pghero (3.3.3)
|
pghero (3.3.3)
|
||||||
activerecord (>= 6)
|
activerecord (>= 6)
|
||||||
pkg-config (1.5.1)
|
|
||||||
posix-spawn (0.3.15)
|
posix-spawn (0.3.15)
|
||||||
premailer (1.21.0)
|
premailer (1.21.0)
|
||||||
addressable
|
addressable
|
||||||
|
@ -511,20 +510,20 @@ GEM
|
||||||
rack
|
rack
|
||||||
rack-test (2.1.0)
|
rack-test (2.1.0)
|
||||||
rack (>= 1.3)
|
rack (>= 1.3)
|
||||||
rails (6.1.7.3)
|
rails (6.1.7.4)
|
||||||
actioncable (= 6.1.7.3)
|
actioncable (= 6.1.7.4)
|
||||||
actionmailbox (= 6.1.7.3)
|
actionmailbox (= 6.1.7.4)
|
||||||
actionmailer (= 6.1.7.3)
|
actionmailer (= 6.1.7.4)
|
||||||
actionpack (= 6.1.7.3)
|
actionpack (= 6.1.7.4)
|
||||||
actiontext (= 6.1.7.3)
|
actiontext (= 6.1.7.4)
|
||||||
actionview (= 6.1.7.3)
|
actionview (= 6.1.7.4)
|
||||||
activejob (= 6.1.7.3)
|
activejob (= 6.1.7.4)
|
||||||
activemodel (= 6.1.7.3)
|
activemodel (= 6.1.7.4)
|
||||||
activerecord (= 6.1.7.3)
|
activerecord (= 6.1.7.4)
|
||||||
activestorage (= 6.1.7.3)
|
activestorage (= 6.1.7.4)
|
||||||
activesupport (= 6.1.7.3)
|
activesupport (= 6.1.7.4)
|
||||||
bundler (>= 1.15.0)
|
bundler (>= 1.15.0)
|
||||||
railties (= 6.1.7.3)
|
railties (= 6.1.7.4)
|
||||||
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)
|
||||||
|
@ -539,9 +538,9 @@ GEM
|
||||||
rails-i18n (6.0.0)
|
rails-i18n (6.0.0)
|
||||||
i18n (>= 0.7, < 2)
|
i18n (>= 0.7, < 2)
|
||||||
railties (>= 6.0.0, < 7)
|
railties (>= 6.0.0, < 7)
|
||||||
railties (6.1.7.3)
|
railties (6.1.7.4)
|
||||||
actionpack (= 6.1.7.3)
|
actionpack (= 6.1.7.4)
|
||||||
activesupport (= 6.1.7.3)
|
activesupport (= 6.1.7.4)
|
||||||
method_source
|
method_source
|
||||||
rake (>= 12.2)
|
rake (>= 12.2)
|
||||||
thor (~> 1.0)
|
thor (~> 1.0)
|
||||||
|
@ -717,7 +716,7 @@ GEM
|
||||||
unf_ext
|
unf_ext
|
||||||
unf_ext (0.0.8.2)
|
unf_ext (0.0.8.2)
|
||||||
unicode-display_width (2.4.2)
|
unicode-display_width (2.4.2)
|
||||||
uri (0.12.1)
|
uri (0.12.2)
|
||||||
validate_email (0.1.6)
|
validate_email (0.1.6)
|
||||||
activemodel (>= 3.0)
|
activemodel (>= 3.0)
|
||||||
mail (>= 2.2.5)
|
mail (>= 2.2.5)
|
||||||
|
@ -833,7 +832,6 @@ DEPENDENCIES
|
||||||
parslet
|
parslet
|
||||||
pg (~> 1.5)
|
pg (~> 1.5)
|
||||||
pghero
|
pghero
|
||||||
pkg-config (~> 1.5)
|
|
||||||
posix-spawn
|
posix-spawn
|
||||||
premailer-rails
|
premailer-rails
|
||||||
private_address_check (~> 0.5)
|
private_address_check (~> 0.5)
|
||||||
|
|
|
@ -2,8 +2,37 @@
|
||||||
|
|
||||||
class AccountsIndex < Chewy::Index
|
class AccountsIndex < Chewy::Index
|
||||||
settings index: { refresh_interval: '30s' }, analysis: {
|
settings index: { refresh_interval: '30s' }, analysis: {
|
||||||
|
filter: {
|
||||||
|
english_stop: {
|
||||||
|
type: 'stop',
|
||||||
|
stopwords: '_english_',
|
||||||
|
},
|
||||||
|
|
||||||
|
english_stemmer: {
|
||||||
|
type: 'stemmer',
|
||||||
|
language: 'english',
|
||||||
|
},
|
||||||
|
|
||||||
|
english_possessive_stemmer: {
|
||||||
|
type: 'stemmer',
|
||||||
|
language: 'possessive_english',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
analyzer: {
|
analyzer: {
|
||||||
content: {
|
natural: {
|
||||||
|
tokenizer: 'uax_url_email',
|
||||||
|
filter: %w(
|
||||||
|
english_possessive_stemmer
|
||||||
|
lowercase
|
||||||
|
asciifolding
|
||||||
|
cjk_width
|
||||||
|
english_stop
|
||||||
|
english_stemmer
|
||||||
|
),
|
||||||
|
},
|
||||||
|
|
||||||
|
verbatim: {
|
||||||
tokenizer: 'whitespace',
|
tokenizer: 'whitespace',
|
||||||
filter: %w(lowercase asciifolding cjk_width),
|
filter: %w(lowercase asciifolding cjk_width),
|
||||||
},
|
},
|
||||||
|
@ -26,18 +55,13 @@ class AccountsIndex < Chewy::Index
|
||||||
index_scope ::Account.searchable.includes(:account_stat)
|
index_scope ::Account.searchable.includes(:account_stat)
|
||||||
|
|
||||||
root date_detection: false do
|
root date_detection: false do
|
||||||
field :id, type: 'long'
|
field(:id, type: 'long')
|
||||||
|
field(:following_count, type: 'long', value: ->(account) { account.public_following_count })
|
||||||
field :display_name, type: 'text', analyzer: 'content' do
|
field(:followers_count, type: 'long', value: ->(account) { account.public_followers_count })
|
||||||
field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'content'
|
field(:properties, type: 'keyword', value: ->(account) { account.searchable_properties })
|
||||||
end
|
field(:last_status_at, type: 'date', value: ->(account) { account.last_status_at || account.created_at })
|
||||||
|
field(:display_name, type: 'text', analyzer: 'verbatim') { field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'verbatim' }
|
||||||
field :acct, type: 'text', analyzer: 'content', value: ->(account) { [account.username, account.domain].compact.join('@') } do
|
field(:username, type: 'text', analyzer: 'verbatim', value: ->(account) { [account.username, account.domain].compact.join('@') }) { field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'verbatim' }
|
||||||
field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'content'
|
field(:text, type: 'text', value: ->(account) { account.searchable_text }) { field :stemmed, type: 'text', analyzer: 'natural' }
|
||||||
end
|
|
||||||
|
|
||||||
field :following_count, type: 'long', value: ->(account) { account.public_following_count }
|
|
||||||
field :followers_count, type: 'long', value: ->(account) { account.public_followers_count }
|
|
||||||
field :last_status_at, type: 'date', value: ->(account) { account.last_status_at || account.created_at }
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -28,6 +28,7 @@ module Admin
|
||||||
authorize :webhook, :create?
|
authorize :webhook, :create?
|
||||||
|
|
||||||
@webhook = Webhook.new(resource_params)
|
@webhook = Webhook.new(resource_params)
|
||||||
|
@webhook.current_account = current_account
|
||||||
|
|
||||||
if @webhook.save
|
if @webhook.save
|
||||||
redirect_to admin_webhook_path(@webhook)
|
redirect_to admin_webhook_path(@webhook)
|
||||||
|
@ -39,6 +40,8 @@ module Admin
|
||||||
def update
|
def update
|
||||||
authorize @webhook, :update?
|
authorize @webhook, :update?
|
||||||
|
|
||||||
|
@webhook.current_account = current_account
|
||||||
|
|
||||||
if @webhook.update(resource_params)
|
if @webhook.update(resource_params)
|
||||||
redirect_to admin_webhook_path(@webhook)
|
redirect_to admin_webhook_path(@webhook)
|
||||||
else
|
else
|
||||||
|
|
|
@ -19,6 +19,11 @@ class Api::V1::ConversationsController < Api::BaseController
|
||||||
render json: @conversation, serializer: REST::ConversationSerializer
|
render json: @conversation, serializer: REST::ConversationSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def unread
|
||||||
|
@conversation.update!(unread: true)
|
||||||
|
render json: @conversation, serializer: REST::ConversationSerializer
|
||||||
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@conversation.destroy!
|
@conversation.destroy!
|
||||||
render_empty
|
render_empty
|
||||||
|
|
|
@ -21,11 +21,35 @@ class Api::V1::DirectoriesController < Api::BaseController
|
||||||
|
|
||||||
def accounts_scope
|
def accounts_scope
|
||||||
Account.discoverable.tap do |scope|
|
Account.discoverable.tap do |scope|
|
||||||
scope.merge!(Account.local) if truthy_param?(:local)
|
scope.merge!(account_order_scope)
|
||||||
scope.merge!(Account.by_recent_status) if params[:order].blank? || params[:order] == 'active'
|
scope.merge!(local_account_scope) if local_accounts?
|
||||||
scope.merge!(Account.order(id: :desc)) if params[:order] == 'new'
|
scope.merge!(account_exclusion_scope) if current_account
|
||||||
scope.merge!(Account.not_excluded_by_account(current_account)) if current_account
|
scope.merge!(account_domain_block_scope) if current_account && !local_accounts?
|
||||||
scope.merge!(Account.not_domain_blocked_by_account(current_account)) if current_account && !truthy_param?(:local)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def local_accounts?
|
||||||
|
truthy_param?(:local)
|
||||||
|
end
|
||||||
|
|
||||||
|
def account_order_scope
|
||||||
|
case params[:order]
|
||||||
|
when 'new'
|
||||||
|
Account.order(id: :desc)
|
||||||
|
when 'active', nil
|
||||||
|
Account.by_recent_status
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def local_account_scope
|
||||||
|
Account.local
|
||||||
|
end
|
||||||
|
|
||||||
|
def account_exclusion_scope
|
||||||
|
Account.not_excluded_by_account(current_account)
|
||||||
|
end
|
||||||
|
|
||||||
|
def account_domain_block_scope
|
||||||
|
Account.not_domain_blocked_by_account(current_account)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,6 +5,7 @@ class Api::V1::Emails::ConfirmationsController < Api::BaseController
|
||||||
before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, except: :check
|
before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, except: :check
|
||||||
before_action :require_user_owned_by_application!, except: :check
|
before_action :require_user_owned_by_application!, except: :check
|
||||||
before_action :require_user_not_confirmed!, except: :check
|
before_action :require_user_not_confirmed!, except: :check
|
||||||
|
before_action :require_authenticated_user!, only: :check
|
||||||
|
|
||||||
def create
|
def create
|
||||||
current_user.update!(email: params[:email]) if params.key?(:email)
|
current_user.update!(email: params[:email]) if params.key?(:email)
|
||||||
|
|
|
@ -8,11 +8,15 @@ class Api::V1::Statuses::HistoriesController < Api::BaseController
|
||||||
|
|
||||||
def show
|
def show
|
||||||
cache_if_unauthenticated!
|
cache_if_unauthenticated!
|
||||||
render json: @status.edits.includes(:account, status: [:account]), each_serializer: REST::StatusEditSerializer
|
render json: status_edits, each_serializer: REST::StatusEditSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def status_edits
|
||||||
|
@status.edits.includes(:account, status: [:account]).to_a.presence || [@status.build_snapshot(at_time: @status.edited_at || @status.created_at)]
|
||||||
|
end
|
||||||
|
|
||||||
def set_status
|
def set_status
|
||||||
@status = Status.find(params[:status_id])
|
@status = Status.find(params[:status_id])
|
||||||
authorize @status, :show?
|
authorize @status, :show?
|
||||||
|
|
|
@ -34,11 +34,11 @@ class Api::V2::SearchController < Api::BaseController
|
||||||
params[:q],
|
params[:q],
|
||||||
current_account,
|
current_account,
|
||||||
limit_param(RESULTS_LIMIT),
|
limit_param(RESULTS_LIMIT),
|
||||||
search_params.merge(resolve: truthy_param?(:resolve), exclude_unreviewed: truthy_param?(:exclude_unreviewed))
|
search_params.merge(resolve: truthy_param?(:resolve), exclude_unreviewed: truthy_param?(:exclude_unreviewed), following: truthy_param?(:following))
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def search_params
|
def search_params
|
||||||
params.permit(:type, :offset, :min_id, :max_id, :account_id, :searchability)
|
params.permit(:type, :offset, :min_id, :max_id, :account_id, :following, :searchability)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
BIN
app/javascript/images/friends-cropped.png
Executable file
BIN
app/javascript/images/friends-cropped.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 189 KiB |
|
@ -132,13 +132,13 @@ export function resetCompose() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const focusCompose = (routerHistory, defaultText) => dispatch => {
|
export const focusCompose = (routerHistory, defaultText) => (dispatch, getState) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: COMPOSE_FOCUS,
|
type: COMPOSE_FOCUS,
|
||||||
defaultText,
|
defaultText,
|
||||||
});
|
});
|
||||||
|
|
||||||
ensureComposeIsVisible(routerHistory);
|
ensureComposeIsVisible(getState, routerHistory);
|
||||||
};
|
};
|
||||||
|
|
||||||
export function mentionCompose(account, routerHistory) {
|
export function mentionCompose(account, routerHistory) {
|
||||||
|
|
|
@ -19,6 +19,10 @@ export const SERVER_DOMAIN_BLOCKS_FETCH_SUCCESS = 'SERVER_DOMAIN_BLOCKS_FETCH_SU
|
||||||
export const SERVER_DOMAIN_BLOCKS_FETCH_FAIL = 'SERVER_DOMAIN_BLOCKS_FETCH_FAIL';
|
export const SERVER_DOMAIN_BLOCKS_FETCH_FAIL = 'SERVER_DOMAIN_BLOCKS_FETCH_FAIL';
|
||||||
|
|
||||||
export const fetchServer = () => (dispatch, getState) => {
|
export const fetchServer = () => (dispatch, getState) => {
|
||||||
|
if (getState().getIn(['server', 'server', 'isLoading'])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
dispatch(fetchServerRequest());
|
dispatch(fetchServerRequest());
|
||||||
|
|
||||||
api(getState)
|
api(getState)
|
||||||
|
@ -66,6 +70,10 @@ const fetchServerTranslationLanguagesFail = error => ({
|
||||||
});
|
});
|
||||||
|
|
||||||
export const fetchExtendedDescription = () => (dispatch, getState) => {
|
export const fetchExtendedDescription = () => (dispatch, getState) => {
|
||||||
|
if (getState().getIn(['server', 'extendedDescription', 'isLoading'])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
dispatch(fetchExtendedDescriptionRequest());
|
dispatch(fetchExtendedDescriptionRequest());
|
||||||
|
|
||||||
api(getState)
|
api(getState)
|
||||||
|
@ -89,6 +97,10 @@ const fetchExtendedDescriptionFail = error => ({
|
||||||
});
|
});
|
||||||
|
|
||||||
export const fetchDomainBlocks = () => (dispatch, getState) => {
|
export const fetchDomainBlocks = () => (dispatch, getState) => {
|
||||||
|
if (getState().getIn(['server', 'domainBlocks', 'isLoading'])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
dispatch(fetchDomainBlocksRequest());
|
dispatch(fetchDomainBlocksRequest());
|
||||||
|
|
||||||
api(getState)
|
api(getState)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { defineMessages, injectIntl } from 'react-intl';
|
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
@ -51,6 +51,7 @@ class Account extends ImmutablePureComponent {
|
||||||
defaultAction: PropTypes.string,
|
defaultAction: PropTypes.string,
|
||||||
onActionClick: PropTypes.func,
|
onActionClick: PropTypes.func,
|
||||||
children: PropTypes.object,
|
children: PropTypes.object,
|
||||||
|
withBio: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
|
@ -82,7 +83,7 @@ class Account extends ImmutablePureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { account, intl, hidden, hideButtons, onActionClick, actionIcon, actionTitle, defaultAction, size, minimal, children } = this.props;
|
const { account, intl, hidden, hideButtons, withBio, onActionClick, actionIcon, actionTitle, defaultAction, size, minimal, children } = this.props;
|
||||||
|
|
||||||
if (!account) {
|
if (!account) {
|
||||||
return <EmptyAccount size={size} minimal={minimal} />;
|
return <EmptyAccount size={size} minimal={minimal} />;
|
||||||
|
@ -178,6 +179,15 @@ class Account extends ImmutablePureComponent {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{withBio && (account.get('note').length > 0 ? (
|
||||||
|
<div
|
||||||
|
className='account__note translate'
|
||||||
|
dangerouslySetInnerHTML={{ __html: account.get('note_emojified') }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className='account__note account__note--missing'><FormattedMessage id='account.no_bio' defaultMessage='No description provided.' /></div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -104,7 +104,7 @@ class StatusContent extends PureComponent {
|
||||||
|
|
||||||
if (mention) {
|
if (mention) {
|
||||||
link.addEventListener('click', this.onMentionClick.bind(this, mention), false);
|
link.addEventListener('click', this.onMentionClick.bind(this, mention), false);
|
||||||
link.setAttribute('title', mention.get('acct'));
|
link.setAttribute('title', `@${mention.get('acct')}`);
|
||||||
link.setAttribute('href', `/@${mention.get('acct')}`);
|
link.setAttribute('href', `/@${mention.get('acct')}`);
|
||||||
} else if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) {
|
} else if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) {
|
||||||
link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false);
|
link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false);
|
||||||
|
|
|
@ -1,11 +1,27 @@
|
||||||
import { Icon } from './icon';
|
import { Icon } from './icon';
|
||||||
|
|
||||||
|
const domParser = new DOMParser();
|
||||||
|
|
||||||
|
const stripRelMe = (html: string) => {
|
||||||
|
const document = domParser.parseFromString(html, 'text/html').documentElement;
|
||||||
|
|
||||||
|
document.querySelectorAll<HTMLAnchorElement>('a[rel]').forEach((link) => {
|
||||||
|
link.rel = link.rel
|
||||||
|
.split(' ')
|
||||||
|
.filter((x: string) => x !== 'me')
|
||||||
|
.join(' ');
|
||||||
|
});
|
||||||
|
|
||||||
|
const body = document.querySelector('body');
|
||||||
|
return body ? { __html: body.innerHTML } : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
link: string;
|
link: string;
|
||||||
}
|
}
|
||||||
export const VerifiedBadge: React.FC<Props> = ({ link }) => (
|
export const VerifiedBadge: React.FC<Props> = ({ link }) => (
|
||||||
<span className='verified-badge'>
|
<span className='verified-badge'>
|
||||||
<Icon id='check' className='verified-badge__mark' />
|
<Icon id='check' className='verified-badge__mark' />
|
||||||
<span dangerouslySetInnerHTML={{ __html: link }} />
|
<span dangerouslySetInnerHTML={stripRelMe(link)} />
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|
|
@ -168,7 +168,7 @@ class About extends PureComponent {
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
<Section title={intl.formatMessage(messages.rules)}>
|
<Section title={intl.formatMessage(messages.rules)}>
|
||||||
{!isLoading && (server.get('rules').isEmpty() ? (
|
{!isLoading && (server.get('rules', []).isEmpty() ? (
|
||||||
<p><FormattedMessage id='about.not_available' defaultMessage='This information has not been made available on this server.' /></p>
|
<p><FormattedMessage id='about.not_available' defaultMessage='This information has not been made available on this server.' /></p>
|
||||||
) : (
|
) : (
|
||||||
<ol className='rules-list'>
|
<ol className='rules-list'>
|
||||||
|
|
|
@ -15,13 +15,14 @@ import { addColumn, removeColumn, moveColumn } from 'mastodon/actions/columns';
|
||||||
import ColumnHeader from 'mastodon/components/column_header';
|
import ColumnHeader from 'mastodon/components/column_header';
|
||||||
import StatusList from 'mastodon/components/status_list';
|
import StatusList from 'mastodon/components/status_list';
|
||||||
import Column from 'mastodon/features/ui/components/column';
|
import Column from 'mastodon/features/ui/components/column';
|
||||||
|
import { getStatusList } from 'mastodon/selectors';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
heading: { id: 'column.bookmarks', defaultMessage: 'Bookmarks' },
|
heading: { id: 'column.bookmarks', defaultMessage: 'Bookmarks' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
statusIds: state.getIn(['status_lists', 'bookmarks', 'items']),
|
statusIds: getStatusList(state, 'bookmarks'),
|
||||||
isLoading: state.getIn(['status_lists', 'bookmarks', 'isLoading'], true),
|
isLoading: state.getIn(['status_lists', 'bookmarks', 'isLoading'], true),
|
||||||
hasMore: !!state.getIn(['status_lists', 'bookmarks', 'next']),
|
hasMore: !!state.getIn(['status_lists', 'bookmarks', 'next']),
|
||||||
});
|
});
|
||||||
|
|
|
@ -140,11 +140,8 @@ class CommunityTimeline extends PureComponent {
|
||||||
<ColumnSettingsContainer columnId={columnId} />
|
<ColumnSettingsContainer columnId={columnId} />
|
||||||
</ColumnHeader>
|
</ColumnHeader>
|
||||||
|
|
||||||
<DismissableBanner id='community_timeline'>
|
|
||||||
<FormattedMessage id='dismissable_banner.community_timeline' defaultMessage='These are the most recent public posts from people whose accounts are hosted by {domain}.' values={{ domain }} />
|
|
||||||
</DismissableBanner>
|
|
||||||
|
|
||||||
<StatusListContainer
|
<StatusListContainer
|
||||||
|
prepend={<DismissableBanner id='community_timeline'><FormattedMessage id='dismissable_banner.community_timeline' defaultMessage='These are the most recent public posts from people whose accounts are hosted by {domain}.' values={{ domain }} /></DismissableBanner>}
|
||||||
trackScroll={!pinned}
|
trackScroll={!pinned}
|
||||||
scrollKey={`community_timeline-${columnId}`}
|
scrollKey={`community_timeline-${columnId}`}
|
||||||
timelineId={`community${onlyMedia ? ':media' : ''}`}
|
timelineId={`community${onlyMedia ? ':media' : ''}`}
|
||||||
|
|
|
@ -66,7 +66,7 @@ class ActionBar extends PureComponent {
|
||||||
return (
|
return (
|
||||||
<div className='compose__action-bar'>
|
<div className='compose__action-bar'>
|
||||||
<div className='compose__action-bar-dropdown'>
|
<div className='compose__action-bar-dropdown'>
|
||||||
<DropdownMenuContainer items={menu} icon='ellipsis-v' size={18} direction='right' />
|
<DropdownMenuContainer items={menu} icon='bars' size={18} direction='right' />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -35,7 +35,7 @@ class Links extends PureComponent {
|
||||||
|
|
||||||
const banner = (
|
const banner = (
|
||||||
<DismissableBanner id='explore/links'>
|
<DismissableBanner id='explore/links'>
|
||||||
<FormattedMessage id='dismissable_banner.explore_links' defaultMessage='These news stories are being talked about by people on this and other servers of the decentralized network right now.' />
|
<FormattedMessage id='dismissable_banner.explore_links' defaultMessage='These are news stories being shared the most on the social web today. Newer news stories posted by more different people are ranked higher.' />
|
||||||
</DismissableBanner>
|
</DismissableBanner>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -11,9 +11,10 @@ import { debounce } from 'lodash';
|
||||||
import { fetchTrendingStatuses, expandTrendingStatuses } from 'mastodon/actions/trends';
|
import { fetchTrendingStatuses, expandTrendingStatuses } from 'mastodon/actions/trends';
|
||||||
import DismissableBanner from 'mastodon/components/dismissable_banner';
|
import DismissableBanner from 'mastodon/components/dismissable_banner';
|
||||||
import StatusList from 'mastodon/components/status_list';
|
import StatusList from 'mastodon/components/status_list';
|
||||||
|
import { getStatusList } from 'mastodon/selectors';
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
statusIds: state.getIn(['status_lists', 'trending', 'items']),
|
statusIds: getStatusList(state, 'trending'),
|
||||||
isLoading: state.getIn(['status_lists', 'trending', 'isLoading'], true),
|
isLoading: state.getIn(['status_lists', 'trending', 'isLoading'], true),
|
||||||
hasMore: !!state.getIn(['status_lists', 'trending', 'next']),
|
hasMore: !!state.getIn(['status_lists', 'trending', 'next']),
|
||||||
});
|
});
|
||||||
|
@ -46,7 +47,7 @@ class Statuses extends PureComponent {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DismissableBanner id='explore/statuses'>
|
<DismissableBanner id='explore/statuses'>
|
||||||
<FormattedMessage id='dismissable_banner.explore_statuses' defaultMessage='These posts from this and other servers in the decentralized network are gaining traction on this server right now.' />
|
<FormattedMessage id='dismissable_banner.explore_statuses' defaultMessage='These are posts from across the social web that are gaining traction today. Newer posts with more boosts and favourites are ranked higher.' />
|
||||||
</DismissableBanner>
|
</DismissableBanner>
|
||||||
|
|
||||||
<StatusList
|
<StatusList
|
||||||
|
|
|
@ -34,7 +34,7 @@ class Tags extends PureComponent {
|
||||||
|
|
||||||
const banner = (
|
const banner = (
|
||||||
<DismissableBanner id='explore/tags'>
|
<DismissableBanner id='explore/tags'>
|
||||||
<FormattedMessage id='dismissable_banner.explore_tags' defaultMessage='These hashtags are gaining traction among people on this and other servers of the decentralized network right now.' />
|
<FormattedMessage id='dismissable_banner.explore_tags' defaultMessage='These are hashtags that are gaining traction on the social web today. Hashtags that are used by more different people are ranked higher.' />
|
||||||
</DismissableBanner>
|
</DismissableBanner>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -15,13 +15,14 @@ import { fetchFavouritedStatuses, expandFavouritedStatuses } from 'mastodon/acti
|
||||||
import ColumnHeader from 'mastodon/components/column_header';
|
import ColumnHeader from 'mastodon/components/column_header';
|
||||||
import StatusList from 'mastodon/components/status_list';
|
import StatusList from 'mastodon/components/status_list';
|
||||||
import Column from 'mastodon/features/ui/components/column';
|
import Column from 'mastodon/features/ui/components/column';
|
||||||
|
import { getStatusList } from 'mastodon/selectors';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
heading: { id: 'column.favourites', defaultMessage: 'Favourites' },
|
heading: { id: 'column.favourites', defaultMessage: 'Favourites' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
statusIds: state.getIn(['status_lists', 'favourites', 'items']),
|
statusIds: getStatusList(state, 'favourites'),
|
||||||
isLoading: state.getIn(['status_lists', 'favourites', 'isLoading'], true),
|
isLoading: state.getIn(['status_lists', 'favourites', 'isLoading'], true),
|
||||||
hasMore: !!state.getIn(['status_lists', 'favourites', 'next']),
|
hasMore: !!state.getIn(['status_lists', 'favourites', 'next']),
|
||||||
});
|
});
|
||||||
|
|
211
app/javascript/mastodon/features/firehose/index.jsx
Normal file
211
app/javascript/mastodon/features/firehose/index.jsx
Normal file
|
@ -0,0 +1,211 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { useRef, useCallback, useEffect } from 'react';
|
||||||
|
|
||||||
|
import { useIntl, defineMessages, FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import { Helmet } from 'react-helmet';
|
||||||
|
import { NavLink } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { addColumn } from 'mastodon/actions/columns';
|
||||||
|
import { changeSetting } from 'mastodon/actions/settings';
|
||||||
|
import { connectPublicStream, connectCommunityStream } from 'mastodon/actions/streaming';
|
||||||
|
import { expandPublicTimeline, expandCommunityTimeline } from 'mastodon/actions/timelines';
|
||||||
|
import DismissableBanner from 'mastodon/components/dismissable_banner';
|
||||||
|
import initialState, { domain } from 'mastodon/initial_state';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||||
|
|
||||||
|
import Column from '../../components/column';
|
||||||
|
import ColumnHeader from '../../components/column_header';
|
||||||
|
import SettingToggle from '../notifications/components/setting_toggle';
|
||||||
|
import StatusListContainer from '../ui/containers/status_list_container';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
title: { id: 'column.firehose', defaultMessage: 'Live feeds' },
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: use a proper React context later on
|
||||||
|
const useIdentity = () => ({
|
||||||
|
signedIn: !!initialState.meta.me,
|
||||||
|
accountId: initialState.meta.me,
|
||||||
|
disabledAccountId: initialState.meta.disabled_account_id,
|
||||||
|
accessToken: initialState.meta.access_token,
|
||||||
|
permissions: initialState.role ? initialState.role.permissions : 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const ColumnSettings = () => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const settings = useAppSelector((state) => state.getIn(['settings', 'firehose']));
|
||||||
|
const onChange = useCallback(
|
||||||
|
(key, checked) => dispatch(changeSetting(['firehose', ...key], checked)),
|
||||||
|
[dispatch],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className='column-settings__row'>
|
||||||
|
<SettingToggle
|
||||||
|
settings={settings}
|
||||||
|
settingPath={['onlyMedia']}
|
||||||
|
onChange={onChange}
|
||||||
|
label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media only' />}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Firehose = ({ feedType, multiColumn }) => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const intl = useIntl();
|
||||||
|
const { signedIn } = useIdentity();
|
||||||
|
const columnRef = useRef(null);
|
||||||
|
|
||||||
|
const onlyMedia = useAppSelector((state) => state.getIn(['settings', 'firehose', 'onlyMedia'], false));
|
||||||
|
const hasUnread = useAppSelector((state) => state.getIn(['timelines', `${feedType}${onlyMedia ? ':media' : ''}`, 'unread'], 0) > 0);
|
||||||
|
|
||||||
|
const handlePin = useCallback(
|
||||||
|
() => {
|
||||||
|
switch(feedType) {
|
||||||
|
case 'community':
|
||||||
|
dispatch(addColumn('COMMUNITY', { other: { onlyMedia } }));
|
||||||
|
break;
|
||||||
|
case 'public':
|
||||||
|
dispatch(addColumn('PUBLIC', { other: { onlyMedia } }));
|
||||||
|
break;
|
||||||
|
case 'public:remote':
|
||||||
|
dispatch(addColumn('REMOTE', { other: { onlyMedia, onlyRemote: true } }));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[dispatch, onlyMedia, feedType],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleLoadMore = useCallback(
|
||||||
|
(maxId) => {
|
||||||
|
switch(feedType) {
|
||||||
|
case 'community':
|
||||||
|
dispatch(expandCommunityTimeline({ maxId, onlyMedia }));
|
||||||
|
break;
|
||||||
|
case 'public':
|
||||||
|
dispatch(expandPublicTimeline({ maxId, onlyMedia }));
|
||||||
|
break;
|
||||||
|
case 'public:remote':
|
||||||
|
dispatch(expandPublicTimeline({ maxId, onlyMedia, onlyRemote: true }));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[dispatch, onlyMedia, feedType],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleHeaderClick = useCallback(() => columnRef.current?.scrollTop(), []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let disconnect;
|
||||||
|
|
||||||
|
switch(feedType) {
|
||||||
|
case 'community':
|
||||||
|
dispatch(expandCommunityTimeline({ onlyMedia }));
|
||||||
|
if (signedIn) {
|
||||||
|
disconnect = dispatch(connectCommunityStream({ onlyMedia }));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'public':
|
||||||
|
dispatch(expandPublicTimeline({ onlyMedia }));
|
||||||
|
if (signedIn) {
|
||||||
|
disconnect = dispatch(connectPublicStream({ onlyMedia }));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'public:remote':
|
||||||
|
dispatch(expandPublicTimeline({ onlyMedia, onlyRemote: true }));
|
||||||
|
if (signedIn) {
|
||||||
|
disconnect = dispatch(connectPublicStream({ onlyMedia, onlyRemote: true }));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => disconnect?.();
|
||||||
|
}, [dispatch, signedIn, feedType, onlyMedia]);
|
||||||
|
|
||||||
|
const prependBanner = feedType === 'community' ? (
|
||||||
|
<DismissableBanner id='community_timeline'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='dismissable_banner.community_timeline'
|
||||||
|
defaultMessage='These are the most recent public posts from people whose accounts are hosted by {domain}.'
|
||||||
|
values={{ domain }}
|
||||||
|
/>
|
||||||
|
</DismissableBanner>
|
||||||
|
) : (
|
||||||
|
<DismissableBanner id='public_timeline'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='dismissable_banner.public_timeline'
|
||||||
|
defaultMessage='These are the most recent public posts from people on the social web that people on {domain} follow.'
|
||||||
|
values={{ domain }}
|
||||||
|
/>
|
||||||
|
</DismissableBanner>
|
||||||
|
);
|
||||||
|
|
||||||
|
const emptyMessage = feedType === 'community' ? (
|
||||||
|
<FormattedMessage
|
||||||
|
id='empty_column.community'
|
||||||
|
defaultMessage='The local timeline is empty. Write something publicly to get the ball rolling!'
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<FormattedMessage
|
||||||
|
id='empty_column.public'
|
||||||
|
defaultMessage='There is nothing here! Write something publicly, or manually follow users from other servers to fill it up'
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Column bindToDocument={!multiColumn} ref={columnRef} label={intl.formatMessage(messages.title)}>
|
||||||
|
<ColumnHeader
|
||||||
|
icon='globe'
|
||||||
|
active={hasUnread}
|
||||||
|
title={intl.formatMessage(messages.title)}
|
||||||
|
onPin={handlePin}
|
||||||
|
onClick={handleHeaderClick}
|
||||||
|
multiColumn={multiColumn}
|
||||||
|
>
|
||||||
|
<ColumnSettings />
|
||||||
|
</ColumnHeader>
|
||||||
|
|
||||||
|
<div className='scrollable scrollable--flex'>
|
||||||
|
<div className='account__section-headline'>
|
||||||
|
<NavLink exact to='/public/local'>
|
||||||
|
<FormattedMessage tagName='div' id='firehose.local' defaultMessage='This server' />
|
||||||
|
</NavLink>
|
||||||
|
|
||||||
|
<NavLink exact to='/public/remote'>
|
||||||
|
<FormattedMessage tagName='div' id='firehose.remote' defaultMessage='Other servers' />
|
||||||
|
</NavLink>
|
||||||
|
|
||||||
|
<NavLink exact to='/public'>
|
||||||
|
<FormattedMessage tagName='div' id='firehose.all' defaultMessage='All' />
|
||||||
|
</NavLink>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<StatusListContainer
|
||||||
|
prepend={prependBanner}
|
||||||
|
timelineId={`${feedType}${onlyMedia ? ':media' : ''}`}
|
||||||
|
onLoadMore={handleLoadMore}
|
||||||
|
trackScroll
|
||||||
|
scrollKey='firehose'
|
||||||
|
emptyMessage={emptyMessage}
|
||||||
|
bindToDocument={!multiColumn}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Helmet>
|
||||||
|
<title>{intl.formatMessage(messages.title)}</title>
|
||||||
|
<meta name='robots' content='noindex' />
|
||||||
|
</Helmet>
|
||||||
|
</Column>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Firehose.propTypes = {
|
||||||
|
multiColumn: PropTypes.bool,
|
||||||
|
feedType: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Firehose;
|
|
@ -0,0 +1,25 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
import background from 'mastodon/../images/friends-cropped.png';
|
||||||
|
import DismissableBanner from 'mastodon/components/dismissable_banner';
|
||||||
|
|
||||||
|
|
||||||
|
export const ExplorePrompt = () => (
|
||||||
|
<DismissableBanner id='home.explore_prompt'>
|
||||||
|
<img src={background} alt='' className='dismissable-banner__background-image' />
|
||||||
|
|
||||||
|
<h1><FormattedMessage id='home.explore_prompt.title' defaultMessage='This is your home base within Mastodon.' /></h1>
|
||||||
|
<p><FormattedMessage id='home.explore_prompt.body' defaultMessage="Your home feed will have a mix of posts from the hashtags you've chosen to follow, the people you've chosen to follow, and the posts they boost. It's looking pretty quiet right now, so how about:" /></p>
|
||||||
|
|
||||||
|
<div className='dismissable-banner__message__actions__wrapper'>
|
||||||
|
<div className='dismissable-banner__message__actions'>
|
||||||
|
<Link to='/explore' className='button'><FormattedMessage id='home.actions.go_to_explore' defaultMessage="See what's trending" /></Link>
|
||||||
|
<Link to='/explore/suggestions' className='button button-tertiary'><FormattedMessage id='home.actions.go_to_suggestions' defaultMessage='Find people to follow' /></Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DismissableBanner>
|
||||||
|
);
|
|
@ -5,14 +5,16 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
|
|
||||||
|
import { List as ImmutableList } from 'immutable';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
|
||||||
import { fetchAnnouncements, toggleShowAnnouncements } from 'mastodon/actions/announcements';
|
import { fetchAnnouncements, toggleShowAnnouncements } from 'mastodon/actions/announcements';
|
||||||
import { IconWithBadge } from 'mastodon/components/icon_with_badge';
|
import { IconWithBadge } from 'mastodon/components/icon_with_badge';
|
||||||
import { NotSignedInIndicator } from 'mastodon/components/not_signed_in_indicator';
|
import { NotSignedInIndicator } from 'mastodon/components/not_signed_in_indicator';
|
||||||
import AnnouncementsContainer from 'mastodon/features/getting_started/containers/announcements_container';
|
import AnnouncementsContainer from 'mastodon/features/getting_started/containers/announcements_container';
|
||||||
|
import { me } from 'mastodon/initial_state';
|
||||||
|
|
||||||
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
|
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
|
||||||
import { expandHomeTimeline } from '../../actions/timelines';
|
import { expandHomeTimeline } from '../../actions/timelines';
|
||||||
|
@ -20,6 +22,7 @@ import Column from '../../components/column';
|
||||||
import ColumnHeader from '../../components/column_header';
|
import ColumnHeader from '../../components/column_header';
|
||||||
import StatusListContainer from '../ui/containers/status_list_container';
|
import StatusListContainer from '../ui/containers/status_list_container';
|
||||||
|
|
||||||
|
import { ExplorePrompt } from './components/explore_prompt';
|
||||||
import ColumnSettingsContainer from './containers/column_settings_container';
|
import ColumnSettingsContainer from './containers/column_settings_container';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
|
@ -28,12 +31,40 @@ const messages = defineMessages({
|
||||||
hide_announcements: { id: 'home.hide_announcements', defaultMessage: 'Hide announcements' },
|
hide_announcements: { id: 'home.hide_announcements', defaultMessage: 'Hide announcements' },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const getHomeFeedSpeed = createSelector([
|
||||||
|
state => state.getIn(['timelines', 'home', 'items'], ImmutableList()),
|
||||||
|
state => state.getIn(['timelines', 'home', 'pendingItems'], ImmutableList()),
|
||||||
|
state => state.get('statuses'),
|
||||||
|
], (statusIds, pendingStatusIds, statusMap) => {
|
||||||
|
const recentStatusIds = pendingStatusIds.size > 0 ? pendingStatusIds : statusIds;
|
||||||
|
const statuses = recentStatusIds.filter(id => id !== null).map(id => statusMap.get(id)).filter(status => status?.get('account') !== me).take(20);
|
||||||
|
const oldest = new Date(statuses.getIn([statuses.size - 1, 'created_at'], 0));
|
||||||
|
const newest = new Date(statuses.getIn([0, 'created_at'], 0));
|
||||||
|
const averageGap = (newest - oldest) / (1000 * (statuses.size + 1)); // Average gap between posts on first page in seconds
|
||||||
|
|
||||||
|
return {
|
||||||
|
gap: averageGap,
|
||||||
|
newest,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const homeTooSlow = createSelector([
|
||||||
|
state => state.getIn(['timelines', 'home', 'isLoading']),
|
||||||
|
state => state.getIn(['timelines', 'home', 'isPartial']),
|
||||||
|
getHomeFeedSpeed,
|
||||||
|
], (isLoading, isPartial, speed) =>
|
||||||
|
!isLoading && !isPartial // Only if the home feed has finished loading
|
||||||
|
&& (speed.gap > (30 * 60) // If the average gap between posts is more than 20 minutes
|
||||||
|
|| (Date.now() - speed.newest) > (1000 * 3600)) // If the most recent post is from over an hour ago
|
||||||
|
);
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
hasUnread: state.getIn(['timelines', 'home', 'unread']) > 0,
|
hasUnread: state.getIn(['timelines', 'home', 'unread']) > 0,
|
||||||
isPartial: state.getIn(['timelines', 'home', 'isPartial']),
|
isPartial: state.getIn(['timelines', 'home', 'isPartial']),
|
||||||
hasAnnouncements: !state.getIn(['announcements', 'items']).isEmpty(),
|
hasAnnouncements: !state.getIn(['announcements', 'items']).isEmpty(),
|
||||||
unreadAnnouncements: state.getIn(['announcements', 'items']).count(item => !item.get('read')),
|
unreadAnnouncements: state.getIn(['announcements', 'items']).count(item => !item.get('read')),
|
||||||
showAnnouncements: state.getIn(['announcements', 'show']),
|
showAnnouncements: state.getIn(['announcements', 'show']),
|
||||||
|
tooSlow: homeTooSlow(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
class HomeTimeline extends PureComponent {
|
class HomeTimeline extends PureComponent {
|
||||||
|
@ -52,6 +83,7 @@ class HomeTimeline extends PureComponent {
|
||||||
hasAnnouncements: PropTypes.bool,
|
hasAnnouncements: PropTypes.bool,
|
||||||
unreadAnnouncements: PropTypes.number,
|
unreadAnnouncements: PropTypes.number,
|
||||||
showAnnouncements: PropTypes.bool,
|
showAnnouncements: PropTypes.bool,
|
||||||
|
tooSlow: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
handlePin = () => {
|
handlePin = () => {
|
||||||
|
@ -121,11 +153,11 @@ class HomeTimeline extends PureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { intl, hasUnread, columnId, multiColumn, hasAnnouncements, unreadAnnouncements, showAnnouncements } = this.props;
|
const { intl, hasUnread, columnId, multiColumn, tooSlow, hasAnnouncements, unreadAnnouncements, showAnnouncements } = this.props;
|
||||||
const pinned = !!columnId;
|
const pinned = !!columnId;
|
||||||
const { signedIn } = this.context.identity;
|
const { signedIn } = this.context.identity;
|
||||||
|
|
||||||
let announcementsButton = null;
|
let announcementsButton, banner;
|
||||||
|
|
||||||
if (hasAnnouncements) {
|
if (hasAnnouncements) {
|
||||||
announcementsButton = (
|
announcementsButton = (
|
||||||
|
@ -141,6 +173,10 @@ class HomeTimeline extends PureComponent {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (tooSlow) {
|
||||||
|
banner = <ExplorePrompt />;
|
||||||
|
}
|
||||||
|
|
||||||
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
|
||||||
|
@ -160,11 +196,13 @@ class HomeTimeline extends PureComponent {
|
||||||
|
|
||||||
{signedIn ? (
|
{signedIn ? (
|
||||||
<StatusListContainer
|
<StatusListContainer
|
||||||
|
prepend={banner}
|
||||||
|
alwaysPrepend
|
||||||
trackScroll={!pinned}
|
trackScroll={!pinned}
|
||||||
scrollKey={`home_timeline-${columnId}`}
|
scrollKey={`home_timeline-${columnId}`}
|
||||||
onLoadMore={this.handleLoadMore}
|
onLoadMore={this.handleLoadMore}
|
||||||
timelineId='home'
|
timelineId='home'
|
||||||
emptyMessage={<FormattedMessage id='empty_column.home' defaultMessage='Your home timeline is empty! Follow more people to fill it up. {suggestions}' values={{ suggestions: <Link to='/start'><FormattedMessage id='empty_column.home.suggestions' defaultMessage='See some suggestions' /></Link> }} />}
|
emptyMessage={<FormattedMessage id='empty_column.home' defaultMessage='Your home timeline is empty! Follow more people to fill it up.' />}
|
||||||
bindToDocument={!multiColumn}
|
bindToDocument={!multiColumn}
|
||||||
/>
|
/>
|
||||||
) : <NotSignedInIndicator />}
|
) : <NotSignedInIndicator />}
|
||||||
|
|
|
@ -3,6 +3,8 @@ import PropTypes from 'prop-types';
|
||||||
import { Check } from 'mastodon/components/check';
|
import { Check } from 'mastodon/components/check';
|
||||||
import { Icon } from 'mastodon/components/icon';
|
import { Icon } from 'mastodon/components/icon';
|
||||||
|
|
||||||
|
import ArrowSmallRight from './arrow_small_right';
|
||||||
|
|
||||||
const Step = ({ label, description, icon, completed, onClick, href }) => {
|
const Step = ({ label, description, icon, completed, onClick, href }) => {
|
||||||
const content = (
|
const content = (
|
||||||
<>
|
<>
|
||||||
|
@ -15,11 +17,9 @@ const Step = ({ label, description, icon, completed, onClick, href }) => {
|
||||||
<p>{description}</p>
|
<p>{description}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{completed && (
|
<div className={completed ? 'onboarding__steps__item__progress' : 'onboarding__steps__item__go'}>
|
||||||
<div className='onboarding__steps__item__progress'>
|
{completed ? <Check /> : <ArrowSmallRight />}
|
||||||
<Check />
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -12,20 +12,11 @@ import Column from 'mastodon/components/column';
|
||||||
import ColumnBackButton from 'mastodon/components/column_back_button';
|
import ColumnBackButton from 'mastodon/components/column_back_button';
|
||||||
import { EmptyAccount } from 'mastodon/components/empty_account';
|
import { EmptyAccount } from 'mastodon/components/empty_account';
|
||||||
import Account from 'mastodon/containers/account_container';
|
import Account from 'mastodon/containers/account_container';
|
||||||
import { me } from 'mastodon/initial_state';
|
|
||||||
import { makeGetAccount } from 'mastodon/selectors';
|
|
||||||
|
|
||||||
import ProgressIndicator from './components/progress_indicator';
|
const mapStateToProps = state => ({
|
||||||
|
|
||||||
const mapStateToProps = () => {
|
|
||||||
const getAccount = makeGetAccount();
|
|
||||||
|
|
||||||
return state => ({
|
|
||||||
account: getAccount(state, me),
|
|
||||||
suggestions: state.getIn(['suggestions', 'items']),
|
suggestions: state.getIn(['suggestions', 'items']),
|
||||||
isLoading: state.getIn(['suggestions', 'isLoading']),
|
isLoading: state.getIn(['suggestions', 'isLoading']),
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
class Follows extends PureComponent {
|
class Follows extends PureComponent {
|
||||||
|
|
||||||
|
@ -33,7 +24,6 @@ class Follows extends PureComponent {
|
||||||
onBack: PropTypes.func,
|
onBack: PropTypes.func,
|
||||||
dispatch: PropTypes.func.isRequired,
|
dispatch: PropTypes.func.isRequired,
|
||||||
suggestions: ImmutablePropTypes.list,
|
suggestions: ImmutablePropTypes.list,
|
||||||
account: ImmutablePropTypes.map,
|
|
||||||
isLoading: PropTypes.bool,
|
isLoading: PropTypes.bool,
|
||||||
multiColumn: PropTypes.bool,
|
multiColumn: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
@ -49,7 +39,7 @@ class Follows extends PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { onBack, isLoading, suggestions, account, multiColumn } = this.props;
|
const { onBack, isLoading, suggestions, multiColumn } = this.props;
|
||||||
|
|
||||||
let loadedContent;
|
let loadedContent;
|
||||||
|
|
||||||
|
@ -58,7 +48,7 @@ class Follows extends PureComponent {
|
||||||
} else if (suggestions.isEmpty()) {
|
} else if (suggestions.isEmpty()) {
|
||||||
loadedContent = <div className='follow-recommendations__empty'><FormattedMessage id='onboarding.follows.empty' defaultMessage='Unfortunately, no results can be shown right now. You can try using search or browsing the explore page to find people to follow, or try again later.' /></div>;
|
loadedContent = <div className='follow-recommendations__empty'><FormattedMessage id='onboarding.follows.empty' defaultMessage='Unfortunately, no results can be shown right now. You can try using search or browsing the explore page to find people to follow, or try again later.' /></div>;
|
||||||
} else {
|
} else {
|
||||||
loadedContent = suggestions.map(suggestion => <Account id={suggestion.get('account')} key={suggestion.get('account')} />);
|
loadedContent = suggestions.map(suggestion => <Account id={suggestion.get('account')} key={suggestion.get('account')} withBio />);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -71,8 +61,6 @@ class Follows extends PureComponent {
|
||||||
<p><FormattedMessage id='onboarding.follows.lead' defaultMessage='You curate your own home feed. The more people you follow, the more active and interesting it will be. These profiles may be a good starting point—you can always unfollow them later!' /></p>
|
<p><FormattedMessage id='onboarding.follows.lead' defaultMessage='You curate your own home feed. The more people you follow, the more active and interesting it will be. These profiles may be a good starting point—you can always unfollow them later!' /></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ProgressIndicator steps={7} completed={account.get('following_count') * 1} />
|
|
||||||
|
|
||||||
<div className='follow-recommendations'>
|
<div className='follow-recommendations'>
|
||||||
{loadedContent}
|
{loadedContent}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -18,6 +18,7 @@ import { closeOnboarding } from 'mastodon/actions/onboarding';
|
||||||
import Column from 'mastodon/features/ui/components/column';
|
import Column from 'mastodon/features/ui/components/column';
|
||||||
import { me } from 'mastodon/initial_state';
|
import { me } from 'mastodon/initial_state';
|
||||||
import { makeGetAccount } from 'mastodon/selectors';
|
import { makeGetAccount } from 'mastodon/selectors';
|
||||||
|
import { assetHost } from 'mastodon/utils/config';
|
||||||
|
|
||||||
import ArrowSmallRight from './components/arrow_small_right';
|
import ArrowSmallRight from './components/arrow_small_right';
|
||||||
import Step from './components/step';
|
import Step from './components/step';
|
||||||
|
@ -121,30 +122,26 @@ class Onboarding extends ImmutablePureComponent {
|
||||||
<div className='onboarding__steps'>
|
<div className='onboarding__steps'>
|
||||||
<Step onClick={this.handleProfileClick} href='/settings/profile' completed={(!account.get('avatar').endsWith('missing.png')) || (account.get('display_name').length > 0 && account.get('note').length > 0)} icon='address-book-o' label={<FormattedMessage id='onboarding.steps.setup_profile.title' defaultMessage='Customize your profile' />} description={<FormattedMessage id='onboarding.steps.setup_profile.body' defaultMessage='Others are more likely to interact with you with a filled out profile.' />} />
|
<Step onClick={this.handleProfileClick} href='/settings/profile' completed={(!account.get('avatar').endsWith('missing.png')) || (account.get('display_name').length > 0 && account.get('note').length > 0)} icon='address-book-o' label={<FormattedMessage id='onboarding.steps.setup_profile.title' defaultMessage='Customize your profile' />} description={<FormattedMessage id='onboarding.steps.setup_profile.body' defaultMessage='Others are more likely to interact with you with a filled out profile.' />} />
|
||||||
<Step onClick={this.handleFollowClick} completed={(account.get('following_count') * 1) >= 7} icon='user-plus' label={<FormattedMessage id='onboarding.steps.follow_people.title' defaultMessage='Find at least {count, plural, one {one person} other {# people}} to follow' values={{ count: 7 }} />} description={<FormattedMessage id='onboarding.steps.follow_people.body' defaultMessage="You curate your own home feed. Let's fill it with interesting people." />} />
|
<Step onClick={this.handleFollowClick} completed={(account.get('following_count') * 1) >= 7} icon='user-plus' label={<FormattedMessage id='onboarding.steps.follow_people.title' defaultMessage='Find at least {count, plural, one {one person} other {# people}} to follow' values={{ count: 7 }} />} description={<FormattedMessage id='onboarding.steps.follow_people.body' defaultMessage="You curate your own home feed. Let's fill it with interesting people." />} />
|
||||||
<Step onClick={this.handleComposeClick} completed={(account.get('statuses_count') * 1) >= 1} icon='pencil-square-o' label={<FormattedMessage id='onboarding.steps.publish_status.title' defaultMessage='Make your first post' />} description={<FormattedMessage id='onboarding.steps.publish_status.body' defaultMessage='Say hello to the world.' />} />
|
<Step onClick={this.handleComposeClick} completed={(account.get('statuses_count') * 1) >= 1} icon='pencil-square-o' label={<FormattedMessage id='onboarding.steps.publish_status.title' defaultMessage='Make your first post' />} description={<FormattedMessage id='onboarding.steps.publish_status.body' defaultMessage='Say hello to the world.' values={{ emoji: <img className='emojione' alt='🐘' src={`${assetHost}/emoji/1f418.svg`} /> }} />} />
|
||||||
<Step onClick={this.handleShareClick} completed={shareClicked} icon='copy' label={<FormattedMessage id='onboarding.steps.share_profile.title' defaultMessage='Share your profile' />} description={<FormattedMessage id='onboarding.steps.share_profile.body' defaultMessage='Let your friends know how to find you on Mastodon!' />} />
|
<Step onClick={this.handleShareClick} completed={shareClicked} icon='copy' label={<FormattedMessage id='onboarding.steps.share_profile.title' defaultMessage='Share your profile' />} description={<FormattedMessage id='onboarding.steps.share_profile.body' defaultMessage='Let your friends know how to find you on Mastodon!' />} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className='onboarding__lead'><FormattedMessage id='onboarding.start.skip' defaultMessage='Want to skip right ahead?' /></p>
|
<p className='onboarding__lead'><FormattedMessage id='onboarding.start.skip' defaultMessage="Don't need help getting started?" /></p>
|
||||||
|
|
||||||
<div className='onboarding__links'>
|
<div className='onboarding__links'>
|
||||||
<Link to='/explore' onClick={this.handleClose} className='onboarding__link'>
|
<Link to='/explore' onClick={this.handleClose} className='onboarding__link'>
|
||||||
|
<FormattedMessage id='onboarding.actions.go_to_explore' defaultMessage='Take me to trending' />
|
||||||
<ArrowSmallRight />
|
<ArrowSmallRight />
|
||||||
<FormattedMessage id='onboarding.actions.go_to_explore' defaultMessage="See what's trending" />
|
|
||||||
</Link>
|
</Link>
|
||||||
<Link to='/public/local' onClick={this.handleClose} className='onboarding__link'>
|
<Link to='/public/local' onClick={this.handleClose} className='onboarding__link'>
|
||||||
<ArrowSmallRight />
|
|
||||||
<FormattedMessage id='onboarding.actions.go_to_local_timeline' defaultMessage='See posts from local' />
|
<FormattedMessage id='onboarding.actions.go_to_local_timeline' defaultMessage='See posts from local' />
|
||||||
|
<ArrowSmallRight />
|
||||||
</Link>
|
</Link>
|
||||||
<Link to='/home' onClick={this.handleClose} className='onboarding__link'>
|
<Link to='/home' onClick={this.handleClose} className='onboarding__link'>
|
||||||
|
<FormattedMessage id='onboarding.actions.go_to_home' defaultMessage='Take me to my home feed' />
|
||||||
<ArrowSmallRight />
|
<ArrowSmallRight />
|
||||||
<FormattedMessage id='onboarding.actions.go_to_home' defaultMessage='See home' />
|
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='onboarding__footer'>
|
|
||||||
<button className='link-button' onClick={this.handleClose}><FormattedMessage id='onboarding.actions.close' defaultMessage="Don't show this screen again" /></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Helmet>
|
<Helmet>
|
||||||
|
|
|
@ -177,13 +177,13 @@ class Share extends PureComponent {
|
||||||
|
|
||||||
<div className='onboarding__links'>
|
<div className='onboarding__links'>
|
||||||
<Link to='/home' className='onboarding__link'>
|
<Link to='/home' className='onboarding__link'>
|
||||||
|
<FormattedMessage id='onboarding.actions.go_to_home' defaultMessage='Take me to my home feed' />
|
||||||
<ArrowSmallRight />
|
<ArrowSmallRight />
|
||||||
<FormattedMessage id='onboarding.actions.go_to_home' defaultMessage='Go to your home feed' />
|
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<Link to='/explore' className='onboarding__link'>
|
<Link to='/explore' className='onboarding__link'>
|
||||||
|
<FormattedMessage id='onboarding.actions.go_to_explore' defaultMessage='Take me to trending' />
|
||||||
<ArrowSmallRight />
|
<ArrowSmallRight />
|
||||||
<FormattedMessage id='onboarding.actions.go_to_explore' defaultMessage="See what's trending" />
|
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,8 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
|
import { getStatusList } from 'mastodon/selectors';
|
||||||
|
|
||||||
import { fetchPinnedStatuses } from '../../actions/pin_statuses';
|
import { fetchPinnedStatuses } from '../../actions/pin_statuses';
|
||||||
import ColumnBackButtonSlim from '../../components/column_back_button_slim';
|
import ColumnBackButtonSlim from '../../components/column_back_button_slim';
|
||||||
import StatusList from '../../components/status_list';
|
import StatusList from '../../components/status_list';
|
||||||
|
@ -18,7 +20,7 @@ const messages = defineMessages({
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
statusIds: state.getIn(['status_lists', 'pins', 'items']),
|
statusIds: getStatusList(state, 'pins'),
|
||||||
hasMore: !!state.getIn(['status_lists', 'pins', 'next']),
|
hasMore: !!state.getIn(['status_lists', 'pins', 'next']),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { Helmet } from 'react-helmet';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import DismissableBanner from 'mastodon/components/dismissable_banner';
|
import DismissableBanner from 'mastodon/components/dismissable_banner';
|
||||||
|
import { domain } from 'mastodon/initial_state';
|
||||||
|
|
||||||
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
|
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
|
||||||
import { connectPublicStream } from '../../actions/streaming';
|
import { connectPublicStream } from '../../actions/streaming';
|
||||||
|
@ -142,11 +143,8 @@ class PublicTimeline extends PureComponent {
|
||||||
<ColumnSettingsContainer columnId={columnId} />
|
<ColumnSettingsContainer columnId={columnId} />
|
||||||
</ColumnHeader>
|
</ColumnHeader>
|
||||||
|
|
||||||
<DismissableBanner id='public_timeline'>
|
|
||||||
<FormattedMessage id='dismissable_banner.public_timeline' defaultMessage='These are the most recent public posts from people on this and other servers of the decentralized network that this server knows about.' />
|
|
||||||
</DismissableBanner>
|
|
||||||
|
|
||||||
<StatusListContainer
|
<StatusListContainer
|
||||||
|
prepend={<DismissableBanner id='public_timeline'><FormattedMessage id='dismissable_banner.public_timeline' defaultMessage='These are the most recent public posts from people on the social web that people on {domain} follow.' values={{ domain }} /></DismissableBanner>}
|
||||||
timelineId={`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`}
|
timelineId={`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`}
|
||||||
onLoadMore={this.handleLoadMore}
|
onLoadMore={this.handleLoadMore}
|
||||||
trackScroll={!pinned}
|
trackScroll={!pinned}
|
||||||
|
|
|
@ -1,14 +1,16 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { PureComponent } from 'react';
|
import { PureComponent } from 'react';
|
||||||
|
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
|
||||||
|
|
||||||
import { Link, withRouter } from 'react-router-dom';
|
import { Link, withRouter } from 'react-router-dom';
|
||||||
|
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import { openModal } from 'mastodon/actions/modal';
|
import { openModal } from 'mastodon/actions/modal';
|
||||||
|
import { fetchServer } from 'mastodon/actions/server';
|
||||||
import { Avatar } from 'mastodon/components/avatar';
|
import { Avatar } from 'mastodon/components/avatar';
|
||||||
|
import { Icon } from 'mastodon/components/icon';
|
||||||
import { WordmarkLogo, SymbolLogo } from 'mastodon/components/logo';
|
import { WordmarkLogo, SymbolLogo } from 'mastodon/components/logo';
|
||||||
import { registrationsOpen, me } from 'mastodon/initial_state';
|
import { registrationsOpen, me } from 'mastodon/initial_state';
|
||||||
|
|
||||||
|
@ -20,6 +22,10 @@ const Account = connect(state => ({
|
||||||
</Link>
|
</Link>
|
||||||
));
|
));
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
search: { id: 'navigation_bar.search', defaultMessage: 'Search' },
|
||||||
|
});
|
||||||
|
|
||||||
const mapStateToProps = (state) => ({
|
const mapStateToProps = (state) => ({
|
||||||
signupUrl: state.getIn(['server', 'server', 'registrations', 'url'], null) || '/auth/sign_up',
|
signupUrl: state.getIn(['server', 'server', 'registrations', 'url'], null) || '/auth/sign_up',
|
||||||
});
|
});
|
||||||
|
@ -28,6 +34,9 @@ const mapDispatchToProps = (dispatch) => ({
|
||||||
openClosedRegistrationsModal() {
|
openClosedRegistrationsModal() {
|
||||||
dispatch(openModal({ modalType: 'CLOSED_REGISTRATIONS' }));
|
dispatch(openModal({ modalType: 'CLOSED_REGISTRATIONS' }));
|
||||||
},
|
},
|
||||||
|
dispatchServer() {
|
||||||
|
dispatch(fetchServer());
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
class Header extends PureComponent {
|
class Header extends PureComponent {
|
||||||
|
@ -40,18 +49,26 @@ class Header extends PureComponent {
|
||||||
openClosedRegistrationsModal: PropTypes.func,
|
openClosedRegistrationsModal: PropTypes.func,
|
||||||
location: PropTypes.object,
|
location: PropTypes.object,
|
||||||
signupUrl: PropTypes.string.isRequired,
|
signupUrl: PropTypes.string.isRequired,
|
||||||
|
dispatchServer: PropTypes.func,
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
const { dispatchServer } = this.props;
|
||||||
|
dispatchServer();
|
||||||
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { signedIn } = this.context.identity;
|
const { signedIn } = this.context.identity;
|
||||||
const { location, openClosedRegistrationsModal, signupUrl } = this.props;
|
const { location, openClosedRegistrationsModal, signupUrl, intl } = this.props;
|
||||||
|
|
||||||
let content;
|
let content;
|
||||||
|
|
||||||
if (signedIn) {
|
if (signedIn) {
|
||||||
content = (
|
content = (
|
||||||
<>
|
<>
|
||||||
{location.pathname !== '/publish' && <Link to='/publish' className='button'><FormattedMessage id='compose_form.publish_form' defaultMessage='Publish' /></Link>}
|
{location.pathname !== '/search' && <Link to='/search' className='button button-secondary' aria-label={intl.formatMessage(messages.search)}><Icon id='search' /></Link>}
|
||||||
|
{location.pathname !== '/publish' && <Link to='/publish' className='button button-secondary'><FormattedMessage id='compose_form.publish_form' defaultMessage='New post' /></Link>}
|
||||||
<Account />
|
<Account />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -96,4 +113,4 @@ class Header extends PureComponent {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Header));
|
export default injectIntl(withRouter(connect(mapStateToProps, mapDispatchToProps)(Header)));
|
||||||
|
|
|
@ -20,8 +20,7 @@ const messages = defineMessages({
|
||||||
home: { id: 'tabs_bar.home', defaultMessage: 'Home' },
|
home: { id: 'tabs_bar.home', defaultMessage: 'Home' },
|
||||||
notifications: { id: 'tabs_bar.notifications', defaultMessage: 'Notifications' },
|
notifications: { id: 'tabs_bar.notifications', defaultMessage: 'Notifications' },
|
||||||
explore: { id: 'explore.title', defaultMessage: 'Explore' },
|
explore: { id: 'explore.title', defaultMessage: 'Explore' },
|
||||||
local: { id: 'tabs_bar.local_timeline', defaultMessage: 'Local' },
|
firehose: { id: 'column.firehose', defaultMessage: 'Live feeds' },
|
||||||
federated: { id: 'tabs_bar.federated_timeline', defaultMessage: 'Federated' },
|
|
||||||
direct: { id: 'navigation_bar.direct', defaultMessage: 'Private mentions' },
|
direct: { id: 'navigation_bar.direct', defaultMessage: 'Private mentions' },
|
||||||
favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' },
|
favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' },
|
||||||
bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' },
|
bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' },
|
||||||
|
@ -43,6 +42,10 @@ class NavigationPanel extends Component {
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
isFirehoseActive = (match, location) => {
|
||||||
|
return match || location.pathname.startsWith('/public');
|
||||||
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { intl } = this.props;
|
const { intl } = this.props;
|
||||||
const { signedIn, disabledAccountId } = this.context.identity;
|
const { signedIn, disabledAccountId } = this.context.identity;
|
||||||
|
@ -70,10 +73,7 @@ class NavigationPanel extends Component {
|
||||||
{!signedIn && explorer}
|
{!signedIn && explorer}
|
||||||
|
|
||||||
{(signedIn || timelinePreview) && (
|
{(signedIn || timelinePreview) && (
|
||||||
<>
|
<ColumnLink transparent to='/public/local' isActive={this.isFirehoseActive} icon='globe' text={intl.formatMessage(messages.firehose)} />
|
||||||
<ColumnLink transparent to='/public/local' icon='users' text={intl.formatMessage(messages.local)} />
|
|
||||||
<ColumnLink transparent exact to='/public' icon='globe' text={intl.formatMessage(messages.federated)} />
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{signedIn && (
|
{signedIn && (
|
||||||
|
|
|
@ -36,8 +36,7 @@ import {
|
||||||
Status,
|
Status,
|
||||||
GettingStarted,
|
GettingStarted,
|
||||||
KeyboardShortcuts,
|
KeyboardShortcuts,
|
||||||
PublicTimeline,
|
Firehose,
|
||||||
CommunityTimeline,
|
|
||||||
AccountTimeline,
|
AccountTimeline,
|
||||||
AccountGallery,
|
AccountGallery,
|
||||||
HomeTimeline,
|
HomeTimeline,
|
||||||
|
@ -192,8 +191,11 @@ class SwitchingColumnsArea extends PureComponent {
|
||||||
<WrappedRoute path='/privacy-policy' component={PrivacyPolicy} content={children} />
|
<WrappedRoute path='/privacy-policy' component={PrivacyPolicy} content={children} />
|
||||||
|
|
||||||
<WrappedRoute path={['/home', '/timelines/home']} component={HomeTimeline} content={children} />
|
<WrappedRoute path={['/home', '/timelines/home']} component={HomeTimeline} content={children} />
|
||||||
<WrappedRoute path={['/public', '/timelines/public']} exact component={PublicTimeline} content={children} />
|
<Redirect from='/timelines/public' to='/public' exact />
|
||||||
<WrappedRoute path={['/public/local', '/timelines/public/local']} exact component={CommunityTimeline} content={children} />
|
<Redirect from='/timelines/public/local' to='/public/local' exact />
|
||||||
|
<WrappedRoute path='/public' exact component={Firehose} componentParams={{ feedType: 'public' }} content={children} />
|
||||||
|
<WrappedRoute path='/public/local' exact component={Firehose} componentParams={{ feedType: 'community' }} content={children} />
|
||||||
|
<WrappedRoute path='/public/remote' exact component={Firehose} componentParams={{ feedType: 'public:remote' }} content={children} />
|
||||||
<WrappedRoute path={['/conversations', '/timelines/direct']} component={DirectTimeline} content={children} />
|
<WrappedRoute path={['/conversations', '/timelines/direct']} component={DirectTimeline} content={children} />
|
||||||
<WrappedRoute path='/tags/:id' component={HashtagTimeline} content={children} />
|
<WrappedRoute path='/tags/:id' component={HashtagTimeline} content={children} />
|
||||||
<WrappedRoute path='/lists/:id' component={ListTimeline} content={children} />
|
<WrappedRoute path='/lists/:id' component={ListTimeline} content={children} />
|
||||||
|
|
|
@ -22,6 +22,10 @@ export function CommunityTimeline () {
|
||||||
return import(/* webpackChunkName: "features/community_timeline" */'../../community_timeline');
|
return import(/* webpackChunkName: "features/community_timeline" */'../../community_timeline');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function Firehose () {
|
||||||
|
return import(/* webpackChunkName: "features/firehose" */'../../firehose');
|
||||||
|
}
|
||||||
|
|
||||||
export function HashtagTimeline () {
|
export function HashtagTimeline () {
|
||||||
return import(/* webpackChunkName: "features/hashtag_timeline" */'../../hashtag_timeline');
|
return import(/* webpackChunkName: "features/hashtag_timeline" */'../../hashtag_timeline');
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,6 +54,7 @@
|
||||||
"account.mute_notifications_short": "Mute notifications",
|
"account.mute_notifications_short": "Mute notifications",
|
||||||
"account.mute_short": "Mute",
|
"account.mute_short": "Mute",
|
||||||
"account.muted": "Muted",
|
"account.muted": "Muted",
|
||||||
|
"account.no_bio": "No description provided.",
|
||||||
"account.open_original_page": "Open original page",
|
"account.open_original_page": "Open original page",
|
||||||
"account.posts": "Posts",
|
"account.posts": "Posts",
|
||||||
"account.posts_with_replies": "Posts and replies",
|
"account.posts_with_replies": "Posts and replies",
|
||||||
|
@ -115,6 +116,7 @@
|
||||||
"column.directory": "Browse profiles",
|
"column.directory": "Browse profiles",
|
||||||
"column.domain_blocks": "Blocked domains",
|
"column.domain_blocks": "Blocked domains",
|
||||||
"column.favourites": "Favourites",
|
"column.favourites": "Favourites",
|
||||||
|
"column.firehose": "Live feeds",
|
||||||
"column.follow_requests": "Follow requests",
|
"column.follow_requests": "Follow requests",
|
||||||
"column.home": "Home",
|
"column.home": "Home",
|
||||||
"column.lists": "Lists",
|
"column.lists": "Lists",
|
||||||
|
@ -152,7 +154,7 @@
|
||||||
"compose_form.poll.switch_to_multiple": "Change poll to allow multiple choices",
|
"compose_form.poll.switch_to_multiple": "Change poll to allow multiple choices",
|
||||||
"compose_form.poll.switch_to_single": "Change poll to allow for a single choice",
|
"compose_form.poll.switch_to_single": "Change poll to allow for a single choice",
|
||||||
"compose_form.publish": "Publish",
|
"compose_form.publish": "Publish",
|
||||||
"compose_form.publish_form": "Publish",
|
"compose_form.publish_form": "New post",
|
||||||
"compose_form.publish_loud": "{publish}!",
|
"compose_form.publish_loud": "{publish}!",
|
||||||
"compose_form.save_changes": "Save changes",
|
"compose_form.save_changes": "Save changes",
|
||||||
"compose_form.sensitive.hide": "{count, plural, one {Mark media as sensitive} other {Mark media as sensitive}}",
|
"compose_form.sensitive.hide": "{count, plural, one {Mark media as sensitive} other {Mark media as sensitive}}",
|
||||||
|
@ -203,10 +205,10 @@
|
||||||
"disabled_account_banner.text": "Your account {disabledAccount} is currently disabled.",
|
"disabled_account_banner.text": "Your account {disabledAccount} is currently disabled.",
|
||||||
"dismissable_banner.community_timeline": "These are the most recent public posts from people whose accounts are hosted by {domain}.",
|
"dismissable_banner.community_timeline": "These are the most recent public posts from people whose accounts are hosted by {domain}.",
|
||||||
"dismissable_banner.dismiss": "Dismiss",
|
"dismissable_banner.dismiss": "Dismiss",
|
||||||
"dismissable_banner.explore_links": "These news stories are being talked about by people on this and other servers of the decentralized network right now.",
|
"dismissable_banner.explore_links": "These are news stories being shared the most on the social web today. Newer news stories posted by more different people are ranked higher.",
|
||||||
"dismissable_banner.explore_statuses": "These posts from this and other servers in the decentralized network are gaining traction on this server right now.",
|
"dismissable_banner.explore_statuses": "These are posts from across the social web that are gaining traction today. Newer posts with more boosts and favourites are ranked higher.",
|
||||||
"dismissable_banner.explore_tags": "These hashtags are gaining traction among people on this and other servers of the decentralized network right now.",
|
"dismissable_banner.explore_tags": "These are hashtags that are gaining traction on the social web today. Hashtags that are used by more different people are ranked higher.",
|
||||||
"dismissable_banner.public_timeline": "These are the most recent public posts from people on this and other servers of the decentralized network that this server knows about.",
|
"dismissable_banner.public_timeline": "These are the most recent public posts from people on the social web that people on {domain} follow.",
|
||||||
"embed.instructions": "Embed this post on your website by copying the code below.",
|
"embed.instructions": "Embed this post on your website by copying the code below.",
|
||||||
"embed.preview": "Here is what it will look like:",
|
"embed.preview": "Here is what it will look like:",
|
||||||
"emoji_button.activity": "Activity",
|
"emoji_button.activity": "Activity",
|
||||||
|
@ -238,8 +240,7 @@
|
||||||
"empty_column.follow_requests": "You don't have any follow requests yet. When you receive one, it will show up here.",
|
"empty_column.follow_requests": "You don't have any follow requests yet. When you receive one, it will show up here.",
|
||||||
"empty_column.followed_tags": "You have not followed any hashtags yet. When you do, they will show up here.",
|
"empty_column.followed_tags": "You have not followed any hashtags yet. When you do, they will show up here.",
|
||||||
"empty_column.hashtag": "There is nothing in this hashtag yet.",
|
"empty_column.hashtag": "There is nothing in this hashtag yet.",
|
||||||
"empty_column.home": "Your home timeline is empty! Follow more people to fill it up. {suggestions}",
|
"empty_column.home": "Your home timeline is empty! Follow more people to fill it up.",
|
||||||
"empty_column.home.suggestions": "See some suggestions",
|
|
||||||
"empty_column.list": "There is nothing in this list yet. When members of this list publish new posts, they will appear here.",
|
"empty_column.list": "There is nothing in this list yet. When members of this list publish new posts, they will appear here.",
|
||||||
"empty_column.lists": "You don't have any lists yet. When you create one, it will show up here.",
|
"empty_column.lists": "You don't have any lists yet. When you create one, it will show up here.",
|
||||||
"empty_column.mutes": "You haven't muted any users yet.",
|
"empty_column.mutes": "You haven't muted any users yet.",
|
||||||
|
@ -273,6 +274,9 @@
|
||||||
"filter_modal.select_filter.subtitle": "Use an existing category or create a new one",
|
"filter_modal.select_filter.subtitle": "Use an existing category or create a new one",
|
||||||
"filter_modal.select_filter.title": "Filter this post",
|
"filter_modal.select_filter.title": "Filter this post",
|
||||||
"filter_modal.title.status": "Filter a post",
|
"filter_modal.title.status": "Filter a post",
|
||||||
|
"firehose.all": "All",
|
||||||
|
"firehose.local": "This server",
|
||||||
|
"firehose.remote": "Other servers",
|
||||||
"follow_request.authorize": "Authorize",
|
"follow_request.authorize": "Authorize",
|
||||||
"follow_request.reject": "Reject",
|
"follow_request.reject": "Reject",
|
||||||
"follow_requests.unlocked_explanation": "Even though your account is not locked, the {domain} staff thought you might want to review follow requests from these accounts manually.",
|
"follow_requests.unlocked_explanation": "Even though your account is not locked, the {domain} staff thought you might want to review follow requests from these accounts manually.",
|
||||||
|
@ -298,9 +302,13 @@
|
||||||
"hashtag.column_settings.tag_toggle": "Include additional tags for this column",
|
"hashtag.column_settings.tag_toggle": "Include additional tags for this column",
|
||||||
"hashtag.follow": "Follow hashtag",
|
"hashtag.follow": "Follow hashtag",
|
||||||
"hashtag.unfollow": "Unfollow hashtag",
|
"hashtag.unfollow": "Unfollow hashtag",
|
||||||
|
"home.actions.go_to_explore": "See what's trending",
|
||||||
|
"home.actions.go_to_suggestions": "Find people to follow",
|
||||||
"home.column_settings.basic": "Basic",
|
"home.column_settings.basic": "Basic",
|
||||||
"home.column_settings.show_reblogs": "Show boosts",
|
"home.column_settings.show_reblogs": "Show boosts",
|
||||||
"home.column_settings.show_replies": "Show replies",
|
"home.column_settings.show_replies": "Show replies",
|
||||||
|
"home.explore_prompt.body": "Your home feed will have a mix of posts from the hashtags you've chosen to follow, the people you've chosen to follow, and the posts they boost. It's looking pretty quiet right now, so how about:",
|
||||||
|
"home.explore_prompt.title": "This is your home base within Mastodon.",
|
||||||
"home.hide_announcements": "Hide announcements",
|
"home.hide_announcements": "Hide announcements",
|
||||||
"home.show_announcements": "Show announcements",
|
"home.show_announcements": "Show announcements",
|
||||||
"interaction_modal.description.favourite": "With an account on Mastodon, you can favourite this post to let the author know you appreciate it and save it for later.",
|
"interaction_modal.description.favourite": "With an account on Mastodon, you can favourite this post to let the author know you appreciate it and save it for later.",
|
||||||
|
@ -459,28 +467,27 @@
|
||||||
"notifications_permission_banner.title": "Never miss a thing",
|
"notifications_permission_banner.title": "Never miss a thing",
|
||||||
"onboarding.action.back": "Take me back",
|
"onboarding.action.back": "Take me back",
|
||||||
"onboarding.actions.back": "Take me back",
|
"onboarding.actions.back": "Take me back",
|
||||||
"onboarding.actions.close": "Don't show this screen again",
|
"onboarding.actions.go_to_explore": "Take me to trending",
|
||||||
"onboarding.actions.go_to_explore": "See what's trending",
|
"onboarding.actions.go_to_home": "Take me to my home feed",
|
||||||
"onboarding.actions.go_to_home": "Go to your home feed",
|
|
||||||
"onboarding.compose.template": "Hello #Mastodon!",
|
"onboarding.compose.template": "Hello #Mastodon!",
|
||||||
"onboarding.follows.empty": "Unfortunately, no results can be shown right now. You can try using search or browsing the explore page to find people to follow, or try again later.",
|
"onboarding.follows.empty": "Unfortunately, no results can be shown right now. You can try using search or browsing the explore page to find people to follow, or try again later.",
|
||||||
"onboarding.follows.lead": "You curate your own home feed. The more people you follow, the more active and interesting it will be. These profiles may be a good starting point—you can always unfollow them later!",
|
"onboarding.follows.lead": "Your home feed is the primary way to experience Mastodon. The more people you follow, the more active and interesting it will be. To get you started, here are some suggestions:",
|
||||||
"onboarding.follows.title": "Popular on Mastodon",
|
"onboarding.follows.title": "Personalize your home feed",
|
||||||
"onboarding.share.lead": "Let people know how they can find you on Mastodon!",
|
"onboarding.share.lead": "Let people know how they can find you on Mastodon!",
|
||||||
"onboarding.share.message": "I'm {username} on #Mastodon! Come follow me at {url}",
|
"onboarding.share.message": "I'm {username} on #Mastodon! Come follow me at {url}",
|
||||||
"onboarding.share.next_steps": "Possible next steps:",
|
"onboarding.share.next_steps": "Possible next steps:",
|
||||||
"onboarding.share.title": "Share your profile",
|
"onboarding.share.title": "Share your profile",
|
||||||
"onboarding.start.lead": "Your new Mastodon account is ready to go. Here's how you can make the most of it:",
|
"onboarding.start.lead": "You're now part of Mastodon, a unique, decentralized social media platform where you—not an algorithm—curate your own experience. Let's get you started on this new social frontier:",
|
||||||
"onboarding.start.skip": "Want to skip right ahead?",
|
"onboarding.start.skip": "Don't need help getting started?",
|
||||||
"onboarding.start.title": "You've made it!",
|
"onboarding.start.title": "You've made it!",
|
||||||
"onboarding.steps.follow_people.body": "You curate your own home feed. Let's fill it with interesting people.",
|
"onboarding.steps.follow_people.body": "Following interesting people is what Mastodon is all about.",
|
||||||
"onboarding.steps.follow_people.title": "Find at least {count, plural, one {one person} other {# people}} to follow",
|
"onboarding.steps.follow_people.title": "Personalize your home feed",
|
||||||
"onboarding.steps.publish_status.body": "Say hello to the world.",
|
"onboarding.steps.publish_status.body": "Say hello to the world with text, photos, videos, or polls {emoji}",
|
||||||
"onboarding.steps.publish_status.title": "Make your first post",
|
"onboarding.steps.publish_status.title": "Make your first post",
|
||||||
"onboarding.steps.setup_profile.body": "Others are more likely to interact with you with a filled out profile.",
|
"onboarding.steps.setup_profile.body": "Boost your interactions by having a comprehensive profile.",
|
||||||
"onboarding.steps.setup_profile.title": "Customize your profile",
|
"onboarding.steps.setup_profile.title": "Personalize your profile",
|
||||||
"onboarding.steps.share_profile.body": "Let your friends know how to find you on Mastodon!",
|
"onboarding.steps.share_profile.body": "Let your friends know how to find you on Mastodon",
|
||||||
"onboarding.steps.share_profile.title": "Share your profile",
|
"onboarding.steps.share_profile.title": "Share your Mastodon profile",
|
||||||
"onboarding.tips.2fa": "<strong>Did you know?</strong> You can secure your account by setting up two-factor authentication in your account settings. It works with any TOTP app of your choice, no phone number necessary!",
|
"onboarding.tips.2fa": "<strong>Did you know?</strong> You can secure your account by setting up two-factor authentication in your account settings. It works with any TOTP app of your choice, no phone number necessary!",
|
||||||
"onboarding.tips.accounts_from_other_servers": "<strong>Did you know?</strong> Since Mastodon is decentralized, some profiles you come across will be hosted on servers other than yours. And yet you can interact with them seamlessly! Their server is in the second half of their username!",
|
"onboarding.tips.accounts_from_other_servers": "<strong>Did you know?</strong> Since Mastodon is decentralized, some profiles you come across will be hosted on servers other than yours. And yet you can interact with them seamlessly! Their server is in the second half of their username!",
|
||||||
"onboarding.tips.migration": "<strong>Did you know?</strong> If you feel like {domain} is not a great server choice for you in the future, you can move to another Mastodon server without losing your followers. You can even host your own server!",
|
"onboarding.tips.migration": "<strong>Did you know?</strong> If you feel like {domain} is not a great server choice for you in the future, you can move to another Mastodon server without losing your followers. You can even host your own server!",
|
||||||
|
@ -672,9 +679,7 @@
|
||||||
"subscribed_languages.target": "Change subscribed languages for {target}",
|
"subscribed_languages.target": "Change subscribed languages for {target}",
|
||||||
"suggestions.dismiss": "Dismiss suggestion",
|
"suggestions.dismiss": "Dismiss suggestion",
|
||||||
"suggestions.header": "You might be interested in…",
|
"suggestions.header": "You might be interested in…",
|
||||||
"tabs_bar.federated_timeline": "Federated",
|
|
||||||
"tabs_bar.home": "Home",
|
"tabs_bar.home": "Home",
|
||||||
"tabs_bar.local_timeline": "Local",
|
|
||||||
"tabs_bar.notifications": "Notifications",
|
"tabs_bar.notifications": "Notifications",
|
||||||
"time_remaining.days": "{number, plural, one {# day} other {# days}} left",
|
"time_remaining.days": "{number, plural, one {# day} other {# days}} left",
|
||||||
"time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left",
|
"time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left",
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { Record as ImmutableRecord } from 'immutable';
|
||||||
|
|
||||||
import { loadingBarReducer } from 'react-redux-loading-bar';
|
import { loadingBarReducer } from 'react-redux-loading-bar';
|
||||||
import { combineReducers } from 'redux-immutable';
|
import { combineReducers } from 'redux-immutable';
|
||||||
|
|
||||||
|
@ -96,6 +98,22 @@ const reducers = {
|
||||||
reaction_deck,
|
reaction_deck,
|
||||||
};
|
};
|
||||||
|
|
||||||
const rootReducer = combineReducers(reducers);
|
// We want the root state to be an ImmutableRecord, which is an object with a defined list of keys,
|
||||||
|
// so it is properly typed and keys can be accessed using `state.<key>` syntax.
|
||||||
|
// This will allow an easy conversion to a plain object once we no longer call `get` or `getIn` on the root state
|
||||||
|
|
||||||
|
// By default with `combineReducers` it is a Collection, so we provide our own implementation to get a Record
|
||||||
|
const initialRootState = Object.fromEntries(
|
||||||
|
Object.entries(reducers).map(([name, reducer]) => [
|
||||||
|
name,
|
||||||
|
reducer(undefined, {
|
||||||
|
// empty action
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
const RootStateRecord = ImmutableRecord(initialRootState, 'RootState');
|
||||||
|
|
||||||
|
const rootReducer = combineReducers(reducers, RootStateRecord);
|
||||||
|
|
||||||
export { rootReducer };
|
export { rootReducer };
|
||||||
|
|
|
@ -17,15 +17,15 @@ import {
|
||||||
|
|
||||||
const initialState = ImmutableMap({
|
const initialState = ImmutableMap({
|
||||||
server: ImmutableMap({
|
server: ImmutableMap({
|
||||||
isLoading: true,
|
isLoading: false,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
extendedDescription: ImmutableMap({
|
extendedDescription: ImmutableMap({
|
||||||
isLoading: true,
|
isLoading: false,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
domainBlocks: ImmutableMap({
|
domainBlocks: ImmutableMap({
|
||||||
isLoading: true,
|
isLoading: false,
|
||||||
isAvailable: true,
|
isAvailable: true,
|
||||||
items: ImmutableList(),
|
items: ImmutableList(),
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -79,6 +79,10 @@ const initialState = ImmutableMap({
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
firehose: ImmutableMap({
|
||||||
|
onlyMedia: false,
|
||||||
|
}),
|
||||||
|
|
||||||
community: ImmutableMap({
|
community: ImmutableMap({
|
||||||
regex: ImmutableMap({
|
regex: ImmutableMap({
|
||||||
body: '',
|
body: '',
|
||||||
|
|
|
@ -137,3 +137,7 @@ export const getAccountHidden = createSelector([
|
||||||
], (hidden, followingOrRequested, isSelf) => {
|
], (hidden, followingOrRequested, isSelf) => {
|
||||||
return hidden && !(isSelf || followingOrRequested);
|
return hidden && !(isSelf || followingOrRequested);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const getStatusList = createSelector([
|
||||||
|
(state, type) => state.getIn(['status_lists', type, 'items']),
|
||||||
|
], (items) => items.toList());
|
||||||
|
|
|
@ -5,19 +5,6 @@ html {
|
||||||
scrollbar-color: $ui-base-color rgba($ui-base-color, 0.25);
|
scrollbar-color: $ui-base-color rgba($ui-base-color, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Change the colors of button texts
|
|
||||||
.button {
|
|
||||||
color: $white;
|
|
||||||
|
|
||||||
&.button-alternative-2 {
|
|
||||||
color: $white;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.button-tertiary {
|
|
||||||
color: $highlight-text-color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.simple_form .button.button-tertiary {
|
.simple_form .button.button-tertiary {
|
||||||
color: $highlight-text-color;
|
color: $highlight-text-color;
|
||||||
}
|
}
|
||||||
|
@ -445,26 +432,6 @@ html {
|
||||||
color: $white;
|
color: $white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button.button-tertiary {
|
|
||||||
&:hover,
|
|
||||||
&:focus,
|
|
||||||
&:active {
|
|
||||||
color: $white;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.button.button-secondary {
|
|
||||||
border-color: $darker-text-color;
|
|
||||||
color: $darker-text-color;
|
|
||||||
|
|
||||||
&:hover,
|
|
||||||
&:focus,
|
|
||||||
&:active {
|
|
||||||
border-color: darken($darker-text-color, 8%);
|
|
||||||
color: darken($darker-text-color, 8%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.flash-message.warning {
|
.flash-message.warning {
|
||||||
color: lighten($gold-star, 16%);
|
color: lighten($gold-star, 16%);
|
||||||
}
|
}
|
||||||
|
@ -662,11 +629,6 @@ html {
|
||||||
border: 1px solid lighten($ui-base-color, 8%);
|
border: 1px solid lighten($ui-base-color, 8%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dismissable-banner {
|
|
||||||
border-left: 1px solid lighten($ui-base-color, 8%);
|
|
||||||
border-right: 1px solid lighten($ui-base-color, 8%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.status__content,
|
.status__content,
|
||||||
.reply-indicator__content {
|
.reply-indicator__content {
|
||||||
a {
|
a {
|
||||||
|
|
|
@ -7,6 +7,12 @@ $classic-primary-color: #9baec8;
|
||||||
$classic-secondary-color: #d9e1e8;
|
$classic-secondary-color: #d9e1e8;
|
||||||
$classic-highlight-color: #6364ff;
|
$classic-highlight-color: #6364ff;
|
||||||
|
|
||||||
|
$blurple-600: #563acc; // Iris
|
||||||
|
$blurple-500: #6364ff; // Brand purple
|
||||||
|
$blurple-300: #858afa; // Faded Blue
|
||||||
|
$grey-600: #4e4c5a; // Trout
|
||||||
|
$grey-100: #dadaf3; // Topaz
|
||||||
|
|
||||||
// Differences
|
// Differences
|
||||||
$success-green: lighten(#3c754d, 8%);
|
$success-green: lighten(#3c754d, 8%);
|
||||||
|
|
||||||
|
@ -19,6 +25,13 @@ $ui-primary-color: #9bcbed;
|
||||||
$ui-secondary-color: $classic-base-color !default;
|
$ui-secondary-color: $classic-base-color !default;
|
||||||
$ui-highlight-color: $classic-highlight-color !default;
|
$ui-highlight-color: $classic-highlight-color !default;
|
||||||
|
|
||||||
|
$ui-button-secondary-color: $grey-600 !default;
|
||||||
|
$ui-button-secondary-border-color: $grey-600 !default;
|
||||||
|
$ui-button-secondary-focus-color: $white !default;
|
||||||
|
|
||||||
|
$ui-button-tertiary-color: $blurple-500 !default;
|
||||||
|
$ui-button-tertiary-border-color: $blurple-500 !default;
|
||||||
|
|
||||||
$primary-text-color: $black !default;
|
$primary-text-color: $black !default;
|
||||||
$darker-text-color: $classic-base-color !default;
|
$darker-text-color: $classic-base-color !default;
|
||||||
$highlight-text-color: darken($ui-highlight-color, 8%) !default;
|
$highlight-text-color: darken($ui-highlight-color, 8%) !default;
|
||||||
|
|
|
@ -128,7 +128,6 @@ $content-width: 840px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.selected {
|
&.selected {
|
||||||
background: darken($ui-base-color, 2%);
|
|
||||||
border-radius: 4px 0 0;
|
border-radius: 4px 0 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -146,13 +145,9 @@ $content-width: 840px;
|
||||||
|
|
||||||
.simple-navigation-active-leaf a {
|
.simple-navigation-active-leaf a {
|
||||||
color: $primary-text-color;
|
color: $primary-text-color;
|
||||||
background-color: darken($ui-highlight-color, 2%);
|
background-color: $ui-highlight-color;
|
||||||
border-bottom: 0;
|
border-bottom: 0;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: $ui-highlight-color;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -246,12 +241,6 @@ $content-width: 840px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: $primary-text-color;
|
color: $primary-text-color;
|
||||||
background: $ui-highlight-color;
|
background: $ui-highlight-color;
|
||||||
|
|
||||||
&:hover,
|
|
||||||
&:focus,
|
|
||||||
&:active {
|
|
||||||
background: lighten($ui-highlight-color, 4%);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,11 +47,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
background-color: darken($ui-highlight-color, 2%);
|
background-color: $ui-button-background-color;
|
||||||
border: 10px none;
|
border: 10px none;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
color: $primary-text-color;
|
color: $ui-button-color;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
|
@ -71,14 +71,14 @@
|
||||||
&:active,
|
&:active,
|
||||||
&:focus,
|
&:focus,
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: $ui-highlight-color;
|
background-color: $ui-button-focus-background-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
&--destructive {
|
&--destructive {
|
||||||
&:active,
|
&:active,
|
||||||
&:focus,
|
&:focus,
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: $error-red;
|
background-color: $ui-button-destructive-focus-background-color;
|
||||||
transition: none;
|
transition: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -108,38 +108,18 @@
|
||||||
outline: 0 !important;
|
outline: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.button-alternative {
|
|
||||||
color: $inverted-text-color;
|
|
||||||
background: $ui-primary-color;
|
|
||||||
|
|
||||||
&:active,
|
|
||||||
&:focus,
|
|
||||||
&:hover {
|
|
||||||
background-color: lighten($ui-primary-color, 4%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.button-alternative-2 {
|
|
||||||
background: $ui-base-lighter-color;
|
|
||||||
|
|
||||||
&:active,
|
|
||||||
&:focus,
|
|
||||||
&:hover {
|
|
||||||
background-color: lighten($ui-base-lighter-color, 4%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.button-secondary {
|
&.button-secondary {
|
||||||
color: $darker-text-color;
|
color: $ui-button-secondary-color;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
padding: 6px 17px;
|
padding: 6px 17px;
|
||||||
border: 1px solid $ui-primary-color;
|
border: 1px solid $ui-button-secondary-border-color;
|
||||||
|
|
||||||
&:active,
|
&:active,
|
||||||
&:focus,
|
&:focus,
|
||||||
&:hover {
|
&:hover {
|
||||||
border-color: lighten($ui-primary-color, 4%);
|
border-color: $ui-button-secondary-focus-background-color;
|
||||||
color: lighten($darker-text-color, 4%);
|
color: $ui-button-secondary-focus-color;
|
||||||
|
background-color: $ui-button-secondary-focus-background-color;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -151,14 +131,14 @@
|
||||||
&.button-tertiary {
|
&.button-tertiary {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
padding: 6px 17px;
|
padding: 6px 17px;
|
||||||
color: $highlight-text-color;
|
color: $ui-button-tertiary-color;
|
||||||
border: 1px solid $highlight-text-color;
|
border: 1px solid $ui-button-tertiary-border-color;
|
||||||
|
|
||||||
&:active,
|
&:active,
|
||||||
&:focus,
|
&:focus,
|
||||||
&:hover {
|
&:hover {
|
||||||
background: $ui-highlight-color;
|
background-color: $ui-button-tertiary-focus-background-color;
|
||||||
color: $primary-text-color;
|
color: $ui-button-tertiary-focus-color;
|
||||||
border: 0;
|
border: 0;
|
||||||
padding: 7px 18px;
|
padding: 7px 18px;
|
||||||
}
|
}
|
||||||
|
@ -1569,12 +1549,37 @@ body > [data-popper-placement] {
|
||||||
}
|
}
|
||||||
|
|
||||||
&__note {
|
&__note {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 400;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
-webkit-line-clamp: 2;
|
-webkit-line-clamp: 1;
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
color: $ui-secondary-color;
|
margin-top: 10px;
|
||||||
|
color: $darker-text-color;
|
||||||
|
|
||||||
|
&--missing {
|
||||||
|
color: $dark-text-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus,
|
||||||
|
&:active {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2672,13 +2677,15 @@ $ui-header-height: 55px;
|
||||||
.onboarding__link {
|
.onboarding__link {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
color: $highlight-text-color;
|
color: $highlight-text-color;
|
||||||
background: lighten($ui-base-color, 4%);
|
background: lighten($ui-base-color, 4%);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 10px;
|
padding: 10px 15px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 17px;
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
height: 56px;
|
height: 56px;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
||||||
|
@ -2740,6 +2747,7 @@ $ui-header-height: 55px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
|
padding-inline-end: 15px;
|
||||||
margin-bottom: 2px;
|
margin-bottom: 2px;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
text-align: start;
|
text-align: start;
|
||||||
|
@ -2752,14 +2760,14 @@ $ui-header-height: 55px;
|
||||||
|
|
||||||
&__icon {
|
&__icon {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
background: $ui-base-color;
|
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
display: none;
|
display: none;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 36px;
|
width: 36px;
|
||||||
height: 36px;
|
height: 36px;
|
||||||
color: $dark-text-color;
|
color: $highlight-text-color;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
|
||||||
@media screen and (width >= 600px) {
|
@media screen and (width >= 600px) {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -2783,16 +2791,33 @@ $ui-header-height: 55px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__go {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 21px;
|
||||||
|
height: 21px;
|
||||||
|
color: $highlight-text-color;
|
||||||
|
font-size: 17px;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
height: 1.5em;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&__description {
|
&__description {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
line-height: 18px;
|
line-height: 20px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
h6 {
|
h6 {
|
||||||
color: $primary-text-color;
|
color: $highlight-text-color;
|
||||||
font-weight: 700;
|
font-weight: 500;
|
||||||
|
font-size: 14px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
@ -3156,7 +3181,7 @@ $ui-header-height: 55px;
|
||||||
.column-back-button {
|
.column-back-button {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background: lighten($ui-base-color, 4%);
|
background: $ui-base-color;
|
||||||
border-radius: 4px 4px 0 0;
|
border-radius: 4px 4px 0 0;
|
||||||
color: $highlight-text-color;
|
color: $highlight-text-color;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
@ -3164,6 +3189,7 @@ $ui-header-height: 55px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
line-height: inherit;
|
line-height: inherit;
|
||||||
border: 0;
|
border: 0;
|
||||||
|
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
||||||
text-align: unset;
|
text-align: unset;
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
@ -3176,7 +3202,7 @@ $ui-header-height: 55px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.column-header__back-button {
|
.column-header__back-button {
|
||||||
background: lighten($ui-base-color, 4%);
|
background: $ui-base-color;
|
||||||
border: 0;
|
border: 0;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
color: $highlight-text-color;
|
color: $highlight-text-color;
|
||||||
|
@ -3211,7 +3237,7 @@ $ui-header-height: 55px;
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset-inline-end: 0;
|
inset-inline-end: 0;
|
||||||
top: -48px;
|
top: -50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-toggle {
|
.react-toggle {
|
||||||
|
@ -3892,7 +3918,8 @@ a.status-card.compact:hover {
|
||||||
.column-header {
|
.column-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
background: lighten($ui-base-color, 4%);
|
background: $ui-base-color;
|
||||||
|
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
||||||
border-radius: 4px 4px 0 0;
|
border-radius: 4px 4px 0 0;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
@ -3947,7 +3974,7 @@ a.status-card.compact:hover {
|
||||||
}
|
}
|
||||||
|
|
||||||
.column-header__button {
|
.column-header__button {
|
||||||
background: lighten($ui-base-color, 4%);
|
background: $ui-base-color;
|
||||||
border: 0;
|
border: 0;
|
||||||
color: $darker-text-color;
|
color: $darker-text-color;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
@ -3955,16 +3982,15 @@ a.status-card.compact:hover {
|
||||||
padding: 0 15px;
|
padding: 0 15px;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: lighten($darker-text-color, 7%);
|
color: lighten($darker-text-color, 4%);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
color: $primary-text-color;
|
color: $primary-text-color;
|
||||||
background: lighten($ui-base-color, 8%);
|
background: lighten($ui-base-color, 4%);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: $primary-text-color;
|
color: $primary-text-color;
|
||||||
background: lighten($ui-base-color, 8%);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3978,6 +4004,7 @@ a.status-card.compact:hover {
|
||||||
max-height: 70vh;
|
max-height: 70vh;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
||||||
color: $darker-text-color;
|
color: $darker-text-color;
|
||||||
transition: max-height 150ms ease-in-out, opacity 300ms linear;
|
transition: max-height 150ms ease-in-out, opacity 300ms linear;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
@ -3997,13 +4024,13 @@ a.status-card.compact:hover {
|
||||||
height: 0;
|
height: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-top: 1px solid lighten($ui-base-color, 12%);
|
border-top: 1px solid lighten($ui-base-color, 8%);
|
||||||
margin: 10px 0;
|
margin: 10px 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.column-header__collapsible-inner {
|
.column-header__collapsible-inner {
|
||||||
background: lighten($ui-base-color, 8%);
|
background: $ui-base-color;
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4428,17 +4455,13 @@ a.status-card.compact:hover {
|
||||||
color: $primary-text-color;
|
color: $primary-text-color;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
display: block;
|
display: block;
|
||||||
background-color: $base-overlay-background;
|
background-color: rgba($black, 0.45);
|
||||||
text-transform: uppercase;
|
backdrop-filter: blur(10px) saturate(180%) contrast(75%) brightness(70%);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: 500;
|
text-transform: uppercase;
|
||||||
padding: 4px;
|
font-weight: 700;
|
||||||
|
padding: 2px 6px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
opacity: 0.7;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.setting-toggle {
|
.setting-toggle {
|
||||||
|
@ -4498,6 +4521,7 @@ a.status-card.compact:hover {
|
||||||
|
|
||||||
.follow_requests-unlocked_explanation {
|
.follow_requests-unlocked_explanation {
|
||||||
background: darken($ui-base-color, 4%);
|
background: darken($ui-base-color, 4%);
|
||||||
|
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
||||||
contain: initial;
|
contain: initial;
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
}
|
}
|
||||||
|
@ -5852,15 +5876,15 @@ a.status-card.compact:hover {
|
||||||
}
|
}
|
||||||
|
|
||||||
.button.button-secondary {
|
.button.button-secondary {
|
||||||
border-color: $inverted-text-color;
|
border-color: $ui-button-secondary-border-color;
|
||||||
color: $inverted-text-color;
|
color: $ui-button-secondary-color;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
|
|
||||||
&:hover,
|
&:hover,
|
||||||
&:focus,
|
&:focus,
|
||||||
&:active {
|
&:active {
|
||||||
border-color: lighten($inverted-text-color, 15%);
|
border-color: $ui-button-secondary-focus-background-color;
|
||||||
color: lighten($inverted-text-color, 15%);
|
color: $ui-button-secondary-focus-color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6203,6 +6227,7 @@ a.status-card.compact:hover {
|
||||||
display: block;
|
display: block;
|
||||||
color: $white;
|
color: $white;
|
||||||
background: rgba($black, 0.65);
|
background: rgba($black, 0.65);
|
||||||
|
backdrop-filter: blur(10px) saturate(180%) contrast(75%) brightness(70%);
|
||||||
padding: 2px 6px;
|
padding: 2px 6px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
|
@ -6896,24 +6921,6 @@ a.status-card.compact:hover {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.directory__section-headline {
|
|
||||||
background: darken($ui-base-color, 2%);
|
|
||||||
border-bottom-color: transparent;
|
|
||||||
|
|
||||||
a,
|
|
||||||
button {
|
|
||||||
&.active {
|
|
||||||
&::before {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
border-color: transparent transparent darken($ui-base-color, 7%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-form {
|
.filter-form {
|
||||||
|
@ -7466,7 +7473,6 @@ noscript {
|
||||||
|
|
||||||
.account__header {
|
.account__header {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: lighten($ui-base-color, 4%);
|
|
||||||
|
|
||||||
&.inactive {
|
&.inactive {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
|
@ -7488,6 +7494,7 @@ noscript {
|
||||||
height: 145px;
|
height: 145px;
|
||||||
position: relative;
|
position: relative;
|
||||||
background: darken($ui-base-color, 4%);
|
background: darken($ui-base-color, 4%);
|
||||||
|
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
||||||
|
|
||||||
img {
|
img {
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
|
@ -7501,7 +7508,7 @@ noscript {
|
||||||
&__bar {
|
&__bar {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 0 20px;
|
padding: 0 20px;
|
||||||
border-bottom: 1px solid lighten($ui-base-color, 12%);
|
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
||||||
|
|
||||||
.avatar {
|
.avatar {
|
||||||
display: block;
|
display: block;
|
||||||
|
@ -7510,7 +7517,7 @@ noscript {
|
||||||
|
|
||||||
.account__avatar {
|
.account__avatar {
|
||||||
background: darken($ui-base-color, 8%);
|
background: darken($ui-base-color, 8%);
|
||||||
border: 2px solid lighten($ui-base-color, 4%);
|
border: 2px solid $ui-base-color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8838,27 +8845,80 @@ noscript {
|
||||||
}
|
}
|
||||||
|
|
||||||
.dismissable-banner {
|
.dismissable-banner {
|
||||||
background: $ui-base-color;
|
position: relative;
|
||||||
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
margin: 10px;
|
||||||
display: flex;
|
margin-bottom: 5px;
|
||||||
align-items: center;
|
border-radius: 8px;
|
||||||
gap: 30px;
|
border: 1px solid $highlight-text-color;
|
||||||
|
background: rgba($highlight-text-color, 0.15);
|
||||||
|
padding-inline-end: 45px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&__background-image {
|
||||||
|
width: 125%;
|
||||||
|
position: absolute;
|
||||||
|
bottom: -25%;
|
||||||
|
inset-inline-end: -25%;
|
||||||
|
z-index: -1;
|
||||||
|
opacity: 0.15;
|
||||||
|
mix-blend-mode: luminosity;
|
||||||
|
}
|
||||||
|
|
||||||
&__message {
|
&__message {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
padding: 20px 15px;
|
padding: 15px;
|
||||||
cursor: default;
|
font-size: 15px;
|
||||||
font-size: 14px;
|
line-height: 22px;
|
||||||
line-height: 18px;
|
font-weight: 500;
|
||||||
color: $primary-text-color;
|
color: $primary-text-color;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
color: $highlight-text-color;
|
||||||
|
font-size: 22px;
|
||||||
|
line-height: 33px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
&__wrapper {
|
||||||
|
display: flex;
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
display: block;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-tertiary {
|
||||||
|
background: rgba($ui-base-color, 0.15);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__action {
|
&__action {
|
||||||
padding: 15px;
|
position: absolute;
|
||||||
flex: 0 0 auto;
|
inset-inline-end: 0;
|
||||||
display: flex;
|
top: 0;
|
||||||
align-items: center;
|
padding: 10px;
|
||||||
justify-content: center;
|
|
||||||
|
.icon-button {
|
||||||
|
color: $highlight-text-color;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -81,7 +81,7 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background: darken($ui-highlight-color, 2%);
|
background: $ui-button-background-color;
|
||||||
color: $primary-text-color;
|
color: $primary-text-color;
|
||||||
transition: all 100ms ease-in;
|
transition: all 100ms ease-in;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
@ -94,7 +94,7 @@
|
||||||
&:active,
|
&:active,
|
||||||
&:focus,
|
&:focus,
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: $ui-highlight-color;
|
background-color: $ui-button-focus-background-color;
|
||||||
transition: all 200ms ease-out;
|
transition: all 200ms ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -511,8 +511,8 @@ code {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background: darken($ui-highlight-color, 2%);
|
background: $ui-button-background-color;
|
||||||
color: $primary-text-color;
|
color: $ui-button-color;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
line-height: inherit;
|
line-height: inherit;
|
||||||
height: auto;
|
height: auto;
|
||||||
|
@ -534,7 +534,7 @@ code {
|
||||||
&:active,
|
&:active,
|
||||||
&:focus,
|
&:focus,
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: $ui-highlight-color;
|
background-color: $ui-button-focus-background-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:disabled:hover {
|
&:disabled:hover {
|
||||||
|
@ -542,15 +542,12 @@ code {
|
||||||
}
|
}
|
||||||
|
|
||||||
&.negative {
|
&.negative {
|
||||||
background: $error-value-color;
|
background: $ui-button-destructive-background-color;
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: lighten($error-value-color, 5%);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
&:hover,
|
||||||
&:active,
|
&:active,
|
||||||
&:focus {
|
&:focus {
|
||||||
background-color: darken($error-value-color, 5%);
|
background-color: $ui-button-destructive-focus-background-color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,16 @@
|
||||||
// Commonly used web colors
|
// Commonly used web colors
|
||||||
$black: #000000; // Black
|
$black: #000000; // Black
|
||||||
$white: #ffffff; // White
|
$white: #ffffff; // White
|
||||||
|
$red-600: #b7253d !default; // Deep Carmine
|
||||||
|
$red-500: #df405a !default; // Cerise
|
||||||
|
$blurple-600: #563acc; // Iris
|
||||||
|
$blurple-500: #6364ff; // Brand purple
|
||||||
|
$blurple-300: #858afa; // Faded Blue
|
||||||
|
$grey-600: #4e4c5a; // Trout
|
||||||
|
$grey-100: #dadaf3; // Topaz
|
||||||
|
|
||||||
$success-green: #79bd9a !default; // Padua
|
$success-green: #79bd9a !default; // Padua
|
||||||
$error-red: #df405a !default; // Cerise
|
$error-red: $red-500 !default; // Cerise
|
||||||
$warning-red: #ff5050 !default; // Sunset Orange
|
$warning-red: #ff5050 !default; // Sunset Orange
|
||||||
$gold-star: #ca8f04 !default; // Dark Goldenrod
|
$gold-star: #ca8f04 !default; // Dark Goldenrod
|
||||||
$kmyblue: #29a5f7 !default;
|
$kmyblue: #29a5f7 !default;
|
||||||
|
@ -32,6 +40,22 @@ $ui-base-lighter-color: lighten(
|
||||||
$ui-primary-color: $classic-primary-color !default; // Lighter
|
$ui-primary-color: $classic-primary-color !default; // Lighter
|
||||||
$ui-secondary-color: $classic-secondary-color !default; // Lightest
|
$ui-secondary-color: $classic-secondary-color !default; // Lightest
|
||||||
$ui-highlight-color: $classic-highlight-color !default;
|
$ui-highlight-color: $classic-highlight-color !default;
|
||||||
|
$ui-button-color: $white !default;
|
||||||
|
$ui-button-background-color: $blurple-500 !default;
|
||||||
|
$ui-button-focus-background-color: $blurple-600 !default;
|
||||||
|
|
||||||
|
$ui-button-secondary-color: $grey-100 !default;
|
||||||
|
$ui-button-secondary-border-color: $grey-100 !default;
|
||||||
|
$ui-button-secondary-focus-background-color: $grey-600 !default;
|
||||||
|
$ui-button-secondary-focus-color: $white !default;
|
||||||
|
|
||||||
|
$ui-button-tertiary-color: $blurple-300 !default;
|
||||||
|
$ui-button-tertiary-border-color: $blurple-300 !default;
|
||||||
|
$ui-button-tertiary-focus-background-color: $blurple-600 !default;
|
||||||
|
$ui-button-tertiary-focus-color: $white !default;
|
||||||
|
|
||||||
|
$ui-button-destructive-background-color: $red-500 !default;
|
||||||
|
$ui-button-destructive-focus-background-color: $red-600 !default;
|
||||||
|
|
||||||
// Variables for texts
|
// Variables for texts
|
||||||
$primary-text-color: $white !default;
|
$primary-text-color: $white !default;
|
||||||
|
@ -40,6 +64,7 @@ $dark-text-color: $ui-base-lighter-color !default;
|
||||||
$secondary-text-color: $ui-secondary-color !default;
|
$secondary-text-color: $ui-secondary-color !default;
|
||||||
$highlight-text-color: lighten($ui-highlight-color, 8%) !default;
|
$highlight-text-color: lighten($ui-highlight-color, 8%) !default;
|
||||||
$action-button-color: $ui-base-lighter-color !default;
|
$action-button-color: $ui-base-lighter-color !default;
|
||||||
|
$action-button-focus-color: lighten($ui-base-lighter-color, 4%) !default;
|
||||||
$passive-text-color: $gold-star !default;
|
$passive-text-color: $gold-star !default;
|
||||||
$active-passive-text-color: $success-green !default;
|
$active-passive-text-color: $success-green !default;
|
||||||
|
|
||||||
|
|
111
app/lib/attachment_batch.rb
Normal file
111
app/lib/attachment_batch.rb
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AttachmentBatch
|
||||||
|
# Maximum amount of objects you can delete in an S3 API call. It's
|
||||||
|
# important to remember that this does not correspond to the number
|
||||||
|
# of records in the batch, since records can have multiple attachments
|
||||||
|
LIMIT = 1_000
|
||||||
|
|
||||||
|
# Attributes generated and maintained by Paperclip (not all of them
|
||||||
|
# are always used on every class, however)
|
||||||
|
NULLABLE_ATTRIBUTES = %w(
|
||||||
|
file_name
|
||||||
|
content_type
|
||||||
|
file_size
|
||||||
|
fingerprint
|
||||||
|
created_at
|
||||||
|
updated_at
|
||||||
|
).freeze
|
||||||
|
|
||||||
|
# Styles that are always present even when not explicitly defined
|
||||||
|
BASE_STYLES = %i(original).freeze
|
||||||
|
|
||||||
|
attr_reader :klass, :records, :storage_mode
|
||||||
|
|
||||||
|
def initialize(klass, records)
|
||||||
|
@klass = klass
|
||||||
|
@records = records
|
||||||
|
@storage_mode = Paperclip::Attachment.default_options[:storage]
|
||||||
|
@attachment_names = klass.attachment_definitions.keys
|
||||||
|
end
|
||||||
|
|
||||||
|
def delete
|
||||||
|
remove_files
|
||||||
|
batch.delete_all
|
||||||
|
end
|
||||||
|
|
||||||
|
def clear
|
||||||
|
remove_files
|
||||||
|
batch.update_all(nullified_attributes) # rubocop:disable Rails/SkipsModelValidations
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def batch
|
||||||
|
klass.where(id: records.map(&:id))
|
||||||
|
end
|
||||||
|
|
||||||
|
def remove_files
|
||||||
|
keys = []
|
||||||
|
|
||||||
|
logger.debug { "Preparing to delete attachments for #{records.size} records" }
|
||||||
|
|
||||||
|
records.each do |record|
|
||||||
|
@attachment_names.each do |attachment_name|
|
||||||
|
attachment = record.public_send(attachment_name)
|
||||||
|
styles = BASE_STYLES | attachment.styles.keys
|
||||||
|
|
||||||
|
next if attachment.blank?
|
||||||
|
|
||||||
|
styles.each do |style|
|
||||||
|
case @storage_mode
|
||||||
|
when :s3
|
||||||
|
logger.debug { "Adding #{attachment.path(style)} to batch for deletion" }
|
||||||
|
keys << attachment.style_name_as_path(style)
|
||||||
|
when :filesystem
|
||||||
|
logger.debug { "Deleting #{attachment.path(style)}" }
|
||||||
|
path = attachment.path(style)
|
||||||
|
FileUtils.remove_file(path, true)
|
||||||
|
|
||||||
|
begin
|
||||||
|
FileUtils.rmdir(File.dirname(path), parents: true)
|
||||||
|
rescue Errno::EEXIST, Errno::ENOTEMPTY, Errno::ENOENT, Errno::EINVAL, Errno::ENOTDIR, Errno::EACCES
|
||||||
|
# Ignore failure to delete a directory, with the same ignored errors
|
||||||
|
# as Paperclip
|
||||||
|
end
|
||||||
|
when :fog
|
||||||
|
logger.debug { "Deleting #{attachment.path(style)}" }
|
||||||
|
attachment.directory.files.new(key: attachment.path(style)).destroy
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return unless storage_mode == :s3
|
||||||
|
|
||||||
|
# We can batch deletes over S3, but there is a limit of how many
|
||||||
|
# objects can be processed at once, so we have to potentially
|
||||||
|
# separate them into multiple calls.
|
||||||
|
|
||||||
|
keys.each_slice(LIMIT) do |keys_slice|
|
||||||
|
logger.debug { "Deleting #{keys_slice.size} objects" }
|
||||||
|
|
||||||
|
bucket.delete_objects(delete: {
|
||||||
|
objects: keys_slice.map { |key| { key: key } },
|
||||||
|
quiet: true,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def bucket
|
||||||
|
@bucket ||= records.first.public_send(@attachment_names.first).s3_bucket
|
||||||
|
end
|
||||||
|
|
||||||
|
def nullified_attributes
|
||||||
|
@attachment_names.flat_map { |attachment_name| NULLABLE_ATTRIBUTES.map { |attribute| "#{attachment_name}_#{attribute}" } & klass.column_names }.index_with(nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
def logger
|
||||||
|
Rails.logger
|
||||||
|
end
|
||||||
|
end
|
|
@ -15,15 +15,15 @@ class Vacuum::MediaAttachmentsVacuum
|
||||||
private
|
private
|
||||||
|
|
||||||
def vacuum_cached_files!
|
def vacuum_cached_files!
|
||||||
media_attachments_past_retention_period.find_each do |media_attachment|
|
media_attachments_past_retention_period.find_in_batches do |media_attachments|
|
||||||
media_attachment.file.destroy
|
AttachmentBatch.new(MediaAttachment, media_attachments).clear
|
||||||
media_attachment.thumbnail.destroy
|
|
||||||
media_attachment.save
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def vacuum_orphaned_records!
|
def vacuum_orphaned_records!
|
||||||
orphaned_media_attachments.in_batches.destroy_all
|
orphaned_media_attachments.find_in_batches do |media_attachments|
|
||||||
|
AttachmentBatch.new(MediaAttachment, media_attachments).delete
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def media_attachments_past_retention_period
|
def media_attachments_past_retention_period
|
||||||
|
|
|
@ -117,7 +117,7 @@ class Account < ApplicationRecord
|
||||||
scope :matches_username, ->(value) { where('lower((username)::text) ~ lower(?)', value.to_s) }
|
scope :matches_username, ->(value) { where('lower((username)::text) ~ lower(?)', value.to_s) }
|
||||||
scope :matches_display_name, ->(value) { where(arel_table[:display_name].matches_regexp(value.to_s)) }
|
scope :matches_display_name, ->(value) { where(arel_table[:display_name].matches_regexp(value.to_s)) }
|
||||||
scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) }
|
scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) }
|
||||||
scope :without_unapproved, -> { left_outer_joins(:user).remote.or(left_outer_joins(:user).merge(User.approved.confirmed)) }
|
scope :without_unapproved, -> { left_outer_joins(:user).merge(User.approved.confirmed).or(remote) }
|
||||||
scope :searchable, -> { without_unapproved.without_suspended.where(moved_to_account_id: nil) }
|
scope :searchable, -> { without_unapproved.without_suspended.where(moved_to_account_id: nil) }
|
||||||
scope :discoverable, -> { searchable.without_silenced.where(discoverable: true).left_outer_joins(:account_stat) }
|
scope :discoverable, -> { searchable.without_silenced.where(discoverable: true).left_outer_joins(:account_stat) }
|
||||||
scope :followable_by, ->(account) { joins(arel_table.join(Follow.arel_table, Arel::Nodes::OuterJoin).on(arel_table[:id].eq(Follow.arel_table[:target_account_id]).and(Follow.arel_table[:account_id].eq(account.id))).join_sources).where(Follow.arel_table[:id].eq(nil)).joins(arel_table.join(FollowRequest.arel_table, Arel::Nodes::OuterJoin).on(arel_table[:id].eq(FollowRequest.arel_table[:target_account_id]).and(FollowRequest.arel_table[:account_id].eq(account.id))).join_sources).where(FollowRequest.arel_table[:id].eq(nil)) }
|
scope :followable_by, ->(account) { joins(arel_table.join(Follow.arel_table, Arel::Nodes::OuterJoin).on(arel_table[:id].eq(Follow.arel_table[:target_account_id]).and(Follow.arel_table[:account_id].eq(account.id))).join_sources).where(Follow.arel_table[:id].eq(nil)).joins(arel_table.join(FollowRequest.arel_table, Arel::Nodes::OuterJoin).on(arel_table[:id].eq(FollowRequest.arel_table[:target_account_id]).and(FollowRequest.arel_table[:account_id].eq(account.id))).join_sources).where(FollowRequest.arel_table[:id].eq(nil)) }
|
||||||
|
|
|
@ -106,6 +106,17 @@ module AccountSearch
|
||||||
LIMIT :limit OFFSET :offset
|
LIMIT :limit OFFSET :offset
|
||||||
SQL
|
SQL
|
||||||
|
|
||||||
|
def searchable_text
|
||||||
|
PlainTextFormatter.new(note, local?).to_s if discoverable?
|
||||||
|
end
|
||||||
|
|
||||||
|
def searchable_properties
|
||||||
|
[].tap do |properties|
|
||||||
|
properties << 'bot' if bot?
|
||||||
|
properties << 'verified' if fields.any?(&:verified?)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
class_methods do
|
class_methods do
|
||||||
def search_for(terms, limit: 10, offset: 0)
|
def search_for(terms, limit: 10, offset: 0)
|
||||||
tsquery = generate_query_for_search(terms)
|
tsquery = generate_query_for_search(terms)
|
||||||
|
|
|
@ -24,6 +24,8 @@ class Webhook < ApplicationRecord
|
||||||
status.updated
|
status.updated
|
||||||
).freeze
|
).freeze
|
||||||
|
|
||||||
|
attr_writer :current_account
|
||||||
|
|
||||||
scope :enabled, -> { where(enabled: true) }
|
scope :enabled, -> { where(enabled: true) }
|
||||||
|
|
||||||
validates :url, presence: true, url: true
|
validates :url, presence: true, url: true
|
||||||
|
@ -31,6 +33,7 @@ class Webhook < ApplicationRecord
|
||||||
validates :events, presence: true
|
validates :events, presence: true
|
||||||
|
|
||||||
validate :validate_events
|
validate :validate_events
|
||||||
|
validate :validate_permissions
|
||||||
validate :validate_template
|
validate :validate_template
|
||||||
|
|
||||||
before_validation :strip_events
|
before_validation :strip_events
|
||||||
|
@ -48,12 +51,31 @@ class Webhook < ApplicationRecord
|
||||||
update!(enabled: false)
|
update!(enabled: false)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def required_permissions
|
||||||
|
events.map { |event| Webhook.permission_for_event(event) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.permission_for_event(event)
|
||||||
|
case event
|
||||||
|
when 'account.approved', 'account.created', 'account.updated'
|
||||||
|
:manage_users
|
||||||
|
when 'report.created'
|
||||||
|
:manage_reports
|
||||||
|
when 'status.created', 'status.updated'
|
||||||
|
:view_devops
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def validate_events
|
def validate_events
|
||||||
errors.add(:events, :invalid) if events.any? { |e| EVENTS.exclude?(e) }
|
errors.add(:events, :invalid) if events.any? { |e| EVENTS.exclude?(e) }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def validate_permissions
|
||||||
|
errors.add(:events, :invalid_permissions) if defined?(@current_account) && required_permissions.any? { |permission| !@current_account.user_role.can?(permission) }
|
||||||
|
end
|
||||||
|
|
||||||
def validate_template
|
def validate_template
|
||||||
return if template.blank?
|
return if template.blank?
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ class WebhookPolicy < ApplicationPolicy
|
||||||
end
|
end
|
||||||
|
|
||||||
def update?
|
def update?
|
||||||
role.can?(:manage_webhooks)
|
role.can?(:manage_webhooks) && record.required_permissions.all? { |permission| role.can?(permission) }
|
||||||
end
|
end
|
||||||
|
|
||||||
def enable?
|
def enable?
|
||||||
|
@ -30,6 +30,6 @@ class WebhookPolicy < ApplicationPolicy
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy?
|
def destroy?
|
||||||
role.can?(:manage_webhooks)
|
role.can?(:manage_webhooks) && record.required_permissions.all? { |permission| role.can?(permission) }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -9,7 +9,6 @@ class AccountSearchService < BaseService
|
||||||
MIN_QUERY_LENGTH = 5
|
MIN_QUERY_LENGTH = 5
|
||||||
|
|
||||||
def call(query, account = nil, options = {})
|
def call(query, account = nil, options = {})
|
||||||
@acct_hint = query&.start_with?('@')
|
|
||||||
@query = query&.strip&.gsub(/\A@/, '')
|
@query = query&.strip&.gsub(/\A@/, '')
|
||||||
@limit = options[:limit].to_i
|
@limit = options[:limit].to_i
|
||||||
@offset = options[:offset].to_i
|
@offset = options[:offset].to_i
|
||||||
|
@ -72,8 +71,8 @@ class AccountSearchService < BaseService
|
||||||
end
|
end
|
||||||
|
|
||||||
def from_elasticsearch
|
def from_elasticsearch
|
||||||
must_clauses = [{ multi_match: { query: terms_for_query, fields: likely_acct? ? %w(acct.edge_ngram acct) : %w(acct.edge_ngram acct display_name.edge_ngram display_name), type: 'most_fields', operator: 'and' } }]
|
must_clauses = must_clause
|
||||||
should_clauses = []
|
should_clauses = should_clause
|
||||||
|
|
||||||
if account
|
if account
|
||||||
return [] if options[:following] && following_ids.empty?
|
return [] if options[:following] && following_ids.empty?
|
||||||
|
@ -88,7 +87,7 @@ class AccountSearchService < BaseService
|
||||||
query = { bool: { must: must_clauses, should: should_clauses } }
|
query = { bool: { must: must_clauses, should: should_clauses } }
|
||||||
functions = [reputation_score_function, followers_score_function, time_distance_function]
|
functions = [reputation_score_function, followers_score_function, time_distance_function]
|
||||||
|
|
||||||
records = AccountsIndex.query(function_score: { query: query, functions: functions, boost_mode: 'multiply', score_mode: 'avg' })
|
records = AccountsIndex.query(function_score: { query: query, functions: functions })
|
||||||
.limit(limit_for_non_exact_results)
|
.limit(limit_for_non_exact_results)
|
||||||
.offset(offset)
|
.offset(offset)
|
||||||
.objects
|
.objects
|
||||||
|
@ -133,6 +132,36 @@ class AccountSearchService < BaseService
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def must_clause
|
||||||
|
fields = %w(username username.* display_name display_name.*)
|
||||||
|
fields << 'text' << 'text.*' if options[:use_searchable_text]
|
||||||
|
|
||||||
|
[
|
||||||
|
{
|
||||||
|
multi_match: {
|
||||||
|
query: terms_for_query,
|
||||||
|
fields: fields,
|
||||||
|
type: 'best_fields',
|
||||||
|
operator: 'or',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
def should_clause
|
||||||
|
[
|
||||||
|
{
|
||||||
|
multi_match: {
|
||||||
|
query: terms_for_query,
|
||||||
|
fields: %w(username username.* display_name display_name.*),
|
||||||
|
type: 'best_fields',
|
||||||
|
operator: 'and',
|
||||||
|
boost: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
def following_ids
|
def following_ids
|
||||||
@following_ids ||= account.active_relationships.pluck(:target_account_id) + [account.id]
|
@following_ids ||= account.active_relationships.pluck(:target_account_id) + [account.id]
|
||||||
end
|
end
|
||||||
|
@ -182,8 +211,4 @@ class AccountSearchService < BaseService
|
||||||
def username_complete?
|
def username_complete?
|
||||||
query.include?('@') && "@#{query}".match?(MENTION_ONLY_RE)
|
query.include?('@') && "@#{query}".match?(MENTION_ONLY_RE)
|
||||||
end
|
end
|
||||||
|
|
||||||
def likely_acct?
|
|
||||||
@acct_hint || username_complete?
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -10,14 +10,6 @@ class ClearDomainMediaService < BaseService
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def invalidate_association_caches!(status_ids)
|
|
||||||
# Normally, associated models of a status are immutable (except for accounts)
|
|
||||||
# so they are aggressively cached. After updating the media attachments to no
|
|
||||||
# longer point to a local file, we need to clear the cache to make those
|
|
||||||
# changes appear in the API and UI
|
|
||||||
Rails.cache.delete_multi(status_ids.map { |id| "statuses/#{id}" })
|
|
||||||
end
|
|
||||||
|
|
||||||
def clear_media!
|
def clear_media!
|
||||||
clear_account_images!
|
clear_account_images!
|
||||||
clear_account_attachments!
|
clear_account_attachments!
|
||||||
|
@ -25,31 +17,21 @@ class ClearDomainMediaService < BaseService
|
||||||
end
|
end
|
||||||
|
|
||||||
def clear_account_images!
|
def clear_account_images!
|
||||||
blocked_domain_accounts.reorder(nil).find_each do |account|
|
blocked_domain_accounts.reorder(nil).find_in_batches do |accounts|
|
||||||
account.avatar.destroy if account.avatar&.exists?
|
AttachmentBatch.new(Account, accounts).clear
|
||||||
account.header.destroy if account.header&.exists?
|
|
||||||
account.save
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def clear_account_attachments!
|
def clear_account_attachments!
|
||||||
media_from_blocked_domain.reorder(nil).find_in_batches do |attachments|
|
media_from_blocked_domain.reorder(nil).find_in_batches do |attachments|
|
||||||
affected_status_ids = []
|
AttachmentBatch.new(MediaAttachment, attachments).clear
|
||||||
|
|
||||||
attachments.each do |attachment|
|
|
||||||
affected_status_ids << attachment.status_id if attachment.status_id.present?
|
|
||||||
|
|
||||||
attachment.file.destroy if attachment.file&.exists?
|
|
||||||
attachment.type = :unknown
|
|
||||||
attachment.save
|
|
||||||
end
|
|
||||||
|
|
||||||
invalidate_association_caches!(affected_status_ids) unless affected_status_ids.empty?
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def clear_emojos!
|
def clear_emojos!
|
||||||
emojis_from_blocked_domains.destroy_all
|
emojis_from_blocked_domains.find_in_batches do |custom_emojis|
|
||||||
|
AttachmentBatch.new(CustomEmoji, custom_emojis).delete
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def blocked_domain
|
def blocked_domain
|
||||||
|
|
|
@ -89,13 +89,28 @@ class ResolveURLService < BaseService
|
||||||
def process_local_url
|
def process_local_url
|
||||||
recognized_params = Rails.application.routes.recognize_path(@url)
|
recognized_params = Rails.application.routes.recognize_path(@url)
|
||||||
|
|
||||||
|
case recognized_params[:controller]
|
||||||
|
when 'statuses'
|
||||||
return unless recognized_params[:action] == 'show'
|
return unless recognized_params[:action] == 'show'
|
||||||
|
|
||||||
if recognized_params[:controller] == 'statuses'
|
|
||||||
status = Status.find_by(id: recognized_params[:id])
|
status = Status.find_by(id: recognized_params[:id])
|
||||||
check_local_status(status)
|
check_local_status(status)
|
||||||
elsif recognized_params[:controller] == 'accounts'
|
when 'accounts'
|
||||||
|
return unless recognized_params[:action] == 'show'
|
||||||
|
|
||||||
Account.find_local(recognized_params[:username])
|
Account.find_local(recognized_params[:username])
|
||||||
|
when 'home'
|
||||||
|
return unless recognized_params[:action] == 'index' && recognized_params[:username_with_domain].present?
|
||||||
|
|
||||||
|
if recognized_params[:any]&.match?(/\A[0-9]+\Z/)
|
||||||
|
status = Status.find_by(id: recognized_params[:any])
|
||||||
|
check_local_status(status)
|
||||||
|
elsif recognized_params[:any].blank?
|
||||||
|
username, domain = recognized_params[:username_with_domain].gsub(/\A@/, '').split('@')
|
||||||
|
return unless username.present? && domain.present?
|
||||||
|
|
||||||
|
Account.find_remote(username, domain)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ class SearchService < BaseService
|
||||||
@limit = limit.to_i
|
@limit = limit.to_i
|
||||||
@offset = options[:type].blank? ? 0 : options[:offset].to_i
|
@offset = options[:type].blank? ? 0 : options[:offset].to_i
|
||||||
@resolve = options[:resolve] || false
|
@resolve = options[:resolve] || false
|
||||||
@searchability = options[:searchability] || 'public'
|
@following = options[:following] || false
|
||||||
|
|
||||||
default_results.tap do |results|
|
default_results.tap do |results|
|
||||||
next if @query.blank? || @limit.zero?
|
next if @query.blank? || @limit.zero?
|
||||||
|
@ -36,7 +36,9 @@ class SearchService < BaseService
|
||||||
@account,
|
@account,
|
||||||
limit: @limit,
|
limit: @limit,
|
||||||
resolve: @resolve,
|
resolve: @resolve,
|
||||||
offset: @offset
|
offset: @offset,
|
||||||
|
use_searchable_text: true,
|
||||||
|
following: @following
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
- content_for :page_title do
|
- content_for :page_title do
|
||||||
= t('.title', domain: Addressable::IDNA.to_unicode(@domain_block.domain))
|
= t('.title', domain: Addressable::IDNA.to_unicode(@domain_block.domain))
|
||||||
|
|
||||||
= simple_form_for @domain_block, url: admin_domain_blocks_path(@domain_block) do |f|
|
= simple_form_for @domain_block, url: admin_domain_blocks_path, method: :post do |f|
|
||||||
|
|
||||||
%p.hint= t('.preamble_html', domain: Addressable::IDNA.to_unicode(@domain_block.domain))
|
%p.hint= t('.preamble_html', domain: Addressable::IDNA.to_unicode(@domain_block.domain))
|
||||||
%ul.hint
|
%ul.hint
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
= f.input :url, wrapper: :with_block_label, input_html: { placeholder: 'https://' }
|
= f.input :url, wrapper: :with_block_label, input_html: { placeholder: 'https://' }
|
||||||
|
|
||||||
.fields-group
|
.fields-group
|
||||||
= f.input :events, collection: Webhook::EVENTS, wrapper: :with_block_label, include_blank: false, as: :check_boxes, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li'
|
= f.input :events, collection: Webhook::EVENTS, wrapper: :with_block_label, include_blank: false, as: :check_boxes, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li', disabled: Webhook::EVENTS.filter { |event| !current_user.role.can?(Webhook.permission_for_event(event)) }
|
||||||
|
|
||||||
.fields-group
|
.fields-group
|
||||||
= f.input :template, wrapper: :with_block_label, input_html: { placeholder: '{ "content": "Hello {{object.username}}" }' }
|
= f.input :template, wrapper: :with_block_label, input_html: { placeholder: '{ "content": "Hello {{object.username}}" }' }
|
||||||
|
|
|
@ -6,12 +6,4 @@ end
|
||||||
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
|
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
|
||||||
|
|
||||||
require 'bundler/setup' # Set up gems listed in the Gemfile.
|
require 'bundler/setup' # Set up gems listed in the Gemfile.
|
||||||
require 'bootsnap' # Speed up boot time by caching expensive operations.
|
require 'bootsnap/setup' # Speed up boot time by caching expensive operations.
|
||||||
|
|
||||||
Bootsnap.setup(
|
|
||||||
cache_dir: File.expand_path('../tmp/cache', __dir__),
|
|
||||||
development_mode: ENV.fetch('RAILS_ENV', 'development') == 'development',
|
|
||||||
load_path_cache: true,
|
|
||||||
compile_cache_iseq: false,
|
|
||||||
compile_cache_yaml: false
|
|
||||||
)
|
|
||||||
|
|
|
@ -53,3 +53,7 @@ en:
|
||||||
position:
|
position:
|
||||||
elevated: cannot be higher than your current role
|
elevated: cannot be higher than your current role
|
||||||
own_role: cannot be changed with your current role
|
own_role: cannot be changed with your current role
|
||||||
|
webhook:
|
||||||
|
attributes:
|
||||||
|
events:
|
||||||
|
invalid_permissions: cannot include events you don't have the rights to
|
||||||
|
|
|
@ -12,6 +12,7 @@ Rails.application.routes.draw do
|
||||||
/home
|
/home
|
||||||
/public
|
/public
|
||||||
/public/local
|
/public/local
|
||||||
|
/public/remote
|
||||||
/conversations
|
/conversations
|
||||||
/lists/(*any)
|
/lists/(*any)
|
||||||
/notifications
|
/notifications
|
||||||
|
@ -105,8 +106,6 @@ Rails.application.routes.draw do
|
||||||
|
|
||||||
resources :followers, only: [:index], controller: :follower_accounts
|
resources :followers, only: [:index], controller: :follower_accounts
|
||||||
resources :following, only: [:index], controller: :following_accounts
|
resources :following, only: [:index], controller: :following_accounts
|
||||||
resource :follow, only: [:create], controller: :account_follow
|
|
||||||
resource :unfollow, only: [:create], controller: :account_unfollow
|
|
||||||
|
|
||||||
resource :outbox, only: [:show], module: :activitypub
|
resource :outbox, only: [:show], module: :activitypub
|
||||||
resource :inbox, only: [:create], module: :activitypub
|
resource :inbox, only: [:create], module: :activitypub
|
||||||
|
@ -167,7 +166,7 @@ Rails.application.routes.draw do
|
||||||
get '/backups/:id/download', to: 'backups#download', as: :download_backup, format: false
|
get '/backups/:id/download', to: 'backups#download', as: :download_backup, format: false
|
||||||
|
|
||||||
resource :authorize_interaction, only: [:show, :create]
|
resource :authorize_interaction, only: [:show, :create]
|
||||||
resource :share, only: [:show, :create]
|
resource :share, only: [:show]
|
||||||
|
|
||||||
draw(:admin)
|
draw(:admin)
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
namespace :admin do
|
namespace :admin do
|
||||||
get '/dashboard', to: 'dashboard#index'
|
get '/dashboard', to: 'dashboard#index'
|
||||||
|
|
||||||
resources :domain_allows, only: [:new, :create, :show, :destroy]
|
resources :domain_allows, only: [:new, :create, :destroy]
|
||||||
resources :domain_blocks, only: [:new, :create, :destroy, :update, :edit] do
|
resources :domain_blocks, only: [:new, :create, :destroy, :update, :edit] do
|
||||||
collection do
|
collection do
|
||||||
post :batch
|
post :batch
|
||||||
|
@ -31,7 +31,7 @@ namespace :admin do
|
||||||
end
|
end
|
||||||
|
|
||||||
resources :action_logs, only: [:index]
|
resources :action_logs, only: [:index]
|
||||||
resources :warning_presets, except: [:new]
|
resources :warning_presets, except: [:new, :show]
|
||||||
resources :media_attachments, only: [:index]
|
resources :media_attachments, only: [:index]
|
||||||
|
|
||||||
resources :announcements, except: [:show] do
|
resources :announcements, except: [:show] do
|
||||||
|
@ -76,7 +76,7 @@ namespace :admin do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
resources :rules
|
resources :rules, only: [:index, :create, :edit, :update, :destroy]
|
||||||
|
|
||||||
resources :webhooks do
|
resources :webhooks do
|
||||||
member do
|
member do
|
||||||
|
|
|
@ -89,6 +89,7 @@ namespace :api, format: false do
|
||||||
resources :conversations, only: [:index, :destroy] do
|
resources :conversations, only: [:index, :destroy] do
|
||||||
member do
|
member do
|
||||||
post :read
|
post :read
|
||||||
|
post :unread
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
|
skip_untranslated_strings: 1
|
||||||
commit_message: '[ci skip]'
|
commit_message: '[ci skip]'
|
||||||
skip_untranslated_strings: true
|
|
||||||
|
|
||||||
files:
|
files:
|
||||||
- source: /app/javascript/mastodon/locales/en.json
|
- source: /app/javascript/mastodon/locales/en.json
|
||||||
translation: /app/javascript/mastodon/locales/%two_letters_code%.json
|
translation: /app/javascript/mastodon/locales/%two_letters_code%.json
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AddIndexBackupsOnUserId < ActiveRecord::Migration[6.1]
|
||||||
|
disable_ddl_transaction!
|
||||||
|
|
||||||
|
def change
|
||||||
|
add_index :backups, :user_id, algorithm: :concurrently
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,9 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AddSuperappIndexToApplications < ActiveRecord::Migration[6.1]
|
||||||
|
disable_ddl_transaction!
|
||||||
|
|
||||||
|
def change
|
||||||
|
add_index :oauth_applications, :superapp, where: 'superapp = true', algorithm: :concurrently
|
||||||
|
end
|
||||||
|
end
|
|
@ -12,7 +12,7 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema.define(version: 2023_06_05_085711) do
|
ActiveRecord::Schema.define(version: 2023_07_02_151753) do
|
||||||
|
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
|
@ -346,6 +346,7 @@ ActiveRecord::Schema.define(version: 2023_06_05_085711) do
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
t.bigint "dump_file_size"
|
t.bigint "dump_file_size"
|
||||||
|
t.index ["user_id"], name: "index_backups_on_user_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "blocks", force: :cascade do |t|
|
create_table "blocks", force: :cascade do |t|
|
||||||
|
@ -804,6 +805,7 @@ ActiveRecord::Schema.define(version: 2023_06_05_085711) do
|
||||||
t.bigint "owner_id"
|
t.bigint "owner_id"
|
||||||
t.boolean "confidential", default: true, null: false
|
t.boolean "confidential", default: true, null: false
|
||||||
t.index ["owner_id", "owner_type"], name: "index_oauth_applications_on_owner_id_and_owner_type"
|
t.index ["owner_id", "owner_type"], name: "index_oauth_applications_on_owner_id_and_owner_type"
|
||||||
|
t.index ["superapp"], name: "index_oauth_applications_on_superapp", where: "(superapp = true)"
|
||||||
t.index ["uid"], name: "index_oauth_applications_on_uid", unique: true
|
t.index ["uid"], name: "index_oauth_applications_on_uid", unique: true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -1218,6 +1220,7 @@ ActiveRecord::Schema.define(version: 2023_06_05_085711) do
|
||||||
t.index ["email"], name: "index_users_on_email", unique: true
|
t.index ["email"], name: "index_users_on_email", unique: true
|
||||||
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, opclass: :text_pattern_ops, where: "(reset_password_token IS NOT NULL)"
|
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, opclass: :text_pattern_ops, where: "(reset_password_token IS NOT NULL)"
|
||||||
t.index ["role_id"], name: "index_users_on_role_id", where: "(role_id IS NOT NULL)"
|
t.index ["role_id"], name: "index_users_on_role_id", where: "(role_id IS NOT NULL)"
|
||||||
|
t.index ["unconfirmed_email"], name: "index_users_on_unconfirmed_email", where: "(unconfirmed_email IS NOT NULL)"
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "web_push_subscriptions", force: :cascade do |t|
|
create_table "web_push_subscriptions", force: :cascade do |t|
|
||||||
|
|
|
@ -23,7 +23,8 @@ RSpec.describe Admin::ChangeEmailsController do
|
||||||
|
|
||||||
describe 'GET #update' do
|
describe 'GET #update' do
|
||||||
before do
|
before do
|
||||||
allow(UserMailer).to receive(:confirmation_instructions).and_return(double('email', deliver_later: nil))
|
allow(UserMailer).to receive(:confirmation_instructions)
|
||||||
|
.and_return(instance_double(ActionMailer::MessageDelivery, deliver_later: nil))
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns http success' do
|
it 'returns http success' do
|
||||||
|
|
|
@ -38,7 +38,7 @@ RSpec.describe Admin::ConfirmationsController do
|
||||||
let!(:user) { Fabricate(:user, confirmed_at: confirmed_at) }
|
let!(:user) { Fabricate(:user, confirmed_at: confirmed_at) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
allow(UserMailer).to receive(:confirmation_instructions) { double(:email, deliver_later: nil) }
|
allow(UserMailer).to receive(:confirmation_instructions) { instance_double(ActionMailer::MessageDelivery, deliver_later: nil) }
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when email is not confirmed' do
|
context 'when email is not confirmed' do
|
||||||
|
|
|
@ -19,7 +19,8 @@ RSpec.describe Admin::Disputes::AppealsController do
|
||||||
let(:current_user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
|
let(:current_user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
allow(UserMailer).to receive(:appeal_approved).and_return(double('email', deliver_later: nil))
|
allow(UserMailer).to receive(:appeal_approved)
|
||||||
|
.and_return(instance_double(ActionMailer::MessageDelivery, deliver_later: nil))
|
||||||
post :approve, params: { id: appeal.id }
|
post :approve, params: { id: appeal.id }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -40,7 +41,8 @@ RSpec.describe Admin::Disputes::AppealsController do
|
||||||
let(:current_user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
|
let(:current_user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
allow(UserMailer).to receive(:appeal_rejected).and_return(double('email', deliver_later: nil))
|
allow(UserMailer).to receive(:appeal_rejected)
|
||||||
|
.and_return(instance_double(ActionMailer::MessageDelivery, deliver_later: nil))
|
||||||
post :reject, params: { id: appeal.id }
|
post :reject, params: { id: appeal.id }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -37,7 +37,7 @@ RSpec.describe Admin::DomainAllowsController do
|
||||||
|
|
||||||
describe 'DELETE #destroy' do
|
describe 'DELETE #destroy' do
|
||||||
it 'disallows the domain' do
|
it 'disallows the domain' do
|
||||||
service = double(call: true)
|
service = instance_double(UnallowDomainService, call: true)
|
||||||
allow(UnallowDomainService).to receive(:new).and_return(service)
|
allow(UnallowDomainService).to receive(:new).and_return(service)
|
||||||
domain_allow = Fabricate(:domain_allow)
|
domain_allow = Fabricate(:domain_allow)
|
||||||
delete :destroy, params: { id: domain_allow.id }
|
delete :destroy, params: { id: domain_allow.id }
|
||||||
|
|
|
@ -213,7 +213,7 @@ RSpec.describe Admin::DomainBlocksController do
|
||||||
|
|
||||||
describe 'DELETE #destroy' do
|
describe 'DELETE #destroy' do
|
||||||
it 'unblocks the domain' do
|
it 'unblocks the domain' do
|
||||||
service = double(call: true)
|
service = instance_double(UnblockDomainService, call: true)
|
||||||
allow(UnblockDomainService).to receive(:new).and_return(service)
|
allow(UnblockDomainService).to receive(:new).and_return(service)
|
||||||
domain_block = Fabricate(:domain_block)
|
domain_block = Fabricate(:domain_block)
|
||||||
delete :destroy, params: { id: domain_block.id }
|
delete :destroy, params: { id: domain_block.id }
|
||||||
|
|
|
@ -62,17 +62,10 @@ describe Admin::Reports::ActionsController do
|
||||||
end
|
end
|
||||||
|
|
||||||
shared_examples 'common behavior' do
|
shared_examples 'common behavior' do
|
||||||
it 'closes the report' do
|
it 'closes the report and redirects' do
|
||||||
expect { subject }.to change { report.reload.action_taken? }.from(false).to(true)
|
expect { subject }.to mark_report_action_taken.and create_target_account_strike
|
||||||
end
|
|
||||||
|
|
||||||
it 'creates a strike with the expected text' do
|
|
||||||
expect { subject }.to change { report.target_account.strikes.count }.by(1)
|
|
||||||
expect(report.target_account.strikes.last.text).to eq text
|
expect(report.target_account.strikes.last.text).to eq text
|
||||||
end
|
|
||||||
|
|
||||||
it 'redirects' do
|
|
||||||
subject
|
|
||||||
expect(response).to redirect_to(admin_reports_path)
|
expect(response).to redirect_to(admin_reports_path)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -81,20 +74,21 @@ describe Admin::Reports::ActionsController do
|
||||||
{ report_id: report.id }
|
{ report_id: report.id }
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'closes the report' do
|
it 'closes the report and redirects' do
|
||||||
expect { subject }.to change { report.reload.action_taken? }.from(false).to(true)
|
expect { subject }.to mark_report_action_taken.and create_target_account_strike
|
||||||
end
|
|
||||||
|
|
||||||
it 'creates a strike with the expected text' do
|
|
||||||
expect { subject }.to change { report.target_account.strikes.count }.by(1)
|
|
||||||
expect(report.target_account.strikes.last.text).to eq ''
|
expect(report.target_account.strikes.last.text).to eq ''
|
||||||
end
|
|
||||||
|
|
||||||
it 'redirects' do
|
|
||||||
subject
|
|
||||||
expect(response).to redirect_to(admin_reports_path)
|
expect(response).to redirect_to(admin_reports_path)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def mark_report_action_taken
|
||||||
|
change { report.reload.action_taken? }.from(false).to(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_target_account_strike
|
||||||
|
change { report.target_account.strikes.count }.by(1)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
shared_examples 'all action types' do
|
shared_examples 'all action types' do
|
||||||
|
|
|
@ -48,7 +48,7 @@ describe Admin::WebhooksController do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with an existing record' do
|
context 'with an existing record' do
|
||||||
let!(:webhook) { Fabricate :webhook }
|
let!(:webhook) { Fabricate(:webhook, events: ['account.created', 'report.created']) }
|
||||||
|
|
||||||
describe 'GET #show' do
|
describe 'GET #show' do
|
||||||
it 'returns http success and renders view' do
|
it 'returns http success and renders view' do
|
||||||
|
|
|
@ -5,19 +5,124 @@ require 'rails_helper'
|
||||||
describe Api::V1::DirectoriesController do
|
describe Api::V1::DirectoriesController do
|
||||||
render_views
|
render_views
|
||||||
|
|
||||||
let(:user) { Fabricate(:user) }
|
let(:user) { Fabricate(:user, confirmed_at: nil) }
|
||||||
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:follows') }
|
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:follows') }
|
||||||
let(:account) { Fabricate(:account) }
|
|
||||||
|
|
||||||
before do
|
before do
|
||||||
allow(controller).to receive(:doorkeeper_token) { token }
|
allow(controller).to receive(:doorkeeper_token) { token }
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'GET #show' do
|
describe 'GET #show' do
|
||||||
it 'returns http success' do
|
context 'with no params' do
|
||||||
|
before do
|
||||||
|
_local_unconfirmed_account = Fabricate(
|
||||||
|
:account,
|
||||||
|
domain: nil,
|
||||||
|
user: Fabricate(:user, confirmed_at: nil, approved: true),
|
||||||
|
username: 'local_unconfirmed'
|
||||||
|
)
|
||||||
|
|
||||||
|
local_unapproved_account = Fabricate(
|
||||||
|
:account,
|
||||||
|
domain: nil,
|
||||||
|
user: Fabricate(:user, confirmed_at: 10.days.ago),
|
||||||
|
username: 'local_unapproved'
|
||||||
|
)
|
||||||
|
local_unapproved_account.user.update(approved: false)
|
||||||
|
|
||||||
|
_local_undiscoverable_account = Fabricate(
|
||||||
|
:account,
|
||||||
|
domain: nil,
|
||||||
|
user: Fabricate(:user, confirmed_at: 10.days.ago, approved: true),
|
||||||
|
discoverable: false,
|
||||||
|
username: 'local_undiscoverable'
|
||||||
|
)
|
||||||
|
|
||||||
|
excluded_from_timeline_account = Fabricate(
|
||||||
|
:account,
|
||||||
|
domain: 'host.example',
|
||||||
|
discoverable: true,
|
||||||
|
username: 'remote_excluded_from_timeline'
|
||||||
|
)
|
||||||
|
Fabricate(:block, account: user.account, target_account: excluded_from_timeline_account)
|
||||||
|
|
||||||
|
_domain_blocked_account = Fabricate(
|
||||||
|
:account,
|
||||||
|
domain: 'test.example',
|
||||||
|
discoverable: true,
|
||||||
|
username: 'remote_domain_blocked'
|
||||||
|
)
|
||||||
|
Fabricate(:account_domain_block, account: user.account, domain: 'test.example')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns only the local discoverable account' do
|
||||||
|
local_discoverable_account = Fabricate(
|
||||||
|
:account,
|
||||||
|
domain: nil,
|
||||||
|
user: Fabricate(:user, confirmed_at: 10.days.ago, approved: true),
|
||||||
|
discoverable: true,
|
||||||
|
username: 'local_discoverable'
|
||||||
|
)
|
||||||
|
|
||||||
|
eligible_remote_account = Fabricate(
|
||||||
|
:account,
|
||||||
|
domain: 'host.example',
|
||||||
|
discoverable: true,
|
||||||
|
username: 'eligible_remote'
|
||||||
|
)
|
||||||
|
|
||||||
get :show
|
get :show
|
||||||
|
|
||||||
expect(response).to have_http_status(200)
|
expect(response).to have_http_status(200)
|
||||||
|
expect(body_as_json.size).to eq(2)
|
||||||
|
expect(body_as_json.first[:id]).to include(eligible_remote_account.id.to_s)
|
||||||
|
expect(body_as_json.second[:id]).to include(local_discoverable_account.id.to_s)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when asking for local accounts only' do
|
||||||
|
it 'returns only the local accounts' do
|
||||||
|
user = Fabricate(:user, confirmed_at: 10.days.ago, approved: true)
|
||||||
|
local_account = Fabricate(:account, domain: nil, user: user)
|
||||||
|
remote_account = Fabricate(:account, domain: 'host.example')
|
||||||
|
|
||||||
|
get :show, params: { local: '1' }
|
||||||
|
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
expect(body_as_json.size).to eq(1)
|
||||||
|
expect(body_as_json.first[:id]).to include(local_account.id.to_s)
|
||||||
|
expect(response.body).to_not include(remote_account.id.to_s)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when ordered by active' do
|
||||||
|
it 'returns accounts in order of most recent status activity' do
|
||||||
|
status_old = Fabricate(:status)
|
||||||
|
travel_to 10.seconds.from_now
|
||||||
|
status_new = Fabricate(:status)
|
||||||
|
|
||||||
|
get :show, params: { order: 'active' }
|
||||||
|
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
expect(body_as_json.size).to eq(2)
|
||||||
|
expect(body_as_json.first[:id]).to include(status_new.account.id.to_s)
|
||||||
|
expect(body_as_json.second[:id]).to include(status_old.account.id.to_s)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when ordered by new' do
|
||||||
|
it 'returns accounts in order of creation' do
|
||||||
|
account_old = Fabricate(:account)
|
||||||
|
travel_to 10.seconds.from_now
|
||||||
|
account_new = Fabricate(:account)
|
||||||
|
|
||||||
|
get :show, params: { order: 'new' }
|
||||||
|
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
expect(body_as_json.size).to eq(2)
|
||||||
|
expect(body_as_json.first[:id]).to include(account_new.id.to_s)
|
||||||
|
expect(body_as_json.second[:id]).to include(account_old.id.to_s)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -130,5 +130,13 @@ RSpec.describe Api::V1::Emails::ConfirmationsController do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'without an oauth token and an authentication cookie' do
|
||||||
|
it 'returns http unauthorized' do
|
||||||
|
get :check
|
||||||
|
|
||||||
|
expect(response).to have_http_status(401)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -23,7 +23,8 @@ RSpec.describe Api::V1::ReportsController do
|
||||||
let(:rule_ids) { nil }
|
let(:rule_ids) { nil }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
allow(AdminMailer).to receive(:new_report).and_return(double('email', deliver_later: nil))
|
allow(AdminMailer).to receive(:new_report)
|
||||||
|
.and_return(instance_double(ActionMailer::MessageDelivery, deliver_later: nil))
|
||||||
post :create, params: { status_ids: [status.id], account_id: target_account.id, comment: 'reasons', category: category, rule_ids: rule_ids, forward: forward }
|
post :create, params: { status_ids: [status.id], account_id: target_account.id, comment: 'reasons', category: category, rule_ids: rule_ids, forward: forward }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,7 @@ describe Api::V1::Statuses::HistoriesController do
|
||||||
|
|
||||||
it 'returns http success' do
|
it 'returns http success' do
|
||||||
expect(response).to have_http_status(200)
|
expect(response).to have_http_status(200)
|
||||||
|
expect(body_as_json.size).to_not be 0
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,37 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require 'rails_helper'
|
|
||||||
|
|
||||||
RSpec.describe Api::V1::SuggestionsController do
|
|
||||||
render_views
|
|
||||||
|
|
||||||
let(:user) { Fabricate(:user) }
|
|
||||||
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read write') }
|
|
||||||
|
|
||||||
before do
|
|
||||||
allow(controller).to receive(:doorkeeper_token) { token }
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'GET #index' do
|
|
||||||
let(:bob) { Fabricate(:account) }
|
|
||||||
let(:jeff) { Fabricate(:account) }
|
|
||||||
|
|
||||||
before do
|
|
||||||
PotentialFriendshipTracker.record(user.account_id, bob.id, :reblog)
|
|
||||||
PotentialFriendshipTracker.record(user.account_id, jeff.id, :favourite)
|
|
||||||
|
|
||||||
get :index
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns http success' do
|
|
||||||
expect(response).to have_http_status(200)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns accounts' do
|
|
||||||
json = body_as_json
|
|
||||||
|
|
||||||
expect(json.size).to be >= 1
|
|
||||||
expect(json.pluck(:id)).to include(*[bob, jeff].map { |i| i.id.to_s })
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -14,13 +14,40 @@ RSpec.describe Api::V2::SearchController do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'GET #index' do
|
describe 'GET #index' do
|
||||||
before do
|
let!(:bob) { Fabricate(:account, username: 'bob_test') }
|
||||||
get :index, params: { q: 'test' }
|
let!(:ana) { Fabricate(:account, username: 'ana_test') }
|
||||||
end
|
let!(:tom) { Fabricate(:account, username: 'tom_test') }
|
||||||
|
let(:params) { { q: 'test' } }
|
||||||
|
|
||||||
it 'returns http success' do
|
it 'returns http success' do
|
||||||
|
get :index, params: params
|
||||||
|
|
||||||
expect(response).to have_http_status(200)
|
expect(response).to have_http_status(200)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when searching accounts' do
|
||||||
|
let(:params) { { q: 'test', type: 'accounts' } }
|
||||||
|
|
||||||
|
it 'returns all matching accounts' do
|
||||||
|
get :index, params: params
|
||||||
|
|
||||||
|
expect(body_as_json[:accounts].pluck(:id)).to contain_exactly(bob.id.to_s, ana.id.to_s, tom.id.to_s)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with following=true' do
|
||||||
|
let(:params) { { q: 'test', type: 'accounts', following: 'true' } }
|
||||||
|
|
||||||
|
before do
|
||||||
|
user.account.follow!(ana)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns only the followed accounts' do
|
||||||
|
get :index, params: params
|
||||||
|
|
||||||
|
expect(body_as_json[:accounts].pluck(:id)).to contain_exactly(ana.id.to_s)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,7 @@ describe Api::Web::EmbedsController do
|
||||||
|
|
||||||
context 'when fails to find status' do
|
context 'when fails to find status' do
|
||||||
let(:url) { 'https://host.test/oembed.html' }
|
let(:url) { 'https://host.test/oembed.html' }
|
||||||
let(:service_instance) { double('fetch_oembed_service') }
|
let(:service_instance) { instance_double(FetchOEmbedService) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
allow(FetchOEmbedService).to receive(:new) { service_instance }
|
allow(FetchOEmbedService).to receive(:new) { service_instance }
|
||||||
|
|
|
@ -127,7 +127,8 @@ RSpec.describe Auth::SessionsController do
|
||||||
|
|
||||||
before do
|
before do
|
||||||
allow_any_instance_of(ActionDispatch::Request).to receive(:remote_ip).and_return(current_ip)
|
allow_any_instance_of(ActionDispatch::Request).to receive(:remote_ip).and_return(current_ip)
|
||||||
allow(UserMailer).to receive(:suspicious_sign_in).and_return(double('email', deliver_later!: nil))
|
allow(UserMailer).to receive(:suspicious_sign_in)
|
||||||
|
.and_return(instance_double(ActionMailer::MessageDelivery, deliver_later!: nil))
|
||||||
user.update(current_sign_in_at: 1.month.ago)
|
user.update(current_sign_in_at: 1.month.ago)
|
||||||
post :create, params: { user: { email: user.email, password: user.password } }
|
post :create, params: { user: { email: user.email, password: user.password } }
|
||||||
end
|
end
|
||||||
|
|
|
@ -28,7 +28,7 @@ describe AuthorizeInteractionsController do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'renders error when account cant be found' do
|
it 'renders error when account cant be found' do
|
||||||
service = double
|
service = instance_double(ResolveAccountService)
|
||||||
allow(ResolveAccountService).to receive(:new).and_return(service)
|
allow(ResolveAccountService).to receive(:new).and_return(service)
|
||||||
allow(service).to receive(:call).with('missing@hostname').and_return(nil)
|
allow(service).to receive(:call).with('missing@hostname').and_return(nil)
|
||||||
|
|
||||||
|
@ -40,7 +40,7 @@ describe AuthorizeInteractionsController do
|
||||||
|
|
||||||
it 'sets resource from url' do
|
it 'sets resource from url' do
|
||||||
account = Fabricate(:account)
|
account = Fabricate(:account)
|
||||||
service = double
|
service = instance_double(ResolveURLService)
|
||||||
allow(ResolveURLService).to receive(:new).and_return(service)
|
allow(ResolveURLService).to receive(:new).and_return(service)
|
||||||
allow(service).to receive(:call).with('http://example.com').and_return(account)
|
allow(service).to receive(:call).with('http://example.com').and_return(account)
|
||||||
|
|
||||||
|
@ -52,7 +52,7 @@ describe AuthorizeInteractionsController do
|
||||||
|
|
||||||
it 'sets resource from acct uri' do
|
it 'sets resource from acct uri' do
|
||||||
account = Fabricate(:account)
|
account = Fabricate(:account)
|
||||||
service = double
|
service = instance_double(ResolveAccountService)
|
||||||
allow(ResolveAccountService).to receive(:new).and_return(service)
|
allow(ResolveAccountService).to receive(:new).and_return(service)
|
||||||
allow(service).to receive(:call).with('found@hostname').and_return(account)
|
allow(service).to receive(:call).with('found@hostname').and_return(account)
|
||||||
|
|
||||||
|
@ -82,7 +82,7 @@ describe AuthorizeInteractionsController do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'shows error when account not found' do
|
it 'shows error when account not found' do
|
||||||
service = double
|
service = instance_double(ResolveAccountService)
|
||||||
|
|
||||||
allow(ResolveAccountService).to receive(:new).and_return(service)
|
allow(ResolveAccountService).to receive(:new).and_return(service)
|
||||||
allow(service).to receive(:call).with('user@hostname').and_return(nil)
|
allow(service).to receive(:call).with('user@hostname').and_return(nil)
|
||||||
|
@ -94,7 +94,7 @@ describe AuthorizeInteractionsController do
|
||||||
|
|
||||||
it 'follows account when found' do
|
it 'follows account when found' do
|
||||||
target_account = Fabricate(:account)
|
target_account = Fabricate(:account)
|
||||||
service = double
|
service = instance_double(ResolveAccountService)
|
||||||
|
|
||||||
allow(ResolveAccountService).to receive(:new).and_return(service)
|
allow(ResolveAccountService).to receive(:new).and_return(service)
|
||||||
allow(service).to receive(:call).with('user@hostname').and_return(target_account)
|
allow(service).to receive(:call).with('user@hostname').and_return(target_account)
|
||||||
|
|
|
@ -14,7 +14,8 @@ RSpec.describe Disputes::AppealsController do
|
||||||
let(:strike) { Fabricate(:account_warning, target_account: current_user.account) }
|
let(:strike) { Fabricate(:account_warning, target_account: current_user.account) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
allow(AdminMailer).to receive(:new_appeal).and_return(double('email', deliver_later: nil))
|
allow(AdminMailer).to receive(:new_appeal)
|
||||||
|
.and_return(instance_double(ActionMailer::MessageDelivery, deliver_later: nil))
|
||||||
post :create, params: { strike_id: strike.id, appeal: { text: 'Foo' } }
|
post :create, params: { strike_id: strike.id, appeal: { text: 'Foo' } }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -75,23 +75,11 @@ describe StatusesController do
|
||||||
context 'with HTML' do
|
context 'with HTML' do
|
||||||
let(:format) { 'html' }
|
let(:format) { 'html' }
|
||||||
|
|
||||||
it 'returns http success' do
|
it 'renders status successfully', :aggregate_failures do
|
||||||
expect(response).to have_http_status(200)
|
expect(response).to have_http_status(200)
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns Link header' do
|
|
||||||
expect(response.headers['Link'].to_s).to include 'activity+json'
|
expect(response.headers['Link'].to_s).to include 'activity+json'
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns Vary header' do
|
|
||||||
expect(response.headers['Vary']).to eq 'Accept, Accept-Language, Cookie'
|
expect(response.headers['Vary']).to eq 'Accept, Accept-Language, Cookie'
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns public Cache-Control header' do
|
|
||||||
expect(response.headers['Cache-Control']).to include 'public'
|
expect(response.headers['Cache-Control']).to include 'public'
|
||||||
end
|
|
||||||
|
|
||||||
it 'renders status' do
|
|
||||||
expect(response).to render_template(:show)
|
expect(response).to render_template(:show)
|
||||||
expect(response.body).to include status.text
|
expect(response.body).to include status.text
|
||||||
end
|
end
|
||||||
|
@ -100,25 +88,13 @@ describe StatusesController do
|
||||||
context 'with JSON' do
|
context 'with JSON' do
|
||||||
let(:format) { 'json' }
|
let(:format) { 'json' }
|
||||||
|
|
||||||
it 'returns http success' do
|
|
||||||
expect(response).to have_http_status(200)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns Link header' do
|
|
||||||
expect(response.headers['Link'].to_s).to include 'activity+json'
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns Vary header' do
|
|
||||||
expect(response.headers['Vary']).to eq 'Accept, Accept-Language, Cookie'
|
|
||||||
end
|
|
||||||
|
|
||||||
it_behaves_like 'cacheable response'
|
it_behaves_like 'cacheable response'
|
||||||
|
|
||||||
it 'returns Content-Type header' do
|
it 'renders ActivityPub Note object successfully', :aggregate_failures do
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
expect(response.headers['Link'].to_s).to include 'activity+json'
|
||||||
|
expect(response.headers['Vary']).to eq 'Accept, Accept-Language, Cookie'
|
||||||
expect(response.headers['Content-Type']).to include 'application/activity+json'
|
expect(response.headers['Content-Type']).to include 'application/activity+json'
|
||||||
end
|
|
||||||
|
|
||||||
it 'renders ActivityPub Note object' do
|
|
||||||
json = body_as_json
|
json = body_as_json
|
||||||
expect(json[:content]).to include status.text
|
expect(json[:content]).to include status.text
|
||||||
end
|
end
|
||||||
|
@ -199,23 +175,11 @@ describe StatusesController do
|
||||||
context 'with HTML' do
|
context 'with HTML' do
|
||||||
let(:format) { 'html' }
|
let(:format) { 'html' }
|
||||||
|
|
||||||
it 'returns http success' do
|
it 'renders status successfully', :aggregate_failures do
|
||||||
expect(response).to have_http_status(200)
|
expect(response).to have_http_status(200)
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns Link header' do
|
|
||||||
expect(response.headers['Link'].to_s).to include 'activity+json'
|
expect(response.headers['Link'].to_s).to include 'activity+json'
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns Vary header' do
|
|
||||||
expect(response.headers['Vary']).to eq 'Accept, Accept-Language, Cookie'
|
expect(response.headers['Vary']).to eq 'Accept, Accept-Language, Cookie'
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns private Cache-Control header' do
|
|
||||||
expect(response.headers['Cache-Control']).to include 'private'
|
expect(response.headers['Cache-Control']).to include 'private'
|
||||||
end
|
|
||||||
|
|
||||||
it 'renders status' do
|
|
||||||
expect(response).to render_template(:show)
|
expect(response).to render_template(:show)
|
||||||
expect(response.body).to include status.text
|
expect(response.body).to include status.text
|
||||||
end
|
end
|
||||||
|
@ -224,27 +188,12 @@ describe StatusesController do
|
||||||
context 'with JSON' do
|
context 'with JSON' do
|
||||||
let(:format) { 'json' }
|
let(:format) { 'json' }
|
||||||
|
|
||||||
it 'returns http success' do
|
it 'renders ActivityPub Note object successfully', :aggregate_failures do
|
||||||
expect(response).to have_http_status(200)
|
expect(response).to have_http_status(200)
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns Link header' do
|
|
||||||
expect(response.headers['Link'].to_s).to include 'activity+json'
|
expect(response.headers['Link'].to_s).to include 'activity+json'
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns Vary header' do
|
|
||||||
expect(response.headers['Vary']).to eq 'Accept, Accept-Language, Cookie'
|
expect(response.headers['Vary']).to eq 'Accept, Accept-Language, Cookie'
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns private Cache-Control header' do
|
|
||||||
expect(response.headers['Cache-Control']).to include 'private'
|
expect(response.headers['Cache-Control']).to include 'private'
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns Content-Type header' do
|
|
||||||
expect(response.headers['Content-Type']).to include 'application/activity+json'
|
expect(response.headers['Content-Type']).to include 'application/activity+json'
|
||||||
end
|
|
||||||
|
|
||||||
it 'renders ActivityPub Note object' do
|
|
||||||
json = body_as_json
|
json = body_as_json
|
||||||
expect(json[:content]).to include status.text
|
expect(json[:content]).to include status.text
|
||||||
end
|
end
|
||||||
|
@ -263,23 +212,11 @@ describe StatusesController do
|
||||||
context 'with HTML' do
|
context 'with HTML' do
|
||||||
let(:format) { 'html' }
|
let(:format) { 'html' }
|
||||||
|
|
||||||
it 'returns http success' do
|
it 'renders status successfully', :aggregate_failures do
|
||||||
expect(response).to have_http_status(200)
|
expect(response).to have_http_status(200)
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns Link header' do
|
|
||||||
expect(response.headers['Link'].to_s).to include 'activity+json'
|
expect(response.headers['Link'].to_s).to include 'activity+json'
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns Vary header' do
|
|
||||||
expect(response.headers['Vary']).to eq 'Accept, Accept-Language, Cookie'
|
expect(response.headers['Vary']).to eq 'Accept, Accept-Language, Cookie'
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns private Cache-Control header' do
|
|
||||||
expect(response.headers['Cache-Control']).to include 'private'
|
expect(response.headers['Cache-Control']).to include 'private'
|
||||||
end
|
|
||||||
|
|
||||||
it 'renders status' do
|
|
||||||
expect(response).to render_template(:show)
|
expect(response).to render_template(:show)
|
||||||
expect(response.body).to include status.text
|
expect(response.body).to include status.text
|
||||||
end
|
end
|
||||||
|
@ -288,27 +225,12 @@ describe StatusesController do
|
||||||
context 'with JSON' do
|
context 'with JSON' do
|
||||||
let(:format) { 'json' }
|
let(:format) { 'json' }
|
||||||
|
|
||||||
it 'returns http success' do
|
it 'renders ActivityPub Note object successfully', :aggregate_failures do
|
||||||
expect(response).to have_http_status(200)
|
expect(response).to have_http_status(200)
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns Link header' do
|
|
||||||
expect(response.headers['Link'].to_s).to include 'activity+json'
|
expect(response.headers['Link'].to_s).to include 'activity+json'
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns Vary header' do
|
|
||||||
expect(response.headers['Vary']).to eq 'Accept, Accept-Language, Cookie'
|
expect(response.headers['Vary']).to eq 'Accept, Accept-Language, Cookie'
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns private Cache-Control header' do
|
|
||||||
expect(response.headers['Cache-Control']).to include 'private'
|
expect(response.headers['Cache-Control']).to include 'private'
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns Content-Type header' do
|
|
||||||
expect(response.headers['Content-Type']).to include 'application/activity+json'
|
expect(response.headers['Content-Type']).to include 'application/activity+json'
|
||||||
end
|
|
||||||
|
|
||||||
it 'renders ActivityPub Note object' do
|
|
||||||
json = body_as_json
|
json = body_as_json
|
||||||
expect(json[:content]).to include status.text
|
expect(json[:content]).to include status.text
|
||||||
end
|
end
|
||||||
|
@ -350,23 +272,11 @@ describe StatusesController do
|
||||||
context 'with HTML' do
|
context 'with HTML' do
|
||||||
let(:format) { 'html' }
|
let(:format) { 'html' }
|
||||||
|
|
||||||
it 'returns http success' do
|
it 'renders status successfully', :aggregate_failures do
|
||||||
expect(response).to have_http_status(200)
|
expect(response).to have_http_status(200)
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns Link header' do
|
|
||||||
expect(response.headers['Link'].to_s).to include 'activity+json'
|
expect(response.headers['Link'].to_s).to include 'activity+json'
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns Vary header' do
|
|
||||||
expect(response.headers['Vary']).to eq 'Accept, Accept-Language, Cookie'
|
expect(response.headers['Vary']).to eq 'Accept, Accept-Language, Cookie'
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns private Cache-Control header' do
|
|
||||||
expect(response.headers['Cache-Control']).to include 'private'
|
expect(response.headers['Cache-Control']).to include 'private'
|
||||||
end
|
|
||||||
|
|
||||||
it 'renders status' do
|
|
||||||
expect(response).to render_template(:show)
|
expect(response).to render_template(:show)
|
||||||
expect(response.body).to include status.text
|
expect(response.body).to include status.text
|
||||||
end
|
end
|
||||||
|
@ -375,27 +285,12 @@ describe StatusesController do
|
||||||
context 'with JSON' do
|
context 'with JSON' do
|
||||||
let(:format) { 'json' }
|
let(:format) { 'json' }
|
||||||
|
|
||||||
it 'returns http success' do
|
it 'renders ActivityPub Note object successfully' do
|
||||||
expect(response).to have_http_status(200)
|
expect(response).to have_http_status(200)
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns Link header' do
|
|
||||||
expect(response.headers['Link'].to_s).to include 'activity+json'
|
expect(response.headers['Link'].to_s).to include 'activity+json'
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns Vary header' do
|
|
||||||
expect(response.headers['Vary']).to eq 'Accept, Accept-Language, Cookie'
|
expect(response.headers['Vary']).to eq 'Accept, Accept-Language, Cookie'
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns private Cache-Control header' do
|
|
||||||
expect(response.headers['Cache-Control']).to include 'private'
|
expect(response.headers['Cache-Control']).to include 'private'
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns Content-Type header' do
|
|
||||||
expect(response.headers['Content-Type']).to include 'application/activity+json'
|
expect(response.headers['Content-Type']).to include 'application/activity+json'
|
||||||
end
|
|
||||||
|
|
||||||
it 'renders ActivityPub Note object' do
|
|
||||||
json = body_as_json
|
json = body_as_json
|
||||||
expect(json[:content]).to include status.text
|
expect(json[:content]).to include status.text
|
||||||
end
|
end
|
||||||
|
@ -463,23 +358,11 @@ describe StatusesController do
|
||||||
context 'with HTML' do
|
context 'with HTML' do
|
||||||
let(:format) { 'html' }
|
let(:format) { 'html' }
|
||||||
|
|
||||||
it 'returns http success' do
|
it 'renders status successfully', :aggregate_failures do
|
||||||
expect(response).to have_http_status(200)
|
expect(response).to have_http_status(200)
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns Link header' do
|
|
||||||
expect(response.headers['Link'].to_s).to include 'activity+json'
|
expect(response.headers['Link'].to_s).to include 'activity+json'
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns Vary header' do
|
|
||||||
expect(response.headers['Vary']).to eq 'Accept, Accept-Language, Cookie'
|
expect(response.headers['Vary']).to eq 'Accept, Accept-Language, Cookie'
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns private Cache-Control header' do
|
|
||||||
expect(response.headers['Cache-Control']).to include 'private'
|
expect(response.headers['Cache-Control']).to include 'private'
|
||||||
end
|
|
||||||
|
|
||||||
it 'renders status' do
|
|
||||||
expect(response).to render_template(:show)
|
expect(response).to render_template(:show)
|
||||||
expect(response.body).to include status.text
|
expect(response.body).to include status.text
|
||||||
end
|
end
|
||||||
|
@ -488,25 +371,13 @@ describe StatusesController do
|
||||||
context 'with JSON' do
|
context 'with JSON' do
|
||||||
let(:format) { 'json' }
|
let(:format) { 'json' }
|
||||||
|
|
||||||
it 'returns http success' do
|
|
||||||
expect(response).to have_http_status(200)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns Link header' do
|
|
||||||
expect(response.headers['Link'].to_s).to include 'activity+json'
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns Vary header' do
|
|
||||||
expect(response.headers['Vary']).to eq 'Accept, Accept-Language, Cookie'
|
|
||||||
end
|
|
||||||
|
|
||||||
it_behaves_like 'cacheable response'
|
it_behaves_like 'cacheable response'
|
||||||
|
|
||||||
it 'returns Content-Type header' do
|
it 'renders ActivityPub Note object successfully', :aggregate_failures do
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
expect(response.headers['Link'].to_s).to include 'activity+json'
|
||||||
|
expect(response.headers['Vary']).to eq 'Accept, Accept-Language, Cookie'
|
||||||
expect(response.headers['Content-Type']).to include 'application/activity+json'
|
expect(response.headers['Content-Type']).to include 'application/activity+json'
|
||||||
end
|
|
||||||
|
|
||||||
it 'renders ActivityPub Note object' do
|
|
||||||
json = body_as_json
|
json = body_as_json
|
||||||
expect(json[:content]).to include status.text
|
expect(json[:content]).to include status.text
|
||||||
end
|
end
|
||||||
|
@ -525,23 +396,11 @@ describe StatusesController do
|
||||||
context 'with HTML' do
|
context 'with HTML' do
|
||||||
let(:format) { 'html' }
|
let(:format) { 'html' }
|
||||||
|
|
||||||
it 'returns http success' do
|
it 'renders status successfully', :aggregate_failures do
|
||||||
expect(response).to have_http_status(200)
|
expect(response).to have_http_status(200)
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns Link header' do
|
|
||||||
expect(response.headers['Link'].to_s).to include 'activity+json'
|
expect(response.headers['Link'].to_s).to include 'activity+json'
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns Vary header' do
|
|
||||||
expect(response.headers['Vary']).to eq 'Accept, Accept-Language, Cookie'
|
expect(response.headers['Vary']).to eq 'Accept, Accept-Language, Cookie'
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns private Cache-Control header' do
|
|
||||||
expect(response.headers['Cache-Control']).to include 'private'
|
expect(response.headers['Cache-Control']).to include 'private'
|
||||||
end
|
|
||||||
|
|
||||||
it 'renders status' do
|
|
||||||
expect(response).to render_template(:show)
|
expect(response).to render_template(:show)
|
||||||
expect(response.body).to include status.text
|
expect(response.body).to include status.text
|
||||||
end
|
end
|
||||||
|
@ -550,27 +409,12 @@ describe StatusesController do
|
||||||
context 'with JSON' do
|
context 'with JSON' do
|
||||||
let(:format) { 'json' }
|
let(:format) { 'json' }
|
||||||
|
|
||||||
it 'returns http success' do
|
it 'renders ActivityPub Note object successfully' do
|
||||||
expect(response).to have_http_status(200)
|
expect(response).to have_http_status(200)
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns Link header' do
|
|
||||||
expect(response.headers['Link'].to_s).to include 'activity+json'
|
expect(response.headers['Link'].to_s).to include 'activity+json'
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns Vary header' do
|
|
||||||
expect(response.headers['Vary']).to eq 'Accept, Accept-Language, Cookie'
|
expect(response.headers['Vary']).to eq 'Accept, Accept-Language, Cookie'
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns private Cache-Control header' do
|
|
||||||
expect(response.headers['Cache-Control']).to include 'private'
|
expect(response.headers['Cache-Control']).to include 'private'
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns Content-Type header' do
|
|
||||||
expect(response.headers['Content-Type']).to include 'application/activity+json'
|
expect(response.headers['Content-Type']).to include 'application/activity+json'
|
||||||
end
|
|
||||||
|
|
||||||
it 'renders ActivityPub Note object' do
|
|
||||||
json = body_as_json
|
json = body_as_json
|
||||||
expect(json[:content]).to include status.text
|
expect(json[:content]).to include status.text
|
||||||
end
|
end
|
||||||
|
@ -612,23 +456,11 @@ describe StatusesController do
|
||||||
context 'with HTML' do
|
context 'with HTML' do
|
||||||
let(:format) { 'html' }
|
let(:format) { 'html' }
|
||||||
|
|
||||||
it 'returns http success' do
|
it 'renders status successfully', :aggregate_failures do
|
||||||
expect(response).to have_http_status(200)
|
expect(response).to have_http_status(200)
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns Link header' do
|
|
||||||
expect(response.headers['Link'].to_s).to include 'activity+json'
|
expect(response.headers['Link'].to_s).to include 'activity+json'
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns Vary header' do
|
|
||||||
expect(response.headers['Vary']).to eq 'Accept, Accept-Language, Cookie'
|
expect(response.headers['Vary']).to eq 'Accept, Accept-Language, Cookie'
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns private Cache-Control header' do
|
|
||||||
expect(response.headers['Cache-Control']).to include 'private'
|
expect(response.headers['Cache-Control']).to include 'private'
|
||||||
end
|
|
||||||
|
|
||||||
it 'renders status' do
|
|
||||||
expect(response).to render_template(:show)
|
expect(response).to render_template(:show)
|
||||||
expect(response.body).to include status.text
|
expect(response.body).to include status.text
|
||||||
end
|
end
|
||||||
|
@ -637,27 +469,12 @@ describe StatusesController do
|
||||||
context 'with JSON' do
|
context 'with JSON' do
|
||||||
let(:format) { 'json' }
|
let(:format) { 'json' }
|
||||||
|
|
||||||
it 'returns http success' do
|
it 'renders ActivityPub Note object', :aggregate_failures do
|
||||||
expect(response).to have_http_status(200)
|
expect(response).to have_http_status(200)
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns Link header' do
|
|
||||||
expect(response.headers['Link'].to_s).to include 'activity+json'
|
expect(response.headers['Link'].to_s).to include 'activity+json'
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns Vary header' do
|
|
||||||
expect(response.headers['Vary']).to eq 'Accept, Accept-Language, Cookie'
|
expect(response.headers['Vary']).to eq 'Accept, Accept-Language, Cookie'
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns private Cache-Control header' do
|
|
||||||
expect(response.headers['Cache-Control']).to include 'private'
|
expect(response.headers['Cache-Control']).to include 'private'
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns Content-Type header' do
|
|
||||||
expect(response.headers['Content-Type']).to include 'application/activity+json'
|
expect(response.headers['Content-Type']).to include 'application/activity+json'
|
||||||
end
|
|
||||||
|
|
||||||
it 'renders ActivityPub Note object' do
|
|
||||||
json = body_as_json
|
json = body_as_json
|
||||||
expect(json[:content]).to include status.text
|
expect(json[:content]).to include status.text
|
||||||
end
|
end
|
||||||
|
@ -933,23 +750,11 @@ describe StatusesController do
|
||||||
get :embed, params: { account_username: status.account.username, id: status.id }
|
get :embed, params: { account_username: status.account.username, id: status.id }
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns http success' do
|
it 'renders status successfully', :aggregate_failures do
|
||||||
expect(response).to have_http_status(200)
|
expect(response).to have_http_status(200)
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns Link header' do
|
|
||||||
expect(response.headers['Link'].to_s).to include 'activity+json'
|
expect(response.headers['Link'].to_s).to include 'activity+json'
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns Vary header' do
|
|
||||||
expect(response.headers['Vary']).to eq 'Accept, Accept-Language, Cookie'
|
expect(response.headers['Vary']).to eq 'Accept, Accept-Language, Cookie'
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns public Cache-Control header' do
|
|
||||||
expect(response.headers['Cache-Control']).to include 'public'
|
expect(response.headers['Cache-Control']).to include 'public'
|
||||||
end
|
|
||||||
|
|
||||||
it 'renders status' do
|
|
||||||
expect(response).to render_template(:embed)
|
expect(response).to render_template(:embed)
|
||||||
expect(response.body).to include status.text
|
expect(response.body).to include status.text
|
||||||
end
|
end
|
||||||
|
|
|
@ -53,7 +53,7 @@ describe 'blocking domains through the moderation interface' do
|
||||||
# Confirming updates the block
|
# Confirming updates the block
|
||||||
click_on I18n.t('admin.domain_blocks.confirm_suspension.confirm')
|
click_on I18n.t('admin.domain_blocks.confirm_suspension.confirm')
|
||||||
|
|
||||||
expect(domain_block.reload.severity).to eq 'silence'
|
expect(domain_block.reload.severity).to eq 'suspend'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -72,7 +72,7 @@ describe 'blocking domains through the moderation interface' do
|
||||||
# Confirming updates the block
|
# Confirming updates the block
|
||||||
click_on I18n.t('admin.domain_blocks.confirm_suspension.confirm')
|
click_on I18n.t('admin.domain_blocks.confirm_suspension.confirm')
|
||||||
|
|
||||||
expect(domain_block.reload.severity).to eq 'silence'
|
expect(domain_block.reload.severity).to eq 'suspend'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -117,42 +117,42 @@ describe StatusesHelper do
|
||||||
|
|
||||||
describe '#style_classes' do
|
describe '#style_classes' do
|
||||||
it do
|
it do
|
||||||
status = double(reblog?: false)
|
status = instance_double(Status, reblog?: false)
|
||||||
classes = helper.style_classes(status, false, false, false)
|
classes = helper.style_classes(status, false, false, false)
|
||||||
|
|
||||||
expect(classes).to eq 'entry'
|
expect(classes).to eq 'entry'
|
||||||
end
|
end
|
||||||
|
|
||||||
it do
|
it do
|
||||||
status = double(reblog?: true)
|
status = instance_double(Status, reblog?: true)
|
||||||
classes = helper.style_classes(status, false, false, false)
|
classes = helper.style_classes(status, false, false, false)
|
||||||
|
|
||||||
expect(classes).to eq 'entry entry-reblog'
|
expect(classes).to eq 'entry entry-reblog'
|
||||||
end
|
end
|
||||||
|
|
||||||
it do
|
it do
|
||||||
status = double(reblog?: false)
|
status = instance_double(Status, reblog?: false)
|
||||||
classes = helper.style_classes(status, true, false, false)
|
classes = helper.style_classes(status, true, false, false)
|
||||||
|
|
||||||
expect(classes).to eq 'entry entry-predecessor'
|
expect(classes).to eq 'entry entry-predecessor'
|
||||||
end
|
end
|
||||||
|
|
||||||
it do
|
it do
|
||||||
status = double(reblog?: false)
|
status = instance_double(Status, reblog?: false)
|
||||||
classes = helper.style_classes(status, false, true, false)
|
classes = helper.style_classes(status, false, true, false)
|
||||||
|
|
||||||
expect(classes).to eq 'entry entry-successor'
|
expect(classes).to eq 'entry entry-successor'
|
||||||
end
|
end
|
||||||
|
|
||||||
it do
|
it do
|
||||||
status = double(reblog?: false)
|
status = instance_double(Status, reblog?: false)
|
||||||
classes = helper.style_classes(status, false, false, true)
|
classes = helper.style_classes(status, false, false, true)
|
||||||
|
|
||||||
expect(classes).to eq 'entry entry-center'
|
expect(classes).to eq 'entry entry-center'
|
||||||
end
|
end
|
||||||
|
|
||||||
it do
|
it do
|
||||||
status = double(reblog?: true)
|
status = instance_double(Status, reblog?: true)
|
||||||
classes = helper.style_classes(status, true, true, true)
|
classes = helper.style_classes(status, true, true, true)
|
||||||
|
|
||||||
expect(classes).to eq 'entry entry-predecessor entry-reblog entry-successor entry-center'
|
expect(classes).to eq 'entry entry-predecessor entry-reblog entry-successor entry-center'
|
||||||
|
@ -161,35 +161,35 @@ describe StatusesHelper do
|
||||||
|
|
||||||
describe '#microformats_classes' do
|
describe '#microformats_classes' do
|
||||||
it do
|
it do
|
||||||
status = double(reblog?: false)
|
status = instance_double(Status, reblog?: false)
|
||||||
classes = helper.microformats_classes(status, false, false)
|
classes = helper.microformats_classes(status, false, false)
|
||||||
|
|
||||||
expect(classes).to eq ''
|
expect(classes).to eq ''
|
||||||
end
|
end
|
||||||
|
|
||||||
it do
|
it do
|
||||||
status = double(reblog?: false)
|
status = instance_double(Status, reblog?: false)
|
||||||
classes = helper.microformats_classes(status, true, false)
|
classes = helper.microformats_classes(status, true, false)
|
||||||
|
|
||||||
expect(classes).to eq 'p-in-reply-to'
|
expect(classes).to eq 'p-in-reply-to'
|
||||||
end
|
end
|
||||||
|
|
||||||
it do
|
it do
|
||||||
status = double(reblog?: false)
|
status = instance_double(Status, reblog?: false)
|
||||||
classes = helper.microformats_classes(status, false, true)
|
classes = helper.microformats_classes(status, false, true)
|
||||||
|
|
||||||
expect(classes).to eq 'p-comment'
|
expect(classes).to eq 'p-comment'
|
||||||
end
|
end
|
||||||
|
|
||||||
it do
|
it do
|
||||||
status = double(reblog?: true)
|
status = instance_double(Status, reblog?: true)
|
||||||
classes = helper.microformats_classes(status, true, false)
|
classes = helper.microformats_classes(status, true, false)
|
||||||
|
|
||||||
expect(classes).to eq 'p-in-reply-to p-repost-of'
|
expect(classes).to eq 'p-in-reply-to p-repost-of'
|
||||||
end
|
end
|
||||||
|
|
||||||
it do
|
it do
|
||||||
status = double(reblog?: true)
|
status = instance_double(Status, reblog?: true)
|
||||||
classes = helper.microformats_classes(status, true, true)
|
classes = helper.microformats_classes(status, true, true)
|
||||||
|
|
||||||
expect(classes).to eq 'p-in-reply-to p-repost-of p-comment'
|
expect(classes).to eq 'p-in-reply-to p-repost-of p-comment'
|
||||||
|
@ -198,42 +198,42 @@ describe StatusesHelper do
|
||||||
|
|
||||||
describe '#microformats_h_class' do
|
describe '#microformats_h_class' do
|
||||||
it do
|
it do
|
||||||
status = double(reblog?: false)
|
status = instance_double(Status, reblog?: false)
|
||||||
css_class = helper.microformats_h_class(status, false, false, false)
|
css_class = helper.microformats_h_class(status, false, false, false)
|
||||||
|
|
||||||
expect(css_class).to eq 'h-entry'
|
expect(css_class).to eq 'h-entry'
|
||||||
end
|
end
|
||||||
|
|
||||||
it do
|
it do
|
||||||
status = double(reblog?: true)
|
status = instance_double(Status, reblog?: true)
|
||||||
css_class = helper.microformats_h_class(status, false, false, false)
|
css_class = helper.microformats_h_class(status, false, false, false)
|
||||||
|
|
||||||
expect(css_class).to eq 'h-cite'
|
expect(css_class).to eq 'h-cite'
|
||||||
end
|
end
|
||||||
|
|
||||||
it do
|
it do
|
||||||
status = double(reblog?: false)
|
status = instance_double(Status, reblog?: false)
|
||||||
css_class = helper.microformats_h_class(status, true, false, false)
|
css_class = helper.microformats_h_class(status, true, false, false)
|
||||||
|
|
||||||
expect(css_class).to eq 'h-cite'
|
expect(css_class).to eq 'h-cite'
|
||||||
end
|
end
|
||||||
|
|
||||||
it do
|
it do
|
||||||
status = double(reblog?: false)
|
status = instance_double(Status, reblog?: false)
|
||||||
css_class = helper.microformats_h_class(status, false, true, false)
|
css_class = helper.microformats_h_class(status, false, true, false)
|
||||||
|
|
||||||
expect(css_class).to eq 'h-cite'
|
expect(css_class).to eq 'h-cite'
|
||||||
end
|
end
|
||||||
|
|
||||||
it do
|
it do
|
||||||
status = double(reblog?: false)
|
status = instance_double(Status, reblog?: false)
|
||||||
css_class = helper.microformats_h_class(status, false, false, true)
|
css_class = helper.microformats_h_class(status, false, false, true)
|
||||||
|
|
||||||
expect(css_class).to eq ''
|
expect(css_class).to eq ''
|
||||||
end
|
end
|
||||||
|
|
||||||
it do
|
it do
|
||||||
status = double(reblog?: true)
|
status = instance_double(Status, reblog?: true)
|
||||||
css_class = helper.microformats_h_class(status, true, true, true)
|
css_class = helper.microformats_h_class(status, true, true, true)
|
||||||
|
|
||||||
expect(css_class).to eq 'h-cite'
|
expect(css_class).to eq 'h-cite'
|
||||||
|
|
|
@ -26,7 +26,7 @@ RSpec.describe ActivityPub::Activity::Add do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when status was not known before' do
|
context 'when status was not known before' do
|
||||||
let(:service_stub) { double }
|
let(:service_stub) { instance_double(ActivityPub::FetchRemoteStatusService) }
|
||||||
|
|
||||||
let(:json) do
|
let(:json) do
|
||||||
{
|
{
|
||||||
|
|
|
@ -26,7 +26,7 @@ RSpec.describe ActivityPub::Activity::Move do
|
||||||
stub_request(:post, old_account.inbox_url).to_return(status: 200)
|
stub_request(:post, old_account.inbox_url).to_return(status: 200)
|
||||||
stub_request(:post, new_account.inbox_url).to_return(status: 200)
|
stub_request(:post, new_account.inbox_url).to_return(status: 200)
|
||||||
|
|
||||||
service_stub = double
|
service_stub = instance_double(ActivityPub::FetchRemoteAccountService)
|
||||||
allow(ActivityPub::FetchRemoteAccountService).to receive(:new).and_return(service_stub)
|
allow(ActivityPub::FetchRemoteAccountService).to receive(:new).and_return(service_stub)
|
||||||
allow(service_stub).to receive(:call).and_return(returned_account)
|
allow(service_stub).to receive(:call).and_return(returned_account)
|
||||||
end
|
end
|
||||||
|
|
|
@ -48,7 +48,7 @@ describe Request do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'executes a HTTP request when the first address is private' do
|
it 'executes a HTTP request when the first address is private' do
|
||||||
resolver = double
|
resolver = instance_double(Resolv::DNS)
|
||||||
|
|
||||||
allow(resolver).to receive(:getaddresses).with('example.com').and_return(%w(0.0.0.0 2001:4860:4860::8844))
|
allow(resolver).to receive(:getaddresses).with('example.com').and_return(%w(0.0.0.0 2001:4860:4860::8844))
|
||||||
allow(resolver).to receive(:timeouts=).and_return(nil)
|
allow(resolver).to receive(:timeouts=).and_return(nil)
|
||||||
|
@ -83,7 +83,7 @@ describe Request do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'raises Mastodon::ValidationError' do
|
it 'raises Mastodon::ValidationError' do
|
||||||
resolver = double
|
resolver = instance_double(Resolv::DNS)
|
||||||
|
|
||||||
allow(resolver).to receive(:getaddresses).with('example.com').and_return(%w(0.0.0.0 2001:db8::face))
|
allow(resolver).to receive(:getaddresses).with('example.com').and_return(%w(0.0.0.0 2001:db8::face))
|
||||||
allow(resolver).to receive(:timeouts=).and_return(nil)
|
allow(resolver).to receive(:timeouts=).and_return(nil)
|
||||||
|
|
|
@ -7,7 +7,7 @@ RSpec.describe SuspiciousSignInDetector do
|
||||||
subject { described_class.new(user).suspicious?(request) }
|
subject { described_class.new(user).suspicious?(request) }
|
||||||
|
|
||||||
let(:user) { Fabricate(:user, current_sign_in_at: 1.day.ago) }
|
let(:user) { Fabricate(:user, current_sign_in_at: 1.day.ago) }
|
||||||
let(:request) { double(remote_ip: remote_ip) }
|
let(:request) { instance_double(ActionDispatch::Request, remote_ip: remote_ip) }
|
||||||
let(:remote_ip) { nil }
|
let(:remote_ip) { nil }
|
||||||
|
|
||||||
context 'when user has 2FA enabled' do
|
context 'when user has 2FA enabled' do
|
||||||
|
|
|
@ -6,7 +6,7 @@ RSpec.describe Account::Field do
|
||||||
describe '#verified?' do
|
describe '#verified?' do
|
||||||
subject { described_class.new(account, 'name' => 'Foo', 'value' => 'Bar', 'verified_at' => verified_at) }
|
subject { described_class.new(account, 'name' => 'Foo', 'value' => 'Bar', 'verified_at' => verified_at) }
|
||||||
|
|
||||||
let(:account) { double('Account', local?: true) }
|
let(:account) { instance_double(Account, local?: true) }
|
||||||
|
|
||||||
context 'when verified_at is set' do
|
context 'when verified_at is set' do
|
||||||
let(:verified_at) { Time.now.utc.iso8601 }
|
let(:verified_at) { Time.now.utc.iso8601 }
|
||||||
|
@ -28,7 +28,7 @@ RSpec.describe Account::Field do
|
||||||
describe '#mark_verified!' do
|
describe '#mark_verified!' do
|
||||||
subject { described_class.new(account, original_hash) }
|
subject { described_class.new(account, original_hash) }
|
||||||
|
|
||||||
let(:account) { double('Account', local?: true) }
|
let(:account) { instance_double(Account, local?: true) }
|
||||||
let(:original_hash) { { 'name' => 'Foo', 'value' => 'Bar' } }
|
let(:original_hash) { { 'name' => 'Foo', 'value' => 'Bar' } }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
|
@ -47,7 +47,7 @@ RSpec.describe Account::Field do
|
||||||
describe '#verifiable?' do
|
describe '#verifiable?' do
|
||||||
subject { described_class.new(account, 'name' => 'Foo', 'value' => value) }
|
subject { described_class.new(account, 'name' => 'Foo', 'value' => value) }
|
||||||
|
|
||||||
let(:account) { double('Account', local?: local) }
|
let(:account) { instance_double(Account, local?: local) }
|
||||||
|
|
||||||
context 'with local accounts' do
|
context 'with local accounts' do
|
||||||
let(:local) { true }
|
let(:local) { true }
|
||||||
|
|
|
@ -15,7 +15,7 @@ RSpec.describe AccountMigration do
|
||||||
before do
|
before do
|
||||||
target_account.aliases.create!(acct: source_account.acct)
|
target_account.aliases.create!(acct: source_account.acct)
|
||||||
|
|
||||||
service_double = double
|
service_double = instance_double(ResolveAccountService)
|
||||||
allow(ResolveAccountService).to receive(:new).and_return(service_double)
|
allow(ResolveAccountService).to receive(:new).and_return(service_double)
|
||||||
allow(service_double).to receive(:call).with(target_acct, anything).and_return(target_account)
|
allow(service_double).to receive(:call).with(target_acct, anything).and_return(target_account)
|
||||||
end
|
end
|
||||||
|
@ -29,7 +29,7 @@ RSpec.describe AccountMigration do
|
||||||
let(:target_acct) { 'target@remote' }
|
let(:target_acct) { 'target@remote' }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
service_double = double
|
service_double = instance_double(ResolveAccountService)
|
||||||
allow(ResolveAccountService).to receive(:new).and_return(service_double)
|
allow(ResolveAccountService).to receive(:new).and_return(service_double)
|
||||||
allow(service_double).to receive(:call).with(target_acct, anything).and_return(nil)
|
allow(service_double).to receive(:call).with(target_acct, anything).and_return(nil)
|
||||||
end
|
end
|
||||||
|
|
|
@ -16,7 +16,7 @@ RSpec.describe SessionActivation do
|
||||||
allow(session_activation).to receive(:detection).and_return(detection)
|
allow(session_activation).to receive(:detection).and_return(detection)
|
||||||
end
|
end
|
||||||
|
|
||||||
let(:detection) { double(id: 1) }
|
let(:detection) { instance_double(Browser::Chrome, id: 1) }
|
||||||
let(:session_activation) { Fabricate(:session_activation) }
|
let(:session_activation) { Fabricate(:session_activation) }
|
||||||
|
|
||||||
it 'returns detection.id' do
|
it 'returns detection.id' do
|
||||||
|
@ -30,7 +30,7 @@ RSpec.describe SessionActivation do
|
||||||
end
|
end
|
||||||
|
|
||||||
let(:session_activation) { Fabricate(:session_activation) }
|
let(:session_activation) { Fabricate(:session_activation) }
|
||||||
let(:detection) { double(platform: double(id: 1)) }
|
let(:detection) { instance_double(Browser::Chrome, platform: instance_double(Browser::Platform, id: 1)) }
|
||||||
|
|
||||||
it 'returns detection.platform.id' do
|
it 'returns detection.platform.id' do
|
||||||
expect(session_activation.platform).to be 1
|
expect(session_activation.platform).to be 1
|
||||||
|
|
|
@ -62,7 +62,7 @@ RSpec.describe Setting do
|
||||||
|
|
||||||
context 'when RailsSettings::Settings.object returns truthy' do
|
context 'when RailsSettings::Settings.object returns truthy' do
|
||||||
let(:object) { db_val }
|
let(:object) { db_val }
|
||||||
let(:db_val) { double(value: 'db_val') }
|
let(:db_val) { instance_double(described_class, value: 'db_val') }
|
||||||
|
|
||||||
context 'when default_value is a Hash' do
|
context 'when default_value is a Hash' do
|
||||||
let(:default_value) { { default_value: 'default_value' } }
|
let(:default_value) { { default_value: 'default_value' } }
|
||||||
|
|
|
@ -8,16 +8,32 @@ describe WebhookPolicy do
|
||||||
let(:admin) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account }
|
let(:admin) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account }
|
||||||
let(:john) { Fabricate(:account) }
|
let(:john) { Fabricate(:account) }
|
||||||
|
|
||||||
permissions :index?, :create?, :show?, :update?, :enable?, :disable?, :rotate_secret?, :destroy? do
|
permissions :index?, :create? do
|
||||||
context 'with an admin' do
|
context 'with an admin' do
|
||||||
it 'permits' do
|
it 'permits' do
|
||||||
expect(policy).to permit(admin, Tag)
|
expect(policy).to permit(admin, Webhook)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with a non-admin' do
|
context 'with a non-admin' do
|
||||||
it 'denies' do
|
it 'denies' do
|
||||||
expect(policy).to_not permit(john, Tag)
|
expect(policy).to_not permit(john, Webhook)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
permissions :show?, :update?, :enable?, :disable?, :rotate_secret?, :destroy? do
|
||||||
|
let(:webhook) { Fabricate(:webhook, events: ['account.created', 'report.created']) }
|
||||||
|
|
||||||
|
context 'with an admin' do
|
||||||
|
it 'permits' do
|
||||||
|
expect(policy).to permit(admin, webhook)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with a non-admin' do
|
||||||
|
it 'denies' do
|
||||||
|
expect(policy).to_not permit(john, webhook)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue