commit
246addd5b3
|
@ -27,10 +27,10 @@ plugins:
|
||||||
enabled: true
|
enabled: true
|
||||||
eslint:
|
eslint:
|
||||||
enabled: true
|
enabled: true
|
||||||
channel: eslint-5
|
channel: eslint-6
|
||||||
rubocop:
|
rubocop:
|
||||||
enabled: true
|
enabled: true
|
||||||
channel: rubocop-0-71
|
channel: rubocop-0-76
|
||||||
sass-lint:
|
sass-lint:
|
||||||
enabled: true
|
enabled: true
|
||||||
exclude_patterns:
|
exclude_patterns:
|
||||||
|
|
|
@ -183,6 +183,8 @@ SMTP_FROM_ADDRESS=notifications@${APP_NAME}.nanoapp.io
|
||||||
# LDAP_BIND_DN=
|
# LDAP_BIND_DN=
|
||||||
# LDAP_PASSWORD=
|
# LDAP_PASSWORD=
|
||||||
# LDAP_UID=cn
|
# LDAP_UID=cn
|
||||||
|
# LDAP_MAIL=mail
|
||||||
|
# LDAP_SEARCH_FILTER=(|(%{uid}=%{email})(%{mail}=%{email}))
|
||||||
# LDAP_UID_CONVERSION_ENABLED=true
|
# LDAP_UID_CONVERSION_ENABLED=true
|
||||||
# LDAP_UID_CONVERSION_SEARCH=., -
|
# LDAP_UID_CONVERSION_SEARCH=., -
|
||||||
# LDAP_UID_CONVERSION_REPLACE=_
|
# LDAP_UID_CONVERSION_REPLACE=_
|
||||||
|
|
|
@ -203,7 +203,8 @@ STREAMING_CLUSTER_NUM=1
|
||||||
# LDAP_BIND_DN=
|
# LDAP_BIND_DN=
|
||||||
# LDAP_PASSWORD=
|
# LDAP_PASSWORD=
|
||||||
# LDAP_UID=cn
|
# LDAP_UID=cn
|
||||||
# LDAP_SEARCH_FILTER=%{uid}=%{email}
|
# LDAP_MAIL=mail
|
||||||
|
# LDAP_SEARCH_FILTER=(|(%{uid}=%{email})(%{mail}=%{email}))
|
||||||
# LDAP_UID_CONVERSION_ENABLED=true
|
# LDAP_UID_CONVERSION_ENABLED=true
|
||||||
# LDAP_UID_CONVERSION_SEARCH=., -
|
# LDAP_UID_CONVERSION_SEARCH=., -
|
||||||
# LDAP_UID_CONVERSION_REPLACE=_
|
# LDAP_UID_CONVERSION_REPLACE=_
|
||||||
|
|
|
@ -71,6 +71,9 @@ Naming/MemoizedInstanceVariableName:
|
||||||
Rails:
|
Rails:
|
||||||
Enabled: true
|
Enabled: true
|
||||||
|
|
||||||
|
Rails/EnumHash:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
Rails/HasAndBelongsToMany:
|
Rails/HasAndBelongsToMany:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
|
@ -102,6 +105,9 @@ Style/Documentation:
|
||||||
Style/DoubleNegation:
|
Style/DoubleNegation:
|
||||||
Enabled: true
|
Enabled: true
|
||||||
|
|
||||||
|
Style/FormatStringToken:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
Style/FrozenStringLiteralComment:
|
Style/FrozenStringLiteralComment:
|
||||||
Enabled: true
|
Enabled: true
|
||||||
|
|
||||||
|
|
12
Gemfile
12
Gemfile
|
@ -5,7 +5,7 @@ ruby '>= 2.4.0', '< 2.7.0'
|
||||||
|
|
||||||
gem 'pkg-config', '~> 1.4'
|
gem 'pkg-config', '~> 1.4'
|
||||||
|
|
||||||
gem 'puma', '~> 4.2'
|
gem 'puma', '~> 4.3'
|
||||||
gem 'rails', '~> 5.2.3'
|
gem 'rails', '~> 5.2.3'
|
||||||
gem 'thor', '~> 0.20'
|
gem 'thor', '~> 0.20'
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ gem 'makara', '~> 0.4'
|
||||||
gem 'pghero', '~> 2.4'
|
gem 'pghero', '~> 2.4'
|
||||||
gem 'dotenv-rails', '~> 2.7'
|
gem 'dotenv-rails', '~> 2.7'
|
||||||
|
|
||||||
gem 'aws-sdk-s3', '~> 1.55', require: false
|
gem 'aws-sdk-s3', '~> 1.57', require: false
|
||||||
gem 'fog-core', '<= 2.1.0'
|
gem 'fog-core', '<= 2.1.0'
|
||||||
gem 'fog-openstack', '~> 0.3', require: false
|
gem 'fog-openstack', '~> 0.3', require: false
|
||||||
gem 'paperclip', '~> 6.0'
|
gem 'paperclip', '~> 6.0'
|
||||||
|
@ -91,7 +91,7 @@ gem 'sprockets-rails', '~> 3.2', require: 'sprockets/railtie'
|
||||||
gem 'stoplight', '~> 2.2.0'
|
gem 'stoplight', '~> 2.2.0'
|
||||||
gem 'strong_migrations', '~> 0.4'
|
gem 'strong_migrations', '~> 0.4'
|
||||||
gem 'tty-command', '~> 0.9', require: false
|
gem 'tty-command', '~> 0.9', require: false
|
||||||
gem 'tty-prompt', '~> 0.19', require: false
|
gem 'tty-prompt', '~> 0.20', require: false
|
||||||
gem 'twitter-text', '~> 1.14'
|
gem 'twitter-text', '~> 1.14'
|
||||||
gem 'tzinfo-data', '~> 1.2019'
|
gem 'tzinfo-data', '~> 1.2019'
|
||||||
gem 'webpacker', '~> 4.2'
|
gem 'webpacker', '~> 4.2'
|
||||||
|
@ -104,7 +104,7 @@ gem 'rdf-normalize', '~> 0.3'
|
||||||
gem 'redcarpet', '~> 3.4'
|
gem 'redcarpet', '~> 3.4'
|
||||||
|
|
||||||
group :development, :test do
|
group :development, :test do
|
||||||
gem 'fabrication', '~> 2.20'
|
gem 'fabrication', '~> 2.21'
|
||||||
gem 'fuubar', '~> 2.5'
|
gem 'fuubar', '~> 2.5'
|
||||||
gem 'i18n-tasks', '~> 0.9', require: false
|
gem 'i18n-tasks', '~> 0.9', require: false
|
||||||
gem 'pry-byebug', '~> 3.7'
|
gem 'pry-byebug', '~> 3.7'
|
||||||
|
@ -119,7 +119,7 @@ end
|
||||||
group :test do
|
group :test do
|
||||||
gem 'capybara', '~> 3.29'
|
gem 'capybara', '~> 3.29'
|
||||||
gem 'climate_control', '~> 0.2'
|
gem 'climate_control', '~> 0.2'
|
||||||
gem 'faker', '~> 2.7'
|
gem 'faker', '~> 2.8'
|
||||||
gem 'microformats', '~> 4.1'
|
gem 'microformats', '~> 4.1'
|
||||||
gem 'rails-controller-testing', '~> 1.0'
|
gem 'rails-controller-testing', '~> 1.0'
|
||||||
gem 'rspec-sidekiq', '~> 3.0'
|
gem 'rspec-sidekiq', '~> 3.0'
|
||||||
|
@ -138,7 +138,7 @@ group :development do
|
||||||
gem 'letter_opener_web', '~> 1.3'
|
gem 'letter_opener_web', '~> 1.3'
|
||||||
gem 'memory_profiler'
|
gem 'memory_profiler'
|
||||||
gem 'rubocop', '~> 0.76', require: false
|
gem 'rubocop', '~> 0.76', require: false
|
||||||
gem 'rubocop-rails', '~> 2.3', require: false
|
gem 'rubocop-rails', '~> 2.4', require: false
|
||||||
gem 'brakeman', '~> 4.7', require: false
|
gem 'brakeman', '~> 4.7', require: false
|
||||||
gem 'bundler-audit', '~> 0.6', require: false
|
gem 'bundler-audit', '~> 0.6', require: false
|
||||||
|
|
||||||
|
|
42
Gemfile.lock
42
Gemfile.lock
|
@ -105,16 +105,16 @@ GEM
|
||||||
av (0.9.0)
|
av (0.9.0)
|
||||||
cocaine (~> 0.5.3)
|
cocaine (~> 0.5.3)
|
||||||
aws-eventstream (1.0.3)
|
aws-eventstream (1.0.3)
|
||||||
aws-partitions (1.240.0)
|
aws-partitions (1.246.0)
|
||||||
aws-sdk-core (3.78.0)
|
aws-sdk-core (3.82.0)
|
||||||
aws-eventstream (~> 1.0, >= 1.0.2)
|
aws-eventstream (~> 1.0, >= 1.0.2)
|
||||||
aws-partitions (~> 1, >= 1.239.0)
|
aws-partitions (~> 1, >= 1.239.0)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.1)
|
||||||
jmespath (~> 1.0)
|
jmespath (~> 1.0)
|
||||||
aws-sdk-kms (1.25.0)
|
aws-sdk-kms (1.26.0)
|
||||||
aws-sdk-core (~> 3, >= 3.71.0)
|
aws-sdk-core (~> 3, >= 3.71.0)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.1)
|
||||||
aws-sdk-s3 (1.55.0)
|
aws-sdk-s3 (1.57.0)
|
||||||
aws-sdk-core (~> 3, >= 3.77.0)
|
aws-sdk-core (~> 3, >= 3.77.0)
|
||||||
aws-sdk-kms (~> 1)
|
aws-sdk-kms (~> 1)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.1)
|
||||||
|
@ -132,7 +132,7 @@ GEM
|
||||||
ffi (~> 1.10.0)
|
ffi (~> 1.10.0)
|
||||||
bootsnap (1.4.5)
|
bootsnap (1.4.5)
|
||||||
msgpack (~> 1.0)
|
msgpack (~> 1.0)
|
||||||
brakeman (4.7.1)
|
brakeman (4.7.2)
|
||||||
browser (2.7.1)
|
browser (2.7.1)
|
||||||
builder (3.2.3)
|
builder (3.2.3)
|
||||||
bullet (6.0.2)
|
bullet (6.0.2)
|
||||||
|
@ -239,8 +239,8 @@ GEM
|
||||||
et-orbi (1.1.6)
|
et-orbi (1.1.6)
|
||||||
tzinfo
|
tzinfo
|
||||||
excon (0.62.0)
|
excon (0.62.0)
|
||||||
fabrication (2.20.2)
|
fabrication (2.21.0)
|
||||||
faker (2.7.0)
|
faker (2.8.0)
|
||||||
i18n (>= 1.6, < 1.8)
|
i18n (>= 1.6, < 1.8)
|
||||||
faraday (0.15.4)
|
faraday (0.15.4)
|
||||||
multipart-post (>= 1.2, < 3)
|
multipart-post (>= 1.2, < 3)
|
||||||
|
@ -384,12 +384,12 @@ GEM
|
||||||
msgpack (1.3.1)
|
msgpack (1.3.1)
|
||||||
multi_json (1.13.1)
|
multi_json (1.13.1)
|
||||||
multipart-post (2.1.1)
|
multipart-post (2.1.1)
|
||||||
necromancer (0.5.0)
|
necromancer (0.5.1)
|
||||||
net-ldap (0.16.2)
|
net-ldap (0.16.2)
|
||||||
net-scp (2.0.0)
|
net-scp (2.0.0)
|
||||||
net-ssh (>= 2.6.5, < 6.0.0)
|
net-ssh (>= 2.6.5, < 6.0.0)
|
||||||
net-ssh (5.2.0)
|
net-ssh (5.2.0)
|
||||||
nio4r (2.5.1)
|
nio4r (2.5.2)
|
||||||
nokogiri (1.10.5)
|
nokogiri (1.10.5)
|
||||||
mini_portile2 (~> 2.4.0)
|
mini_portile2 (~> 2.4.0)
|
||||||
nokogumbo (2.0.1)
|
nokogumbo (2.0.1)
|
||||||
|
@ -455,7 +455,7 @@ GEM
|
||||||
pry-rails (0.3.9)
|
pry-rails (0.3.9)
|
||||||
pry (>= 0.10.4)
|
pry (>= 0.10.4)
|
||||||
public_suffix (4.0.1)
|
public_suffix (4.0.1)
|
||||||
puma (4.2.0)
|
puma (4.3.1)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
pundit (2.1.0)
|
pundit (2.1.0)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
|
@ -568,7 +568,7 @@ GEM
|
||||||
rainbow (>= 2.2.2, < 4.0)
|
rainbow (>= 2.2.2, < 4.0)
|
||||||
ruby-progressbar (~> 1.7)
|
ruby-progressbar (~> 1.7)
|
||||||
unicode-display_width (>= 1.4.0, < 1.7)
|
unicode-display_width (>= 1.4.0, < 1.7)
|
||||||
rubocop-rails (2.3.2)
|
rubocop-rails (2.4.0)
|
||||||
rack (>= 1.1)
|
rack (>= 1.1)
|
||||||
rubocop (>= 0.72.0)
|
rubocop (>= 0.72.0)
|
||||||
ruby-progressbar (1.10.1)
|
ruby-progressbar (1.10.1)
|
||||||
|
@ -637,11 +637,11 @@ GEM
|
||||||
tty-command (0.9.0)
|
tty-command (0.9.0)
|
||||||
pastel (~> 0.7.0)
|
pastel (~> 0.7.0)
|
||||||
tty-cursor (0.7.0)
|
tty-cursor (0.7.0)
|
||||||
tty-prompt (0.19.0)
|
tty-prompt (0.20.0)
|
||||||
necromancer (~> 0.5.0)
|
necromancer (~> 0.5.0)
|
||||||
pastel (~> 0.7.0)
|
pastel (~> 0.7.0)
|
||||||
tty-reader (~> 0.6.0)
|
tty-reader (~> 0.7.0)
|
||||||
tty-reader (0.6.0)
|
tty-reader (0.7.0)
|
||||||
tty-cursor (~> 0.7)
|
tty-cursor (~> 0.7)
|
||||||
tty-screen (~> 0.7)
|
tty-screen (~> 0.7)
|
||||||
wisper (~> 2.0.0)
|
wisper (~> 2.0.0)
|
||||||
|
@ -673,7 +673,7 @@ GEM
|
||||||
websocket-driver (0.7.0)
|
websocket-driver (0.7.0)
|
||||||
websocket-extensions (>= 0.1.0)
|
websocket-extensions (>= 0.1.0)
|
||||||
websocket-extensions (0.1.3)
|
websocket-extensions (0.1.3)
|
||||||
wisper (2.0.0)
|
wisper (2.0.1)
|
||||||
xpath (3.2.0)
|
xpath (3.2.0)
|
||||||
nokogiri (~> 1.8)
|
nokogiri (~> 1.8)
|
||||||
|
|
||||||
|
@ -685,7 +685,7 @@ DEPENDENCIES
|
||||||
active_record_query_trace (~> 1.7)
|
active_record_query_trace (~> 1.7)
|
||||||
addressable (~> 2.7)
|
addressable (~> 2.7)
|
||||||
annotate (~> 3.0)
|
annotate (~> 3.0)
|
||||||
aws-sdk-s3 (~> 1.55)
|
aws-sdk-s3 (~> 1.57)
|
||||||
better_errors (~> 2.5)
|
better_errors (~> 2.5)
|
||||||
binding_of_caller (~> 0.7)
|
binding_of_caller (~> 0.7)
|
||||||
blurhash (~> 0.1)
|
blurhash (~> 0.1)
|
||||||
|
@ -712,8 +712,8 @@ DEPENDENCIES
|
||||||
discard (~> 1.1)
|
discard (~> 1.1)
|
||||||
doorkeeper (~> 5.2)
|
doorkeeper (~> 5.2)
|
||||||
dotenv-rails (~> 2.7)
|
dotenv-rails (~> 2.7)
|
||||||
fabrication (~> 2.20)
|
fabrication (~> 2.21)
|
||||||
faker (~> 2.7)
|
faker (~> 2.8)
|
||||||
fast_blank (~> 1.0)
|
fast_blank (~> 1.0)
|
||||||
fastimage
|
fastimage
|
||||||
fog-core (<= 2.1.0)
|
fog-core (<= 2.1.0)
|
||||||
|
@ -767,7 +767,7 @@ DEPENDENCIES
|
||||||
private_address_check (~> 0.5)
|
private_address_check (~> 0.5)
|
||||||
pry-byebug (~> 3.7)
|
pry-byebug (~> 3.7)
|
||||||
pry-rails (~> 0.3)
|
pry-rails (~> 0.3)
|
||||||
puma (~> 4.2)
|
puma (~> 4.3)
|
||||||
pundit (~> 2.1)
|
pundit (~> 2.1)
|
||||||
rack-attack (~> 6.2)
|
rack-attack (~> 6.2)
|
||||||
rack-cors (~> 1.1)
|
rack-cors (~> 1.1)
|
||||||
|
@ -784,7 +784,7 @@ DEPENDENCIES
|
||||||
rspec-rails (~> 3.9)
|
rspec-rails (~> 3.9)
|
||||||
rspec-sidekiq (~> 3.0)
|
rspec-sidekiq (~> 3.0)
|
||||||
rubocop (~> 0.76)
|
rubocop (~> 0.76)
|
||||||
rubocop-rails (~> 2.3)
|
rubocop-rails (~> 2.4)
|
||||||
ruby-progressbar (~> 1.10)
|
ruby-progressbar (~> 1.10)
|
||||||
sanitize (~> 5.1)
|
sanitize (~> 5.1)
|
||||||
sidekiq (~> 5.2)
|
sidekiq (~> 5.2)
|
||||||
|
@ -801,7 +801,7 @@ DEPENDENCIES
|
||||||
strong_migrations (~> 0.4)
|
strong_migrations (~> 0.4)
|
||||||
thor (~> 0.20)
|
thor (~> 0.20)
|
||||||
tty-command (~> 0.9)
|
tty-command (~> 0.9)
|
||||||
tty-prompt (~> 0.19)
|
tty-prompt (~> 0.20)
|
||||||
twitter-text (~> 1.14)
|
twitter-text (~> 1.14)
|
||||||
tzinfo-data (~> 1.2019)
|
tzinfo-data (~> 1.2019)
|
||||||
webmock (~> 3.7)
|
webmock (~> 3.7)
|
||||||
|
|
|
@ -20,6 +20,10 @@ class Api::BaseController < ApplicationController
|
||||||
render json: { error: e.to_s }, status: 422
|
render json: { error: e.to_s }, status: 422
|
||||||
end
|
end
|
||||||
|
|
||||||
|
rescue_from ActiveRecord::RecordNotUnique do
|
||||||
|
render json: { error: 'Duplicate record' }, status: 422
|
||||||
|
end
|
||||||
|
|
||||||
rescue_from ActiveRecord::RecordNotFound do
|
rescue_from ActiveRecord::RecordNotFound do
|
||||||
render json: { error: 'Record not found' }, status: 404
|
render json: { error: 'Record not found' }, status: 404
|
||||||
end
|
end
|
||||||
|
|
|
@ -51,6 +51,6 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController
|
||||||
|
|
||||||
def data_params
|
def data_params
|
||||||
return {} if params[:data].blank?
|
return {} if params[:data].blank?
|
||||||
params.require(:data).permit(alerts: [:follow, :favourite, :reblog, :mention, :poll])
|
params.require(:data).permit(alerts: [:follow, :follow_request, :favourite, :reblog, :mention, :poll])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -19,6 +19,7 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController
|
||||||
data = {
|
data = {
|
||||||
alerts: {
|
alerts: {
|
||||||
follow: alerts_enabled,
|
follow: alerts_enabled,
|
||||||
|
follow_request: false,
|
||||||
favourite: alerts_enabled,
|
favourite: alerts_enabled,
|
||||||
reblog: alerts_enabled,
|
reblog: alerts_enabled,
|
||||||
mention: alerts_enabled,
|
mention: alerts_enabled,
|
||||||
|
@ -58,6 +59,6 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def data_params
|
def data_params
|
||||||
@data_params ||= params.require(:data).permit(alerts: [:follow, :favourite, :reblog, :mention, :poll])
|
@data_params ||= params.require(:data).permit(alerts: [:follow, :follow_request, :favourite, :reblog, :mention, :poll])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -62,6 +62,8 @@ module AccountsHelper
|
||||||
def account_badge(account, all: false)
|
def account_badge(account, all: false)
|
||||||
if account.bot?
|
if account.bot?
|
||||||
content_tag(:div, content_tag(:div, t('accounts.roles.bot'), class: 'account-role bot'), class: 'roles')
|
content_tag(:div, content_tag(:div, t('accounts.roles.bot'), class: 'account-role bot'), class: 'roles')
|
||||||
|
elsif account.group?
|
||||||
|
content_tag(:div, content_tag(:div, t('accounts.roles.group'), class: 'account-role group'), class: 'roles')
|
||||||
elsif (Setting.show_staff_badge && account.user_staff?) || all
|
elsif (Setting.show_staff_badge && account.user_staff?) || all
|
||||||
content_tag(:div, class: 'roles') do
|
content_tag(:div, class: 'roles') do
|
||||||
if all && !account.user_staff?
|
if all && !account.user_staff?
|
||||||
|
|
|
@ -121,7 +121,7 @@ const excludeTypesFromSettings = state => state.getIn(['settings', 'notification
|
||||||
|
|
||||||
|
|
||||||
const excludeTypesFromFilter = filter => {
|
const excludeTypesFromFilter = filter => {
|
||||||
const allTypes = ImmutableList(['follow', 'favourite', 'reblog', 'mention', 'poll']);
|
const allTypes = ImmutableList(['follow', 'follow_request', 'favourite', 'reblog', 'mention', 'poll']);
|
||||||
return allTypes.filterNot(item => item === filter).toJS();
|
return allTypes.filterNot(item => item === filter).toJS();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -67,9 +67,7 @@ class Poll extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleOptionChange = e => {
|
_toggleOption = value => {
|
||||||
const { target: { value } } = e;
|
|
||||||
|
|
||||||
if (this.props.poll.get('multiple')) {
|
if (this.props.poll.get('multiple')) {
|
||||||
const tmp = { ...this.state.selected };
|
const tmp = { ...this.state.selected };
|
||||||
if (tmp[value]) {
|
if (tmp[value]) {
|
||||||
|
@ -83,8 +81,20 @@ class Poll extends ImmutablePureComponent {
|
||||||
tmp[value] = true;
|
tmp[value] = true;
|
||||||
this.setState({ selected: tmp });
|
this.setState({ selected: tmp });
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleOptionChange = ({ target: { value } }) => {
|
||||||
|
this._toggleOption(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
handleOptionKeyPress = (e) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
this._toggleOption(e.target.getAttribute('data-index'));
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handleVote = () => {
|
handleVote = () => {
|
||||||
if (this.props.disabled) {
|
if (this.props.disabled) {
|
||||||
return;
|
return;
|
||||||
|
@ -135,7 +145,17 @@ class Poll extends ImmutablePureComponent {
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{!showResults && <span className={classNames('poll__input', { checkbox: poll.get('multiple'), active })} />}
|
{!showResults && (
|
||||||
|
<span
|
||||||
|
className={classNames('poll__input', { checkbox: poll.get('multiple'), active })}
|
||||||
|
tabIndex='0'
|
||||||
|
role={poll.get('multiple') ? 'checkbox' : 'radio'}
|
||||||
|
onKeyPress={this.handleOptionKeyPress}
|
||||||
|
aria-checked={active}
|
||||||
|
aria-label={option.get('title')}
|
||||||
|
data-index={optionIndex}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{showResults && <span className='poll__number'>
|
{showResults && <span className='poll__number'>
|
||||||
{!!voted && <Icon id='check' className='poll__vote__mark' title={intl.formatMessage(messages.voted)} />}
|
{!!voted && <Icon id='check' className='poll__vote__mark' title={intl.formatMessage(messages.voted)} />}
|
||||||
{Math.round(percent)}%
|
{Math.round(percent)}%
|
||||||
|
|
|
@ -232,9 +232,18 @@ class Header extends ImmutablePureComponent {
|
||||||
const content = { __html: account.get('note_emojified') };
|
const content = { __html: account.get('note_emojified') };
|
||||||
const displayNameHtml = { __html: account.get('display_name_html') };
|
const displayNameHtml = { __html: account.get('display_name_html') };
|
||||||
const fields = account.get('fields');
|
const fields = account.get('fields');
|
||||||
const badge = account.get('bot') ? (<div className='account-role bot'><FormattedMessage id='account.badges.bot' defaultMessage='Bot' /></div>) : null;
|
|
||||||
const acct = account.get('acct').indexOf('@') === -1 && domain ? `${account.get('acct')}@${domain}` : account.get('acct');
|
const acct = account.get('acct').indexOf('@') === -1 && domain ? `${account.get('acct')}@${domain}` : account.get('acct');
|
||||||
|
|
||||||
|
let badge;
|
||||||
|
|
||||||
|
if (account.get('bot')) {
|
||||||
|
badge = (<div className='account-role bot'><FormattedMessage id='account.badges.bot' defaultMessage='Bot' /></div>);
|
||||||
|
} else if (account.get('group')) {
|
||||||
|
badge = (<div className='account-role group'><FormattedMessage id='account.badges.group' defaultMessage='Group' /></div>);
|
||||||
|
} else {
|
||||||
|
badge = null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames('account__header', { inactive: !!account.get('moved') })} ref={this.setRef}>
|
<div className={classNames('account__header', { inactive: !!account.get('moved') })} ref={this.setRef}>
|
||||||
<div className='account__header__image'>
|
<div className='account__header__image'>
|
||||||
|
|
|
@ -58,6 +58,17 @@ export default class ColumnSettings extends React.PureComponent {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div role='group' aria-labelledby='notifications-follow-request'>
|
||||||
|
<span id='notifications-follow-request' className='column-settings__section'><FormattedMessage id='notifications.column_settings.follow_request' defaultMessage='New follow requests:' /></span>
|
||||||
|
|
||||||
|
<div className='column-settings__row'>
|
||||||
|
<SettingToggle prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'follow_request']} onChange={onChange} label={alertStr} />
|
||||||
|
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'follow_request']} onChange={this.onPushChange} label={pushStr} />}
|
||||||
|
<SettingToggle prefix='notifications' settings={settings} settingPath={['shows', 'follow_request']} onChange={onChange} label={showStr} />
|
||||||
|
<SettingToggle prefix='notifications' settings={settings} settingPath={['sounds', 'follow_request']} onChange={onChange} label={soundStr} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div role='group' aria-labelledby='notifications-favourite'>
|
<div role='group' aria-labelledby='notifications-favourite'>
|
||||||
<span id='notifications-favourite' className='column-settings__section'><FormattedMessage id='notifications.column_settings.favourite' defaultMessage='Favourites:' /></span>
|
<span id='notifications-favourite' className='column-settings__section'><FormattedMessage id='notifications.column_settings.favourite' defaultMessage='Favourites:' /></span>
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,130 @@
|
||||||
|
import React, { Fragment } from 'react';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import Avatar from 'flavours/glitch/components/avatar';
|
||||||
|
import DisplayName from 'flavours/glitch/components/display_name';
|
||||||
|
import Permalink from 'flavours/glitch/components/permalink';
|
||||||
|
import IconButton from 'flavours/glitch/components/icon_button';
|
||||||
|
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
import NotificationOverlayContainer from '../containers/overlay_container';
|
||||||
|
import { HotKeys } from 'react-hotkeys';
|
||||||
|
import Icon from 'flavours/glitch/components/icon';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
authorize: { id: 'follow_request.authorize', defaultMessage: 'Authorize' },
|
||||||
|
reject: { id: 'follow_request.reject', defaultMessage: 'Reject' },
|
||||||
|
});
|
||||||
|
|
||||||
|
export default @injectIntl
|
||||||
|
class FollowRequest extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
account: ImmutablePropTypes.map.isRequired,
|
||||||
|
onAuthorize: PropTypes.func.isRequired,
|
||||||
|
onReject: PropTypes.func.isRequired,
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
|
notification: ImmutablePropTypes.map.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
handleMoveUp = () => {
|
||||||
|
const { notification, onMoveUp } = this.props;
|
||||||
|
onMoveUp(notification.get('id'));
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMoveDown = () => {
|
||||||
|
const { notification, onMoveDown } = this.props;
|
||||||
|
onMoveDown(notification.get('id'));
|
||||||
|
}
|
||||||
|
|
||||||
|
handleOpen = () => {
|
||||||
|
this.handleOpenProfile();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleOpenProfile = () => {
|
||||||
|
const { notification } = this.props;
|
||||||
|
this.context.router.history.push(`/accounts/${notification.getIn(['account', 'id'])}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMention = e => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const { notification, onMention } = this.props;
|
||||||
|
onMention(notification.get('account'), this.context.router.history);
|
||||||
|
}
|
||||||
|
|
||||||
|
getHandlers () {
|
||||||
|
return {
|
||||||
|
moveUp: this.handleMoveUp,
|
||||||
|
moveDown: this.handleMoveDown,
|
||||||
|
open: this.handleOpen,
|
||||||
|
openProfile: this.handleOpenProfile,
|
||||||
|
mention: this.handleMention,
|
||||||
|
reply: this.handleMention,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { intl, hidden, account, onAuthorize, onReject, notification } = this.props;
|
||||||
|
|
||||||
|
if (!account) {
|
||||||
|
return <div />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hidden) {
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
{account.get('display_name')}
|
||||||
|
{account.get('username')}
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Links to the display name.
|
||||||
|
const displayName = account.get('display_name_html') || account.get('username');
|
||||||
|
const link = (
|
||||||
|
<bdi><Permalink
|
||||||
|
className='notification__display-name'
|
||||||
|
href={account.get('url')}
|
||||||
|
title={account.get('acct')}
|
||||||
|
to={`/accounts/${account.get('id')}`}
|
||||||
|
dangerouslySetInnerHTML={{ __html: displayName }}
|
||||||
|
/></bdi>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HotKeys handlers={this.getHandlers()}>
|
||||||
|
<div className='notification notification-follow-request focusable' tabIndex='0'>
|
||||||
|
<div className='notification__message'>
|
||||||
|
<div className='notification__favourite-icon-wrapper'>
|
||||||
|
<Icon id='user' fixedWidth />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormattedMessage
|
||||||
|
id='notification.follow_request'
|
||||||
|
defaultMessage='{name} has requested to follow you'
|
||||||
|
values={{ name: link }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='account'>
|
||||||
|
<div className='account__wrapper'>
|
||||||
|
<Permalink key={account.get('id')} className='account__display-name' title={account.get('acct')} href={account.get('url')} to={`/accounts/${account.get('id')}`}>
|
||||||
|
<div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div>
|
||||||
|
<DisplayName account={account} />
|
||||||
|
</Permalink>
|
||||||
|
|
||||||
|
<div className='account__relationship'>
|
||||||
|
<IconButton title={intl.formatMessage(messages.authorize)} icon='check' onClick={onAuthorize} />
|
||||||
|
<IconButton title={intl.formatMessage(messages.reject)} icon='times' onClick={onReject} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<NotificationOverlayContainer notification={notification} />
|
||||||
|
</div>
|
||||||
|
</HotKeys>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -7,6 +7,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
// Our imports,
|
// Our imports,
|
||||||
import StatusContainer from 'flavours/glitch/containers/status_container';
|
import StatusContainer from 'flavours/glitch/containers/status_container';
|
||||||
import NotificationFollow from './follow';
|
import NotificationFollow from './follow';
|
||||||
|
import NotificationFollowRequestContainer from '../containers/follow_request_container';
|
||||||
|
|
||||||
export default class Notification extends ImmutablePureComponent {
|
export default class Notification extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
@ -47,6 +48,18 @@ export default class Notification extends ImmutablePureComponent {
|
||||||
onMention={onMention}
|
onMention={onMention}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
case 'follow_request':
|
||||||
|
return (
|
||||||
|
<NotificationFollowRequestContainer
|
||||||
|
hidden={hidden}
|
||||||
|
id={notification.get('id')}
|
||||||
|
account={notification.get('account')}
|
||||||
|
notification={notification}
|
||||||
|
onMoveDown={onMoveDown}
|
||||||
|
onMoveUp={onMoveUp}
|
||||||
|
onMention={onMention}
|
||||||
|
/>
|
||||||
|
);
|
||||||
case 'mention':
|
case 'mention':
|
||||||
return (
|
return (
|
||||||
<StatusContainer
|
<StatusContainer
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { makeGetAccount } from 'flavours/glitch/selectors';
|
||||||
|
import FollowRequest from '../components/follow_request';
|
||||||
|
import { authorizeFollowRequest, rejectFollowRequest } from 'flavours/glitch/actions/accounts';
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch, { account }) => ({
|
||||||
|
onAuthorize () {
|
||||||
|
dispatch(authorizeFollowRequest(account.get('id')));
|
||||||
|
},
|
||||||
|
|
||||||
|
onReject () {
|
||||||
|
dispatch(rejectFollowRequest(account.get('id')));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(null, mapDispatchToProps)(FollowRequest);
|
|
@ -4,12 +4,10 @@ import { fetchFollowRequests } from 'flavours/glitch/actions/accounts';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { NavLink, withRouter } from 'react-router-dom';
|
import { NavLink, withRouter } from 'react-router-dom';
|
||||||
import IconWithBadge from 'flavours/glitch/components/icon_with_badge';
|
import IconWithBadge from 'flavours/glitch/components/icon_with_badge';
|
||||||
import { me } from 'flavours/glitch/util/initial_state';
|
|
||||||
import { List as ImmutableList } from 'immutable';
|
import { List as ImmutableList } from 'immutable';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
locked: state.getIn(['accounts', me, 'locked']),
|
|
||||||
count: state.getIn(['user_lists', 'follow_requests', 'items'], ImmutableList()).size,
|
count: state.getIn(['user_lists', 'follow_requests', 'items'], ImmutableList()).size,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -19,22 +17,19 @@ class FollowRequestsNavLink extends React.Component {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
dispatch: PropTypes.func.isRequired,
|
dispatch: PropTypes.func.isRequired,
|
||||||
locked: PropTypes.bool,
|
|
||||||
count: PropTypes.number.isRequired,
|
count: PropTypes.number.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
const { dispatch, locked } = this.props;
|
const { dispatch } = this.props;
|
||||||
|
|
||||||
if (locked) {
|
dispatch(fetchFollowRequests());
|
||||||
dispatch(fetchFollowRequests());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { locked, count } = this.props;
|
const { count } = this.props;
|
||||||
|
|
||||||
if (!locked || count === 0) {
|
if (count === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ import Video from 'flavours/glitch/features/video';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import Icon from 'mastodon/components/icon';
|
import Icon from 'flavours/glitch/components/icon';
|
||||||
|
|
||||||
export default class VideoModal extends ImmutablePureComponent {
|
export default class VideoModal extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,8 @@ import {
|
||||||
import {
|
import {
|
||||||
ACCOUNT_BLOCK_SUCCESS,
|
ACCOUNT_BLOCK_SUCCESS,
|
||||||
ACCOUNT_MUTE_SUCCESS,
|
ACCOUNT_MUTE_SUCCESS,
|
||||||
|
FOLLOW_REQUEST_AUTHORIZE_SUCCESS,
|
||||||
|
FOLLOW_REQUEST_REJECT_SUCCESS,
|
||||||
} from 'flavours/glitch/actions/accounts';
|
} from 'flavours/glitch/actions/accounts';
|
||||||
import { DOMAIN_BLOCK_SUCCESS } from 'flavours/glitch/actions/domain_blocks';
|
import { DOMAIN_BLOCK_SUCCESS } from 'flavours/glitch/actions/domain_blocks';
|
||||||
import { TIMELINE_DELETE, TIMELINE_DISCONNECT } from 'flavours/glitch/actions/timelines';
|
import { TIMELINE_DELETE, TIMELINE_DISCONNECT } from 'flavours/glitch/actions/timelines';
|
||||||
|
@ -113,8 +115,8 @@ const expandNormalizedNotifications = (state, notifications, next, isLoadingRece
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const filterNotifications = (state, accountIds) => {
|
const filterNotifications = (state, accountIds, type) => {
|
||||||
const helper = list => list.filterNot(item => item !== null && accountIds.includes(item.get('account')));
|
const helper = list => list.filterNot(item => item !== null && accountIds.includes(item.get('account')) && (type === undefined || type === item.get('type')));
|
||||||
return state.update('items', helper).update('pendingItems', helper);
|
return state.update('items', helper).update('pendingItems', helper);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -227,6 +229,11 @@ export default function notifications(state = initialState, action) {
|
||||||
return action.relationship.muting_notifications ? filterNotifications(state, [action.relationship.id]) : state;
|
return action.relationship.muting_notifications ? filterNotifications(state, [action.relationship.id]) : state;
|
||||||
case DOMAIN_BLOCK_SUCCESS:
|
case DOMAIN_BLOCK_SUCCESS:
|
||||||
return filterNotifications(state, action.accounts);
|
return filterNotifications(state, action.accounts);
|
||||||
|
case FOLLOW_REQUEST_AUTHORIZE_SUCCESS:
|
||||||
|
case FOLLOW_REQUEST_REJECT_SUCCESS:
|
||||||
|
return filterNotifications(state, [action.id], 'follow_request');
|
||||||
|
case ACCOUNT_MUTE_SUCCESS:
|
||||||
|
return action.relationship.muting_notifications ? filterNotifications(state, [action.relationship.id]) : state;
|
||||||
case NOTIFICATIONS_CLEAR:
|
case NOTIFICATIONS_CLEAR:
|
||||||
return state.set('items', ImmutableList()).set('pendingItems', ImmutableList()).set('hasMore', false);
|
return state.set('items', ImmutableList()).set('pendingItems', ImmutableList()).set('hasMore', false);
|
||||||
case TIMELINE_DELETE:
|
case TIMELINE_DELETE:
|
||||||
|
|
|
@ -6,6 +6,7 @@ const initialState = Immutable.Map({
|
||||||
subscription: null,
|
subscription: null,
|
||||||
alerts: new Immutable.Map({
|
alerts: new Immutable.Map({
|
||||||
follow: false,
|
follow: false,
|
||||||
|
follow_request: false,
|
||||||
favourite: false,
|
favourite: false,
|
||||||
reblog: false,
|
reblog: false,
|
||||||
mention: false,
|
mention: false,
|
||||||
|
|
|
@ -34,6 +34,7 @@ const initialState = ImmutableMap({
|
||||||
notifications: ImmutableMap({
|
notifications: ImmutableMap({
|
||||||
alerts: ImmutableMap({
|
alerts: ImmutableMap({
|
||||||
follow: true,
|
follow: true,
|
||||||
|
follow_request: false,
|
||||||
favourite: true,
|
favourite: true,
|
||||||
reblog: true,
|
reblog: true,
|
||||||
mention: true,
|
mention: true,
|
||||||
|
@ -48,6 +49,7 @@ const initialState = ImmutableMap({
|
||||||
|
|
||||||
shows: ImmutableMap({
|
shows: ImmutableMap({
|
||||||
follow: true,
|
follow: true,
|
||||||
|
follow_request: false,
|
||||||
favourite: true,
|
favourite: true,
|
||||||
reblog: true,
|
reblog: true,
|
||||||
mention: true,
|
mention: true,
|
||||||
|
@ -56,6 +58,7 @@ const initialState = ImmutableMap({
|
||||||
|
|
||||||
sounds: ImmutableMap({
|
sounds: ImmutableMap({
|
||||||
follow: true,
|
follow: true,
|
||||||
|
follow_request: false,
|
||||||
favourite: true,
|
favourite: true,
|
||||||
reblog: true,
|
reblog: true,
|
||||||
mention: true,
|
mention: true,
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
import {
|
||||||
|
NOTIFICATIONS_UPDATE,
|
||||||
|
} from '../actions/notifications';
|
||||||
import {
|
import {
|
||||||
FOLLOWERS_FETCH_SUCCESS,
|
FOLLOWERS_FETCH_SUCCESS,
|
||||||
FOLLOWERS_EXPAND_SUCCESS,
|
FOLLOWERS_EXPAND_SUCCESS,
|
||||||
|
@ -53,6 +56,12 @@ const appendToList = (state, type, id, accounts, next) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const normalizeFollowRequest = (state, notification) => {
|
||||||
|
return state.updateIn(['follow_requests', 'items'], list => {
|
||||||
|
return list.filterNot(item => item === notification.account.id).unshift(notification.account.id);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export default function userLists(state = initialState, action) {
|
export default function userLists(state = initialState, action) {
|
||||||
switch(action.type) {
|
switch(action.type) {
|
||||||
case FOLLOWERS_FETCH_SUCCESS:
|
case FOLLOWERS_FETCH_SUCCESS:
|
||||||
|
@ -67,6 +76,8 @@ export default function userLists(state = initialState, action) {
|
||||||
return state.setIn(['reblogged_by', action.id], ImmutableList(action.accounts.map(item => item.id)));
|
return state.setIn(['reblogged_by', action.id], ImmutableList(action.accounts.map(item => item.id)));
|
||||||
case FAVOURITES_FETCH_SUCCESS:
|
case FAVOURITES_FETCH_SUCCESS:
|
||||||
return state.setIn(['favourited_by', action.id], ImmutableList(action.accounts.map(item => item.id)));
|
return state.setIn(['favourited_by', action.id], ImmutableList(action.accounts.map(item => item.id)));
|
||||||
|
case NOTIFICATIONS_UPDATE:
|
||||||
|
return action.notification.type === 'follow_request' ? normalizeFollowRequest(state, action.notification) : state;
|
||||||
case FOLLOW_REQUESTS_FETCH_SUCCESS:
|
case FOLLOW_REQUESTS_FETCH_SUCCESS:
|
||||||
return state.setIn(['follow_requests', 'items'], ImmutableList(action.accounts.map(item => item.id))).setIn(['follow_requests', 'next'], action.next);
|
return state.setIn(['follow_requests', 'items'], ImmutableList(action.accounts.map(item => item.id))).setIn(['follow_requests', 'next'], action.next);
|
||||||
case FOLLOW_REQUESTS_EXPAND_SUCCESS:
|
case FOLLOW_REQUESTS_EXPAND_SUCCESS:
|
||||||
|
|
|
@ -232,7 +232,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.notif-cleaning {
|
.notif-cleaning {
|
||||||
.status, .notification-follow {
|
.status,
|
||||||
|
.notification-follow,
|
||||||
|
.notification-follow-request {
|
||||||
padding-right: ($dismiss-overlay-width + 0.5rem);
|
padding-right: ($dismiss-overlay-width + 0.5rem);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -256,7 +258,8 @@
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notification-follow {
|
.notification-follow,
|
||||||
|
.notification-follow-request {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
// same like Status
|
// same like Status
|
||||||
|
|
|
@ -98,6 +98,23 @@
|
||||||
border-color: $valid-value-color;
|
border-color: $valid-value-color;
|
||||||
background: $valid-value-color;
|
background: $valid-value-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:active,
|
||||||
|
&:focus,
|
||||||
|
&:hover {
|
||||||
|
border-width: 4px;
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-moz-focus-inner {
|
||||||
|
outline: 0 !important;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus,
|
||||||
|
&:active {
|
||||||
|
outline: 0 !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__number {
|
&__number {
|
||||||
|
@ -168,6 +185,10 @@
|
||||||
select {
|
select {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
flex: 1 1 50%;
|
flex: 1 1 50%;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-color: $highlight-text-color;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import WebSocketClient from 'websocket.js';
|
import WebSocketClient from '@gamestdio/websocket';
|
||||||
|
|
||||||
const randomIntUpTo = max => Math.floor(Math.random() * Math.floor(max));
|
const randomIntUpTo = max => Math.floor(Math.random() * Math.floor(max));
|
||||||
|
|
||||||
|
|
|
@ -110,7 +110,7 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
|
||||||
const excludeTypesFromSettings = state => state.getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS();
|
const excludeTypesFromSettings = state => state.getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS();
|
||||||
|
|
||||||
const excludeTypesFromFilter = filter => {
|
const excludeTypesFromFilter = filter => {
|
||||||
const allTypes = ImmutableList(['follow', 'favourite', 'reblog', 'mention', 'poll']);
|
const allTypes = ImmutableList(['follow', 'follow_request', 'favourite', 'reblog', 'mention', 'poll']);
|
||||||
return allTypes.filterNot(item => item === filter).toJS();
|
return allTypes.filterNot(item => item === filter).toJS();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -67,9 +67,7 @@ class Poll extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleOptionChange = e => {
|
_toggleOption = value => {
|
||||||
const { target: { value } } = e;
|
|
||||||
|
|
||||||
if (this.props.poll.get('multiple')) {
|
if (this.props.poll.get('multiple')) {
|
||||||
const tmp = { ...this.state.selected };
|
const tmp = { ...this.state.selected };
|
||||||
if (tmp[value]) {
|
if (tmp[value]) {
|
||||||
|
@ -83,8 +81,20 @@ class Poll extends ImmutablePureComponent {
|
||||||
tmp[value] = true;
|
tmp[value] = true;
|
||||||
this.setState({ selected: tmp });
|
this.setState({ selected: tmp });
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleOptionChange = ({ target: { value } }) => {
|
||||||
|
this._toggleOption(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
handleOptionKeyPress = (e) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
this._toggleOption(e.target.getAttribute('data-index'));
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handleVote = () => {
|
handleVote = () => {
|
||||||
if (this.props.disabled) {
|
if (this.props.disabled) {
|
||||||
return;
|
return;
|
||||||
|
@ -135,7 +145,17 @@ class Poll extends ImmutablePureComponent {
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{!showResults && <span className={classNames('poll__input', { checkbox: poll.get('multiple'), active })} />}
|
{!showResults && (
|
||||||
|
<span
|
||||||
|
className={classNames('poll__input', { checkbox: poll.get('multiple'), active })}
|
||||||
|
tabIndex='0'
|
||||||
|
role={poll.get('multiple') ? 'checkbox' : 'radio'}
|
||||||
|
onKeyPress={this.handleOptionKeyPress}
|
||||||
|
aria-checked={active}
|
||||||
|
aria-label={option.get('title')}
|
||||||
|
data-index={optionIndex}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{showResults && <span className='poll__number'>
|
{showResults && <span className='poll__number'>
|
||||||
{!!voted && <Icon id='check' className='poll__vote__mark' title={intl.formatMessage(messages.voted)} />}
|
{!!voted && <Icon id='check' className='poll__vote__mark' title={intl.formatMessage(messages.voted)} />}
|
||||||
{Math.round(percent)}%
|
{Math.round(percent)}%
|
||||||
|
|
|
@ -215,7 +215,8 @@ class Status extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleHotkeyOpenMedia = e => {
|
handleHotkeyOpenMedia = e => {
|
||||||
const { status, onOpenMedia, onOpenVideo } = this.props;
|
const { onOpenMedia, onOpenVideo } = this.props;
|
||||||
|
const status = this._properStatus();
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
|
|
|
@ -173,9 +173,9 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
const account = status.get('account');
|
const account = status.get('account');
|
||||||
|
|
||||||
if (relationship && relationship.get('blocking')) {
|
if (relationship && relationship.get('blocking')) {
|
||||||
onBlock(status);
|
|
||||||
} else {
|
|
||||||
onUnblock(account);
|
onUnblock(account);
|
||||||
|
} else {
|
||||||
|
onBlock(status);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -238,9 +238,18 @@ class Header extends ImmutablePureComponent {
|
||||||
const content = { __html: account.get('note_emojified') };
|
const content = { __html: account.get('note_emojified') };
|
||||||
const displayNameHtml = { __html: account.get('display_name_html') };
|
const displayNameHtml = { __html: account.get('display_name_html') };
|
||||||
const fields = account.get('fields');
|
const fields = account.get('fields');
|
||||||
const badge = account.get('bot') ? (<div className='account-role bot'><FormattedMessage id='account.badges.bot' defaultMessage='Bot' /></div>) : null;
|
|
||||||
const acct = account.get('acct').indexOf('@') === -1 && domain ? `${account.get('acct')}@${domain}` : account.get('acct');
|
const acct = account.get('acct').indexOf('@') === -1 && domain ? `${account.get('acct')}@${domain}` : account.get('acct');
|
||||||
|
|
||||||
|
let badge;
|
||||||
|
|
||||||
|
if (account.get('bot')) {
|
||||||
|
badge = (<div className='account-role bot'><FormattedMessage id='account.badges.bot' defaultMessage='Bot' /></div>);
|
||||||
|
} else if (account.get('group')) {
|
||||||
|
badge = (<div className='account-role group'><FormattedMessage id='account.badges.group' defaultMessage='Group' /></div>);
|
||||||
|
} else {
|
||||||
|
badge = null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames('account__header', { inactive: !!account.get('moved') })} ref={this.setRef}>
|
<div className={classNames('account__header', { inactive: !!account.get('moved') })} ref={this.setRef}>
|
||||||
<div className='account__header__image'>
|
<div className='account__header__image'>
|
||||||
|
|
|
@ -13,6 +13,8 @@ const messages = defineMessages({
|
||||||
add_option: { id: 'compose_form.poll.add_option', defaultMessage: 'Add a choice' },
|
add_option: { id: 'compose_form.poll.add_option', defaultMessage: 'Add a choice' },
|
||||||
remove_option: { id: 'compose_form.poll.remove_option', defaultMessage: 'Remove this choice' },
|
remove_option: { id: 'compose_form.poll.remove_option', defaultMessage: 'Remove this choice' },
|
||||||
poll_duration: { id: 'compose_form.poll.duration', defaultMessage: 'Poll duration' },
|
poll_duration: { id: 'compose_form.poll.duration', defaultMessage: 'Poll duration' },
|
||||||
|
switchToMultiple: { id: 'compose_form.poll.switch_to_multiple', defaultMessage: 'Change poll to allow multiple choices' },
|
||||||
|
switchToSingle: { id: 'compose_form.poll.switch_to_single', defaultMessage: 'Change poll to allow for a single choice' },
|
||||||
minutes: { id: 'intervals.full.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}}' },
|
minutes: { id: 'intervals.full.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}}' },
|
||||||
hours: { id: 'intervals.full.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}}' },
|
hours: { id: 'intervals.full.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}}' },
|
||||||
days: { id: 'intervals.full.days', defaultMessage: '{number, plural, one {# day} other {# days}}' },
|
days: { id: 'intervals.full.days', defaultMessage: '{number, plural, one {# day} other {# days}}' },
|
||||||
|
@ -50,6 +52,12 @@ class Option extends React.PureComponent {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
handleCheckboxKeypress = e => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
this.handleToggleMultiple(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onSuggestionsClearRequested = () => {
|
onSuggestionsClearRequested = () => {
|
||||||
this.props.onClearSuggestions();
|
this.props.onClearSuggestions();
|
||||||
}
|
}
|
||||||
|
@ -71,8 +79,11 @@ class Option extends React.PureComponent {
|
||||||
<span
|
<span
|
||||||
className={classNames('poll__input', { checkbox: isPollMultiple })}
|
className={classNames('poll__input', { checkbox: isPollMultiple })}
|
||||||
onClick={this.handleToggleMultiple}
|
onClick={this.handleToggleMultiple}
|
||||||
|
onKeyPress={this.handleCheckboxKeypress}
|
||||||
role='button'
|
role='button'
|
||||||
tabIndex='0'
|
tabIndex='0'
|
||||||
|
title={intl.formatMessage(isPollMultiple ? messages.switchToMultiple : messages.switchToSingle)}
|
||||||
|
aria-label={intl.formatMessage(isPollMultiple ? messages.switchToMultiple : messages.switchToSingle)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AutosuggestInput
|
<AutosuggestInput
|
||||||
|
|
|
@ -57,6 +57,17 @@ export default class ColumnSettings extends React.PureComponent {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div role='group' aria-labelledby='notifications-follow-request'>
|
||||||
|
<span id='notifications-follow-request' className='column-settings__section'><FormattedMessage id='notifications.column_settings.follow_request' defaultMessage='New follow requests:' /></span>
|
||||||
|
|
||||||
|
<div className='column-settings__row'>
|
||||||
|
<SettingToggle prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'follow_request']} onChange={onChange} label={alertStr} />
|
||||||
|
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'follow_request']} onChange={this.onPushChange} label={pushStr} />}
|
||||||
|
<SettingToggle prefix='notifications' settings={settings} settingPath={['shows', 'follow_request']} onChange={onChange} label={showStr} />
|
||||||
|
<SettingToggle prefix='notifications' settings={settings} settingPath={['sounds', 'follow_request']} onChange={onChange} label={soundStr} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div role='group' aria-labelledby='notifications-favourite'>
|
<div role='group' aria-labelledby='notifications-favourite'>
|
||||||
<span id='notifications-favourite' className='column-settings__section'><FormattedMessage id='notifications.column_settings.favourite' defaultMessage='Favourites:' /></span>
|
<span id='notifications-favourite' className='column-settings__section'><FormattedMessage id='notifications.column_settings.favourite' defaultMessage='Favourites:' /></span>
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
import React, { Fragment } from 'react';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import Avatar from 'mastodon/components/avatar';
|
||||||
|
import DisplayName from 'mastodon/components/display_name';
|
||||||
|
import Permalink from 'mastodon/components/permalink';
|
||||||
|
import IconButton from 'mastodon/components/icon_button';
|
||||||
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
authorize: { id: 'follow_request.authorize', defaultMessage: 'Authorize' },
|
||||||
|
reject: { id: 'follow_request.reject', defaultMessage: 'Reject' },
|
||||||
|
});
|
||||||
|
|
||||||
|
export default @injectIntl
|
||||||
|
class FollowRequest extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
account: ImmutablePropTypes.map.isRequired,
|
||||||
|
onAuthorize: PropTypes.func.isRequired,
|
||||||
|
onReject: PropTypes.func.isRequired,
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { intl, hidden, account, onAuthorize, onReject } = this.props;
|
||||||
|
|
||||||
|
if (!account) {
|
||||||
|
return <div />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hidden) {
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
{account.get('display_name')}
|
||||||
|
{account.get('username')}
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='account'>
|
||||||
|
<div className='account__wrapper'>
|
||||||
|
<Permalink key={account.get('id')} className='account__display-name' title={account.get('acct')} href={account.get('url')} to={`/accounts/${account.get('id')}`}>
|
||||||
|
<div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div>
|
||||||
|
<DisplayName account={account} />
|
||||||
|
</Permalink>
|
||||||
|
|
||||||
|
<div className='account__relationship'>
|
||||||
|
<IconButton title={intl.formatMessage(messages.authorize)} icon='check' onClick={onAuthorize} />
|
||||||
|
<IconButton title={intl.formatMessage(messages.reject)} icon='times' onClick={onReject} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -7,6 +7,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import { me } from 'mastodon/initial_state';
|
import { me } from 'mastodon/initial_state';
|
||||||
import StatusContainer from 'mastodon/containers/status_container';
|
import StatusContainer from 'mastodon/containers/status_container';
|
||||||
import AccountContainer from 'mastodon/containers/account_container';
|
import AccountContainer from 'mastodon/containers/account_container';
|
||||||
|
import FollowRequestContainer from '../containers/follow_request_container';
|
||||||
import Icon from 'mastodon/components/icon';
|
import Icon from 'mastodon/components/icon';
|
||||||
import Permalink from 'mastodon/components/permalink';
|
import Permalink from 'mastodon/components/permalink';
|
||||||
|
|
||||||
|
@ -127,7 +128,29 @@ class Notification extends ImmutablePureComponent {
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AccountContainer id={account.get('id')} withNote={false} hidden={this.props.hidden} />
|
<AccountContainer id={account.get('id')} hidden={this.props.hidden} />
|
||||||
|
</div>
|
||||||
|
</HotKeys>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderFollowRequest (notification, account, link) {
|
||||||
|
const { intl } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HotKeys handlers={this.getHandlers()}>
|
||||||
|
<div className='notification notification-follow-request focusable' tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage({ id: 'notification.follow_request', defaultMessage: '{name} has requested to follow you' }, { name: account.get('acct') }), notification.get('created_at'))}>
|
||||||
|
<div className='notification__message'>
|
||||||
|
<div className='notification__favourite-icon-wrapper'>
|
||||||
|
<Icon id='user' fixedWidth />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span title={notification.get('created_at')}>
|
||||||
|
<FormattedMessage id='notification.follow_request' defaultMessage='{name} has requested to follow you' values={{ name: link }} />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FollowRequestContainer id={account.get('id')} withNote={false} hidden={this.props.hidden} />
|
||||||
</div>
|
</div>
|
||||||
</HotKeys>
|
</HotKeys>
|
||||||
);
|
);
|
||||||
|
@ -261,6 +284,8 @@ class Notification extends ImmutablePureComponent {
|
||||||
switch(notification.get('type')) {
|
switch(notification.get('type')) {
|
||||||
case 'follow':
|
case 'follow':
|
||||||
return this.renderFollow(notification, account, link);
|
return this.renderFollow(notification, account, link);
|
||||||
|
case 'follow_request':
|
||||||
|
return this.renderFollowRequest(notification, account, link);
|
||||||
case 'mention':
|
case 'mention':
|
||||||
return this.renderMention(notification);
|
return this.renderMention(notification);
|
||||||
case 'favourite':
|
case 'favourite':
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { makeGetAccount } from 'mastodon/selectors';
|
||||||
|
import FollowRequest from '../components/follow_request';
|
||||||
|
import { authorizeFollowRequest, rejectFollowRequest } from 'mastodon/actions/accounts';
|
||||||
|
|
||||||
|
const makeMapStateToProps = () => {
|
||||||
|
const getAccount = makeGetAccount();
|
||||||
|
|
||||||
|
const mapStateToProps = (state, props) => ({
|
||||||
|
account: getAccount(state, props.id),
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapStateToProps;
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch, { id }) => ({
|
||||||
|
onAuthorize () {
|
||||||
|
dispatch(authorizeFollowRequest(id));
|
||||||
|
},
|
||||||
|
|
||||||
|
onReject () {
|
||||||
|
dispatch(rejectFollowRequest(id));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(makeMapStateToProps, mapDispatchToProps)(FollowRequest);
|
|
@ -120,9 +120,9 @@ class ActionBar extends React.PureComponent {
|
||||||
const account = status.get('account');
|
const account = status.get('account');
|
||||||
|
|
||||||
if (relationship && relationship.get('blocking')) {
|
if (relationship && relationship.get('blocking')) {
|
||||||
onBlock(status);
|
|
||||||
} else {
|
|
||||||
onUnblock(account);
|
onUnblock(account);
|
||||||
|
} else {
|
||||||
|
onBlock(status);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -282,7 +282,7 @@ class Status extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleHotkeyOpenMedia = e => {
|
handleHotkeyOpenMedia = e => {
|
||||||
const { status } = this.props;
|
const status = this._properStatus();
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
|
|
|
@ -4,12 +4,10 @@ import { fetchFollowRequests } from 'mastodon/actions/accounts';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { NavLink, withRouter } from 'react-router-dom';
|
import { NavLink, withRouter } from 'react-router-dom';
|
||||||
import IconWithBadge from 'mastodon/components/icon_with_badge';
|
import IconWithBadge from 'mastodon/components/icon_with_badge';
|
||||||
import { me } from 'mastodon/initial_state';
|
|
||||||
import { List as ImmutableList } from 'immutable';
|
import { List as ImmutableList } from 'immutable';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
locked: state.getIn(['accounts', me, 'locked']),
|
|
||||||
count: state.getIn(['user_lists', 'follow_requests', 'items'], ImmutableList()).size,
|
count: state.getIn(['user_lists', 'follow_requests', 'items'], ImmutableList()).size,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -19,22 +17,19 @@ class FollowRequestsNavLink extends React.Component {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
dispatch: PropTypes.func.isRequired,
|
dispatch: PropTypes.func.isRequired,
|
||||||
locked: PropTypes.bool,
|
|
||||||
count: PropTypes.number.isRequired,
|
count: PropTypes.number.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
const { dispatch, locked } = this.props;
|
const { dispatch } = this.props;
|
||||||
|
|
||||||
if (locked) {
|
dispatch(fetchFollowRequests());
|
||||||
dispatch(fetchFollowRequests());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { locked, count } = this.props;
|
const { count } = this.props;
|
||||||
|
|
||||||
if (!locked || count === 0) {
|
if (count === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -398,6 +398,14 @@
|
||||||
"defaultMessage": "Favourite",
|
"defaultMessage": "Favourite",
|
||||||
"id": "status.favourite"
|
"id": "status.favourite"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"defaultMessage": "Bookmark",
|
||||||
|
"id": "status.bookmark"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"defaultMessage": "Remove bookmark",
|
||||||
|
"id": "status.remove_bookmark"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"defaultMessage": "Expand this status",
|
"defaultMessage": "Expand this status",
|
||||||
"id": "status.open"
|
"id": "status.open"
|
||||||
|
@ -437,6 +445,22 @@
|
||||||
{
|
{
|
||||||
"defaultMessage": "Copy link to status",
|
"defaultMessage": "Copy link to status",
|
||||||
"id": "status.copy"
|
"id": "status.copy"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"defaultMessage": "Hide everything from {domain}",
|
||||||
|
"id": "account.block_domain"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"defaultMessage": "Unhide {domain}",
|
||||||
|
"id": "account.unblock_domain"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"defaultMessage": "Unmute @{name}",
|
||||||
|
"id": "account.unmute"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"defaultMessage": "Unblock @{name}",
|
||||||
|
"id": "account.unblock"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"path": "app/javascript/mastodon/components/status_action_bar.json"
|
"path": "app/javascript/mastodon/components/status_action_bar.json"
|
||||||
|
@ -530,6 +554,14 @@
|
||||||
{
|
{
|
||||||
"defaultMessage": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?",
|
"defaultMessage": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?",
|
||||||
"id": "confirmations.reply.message"
|
"id": "confirmations.reply.message"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"defaultMessage": "Hide entire domain",
|
||||||
|
"id": "confirmations.domain_block.confirm"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"defaultMessage": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.",
|
||||||
|
"id": "confirmations.domain_block.message"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"path": "app/javascript/mastodon/containers/status_container.json"
|
"path": "app/javascript/mastodon/containers/status_container.json"
|
||||||
|
@ -797,6 +829,19 @@
|
||||||
],
|
],
|
||||||
"path": "app/javascript/mastodon/features/blocks/index.json"
|
"path": "app/javascript/mastodon/features/blocks/index.json"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"descriptors": [
|
||||||
|
{
|
||||||
|
"defaultMessage": "Bookmarks",
|
||||||
|
"id": "column.bookmarks"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"defaultMessage": "You don't have any bookmarked toots yet. When you bookmark one, it will show up here.",
|
||||||
|
"id": "empty_column.bookmarked_statuses"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"path": "app/javascript/mastodon/features/bookmarked_statuses/index.json"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"descriptors": [
|
"descriptors": [
|
||||||
{
|
{
|
||||||
|
@ -1528,6 +1573,10 @@
|
||||||
"defaultMessage": "Direct messages",
|
"defaultMessage": "Direct messages",
|
||||||
"id": "navigation_bar.direct"
|
"id": "navigation_bar.direct"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"defaultMessage": "Bookmarks",
|
||||||
|
"id": "navigation_bar.bookmarks"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"defaultMessage": "Preferences",
|
"defaultMessage": "Preferences",
|
||||||
"id": "navigation_bar.preferences"
|
"id": "navigation_bar.preferences"
|
||||||
|
@ -1778,6 +1827,10 @@
|
||||||
"defaultMessage": "to open status",
|
"defaultMessage": "to open status",
|
||||||
"id": "keyboard_shortcuts.enter"
|
"id": "keyboard_shortcuts.enter"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"defaultMessage": "to open media",
|
||||||
|
"id": "keyboard_shortcuts.open_media"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"defaultMessage": "to show/hide text behind CW",
|
"defaultMessage": "to show/hide text behind CW",
|
||||||
"id": "keyboard_shortcuts.toggle_hidden"
|
"id": "keyboard_shortcuts.toggle_hidden"
|
||||||
|
@ -2028,6 +2081,10 @@
|
||||||
"defaultMessage": "New followers:",
|
"defaultMessage": "New followers:",
|
||||||
"id": "notifications.column_settings.follow"
|
"id": "notifications.column_settings.follow"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"defaultMessage": "New follow requests:",
|
||||||
|
"id": "notifications.column_settings.follow_request"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"defaultMessage": "Favourites:",
|
"defaultMessage": "Favourites:",
|
||||||
"id": "notifications.column_settings.favourite"
|
"id": "notifications.column_settings.favourite"
|
||||||
|
@ -2076,6 +2133,19 @@
|
||||||
],
|
],
|
||||||
"path": "app/javascript/mastodon/features/notifications/components/filter_bar.json"
|
"path": "app/javascript/mastodon/features/notifications/components/filter_bar.json"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"descriptors": [
|
||||||
|
{
|
||||||
|
"defaultMessage": "Authorize",
|
||||||
|
"id": "follow_request.authorize"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"defaultMessage": "Reject",
|
||||||
|
"id": "follow_request.reject"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"path": "app/javascript/mastodon/features/notifications/components/follow_request.json"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"descriptors": [
|
"descriptors": [
|
||||||
{
|
{
|
||||||
|
@ -2097,6 +2167,10 @@
|
||||||
{
|
{
|
||||||
"defaultMessage": "{name} boosted your status",
|
"defaultMessage": "{name} boosted your status",
|
||||||
"id": "notification.reblog"
|
"id": "notification.reblog"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"defaultMessage": "{name} has requested to follow you",
|
||||||
|
"id": "notification.follow_request"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"path": "app/javascript/mastodon/features/notifications/components/notification.json"
|
"path": "app/javascript/mastodon/features/notifications/components/notification.json"
|
||||||
|
@ -2204,6 +2278,10 @@
|
||||||
"defaultMessage": "Favourite",
|
"defaultMessage": "Favourite",
|
||||||
"id": "status.favourite"
|
"id": "status.favourite"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"defaultMessage": "Bookmark",
|
||||||
|
"id": "status.bookmark"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"defaultMessage": "Mute @{name}",
|
"defaultMessage": "Mute @{name}",
|
||||||
"id": "status.mute"
|
"id": "status.mute"
|
||||||
|
@ -2251,6 +2329,22 @@
|
||||||
{
|
{
|
||||||
"defaultMessage": "Copy link to status",
|
"defaultMessage": "Copy link to status",
|
||||||
"id": "status.copy"
|
"id": "status.copy"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"defaultMessage": "Hide everything from {domain}",
|
||||||
|
"id": "account.block_domain"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"defaultMessage": "Unhide {domain}",
|
||||||
|
"id": "account.unblock_domain"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"defaultMessage": "Unmute @{name}",
|
||||||
|
"id": "account.unmute"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"defaultMessage": "Unblock @{name}",
|
||||||
|
"id": "account.unblock"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"path": "app/javascript/mastodon/features/status/components/action_bar.json"
|
"path": "app/javascript/mastodon/features/status/components/action_bar.json"
|
||||||
|
@ -2321,6 +2415,14 @@
|
||||||
{
|
{
|
||||||
"defaultMessage": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?",
|
"defaultMessage": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?",
|
||||||
"id": "confirmations.reply.message"
|
"id": "confirmations.reply.message"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"defaultMessage": "Hide entire domain",
|
||||||
|
"id": "confirmations.domain_block.confirm"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"defaultMessage": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.",
|
||||||
|
"id": "confirmations.domain_block.message"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"path": "app/javascript/mastodon/features/status/index.json"
|
"path": "app/javascript/mastodon/features/status/index.json"
|
||||||
|
@ -2472,6 +2574,18 @@
|
||||||
"defaultMessage": "A quick brown fox jumps over the lazy dog",
|
"defaultMessage": "A quick brown fox jumps over the lazy dog",
|
||||||
"id": "upload_modal.description_placeholder"
|
"id": "upload_modal.description_placeholder"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"defaultMessage": "Describe for people with hearing loss",
|
||||||
|
"id": "upload_form.audio_description"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"defaultMessage": "Describe for people with hearing loss or visual impairment",
|
||||||
|
"id": "upload_form.video_description"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"defaultMessage": "Describe for the visually impaired",
|
||||||
|
"id": "upload_form.description"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"defaultMessage": "Edit media",
|
"defaultMessage": "Edit media",
|
||||||
"id": "upload_modal.edit_media"
|
"id": "upload_modal.edit_media"
|
||||||
|
@ -2480,10 +2594,6 @@
|
||||||
"defaultMessage": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
|
"defaultMessage": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
|
||||||
"id": "upload_modal.hint"
|
"id": "upload_modal.hint"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"defaultMessage": "Describe for the visually impaired",
|
|
||||||
"id": "upload_form.description"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"defaultMessage": "Analyzing picture…",
|
"defaultMessage": "Analyzing picture…",
|
||||||
"id": "upload_modal.analyzing_picture"
|
"id": "upload_modal.analyzing_picture"
|
||||||
|
@ -2633,6 +2743,10 @@
|
||||||
"defaultMessage": "Favourites",
|
"defaultMessage": "Favourites",
|
||||||
"id": "navigation_bar.favourites"
|
"id": "navigation_bar.favourites"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"defaultMessage": "Bookmarks",
|
||||||
|
"id": "navigation_bar.bookmarks"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"defaultMessage": "Lists",
|
"defaultMessage": "Lists",
|
||||||
"id": "navigation_bar.lists"
|
"id": "navigation_bar.lists"
|
||||||
|
|
|
@ -51,6 +51,7 @@
|
||||||
"bundle_modal_error.message": "Something went wrong while loading this component.",
|
"bundle_modal_error.message": "Something went wrong while loading this component.",
|
||||||
"bundle_modal_error.retry": "Try again",
|
"bundle_modal_error.retry": "Try again",
|
||||||
"column.blocks": "Blocked users",
|
"column.blocks": "Blocked users",
|
||||||
|
"column.bookmarks": "Bookmarks",
|
||||||
"column.community": "Local timeline",
|
"column.community": "Local timeline",
|
||||||
"column.direct": "Direct messages",
|
"column.direct": "Direct messages",
|
||||||
"column.directory": "Browse profiles",
|
"column.directory": "Browse profiles",
|
||||||
|
@ -142,6 +143,7 @@
|
||||||
"empty_column.account_timeline": "No toots here!",
|
"empty_column.account_timeline": "No toots here!",
|
||||||
"empty_column.account_unavailable": "Profile unavailable",
|
"empty_column.account_unavailable": "Profile unavailable",
|
||||||
"empty_column.blocks": "You haven't blocked any users yet.",
|
"empty_column.blocks": "You haven't blocked any users yet.",
|
||||||
|
"empty_column.bookmarked_statuses": "You don't have any bookmarked toots yet. When you bookmark one, it will show up here.",
|
||||||
"empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!",
|
"empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!",
|
||||||
"empty_column.direct": "You don't have any direct messages yet. When you send or receive one, it will show up here.",
|
"empty_column.direct": "You don't have any direct messages yet. When you send or receive one, it will show up here.",
|
||||||
"empty_column.domain_blocks": "There are no hidden domains yet.",
|
"empty_column.domain_blocks": "There are no hidden domains yet.",
|
||||||
|
@ -223,6 +225,7 @@
|
||||||
"keyboard_shortcuts.muted": "to open muted users list",
|
"keyboard_shortcuts.muted": "to open muted users list",
|
||||||
"keyboard_shortcuts.my_profile": "to open your profile",
|
"keyboard_shortcuts.my_profile": "to open your profile",
|
||||||
"keyboard_shortcuts.notifications": "to open notifications column",
|
"keyboard_shortcuts.notifications": "to open notifications column",
|
||||||
|
"keyboard_shortcuts.open_media": "to open media",
|
||||||
"keyboard_shortcuts.pinned": "to open pinned toots list",
|
"keyboard_shortcuts.pinned": "to open pinned toots list",
|
||||||
"keyboard_shortcuts.profile": "to open author's profile",
|
"keyboard_shortcuts.profile": "to open author's profile",
|
||||||
"keyboard_shortcuts.reply": "to reply",
|
"keyboard_shortcuts.reply": "to reply",
|
||||||
|
@ -255,6 +258,7 @@
|
||||||
"mute_modal.hide_notifications": "Hide notifications from this user?",
|
"mute_modal.hide_notifications": "Hide notifications from this user?",
|
||||||
"navigation_bar.apps": "Mobile apps",
|
"navigation_bar.apps": "Mobile apps",
|
||||||
"navigation_bar.blocks": "Blocked users",
|
"navigation_bar.blocks": "Blocked users",
|
||||||
|
"navigation_bar.bookmarks": "Bookmarks",
|
||||||
"navigation_bar.community_timeline": "Local timeline",
|
"navigation_bar.community_timeline": "Local timeline",
|
||||||
"navigation_bar.compose": "Compose new toot",
|
"navigation_bar.compose": "Compose new toot",
|
||||||
"navigation_bar.direct": "Direct messages",
|
"navigation_bar.direct": "Direct messages",
|
||||||
|
@ -278,6 +282,7 @@
|
||||||
"navigation_bar.security": "Security",
|
"navigation_bar.security": "Security",
|
||||||
"notification.favourite": "{name} favourited your status",
|
"notification.favourite": "{name} favourited your status",
|
||||||
"notification.follow": "{name} followed you",
|
"notification.follow": "{name} followed you",
|
||||||
|
"notification.follow_request": "{name} has requested to follow you",
|
||||||
"notification.mention": "{name} mentioned you",
|
"notification.mention": "{name} mentioned you",
|
||||||
"notification.own_poll": "Your poll has ended",
|
"notification.own_poll": "Your poll has ended",
|
||||||
"notification.poll": "A poll you have voted in has ended",
|
"notification.poll": "A poll you have voted in has ended",
|
||||||
|
@ -290,6 +295,7 @@
|
||||||
"notifications.column_settings.filter_bar.category": "Quick filter bar",
|
"notifications.column_settings.filter_bar.category": "Quick filter bar",
|
||||||
"notifications.column_settings.filter_bar.show": "Show",
|
"notifications.column_settings.filter_bar.show": "Show",
|
||||||
"notifications.column_settings.follow": "New followers:",
|
"notifications.column_settings.follow": "New followers:",
|
||||||
|
"notifications.column_settings.follow_request": "New follow requests:",
|
||||||
"notifications.column_settings.mention": "Mentions:",
|
"notifications.column_settings.mention": "Mentions:",
|
||||||
"notifications.column_settings.poll": "Poll results:",
|
"notifications.column_settings.poll": "Poll results:",
|
||||||
"notifications.column_settings.push": "Push notifications",
|
"notifications.column_settings.push": "Push notifications",
|
||||||
|
@ -350,6 +356,7 @@
|
||||||
"status.admin_account": "Open moderation interface for @{name}",
|
"status.admin_account": "Open moderation interface for @{name}",
|
||||||
"status.admin_status": "Open this status in the moderation interface",
|
"status.admin_status": "Open this status in the moderation interface",
|
||||||
"status.block": "Block @{name}",
|
"status.block": "Block @{name}",
|
||||||
|
"status.bookmark": "Bookmark",
|
||||||
"status.cancel_reblog_private": "Unboost",
|
"status.cancel_reblog_private": "Unboost",
|
||||||
"status.cannot_reblog": "This post cannot be boosted",
|
"status.cannot_reblog": "This post cannot be boosted",
|
||||||
"status.copy": "Copy link to status",
|
"status.copy": "Copy link to status",
|
||||||
|
@ -374,6 +381,7 @@
|
||||||
"status.reblogged_by": "{name} boosted",
|
"status.reblogged_by": "{name} boosted",
|
||||||
"status.reblogs.empty": "No one has boosted this toot yet. When someone does, they will show up here.",
|
"status.reblogs.empty": "No one has boosted this toot yet. When someone does, they will show up here.",
|
||||||
"status.redraft": "Delete & re-draft",
|
"status.redraft": "Delete & re-draft",
|
||||||
|
"status.remove_bookmark": "Remove bookmark",
|
||||||
"status.reply": "Reply",
|
"status.reply": "Reply",
|
||||||
"status.replyAll": "Reply to thread",
|
"status.replyAll": "Reply to thread",
|
||||||
"status.report": "Report @{name}",
|
"status.report": "Report @{name}",
|
||||||
|
@ -406,9 +414,11 @@
|
||||||
"upload_button.label": "Add media ({formats})",
|
"upload_button.label": "Add media ({formats})",
|
||||||
"upload_error.limit": "File upload limit exceeded.",
|
"upload_error.limit": "File upload limit exceeded.",
|
||||||
"upload_error.poll": "File upload not allowed with polls.",
|
"upload_error.poll": "File upload not allowed with polls.",
|
||||||
|
"upload_form.audio_description": "Describe for people with hearing loss",
|
||||||
"upload_form.description": "Describe for the visually impaired",
|
"upload_form.description": "Describe for the visually impaired",
|
||||||
"upload_form.edit": "Edit",
|
"upload_form.edit": "Edit",
|
||||||
"upload_form.undo": "Delete",
|
"upload_form.undo": "Delete",
|
||||||
|
"upload_form.video_description": "Describe for people with hearing loss or visual impairment",
|
||||||
"upload_modal.analyzing_picture": "Analyzing picture…",
|
"upload_modal.analyzing_picture": "Analyzing picture…",
|
||||||
"upload_modal.apply": "Apply",
|
"upload_modal.apply": "Apply",
|
||||||
"upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
|
"upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
|
||||||
|
|
|
@ -13,6 +13,8 @@ import {
|
||||||
import {
|
import {
|
||||||
ACCOUNT_BLOCK_SUCCESS,
|
ACCOUNT_BLOCK_SUCCESS,
|
||||||
ACCOUNT_MUTE_SUCCESS,
|
ACCOUNT_MUTE_SUCCESS,
|
||||||
|
FOLLOW_REQUEST_AUTHORIZE_SUCCESS,
|
||||||
|
FOLLOW_REQUEST_REJECT_SUCCESS,
|
||||||
} from '../actions/accounts';
|
} from '../actions/accounts';
|
||||||
import { DOMAIN_BLOCK_SUCCESS } from 'mastodon/actions/domain_blocks';
|
import { DOMAIN_BLOCK_SUCCESS } from 'mastodon/actions/domain_blocks';
|
||||||
import { TIMELINE_DELETE, TIMELINE_DISCONNECT } from '../actions/timelines';
|
import { TIMELINE_DELETE, TIMELINE_DISCONNECT } from '../actions/timelines';
|
||||||
|
@ -89,8 +91,8 @@ const expandNormalizedNotifications = (state, notifications, next, isLoadingRece
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const filterNotifications = (state, accountIds) => {
|
const filterNotifications = (state, accountIds, type) => {
|
||||||
const helper = list => list.filterNot(item => item !== null && accountIds.includes(item.get('account')));
|
const helper = list => list.filterNot(item => item !== null && accountIds.includes(item.get('account')) && (type === undefined || type === item.get('type')));
|
||||||
return state.update('items', helper).update('pendingItems', helper);
|
return state.update('items', helper).update('pendingItems', helper);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -129,6 +131,11 @@ export default function notifications(state = initialState, action) {
|
||||||
return action.relationship.muting_notifications ? filterNotifications(state, [action.relationship.id]) : state;
|
return action.relationship.muting_notifications ? filterNotifications(state, [action.relationship.id]) : state;
|
||||||
case DOMAIN_BLOCK_SUCCESS:
|
case DOMAIN_BLOCK_SUCCESS:
|
||||||
return filterNotifications(state, action.accounts);
|
return filterNotifications(state, action.accounts);
|
||||||
|
case FOLLOW_REQUEST_AUTHORIZE_SUCCESS:
|
||||||
|
case FOLLOW_REQUEST_REJECT_SUCCESS:
|
||||||
|
return filterNotifications(state, [action.id], 'follow_request');
|
||||||
|
case ACCOUNT_MUTE_SUCCESS:
|
||||||
|
return action.relationship.muting_notifications ? filterNotifications(state, [action.relationship.id]) : state;
|
||||||
case NOTIFICATIONS_CLEAR:
|
case NOTIFICATIONS_CLEAR:
|
||||||
return state.set('items', ImmutableList()).set('pendingItems', ImmutableList()).set('hasMore', false);
|
return state.set('items', ImmutableList()).set('pendingItems', ImmutableList()).set('hasMore', false);
|
||||||
case TIMELINE_DELETE:
|
case TIMELINE_DELETE:
|
||||||
|
|
|
@ -6,6 +6,7 @@ const initialState = Immutable.Map({
|
||||||
subscription: null,
|
subscription: null,
|
||||||
alerts: new Immutable.Map({
|
alerts: new Immutable.Map({
|
||||||
follow: false,
|
follow: false,
|
||||||
|
follow_request: false,
|
||||||
favourite: false,
|
favourite: false,
|
||||||
reblog: false,
|
reblog: false,
|
||||||
mention: false,
|
mention: false,
|
||||||
|
|
|
@ -30,6 +30,7 @@ const initialState = ImmutableMap({
|
||||||
notifications: ImmutableMap({
|
notifications: ImmutableMap({
|
||||||
alerts: ImmutableMap({
|
alerts: ImmutableMap({
|
||||||
follow: true,
|
follow: true,
|
||||||
|
follow_request: false,
|
||||||
favourite: true,
|
favourite: true,
|
||||||
reblog: true,
|
reblog: true,
|
||||||
mention: true,
|
mention: true,
|
||||||
|
@ -44,6 +45,7 @@ const initialState = ImmutableMap({
|
||||||
|
|
||||||
shows: ImmutableMap({
|
shows: ImmutableMap({
|
||||||
follow: true,
|
follow: true,
|
||||||
|
follow_request: false,
|
||||||
favourite: true,
|
favourite: true,
|
||||||
reblog: true,
|
reblog: true,
|
||||||
mention: true,
|
mention: true,
|
||||||
|
@ -52,6 +54,7 @@ const initialState = ImmutableMap({
|
||||||
|
|
||||||
sounds: ImmutableMap({
|
sounds: ImmutableMap({
|
||||||
follow: true,
|
follow: true,
|
||||||
|
follow_request: false,
|
||||||
favourite: true,
|
favourite: true,
|
||||||
reblog: true,
|
reblog: true,
|
||||||
mention: true,
|
mention: true,
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
import {
|
||||||
|
NOTIFICATIONS_UPDATE,
|
||||||
|
} from '../actions/notifications';
|
||||||
import {
|
import {
|
||||||
FOLLOWERS_FETCH_SUCCESS,
|
FOLLOWERS_FETCH_SUCCESS,
|
||||||
FOLLOWERS_EXPAND_SUCCESS,
|
FOLLOWERS_EXPAND_SUCCESS,
|
||||||
|
@ -53,6 +56,12 @@ const appendToList = (state, type, id, accounts, next) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const normalizeFollowRequest = (state, notification) => {
|
||||||
|
return state.updateIn(['follow_requests', 'items'], list => {
|
||||||
|
return list.filterNot(item => item === notification.account.id).unshift(notification.account.id);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export default function userLists(state = initialState, action) {
|
export default function userLists(state = initialState, action) {
|
||||||
switch(action.type) {
|
switch(action.type) {
|
||||||
case FOLLOWERS_FETCH_SUCCESS:
|
case FOLLOWERS_FETCH_SUCCESS:
|
||||||
|
@ -67,6 +76,8 @@ export default function userLists(state = initialState, action) {
|
||||||
return state.setIn(['reblogged_by', action.id], ImmutableList(action.accounts.map(item => item.id)));
|
return state.setIn(['reblogged_by', action.id], ImmutableList(action.accounts.map(item => item.id)));
|
||||||
case FAVOURITES_FETCH_SUCCESS:
|
case FAVOURITES_FETCH_SUCCESS:
|
||||||
return state.setIn(['favourited_by', action.id], ImmutableList(action.accounts.map(item => item.id)));
|
return state.setIn(['favourited_by', action.id], ImmutableList(action.accounts.map(item => item.id)));
|
||||||
|
case NOTIFICATIONS_UPDATE:
|
||||||
|
return action.notification.type === 'follow_request' ? normalizeFollowRequest(state, action.notification) : state;
|
||||||
case FOLLOW_REQUESTS_FETCH_SUCCESS:
|
case FOLLOW_REQUESTS_FETCH_SUCCESS:
|
||||||
return state.setIn(['follow_requests', 'items'], ImmutableList(action.accounts.map(item => item.id))).setIn(['follow_requests', 'next'], action.next);
|
return state.setIn(['follow_requests', 'items'], ImmutableList(action.accounts.map(item => item.id))).setIn(['follow_requests', 'next'], action.next);
|
||||||
case FOLLOW_REQUESTS_EXPAND_SUCCESS:
|
case FOLLOW_REQUESTS_EXPAND_SUCCESS:
|
||||||
|
|
|
@ -16,6 +16,7 @@ filenames.forEach(filename => {
|
||||||
filtered[locale] = {
|
filtered[locale] = {
|
||||||
'notification.favourite': full['notification.favourite'] || '',
|
'notification.favourite': full['notification.favourite'] || '',
|
||||||
'notification.follow': full['notification.follow'] || '',
|
'notification.follow': full['notification.follow'] || '',
|
||||||
|
'notification.follow_request': full['notification.follow_request'] || '',
|
||||||
'notification.mention': full['notification.mention'] || '',
|
'notification.mention': full['notification.mention'] || '',
|
||||||
'notification.reblog': full['notification.reblog'] || '',
|
'notification.reblog': full['notification.reblog'] || '',
|
||||||
'notification.poll': full['notification.poll'] || '',
|
'notification.poll': full['notification.poll'] || '',
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import WebSocketClient from 'websocket.js';
|
import WebSocketClient from '@gamestdio/websocket';
|
||||||
|
|
||||||
const randomIntUpTo = max => Math.floor(Math.random() * Math.floor(max));
|
const randomIntUpTo = max => Math.floor(Math.random() * Math.floor(max));
|
||||||
|
|
||||||
|
|
|
@ -91,6 +91,23 @@
|
||||||
border-color: $valid-value-color;
|
border-color: $valid-value-color;
|
||||||
background: $valid-value-color;
|
background: $valid-value-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:active,
|
||||||
|
&:focus,
|
||||||
|
&:hover {
|
||||||
|
border-width: 4px;
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-moz-focus-inner {
|
||||||
|
outline: 0 !important;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus,
|
||||||
|
&:active {
|
||||||
|
outline: 0 !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__number {
|
&__number {
|
||||||
|
@ -160,6 +177,10 @@
|
||||||
button,
|
button,
|
||||||
select {
|
select {
|
||||||
flex: 1 1 50%;
|
flex: 1 1 50%;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-color: $highlight-text-color;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -89,7 +89,7 @@ class ActivityPub::Activity
|
||||||
def distribute(status)
|
def distribute(status)
|
||||||
crawl_links(status)
|
crawl_links(status)
|
||||||
|
|
||||||
notify_about_reblog(status) if reblog_of_local_account?(status)
|
notify_about_reblog(status) if reblog_of_local_account?(status) && !reblog_by_following_group_account?(status)
|
||||||
notify_about_mentions(status)
|
notify_about_mentions(status)
|
||||||
|
|
||||||
# Only continue if the status is supposed to have arrived in real-time.
|
# Only continue if the status is supposed to have arrived in real-time.
|
||||||
|
@ -105,6 +105,10 @@ class ActivityPub::Activity
|
||||||
status.reblog? && status.reblog.account.local?
|
status.reblog? && status.reblog.account.local?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def reblog_by_following_group_account?(status)
|
||||||
|
status.reblog? && status.account.group? && status.reblog.account.following?(status.account)
|
||||||
|
end
|
||||||
|
|
||||||
def notify_about_reblog(status)
|
def notify_about_reblog(status)
|
||||||
NotifyService.new.call(status.reblog.account, status)
|
NotifyService.new.call(status.reblog.account, status)
|
||||||
end
|
end
|
||||||
|
|
|
@ -35,6 +35,7 @@ class ActivityPub::Adapter < ActiveModelSerializers::Adapter::Base
|
||||||
def serializable_hash(options = nil)
|
def serializable_hash(options = nil)
|
||||||
named_contexts = {}
|
named_contexts = {}
|
||||||
context_extensions = {}
|
context_extensions = {}
|
||||||
|
|
||||||
options = serialization_options(options)
|
options = serialization_options(options)
|
||||||
serialized_hash = serializer.serializable_hash(options.merge(named_contexts: named_contexts, context_extensions: context_extensions))
|
serialized_hash = serializer.serializable_hash(options.merge(named_contexts: named_contexts, context_extensions: context_extensions))
|
||||||
serialized_hash = serialized_hash.select { |k, _| options[:fields].include?(k) } if options[:fields]
|
serialized_hash = serialized_hash.select { |k, _| options[:fields].include?(k) } if options[:fields]
|
||||||
|
|
|
@ -68,10 +68,19 @@ class ActivityPub::TagManager
|
||||||
if status.account.silenced?
|
if status.account.silenced?
|
||||||
# Only notify followers if the account is locally silenced
|
# Only notify followers if the account is locally silenced
|
||||||
account_ids = status.active_mentions.pluck(:account_id)
|
account_ids = status.active_mentions.pluck(:account_id)
|
||||||
to = status.account.followers.where(id: account_ids).map { |account| uri_for(account) }
|
to = status.account.followers.where(id: account_ids).each_with_object([]) do |account, result|
|
||||||
to.concat(FollowRequest.where(target_account_id: status.account_id, account_id: account_ids).map { |request| uri_for(request.account) })
|
result << uri_for(account)
|
||||||
|
result << account.followers_url if account.group?
|
||||||
|
end
|
||||||
|
to.concat(FollowRequest.where(target_account_id: status.account_id, account_id: account_ids).each_with_object([]) do |request, result|
|
||||||
|
result << uri_for(request.account)
|
||||||
|
result << request.account.followers_url if request.account.group?
|
||||||
|
end)
|
||||||
else
|
else
|
||||||
status.active_mentions.map { |mention| uri_for(mention.account) }
|
status.active_mentions.each_with_object([]) do |mention, result|
|
||||||
|
result << uri_for(mention.account)
|
||||||
|
result << mention.account.followers_url if mention.account.group?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -97,10 +106,19 @@ class ActivityPub::TagManager
|
||||||
if status.account.silenced?
|
if status.account.silenced?
|
||||||
# Only notify followers if the account is locally silenced
|
# Only notify followers if the account is locally silenced
|
||||||
account_ids = status.active_mentions.pluck(:account_id)
|
account_ids = status.active_mentions.pluck(:account_id)
|
||||||
cc.concat(status.account.followers.where(id: account_ids).map { |account| uri_for(account) })
|
cc.concat(status.account.followers.where(id: account_ids).each_with_object([]) do |account, result|
|
||||||
cc.concat(FollowRequest.where(target_account_id: status.account_id, account_id: account_ids).map { |request| uri_for(request.account) })
|
result << uri_for(account)
|
||||||
|
result << account.followers_url if account.group?
|
||||||
|
end)
|
||||||
|
cc.concat(FollowRequest.where(target_account_id: status.account_id, account_id: account_ids).each_with_object([]) do |request, result|
|
||||||
|
result << uri_for(request.account)
|
||||||
|
result << request.account.followers_url if request.account.group?
|
||||||
|
end)
|
||||||
else
|
else
|
||||||
cc.concat(status.active_mentions.map { |mention| uri_for(mention.account) })
|
cc.concat(status.active_mentions.each_with_object([]) do |mention, result|
|
||||||
|
result << uri_for(mention.account)
|
||||||
|
result << mention.account.followers_url if mention.account.group?
|
||||||
|
end)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -44,7 +44,7 @@ class LanguageDetector
|
||||||
words = text.scan(RELIABLE_CHARACTERS_RE)
|
words = text.scan(RELIABLE_CHARACTERS_RE)
|
||||||
|
|
||||||
if words.present?
|
if words.present?
|
||||||
words.reduce(0) { |acc, elem| acc + elem.size }.to_f / text.size.to_f > 0.3
|
words.reduce(0) { |acc, elem| acc + elem.size }.to_f / text.size > 0.3
|
||||||
else
|
else
|
||||||
false
|
false
|
||||||
end
|
end
|
||||||
|
|
|
@ -97,6 +97,7 @@ class Account < ApplicationRecord
|
||||||
scope :without_silenced, -> { where(silenced_at: nil) }
|
scope :without_silenced, -> { where(silenced_at: nil) }
|
||||||
scope :recent, -> { reorder(id: :desc) }
|
scope :recent, -> { reorder(id: :desc) }
|
||||||
scope :bots, -> { where(actor_type: %w(Application Service)) }
|
scope :bots, -> { where(actor_type: %w(Application Service)) }
|
||||||
|
scope :groups, -> { where(actor_type: 'Group') }
|
||||||
scope :alphabetic, -> { order(domain: :asc, username: :asc) }
|
scope :alphabetic, -> { order(domain: :asc, username: :asc) }
|
||||||
scope :by_domain_accounts, -> { group(:domain).select(:domain, 'COUNT(*) AS accounts_count').order('accounts_count desc') }
|
scope :by_domain_accounts, -> { group(:domain).select(:domain, 'COUNT(*) AS accounts_count').order('accounts_count desc') }
|
||||||
scope :matches_username, ->(value) { where(arel_table[:username].matches("#{value}%")) }
|
scope :matches_username, ->(value) { where(arel_table[:username].matches("#{value}%")) }
|
||||||
|
@ -157,6 +158,12 @@ class Account < ApplicationRecord
|
||||||
self.actor_type = ActiveModel::Type::Boolean.new.cast(val) ? 'Service' : 'Person'
|
self.actor_type = ActiveModel::Type::Boolean.new.cast(val) ? 'Service' : 'Person'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def group?
|
||||||
|
actor_type == 'Group'
|
||||||
|
end
|
||||||
|
|
||||||
|
alias group group?
|
||||||
|
|
||||||
def acct
|
def acct
|
||||||
local? ? username : "#{username}@#{domain}"
|
local? ? username : "#{username}@#{domain}"
|
||||||
end
|
end
|
||||||
|
|
|
@ -6,7 +6,7 @@ module LdapAuthenticable
|
||||||
class_methods do
|
class_methods do
|
||||||
def authenticate_with_ldap(params = {})
|
def authenticate_with_ldap(params = {})
|
||||||
ldap = Net::LDAP.new(ldap_options)
|
ldap = Net::LDAP.new(ldap_options)
|
||||||
filter = format(Devise.ldap_search_filter, uid: Devise.ldap_uid, email: params[:email])
|
filter = format(Devise.ldap_search_filter, uid: Devise.ldap_uid, mail: Devise.ldap_mail, email: params[:email])
|
||||||
|
|
||||||
if (user_info = ldap.bind_as(base: Devise.ldap_base, filter: filter, password: params[:password]))
|
if (user_info = ldap.bind_as(base: Devise.ldap_base, filter: filter, password: params[:password]))
|
||||||
ldap_get_user(user_info.first)
|
ldap_get_user(user_info.first)
|
||||||
|
@ -25,7 +25,7 @@ module LdapAuthenticable
|
||||||
resource = joins(:account).find_by(accounts: { username: safe_username })
|
resource = joins(:account).find_by(accounts: { username: safe_username })
|
||||||
|
|
||||||
if resource.blank?
|
if resource.blank?
|
||||||
resource = new(email: attributes[:mail].first, agreement: true, account_attributes: { username: safe_username }, admin: false, external: true, confirmed_at: Time.now.utc)
|
resource = new(email: attributes[Devise.ldap_mail.to_sym].first, agreement: true, account_attributes: { username: safe_username }, admin: false, external: true, confirmed_at: Time.now.utc)
|
||||||
resource.save!
|
resource.save!
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -287,7 +287,7 @@ class MediaAttachment < ApplicationRecord
|
||||||
width: width,
|
width: width,
|
||||||
height: height,
|
height: height,
|
||||||
size: "#{width}x#{height}",
|
size: "#{width}x#{height}",
|
||||||
aspect: width.to_f / height.to_f,
|
aspect: width.to_f / height,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -42,7 +42,7 @@ class Notification < ApplicationRecord
|
||||||
validates :activity_type, inclusion: { in: TYPE_CLASS_MAP.values }
|
validates :activity_type, inclusion: { in: TYPE_CLASS_MAP.values }
|
||||||
|
|
||||||
scope :browserable, ->(exclude_types = [], account_id = nil) {
|
scope :browserable, ->(exclude_types = [], account_id = nil) {
|
||||||
types = TYPE_CLASS_MAP.values - activity_types_from_types(exclude_types + [:follow_request])
|
types = TYPE_CLASS_MAP.values - activity_types_from_types(exclude_types)
|
||||||
if account_id.nil?
|
if account_id.nil?
|
||||||
where(activity_type: types)
|
where(activity_type: types)
|
||||||
else
|
else
|
||||||
|
@ -50,7 +50,7 @@ class Notification < ApplicationRecord
|
||||||
end
|
end
|
||||||
}
|
}
|
||||||
|
|
||||||
cache_associated :from_account, status: STATUS_INCLUDES, mention: [status: STATUS_INCLUDES], favourite: [:account, status: STATUS_INCLUDES], follow: :account, poll: [status: STATUS_INCLUDES]
|
cache_associated :from_account, status: STATUS_INCLUDES, mention: [status: STATUS_INCLUDES], favourite: [:account, status: STATUS_INCLUDES], follow: :account, follow_request: :account, poll: [status: STATUS_INCLUDES]
|
||||||
|
|
||||||
def type
|
def type
|
||||||
@type ||= TYPE_CLASS_MAP.invert[activity_type].to_sym
|
@type ||= TYPE_CLASS_MAP.invert[activity_type].to_sym
|
||||||
|
@ -69,10 +69,6 @@ class Notification < ApplicationRecord
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def browserable?
|
|
||||||
type != :follow_request
|
|
||||||
end
|
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
def cache_ids
|
def cache_ids
|
||||||
select(:id, :updated_at, :activity_type, :activity_id)
|
select(:id, :updated_at, :activity_type, :activity_id)
|
||||||
|
|
|
@ -36,7 +36,7 @@ class Poll < ApplicationRecord
|
||||||
scope :attached, -> { where.not(status_id: nil) }
|
scope :attached, -> { where.not(status_id: nil) }
|
||||||
scope :unattached, -> { where(status_id: nil) }
|
scope :unattached, -> { where(status_id: nil) }
|
||||||
|
|
||||||
before_validation :prepare_options
|
before_validation :prepare_options, if: :local?
|
||||||
before_validation :prepare_votes_count
|
before_validation :prepare_votes_count
|
||||||
|
|
||||||
after_initialize :prepare_cached_tallies
|
after_initialize :prepare_cached_tallies
|
||||||
|
|
|
@ -49,6 +49,8 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer
|
||||||
'Application'
|
'Application'
|
||||||
elsif object.bot?
|
elsif object.bot?
|
||||||
'Service'
|
'Service'
|
||||||
|
elsif object.group?
|
||||||
|
'Group'
|
||||||
else
|
else
|
||||||
'Person'
|
'Person'
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
class REST::AccountSerializer < ActiveModel::Serializer
|
class REST::AccountSerializer < ActiveModel::Serializer
|
||||||
include RoutingHelper
|
include RoutingHelper
|
||||||
|
|
||||||
attributes :id, :username, :acct, :display_name, :locked, :bot, :created_at,
|
attributes :id, :username, :acct, :display_name, :locked, :bot, :discoverable, :group, :created_at,
|
||||||
:note, :url, :avatar, :avatar_static, :header, :header_static,
|
:note, :url, :avatar, :avatar_static, :header, :header_static,
|
||||||
:followers_count, :following_count, :statuses_count, :last_status_at
|
:followers_count, :following_count, :statuses_count, :last_status_at
|
||||||
|
|
||||||
|
|
|
@ -4,8 +4,8 @@ class AccountSearchService < BaseService
|
||||||
attr_reader :query, :limit, :offset, :options, :account
|
attr_reader :query, :limit, :offset, :options, :account
|
||||||
|
|
||||||
def call(query, account = nil, options = {})
|
def call(query, account = nil, options = {})
|
||||||
@acct_hint = query.start_with?('@')
|
@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
|
||||||
@options = options
|
@options = options
|
||||||
|
|
|
@ -9,7 +9,7 @@ class NotifyService < BaseService
|
||||||
return if recipient.user.nil? || blocked?
|
return if recipient.user.nil? || blocked?
|
||||||
|
|
||||||
create_notification!
|
create_notification!
|
||||||
push_notification! if @notification.browserable?
|
push_notification!
|
||||||
push_to_conversation! if direct_message?
|
push_to_conversation! if direct_message?
|
||||||
send_email! if email_enabled?
|
send_email! if email_enabled?
|
||||||
rescue ActiveRecord::RecordInvalid
|
rescue ActiveRecord::RecordInvalid
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
class SearchService < BaseService
|
class SearchService < BaseService
|
||||||
def call(query, account, limit, options = {})
|
def call(query, account, limit, options = {})
|
||||||
@query = query.strip
|
@query = query&.strip
|
||||||
@account = account
|
@account = account
|
||||||
@options = options
|
@options = options
|
||||||
@limit = limit.to_i
|
@limit = limit.to_i
|
||||||
|
@ -10,6 +10,8 @@ class SearchService < BaseService
|
||||||
@resolve = options[:resolve] || false
|
@resolve = options[:resolve] || false
|
||||||
|
|
||||||
default_results.tap do |results|
|
default_results.tap do |results|
|
||||||
|
next if @query.blank?
|
||||||
|
|
||||||
if url_query?
|
if url_query?
|
||||||
results.merge!(url_resource_results) unless url_resource.nil? || (@options[:type].present? && url_resource_symbol != @options[:type].to_sym)
|
results.merge!(url_resource_results) unless url_resource.nil? || (@options[:type].present? && url_resource_symbol != @options[:type].to_sym)
|
||||||
elsif @query.present?
|
elsif @query.present?
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
.dashboard__counters__num= number_with_delimiter @blocks_count
|
.dashboard__counters__num= number_with_delimiter @blocks_count
|
||||||
.dashboard__counters__label= t 'admin.instances.total_blocked_by_us'
|
.dashboard__counters__label= t 'admin.instances.total_blocked_by_us'
|
||||||
%div
|
%div
|
||||||
%div
|
= link_to admin_reports_path(by_target_domain: @instance.domain) do
|
||||||
.dashboard__counters__num= number_with_delimiter @reports_count
|
.dashboard__counters__num= number_with_delimiter @reports_count
|
||||||
.dashboard__counters__label= t 'admin.instances.total_reported'
|
.dashboard__counters__label= t 'admin.instances.total_reported'
|
||||||
%div
|
%div
|
||||||
|
|
|
@ -4,9 +4,9 @@
|
||||||
= simple_form_for current_user, url: settings_preferences_notifications_path, html: { method: :put } do |f|
|
= simple_form_for current_user, url: settings_preferences_notifications_path, html: { method: :put } do |f|
|
||||||
= render 'shared/error_messages', object: current_user
|
= render 'shared/error_messages', object: current_user
|
||||||
|
|
||||||
%h4= t('notifications.email_events')
|
%h4= t 'notifications.email_events'
|
||||||
|
|
||||||
%p.hint = t('notifications.email_events_hint')
|
%p.hint= t 'notifications.email_events_hint'
|
||||||
|
|
||||||
.fields-group
|
.fields-group
|
||||||
= f.simple_fields_for :notification_emails, hash_to_object(current_user.settings.notification_emails) do |ff|
|
= f.simple_fields_for :notification_emails, hash_to_object(current_user.settings.notification_emails) do |ff|
|
||||||
|
@ -25,7 +25,7 @@
|
||||||
= f.simple_fields_for :notification_emails, hash_to_object(current_user.settings.notification_emails) do |ff|
|
= f.simple_fields_for :notification_emails, hash_to_object(current_user.settings.notification_emails) do |ff|
|
||||||
= ff.input :digest, as: :boolean, wrapper: :with_label
|
= ff.input :digest, as: :boolean, wrapper: :with_label
|
||||||
|
|
||||||
%h4 = t('notifications.other_settings')
|
%h4= t 'notifications.other_settings'
|
||||||
|
|
||||||
.fields-group
|
.fields-group
|
||||||
= f.simple_fields_for :interactions, hash_to_object(current_user.settings.interactions) do |ff|
|
= f.simple_fields_for :interactions, hash_to_object(current_user.settings.interactions) do |ff|
|
||||||
|
|
|
@ -1,36 +1,39 @@
|
||||||
module.exports = (api) => {
|
module.exports = (api) => {
|
||||||
const env = api.env();
|
const env = api.env();
|
||||||
|
|
||||||
|
const reactOptions = {
|
||||||
|
development: false,
|
||||||
|
};
|
||||||
|
|
||||||
const envOptions = {
|
const envOptions = {
|
||||||
debug: false,
|
|
||||||
loose: true,
|
loose: true,
|
||||||
modules: false,
|
modules: false,
|
||||||
|
debug: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
presets: [
|
presets: [
|
||||||
'@babel/react',
|
['@babel/react', reactOptions],
|
||||||
['@babel/env', envOptions],
|
['@babel/env', envOptions],
|
||||||
],
|
],
|
||||||
plugins: [
|
plugins: [
|
||||||
'@babel/syntax-dynamic-import',
|
|
||||||
['@babel/proposal-object-rest-spread', { useBuiltIns: true }],
|
|
||||||
['@babel/proposal-decorators', { legacy: true }],
|
['@babel/proposal-decorators', { legacy: true }],
|
||||||
'@babel/proposal-class-properties',
|
'@babel/proposal-class-properties',
|
||||||
['react-intl', { messagesDir: './build/messages' }],
|
['react-intl', { messagesDir: './build/messages' }],
|
||||||
'preval',
|
'preval',
|
||||||
],
|
],
|
||||||
overrides: [{
|
overrides: [
|
||||||
test: /tesseract\.js/,
|
{
|
||||||
presets: [
|
test: /tesseract\.js/,
|
||||||
['@babel/env', { ...envOptions, modules: 'commonjs' }],
|
presets: [
|
||||||
],
|
['@babel/env', { ...envOptions, modules: 'commonjs' }],
|
||||||
}],
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
switch (env) {
|
switch (env) {
|
||||||
case 'production':
|
case 'production':
|
||||||
envOptions.debug = false;
|
|
||||||
config.plugins.push(...[
|
config.plugins.push(...[
|
||||||
'lodash',
|
'lodash',
|
||||||
[
|
[
|
||||||
|
@ -55,11 +58,8 @@ module.exports = (api) => {
|
||||||
]);
|
]);
|
||||||
break;
|
break;
|
||||||
case 'development':
|
case 'development':
|
||||||
|
reactOptions.development = true;
|
||||||
envOptions.debug = true;
|
envOptions.debug = true;
|
||||||
config.plugins.push(...[
|
|
||||||
'@babel/transform-react-jsx-source',
|
|
||||||
'@babel/transform-react-jsx-self',
|
|
||||||
]);
|
|
||||||
break;
|
break;
|
||||||
case 'test':
|
case 'test':
|
||||||
envOptions.modules = 'commonjs';
|
envOptions.modules = 'commonjs';
|
||||||
|
|
|
@ -53,6 +53,8 @@ module Devise
|
||||||
@@ldap_base = nil
|
@@ldap_base = nil
|
||||||
mattr_accessor :ldap_uid
|
mattr_accessor :ldap_uid
|
||||||
@@ldap_uid = nil
|
@@ldap_uid = nil
|
||||||
|
mattr_accessor :ldap_mail
|
||||||
|
@@ldap_mail = nil
|
||||||
mattr_accessor :ldap_bind_dn
|
mattr_accessor :ldap_bind_dn
|
||||||
@@ldap_bind_dn = nil
|
@@ldap_bind_dn = nil
|
||||||
mattr_accessor :ldap_password
|
mattr_accessor :ldap_password
|
||||||
|
@ -369,8 +371,9 @@ Devise.setup do |config|
|
||||||
config.ldap_bind_dn = ENV.fetch('LDAP_BIND_DN')
|
config.ldap_bind_dn = ENV.fetch('LDAP_BIND_DN')
|
||||||
config.ldap_password = ENV.fetch('LDAP_PASSWORD')
|
config.ldap_password = ENV.fetch('LDAP_PASSWORD')
|
||||||
config.ldap_uid = ENV.fetch('LDAP_UID', 'cn')
|
config.ldap_uid = ENV.fetch('LDAP_UID', 'cn')
|
||||||
|
config.ldap_mail = ENV.fetch('LDAP_MAIL', 'mail')
|
||||||
config.ldap_tls_no_verify = ENV['LDAP_TLS_NO_VERIFY'] == 'true'
|
config.ldap_tls_no_verify = ENV['LDAP_TLS_NO_VERIFY'] == 'true'
|
||||||
config.ldap_search_filter = ENV.fetch('LDAP_SEARCH_FILTER', '%{uid}=%{email}')
|
config.ldap_search_filter = ENV.fetch('LDAP_SEARCH_FILTER', '(|(%{uid}=%{email})(%{mail}=%{email}))')
|
||||||
config.ldap_uid_conversion_enabled = ENV['LDAP_UID_CONVERSION_ENABLED'] == 'true'
|
config.ldap_uid_conversion_enabled = ENV['LDAP_UID_CONVERSION_ENABLED'] == 'true'
|
||||||
config.ldap_uid_conversion_search = ENV.fetch('LDAP_UID_CONVERSION_SEARCH', '.,- ')
|
config.ldap_uid_conversion_search = ENV.fetch('LDAP_UID_CONVERSION_SEARCH', '.,- ')
|
||||||
config.ldap_uid_conversion_replace = ENV.fetch('LDAP_UID_CONVERSION_REPLACE', '_')
|
config.ldap_uid_conversion_replace = ENV.fetch('LDAP_UID_CONVERSION_REPLACE', '_')
|
||||||
|
|
|
@ -42,7 +42,7 @@ if ENV['S3_ENABLED'] == 'true'
|
||||||
|
|
||||||
s3_options: {
|
s3_options: {
|
||||||
signature_version: ENV.fetch('S3_SIGNATURE_VERSION') { 'v4' },
|
signature_version: ENV.fetch('S3_SIGNATURE_VERSION') { 'v4' },
|
||||||
http_open_timeout: 5,
|
http_open_timeout: ENV.fetch('S3_OPEN_TIMEOUT'){ '5' }.to_i,
|
||||||
http_read_timeout: 5,
|
http_read_timeout: 5,
|
||||||
http_idle_timeout: 5,
|
http_idle_timeout: 5,
|
||||||
retry_limit: 0,
|
retry_limit: 0,
|
||||||
|
|
|
@ -78,6 +78,7 @@ en:
|
||||||
roles:
|
roles:
|
||||||
admin: Admin
|
admin: Admin
|
||||||
bot: Bot
|
bot: Bot
|
||||||
|
group: Group
|
||||||
moderator: Mod
|
moderator: Mod
|
||||||
unavailable: Profile unavailable
|
unavailable: Profile unavailable
|
||||||
unfollow: Unfollow
|
unfollow: Unfollow
|
||||||
|
|
|
@ -1,6 +1,66 @@
|
||||||
class MigrateAccountConversations < ActiveRecord::Migration[5.2]
|
class MigrateAccountConversations < ActiveRecord::Migration[5.2]
|
||||||
disable_ddl_transaction!
|
disable_ddl_transaction!
|
||||||
|
|
||||||
|
class Mention < ApplicationRecord
|
||||||
|
belongs_to :account, inverse_of: :mentions
|
||||||
|
belongs_to :status, -> { unscope(where: :deleted_at) }
|
||||||
|
|
||||||
|
delegate(
|
||||||
|
:username,
|
||||||
|
:acct,
|
||||||
|
to: :account,
|
||||||
|
prefix: true
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
class Notification < ApplicationRecord
|
||||||
|
belongs_to :account, optional: true
|
||||||
|
belongs_to :activity, polymorphic: true, optional: true
|
||||||
|
|
||||||
|
belongs_to :status, foreign_type: 'Status', foreign_key: 'activity_id', optional: true
|
||||||
|
belongs_to :mention, foreign_type: 'Mention', foreign_key: 'activity_id', optional: true
|
||||||
|
|
||||||
|
def target_status
|
||||||
|
mention&.status
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class AccountConversation < ApplicationRecord
|
||||||
|
belongs_to :account
|
||||||
|
belongs_to :conversation
|
||||||
|
belongs_to :last_status, -> { unscope(where: :deleted_at) }, class_name: 'Status'
|
||||||
|
|
||||||
|
before_validation :set_last_status
|
||||||
|
|
||||||
|
class << self
|
||||||
|
def add_status(recipient, status)
|
||||||
|
conversation = find_or_initialize_by(account: recipient, conversation_id: status.conversation_id, participant_account_ids: participants_from_status(recipient, status))
|
||||||
|
|
||||||
|
return conversation if conversation.status_ids.include?(status.id)
|
||||||
|
|
||||||
|
conversation.status_ids << status.id
|
||||||
|
conversation.unread = status.account_id != recipient.id
|
||||||
|
conversation.save
|
||||||
|
conversation
|
||||||
|
rescue ActiveRecord::StaleObjectError
|
||||||
|
retry
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def participants_from_status(recipient, status)
|
||||||
|
((status.active_mentions.pluck(:account_id) + [status.account_id]).uniq - [recipient.id]).sort
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_last_status
|
||||||
|
self.status_ids = status_ids.sort
|
||||||
|
self.last_status_id = status_ids.last
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def up
|
def up
|
||||||
say ''
|
say ''
|
||||||
say 'WARNING: This migration may take a *long* time for large instances'
|
say 'WARNING: This migration may take a *long* time for large instances'
|
||||||
|
|
|
@ -9,8 +9,8 @@ module Paperclip
|
||||||
min_side = [@current_geometry.width, @current_geometry.height].min.to_i
|
min_side = [@current_geometry.width, @current_geometry.height].min.to_i
|
||||||
options[:geometry] = "#{min_side}x#{min_side}#" if @target_geometry.square? && min_side < @target_geometry.width
|
options[:geometry] = "#{min_side}x#{min_side}#" if @target_geometry.square? && min_side < @target_geometry.width
|
||||||
elsif options[:pixels]
|
elsif options[:pixels]
|
||||||
width = Math.sqrt(options[:pixels] * (@current_geometry.width.to_f / @current_geometry.height.to_f)).round.to_i
|
width = Math.sqrt(options[:pixels] * (@current_geometry.width.to_f / @current_geometry.height)).round.to_i
|
||||||
height = Math.sqrt(options[:pixels] * (@current_geometry.height.to_f / @current_geometry.width.to_f)).round.to_i
|
height = Math.sqrt(options[:pixels] * (@current_geometry.height.to_f / @current_geometry.width)).round.to_i
|
||||||
options[:geometry] = "#{width}x#{height}>"
|
options[:geometry] = "#{width}x#{height}>"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
26
package.json
26
package.json
|
@ -60,23 +60,20 @@
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/core": "^7.7.2",
|
"@babel/core": "^7.7.4",
|
||||||
"@babel/plugin-proposal-class-properties": "^7.7.0",
|
"@babel/plugin-proposal-class-properties": "^7.7.4",
|
||||||
"@babel/plugin-proposal-decorators": "^7.7.0",
|
"@babel/plugin-proposal-decorators": "^7.7.4",
|
||||||
"@babel/plugin-proposal-object-rest-spread": "^7.6.2",
|
|
||||||
"@babel/plugin-syntax-dynamic-import": "^7.7.4",
|
|
||||||
"@babel/plugin-transform-react-inline-elements": "^7.7.4",
|
"@babel/plugin-transform-react-inline-elements": "^7.7.4",
|
||||||
"@babel/plugin-transform-react-jsx-self": "^7.7.4",
|
|
||||||
"@babel/plugin-transform-react-jsx-source": "^7.5.0",
|
|
||||||
"@babel/plugin-transform-runtime": "^7.7.4",
|
"@babel/plugin-transform-runtime": "^7.7.4",
|
||||||
"@babel/preset-env": "^7.7.4",
|
"@babel/preset-env": "^7.7.4",
|
||||||
"@babel/preset-react": "^7.7.0",
|
"@babel/preset-react": "^7.7.4",
|
||||||
"@babel/runtime": "^7.7.4",
|
"@babel/runtime": "^7.7.4",
|
||||||
|
"@gamestdio/websocket": "^0.3.2",
|
||||||
"@clusterws/cws": "^0.16.0",
|
"@clusterws/cws": "^0.16.0",
|
||||||
"array-includes": "^3.0.3",
|
"array-includes": "^3.0.3",
|
||||||
"atrament": "^0.2.3",
|
"atrament": "^0.2.3",
|
||||||
"arrow-key-navigation": "^1.0.2",
|
"arrow-key-navigation": "^1.1.0",
|
||||||
"autoprefixer": "^9.6.1",
|
"autoprefixer": "^9.7.3",
|
||||||
"axios": "^0.19.0",
|
"axios": "^0.19.0",
|
||||||
"babel-loader": "^8.0.6",
|
"babel-loader": "^8.0.6",
|
||||||
"babel-plugin-lodash": "^3.3.4",
|
"babel-plugin-lodash": "^3.3.4",
|
||||||
|
@ -84,7 +81,7 @@
|
||||||
"babel-plugin-react-intl": "^3.4.1",
|
"babel-plugin-react-intl": "^3.4.1",
|
||||||
"babel-plugin-transform-react-remove-prop-types": "^0.4.24",
|
"babel-plugin-transform-react-remove-prop-types": "^0.4.24",
|
||||||
"babel-runtime": "^6.26.0",
|
"babel-runtime": "^6.26.0",
|
||||||
"blurhash": "^1.0.0",
|
"blurhash": "^1.1.3",
|
||||||
"classnames": "^2.2.5",
|
"classnames": "^2.2.5",
|
||||||
"compression-webpack-plugin": "^3.0.0",
|
"compression-webpack-plugin": "^3.0.0",
|
||||||
"copy-webpack-plugin": "^5.0.5",
|
"copy-webpack-plugin": "^5.0.5",
|
||||||
|
@ -128,7 +125,7 @@
|
||||||
"postcss-object-fit-images": "^1.1.2",
|
"postcss-object-fit-images": "^1.1.2",
|
||||||
"prop-types": "^15.5.10",
|
"prop-types": "^15.5.10",
|
||||||
"punycode": "^2.1.0",
|
"punycode": "^2.1.0",
|
||||||
"rails-ujs": "^5.2.3",
|
"rails-ujs": "^5.2.4",
|
||||||
"react": "^16.10.2",
|
"react": "^16.10.2",
|
||||||
"react-dom": "^16.12.0",
|
"react-dom": "^16.12.0",
|
||||||
"react-hotkeys": "^1.1.4",
|
"react-hotkeys": "^1.1.4",
|
||||||
|
@ -171,7 +168,6 @@
|
||||||
"webpack-bundle-analyzer": "^3.6.0",
|
"webpack-bundle-analyzer": "^3.6.0",
|
||||||
"webpack-cli": "^3.3.10",
|
"webpack-cli": "^3.3.10",
|
||||||
"webpack-merge": "^4.2.1",
|
"webpack-merge": "^4.2.1",
|
||||||
"websocket.js": "^0.1.12",
|
|
||||||
"wicg-inert": "^3.0.0"
|
"wicg-inert": "^3.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -179,11 +175,11 @@
|
||||||
"babel-jest": "^24.9.0",
|
"babel-jest": "^24.9.0",
|
||||||
"enzyme": "^3.10.0",
|
"enzyme": "^3.10.0",
|
||||||
"enzyme-adapter-react-16": "^1.15.1",
|
"enzyme-adapter-react-16": "^1.15.1",
|
||||||
"eslint": "^6.5.1",
|
"eslint": "^6.7.2",
|
||||||
"eslint-plugin-import": "~2.18.2",
|
"eslint-plugin-import": "~2.18.2",
|
||||||
"eslint-plugin-jsx-a11y": "~6.2.3",
|
"eslint-plugin-jsx-a11y": "~6.2.3",
|
||||||
"eslint-plugin-promise": "~4.2.1",
|
"eslint-plugin-promise": "~4.2.1",
|
||||||
"eslint-plugin-react": "~7.16.0",
|
"eslint-plugin-react": "~7.17.0",
|
||||||
"jest": "^24.9.0",
|
"jest": "^24.9.0",
|
||||||
"raf": "^3.4.1",
|
"raf": "^3.4.1",
|
||||||
"react-intl-translations-manager": "^5.0.3",
|
"react-intl-translations-manager": "^5.0.3",
|
||||||
|
|
|
@ -34,32 +34,6 @@ RSpec.describe Notification, type: :model do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#browserable?' do
|
|
||||||
let(:notification) { Fabricate(:notification) }
|
|
||||||
|
|
||||||
subject { notification.browserable? }
|
|
||||||
|
|
||||||
context 'type is :follow_request' do
|
|
||||||
before do
|
|
||||||
allow(notification).to receive(:type).and_return(:follow_request)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns false' do
|
|
||||||
is_expected.to be false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'type is not :follow_request' do
|
|
||||||
before do
|
|
||||||
allow(notification).to receive(:type).and_return(:else)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns true' do
|
|
||||||
is_expected.to be true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe '#type' do
|
describe '#type' do
|
||||||
it 'returns :reblog for a Status' do
|
it 'returns :reblog for a Status' do
|
||||||
notification = Notification.new(activity: Status.new)
|
notification = Notification.new(activity: Status.new)
|
||||||
|
|
|
@ -12,7 +12,7 @@ end
|
||||||
gc_counter = -1
|
gc_counter = -1
|
||||||
|
|
||||||
RSpec.configure do |config|
|
RSpec.configure do |config|
|
||||||
config.example_status_persistence_file_path = ".cache/rspec"
|
config.example_status_persistence_file_path = "tmp/rspec/examples.txt"
|
||||||
config.expect_with :rspec do |expectations|
|
config.expect_with :rspec do |expectations|
|
||||||
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
|
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in New Issue