forked from treehouse/mastodon
Merge branch 'master' into glitch-soc/merge-upstream
commit
5088eb8388
|
@ -3,7 +3,7 @@ version: 2
|
||||||
aliases:
|
aliases:
|
||||||
- &defaults
|
- &defaults
|
||||||
docker:
|
docker:
|
||||||
- image: circleci/ruby:2.6.0-stretch-node
|
- image: circleci/ruby:2.6-stretch-node
|
||||||
environment: &ruby_environment
|
environment: &ruby_environment
|
||||||
BUNDLE_APP_CONFIG: ./.bundle/
|
BUNDLE_APP_CONFIG: ./.bundle/
|
||||||
DB_HOST: localhost
|
DB_HOST: localhost
|
||||||
|
@ -105,14 +105,14 @@ jobs:
|
||||||
install-ruby2.5:
|
install-ruby2.5:
|
||||||
<<: *defaults
|
<<: *defaults
|
||||||
docker:
|
docker:
|
||||||
- image: circleci/ruby:2.5.3-stretch-node
|
- image: circleci/ruby:2.5-stretch-node
|
||||||
environment: *ruby_environment
|
environment: *ruby_environment
|
||||||
<<: *install_ruby_dependencies
|
<<: *install_ruby_dependencies
|
||||||
|
|
||||||
install-ruby2.4:
|
install-ruby2.4:
|
||||||
<<: *defaults
|
<<: *defaults
|
||||||
docker:
|
docker:
|
||||||
- image: circleci/ruby:2.4.5-stretch-node
|
- image: circleci/ruby:2.4-stretch-node
|
||||||
environment: *ruby_environment
|
environment: *ruby_environment
|
||||||
<<: *install_ruby_dependencies
|
<<: *install_ruby_dependencies
|
||||||
|
|
||||||
|
@ -134,40 +134,40 @@ jobs:
|
||||||
test-ruby2.6:
|
test-ruby2.6:
|
||||||
<<: *defaults
|
<<: *defaults
|
||||||
docker:
|
docker:
|
||||||
- image: circleci/ruby:2.6.0-stretch-node
|
- image: circleci/ruby:2.6-stretch-node
|
||||||
environment: *ruby_environment
|
environment: *ruby_environment
|
||||||
- image: circleci/postgres:10.6-alpine
|
- image: circleci/postgres:10.6-alpine
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_USER: root
|
POSTGRES_USER: root
|
||||||
- image: circleci/redis:5.0.3-alpine3.8
|
- image: circleci/redis:5-alpine
|
||||||
<<: *test_steps
|
<<: *test_steps
|
||||||
|
|
||||||
test-ruby2.5:
|
test-ruby2.5:
|
||||||
<<: *defaults
|
<<: *defaults
|
||||||
docker:
|
docker:
|
||||||
- image: circleci/ruby:2.5.3-stretch-node
|
- image: circleci/ruby:2.5-stretch-node
|
||||||
environment: *ruby_environment
|
environment: *ruby_environment
|
||||||
- image: circleci/postgres:10.6-alpine
|
- image: circleci/postgres:10.6-alpine
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_USER: root
|
POSTGRES_USER: root
|
||||||
- image: circleci/redis:4.0.12-alpine
|
- image: circleci/redis:5-alpine
|
||||||
<<: *test_steps
|
<<: *test_steps
|
||||||
|
|
||||||
test-ruby2.4:
|
test-ruby2.4:
|
||||||
<<: *defaults
|
<<: *defaults
|
||||||
docker:
|
docker:
|
||||||
- image: circleci/ruby:2.4.5-stretch-node
|
- image: circleci/ruby:2.4-stretch-node
|
||||||
environment: *ruby_environment
|
environment: *ruby_environment
|
||||||
- image: circleci/postgres:10.6-alpine
|
- image: circleci/postgres:10.6-alpine
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_USER: root
|
POSTGRES_USER: root
|
||||||
- image: circleci/redis:4.0.12-alpine
|
- image: circleci/redis:5-alpine
|
||||||
<<: *test_steps
|
<<: *test_steps
|
||||||
|
|
||||||
test-webui:
|
test-webui:
|
||||||
<<: *defaults
|
<<: *defaults
|
||||||
docker:
|
docker:
|
||||||
- image: circleci/node:8.15.0-stretch
|
- image: circleci/node:12.9-stretch
|
||||||
steps:
|
steps:
|
||||||
- *attach_workspace
|
- *attach_workspace
|
||||||
- run: ./bin/retry yarn test:jest
|
- run: ./bin/retry yarn test:jest
|
||||||
|
|
|
@ -69,6 +69,7 @@ SMTP_PORT=587
|
||||||
SMTP_LOGIN=
|
SMTP_LOGIN=
|
||||||
SMTP_PASSWORD=
|
SMTP_PASSWORD=
|
||||||
SMTP_FROM_ADDRESS=notifications@example.com
|
SMTP_FROM_ADDRESS=notifications@example.com
|
||||||
|
#SMTP_REPLY_TO=
|
||||||
#SMTP_DOMAIN= # defaults to LOCAL_DOMAIN
|
#SMTP_DOMAIN= # defaults to LOCAL_DOMAIN
|
||||||
#SMTP_DELIVERY_METHOD=smtp # delivery method can also be sendmail
|
#SMTP_DELIVERY_METHOD=smtp # delivery method can also be sendmail
|
||||||
#SMTP_AUTH_METHOD=plain
|
#SMTP_AUTH_METHOD=plain
|
||||||
|
|
|
@ -4,7 +4,7 @@ FROM ubuntu:18.04 as build-dep
|
||||||
SHELL ["bash", "-c"]
|
SHELL ["bash", "-c"]
|
||||||
|
|
||||||
# Install Node
|
# Install Node
|
||||||
ENV NODE_VER="8.15.0"
|
ENV NODE_VER="12.9.1"
|
||||||
RUN echo "Etc/UTC" > /etc/localtime && \
|
RUN echo "Etc/UTC" > /etc/localtime && \
|
||||||
apt update && \
|
apt update && \
|
||||||
apt -y install wget make gcc g++ python && \
|
apt -y install wget make gcc g++ python && \
|
||||||
|
@ -17,7 +17,7 @@ RUN echo "Etc/UTC" > /etc/localtime && \
|
||||||
make install
|
make install
|
||||||
|
|
||||||
# Install jemalloc
|
# Install jemalloc
|
||||||
ENV JE_VER="5.1.0"
|
ENV JE_VER="5.2.1"
|
||||||
RUN apt update && \
|
RUN apt update && \
|
||||||
apt -y install autoconf && \
|
apt -y install autoconf && \
|
||||||
cd ~ && \
|
cd ~ && \
|
||||||
|
@ -30,7 +30,7 @@ RUN apt update && \
|
||||||
make install_bin install_include install_lib
|
make install_bin install_include install_lib
|
||||||
|
|
||||||
# Install ruby
|
# Install ruby
|
||||||
ENV RUBY_VER="2.6.1"
|
ENV RUBY_VER="2.6.4"
|
||||||
ENV CPPFLAGS="-I/opt/jemalloc/include"
|
ENV CPPFLAGS="-I/opt/jemalloc/include"
|
||||||
ENV LDFLAGS="-L/opt/jemalloc/lib/"
|
ENV LDFLAGS="-L/opt/jemalloc/lib/"
|
||||||
RUN apt update && \
|
RUN apt update && \
|
||||||
|
|
8
Gemfile
8
Gemfile
|
@ -15,7 +15,7 @@ gem 'makara', '~> 0.4'
|
||||||
gem 'pghero', '~> 2.3'
|
gem 'pghero', '~> 2.3'
|
||||||
gem 'dotenv-rails', '~> 2.7'
|
gem 'dotenv-rails', '~> 2.7'
|
||||||
|
|
||||||
gem 'aws-sdk-s3', '~> 1.46', require: false
|
gem 'aws-sdk-s3', '~> 1.48', 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'
|
||||||
|
@ -24,7 +24,7 @@ gem 'streamio-ffmpeg', '~> 3.0'
|
||||||
gem 'blurhash', '~> 0.1'
|
gem 'blurhash', '~> 0.1'
|
||||||
|
|
||||||
gem 'active_model_serializers', '~> 0.10'
|
gem 'active_model_serializers', '~> 0.10'
|
||||||
gem 'addressable', '~> 2.6'
|
gem 'addressable', '~> 2.7'
|
||||||
gem 'bootsnap', '~> 1.4', require: false
|
gem 'bootsnap', '~> 1.4', require: false
|
||||||
gem 'browser'
|
gem 'browser'
|
||||||
gem 'charlock_holmes', '~> 0.7.6'
|
gem 'charlock_holmes', '~> 0.7.6'
|
||||||
|
@ -116,12 +116,12 @@ end
|
||||||
group :test do
|
group :test do
|
||||||
gem 'capybara', '~> 3.28'
|
gem 'capybara', '~> 3.28'
|
||||||
gem 'climate_control', '~> 0.2'
|
gem 'climate_control', '~> 0.2'
|
||||||
gem 'faker', '~> 2.1'
|
gem 'faker', '~> 2.2'
|
||||||
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'
|
||||||
gem 'simplecov', '~> 0.17', require: false
|
gem 'simplecov', '~> 0.17', require: false
|
||||||
gem 'webmock', '~> 3.6'
|
gem 'webmock', '~> 3.7'
|
||||||
gem 'parallel_tests', '~> 2.29'
|
gem 'parallel_tests', '~> 2.29'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
44
Gemfile.lock
44
Gemfile.lock
|
@ -83,9 +83,9 @@ GEM
|
||||||
i18n (>= 0.7, < 2)
|
i18n (>= 0.7, < 2)
|
||||||
minitest (~> 5.1)
|
minitest (~> 5.1)
|
||||||
tzinfo (~> 1.1)
|
tzinfo (~> 1.1)
|
||||||
addressable (2.6.0)
|
addressable (2.7.0)
|
||||||
public_suffix (>= 2.0.2, < 4.0)
|
public_suffix (>= 2.0.2, < 5.0)
|
||||||
airbrussh (1.3.0)
|
airbrussh (1.3.3)
|
||||||
sshkit (>= 1.6.1, != 1.7.0)
|
sshkit (>= 1.6.1, != 1.7.0)
|
||||||
annotate (2.7.5)
|
annotate (2.7.5)
|
||||||
activerecord (>= 3.2, < 7.0)
|
activerecord (>= 3.2, < 7.0)
|
||||||
|
@ -97,8 +97,8 @@ 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.193.0)
|
aws-partitions (1.207.0)
|
||||||
aws-sdk-core (3.61.1)
|
aws-sdk-core (3.65.1)
|
||||||
aws-eventstream (~> 1.0, >= 1.0.2)
|
aws-eventstream (~> 1.0, >= 1.0.2)
|
||||||
aws-partitions (~> 1.0)
|
aws-partitions (~> 1.0)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.1)
|
||||||
|
@ -106,7 +106,7 @@ GEM
|
||||||
aws-sdk-kms (1.24.0)
|
aws-sdk-kms (1.24.0)
|
||||||
aws-sdk-core (~> 3, >= 3.61.1)
|
aws-sdk-core (~> 3, >= 3.61.1)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.1)
|
||||||
aws-sdk-s3 (1.46.0)
|
aws-sdk-s3 (1.48.0)
|
||||||
aws-sdk-core (~> 3, >= 3.61.1)
|
aws-sdk-core (~> 3, >= 3.61.1)
|
||||||
aws-sdk-kms (~> 1)
|
aws-sdk-kms (~> 1)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.1)
|
||||||
|
@ -122,7 +122,7 @@ GEM
|
||||||
debug_inspector (>= 0.0.1)
|
debug_inspector (>= 0.0.1)
|
||||||
blurhash (0.1.3)
|
blurhash (0.1.3)
|
||||||
ffi (~> 1.10.0)
|
ffi (~> 1.10.0)
|
||||||
bootsnap (1.4.4)
|
bootsnap (1.4.5)
|
||||||
msgpack (~> 1.0)
|
msgpack (~> 1.0)
|
||||||
brakeman (4.6.1)
|
brakeman (4.6.1)
|
||||||
browser (2.6.1)
|
browser (2.6.1)
|
||||||
|
@ -134,7 +134,7 @@ GEM
|
||||||
bundler (>= 1.2.0, < 3)
|
bundler (>= 1.2.0, < 3)
|
||||||
thor (~> 0.18)
|
thor (~> 0.18)
|
||||||
byebug (11.0.0)
|
byebug (11.0.0)
|
||||||
capistrano (3.11.0)
|
capistrano (3.11.1)
|
||||||
airbrussh (>= 1.0.0)
|
airbrussh (>= 1.0.0)
|
||||||
i18n
|
i18n
|
||||||
rake (>= 10.0.0)
|
rake (>= 10.0.0)
|
||||||
|
@ -231,7 +231,7 @@ GEM
|
||||||
tzinfo
|
tzinfo
|
||||||
excon (0.62.0)
|
excon (0.62.0)
|
||||||
fabrication (2.20.2)
|
fabrication (2.20.2)
|
||||||
faker (2.1.2)
|
faker (2.2.1)
|
||||||
i18n (>= 0.8)
|
i18n (>= 0.8)
|
||||||
faraday (0.15.0)
|
faraday (0.15.0)
|
||||||
multipart-post (>= 1.2, < 3)
|
multipart-post (>= 1.2, < 3)
|
||||||
|
@ -371,14 +371,14 @@ GEM
|
||||||
mini_mime (1.0.2)
|
mini_mime (1.0.2)
|
||||||
mini_portile2 (2.4.0)
|
mini_portile2 (2.4.0)
|
||||||
minitest (5.11.3)
|
minitest (5.11.3)
|
||||||
msgpack (1.2.10)
|
msgpack (1.3.1)
|
||||||
multi_json (1.13.1)
|
multi_json (1.13.1)
|
||||||
multipart-post (2.0.0)
|
multipart-post (2.0.0)
|
||||||
necromancer (0.5.0)
|
necromancer (0.5.0)
|
||||||
net-ldap (0.16.1)
|
net-ldap (0.16.1)
|
||||||
net-scp (1.2.1)
|
net-scp (2.0.0)
|
||||||
net-ssh (>= 2.6.5)
|
net-ssh (>= 2.6.5, < 6.0.0)
|
||||||
net-ssh (5.0.2)
|
net-ssh (5.2.0)
|
||||||
nio4r (2.4.0)
|
nio4r (2.4.0)
|
||||||
nokogiri (1.10.4)
|
nokogiri (1.10.4)
|
||||||
mini_portile2 (~> 2.4.0)
|
mini_portile2 (~> 2.4.0)
|
||||||
|
@ -418,7 +418,7 @@ GEM
|
||||||
parallel (1.17.0)
|
parallel (1.17.0)
|
||||||
parallel_tests (2.29.2)
|
parallel_tests (2.29.2)
|
||||||
parallel
|
parallel
|
||||||
parser (2.6.3.0)
|
parser (2.6.4.0)
|
||||||
ast (~> 2.4.0)
|
ast (~> 2.4.0)
|
||||||
parslet (1.8.2)
|
parslet (1.8.2)
|
||||||
pastel (0.7.2)
|
pastel (0.7.2)
|
||||||
|
@ -444,7 +444,7 @@ GEM
|
||||||
pry (~> 0.10)
|
pry (~> 0.10)
|
||||||
pry-rails (0.3.9)
|
pry-rails (0.3.9)
|
||||||
pry (>= 0.10.4)
|
pry (>= 0.10.4)
|
||||||
public_suffix (3.1.1)
|
public_suffix (4.0.1)
|
||||||
puma (4.1.0)
|
puma (4.1.0)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
pundit (2.1.0)
|
pundit (2.1.0)
|
||||||
|
@ -557,7 +557,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.1)
|
rubocop-rails (2.3.2)
|
||||||
rack (>= 1.1)
|
rack (>= 1.1)
|
||||||
rubocop (>= 0.72.0)
|
rubocop (>= 0.72.0)
|
||||||
ruby-progressbar (1.10.1)
|
ruby-progressbar (1.10.1)
|
||||||
|
@ -603,7 +603,7 @@ GEM
|
||||||
actionpack (>= 4.0)
|
actionpack (>= 4.0)
|
||||||
activesupport (>= 4.0)
|
activesupport (>= 4.0)
|
||||||
sprockets (>= 3.0.0)
|
sprockets (>= 3.0.0)
|
||||||
sshkit (1.17.0)
|
sshkit (1.20.0)
|
||||||
net-scp (>= 1.1.2)
|
net-scp (>= 1.1.2)
|
||||||
net-ssh (>= 2.8.0)
|
net-ssh (>= 2.8.0)
|
||||||
stackprof (0.2.12)
|
stackprof (0.2.12)
|
||||||
|
@ -647,7 +647,7 @@ GEM
|
||||||
uniform_notifier (1.12.1)
|
uniform_notifier (1.12.1)
|
||||||
warden (1.2.8)
|
warden (1.2.8)
|
||||||
rack (>= 2.0.6)
|
rack (>= 2.0.6)
|
||||||
webmock (3.6.2)
|
webmock (3.7.1)
|
||||||
addressable (>= 2.3.6)
|
addressable (>= 2.3.6)
|
||||||
crack (>= 0.3.2)
|
crack (>= 0.3.2)
|
||||||
hashdiff (>= 0.4.0, < 2.0.0)
|
hashdiff (>= 0.4.0, < 2.0.0)
|
||||||
|
@ -671,9 +671,9 @@ PLATFORMS
|
||||||
DEPENDENCIES
|
DEPENDENCIES
|
||||||
active_model_serializers (~> 0.10)
|
active_model_serializers (~> 0.10)
|
||||||
active_record_query_trace (~> 1.6)
|
active_record_query_trace (~> 1.6)
|
||||||
addressable (~> 2.6)
|
addressable (~> 2.7)
|
||||||
annotate (~> 2.7)
|
annotate (~> 2.7)
|
||||||
aws-sdk-s3 (~> 1.46)
|
aws-sdk-s3 (~> 1.48)
|
||||||
better_errors (~> 2.5)
|
better_errors (~> 2.5)
|
||||||
binding_of_caller (~> 0.7)
|
binding_of_caller (~> 0.7)
|
||||||
blurhash (~> 0.1)
|
blurhash (~> 0.1)
|
||||||
|
@ -701,7 +701,7 @@ DEPENDENCIES
|
||||||
doorkeeper (~> 5.1)
|
doorkeeper (~> 5.1)
|
||||||
dotenv-rails (~> 2.7)
|
dotenv-rails (~> 2.7)
|
||||||
fabrication (~> 2.20)
|
fabrication (~> 2.20)
|
||||||
faker (~> 2.1)
|
faker (~> 2.2)
|
||||||
fast_blank (~> 1.0)
|
fast_blank (~> 1.0)
|
||||||
fastimage
|
fastimage
|
||||||
fog-core (<= 2.1.0)
|
fog-core (<= 2.1.0)
|
||||||
|
@ -789,7 +789,7 @@ DEPENDENCIES
|
||||||
tty-prompt (~> 0.19)
|
tty-prompt (~> 0.19)
|
||||||
twitter-text (~> 1.14)
|
twitter-text (~> 1.14)
|
||||||
tzinfo-data (~> 1.2019)
|
tzinfo-data (~> 1.2019)
|
||||||
webmock (~> 3.6)
|
webmock (~> 3.7)
|
||||||
webpacker (~> 4.0)
|
webpacker (~> 4.0)
|
||||||
webpush
|
webpush
|
||||||
|
|
||||||
|
|
|
@ -37,7 +37,8 @@ module Admin
|
||||||
|
|
||||||
def set_usage_by_domain
|
def set_usage_by_domain
|
||||||
@usage_by_domain = @tag.statuses
|
@usage_by_domain = @tag.statuses
|
||||||
.where(visibility: :public)
|
.with_public_visibility
|
||||||
|
.excluding_silenced_accounts
|
||||||
.where(Status.arel_table[:id].gteq(Mastodon::Snowflake.id_at(Time.now.utc.beginning_of_day)))
|
.where(Status.arel_table[:id].gteq(Mastodon::Snowflake.id_at(Time.now.utc.beginning_of_day)))
|
||||||
.joins(:account)
|
.joins(:account)
|
||||||
.group('accounts.domain')
|
.group('accounts.domain')
|
||||||
|
@ -56,7 +57,7 @@ module Admin
|
||||||
scope = scope.unreviewed if filter_params[:review] == 'unreviewed'
|
scope = scope.unreviewed if filter_params[:review] == 'unreviewed'
|
||||||
scope = scope.reviewed.order(reviewed_at: :desc) if filter_params[:review] == 'reviewed'
|
scope = scope.reviewed.order(reviewed_at: :desc) if filter_params[:review] == 'reviewed'
|
||||||
scope = scope.pending_review.order(requested_review_at: :desc) if filter_params[:review] == 'pending_review'
|
scope = scope.pending_review.order(requested_review_at: :desc) if filter_params[:review] == 'pending_review'
|
||||||
scope.order(score: :desc)
|
scope.order(max_score: :desc)
|
||||||
end
|
end
|
||||||
|
|
||||||
def filter_params
|
def filter_params
|
||||||
|
|
|
@ -5,19 +5,42 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController
|
||||||
|
|
||||||
before_action :set_body_classes
|
before_action :set_body_classes
|
||||||
before_action :set_pack
|
before_action :set_pack
|
||||||
|
before_action :require_unconfirmed!
|
||||||
|
|
||||||
skip_before_action :require_functional!
|
skip_before_action :require_functional!
|
||||||
|
|
||||||
|
def new
|
||||||
|
super
|
||||||
|
|
||||||
|
resource.email = current_user.unconfirmed_email || current_user.email if user_signed_in?
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_pack
|
def set_pack
|
||||||
use_pack 'auth'
|
use_pack 'auth'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def require_unconfirmed!
|
||||||
|
redirect_to edit_user_registration_path if user_signed_in? && current_user.confirmed? && current_user.unconfirmed_email.blank?
|
||||||
|
end
|
||||||
|
|
||||||
def set_body_classes
|
def set_body_classes
|
||||||
@body_classes = 'lighter'
|
@body_classes = 'lighter'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def after_resending_confirmation_instructions_path_for(_resource_name)
|
||||||
|
if user_signed_in?
|
||||||
|
if current_user.confirmed? && current_user.approved?
|
||||||
|
edit_user_registration_path
|
||||||
|
else
|
||||||
|
auth_setup_path
|
||||||
|
end
|
||||||
|
else
|
||||||
|
new_user_session_path
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def after_confirmation_path_for(_resource_name, user)
|
def after_confirmation_path_for(_resource_name, user)
|
||||||
if user.created_by_application && truthy_param?(:redirect_to_app)
|
if user.created_by_application && truthy_param?(:redirect_to_app)
|
||||||
user.created_by_application.redirect_uri
|
user.created_by_application.redirect_uri
|
||||||
|
|
|
@ -8,4 +8,16 @@ module InstanceHelper
|
||||||
def site_hostname
|
def site_hostname
|
||||||
@site_hostname ||= Addressable::URI.parse("//#{Rails.configuration.x.local_domain}").display_uri.host
|
@site_hostname ||= Addressable::URI.parse("//#{Rails.configuration.x.local_domain}").display_uri.host
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def description_for_sign_up
|
||||||
|
prefix = begin
|
||||||
|
if @invite.present?
|
||||||
|
I18n.t('auth.description.prefix_invited_by_user', name: @invite.user.account.username)
|
||||||
|
else
|
||||||
|
I18n.t('auth.description.prefix_sign_up')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
safe_join([prefix, I18n.t('auth.description.suffix')], ' ')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
// This file will be loaded on admin pages, regardless of theme.
|
// This file will be loaded on admin pages, regardless of theme.
|
||||||
|
|
||||||
import { delegate } from 'rails-ujs';
|
import { delegate } from 'rails-ujs';
|
||||||
|
import ready from '../mastodon/ready';
|
||||||
|
|
||||||
const batchCheckboxClassName = '.batch-checkbox input[type="checkbox"]';
|
const batchCheckboxClassName = '.batch-checkbox input[type="checkbox"]';
|
||||||
|
|
||||||
|
@ -31,7 +32,7 @@ delegate(document, '.media-spoiler-hide-button', 'click', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
delegate(document, '#domain_block_severity', 'change', ({ target }) => {
|
const onDomainBlockSeverityChange = (target) => {
|
||||||
const rejectMediaDiv = document.querySelector('.input.with_label.domain_block_reject_media');
|
const rejectMediaDiv = document.querySelector('.input.with_label.domain_block_reject_media');
|
||||||
const rejectReportsDiv = document.querySelector('.input.with_label.domain_block_reject_reports');
|
const rejectReportsDiv = document.querySelector('.input.with_label.domain_block_reject_reports');
|
||||||
|
|
||||||
|
@ -42,4 +43,11 @@ delegate(document, '#domain_block_severity', 'change', ({ target }) => {
|
||||||
if (rejectReportsDiv) {
|
if (rejectReportsDiv) {
|
||||||
rejectReportsDiv.style.display = (target.value === 'suspend') ? 'none' : 'block';
|
rejectReportsDiv.style.display = (target.value === 'suspend') ? 'none' : 'block';
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
delegate(document, '#domain_block_severity', 'change', ({ target }) => onDomainBlockSeverityChange(target));
|
||||||
|
|
||||||
|
ready(() => {
|
||||||
|
const input = document.getElementById('domain_block_severity');
|
||||||
|
if (input) onDomainBlockSeverityChange(input);
|
||||||
});
|
});
|
||||||
|
|
|
@ -12,11 +12,11 @@ const Hashtag = ({ hashtag }) => (
|
||||||
#<span>{hashtag.get('name')}</span>
|
#<span>{hashtag.get('name')}</span>
|
||||||
</Permalink>
|
</Permalink>
|
||||||
|
|
||||||
<FormattedMessage id='trends.count_by_accounts' defaultMessage='{count} {rawCount, plural, one {person} other {people}} talking' values={{ rawCount: hashtag.getIn(['history', 0, 'accounts']), count: <strong>{shortNumberFormat(hashtag.getIn(['history', 0, 'accounts']))}</strong> }} />
|
<FormattedMessage id='trends.count_by_accounts' defaultMessage='{count} {rawCount, plural, one {person} other {people}} talking' values={{ rawCount: hashtag.getIn(['history', 0, 'accounts']) * 1 + hashtag.getIn(['history', 1, 'accounts']) * 1, count: <strong>{shortNumberFormat(hashtag.getIn(['history', 0, 'accounts']) * 1 + hashtag.getIn(['history', 1, 'accounts']) * 1)}</strong> }} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='trends__item__current'>
|
<div className='trends__item__current'>
|
||||||
{shortNumberFormat(hashtag.getIn(['history', 0, 'uses']))}
|
{shortNumberFormat(hashtag.getIn(['history', 0, 'uses']) * 1 + hashtag.getIn(['history', 1, 'uses']) * 1)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='trends__item__sparkline'>
|
<div className='trends__item__sparkline'>
|
||||||
|
|
|
@ -159,7 +159,7 @@ class Item extends React.PureComponent {
|
||||||
if (attachment.get('type') === 'unknown') {
|
if (attachment.get('type') === 'unknown') {
|
||||||
return (
|
return (
|
||||||
<div className={classNames('media-gallery__item', { standalone })} key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}>
|
<div className={classNames('media-gallery__item', { standalone })} key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}>
|
||||||
<a className='media-gallery__item-thumbnail' href={attachment.get('remote_url')} target='_blank' style={{ cursor: 'pointer' }} title={attachment.get('description')}>
|
<a className='media-gallery__item-thumbnail' href={attachment.get('remote_url') || attachment.get('url')} target='_blank' style={{ cursor: 'pointer' }} title={attachment.get('description')}>
|
||||||
<canvas width={32} height={32} ref={this.setCanvasRef} className='media-gallery__preview' />
|
<canvas width={32} height={32} ref={this.setCanvasRef} className='media-gallery__preview' />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -315,15 +315,22 @@ class MediaGallery extends React.PureComponent {
|
||||||
style.height = height;
|
style.height = height;
|
||||||
}
|
}
|
||||||
|
|
||||||
const size = media.take(4).size;
|
const size = media.take(4).size;
|
||||||
|
const uncached = media.every(attachment => attachment.get('type') === 'unknown');
|
||||||
|
|
||||||
if (this.isStandaloneEligible()) {
|
if (this.isStandaloneEligible()) {
|
||||||
children = <Item standalone onClick={this.handleClick} attachment={media.get(0)} displayWidth={width} visible={visible} />;
|
children = <Item standalone onClick={this.handleClick} attachment={media.get(0)} displayWidth={width} visible={visible} />;
|
||||||
} else {
|
} else {
|
||||||
children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} onClick={this.handleClick} attachment={attachment} index={i} size={size} displayWidth={width} visible={visible} />);
|
children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} onClick={this.handleClick} attachment={attachment} index={i} size={size} displayWidth={width} visible={visible || uncached} />);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (visible) {
|
if (uncached) {
|
||||||
|
spoilerButton = (
|
||||||
|
<button type='button' disabled className='spoiler-button__overlay'>
|
||||||
|
<span className='spoiler-button__overlay__label'><FormattedMessage id='status.uncached_media_warning' defaultMessage='Not available' /></span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
} else if (visible) {
|
||||||
spoilerButton = <IconButton title={intl.formatMessage(messages.toggle_visible)} icon='eye-slash' overlay onClick={this.handleOpen} />;
|
spoilerButton = <IconButton title={intl.formatMessage(messages.toggle_visible)} icon='eye-slash' overlay onClick={this.handleOpen} />;
|
||||||
} else {
|
} else {
|
||||||
spoilerButton = (
|
spoilerButton = (
|
||||||
|
@ -335,7 +342,7 @@ class MediaGallery extends React.PureComponent {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='media-gallery' style={style} ref={this.handleRef}>
|
<div className='media-gallery' style={style} ref={this.handleRef}>
|
||||||
<div className={classNames('spoiler-button', { 'spoiler-button--minified': visible })}>
|
<div className={classNames('spoiler-button', { 'spoiler-button--minified': visible && !uncached, 'spoiler-button--click-thru': uncached })}>
|
||||||
{spoilerButton}
|
{spoilerButton}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -82,6 +82,43 @@ class AccountCard extends ImmutablePureComponent {
|
||||||
onMute: PropTypes.func.isRequired,
|
onMute: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_updateEmojis () {
|
||||||
|
const node = this.node;
|
||||||
|
|
||||||
|
if (!node || autoPlayGif) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const emojis = node.querySelectorAll('.custom-emoji');
|
||||||
|
|
||||||
|
for (var i = 0; i < emojis.length; i++) {
|
||||||
|
let emoji = emojis[i];
|
||||||
|
if (emoji.classList.contains('status-emoji')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
emoji.classList.add('status-emoji');
|
||||||
|
|
||||||
|
emoji.addEventListener('mouseenter', this.handleEmojiMouseEnter, false);
|
||||||
|
emoji.addEventListener('mouseleave', this.handleEmojiMouseLeave, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
this._updateEmojis();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate () {
|
||||||
|
this._updateEmojis();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleEmojiMouseEnter = ({ target }) => {
|
||||||
|
target.src = target.getAttribute('data-original');
|
||||||
|
}
|
||||||
|
|
||||||
|
handleEmojiMouseLeave = ({ target }) => {
|
||||||
|
target.src = target.getAttribute('data-static');
|
||||||
|
}
|
||||||
|
|
||||||
handleFollow = () => {
|
handleFollow = () => {
|
||||||
this.props.onFollow(this.props.account);
|
this.props.onFollow(this.props.account);
|
||||||
}
|
}
|
||||||
|
@ -94,6 +131,10 @@ class AccountCard extends ImmutablePureComponent {
|
||||||
this.props.onMute(this.props.account);
|
this.props.onMute(this.props.account);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setRef = (c) => {
|
||||||
|
this.node = c;
|
||||||
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { account, intl } = this.props;
|
const { account, intl } = this.props;
|
||||||
|
|
||||||
|
@ -133,7 +174,7 @@ class AccountCard extends ImmutablePureComponent {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='directory__card__extra'>
|
<div className='directory__card__extra' ref={this.setRef}>
|
||||||
<div className='account__header__content' dangerouslySetInnerHTML={{ __html: account.get('note_emojified') }} />
|
<div className='account__header__content' dangerouslySetInnerHTML={{ __html: account.get('note_emojified') }} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,7 @@ const NavigationPanel = () => (
|
||||||
<NavLink className='column-link column-link--transparent' to='/timelines/direct'><Icon className='column-link__icon' id='envelope' fixedWidth /><FormattedMessage id='navigation_bar.direct' defaultMessage='Direct messages' /></NavLink>
|
<NavLink className='column-link column-link--transparent' to='/timelines/direct'><Icon className='column-link__icon' id='envelope' fixedWidth /><FormattedMessage id='navigation_bar.direct' defaultMessage='Direct messages' /></NavLink>
|
||||||
<NavLink className='column-link column-link--transparent' to='/favourites'><Icon className='column-link__icon' id='star' fixedWidth /><FormattedMessage id='navigation_bar.favourites' defaultMessage='Favourites' /></NavLink>
|
<NavLink className='column-link column-link--transparent' to='/favourites'><Icon className='column-link__icon' id='star' fixedWidth /><FormattedMessage id='navigation_bar.favourites' defaultMessage='Favourites' /></NavLink>
|
||||||
<NavLink className='column-link column-link--transparent' to='/lists'><Icon className='column-link__icon' id='list-ul' fixedWidth /><FormattedMessage id='navigation_bar.lists' defaultMessage='Lists' /></NavLink>
|
<NavLink className='column-link column-link--transparent' to='/lists'><Icon className='column-link__icon' id='list-ul' fixedWidth /><FormattedMessage id='navigation_bar.lists' defaultMessage='Lists' /></NavLink>
|
||||||
{profile_directory && <NavLink className='column-link column-link--transparent' to='/directory'><Icon className='column-link__icon' id='address-book-o' fixedWidth /><FormattedMessage id='getting_started.profile_directory' defaultMessage='Profile directory' /></NavLink>}
|
{profile_directory && <NavLink className='column-link column-link--transparent' to='/directory'><Icon className='column-link__icon' id='address-book-o' fixedWidth /><FormattedMessage id='getting_started.directory' defaultMessage='Profile directory' /></NavLink>}
|
||||||
|
|
||||||
<ListPanel />
|
<ListPanel />
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,14 @@
|
||||||
{
|
{
|
||||||
"defaultMessage": "An unexpected error occurred.",
|
"defaultMessage": "An unexpected error occurred.",
|
||||||
"id": "alert.unexpected.message"
|
"id": "alert.unexpected.message"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"defaultMessage": "Rate limited",
|
||||||
|
"id": "alert.rate_limited.title"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"defaultMessage": "Please retry after {retry_time, time, medium}.",
|
||||||
|
"id": "alert.rate_limited.message"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"path": "app/javascript/mastodon/actions/alerts.json"
|
"path": "app/javascript/mastodon/actions/alerts.json"
|
||||||
|
@ -191,6 +199,10 @@
|
||||||
"defaultMessage": "Toggle visibility",
|
"defaultMessage": "Toggle visibility",
|
||||||
"id": "media_gallery.toggle_visible"
|
"id": "media_gallery.toggle_visible"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"defaultMessage": "Not available",
|
||||||
|
"id": "status.uncached_media_warning"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"defaultMessage": "Sensitive content",
|
"defaultMessage": "Sensitive content",
|
||||||
"id": "status.sensitive_warning"
|
"id": "status.sensitive_warning"
|
||||||
|
@ -1130,6 +1142,19 @@
|
||||||
],
|
],
|
||||||
"path": "app/javascript/mastodon/features/compose/components/upload.json"
|
"path": "app/javascript/mastodon/features/compose/components/upload.json"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"descriptors": [
|
||||||
|
{
|
||||||
|
"defaultMessage": "Are you sure you want to log out?",
|
||||||
|
"id": "confirmations.logout.message"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"defaultMessage": "Log out",
|
||||||
|
"id": "confirmations.logout.confirm"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"path": "app/javascript/mastodon/features/compose/containers/navigation_container.json"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"descriptors": [
|
"descriptors": [
|
||||||
{
|
{
|
||||||
|
@ -1218,6 +1243,14 @@
|
||||||
{
|
{
|
||||||
"defaultMessage": "Compose new toot",
|
"defaultMessage": "Compose new toot",
|
||||||
"id": "navigation_bar.compose"
|
"id": "navigation_bar.compose"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"defaultMessage": "Are you sure you want to log out?",
|
||||||
|
"id": "confirmations.logout.message"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"defaultMessage": "Log out",
|
||||||
|
"id": "confirmations.logout.confirm"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"path": "app/javascript/mastodon/features/compose/index.json"
|
"path": "app/javascript/mastodon/features/compose/index.json"
|
||||||
|
@ -1235,6 +1268,76 @@
|
||||||
],
|
],
|
||||||
"path": "app/javascript/mastodon/features/direct_timeline/index.json"
|
"path": "app/javascript/mastodon/features/direct_timeline/index.json"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"descriptors": [
|
||||||
|
{
|
||||||
|
"defaultMessage": "Follow",
|
||||||
|
"id": "account.follow"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"defaultMessage": "Unfollow",
|
||||||
|
"id": "account.unfollow"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"defaultMessage": "Awaiting approval",
|
||||||
|
"id": "account.requested"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"defaultMessage": "Unblock @{name}",
|
||||||
|
"id": "account.unblock"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"defaultMessage": "Unmute @{name}",
|
||||||
|
"id": "account.unmute"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"defaultMessage": "Are you sure you want to unfollow {name}?",
|
||||||
|
"id": "confirmations.unfollow.message"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"defaultMessage": "Toots",
|
||||||
|
"id": "account.posts"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"defaultMessage": "Followers",
|
||||||
|
"id": "account.followers"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"defaultMessage": "Never",
|
||||||
|
"id": "account.never_active"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"defaultMessage": "Last active",
|
||||||
|
"id": "account.last_status"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"path": "app/javascript/mastodon/features/directory/components/account_card.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"descriptors": [
|
||||||
|
{
|
||||||
|
"defaultMessage": "Browse profiles",
|
||||||
|
"id": "column.directory"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"defaultMessage": "Recently active",
|
||||||
|
"id": "directory.recently_active"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"defaultMessage": "New arrivals",
|
||||||
|
"id": "directory.new_arrivals"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"defaultMessage": "From {domain} only",
|
||||||
|
"id": "directory.local"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"defaultMessage": "From known fediverse",
|
||||||
|
"id": "directory.federated"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"path": "app/javascript/mastodon/features/directory/index.json"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"descriptors": [
|
"descriptors": [
|
||||||
{
|
{
|
||||||
|
@ -2325,6 +2428,14 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"descriptors": [
|
"descriptors": [
|
||||||
|
{
|
||||||
|
"defaultMessage": "Are you sure you want to log out?",
|
||||||
|
"id": "confirmations.logout.message"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"defaultMessage": "Log out",
|
||||||
|
"id": "confirmations.logout.confirm"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"defaultMessage": "Invite people",
|
"defaultMessage": "Invite people",
|
||||||
"id": "getting_started.invite"
|
"id": "getting_started.invite"
|
||||||
|
@ -2440,6 +2551,10 @@
|
||||||
"defaultMessage": "Lists",
|
"defaultMessage": "Lists",
|
||||||
"id": "navigation_bar.lists"
|
"id": "navigation_bar.lists"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"defaultMessage": "Profile directory",
|
||||||
|
"id": "getting_started.directory"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"defaultMessage": "Preferences",
|
"defaultMessage": "Preferences",
|
||||||
"id": "navigation_bar.preferences"
|
"id": "navigation_bar.preferences"
|
||||||
|
@ -2447,10 +2562,6 @@
|
||||||
{
|
{
|
||||||
"defaultMessage": "Follows and followers",
|
"defaultMessage": "Follows and followers",
|
||||||
"id": "navigation_bar.follows_and_followers"
|
"id": "navigation_bar.follows_and_followers"
|
||||||
},
|
|
||||||
{
|
|
||||||
"defaultMessage": "Profile directory",
|
|
||||||
"id": "navigation_bar.profile_directory"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"path": "app/javascript/mastodon/features/ui/components/navigation_panel.json"
|
"path": "app/javascript/mastodon/features/ui/components/navigation_panel.json"
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
"account.follows.empty": "This user doesn't follow anyone yet.",
|
"account.follows.empty": "This user doesn't follow anyone yet.",
|
||||||
"account.follows_you": "Follows you",
|
"account.follows_you": "Follows you",
|
||||||
"account.hide_reblogs": "Hide boosts from @{name}",
|
"account.hide_reblogs": "Hide boosts from @{name}",
|
||||||
|
"account.last_status": "Last active",
|
||||||
"account.link_verified_on": "Ownership of this link was checked on {date}",
|
"account.link_verified_on": "Ownership of this link was checked on {date}",
|
||||||
"account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.",
|
"account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.",
|
||||||
"account.media": "Media",
|
"account.media": "Media",
|
||||||
|
@ -24,6 +25,7 @@
|
||||||
"account.mute": "Mute @{name}",
|
"account.mute": "Mute @{name}",
|
||||||
"account.mute_notifications": "Mute notifications from @{name}",
|
"account.mute_notifications": "Mute notifications from @{name}",
|
||||||
"account.muted": "Muted",
|
"account.muted": "Muted",
|
||||||
|
"account.never_active": "Never",
|
||||||
"account.posts": "Toots",
|
"account.posts": "Toots",
|
||||||
"account.posts_with_replies": "Toots and replies",
|
"account.posts_with_replies": "Toots and replies",
|
||||||
"account.report": "Report @{name}",
|
"account.report": "Report @{name}",
|
||||||
|
@ -36,6 +38,8 @@
|
||||||
"account.unfollow": "Unfollow",
|
"account.unfollow": "Unfollow",
|
||||||
"account.unmute": "Unmute @{name}",
|
"account.unmute": "Unmute @{name}",
|
||||||
"account.unmute_notifications": "Unmute notifications from @{name}",
|
"account.unmute_notifications": "Unmute notifications from @{name}",
|
||||||
|
"alert.rate_limited.message": "Please retry after {retry_time, time, medium}.",
|
||||||
|
"alert.rate_limited.title": "Rate limited",
|
||||||
"alert.unexpected.message": "An unexpected error occurred.",
|
"alert.unexpected.message": "An unexpected error occurred.",
|
||||||
"alert.unexpected.title": "Oops!",
|
"alert.unexpected.title": "Oops!",
|
||||||
"autosuggest_hashtag.per_week": "{count} per week",
|
"autosuggest_hashtag.per_week": "{count} per week",
|
||||||
|
@ -49,6 +53,7 @@
|
||||||
"column.blocks": "Blocked users",
|
"column.blocks": "Blocked users",
|
||||||
"column.community": "Local timeline",
|
"column.community": "Local timeline",
|
||||||
"column.direct": "Direct messages",
|
"column.direct": "Direct messages",
|
||||||
|
"column.directory": "Browse profiles",
|
||||||
"column.domain_blocks": "Hidden domains",
|
"column.domain_blocks": "Hidden domains",
|
||||||
"column.favourites": "Favourites",
|
"column.favourites": "Favourites",
|
||||||
"column.follow_requests": "Follow requests",
|
"column.follow_requests": "Follow requests",
|
||||||
|
@ -99,6 +104,8 @@
|
||||||
"confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
|
"confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
|
||||||
"confirmations.domain_block.confirm": "Hide entire domain",
|
"confirmations.domain_block.confirm": "Hide entire domain",
|
||||||
"confirmations.domain_block.message": "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.",
|
"confirmations.domain_block.message": "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.",
|
||||||
|
"confirmations.logout.confirm": "Log out",
|
||||||
|
"confirmations.logout.message": "Are you sure you want to log out?",
|
||||||
"confirmations.mute.confirm": "Mute",
|
"confirmations.mute.confirm": "Mute",
|
||||||
"confirmations.mute.message": "Are you sure you want to mute {name}?",
|
"confirmations.mute.message": "Are you sure you want to mute {name}?",
|
||||||
"confirmations.redraft.confirm": "Delete & redraft",
|
"confirmations.redraft.confirm": "Delete & redraft",
|
||||||
|
@ -107,6 +114,10 @@
|
||||||
"confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?",
|
"confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?",
|
||||||
"confirmations.unfollow.confirm": "Unfollow",
|
"confirmations.unfollow.confirm": "Unfollow",
|
||||||
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
|
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
|
||||||
|
"directory.federated": "From known fediverse",
|
||||||
|
"directory.local": "From {domain} only",
|
||||||
|
"directory.new_arrivals": "New arrivals",
|
||||||
|
"directory.recently_active": "Recently active",
|
||||||
"embed.instructions": "Embed this status on your website by copying the code below.",
|
"embed.instructions": "Embed this status on your website by copying the code below.",
|
||||||
"embed.preview": "Here is what it will look like:",
|
"embed.preview": "Here is what it will look like:",
|
||||||
"emoji_button.activity": "Activity",
|
"emoji_button.activity": "Activity",
|
||||||
|
@ -254,7 +265,6 @@
|
||||||
"navigation_bar.personal": "Personal",
|
"navigation_bar.personal": "Personal",
|
||||||
"navigation_bar.pins": "Pinned toots",
|
"navigation_bar.pins": "Pinned toots",
|
||||||
"navigation_bar.preferences": "Preferences",
|
"navigation_bar.preferences": "Preferences",
|
||||||
"navigation_bar.profile_directory": "Profile directory",
|
|
||||||
"navigation_bar.public_timeline": "Federated timeline",
|
"navigation_bar.public_timeline": "Federated timeline",
|
||||||
"navigation_bar.security": "Security",
|
"navigation_bar.security": "Security",
|
||||||
"notification.favourite": "{name} favourited your status",
|
"notification.favourite": "{name} favourited your status",
|
||||||
|
@ -361,6 +371,7 @@
|
||||||
"status.show_more": "Show more",
|
"status.show_more": "Show more",
|
||||||
"status.show_more_all": "Show more for all",
|
"status.show_more_all": "Show more for all",
|
||||||
"status.show_thread": "Show thread",
|
"status.show_thread": "Show thread",
|
||||||
|
"status.uncached_media_warning": "Not available",
|
||||||
"status.unmute_conversation": "Unmute conversation",
|
"status.unmute_conversation": "Unmute conversation",
|
||||||
"status.unpin": "Unpin from profile",
|
"status.unpin": "Unpin from profile",
|
||||||
"suggestions.dismiss": "Dismiss suggestion",
|
"suggestions.dismiss": "Dismiss suggestion",
|
||||||
|
|
|
@ -20,6 +20,7 @@ export function isRtl(text) {
|
||||||
text = text.replace(/(?:^|[^\/\w])@([a-z0-9_]+(@[a-z0-9\.\-]+)?)/ig, '');
|
text = text.replace(/(?:^|[^\/\w])@([a-z0-9_]+(@[a-z0-9\.\-]+)?)/ig, '');
|
||||||
text = text.replace(/(?:^|[^\/\w])#([\S]+)/ig, '');
|
text = text.replace(/(?:^|[^\/\w])#([\S]+)/ig, '');
|
||||||
text = text.replace(/\s+/g, '');
|
text = text.replace(/\s+/g, '');
|
||||||
|
text = text.replace(/(\w\S+\.\w{2,}\S*)/g, '');
|
||||||
|
|
||||||
const matches = text.match(rtlChars);
|
const matches = text.match(rtlChars);
|
||||||
|
|
||||||
|
|
|
@ -507,6 +507,7 @@
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
strong {
|
strong {
|
||||||
|
@ -515,8 +516,10 @@
|
||||||
|
|
||||||
&__uses {
|
&__uses {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
width: 80px;
|
|
||||||
text-align: right;
|
text-align: right;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3449,6 +3452,10 @@ a.status-card.compact:hover {
|
||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--click-thru {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
&--hidden {
|
&--hidden {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
@ -3477,6 +3484,12 @@ a.status-card.compact:hover {
|
||||||
background: rgba($base-overlay-background, 0.8);
|
background: rgba($base-overlay-background, 0.8);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
.spoiler-button__overlay__label {
|
||||||
|
background: rgba($base-overlay-background, 0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,8 @@
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
background: lighten($ui-base-color, 4%);
|
background: lighten($ui-base-color, 4%);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
& > a {
|
& > a {
|
||||||
|
|
|
@ -128,7 +128,7 @@
|
||||||
&:hover,
|
&:hover,
|
||||||
&:focus,
|
&:focus,
|
||||||
&:active {
|
&:active {
|
||||||
svg path {
|
svg {
|
||||||
fill: lighten($ui-base-color, 38%);
|
fill: lighten($ui-base-color, 38%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -112,6 +112,15 @@ code {
|
||||||
padding: 0.2em 0.4em;
|
padding: 0.2em 0.4em;
|
||||||
background: darken($ui-base-color, 12%);
|
background: darken($ui-base-color, 12%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
list-style: disc;
|
||||||
|
margin-left: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.hint {
|
||||||
|
margin-bottom: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
span.hint {
|
span.hint {
|
||||||
|
|
|
@ -32,22 +32,23 @@ class ActivityPub::Adapter < ActiveModelSerializers::Adapter::Base
|
||||||
end
|
end
|
||||||
|
|
||||||
def serializable_hash(options = nil)
|
def serializable_hash(options = nil)
|
||||||
|
named_contexts = {}
|
||||||
|
context_extensions = {}
|
||||||
options = serialization_options(options)
|
options = serialization_options(options)
|
||||||
serialized_hash = serializer.serializable_hash(options)
|
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]
|
||||||
serialized_hash = self.class.transform_key_casing!(serialized_hash, instance_options)
|
serialized_hash = self.class.transform_key_casing!(serialized_hash, instance_options)
|
||||||
|
|
||||||
{ '@context' => serialized_context }.merge(serialized_hash)
|
{ '@context' => serialized_context(named_contexts, context_extensions) }.merge(serialized_hash)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def serialized_context
|
def serialized_context(named_contexts_map, context_extensions_map)
|
||||||
context_array = []
|
context_array = []
|
||||||
|
|
||||||
serializer_options = serializer.send(:instance_options) || {}
|
named_contexts = [:activitystreams] + named_contexts_map.keys
|
||||||
named_contexts = [:activitystreams] + serializer._named_contexts.keys + serializer_options.fetch(:named_contexts, {}).keys
|
context_extensions = context_extensions_map.keys
|
||||||
context_extensions = serializer._context_extensions.keys + serializer_options.fetch(:context_extensions, {}).keys
|
|
||||||
|
|
||||||
named_contexts.each do |key|
|
named_contexts.each do |key|
|
||||||
context_array << NAMED_CONTEXT_MAP[key]
|
context_array << NAMED_CONTEXT_MAP[key]
|
||||||
|
|
|
@ -27,4 +27,12 @@ class ActivityPub::Serializer < ActiveModel::Serializer
|
||||||
_context_extensions[extension_name] = true
|
_context_extensions[extension_name] = true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def serializable_hash(adapter_options = nil, options = {}, adapter_instance = self.class.serialization_adapter_instance)
|
||||||
|
unless adapter_options&.fetch(:named_contexts, nil).nil?
|
||||||
|
adapter_options[:named_contexts].merge!(_named_contexts)
|
||||||
|
adapter_options[:context_extensions].merge!(_context_extensions)
|
||||||
|
end
|
||||||
|
super(adapter_options, options, adapter_instance)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -78,7 +78,7 @@ class FeedManager
|
||||||
reblog_key = key(type, account_id, 'reblogs')
|
reblog_key = key(type, account_id, 'reblogs')
|
||||||
|
|
||||||
# Remove any items past the MAX_ITEMS'th entry in our feed
|
# Remove any items past the MAX_ITEMS'th entry in our feed
|
||||||
redis.zremrangebyrank(timeline_key, '0', (-(FeedManager::MAX_ITEMS + 1)).to_s)
|
redis.zremrangebyrank(timeline_key, 0, -(FeedManager::MAX_ITEMS + 1))
|
||||||
|
|
||||||
# Get the score of the REBLOG_FALLOFF'th item in our feed, and stop
|
# Get the score of the REBLOG_FALLOFF'th item in our feed, and stop
|
||||||
# tracking anything after it for deduplication purposes.
|
# tracking anything after it for deduplication purposes.
|
||||||
|
|
|
@ -191,6 +191,9 @@ class Request
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
socks = []
|
||||||
|
addr_by_socket = {}
|
||||||
|
|
||||||
addresses.each do |address|
|
addresses.each do |address|
|
||||||
begin
|
begin
|
||||||
check_private_address(address)
|
check_private_address(address)
|
||||||
|
@ -200,30 +203,45 @@ class Request
|
||||||
|
|
||||||
sock.setsockopt(::Socket::IPPROTO_TCP, ::Socket::TCP_NODELAY, 1)
|
sock.setsockopt(::Socket::IPPROTO_TCP, ::Socket::TCP_NODELAY, 1)
|
||||||
|
|
||||||
begin
|
sock.connect_nonblock(sockaddr)
|
||||||
sock.connect_nonblock(sockaddr)
|
|
||||||
rescue IO::WaitWritable
|
|
||||||
if IO.select(nil, [sock], nil, Request::TIMEOUT[:connect])
|
|
||||||
begin
|
|
||||||
sock.connect_nonblock(sockaddr)
|
|
||||||
rescue Errno::EISCONN
|
|
||||||
# Yippee!
|
|
||||||
rescue
|
|
||||||
sock.close
|
|
||||||
raise
|
|
||||||
end
|
|
||||||
else
|
|
||||||
sock.close
|
|
||||||
raise HTTP::TimeoutError, "Connect timed out after #{Request::TIMEOUT[:connect]} seconds"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
|
# If that hasn't raised an exception, we somehow managed to connect
|
||||||
|
# immediately, close pending sockets and return immediately
|
||||||
|
socks.each(&:close)
|
||||||
return sock
|
return sock
|
||||||
|
rescue IO::WaitWritable
|
||||||
|
socks << sock
|
||||||
|
addr_by_socket[sock] = sockaddr
|
||||||
rescue => e
|
rescue => e
|
||||||
outer_e = e
|
outer_e = e
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
until socks.empty?
|
||||||
|
_, available_socks, = IO.select(nil, socks, nil, Request::TIMEOUT[:connect])
|
||||||
|
|
||||||
|
if available_socks.nil?
|
||||||
|
socks.each(&:close)
|
||||||
|
raise HTTP::TimeoutError, "Connect timed out after #{Request::TIMEOUT[:connect]} seconds"
|
||||||
|
end
|
||||||
|
|
||||||
|
available_socks.each do |sock|
|
||||||
|
socks.delete(sock)
|
||||||
|
|
||||||
|
begin
|
||||||
|
sock.connect_nonblock(addr_by_socket[sock])
|
||||||
|
rescue Errno::EISCONN
|
||||||
|
rescue => e
|
||||||
|
sock.close
|
||||||
|
outer_e = e
|
||||||
|
next
|
||||||
|
end
|
||||||
|
|
||||||
|
socks.each(&:close)
|
||||||
|
return sock
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
if outer_e
|
if outer_e
|
||||||
raise outer_e
|
raise outer_e
|
||||||
else
|
else
|
||||||
|
|
|
@ -7,14 +7,14 @@
|
||||||
# name :string default(""), not null
|
# name :string default(""), not null
|
||||||
# created_at :datetime not null
|
# created_at :datetime not null
|
||||||
# updated_at :datetime not null
|
# updated_at :datetime not null
|
||||||
# score :integer
|
|
||||||
# usable :boolean
|
# usable :boolean
|
||||||
# trendable :boolean
|
# trendable :boolean
|
||||||
# listable :boolean
|
# listable :boolean
|
||||||
# reviewed_at :datetime
|
# reviewed_at :datetime
|
||||||
# requested_review_at :datetime
|
# requested_review_at :datetime
|
||||||
# last_status_at :datetime
|
# last_status_at :datetime
|
||||||
# last_trend_at :datetime
|
# max_score :float
|
||||||
|
# max_score_at :datetime
|
||||||
#
|
#
|
||||||
|
|
||||||
class Tag < ApplicationRecord
|
class Tag < ApplicationRecord
|
||||||
|
|
|
@ -7,6 +7,8 @@ class TrendingTags
|
||||||
THRESHOLD = 5
|
THRESHOLD = 5
|
||||||
LIMIT = 10
|
LIMIT = 10
|
||||||
REVIEW_THRESHOLD = 3
|
REVIEW_THRESHOLD = 3
|
||||||
|
MAX_SCORE_COOLDOWN = 3.days.freeze
|
||||||
|
MAX_SCORE_HALFLIFE = 6.hours.freeze
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
include Redisable
|
include Redisable
|
||||||
|
@ -16,14 +18,75 @@ class TrendingTags
|
||||||
|
|
||||||
increment_historical_use!(tag.id, at_time)
|
increment_historical_use!(tag.id, at_time)
|
||||||
increment_unique_use!(tag.id, account.id, at_time)
|
increment_unique_use!(tag.id, account.id, at_time)
|
||||||
increment_vote!(tag, at_time)
|
increment_use!(tag.id, at_time)
|
||||||
|
|
||||||
tag.update(last_status_at: Time.now.utc) if tag.last_status_at.nil? || tag.last_status_at < 12.hours.ago
|
tag.update(last_status_at: Time.now.utc) if tag.last_status_at.nil? || tag.last_status_at < 12.hours.ago
|
||||||
tag.update(last_trend_at: Time.now.utc) if trending?(tag) && (tag.last_trend_at.nil? || tag.last_trend_at < 12.hours.ago)
|
end
|
||||||
|
|
||||||
|
def update!(at_time = Time.now.utc)
|
||||||
|
tag_ids = redis.smembers("#{KEY}:used:#{at_time.beginning_of_day.to_i}") + redis.zrange(KEY, 0, -1)
|
||||||
|
tags = Tag.where(id: tag_ids.uniq)
|
||||||
|
|
||||||
|
# First pass to calculate scores and update the set
|
||||||
|
|
||||||
|
tags.each do |tag|
|
||||||
|
expected = redis.pfcount("activity:tags:#{tag.id}:#{(at_time - 1.day).beginning_of_day.to_i}:accounts").to_f
|
||||||
|
expected = 1.0 if expected.zero?
|
||||||
|
observed = redis.pfcount("activity:tags:#{tag.id}:#{at_time.beginning_of_day.to_i}:accounts").to_f
|
||||||
|
max_time = tag.max_score_at
|
||||||
|
max_score = tag.max_score
|
||||||
|
max_score = 0 if max_time.nil? || max_time < (at_time - MAX_SCORE_COOLDOWN)
|
||||||
|
|
||||||
|
score = begin
|
||||||
|
if expected > observed || observed < THRESHOLD
|
||||||
|
0
|
||||||
|
else
|
||||||
|
((observed - expected)**2) / expected
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if score > max_score
|
||||||
|
max_score = score
|
||||||
|
max_time = at_time
|
||||||
|
|
||||||
|
# Not interested in triggering any callbacks for this
|
||||||
|
tag.update_columns(max_score: max_score, max_score_at: max_time)
|
||||||
|
end
|
||||||
|
|
||||||
|
decaying_score = max_score * (0.5**((at_time.to_f - max_time.to_f) / MAX_SCORE_HALFLIFE.to_f))
|
||||||
|
|
||||||
|
if decaying_score.zero?
|
||||||
|
redis.zrem(KEY, tag.id)
|
||||||
|
else
|
||||||
|
redis.zadd(KEY, decaying_score, tag.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
users_for_review = User.staff.includes(:account).to_a.select(&:allows_trending_tag_emails?)
|
||||||
|
|
||||||
|
# Second pass to notify about previously unreviewed trends
|
||||||
|
|
||||||
|
tags.each do |tag|
|
||||||
|
current_rank = redis.zrevrank(KEY, tag.id)
|
||||||
|
needs_review_notification = tag.requires_review? && !tag.requested_review?
|
||||||
|
rank_passes_threshold = current_rank.present? && current_rank <= REVIEW_THRESHOLD
|
||||||
|
|
||||||
|
next unless !tag.trendable? && rank_passes_threshold && needs_review_notification
|
||||||
|
|
||||||
|
tag.touch(:requested_review_at)
|
||||||
|
|
||||||
|
users_for_review.each do |user|
|
||||||
|
AdminMailer.new_trending_tag(user.account, tag).deliver_later!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Trim older items
|
||||||
|
|
||||||
|
redis.zremrangebyrank(KEY, 0, -(LIMIT + 1))
|
||||||
end
|
end
|
||||||
|
|
||||||
def get(limit, filtered: true)
|
def get(limit, filtered: true)
|
||||||
tag_ids = redis.zrevrange("#{KEY}:#{Time.now.utc.beginning_of_day.to_i}", 0, LIMIT - 1).map(&:to_i)
|
tag_ids = redis.zrevrange(KEY, 0, LIMIT - 1).map(&:to_i)
|
||||||
|
|
||||||
tags = Tag.where(id: tag_ids)
|
tags = Tag.where(id: tag_ids)
|
||||||
tags = tags.where(trendable: true) if filtered
|
tags = tags.where(trendable: true) if filtered
|
||||||
|
@ -33,8 +96,8 @@ class TrendingTags
|
||||||
end
|
end
|
||||||
|
|
||||||
def trending?(tag)
|
def trending?(tag)
|
||||||
rank = redis.zrevrank("#{KEY}:#{Time.now.utc.beginning_of_day.to_i}", tag.id)
|
rank = redis.zrevrank(KEY, tag.id)
|
||||||
rank.present? && rank <= LIMIT
|
rank.present? && rank < LIMIT
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
@ -51,31 +114,10 @@ class TrendingTags
|
||||||
redis.expire(key, EXPIRE_HISTORY_AFTER)
|
redis.expire(key, EXPIRE_HISTORY_AFTER)
|
||||||
end
|
end
|
||||||
|
|
||||||
def increment_vote!(tag, at_time)
|
def increment_use!(tag_id, at_time)
|
||||||
key = "#{KEY}:#{at_time.beginning_of_day.to_i}"
|
key = "#{KEY}:used:#{at_time.beginning_of_day.to_i}"
|
||||||
expected = redis.pfcount("activity:tags:#{tag.id}:#{(at_time - 1.day).beginning_of_day.to_i}:accounts").to_f
|
redis.sadd(key, tag_id)
|
||||||
expected = 1.0 if expected.zero?
|
redis.expire(key, EXPIRE_HISTORY_AFTER)
|
||||||
observed = redis.pfcount("activity:tags:#{tag.id}:#{at_time.beginning_of_day.to_i}:accounts").to_f
|
|
||||||
|
|
||||||
if expected > observed || observed < THRESHOLD
|
|
||||||
redis.zrem(key, tag.id)
|
|
||||||
else
|
|
||||||
score = ((observed - expected)**2) / expected
|
|
||||||
old_rank = redis.zrevrank(key, tag.id)
|
|
||||||
|
|
||||||
redis.zadd(key, score, tag.id)
|
|
||||||
request_review!(tag) if (old_rank.nil? || old_rank > REVIEW_THRESHOLD) && redis.zrevrank(key, tag.id) <= REVIEW_THRESHOLD && !tag.trendable? && tag.requires_review? && !tag.requested_review?
|
|
||||||
end
|
|
||||||
|
|
||||||
redis.expire(key, EXPIRE_TRENDS_AFTER)
|
|
||||||
end
|
|
||||||
|
|
||||||
def request_review!(tag)
|
|
||||||
return unless Setting.trends
|
|
||||||
|
|
||||||
tag.touch(:requested_review_at)
|
|
||||||
|
|
||||||
User.staff.includes(:account).find_each { |u| AdminMailer.new_trending_tag(u.account, tag).deliver_later! if u.allows_trending_tag_emails? }
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -6,7 +6,7 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer
|
||||||
context :security
|
context :security
|
||||||
|
|
||||||
context_extensions :manually_approves_followers, :featured, :also_known_as,
|
context_extensions :manually_approves_followers, :featured, :also_known_as,
|
||||||
:moved_to, :property_value, :hashtag, :emoji, :identity_proof,
|
:moved_to, :property_value, :identity_proof,
|
||||||
:discoverable
|
:discoverable
|
||||||
|
|
||||||
attributes :id, :type, :following, :followers,
|
attributes :id, :type, :following, :followers,
|
||||||
|
@ -138,6 +138,8 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer
|
||||||
end
|
end
|
||||||
|
|
||||||
class TagSerializer < ActivityPub::Serializer
|
class TagSerializer < ActivityPub::Serializer
|
||||||
|
context_extensions :hashtag
|
||||||
|
|
||||||
include RoutingHelper
|
include RoutingHelper
|
||||||
|
|
||||||
attributes :type, :href, :name
|
attributes :type, :href, :name
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class ActivityPub::NoteSerializer < ActivityPub::Serializer
|
class ActivityPub::NoteSerializer < ActivityPub::Serializer
|
||||||
context_extensions :atom_uri, :conversation, :sensitive,
|
context_extensions :atom_uri, :conversation, :sensitive
|
||||||
:hashtag, :emoji, :focal_point, :blurhash
|
|
||||||
|
|
||||||
attributes :id, :type, :summary,
|
attributes :id, :type, :summary,
|
||||||
:in_reply_to, :published, :url,
|
:in_reply_to, :published, :url,
|
||||||
|
@ -152,6 +151,8 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer
|
||||||
end
|
end
|
||||||
|
|
||||||
class MediaAttachmentSerializer < ActivityPub::Serializer
|
class MediaAttachmentSerializer < ActivityPub::Serializer
|
||||||
|
context_extensions :blurhash, :focal_point
|
||||||
|
|
||||||
include RoutingHelper
|
include RoutingHelper
|
||||||
|
|
||||||
attributes :type, :media_type, :url, :name, :blurhash
|
attributes :type, :media_type, :url, :name, :blurhash
|
||||||
|
@ -199,6 +200,8 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer
|
||||||
end
|
end
|
||||||
|
|
||||||
class TagSerializer < ActivityPub::Serializer
|
class TagSerializer < ActivityPub::Serializer
|
||||||
|
context_extensions :hashtag
|
||||||
|
|
||||||
include RoutingHelper
|
include RoutingHelper
|
||||||
|
|
||||||
attributes :type, :href, :name
|
attributes :type, :href, :name
|
||||||
|
|
|
@ -61,6 +61,7 @@ class SuspendAccountService < BaseService
|
||||||
return if !@account.local? || @account.user.nil?
|
return if !@account.local? || @account.user.nil?
|
||||||
|
|
||||||
if @options[:including_user]
|
if @options[:including_user]
|
||||||
|
@options[:destroy] = true if !@account.user_confirmed? || @account.user_pending?
|
||||||
@account.user.destroy
|
@account.user.destroy
|
||||||
else
|
else
|
||||||
@account.user.disable!
|
@account.user.disable!
|
||||||
|
|
|
@ -44,15 +44,16 @@
|
||||||
- if !instance.domain_block.noop?
|
- if !instance.domain_block.noop?
|
||||||
= t("admin.domain_blocks.severity.#{instance.domain_block.severity}")
|
= t("admin.domain_blocks.severity.#{instance.domain_block.severity}")
|
||||||
- first_item = false
|
- first_item = false
|
||||||
- if instance.domain_block.reject_media?
|
- unless instance.domain_block.suspend?
|
||||||
- unless first_item
|
- if instance.domain_block.reject_media?
|
||||||
•
|
- unless first_item
|
||||||
= t('admin.domain_blocks.rejecting_media')
|
•
|
||||||
- first_item = false
|
= t('admin.domain_blocks.rejecting_media')
|
||||||
- if instance.domain_block.reject_reports?
|
- first_item = false
|
||||||
- unless first_item
|
- if instance.domain_block.reject_reports?
|
||||||
•
|
- unless first_item
|
||||||
= t('admin.domain_blocks.rejecting_reports')
|
•
|
||||||
|
= t('admin.domain_blocks.rejecting_reports')
|
||||||
- elsif whitelist_mode?
|
- elsif whitelist_mode?
|
||||||
= t('admin.accounts.whitelisted')
|
= t('admin.accounts.whitelisted')
|
||||||
- else
|
- else
|
||||||
|
|
|
@ -38,8 +38,10 @@
|
||||||
.table-wrapper
|
.table-wrapper
|
||||||
%table.table
|
%table.table
|
||||||
%tbody
|
%tbody
|
||||||
|
- total = @usage_by_domain.sum(&:last).to_f
|
||||||
|
|
||||||
- @usage_by_domain.each do |(domain, count)|
|
- @usage_by_domain.each do |(domain, count)|
|
||||||
%tr
|
%tr
|
||||||
%th= domain || site_hostname
|
%th= domain || site_hostname
|
||||||
%td= number_to_percentage((count / @tag.history[0][:uses].to_f) * 100)
|
%td= number_to_percentage((count / total) * 100, precision: 1)
|
||||||
%td= number_with_delimiter count
|
%td= number_with_delimiter count
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
.hero-widget__text
|
.hero-widget__text
|
||||||
%p= @instance_presenter.site_short_description.html_safe.presence || @instance_presenter.site_description.html_safe.presence || t('about.generic_description', domain: site_hostname)
|
%p= @instance_presenter.site_short_description.html_safe.presence || @instance_presenter.site_description.html_safe.presence || t('about.generic_description', domain: site_hostname)
|
||||||
|
|
||||||
- if Setting.trends
|
- if Setting.trends && !(user_signed_in? && !current_user.setting_trends)
|
||||||
- trends = TrendingTags.get(3)
|
- trends = TrendingTags.get(3)
|
||||||
|
|
||||||
- unless trends.empty?
|
- unless trends.empty?
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
= t('auth.register')
|
= t('auth.register')
|
||||||
|
|
||||||
- content_for :header_tags do
|
- content_for :header_tags do
|
||||||
= render partial: 'shared/og'
|
= render partial: 'shared/og', locals: { description: description_for_sign_up }
|
||||||
|
|
||||||
= simple_form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f|
|
= simple_form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f|
|
||||||
= render 'shared/error_messages', object: resource
|
= render 'shared/error_messages', object: resource
|
||||||
|
|
|
@ -17,7 +17,4 @@
|
||||||
.simple_form
|
.simple_form
|
||||||
%p.hint= t('auth.setup.email_settings_hint_html', email: content_tag(:strong, @user.email))
|
%p.hint= t('auth.setup.email_settings_hint_html', email: content_tag(:strong, @user.email))
|
||||||
|
|
||||||
.form-footer
|
.form-footer= render 'auth/shared/links'
|
||||||
%ul.no-list
|
|
||||||
%li= link_to t('settings.account_settings'), edit_user_registration_path
|
|
||||||
%li= link_to t('auth.logout'), destroy_user_session_path, data: { method: :delete }
|
|
||||||
|
|
|
@ -1,12 +1,18 @@
|
||||||
%ul.no-list
|
%ul.no-list
|
||||||
- if controller_name != 'sessions'
|
- if user_signed_in?
|
||||||
%li= link_to t('auth.login'), new_session_path(resource_name)
|
%li= link_to t('settings.account_settings'), edit_user_registration_path
|
||||||
|
- else
|
||||||
|
- if controller_name != 'sessions'
|
||||||
|
%li= link_to t('auth.login'), new_user_session_path
|
||||||
|
|
||||||
- if devise_mapping.registerable? && controller_name != 'registrations'
|
- if controller_name != 'registrations'
|
||||||
%li= link_to t('auth.register'), available_sign_up_path
|
%li= link_to t('auth.register'), available_sign_up_path
|
||||||
|
|
||||||
- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations'
|
- if controller_name != 'passwords' && controller_name != 'registrations'
|
||||||
%li= link_to t('auth.forgot_password'), new_password_path(resource_name)
|
%li= link_to t('auth.forgot_password'), new_user_password_path
|
||||||
|
|
||||||
- if devise_mapping.confirmable? && controller_name != 'confirmations'
|
- if controller_name != 'confirmations'
|
||||||
%li= link_to t('auth.didnt_get_confirmation'), new_confirmation_path(resource_name)
|
%li= link_to t('auth.didnt_get_confirmation'), new_user_confirmation_path
|
||||||
|
|
||||||
|
- if user_signed_in? && controller_name != 'setup'
|
||||||
|
%li= link_to t('auth.logout'), destroy_user_session_path, data: { method: :delete }
|
||||||
|
|
|
@ -49,7 +49,7 @@
|
||||||
- if account.last_status_at.present?
|
- if account.last_status_at.present?
|
||||||
%time.time-ago{ datetime: account.last_status_at.iso8601, title: l(account.last_status_at) }= l account.last_status_at
|
%time.time-ago{ datetime: account.last_status_at.iso8601, title: l(account.last_status_at) }= l account.last_status_at
|
||||||
- else
|
- else
|
||||||
= t('invites.expires_in_prompt')
|
= t('accounts.never_active')
|
||||||
|
|
||||||
%small= t('accounts.last_active')
|
%small= t('accounts.last_active')
|
||||||
|
|
||||||
|
|
|
@ -36,7 +36,10 @@
|
||||||
- if status.media_attachments.size > 0
|
- if status.media_attachments.size > 0
|
||||||
%p
|
%p
|
||||||
- status.media_attachments.each do |a|
|
- status.media_attachments.each do |a|
|
||||||
= link_to medium_url(a), medium_url(a)
|
- if status.local?
|
||||||
|
= link_to medium_url(a), medium_url(a)
|
||||||
|
- else
|
||||||
|
= link_to a.remote_url, a.remote_url
|
||||||
|
|
||||||
%p.status-footer
|
%p.status-footer
|
||||||
= link_to l(status.created_at), web_url("statuses/#{status.id}")
|
= link_to l(status.created_at), web_url("statuses/#{status.id}")
|
||||||
|
|
|
@ -2,15 +2,25 @@
|
||||||
= t('settings.delete')
|
= t('settings.delete')
|
||||||
|
|
||||||
= simple_form_for @confirmation, url: settings_delete_path, method: :delete do |f|
|
= simple_form_for @confirmation, url: settings_delete_path, method: :delete do |f|
|
||||||
.warning
|
%p.hint= t('deletes.warning.before')
|
||||||
%strong
|
|
||||||
= fa_icon('warning')
|
|
||||||
= t('deletes.warning_title')
|
|
||||||
= t('deletes.warning_html')
|
|
||||||
|
|
||||||
%p.hint= t('deletes.description_html')
|
%ul.hint
|
||||||
|
- if current_user.confirmed? && current_user.approved?
|
||||||
|
%li.warning-hint= t('deletes.warning.irreversible')
|
||||||
|
%li.warning-hint= t('deletes.warning.username_unavailable')
|
||||||
|
%li.warning-hint= t('deletes.warning.data_removal')
|
||||||
|
%li.warning-hint= t('deletes.warning.caches')
|
||||||
|
- else
|
||||||
|
%li.positive-hint= t('deletes.warning.email_change_html', path: edit_user_registration_path)
|
||||||
|
%li.positive-hint= t('deletes.warning.email_reconfirmation_html', path: new_user_confirmation_path)
|
||||||
|
%li.positive-hint= t('deletes.warning.email_contact_html', email: Setting.site_contact_email)
|
||||||
|
%li.positive-hint= t('deletes.warning.username_available')
|
||||||
|
|
||||||
= f.input :password, placeholder: t('simple_form.labels.defaults.current_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.current_password'), :autocomplete => 'off' }, hint: t('deletes.confirm_password')
|
%p.hint= t('deletes.warning.more_details_html', terms_path: terms_path)
|
||||||
|
|
||||||
|
%hr.spacer/
|
||||||
|
|
||||||
|
= f.input :password, wrapper: :with_block_label, input_html: { :autocomplete => 'off' }, hint: t('deletes.confirm_password')
|
||||||
|
|
||||||
.actions
|
.actions
|
||||||
= f.button :button, t('deletes.proceed'), type: :submit, class: 'negative'
|
= f.button :button, t('deletes.proceed'), type: :submit, class: 'negative'
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
- thumbnail = @instance_presenter.thumbnail
|
- thumbnail = @instance_presenter.thumbnail
|
||||||
- description = strip_tags(@instance_presenter.site_short_description.presence || @instance_presenter.site_description.presence || t('about.about_mastodon_html'))
|
- description ||= strip_tags(@instance_presenter.site_short_description.presence || @instance_presenter.site_description.presence || t('about.about_mastodon_html'))
|
||||||
|
|
||||||
%meta{ name: 'description', content: description }/
|
%meta{ name: 'description', content: description }/
|
||||||
|
|
||||||
|
|
|
@ -42,11 +42,11 @@
|
||||||
- unless @warning.text.blank?
|
- unless @warning.text.blank?
|
||||||
= Formatter.instance.linkify(@warning.text)
|
= Formatter.instance.linkify(@warning.text)
|
||||||
|
|
||||||
- unless @statuses&.empty?
|
- if !@statuses.nil? && !@statuses.empty?
|
||||||
%p
|
%p
|
||||||
%strong= t('user_mailer.warning.statuses')
|
%strong= t('user_mailer.warning.statuses')
|
||||||
|
|
||||||
- unless @statuses&.empty?
|
- if !@statuses.nil? && !@statuses.empty?
|
||||||
- @statuses.each_with_index do |status, i|
|
- @statuses.each_with_index do |status, i|
|
||||||
= render 'notification_mailer/status', status: status, i: i + 1, highlighted: true
|
= render 'notification_mailer/status', status: status, i: i + 1, highlighted: true
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
|
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= @warning.text %>
|
<%= @warning.text %>
|
||||||
<% unless @statuses&.empty? %>
|
<% if !@statuses.nil? && !@statuses.empty? %>
|
||||||
<%= t('user_mailer.warning.statuses') %>
|
<%= t('user_mailer.warning.statuses') %>
|
||||||
|
|
||||||
<% @statuses.each do |status| %>
|
<% @statuses.each do |status| %>
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Scheduler::TrendingTagsScheduler
|
||||||
|
include Sidekiq::Worker
|
||||||
|
|
||||||
|
sidekiq_options unique: :until_executed, retry: 0
|
||||||
|
|
||||||
|
def perform
|
||||||
|
TrendingTags.update! if Setting.trends
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,6 +1,6 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
lock '3.11.0'
|
lock '3.11.1'
|
||||||
|
|
||||||
set :repo_url, ENV.fetch('REPO', 'https://github.com/tootsuite/mastodon.git')
|
set :repo_url, ENV.fetch('REPO', 'https://github.com/tootsuite/mastodon.git')
|
||||||
set :branch, ENV.fetch('BRANCH', 'master')
|
set :branch, ENV.fetch('BRANCH', 'master')
|
||||||
|
|
|
@ -83,7 +83,10 @@ Rails.application.configure do
|
||||||
config.action_mailer.perform_caching = false
|
config.action_mailer.perform_caching = false
|
||||||
|
|
||||||
# E-mails
|
# E-mails
|
||||||
config.action_mailer.default_options = { from: ENV.fetch('SMTP_FROM_ADDRESS', 'notifications@localhost') }
|
config.action_mailer.default_options = {
|
||||||
|
from: ENV.fetch('SMTP_FROM_ADDRESS', 'notifications@localhost'),
|
||||||
|
reply_to: ENV['SMTP_REPLY_TO']
|
||||||
|
}
|
||||||
|
|
||||||
config.action_mailer.smtp_settings = {
|
config.action_mailer.smtp_settings = {
|
||||||
:port => ENV['SMTP_PORT'],
|
:port => ENV['SMTP_PORT'],
|
||||||
|
|
|
@ -3,22 +3,3 @@ ActiveModelSerializers.config.tap do |config|
|
||||||
end
|
end
|
||||||
|
|
||||||
ActiveSupport::Notifications.unsubscribe(ActiveModelSerializers::Logging::RENDER_EVENT)
|
ActiveSupport::Notifications.unsubscribe(ActiveModelSerializers::Logging::RENDER_EVENT)
|
||||||
|
|
||||||
class ActiveModel::Serializer::Reflection
|
|
||||||
# We monkey-patch this method so that when we include associations in a serializer,
|
|
||||||
# the nested serializers can send information about used contexts upwards back to
|
|
||||||
# the root. We do this via instance_options because the nesting can be dynamic.
|
|
||||||
def build_association(parent_serializer, parent_serializer_options, include_slice = {})
|
|
||||||
serializer = options[:serializer]
|
|
||||||
|
|
||||||
parent_serializer_options.merge!(named_contexts: serializer._named_contexts, context_extensions: serializer._context_extensions) if serializer.respond_to?(:_named_contexts)
|
|
||||||
|
|
||||||
association_options = {
|
|
||||||
parent_serializer: parent_serializer,
|
|
||||||
parent_serializer_options: parent_serializer_options,
|
|
||||||
include_slice: include_slice,
|
|
||||||
}
|
|
||||||
|
|
||||||
ActiveModel::Serializer::Association.new(self, association_options)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
|
@ -58,6 +58,7 @@ en:
|
||||||
media: Media
|
media: Media
|
||||||
moved_html: "%{name} has moved to %{new_profile_link}:"
|
moved_html: "%{name} has moved to %{new_profile_link}:"
|
||||||
network_hidden: This information is not available
|
network_hidden: This information is not available
|
||||||
|
never_active: Never
|
||||||
nothing_here: There is nothing here!
|
nothing_here: There is nothing here!
|
||||||
people_followed_by: People whom %{name} follows
|
people_followed_by: People whom %{name} follows
|
||||||
people_who_follow: People who follow %{name}
|
people_who_follow: People who follow %{name}
|
||||||
|
@ -581,6 +582,10 @@ en:
|
||||||
checkbox_agreement_without_rules_html: I agree to the <a href="%{terms_path}" target="_blank">terms of service</a>
|
checkbox_agreement_without_rules_html: I agree to the <a href="%{terms_path}" target="_blank">terms of service</a>
|
||||||
delete_account: Delete account
|
delete_account: Delete account
|
||||||
delete_account_html: If you wish to delete your account, you can <a href="%{path}">proceed here</a>. You will be asked for confirmation.
|
delete_account_html: If you wish to delete your account, you can <a href="%{path}">proceed here</a>. You will be asked for confirmation.
|
||||||
|
description:
|
||||||
|
prefix_invited_by_user: "@%{name} invites you to join this server of Mastodon!"
|
||||||
|
prefix_sign_up: Sign up on Mastodon today!
|
||||||
|
suffix: With an account, you will be able to follow people, post updates and exchange messages with users from any Mastodon server and more!
|
||||||
didnt_get_confirmation: Didn't receive confirmation instructions?
|
didnt_get_confirmation: Didn't receive confirmation instructions?
|
||||||
forgot_password: Forgot your password?
|
forgot_password: Forgot your password?
|
||||||
invalid_reset_password_token: Password reset token is invalid or expired. Please request a new one.
|
invalid_reset_password_token: Password reset token is invalid or expired. Please request a new one.
|
||||||
|
@ -634,13 +639,21 @@ en:
|
||||||
x_months: "%{count}mo"
|
x_months: "%{count}mo"
|
||||||
x_seconds: "%{count}s"
|
x_seconds: "%{count}s"
|
||||||
deletes:
|
deletes:
|
||||||
bad_password_msg: Nice try, hackers! Incorrect password
|
bad_password_msg: The password you entered was incorrect
|
||||||
confirm_password: Enter your current password to verify your identity
|
confirm_password: Enter your current password to verify your identity
|
||||||
description_html: This will <strong>permanently, irreversibly</strong> remove content from your account and deactivate it. Your username will remain reserved to prevent future impersonations.
|
|
||||||
proceed: Delete account
|
proceed: Delete account
|
||||||
success_msg: Your account was successfully deleted
|
success_msg: Your account was successfully deleted
|
||||||
warning_html: Only deletion of content from this particular server is guaranteed. Content that has been widely shared is likely to leave traces. Offline servers and servers that have unsubscribed from your updates will not update their databases.
|
warning:
|
||||||
warning_title: Disseminated content availability
|
before: 'Before proceeding, please read these notes carefully:'
|
||||||
|
caches: Content that has been cached by other servers may persist
|
||||||
|
data_removal: Your posts and other data will be permanently removed
|
||||||
|
email_change_html: You can <a href="%{path}">change your e-mail address</a> without deleting your account
|
||||||
|
email_contact_html: If it still doesn't arrive, you can e-mail <a href="mailto:%{email}">%{email}</a> for help
|
||||||
|
email_reconfirmation_html: If you are not receiving the confirmation e-mail, you can <a href="%{path}">request it again</a>
|
||||||
|
irreversible: You will not be able to restore or reactivate your account
|
||||||
|
more_details_html: For more details, see the <a href="%{terms_path}">privacy policy</a>.
|
||||||
|
username_available: Your username will become available again
|
||||||
|
username_unavailable: Your username will remain unavailable
|
||||||
directories:
|
directories:
|
||||||
directory: Profile directory
|
directory: Profile directory
|
||||||
explanation: Discover users based on their interests
|
explanation: Discover users based on their interests
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
persistent_timeout ENV.fetch('PERSISTENT_TIMEOUT') { 20 }.to_i
|
||||||
|
|
||||||
threads_count = ENV.fetch('MAX_THREADS') { 5 }.to_i
|
threads_count = ENV.fetch('MAX_THREADS') { 5 }.to_i
|
||||||
threads threads_count, threads_count
|
threads threads_count, threads_count
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,9 @@
|
||||||
scheduled_statuses_scheduler:
|
scheduled_statuses_scheduler:
|
||||||
every: '5m'
|
every: '5m'
|
||||||
class: Scheduler::ScheduledStatusesScheduler
|
class: Scheduler::ScheduledStatusesScheduler
|
||||||
|
trending_tags_scheduler:
|
||||||
|
every: '5m'
|
||||||
|
class: Scheduler::TrendingTagsScheduler
|
||||||
media_cleanup_scheduler:
|
media_cleanup_scheduler:
|
||||||
cron: '<%= Random.rand(0..59) %> <%= Random.rand(3..5) %> * * *'
|
cron: '<%= Random.rand(0..59) %> <%= Random.rand(3..5) %> * * *'
|
||||||
class: Scheduler::MediaCleanupScheduler
|
class: Scheduler::MediaCleanupScheduler
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
class AddMaxScoreToTags < ActiveRecord::Migration[5.2]
|
||||||
|
def change
|
||||||
|
add_column :tags, :max_score, :float
|
||||||
|
add_column :tags, :max_score_at, :datetime
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,12 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class RemoveScoreFromTags < ActiveRecord::Migration[5.2]
|
||||||
|
disable_ddl_transaction!
|
||||||
|
|
||||||
|
def change
|
||||||
|
safety_assured do
|
||||||
|
remove_column :tags, :score, :int
|
||||||
|
remove_column :tags, :last_trend_at, :datetime
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -10,7 +10,7 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema.define(version: 2019_08_23_221802) do
|
ActiveRecord::Schema.define(version: 2019_09_01_040524) do
|
||||||
|
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
|
@ -677,14 +677,14 @@ ActiveRecord::Schema.define(version: 2019_08_23_221802) do
|
||||||
t.string "name", default: "", null: false
|
t.string "name", default: "", null: false
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
t.integer "score"
|
|
||||||
t.boolean "usable"
|
t.boolean "usable"
|
||||||
t.boolean "trendable"
|
t.boolean "trendable"
|
||||||
t.boolean "listable"
|
t.boolean "listable"
|
||||||
t.datetime "reviewed_at"
|
t.datetime "reviewed_at"
|
||||||
t.datetime "requested_review_at"
|
t.datetime "requested_review_at"
|
||||||
t.datetime "last_status_at"
|
t.datetime "last_status_at"
|
||||||
t.datetime "last_trend_at"
|
t.float "max_score"
|
||||||
|
t.datetime "max_score_at"
|
||||||
t.index "lower((name)::text)", name: "index_tags_on_name_lower", unique: true
|
t.index "lower((name)::text)", name: "index_tags_on_name_lower", unique: true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -68,7 +68,7 @@
|
||||||
"@babel/plugin-transform-react-inline-elements": "^7.2.0",
|
"@babel/plugin-transform-react-inline-elements": "^7.2.0",
|
||||||
"@babel/plugin-transform-react-jsx-self": "^7.2.0",
|
"@babel/plugin-transform-react-jsx-self": "^7.2.0",
|
||||||
"@babel/plugin-transform-react-jsx-source": "^7.5.0",
|
"@babel/plugin-transform-react-jsx-source": "^7.5.0",
|
||||||
"@babel/plugin-transform-runtime": "^7.4.4",
|
"@babel/plugin-transform-runtime": "^7.5.5",
|
||||||
"@babel/preset-env": "^7.5.5",
|
"@babel/preset-env": "^7.5.5",
|
||||||
"@babel/preset-react": "^7.0.0",
|
"@babel/preset-react": "^7.0.0",
|
||||||
"@babel/runtime": "^7.5.4",
|
"@babel/runtime": "^7.5.4",
|
||||||
|
@ -172,7 +172,7 @@
|
||||||
"websocket.js": "^0.1.12"
|
"websocket.js": "^0.1.12"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"babel-eslint": "^10.0.2",
|
"babel-eslint": "^10.0.3",
|
||||||
"babel-jest": "^24.8.0",
|
"babel-jest": "^24.8.0",
|
||||||
"enzyme": "^3.10.0",
|
"enzyme": "^3.10.0",
|
||||||
"enzyme-adapter-react-16": "^1.14.0",
|
"enzyme-adapter-react-16": "^1.14.0",
|
||||||
|
|
|
@ -19,7 +19,7 @@ RSpec.describe ActivityPub::Activity::Update do
|
||||||
end
|
end
|
||||||
|
|
||||||
let(:actor_json) do
|
let(:actor_json) do
|
||||||
ActiveModelSerializers::SerializableResource.new(modified_sender, serializer: ActivityPub::ActorSerializer, key_transform: :camel_lower).as_json
|
ActiveModelSerializers::SerializableResource.new(modified_sender, serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter).as_json
|
||||||
end
|
end
|
||||||
|
|
||||||
let(:json) do
|
let(:json) do
|
||||||
|
|
|
@ -0,0 +1,68 @@
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe TrendingTags do
|
||||||
|
describe '.record_use!' do
|
||||||
|
pending
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '.update!' do
|
||||||
|
let!(:at_time) { Time.now.utc }
|
||||||
|
let!(:tag1) { Fabricate(:tag, name: 'Catstodon') }
|
||||||
|
let!(:tag2) { Fabricate(:tag, name: 'DogsOfMastodon') }
|
||||||
|
let!(:tag3) { Fabricate(:tag, name: 'OCs') }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(Redis.current).to receive(:pfcount) do |key|
|
||||||
|
case key
|
||||||
|
when "activity:tags:#{tag1.id}:#{(at_time - 1.day).beginning_of_day.to_i}:accounts"
|
||||||
|
2
|
||||||
|
when "activity:tags:#{tag1.id}:#{at_time.beginning_of_day.to_i}:accounts"
|
||||||
|
16
|
||||||
|
when "activity:tags:#{tag2.id}:#{(at_time - 1.day).beginning_of_day.to_i}:accounts"
|
||||||
|
0
|
||||||
|
when "activity:tags:#{tag2.id}:#{at_time.beginning_of_day.to_i}:accounts"
|
||||||
|
4
|
||||||
|
when "activity:tags:#{tag3.id}:#{(at_time - 1.day).beginning_of_day.to_i}:accounts"
|
||||||
|
13
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Redis.current.zadd('trending_tags', 0.9, tag3.id)
|
||||||
|
Redis.current.sadd("trending_tags:used:#{at_time.beginning_of_day.to_i}", [tag1.id, tag2.id])
|
||||||
|
|
||||||
|
tag3.update(max_score: 0.9, max_score_at: (at_time - 1.day).beginning_of_day + 12.hours)
|
||||||
|
|
||||||
|
described_class.update!(at_time)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'calculates and re-calculates scores' do
|
||||||
|
expect(described_class.get(10, filtered: false)).to eq [tag1, tag3]
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'omits hashtags below threshold' do
|
||||||
|
expect(described_class.get(10, filtered: false)).to_not include(tag2)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'decays scores' do
|
||||||
|
expect(Redis.current.zscore('trending_tags', tag3.id)).to be < 0.9
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '.trending?' do
|
||||||
|
let(:tag) { Fabricate(:tag) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
10.times { |i| Redis.current.zadd('trending_tags', i + 1, Fabricate(:tag).id) }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns true if the hashtag is within limit' do
|
||||||
|
Redis.current.zadd('trending_tags', 11, tag.id)
|
||||||
|
expect(described_class.trending?(tag)).to be true
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns false if the hashtag is outside the limit' do
|
||||||
|
Redis.current.zadd('trending_tags', 0, tag.id)
|
||||||
|
expect(described_class.trending?(tag)).to be false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
67
yarn.lock
67
yarn.lock
|
@ -2,14 +2,7 @@
|
||||||
# yarn lockfile v1
|
# yarn lockfile v1
|
||||||
|
|
||||||
|
|
||||||
"@babel/code-frame@^7.0.0":
|
"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.5.5":
|
||||||
version "7.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0.tgz#06e2ab19bdb535385559aabb5ba59729482800f8"
|
|
||||||
integrity sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==
|
|
||||||
dependencies:
|
|
||||||
"@babel/highlight" "^7.0.0"
|
|
||||||
|
|
||||||
"@babel/code-frame@^7.5.5":
|
|
||||||
version "7.5.5"
|
version "7.5.5"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.5.5.tgz#bc0782f6d69f7b7d49531219699b988f669a8f9d"
|
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.5.5.tgz#bc0782f6d69f7b7d49531219699b988f669a8f9d"
|
||||||
integrity sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw==
|
integrity sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw==
|
||||||
|
@ -291,12 +284,7 @@
|
||||||
esutils "^2.0.2"
|
esutils "^2.0.2"
|
||||||
js-tokens "^4.0.0"
|
js-tokens "^4.0.0"
|
||||||
|
|
||||||
"@babel/parser@^7.0.0", "@babel/parser@^7.1.0", "@babel/parser@^7.2.2", "@babel/parser@^7.3.4", "@babel/parser@^7.4.4", "@babel/parser@^7.4.5":
|
"@babel/parser@^7.0.0", "@babel/parser@^7.1.0", "@babel/parser@^7.2.2", "@babel/parser@^7.3.4", "@babel/parser@^7.4.4", "@babel/parser@^7.4.5", "@babel/parser@^7.5.5":
|
||||||
version "7.4.5"
|
|
||||||
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.4.5.tgz#04af8d5d5a2b044a2a1bffacc1e5e6673544e872"
|
|
||||||
integrity sha512-9mUqkL1FF5T7f0WDFfAoDdiMVPWsdD1gZYzSnaXsxUCUqzuch/8of9G3VUSNiZmMBoRxT3neyVsqeiL/ZPcjew==
|
|
||||||
|
|
||||||
"@babel/parser@^7.5.5":
|
|
||||||
version "7.5.5"
|
version "7.5.5"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.5.5.tgz#02f077ac8817d3df4a832ef59de67565e71cca4b"
|
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.5.5.tgz#02f077ac8817d3df4a832ef59de67565e71cca4b"
|
||||||
integrity sha512-E5BN68cqR7dhKan1SfqgPGhQ178bkVKpXTPEXnFJBrEt8/DKRZlybmy+IgYLTeN7tp1R5Ccmbm2rBk17sHYU3g==
|
integrity sha512-E5BN68cqR7dhKan1SfqgPGhQ178bkVKpXTPEXnFJBrEt8/DKRZlybmy+IgYLTeN7tp1R5Ccmbm2rBk17sHYU3g==
|
||||||
|
@ -657,10 +645,10 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/helper-plugin-utils" "^7.0.0"
|
"@babel/helper-plugin-utils" "^7.0.0"
|
||||||
|
|
||||||
"@babel/plugin-transform-runtime@^7.4.4":
|
"@babel/plugin-transform-runtime@^7.5.5":
|
||||||
version "7.4.4"
|
version "7.5.5"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.4.4.tgz#a50f5d16e9c3a4ac18a1a9f9803c107c380bce08"
|
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.5.5.tgz#a6331afbfc59189d2135b2e09474457a8e3d28bc"
|
||||||
integrity sha512-aMVojEjPszvau3NRg+TIH14ynZLvPewH4xhlCW1w6A3rkxTS1m4uwzRclYR9oS+rl/dr+kT+pzbfHuAWP/lc7Q==
|
integrity sha512-6Xmeidsun5rkwnGfMOp6/z9nSzWpHFNVr2Jx7kwoq4mVatQfQx5S56drBgEHF+XQbKOdIaOiMIINvp/kAwMN+w==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/helper-module-imports" "^7.0.0"
|
"@babel/helper-module-imports" "^7.0.0"
|
||||||
"@babel/helper-plugin-utils" "^7.0.0"
|
"@babel/helper-plugin-utils" "^7.0.0"
|
||||||
|
@ -819,22 +807,7 @@
|
||||||
"@babel/parser" "^7.4.4"
|
"@babel/parser" "^7.4.4"
|
||||||
"@babel/types" "^7.4.4"
|
"@babel/types" "^7.4.4"
|
||||||
|
|
||||||
"@babel/traverse@^7.0.0", "@babel/traverse@^7.1.0", "@babel/traverse@^7.1.5", "@babel/traverse@^7.3.4", "@babel/traverse@^7.4.4", "@babel/traverse@^7.4.5":
|
"@babel/traverse@^7.0.0", "@babel/traverse@^7.1.0", "@babel/traverse@^7.1.5", "@babel/traverse@^7.3.4", "@babel/traverse@^7.4.4", "@babel/traverse@^7.4.5", "@babel/traverse@^7.5.5":
|
||||||
version "7.4.5"
|
|
||||||
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.4.5.tgz#4e92d1728fd2f1897dafdd321efbff92156c3216"
|
|
||||||
integrity sha512-Vc+qjynwkjRmIFGxy0KYoPj4FdVDxLej89kMHFsWScq999uX+pwcX4v9mWRjW0KcAYTPAuVQl2LKP1wEVLsp+A==
|
|
||||||
dependencies:
|
|
||||||
"@babel/code-frame" "^7.0.0"
|
|
||||||
"@babel/generator" "^7.4.4"
|
|
||||||
"@babel/helper-function-name" "^7.1.0"
|
|
||||||
"@babel/helper-split-export-declaration" "^7.4.4"
|
|
||||||
"@babel/parser" "^7.4.5"
|
|
||||||
"@babel/types" "^7.4.4"
|
|
||||||
debug "^4.1.0"
|
|
||||||
globals "^11.1.0"
|
|
||||||
lodash "^4.17.11"
|
|
||||||
|
|
||||||
"@babel/traverse@^7.5.5":
|
|
||||||
version "7.5.5"
|
version "7.5.5"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.5.5.tgz#f664f8f368ed32988cd648da9f72d5ca70f165bb"
|
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.5.5.tgz#f664f8f368ed32988cd648da9f72d5ca70f165bb"
|
||||||
integrity sha512-MqB0782whsfffYfSjH4TM+LMjrJnhCNEDMDIjeTpl+ASaUvxcjoiVCo/sM1GhS1pHOXYfWVCYneLjMckuUxDaQ==
|
integrity sha512-MqB0782whsfffYfSjH4TM+LMjrJnhCNEDMDIjeTpl+ASaUvxcjoiVCo/sM1GhS1pHOXYfWVCYneLjMckuUxDaQ==
|
||||||
|
@ -1737,17 +1710,17 @@ axobject-query@^2.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
ast-types-flow "0.0.7"
|
ast-types-flow "0.0.7"
|
||||||
|
|
||||||
babel-eslint@^10.0.2:
|
babel-eslint@^10.0.3:
|
||||||
version "10.0.2"
|
version "10.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-10.0.2.tgz#182d5ac204579ff0881684b040560fdcc1558456"
|
resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-10.0.3.tgz#81a2c669be0f205e19462fed2482d33e4687a88a"
|
||||||
integrity sha512-UdsurWPtgiPgpJ06ryUnuaSXC2s0WoSZnQmEpbAH65XZSdwowgN5MvyP7e88nW07FYXv72erVtpBkxyDVKhH1Q==
|
integrity sha512-z3U7eMY6r/3f3/JB9mTsLjyxrv0Yb1zb8PCWCLpguxfCzBIZUwy23R1t/XKewP+8mEN2Ck8Dtr4q20z6ce6SoA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/code-frame" "^7.0.0"
|
"@babel/code-frame" "^7.0.0"
|
||||||
"@babel/parser" "^7.0.0"
|
"@babel/parser" "^7.0.0"
|
||||||
"@babel/traverse" "^7.0.0"
|
"@babel/traverse" "^7.0.0"
|
||||||
"@babel/types" "^7.0.0"
|
"@babel/types" "^7.0.0"
|
||||||
eslint-scope "3.7.1"
|
|
||||||
eslint-visitor-keys "^1.0.0"
|
eslint-visitor-keys "^1.0.0"
|
||||||
|
resolve "^1.12.0"
|
||||||
|
|
||||||
babel-jest@^24.8.0:
|
babel-jest@^24.8.0:
|
||||||
version "24.8.0"
|
version "24.8.0"
|
||||||
|
@ -3816,14 +3789,6 @@ eslint-plugin-react@~7.14.3:
|
||||||
prop-types "^15.7.2"
|
prop-types "^15.7.2"
|
||||||
resolve "^1.10.1"
|
resolve "^1.10.1"
|
||||||
|
|
||||||
eslint-scope@3.7.1:
|
|
||||||
version "3.7.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-3.7.1.tgz#3d63c3edfda02e06e01a452ad88caacc7cdcb6e8"
|
|
||||||
integrity sha1-PWPD7f2gLgbgGkUq2IyqzHzctug=
|
|
||||||
dependencies:
|
|
||||||
esrecurse "^4.1.0"
|
|
||||||
estraverse "^4.1.1"
|
|
||||||
|
|
||||||
eslint-scope@^4.0.0:
|
eslint-scope@^4.0.0:
|
||||||
version "4.0.3"
|
version "4.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.3.tgz#ca03833310f6889a3264781aa82e63eb9cfe7848"
|
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.3.tgz#ca03833310f6889a3264781aa82e63eb9cfe7848"
|
||||||
|
@ -9027,10 +8992,10 @@ resolve@1.1.7:
|
||||||
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b"
|
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b"
|
||||||
integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=
|
integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=
|
||||||
|
|
||||||
resolve@^1.10.0, resolve@^1.10.1, resolve@^1.11.0, resolve@^1.3.2, resolve@^1.5.0, resolve@^1.8.1:
|
resolve@^1.10.0, resolve@^1.10.1, resolve@^1.11.0, resolve@^1.12.0, resolve@^1.3.2, resolve@^1.5.0, resolve@^1.8.1:
|
||||||
version "1.11.1"
|
version "1.12.0"
|
||||||
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.11.1.tgz#ea10d8110376982fef578df8fc30b9ac30a07a3e"
|
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.12.0.tgz#3fc644a35c84a48554609ff26ec52b66fa577df6"
|
||||||
integrity sha512-vIpgF6wfuJOZI7KKKSP+HmiKggadPQAdsp5HiC1mvqnfp0gF1vdwgBWZIdrVft9pgqoMFQN+R7BSWZiBxx+BBw==
|
integrity sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w==
|
||||||
dependencies:
|
dependencies:
|
||||||
path-parse "^1.0.6"
|
path-parse "^1.0.6"
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue