Merge branch 'master' into glitch-soc/merge-upstream

main
Thibaut Girka 2019-09-05 11:36:41 +02:00
commit 5088eb8388
55 changed files with 635 additions and 237 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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?
&bull; - unless first_item
= t('admin.domain_blocks.rejecting_media') &bull;
- 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?
&bull; - unless first_item
= t('admin.domain_blocks.rejecting_reports') &bull;
= t('admin.domain_blocks.rejecting_reports')
- elsif whitelist_mode? - elsif whitelist_mode?
= t('admin.accounts.whitelisted') = t('admin.accounts.whitelisted')
- else - else

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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