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

This commit is contained in:
Thibaut Girka
2019-09-05 11:36:41 +02:00
55 changed files with 635 additions and 237 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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>
@ -315,15 +315,22 @@ class MediaGallery extends React.PureComponent {
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()) {
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>

View File

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

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='/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 />

View File

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

View File

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

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])#([\S]+)/ig, '');
text = text.replace(/\s+/g, '');
text = text.replace(/(\w\S+\.\w{2,}\S*)/g, '');
const matches = text.match(rtlChars);

View File

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

View File

@ -15,6 +15,8 @@
padding: 20px;
background: lighten($ui-base-color, 4%);
border-radius: 4px;
box-sizing: border-box;
height: 100%;
}
& > a {

View File

@ -128,7 +128,7 @@
&:hover,
&:focus,
&:active {
svg path {
svg {
fill: lighten($ui-base-color, 38%);
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -191,6 +191,9 @@ class Request
end
end
socks = []
addr_by_socket = {}
addresses.each do |address|
begin
check_private_address(address)
@ -200,30 +203,45 @@ class Request
sock.setsockopt(::Socket::IPPROTO_TCP, ::Socket::TCP_NODELAY, 1)
begin
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
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
socks << sock
addr_by_socket[sock] = sockaddr
rescue => e
outer_e = e
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
raise outer_e
else

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -44,15 +44,16 @@
- if !instance.domain_block.noop?
= t("admin.domain_blocks.severity.#{instance.domain_block.severity}")
- first_item = false
- if instance.domain_block.reject_media?
- unless first_item
&bull;
= t('admin.domain_blocks.rejecting_media')
- first_item = false
- if instance.domain_block.reject_reports?
- unless first_item
&bull;
= t('admin.domain_blocks.rejecting_reports')
- unless instance.domain_block.suspend?
- if instance.domain_block.reject_media?
- unless first_item
&bull;
= t('admin.domain_blocks.rejecting_media')
- first_item = false
- if instance.domain_block.reject_reports?
- unless first_item
&bull;
= t('admin.domain_blocks.rejecting_reports')
- elsif whitelist_mode?
= t('admin.accounts.whitelisted')
- else

View File

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

View File

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

View File

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

View File

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

View File

@ -1,12 +1,18 @@
%ul.no-list
- if controller_name != 'sessions'
%li= link_to t('auth.login'), new_session_path(resource_name)
- 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_user_session_path
- if devise_mapping.registerable? && controller_name != 'registrations'
%li= link_to t('auth.register'), available_sign_up_path
- 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 }

View File

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

View File

@ -36,7 +36,10 @@
- if status.media_attachments.size > 0
%p
- 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
= link_to l(status.created_at), web_url("statuses/#{status.id}")

View File

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

View File

@ -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'))
- thumbnail = @instance_presenter.thumbnail
- description ||= strip_tags(@instance_presenter.site_short_description.presence || @instance_presenter.site_description.presence || t('about.about_mastodon_html'))
%meta{ name: 'description', content: description }/

View File

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

View File

@ -7,7 +7,7 @@
<% end %>
<%= @warning.text %>
<% unless @statuses&.empty? %>
<% if !@statuses.nil? && !@statuses.empty? %>
<%= t('user_mailer.warning.statuses') %>
<% @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