Merge branch 'master' into glitch-soc/merge-upstream
This commit is contained in:
@ -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>
|
||||
@ -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>
|
||||
|
||||
|
@ -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,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
|
||||
|
@ -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,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
|
||||
•
|
||||
= t('admin.domain_blocks.rejecting_media')
|
||||
- first_item = false
|
||||
- if instance.domain_block.reject_reports?
|
||||
- unless first_item
|
||||
•
|
||||
= t('admin.domain_blocks.rejecting_reports')
|
||||
- unless instance.domain_block.suspend?
|
||||
- if instance.domain_block.reject_media?
|
||||
- unless first_item
|
||||
•
|
||||
= t('admin.domain_blocks.rejecting_media')
|
||||
- first_item = false
|
||||
- if instance.domain_block.reject_reports?
|
||||
- unless first_item
|
||||
•
|
||||
= t('admin.domain_blocks.rejecting_reports')
|
||||
- elsif whitelist_mode?
|
||||
= t('admin.accounts.whitelisted')
|
||||
- else
|
||||
|
@ -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 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 }
|
||||
|
@ -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|
|
||||
= 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}")
|
||||
|
@ -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'))
|
||||
- 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 }/
|
||||
|
||||
|
@ -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
|
Reference in New Issue
Block a user