mirror of
https://framagit.org/tykayn/mastodon.git
synced 2023-08-25 08:33:12 +02:00
Merge branch 'master' into glitch-soc/merge-upstream
This commit is contained in:
commit
5088eb8388
@ -3,7 +3,7 @@ version: 2
|
||||
aliases:
|
||||
- &defaults
|
||||
docker:
|
||||
- image: circleci/ruby:2.6.0-stretch-node
|
||||
- image: circleci/ruby:2.6-stretch-node
|
||||
environment: &ruby_environment
|
||||
BUNDLE_APP_CONFIG: ./.bundle/
|
||||
DB_HOST: localhost
|
||||
@ -105,14 +105,14 @@ jobs:
|
||||
install-ruby2.5:
|
||||
<<: *defaults
|
||||
docker:
|
||||
- image: circleci/ruby:2.5.3-stretch-node
|
||||
- image: circleci/ruby:2.5-stretch-node
|
||||
environment: *ruby_environment
|
||||
<<: *install_ruby_dependencies
|
||||
|
||||
install-ruby2.4:
|
||||
<<: *defaults
|
||||
docker:
|
||||
- image: circleci/ruby:2.4.5-stretch-node
|
||||
- image: circleci/ruby:2.4-stretch-node
|
||||
environment: *ruby_environment
|
||||
<<: *install_ruby_dependencies
|
||||
|
||||
@ -134,40 +134,40 @@ jobs:
|
||||
test-ruby2.6:
|
||||
<<: *defaults
|
||||
docker:
|
||||
- image: circleci/ruby:2.6.0-stretch-node
|
||||
- image: circleci/ruby:2.6-stretch-node
|
||||
environment: *ruby_environment
|
||||
- image: circleci/postgres:10.6-alpine
|
||||
environment:
|
||||
POSTGRES_USER: root
|
||||
- image: circleci/redis:5.0.3-alpine3.8
|
||||
- image: circleci/redis:5-alpine
|
||||
<<: *test_steps
|
||||
|
||||
test-ruby2.5:
|
||||
<<: *defaults
|
||||
docker:
|
||||
- image: circleci/ruby:2.5.3-stretch-node
|
||||
- image: circleci/ruby:2.5-stretch-node
|
||||
environment: *ruby_environment
|
||||
- image: circleci/postgres:10.6-alpine
|
||||
environment:
|
||||
POSTGRES_USER: root
|
||||
- image: circleci/redis:4.0.12-alpine
|
||||
- image: circleci/redis:5-alpine
|
||||
<<: *test_steps
|
||||
|
||||
test-ruby2.4:
|
||||
<<: *defaults
|
||||
docker:
|
||||
- image: circleci/ruby:2.4.5-stretch-node
|
||||
- image: circleci/ruby:2.4-stretch-node
|
||||
environment: *ruby_environment
|
||||
- image: circleci/postgres:10.6-alpine
|
||||
environment:
|
||||
POSTGRES_USER: root
|
||||
- image: circleci/redis:4.0.12-alpine
|
||||
- image: circleci/redis:5-alpine
|
||||
<<: *test_steps
|
||||
|
||||
test-webui:
|
||||
<<: *defaults
|
||||
docker:
|
||||
- image: circleci/node:8.15.0-stretch
|
||||
- image: circleci/node:12.9-stretch
|
||||
steps:
|
||||
- *attach_workspace
|
||||
- run: ./bin/retry yarn test:jest
|
||||
|
@ -69,6 +69,7 @@ SMTP_PORT=587
|
||||
SMTP_LOGIN=
|
||||
SMTP_PASSWORD=
|
||||
SMTP_FROM_ADDRESS=notifications@example.com
|
||||
#SMTP_REPLY_TO=
|
||||
#SMTP_DOMAIN= # defaults to LOCAL_DOMAIN
|
||||
#SMTP_DELIVERY_METHOD=smtp # delivery method can also be sendmail
|
||||
#SMTP_AUTH_METHOD=plain
|
||||
|
@ -4,7 +4,7 @@ FROM ubuntu:18.04 as build-dep
|
||||
SHELL ["bash", "-c"]
|
||||
|
||||
# Install Node
|
||||
ENV NODE_VER="8.15.0"
|
||||
ENV NODE_VER="12.9.1"
|
||||
RUN echo "Etc/UTC" > /etc/localtime && \
|
||||
apt update && \
|
||||
apt -y install wget make gcc g++ python && \
|
||||
@ -17,7 +17,7 @@ RUN echo "Etc/UTC" > /etc/localtime && \
|
||||
make install
|
||||
|
||||
# Install jemalloc
|
||||
ENV JE_VER="5.1.0"
|
||||
ENV JE_VER="5.2.1"
|
||||
RUN apt update && \
|
||||
apt -y install autoconf && \
|
||||
cd ~ && \
|
||||
@ -30,7 +30,7 @@ RUN apt update && \
|
||||
make install_bin install_include install_lib
|
||||
|
||||
# Install ruby
|
||||
ENV RUBY_VER="2.6.1"
|
||||
ENV RUBY_VER="2.6.4"
|
||||
ENV CPPFLAGS="-I/opt/jemalloc/include"
|
||||
ENV LDFLAGS="-L/opt/jemalloc/lib/"
|
||||
RUN apt update && \
|
||||
|
8
Gemfile
8
Gemfile
@ -15,7 +15,7 @@ gem 'makara', '~> 0.4'
|
||||
gem 'pghero', '~> 2.3'
|
||||
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-openstack', '~> 0.3', require: false
|
||||
gem 'paperclip', '~> 6.0'
|
||||
@ -24,7 +24,7 @@ gem 'streamio-ffmpeg', '~> 3.0'
|
||||
gem 'blurhash', '~> 0.1'
|
||||
|
||||
gem 'active_model_serializers', '~> 0.10'
|
||||
gem 'addressable', '~> 2.6'
|
||||
gem 'addressable', '~> 2.7'
|
||||
gem 'bootsnap', '~> 1.4', require: false
|
||||
gem 'browser'
|
||||
gem 'charlock_holmes', '~> 0.7.6'
|
||||
@ -116,12 +116,12 @@ end
|
||||
group :test do
|
||||
gem 'capybara', '~> 3.28'
|
||||
gem 'climate_control', '~> 0.2'
|
||||
gem 'faker', '~> 2.1'
|
||||
gem 'faker', '~> 2.2'
|
||||
gem 'microformats', '~> 4.1'
|
||||
gem 'rails-controller-testing', '~> 1.0'
|
||||
gem 'rspec-sidekiq', '~> 3.0'
|
||||
gem 'simplecov', '~> 0.17', require: false
|
||||
gem 'webmock', '~> 3.6'
|
||||
gem 'webmock', '~> 3.7'
|
||||
gem 'parallel_tests', '~> 2.29'
|
||||
end
|
||||
|
||||
|
44
Gemfile.lock
44
Gemfile.lock
@ -83,9 +83,9 @@ GEM
|
||||
i18n (>= 0.7, < 2)
|
||||
minitest (~> 5.1)
|
||||
tzinfo (~> 1.1)
|
||||
addressable (2.6.0)
|
||||
public_suffix (>= 2.0.2, < 4.0)
|
||||
airbrussh (1.3.0)
|
||||
addressable (2.7.0)
|
||||
public_suffix (>= 2.0.2, < 5.0)
|
||||
airbrussh (1.3.3)
|
||||
sshkit (>= 1.6.1, != 1.7.0)
|
||||
annotate (2.7.5)
|
||||
activerecord (>= 3.2, < 7.0)
|
||||
@ -97,8 +97,8 @@ GEM
|
||||
av (0.9.0)
|
||||
cocaine (~> 0.5.3)
|
||||
aws-eventstream (1.0.3)
|
||||
aws-partitions (1.193.0)
|
||||
aws-sdk-core (3.61.1)
|
||||
aws-partitions (1.207.0)
|
||||
aws-sdk-core (3.65.1)
|
||||
aws-eventstream (~> 1.0, >= 1.0.2)
|
||||
aws-partitions (~> 1.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
@ -106,7 +106,7 @@ GEM
|
||||
aws-sdk-kms (1.24.0)
|
||||
aws-sdk-core (~> 3, >= 3.61.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-kms (~> 1)
|
||||
aws-sigv4 (~> 1.1)
|
||||
@ -122,7 +122,7 @@ GEM
|
||||
debug_inspector (>= 0.0.1)
|
||||
blurhash (0.1.3)
|
||||
ffi (~> 1.10.0)
|
||||
bootsnap (1.4.4)
|
||||
bootsnap (1.4.5)
|
||||
msgpack (~> 1.0)
|
||||
brakeman (4.6.1)
|
||||
browser (2.6.1)
|
||||
@ -134,7 +134,7 @@ GEM
|
||||
bundler (>= 1.2.0, < 3)
|
||||
thor (~> 0.18)
|
||||
byebug (11.0.0)
|
||||
capistrano (3.11.0)
|
||||
capistrano (3.11.1)
|
||||
airbrussh (>= 1.0.0)
|
||||
i18n
|
||||
rake (>= 10.0.0)
|
||||
@ -231,7 +231,7 @@ GEM
|
||||
tzinfo
|
||||
excon (0.62.0)
|
||||
fabrication (2.20.2)
|
||||
faker (2.1.2)
|
||||
faker (2.2.1)
|
||||
i18n (>= 0.8)
|
||||
faraday (0.15.0)
|
||||
multipart-post (>= 1.2, < 3)
|
||||
@ -371,14 +371,14 @@ GEM
|
||||
mini_mime (1.0.2)
|
||||
mini_portile2 (2.4.0)
|
||||
minitest (5.11.3)
|
||||
msgpack (1.2.10)
|
||||
msgpack (1.3.1)
|
||||
multi_json (1.13.1)
|
||||
multipart-post (2.0.0)
|
||||
necromancer (0.5.0)
|
||||
net-ldap (0.16.1)
|
||||
net-scp (1.2.1)
|
||||
net-ssh (>= 2.6.5)
|
||||
net-ssh (5.0.2)
|
||||
net-scp (2.0.0)
|
||||
net-ssh (>= 2.6.5, < 6.0.0)
|
||||
net-ssh (5.2.0)
|
||||
nio4r (2.4.0)
|
||||
nokogiri (1.10.4)
|
||||
mini_portile2 (~> 2.4.0)
|
||||
@ -418,7 +418,7 @@ GEM
|
||||
parallel (1.17.0)
|
||||
parallel_tests (2.29.2)
|
||||
parallel
|
||||
parser (2.6.3.0)
|
||||
parser (2.6.4.0)
|
||||
ast (~> 2.4.0)
|
||||
parslet (1.8.2)
|
||||
pastel (0.7.2)
|
||||
@ -444,7 +444,7 @@ GEM
|
||||
pry (~> 0.10)
|
||||
pry-rails (0.3.9)
|
||||
pry (>= 0.10.4)
|
||||
public_suffix (3.1.1)
|
||||
public_suffix (4.0.1)
|
||||
puma (4.1.0)
|
||||
nio4r (~> 2.0)
|
||||
pundit (2.1.0)
|
||||
@ -557,7 +557,7 @@ GEM
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 1.4.0, < 1.7)
|
||||
rubocop-rails (2.3.1)
|
||||
rubocop-rails (2.3.2)
|
||||
rack (>= 1.1)
|
||||
rubocop (>= 0.72.0)
|
||||
ruby-progressbar (1.10.1)
|
||||
@ -603,7 +603,7 @@ GEM
|
||||
actionpack (>= 4.0)
|
||||
activesupport (>= 4.0)
|
||||
sprockets (>= 3.0.0)
|
||||
sshkit (1.17.0)
|
||||
sshkit (1.20.0)
|
||||
net-scp (>= 1.1.2)
|
||||
net-ssh (>= 2.8.0)
|
||||
stackprof (0.2.12)
|
||||
@ -647,7 +647,7 @@ GEM
|
||||
uniform_notifier (1.12.1)
|
||||
warden (1.2.8)
|
||||
rack (>= 2.0.6)
|
||||
webmock (3.6.2)
|
||||
webmock (3.7.1)
|
||||
addressable (>= 2.3.6)
|
||||
crack (>= 0.3.2)
|
||||
hashdiff (>= 0.4.0, < 2.0.0)
|
||||
@ -671,9 +671,9 @@ PLATFORMS
|
||||
DEPENDENCIES
|
||||
active_model_serializers (~> 0.10)
|
||||
active_record_query_trace (~> 1.6)
|
||||
addressable (~> 2.6)
|
||||
addressable (~> 2.7)
|
||||
annotate (~> 2.7)
|
||||
aws-sdk-s3 (~> 1.46)
|
||||
aws-sdk-s3 (~> 1.48)
|
||||
better_errors (~> 2.5)
|
||||
binding_of_caller (~> 0.7)
|
||||
blurhash (~> 0.1)
|
||||
@ -701,7 +701,7 @@ DEPENDENCIES
|
||||
doorkeeper (~> 5.1)
|
||||
dotenv-rails (~> 2.7)
|
||||
fabrication (~> 2.20)
|
||||
faker (~> 2.1)
|
||||
faker (~> 2.2)
|
||||
fast_blank (~> 1.0)
|
||||
fastimage
|
||||
fog-core (<= 2.1.0)
|
||||
@ -789,7 +789,7 @@ DEPENDENCIES
|
||||
tty-prompt (~> 0.19)
|
||||
twitter-text (~> 1.14)
|
||||
tzinfo-data (~> 1.2019)
|
||||
webmock (~> 3.6)
|
||||
webmock (~> 3.7)
|
||||
webpacker (~> 4.0)
|
||||
webpush
|
||||
|
||||
|
@ -37,7 +37,8 @@ module Admin
|
||||
|
||||
def set_usage_by_domain
|
||||
@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)))
|
||||
.joins(:account)
|
||||
.group('accounts.domain')
|
||||
@ -56,7 +57,7 @@ module Admin
|
||||
scope = scope.unreviewed if filter_params[:review] == 'unreviewed'
|
||||
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.order(score: :desc)
|
||||
scope.order(max_score: :desc)
|
||||
end
|
||||
|
||||
def filter_params
|
||||
|
@ -5,19 +5,42 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController
|
||||
|
||||
before_action :set_body_classes
|
||||
before_action :set_pack
|
||||
before_action :require_unconfirmed!
|
||||
|
||||
skip_before_action :require_functional!
|
||||
|
||||
def new
|
||||
super
|
||||
|
||||
resource.email = current_user.unconfirmed_email || current_user.email if user_signed_in?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_pack
|
||||
use_pack 'auth'
|
||||
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
|
||||
@body_classes = 'lighter'
|
||||
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)
|
||||
if user.created_by_application && truthy_param?(:redirect_to_app)
|
||||
user.created_by_application.redirect_uri
|
||||
|
@ -8,4 +8,16 @@ module InstanceHelper
|
||||
def site_hostname
|
||||
@site_hostname ||= Addressable::URI.parse("//#{Rails.configuration.x.local_domain}").display_uri.host
|
||||
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
|
||||
|
@ -1,6 +1,7 @@
|
||||
// This file will be loaded on admin pages, regardless of theme.
|
||||
|
||||
import { delegate } from 'rails-ujs';
|
||||
import ready from '../mastodon/ready';
|
||||
|
||||
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 rejectReportsDiv = document.querySelector('.input.with_label.domain_block_reject_reports');
|
||||
|
||||
@ -42,4 +43,11 @@ delegate(document, '#domain_block_severity', 'change', ({ target }) => {
|
||||
if (rejectReportsDiv) {
|
||||
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>
|
||||
</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 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 className='trends__item__sparkline'>
|
||||
|
@ -159,7 +159,7 @@ class Item extends React.PureComponent {
|
||||
if (attachment.get('type') === 'unknown') {
|
||||
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}%` }}>
|
||||
<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' />
|
||||
</a>
|
||||
</div>
|
||||
@ -316,14 +316,21 @@ class MediaGallery extends React.PureComponent {
|
||||
}
|
||||
|
||||
const size = media.take(4).size;
|
||||
const uncached = media.every(attachment => attachment.get('type') === 'unknown');
|
||||
|
||||
if (this.isStandaloneEligible()) {
|
||||
children = <Item standalone onClick={this.handleClick} attachment={media.get(0)} displayWidth={width} visible={visible} />;
|
||||
} 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} />;
|
||||
} else {
|
||||
spoilerButton = (
|
||||
@ -335,7 +342,7 @@ class MediaGallery extends React.PureComponent {
|
||||
|
||||
return (
|
||||
<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}
|
||||
</div>
|
||||
|
||||
|
@ -82,6 +82,43 @@ class AccountCard extends ImmutablePureComponent {
|
||||
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 = () => {
|
||||
this.props.onFollow(this.props.account);
|
||||
}
|
||||
@ -94,6 +131,10 @@ class AccountCard extends ImmutablePureComponent {
|
||||
this.props.onMute(this.props.account);
|
||||
}
|
||||
|
||||
setRef = (c) => {
|
||||
this.node = c;
|
||||
}
|
||||
|
||||
render () {
|
||||
const { account, intl } = this.props;
|
||||
|
||||
@ -133,7 +174,7 @@ class AccountCard extends ImmutablePureComponent {
|
||||
</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>
|
||||
|
||||
|
@ -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='/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>
|
||||
{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 />
|
||||
|
||||
|
@ -8,6 +8,14 @@
|
||||
{
|
||||
"defaultMessage": "An unexpected error occurred.",
|
||||
"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"
|
||||
@ -191,6 +199,10 @@
|
||||
"defaultMessage": "Toggle visibility",
|
||||
"id": "media_gallery.toggle_visible"
|
||||
},
|
||||
{
|
||||
"defaultMessage": "Not available",
|
||||
"id": "status.uncached_media_warning"
|
||||
},
|
||||
{
|
||||
"defaultMessage": "Sensitive content",
|
||||
"id": "status.sensitive_warning"
|
||||
@ -1130,6 +1142,19 @@
|
||||
],
|
||||
"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": [
|
||||
{
|
||||
@ -1218,6 +1243,14 @@
|
||||
{
|
||||
"defaultMessage": "Compose new toot",
|
||||
"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"
|
||||
@ -1235,6 +1268,76 @@
|
||||
],
|
||||
"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": [
|
||||
{
|
||||
@ -2325,6 +2428,14 @@
|
||||
},
|
||||
{
|
||||
"descriptors": [
|
||||
{
|
||||
"defaultMessage": "Are you sure you want to log out?",
|
||||
"id": "confirmations.logout.message"
|
||||
},
|
||||
{
|
||||
"defaultMessage": "Log out",
|
||||
"id": "confirmations.logout.confirm"
|
||||
},
|
||||
{
|
||||
"defaultMessage": "Invite people",
|
||||
"id": "getting_started.invite"
|
||||
@ -2440,6 +2551,10 @@
|
||||
"defaultMessage": "Lists",
|
||||
"id": "navigation_bar.lists"
|
||||
},
|
||||
{
|
||||
"defaultMessage": "Profile directory",
|
||||
"id": "getting_started.directory"
|
||||
},
|
||||
{
|
||||
"defaultMessage": "Preferences",
|
||||
"id": "navigation_bar.preferences"
|
||||
@ -2447,10 +2562,6 @@
|
||||
{
|
||||
"defaultMessage": "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"
|
||||
|
@ -16,6 +16,7 @@
|
||||
"account.follows.empty": "This user doesn't follow anyone yet.",
|
||||
"account.follows_you": "Follows you",
|
||||
"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.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.",
|
||||
"account.media": "Media",
|
||||
@ -24,6 +25,7 @@
|
||||
"account.mute": "Mute @{name}",
|
||||
"account.mute_notifications": "Mute notifications from @{name}",
|
||||
"account.muted": "Muted",
|
||||
"account.never_active": "Never",
|
||||
"account.posts": "Toots",
|
||||
"account.posts_with_replies": "Toots and replies",
|
||||
"account.report": "Report @{name}",
|
||||
@ -36,6 +38,8 @@
|
||||
"account.unfollow": "Unfollow",
|
||||
"account.unmute": "Unmute @{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.title": "Oops!",
|
||||
"autosuggest_hashtag.per_week": "{count} per week",
|
||||
@ -49,6 +53,7 @@
|
||||
"column.blocks": "Blocked users",
|
||||
"column.community": "Local timeline",
|
||||
"column.direct": "Direct messages",
|
||||
"column.directory": "Browse profiles",
|
||||
"column.domain_blocks": "Hidden domains",
|
||||
"column.favourites": "Favourites",
|
||||
"column.follow_requests": "Follow requests",
|
||||
@ -99,6 +104,8 @@
|
||||
"confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
|
||||
"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.logout.confirm": "Log out",
|
||||
"confirmations.logout.message": "Are you sure you want to log out?",
|
||||
"confirmations.mute.confirm": "Mute",
|
||||
"confirmations.mute.message": "Are you sure you want to mute {name}?",
|
||||
"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.unfollow.confirm": "Unfollow",
|
||||
"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.preview": "Here is what it will look like:",
|
||||
"emoji_button.activity": "Activity",
|
||||
@ -254,7 +265,6 @@
|
||||
"navigation_bar.personal": "Personal",
|
||||
"navigation_bar.pins": "Pinned toots",
|
||||
"navigation_bar.preferences": "Preferences",
|
||||
"navigation_bar.profile_directory": "Profile directory",
|
||||
"navigation_bar.public_timeline": "Federated timeline",
|
||||
"navigation_bar.security": "Security",
|
||||
"notification.favourite": "{name} favourited your status",
|
||||
@ -361,6 +371,7 @@
|
||||
"status.show_more": "Show more",
|
||||
"status.show_more_all": "Show more for all",
|
||||
"status.show_thread": "Show thread",
|
||||
"status.uncached_media_warning": "Not available",
|
||||
"status.unmute_conversation": "Unmute conversation",
|
||||
"status.unpin": "Unpin from profile",
|
||||
"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])#([\S]+)/ig, '');
|
||||
text = text.replace(/\s+/g, '');
|
||||
text = text.replace(/(\w\S+\.\w{2,}\S*)/g, '');
|
||||
|
||||
const matches = text.match(rtlChars);
|
||||
|
||||
|
@ -507,6 +507,7 @@
|
||||
flex: 1 1 auto;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
strong {
|
||||
@ -515,8 +516,10 @@
|
||||
|
||||
&__uses {
|
||||
flex: 0 0 auto;
|
||||
width: 80px;
|
||||
text-align: right;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
@ -3449,6 +3452,10 @@ a.status-card.compact:hover {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
&--click-thru {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&--hidden {
|
||||
display: none;
|
||||
}
|
||||
@ -3477,6 +3484,12 @@ a.status-card.compact:hover {
|
||||
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;
|
||||
background: lighten($ui-base-color, 4%);
|
||||
border-radius: 4px;
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
& > a {
|
||||
|
@ -128,7 +128,7 @@
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
svg path {
|
||||
svg {
|
||||
fill: lighten($ui-base-color, 38%);
|
||||
}
|
||||
}
|
||||
|
@ -112,6 +112,15 @@ code {
|
||||
padding: 0.2em 0.4em;
|
||||
background: darken($ui-base-color, 12%);
|
||||
}
|
||||
|
||||
li {
|
||||
list-style: disc;
|
||||
margin-left: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
ul.hint {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
span.hint {
|
||||
|
@ -32,22 +32,23 @@ class ActivityPub::Adapter < ActiveModelSerializers::Adapter::Base
|
||||
end
|
||||
|
||||
def serializable_hash(options = nil)
|
||||
named_contexts = {}
|
||||
context_extensions = {}
|
||||
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 = 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
|
||||
|
||||
private
|
||||
|
||||
def serialized_context
|
||||
def serialized_context(named_contexts_map, context_extensions_map)
|
||||
context_array = []
|
||||
|
||||
serializer_options = serializer.send(:instance_options) || {}
|
||||
named_contexts = [:activitystreams] + serializer._named_contexts.keys + serializer_options.fetch(:named_contexts, {}).keys
|
||||
context_extensions = serializer._context_extensions.keys + serializer_options.fetch(:context_extensions, {}).keys
|
||||
named_contexts = [:activitystreams] + named_contexts_map.keys
|
||||
context_extensions = context_extensions_map.keys
|
||||
|
||||
named_contexts.each do |key|
|
||||
context_array << NAMED_CONTEXT_MAP[key]
|
||||
|
@ -27,4 +27,12 @@ class ActivityPub::Serializer < ActiveModel::Serializer
|
||||
_context_extensions[extension_name] = true
|
||||
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
|
||||
|
@ -78,7 +78,7 @@ class FeedManager
|
||||
reblog_key = key(type, account_id, 'reblogs')
|
||||
|
||||
# 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
|
||||
# tracking anything after it for deduplication purposes.
|
||||
|
@ -191,6 +191,9 @@ class Request
|
||||
end
|
||||
end
|
||||
|
||||
socks = []
|
||||
addr_by_socket = {}
|
||||
|
||||
addresses.each do |address|
|
||||
begin
|
||||
check_private_address(address)
|
||||
@ -200,27 +203,42 @@ class Request
|
||||
|
||||
sock.setsockopt(::Socket::IPPROTO_TCP, ::Socket::TCP_NODELAY, 1)
|
||||
|
||||
begin
|
||||
sock.connect_nonblock(sockaddr)
|
||||
|
||||
# If that hasn't raised an exception, we somehow managed to connect
|
||||
# immediately, close pending sockets and return immediately
|
||||
socks.each(&:close)
|
||||
return sock
|
||||
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"
|
||||
socks << sock
|
||||
addr_by_socket[sock] = sockaddr
|
||||
rescue => e
|
||||
outer_e = e
|
||||
end
|
||||
end
|
||||
|
||||
return sock
|
||||
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
|
||||
|
||||
|
@ -7,14 +7,14 @@
|
||||
# name :string default(""), not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# score :integer
|
||||
# usable :boolean
|
||||
# trendable :boolean
|
||||
# listable :boolean
|
||||
# reviewed_at :datetime
|
||||
# requested_review_at :datetime
|
||||
# last_status_at :datetime
|
||||
# last_trend_at :datetime
|
||||
# max_score :float
|
||||
# max_score_at :datetime
|
||||
#
|
||||
|
||||
class Tag < ApplicationRecord
|
||||
|
@ -7,6 +7,8 @@ class TrendingTags
|
||||
THRESHOLD = 5
|
||||
LIMIT = 10
|
||||
REVIEW_THRESHOLD = 3
|
||||
MAX_SCORE_COOLDOWN = 3.days.freeze
|
||||
MAX_SCORE_HALFLIFE = 6.hours.freeze
|
||||
|
||||
class << self
|
||||
include Redisable
|
||||
@ -16,14 +18,75 @@ class TrendingTags
|
||||
|
||||
increment_historical_use!(tag.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_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
|
||||
|
||||
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 = tags.where(trendable: true) if filtered
|
||||
@ -33,8 +96,8 @@ class TrendingTags
|
||||
end
|
||||
|
||||
def trending?(tag)
|
||||
rank = redis.zrevrank("#{KEY}:#{Time.now.utc.beginning_of_day.to_i}", tag.id)
|
||||
rank.present? && rank <= LIMIT
|
||||
rank = redis.zrevrank(KEY, tag.id)
|
||||
rank.present? && rank < LIMIT
|
||||
end
|
||||
|
||||
private
|
||||
@ -51,31 +114,10 @@ class TrendingTags
|
||||
redis.expire(key, EXPIRE_HISTORY_AFTER)
|
||||
end
|
||||
|
||||
def increment_vote!(tag, at_time)
|
||||
key = "#{KEY}:#{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
|
||||
expected = 1.0 if expected.zero?
|
||||
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? }
|
||||
def increment_use!(tag_id, at_time)
|
||||
key = "#{KEY}:used:#{at_time.beginning_of_day.to_i}"
|
||||
redis.sadd(key, tag_id)
|
||||
redis.expire(key, EXPIRE_HISTORY_AFTER)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -6,7 +6,7 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer
|
||||
context :security
|
||||
|
||||
context_extensions :manually_approves_followers, :featured, :also_known_as,
|
||||
:moved_to, :property_value, :hashtag, :emoji, :identity_proof,
|
||||
:moved_to, :property_value, :identity_proof,
|
||||
:discoverable
|
||||
|
||||
attributes :id, :type, :following, :followers,
|
||||
@ -138,6 +138,8 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer
|
||||
end
|
||||
|
||||
class TagSerializer < ActivityPub::Serializer
|
||||
context_extensions :hashtag
|
||||
|
||||
include RoutingHelper
|
||||
|
||||
attributes :type, :href, :name
|
||||
|
@ -1,8 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ActivityPub::NoteSerializer < ActivityPub::Serializer
|
||||
context_extensions :atom_uri, :conversation, :sensitive,
|
||||
:hashtag, :emoji, :focal_point, :blurhash
|
||||
context_extensions :atom_uri, :conversation, :sensitive
|
||||
|
||||
attributes :id, :type, :summary,
|
||||
:in_reply_to, :published, :url,
|
||||
@ -152,6 +151,8 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer
|
||||
end
|
||||
|
||||
class MediaAttachmentSerializer < ActivityPub::Serializer
|
||||
context_extensions :blurhash, :focal_point
|
||||
|
||||
include RoutingHelper
|
||||
|
||||
attributes :type, :media_type, :url, :name, :blurhash
|
||||
@ -199,6 +200,8 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer
|
||||
end
|
||||
|
||||
class TagSerializer < ActivityPub::Serializer
|
||||
context_extensions :hashtag
|
||||
|
||||
include RoutingHelper
|
||||
|
||||
attributes :type, :href, :name
|
||||
|
@ -61,6 +61,7 @@ class SuspendAccountService < BaseService
|
||||
return if !@account.local? || @account.user.nil?
|
||||
|
||||
if @options[:including_user]
|
||||
@options[:destroy] = true if !@account.user_confirmed? || @account.user_pending?
|
||||
@account.user.destroy
|
||||
else
|
||||
@account.user.disable!
|
||||
|
@ -44,6 +44,7 @@
|
||||
- if !instance.domain_block.noop?
|
||||
= t("admin.domain_blocks.severity.#{instance.domain_block.severity}")
|
||||
- first_item = false
|
||||
- unless instance.domain_block.suspend?
|
||||
- if instance.domain_block.reject_media?
|
||||
- unless first_item
|
||||
•
|
||||
|
@ -38,8 +38,10 @@
|
||||
.table-wrapper
|
||||
%table.table
|
||||
%tbody
|
||||
- total = @usage_by_domain.sum(&:last).to_f
|
||||
|
||||
- @usage_by_domain.each do |(domain, count)|
|
||||
%tr
|
||||
%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
|
||||
|
@ -5,7 +5,7 @@
|
||||
.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)
|
||||
|
||||
- if Setting.trends
|
||||
- if Setting.trends && !(user_signed_in? && !current_user.setting_trends)
|
||||
- trends = TrendingTags.get(3)
|
||||
|
||||
- unless trends.empty?
|
||||
|
@ -2,7 +2,7 @@
|
||||
= t('auth.register')
|
||||
|
||||
- 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|
|
||||
= render 'shared/error_messages', object: resource
|
||||
|
@ -17,7 +17,4 @@
|
||||
.simple_form
|
||||
%p.hint= t('auth.setup.email_settings_hint_html', email: content_tag(:strong, @user.email))
|
||||
|
||||
.form-footer
|
||||
%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 }
|
||||
.form-footer= render 'auth/shared/links'
|
||||
|
@ -1,12 +1,18 @@
|
||||
%ul.no-list
|
||||
- if user_signed_in?
|
||||
%li= link_to t('settings.account_settings'), edit_user_registration_path
|
||||
- else
|
||||
- if controller_name != 'sessions'
|
||||
%li= link_to t('auth.login'), new_session_path(resource_name)
|
||||
%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
|
||||
|
||||
- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations'
|
||||
%li= link_to t('auth.forgot_password'), new_password_path(resource_name)
|
||||
- if controller_name != 'passwords' && controller_name != 'registrations'
|
||||
%li= link_to t('auth.forgot_password'), new_user_password_path
|
||||
|
||||
- if devise_mapping.confirmable? && controller_name != 'confirmations'
|
||||
%li= link_to t('auth.didnt_get_confirmation'), new_confirmation_path(resource_name)
|
||||
- if controller_name != 'confirmations'
|
||||
%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?
|
||||
%time.time-ago{ datetime: account.last_status_at.iso8601, title: l(account.last_status_at) }= l account.last_status_at
|
||||
- else
|
||||
= t('invites.expires_in_prompt')
|
||||
= t('accounts.never_active')
|
||||
|
||||
%small= t('accounts.last_active')
|
||||
|
||||
|
@ -36,7 +36,10 @@
|
||||
- if status.media_attachments.size > 0
|
||||
%p
|
||||
- status.media_attachments.each do |a|
|
||||
- if status.local?
|
||||
= link_to medium_url(a), medium_url(a)
|
||||
- else
|
||||
= link_to a.remote_url, a.remote_url
|
||||
|
||||
%p.status-footer
|
||||
= link_to l(status.created_at), web_url("statuses/#{status.id}")
|
||||
|
@ -2,15 +2,25 @@
|
||||
= t('settings.delete')
|
||||
|
||||
= simple_form_for @confirmation, url: settings_delete_path, method: :delete do |f|
|
||||
.warning
|
||||
%strong
|
||||
= fa_icon('warning')
|
||||
= t('deletes.warning_title')
|
||||
= t('deletes.warning_html')
|
||||
%p.hint= t('deletes.warning.before')
|
||||
|
||||
%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
|
||||
= f.button :button, t('deletes.proceed'), type: :submit, class: 'negative'
|
||||
|
@ -1,5 +1,5 @@
|
||||
- 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 }/
|
||||
|
||||
|
@ -42,11 +42,11 @@
|
||||
- unless @warning.text.blank?
|
||||
= Formatter.instance.linkify(@warning.text)
|
||||
|
||||
- unless @statuses&.empty?
|
||||
- if !@statuses.nil? && !@statuses.empty?
|
||||
%p
|
||||
%strong= t('user_mailer.warning.statuses')
|
||||
|
||||
- unless @statuses&.empty?
|
||||
- if !@statuses.nil? && !@statuses.empty?
|
||||
- @statuses.each_with_index do |status, i|
|
||||
= render 'notification_mailer/status', status: status, i: i + 1, highlighted: true
|
||||
|
||||
|
@ -7,7 +7,7 @@
|
||||
|
||||
<% end %>
|
||||
<%= @warning.text %>
|
||||
<% unless @statuses&.empty? %>
|
||||
<% if !@statuses.nil? && !@statuses.empty? %>
|
||||
<%= t('user_mailer.warning.statuses') %>
|
||||
|
||||
<% @statuses.each do |status| %>
|
||||
|
11
app/workers/scheduler/trending_tags_scheduler.rb
Normal file
11
app/workers/scheduler/trending_tags_scheduler.rb
Normal 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
|
@ -1,6 +1,6 @@
|
||||
# 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 :branch, ENV.fetch('BRANCH', 'master')
|
||||
|
@ -83,7 +83,10 @@ Rails.application.configure do
|
||||
config.action_mailer.perform_caching = false
|
||||
|
||||
# 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 = {
|
||||
:port => ENV['SMTP_PORT'],
|
||||
|
@ -3,22 +3,3 @@ ActiveModelSerializers.config.tap do |config|
|
||||
end
|
||||
|
||||
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
|
||||
moved_html: "%{name} has moved to %{new_profile_link}:"
|
||||
network_hidden: This information is not available
|
||||
never_active: Never
|
||||
nothing_here: There is nothing here!
|
||||
people_followed_by: People whom %{name} follows
|
||||
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>
|
||||
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.
|
||||
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?
|
||||
forgot_password: Forgot your password?
|
||||
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_seconds: "%{count}s"
|
||||
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
|
||||
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
|
||||
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_title: Disseminated content availability
|
||||
warning:
|
||||
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:
|
||||
directory: Profile directory
|
||||
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 threads_count, threads_count
|
||||
|
||||
|
@ -9,6 +9,9 @@
|
||||
scheduled_statuses_scheduler:
|
||||
every: '5m'
|
||||
class: Scheduler::ScheduledStatusesScheduler
|
||||
trending_tags_scheduler:
|
||||
every: '5m'
|
||||
class: Scheduler::TrendingTagsScheduler
|
||||
media_cleanup_scheduler:
|
||||
cron: '<%= Random.rand(0..59) %> <%= Random.rand(3..5) %> * * *'
|
||||
class: Scheduler::MediaCleanupScheduler
|
||||
|
6
db/migrate/20190901035623_add_max_score_to_tags.rb
Normal file
6
db/migrate/20190901035623_add_max_score_to_tags.rb
Normal 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
|
12
db/post_migrate/20190901040524_remove_score_from_tags.rb
Normal file
12
db/post_migrate/20190901040524_remove_score_from_tags.rb
Normal 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
|
@ -10,7 +10,7 @@
|
||||
#
|
||||
# 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
|
||||
enable_extension "plpgsql"
|
||||
@ -677,14 +677,14 @@ ActiveRecord::Schema.define(version: 2019_08_23_221802) do
|
||||
t.string "name", default: "", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.integer "score"
|
||||
t.boolean "usable"
|
||||
t.boolean "trendable"
|
||||
t.boolean "listable"
|
||||
t.datetime "reviewed_at"
|
||||
t.datetime "requested_review_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
|
||||
end
|
||||
|
||||
|
@ -68,7 +68,7 @@
|
||||
"@babel/plugin-transform-react-inline-elements": "^7.2.0",
|
||||
"@babel/plugin-transform-react-jsx-self": "^7.2.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-react": "^7.0.0",
|
||||
"@babel/runtime": "^7.5.4",
|
||||
@ -172,7 +172,7 @@
|
||||
"websocket.js": "^0.1.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-eslint": "^10.0.2",
|
||||
"babel-eslint": "^10.0.3",
|
||||
"babel-jest": "^24.8.0",
|
||||
"enzyme": "^3.10.0",
|
||||
"enzyme-adapter-react-16": "^1.14.0",
|
||||
|
@ -19,7 +19,7 @@ RSpec.describe ActivityPub::Activity::Update do
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
let(:json) do
|
||||
|
68
spec/models/trending_tags_spec.rb
Normal file
68
spec/models/trending_tags_spec.rb
Normal 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
|
67
yarn.lock
67
yarn.lock
@ -2,14 +2,7 @@
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@babel/code-frame@^7.0.0":
|
||||
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":
|
||||
"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.5.5":
|
||||
version "7.5.5"
|
||||
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.5.5.tgz#bc0782f6d69f7b7d49531219699b988f669a8f9d"
|
||||
integrity sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw==
|
||||
@ -291,12 +284,7 @@
|
||||
esutils "^2.0.2"
|
||||
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":
|
||||
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":
|
||||
"@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.5.5"
|
||||
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.5.5.tgz#02f077ac8817d3df4a832ef59de67565e71cca4b"
|
||||
integrity sha512-E5BN68cqR7dhKan1SfqgPGhQ178bkVKpXTPEXnFJBrEt8/DKRZlybmy+IgYLTeN7tp1R5Ccmbm2rBk17sHYU3g==
|
||||
@ -657,10 +645,10 @@
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils" "^7.0.0"
|
||||
|
||||
"@babel/plugin-transform-runtime@^7.4.4":
|
||||
version "7.4.4"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.4.4.tgz#a50f5d16e9c3a4ac18a1a9f9803c107c380bce08"
|
||||
integrity sha512-aMVojEjPszvau3NRg+TIH14ynZLvPewH4xhlCW1w6A3rkxTS1m4uwzRclYR9oS+rl/dr+kT+pzbfHuAWP/lc7Q==
|
||||
"@babel/plugin-transform-runtime@^7.5.5":
|
||||
version "7.5.5"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.5.5.tgz#a6331afbfc59189d2135b2e09474457a8e3d28bc"
|
||||
integrity sha512-6Xmeidsun5rkwnGfMOp6/z9nSzWpHFNVr2Jx7kwoq4mVatQfQx5S56drBgEHF+XQbKOdIaOiMIINvp/kAwMN+w==
|
||||
dependencies:
|
||||
"@babel/helper-module-imports" "^7.0.0"
|
||||
"@babel/helper-plugin-utils" "^7.0.0"
|
||||
@ -819,22 +807,7 @@
|
||||
"@babel/parser" "^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":
|
||||
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":
|
||||
"@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.5.5"
|
||||
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.5.5.tgz#f664f8f368ed32988cd648da9f72d5ca70f165bb"
|
||||
integrity sha512-MqB0782whsfffYfSjH4TM+LMjrJnhCNEDMDIjeTpl+ASaUvxcjoiVCo/sM1GhS1pHOXYfWVCYneLjMckuUxDaQ==
|
||||
@ -1737,17 +1710,17 @@ axobject-query@^2.0.2:
|
||||
dependencies:
|
||||
ast-types-flow "0.0.7"
|
||||
|
||||
babel-eslint@^10.0.2:
|
||||
version "10.0.2"
|
||||
resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-10.0.2.tgz#182d5ac204579ff0881684b040560fdcc1558456"
|
||||
integrity sha512-UdsurWPtgiPgpJ06ryUnuaSXC2s0WoSZnQmEpbAH65XZSdwowgN5MvyP7e88nW07FYXv72erVtpBkxyDVKhH1Q==
|
||||
babel-eslint@^10.0.3:
|
||||
version "10.0.3"
|
||||
resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-10.0.3.tgz#81a2c669be0f205e19462fed2482d33e4687a88a"
|
||||
integrity sha512-z3U7eMY6r/3f3/JB9mTsLjyxrv0Yb1zb8PCWCLpguxfCzBIZUwy23R1t/XKewP+8mEN2Ck8Dtr4q20z6ce6SoA==
|
||||
dependencies:
|
||||
"@babel/code-frame" "^7.0.0"
|
||||
"@babel/parser" "^7.0.0"
|
||||
"@babel/traverse" "^7.0.0"
|
||||
"@babel/types" "^7.0.0"
|
||||
eslint-scope "3.7.1"
|
||||
eslint-visitor-keys "^1.0.0"
|
||||
resolve "^1.12.0"
|
||||
|
||||
babel-jest@^24.8.0:
|
||||
version "24.8.0"
|
||||
@ -3816,14 +3789,6 @@ eslint-plugin-react@~7.14.3:
|
||||
prop-types "^15.7.2"
|
||||
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:
|
||||
version "4.0.3"
|
||||
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"
|
||||
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:
|
||||
version "1.11.1"
|
||||
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.11.1.tgz#ea10d8110376982fef578df8fc30b9ac30a07a3e"
|
||||
integrity sha512-vIpgF6wfuJOZI7KKKSP+HmiKggadPQAdsp5HiC1mvqnfp0gF1vdwgBWZIdrVft9pgqoMFQN+R7BSWZiBxx+BBw==
|
||||
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.12.0"
|
||||
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.12.0.tgz#3fc644a35c84a48554609ff26ec52b66fa577df6"
|
||||
integrity sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w==
|
||||
dependencies:
|
||||
path-parse "^1.0.6"
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user