Merge branch 'master' into glitch-soc/merge-upstream
Conflicts: - README.md - app/helpers/statuses_helper.rb Upstream moved account helpers to their own file, we had extra helpers there, moved too. - app/lib/sanitize_config.rb - app/models/user.rb - app/serializers/initial_state_serializer.rb - config/locales/simple_form.en.yml - spec/lib/sanitize_config_spec.rb
This commit is contained in:
@@ -7,6 +7,7 @@ class ActivityPub::InboxesController < ActivityPub::BaseController
|
||||
|
||||
before_action :skip_unknown_actor_delete
|
||||
before_action :require_signature!
|
||||
skip_before_action :authenticate_user!
|
||||
|
||||
def create
|
||||
upgrade_account
|
||||
|
||||
@@ -60,6 +60,7 @@ class Settings::PreferencesController < Settings::BaseController
|
||||
:setting_use_blurhash,
|
||||
:setting_use_pending_items,
|
||||
:setting_trends,
|
||||
:setting_crop_images,
|
||||
notification_emails: %i(follow follow_request reblog favourite mention digest report pending_account trending_tag),
|
||||
interactions: %i(must_be_follower must_be_following must_be_following_dm)
|
||||
)
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module AccountsHelper
|
||||
def display_name(account, **options)
|
||||
if options[:custom_emojify]
|
||||
Formatter.instance.format_display_name(account, options)
|
||||
else
|
||||
account.display_name.presence || account.username
|
||||
end
|
||||
end
|
||||
|
||||
def acct(account)
|
||||
if account.local?
|
||||
"@#{account.acct}@#{Rails.configuration.x.local_domain}"
|
||||
else
|
||||
"@#{account.acct}"
|
||||
end
|
||||
end
|
||||
|
||||
def account_action_button(account)
|
||||
if user_signed_in?
|
||||
if account.id == current_user.account_id
|
||||
link_to settings_profile_url, class: 'button logo-button' do
|
||||
safe_join([svg_logo, t('settings.edit_profile')])
|
||||
end
|
||||
elsif current_account.following?(account) || current_account.requested?(account)
|
||||
link_to account_unfollow_path(account), class: 'button logo-button button--destructive', data: { method: :post } do
|
||||
safe_join([svg_logo, t('accounts.unfollow')])
|
||||
end
|
||||
elsif !(account.memorial? || account.moved?)
|
||||
link_to account_follow_path(account), class: "button logo-button#{account.blocking?(current_account) ? ' disabled' : ''}", data: { method: :post } do
|
||||
safe_join([svg_logo, t('accounts.follow')])
|
||||
end
|
||||
end
|
||||
elsif !(account.memorial? || account.moved?)
|
||||
link_to account_remote_follow_path(account), class: 'button logo-button modal-button', target: '_new' do
|
||||
safe_join([svg_logo, t('accounts.follow')])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def minimal_account_action_button(account)
|
||||
if user_signed_in?
|
||||
return if account.id == current_user.account_id
|
||||
|
||||
if current_account.following?(account) || current_account.requested?(account)
|
||||
link_to account_unfollow_path(account), class: 'icon-button active', data: { method: :post }, title: t('accounts.unfollow') do
|
||||
fa_icon('user-times fw')
|
||||
end
|
||||
elsif !(account.memorial? || account.moved?)
|
||||
link_to account_follow_path(account), class: "icon-button#{account.blocking?(current_account) ? ' disabled' : ''}", data: { method: :post }, title: t('accounts.follow') do
|
||||
fa_icon('user-plus fw')
|
||||
end
|
||||
end
|
||||
elsif !(account.memorial? || account.moved?)
|
||||
link_to account_remote_follow_path(account), class: 'icon-button modal-button', target: '_new', title: t('accounts.follow') do
|
||||
fa_icon('user-plus fw')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def account_badge(account, all: false)
|
||||
if account.bot?
|
||||
content_tag(:div, content_tag(:div, t('accounts.roles.bot'), class: 'account-role bot'), class: 'roles')
|
||||
elsif (Setting.show_staff_badge && account.user_staff?) || all
|
||||
content_tag(:div, class: 'roles') do
|
||||
if all && !account.user_staff?
|
||||
content_tag(:div, t('admin.accounts.roles.user'), class: 'account-role')
|
||||
elsif account.user_admin?
|
||||
content_tag(:div, t('accounts.roles.admin'), class: 'account-role admin')
|
||||
elsif account.user_moderator?
|
||||
content_tag(:div, t('accounts.roles.moderator'), class: 'account-role moderator')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def hide_followers_count?(account)
|
||||
Setting.hide_followers_count || account.user&.setting_hide_followers_count
|
||||
end
|
||||
|
||||
def account_description(account)
|
||||
prepend_stats = [
|
||||
[
|
||||
number_to_human(account.statuses_count, strip_insignificant_zeros: true),
|
||||
I18n.t('accounts.posts', count: account.statuses_count),
|
||||
].join(' '),
|
||||
|
||||
[
|
||||
number_to_human(account.following_count, strip_insignificant_zeros: true),
|
||||
I18n.t('accounts.following', count: account.following_count),
|
||||
].join(' '),
|
||||
]
|
||||
|
||||
unless hide_followers_count?(account)
|
||||
prepend_stats << [
|
||||
number_to_human(account.followers_count, strip_insignificant_zeros: true),
|
||||
I18n.t('accounts.followers', count: account.followers_count),
|
||||
].join(' ')
|
||||
end
|
||||
|
||||
[prepend_stats.join(', '), account.note].join(' · ')
|
||||
end
|
||||
|
||||
def svg_logo
|
||||
content_tag(:svg, tag(:use, 'xlink:href' => '#mastodon-svg-logo'), 'viewBox' => '0 0 216.4144 232.00976')
|
||||
end
|
||||
|
||||
def svg_logo_full
|
||||
content_tag(:svg, tag(:use, 'xlink:href' => '#mastodon-svg-logo-full'), 'viewBox' => '0 0 713.35878 175.8678')
|
||||
end
|
||||
end
|
||||
@@ -4,80 +4,6 @@ module StatusesHelper
|
||||
EMBEDDED_CONTROLLER = 'statuses'
|
||||
EMBEDDED_ACTION = 'embed'
|
||||
|
||||
def display_name(account, **options)
|
||||
if options[:custom_emojify]
|
||||
Formatter.instance.format_display_name(account, options)
|
||||
else
|
||||
account.display_name.presence || account.username
|
||||
end
|
||||
end
|
||||
|
||||
def account_action_button(account)
|
||||
if user_signed_in?
|
||||
if account.id == current_user.account_id
|
||||
link_to settings_profile_url, class: 'button logo-button' do
|
||||
safe_join([svg_logo, t('settings.edit_profile')])
|
||||
end
|
||||
elsif current_account.following?(account) || current_account.requested?(account)
|
||||
link_to account_unfollow_path(account), class: 'button logo-button button--destructive', data: { method: :post } do
|
||||
safe_join([svg_logo, t('accounts.unfollow')])
|
||||
end
|
||||
elsif !(account.memorial? || account.moved?)
|
||||
link_to account_follow_path(account), class: "button logo-button#{account.blocking?(current_account) ? ' disabled' : ''}", data: { method: :post } do
|
||||
safe_join([svg_logo, t('accounts.follow')])
|
||||
end
|
||||
end
|
||||
elsif !(account.memorial? || account.moved?)
|
||||
link_to account_remote_follow_path(account), class: 'button logo-button modal-button', target: '_new' do
|
||||
safe_join([svg_logo, t('accounts.follow')])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def minimal_account_action_button(account)
|
||||
if user_signed_in?
|
||||
return if account.id == current_user.account_id
|
||||
|
||||
if current_account.following?(account) || current_account.requested?(account)
|
||||
link_to account_unfollow_path(account), class: 'icon-button active', data: { method: :post }, title: t('accounts.unfollow') do
|
||||
fa_icon('user-times fw')
|
||||
end
|
||||
elsif !(account.memorial? || account.moved?)
|
||||
link_to account_follow_path(account), class: "icon-button#{account.blocking?(current_account) ? ' disabled' : ''}", data: { method: :post }, title: t('accounts.follow') do
|
||||
fa_icon('user-plus fw')
|
||||
end
|
||||
end
|
||||
elsif !(account.memorial? || account.moved?)
|
||||
link_to account_remote_follow_path(account), class: 'icon-button modal-button', target: '_new', title: t('accounts.follow') do
|
||||
fa_icon('user-plus fw')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def svg_logo
|
||||
content_tag(:svg, tag(:use, 'xlink:href' => '#mastodon-svg-logo'), 'viewBox' => '0 0 216.4144 232.00976')
|
||||
end
|
||||
|
||||
def svg_logo_full
|
||||
content_tag(:svg, tag(:use, 'xlink:href' => '#mastodon-svg-logo-full'), 'viewBox' => '0 0 713.35878 175.8678')
|
||||
end
|
||||
|
||||
def account_badge(account, all: false)
|
||||
if account.bot?
|
||||
content_tag(:div, content_tag(:div, t('accounts.roles.bot'), class: 'account-role bot'), class: 'roles')
|
||||
elsif (Setting.show_staff_badge && account.user_staff?) || all
|
||||
content_tag(:div, class: 'roles') do
|
||||
if all && !account.user_staff?
|
||||
content_tag(:div, t('admin.accounts.roles.user'), class: 'account-role')
|
||||
elsif account.user_admin?
|
||||
content_tag(:div, t('accounts.roles.admin'), class: 'account-role admin')
|
||||
elsif account.user_moderator?
|
||||
content_tag(:div, t('accounts.roles.moderator'), class: 'account-role moderator')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def link_to_more(url)
|
||||
link_to t('statuses.show_more'), url, class: 'load-more load-gap'
|
||||
end
|
||||
@@ -88,33 +14,6 @@ module StatusesHelper
|
||||
end
|
||||
end
|
||||
|
||||
def hide_followers_count?(account)
|
||||
Setting.hide_followers_count || account.user&.setting_hide_followers_count
|
||||
end
|
||||
|
||||
def account_description(account)
|
||||
prepend_stats = [
|
||||
[
|
||||
number_to_human(account.statuses_count, strip_insignificant_zeros: true),
|
||||
I18n.t('accounts.posts', count: account.statuses_count),
|
||||
].join(' '),
|
||||
|
||||
[
|
||||
number_to_human(account.following_count, strip_insignificant_zeros: true),
|
||||
I18n.t('accounts.following', count: account.following_count),
|
||||
].join(' '),
|
||||
]
|
||||
|
||||
unless hide_followers_count?(account)
|
||||
prepend_stats << [
|
||||
number_to_human(account.followers_count, strip_insignificant_zeros: true),
|
||||
I18n.t('accounts.followers', count: account.followers_count),
|
||||
].join(' ')
|
||||
end
|
||||
|
||||
[prepend_stats.join(', '), account.note].join(' · ')
|
||||
end
|
||||
|
||||
def media_summary(status)
|
||||
attachments = { image: 0, video: 0 }
|
||||
|
||||
@@ -160,14 +59,6 @@ module StatusesHelper
|
||||
embedded_view? ? '_blank' : nil
|
||||
end
|
||||
|
||||
def acct(account)
|
||||
if account.local?
|
||||
"@#{account.acct}@#{Rails.configuration.x.local_domain}"
|
||||
else
|
||||
"@#{account.acct}"
|
||||
end
|
||||
end
|
||||
|
||||
def style_classes(status, is_predecessor, is_successor, include_threads)
|
||||
classes = ['entry']
|
||||
classes << 'entry-predecessor' if is_predecessor
|
||||
|
||||
@@ -25,7 +25,7 @@ export default class AttachmentList extends ImmutablePureComponent {
|
||||
|
||||
return (
|
||||
<li key={attachment.get('id')}>
|
||||
<a href={displayUrl} target='_blank' rel='noopener'><Icon id='link' /> {filename(displayUrl)}</a>
|
||||
<a href={displayUrl} target='_blank' rel='noopener noreferrer'><Icon id='link' /> {filename(displayUrl)}</a>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
@@ -46,7 +46,7 @@ export default class AttachmentList extends ImmutablePureComponent {
|
||||
|
||||
return (
|
||||
<li key={attachment.get('id')}>
|
||||
<a href={displayUrl} target='_blank' rel='noopener'>{filename(displayUrl)}</a>
|
||||
<a href={displayUrl} target='_blank' rel='noopener noreferrer'>{filename(displayUrl)}</a>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -143,7 +143,7 @@ class DropdownMenu extends React.PureComponent {
|
||||
|
||||
return (
|
||||
<li className='dropdown-menu__item' key={`${text}-${i}`}>
|
||||
<a href={href} target={target} data-method={method} rel='noopener' role='button' tabIndex='0' ref={i === 0 ? this.setFocusRef : null} onClick={this.handleClick} onKeyPress={this.handleItemKeyPress} data-index={i}>
|
||||
<a href={href} target={target} data-method={method} rel='noopener noreferrer' role='button' tabIndex='0' ref={i === 0 ? this.setFocusRef : null} onClick={this.handleClick} onKeyPress={this.handleItemKeyPress} data-index={i}>
|
||||
{text}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
@@ -58,7 +58,7 @@ export default class ErrorBoundary extends React.PureComponent {
|
||||
<div>
|
||||
<p className='error-boundary__error'><FormattedMessage id='error.unexpected_crash.explanation' defaultMessage='Due to a bug in our code or a browser compatibility issue, this page could not be displayed correctly.' /></p>
|
||||
<p><FormattedMessage id='error.unexpected_crash.next_steps' defaultMessage='Try refreshing the page. If that does not help, you may still be able to use Mastodon through a different browser or native app.' /></p>
|
||||
<p className='error-boundary__footer'>Mastodon v{version} · <a href={source_url} rel='noopener' target='_blank'><FormattedMessage id='errors.unexpected_crash.report_issue' defaultMessage='Report issue' /></a> · <button onClick={this.handleCopyStackTrace} className={copied && 'copied'}><FormattedMessage id='errors.unexpected_crash.copy_stacktrace' defaultMessage='Copy stacktrace to clipboard' /></button></p>
|
||||
<p className='error-boundary__footer'>Mastodon v{version} · <a href={source_url} rel='noopener noreferrer' target='_blank'><FormattedMessage id='errors.unexpected_crash.report_issue' defaultMessage='Report issue' /></a> · <button onClick={this.handleCopyStackTrace} className={copied && 'copied'}><FormattedMessage id='errors.unexpected_crash.copy_stacktrace' defaultMessage='Copy stacktrace to clipboard' /></button></p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import React from 'react';
|
||||
import Motion from '../features/ui/util/optional_motion';
|
||||
import spring from 'react-motion/lib/spring';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import Icon from 'mastodon/components/icon';
|
||||
@@ -37,6 +35,21 @@ export default class IconButton extends React.PureComponent {
|
||||
tabIndex: '0',
|
||||
};
|
||||
|
||||
state = {
|
||||
activate: false,
|
||||
deactivate: false,
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (!nextProps.animate) return;
|
||||
|
||||
if (this.props.active && !nextProps.active) {
|
||||
this.setState({ activate: false, deactivate: true });
|
||||
} else if (!this.props.active && nextProps.active) {
|
||||
this.setState({ activate: true, deactivate: false });
|
||||
}
|
||||
}
|
||||
|
||||
handleClick = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -75,7 +88,6 @@ export default class IconButton extends React.PureComponent {
|
||||
|
||||
const {
|
||||
active,
|
||||
animate,
|
||||
className,
|
||||
disabled,
|
||||
expanded,
|
||||
@@ -87,57 +99,37 @@ export default class IconButton extends React.PureComponent {
|
||||
title,
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
activate,
|
||||
deactivate,
|
||||
} = this.state;
|
||||
|
||||
const classes = classNames(className, 'icon-button', {
|
||||
active,
|
||||
disabled,
|
||||
inverted,
|
||||
activate,
|
||||
deactivate,
|
||||
overlayed: overlay,
|
||||
});
|
||||
|
||||
if (!animate) {
|
||||
// Perf optimization: avoid unnecessary <Motion> components unless
|
||||
// we actually need to animate.
|
||||
return (
|
||||
<button
|
||||
aria-label={title}
|
||||
aria-pressed={pressed}
|
||||
aria-expanded={expanded}
|
||||
title={title}
|
||||
className={classes}
|
||||
onClick={this.handleClick}
|
||||
onMouseDown={this.handleMouseDown}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
onKeyPress={this.handleKeyPress}
|
||||
style={style}
|
||||
tabIndex={tabIndex}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Icon id={icon} fixedWidth aria-hidden='true' />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Motion defaultStyle={{ rotate: active ? -360 : 0 }} style={{ rotate: animate ? spring(active ? -360 : 0, { stiffness: 120, damping: 7 }) : 0 }}>
|
||||
{({ rotate }) => (
|
||||
<button
|
||||
aria-label={title}
|
||||
aria-pressed={pressed}
|
||||
aria-expanded={expanded}
|
||||
title={title}
|
||||
className={classes}
|
||||
onClick={this.handleClick}
|
||||
onMouseDown={this.handleMouseDown}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
onKeyPress={this.handleKeyPress}
|
||||
style={style}
|
||||
tabIndex={tabIndex}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Icon id={icon} style={{ transform: `rotate(${rotate}deg)` }} fixedWidth aria-hidden='true' />
|
||||
</button>
|
||||
)}
|
||||
</Motion>
|
||||
<button
|
||||
aria-label={title}
|
||||
aria-pressed={pressed}
|
||||
aria-expanded={expanded}
|
||||
title={title}
|
||||
className={classes}
|
||||
onClick={this.handleClick}
|
||||
onMouseDown={this.handleMouseDown}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
onKeyPress={this.handleKeyPress}
|
||||
style={style}
|
||||
tabIndex={tabIndex}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Icon id={icon} fixedWidth aria-hidden='true' />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import IconButton from './icon_button';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import { isIOS } from '../is_mobile';
|
||||
import classNames from 'classnames';
|
||||
import { autoPlayGif, displayMedia, useBlurhash } from '../initial_state';
|
||||
import { autoPlayGif, cropImages, displayMedia, useBlurhash } from '../initial_state';
|
||||
import { decode } from 'blurhash';
|
||||
|
||||
const messages = defineMessages({
|
||||
@@ -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') || attachment.get('url')} target='_blank' style={{ cursor: 'pointer' }} title={attachment.get('description')}>
|
||||
<a className='media-gallery__item-thumbnail' href={attachment.get('remote_url') || attachment.get('url')} style={{ cursor: 'pointer' }} title={attachment.get('description')} target='_blank' rel='noopener noreferrer'>
|
||||
<canvas width={32} height={32} ref={this.setCanvasRef} className='media-gallery__preview' />
|
||||
</a>
|
||||
</div>
|
||||
@@ -187,6 +187,7 @@ class Item extends React.PureComponent {
|
||||
href={attachment.get('remote_url') || originalUrl}
|
||||
onClick={this.handleClick}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
>
|
||||
<img
|
||||
src={previewUrl}
|
||||
@@ -280,7 +281,7 @@ class MediaGallery extends React.PureComponent {
|
||||
}
|
||||
|
||||
handleRef = (node) => {
|
||||
if (node /*&& this.isStandaloneEligible()*/) {
|
||||
if (node) {
|
||||
// offsetWidth triggers a layout, so only calculate when we need to
|
||||
if (this.props.cacheWidth) this.props.cacheWidth(node.offsetWidth);
|
||||
|
||||
@@ -290,13 +291,13 @@ class MediaGallery extends React.PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
isStandaloneEligible() {
|
||||
const { media, standalone } = this.props;
|
||||
return standalone && media.size === 1 && media.getIn([0, 'meta', 'small', 'aspect']);
|
||||
isFullSizeEligible() {
|
||||
const { media } = this.props;
|
||||
return media.size === 1 && media.getIn([0, 'meta', 'small', 'aspect']);
|
||||
}
|
||||
|
||||
render () {
|
||||
const { media, intl, sensitive, height, defaultWidth } = this.props;
|
||||
const { media, intl, sensitive, height, defaultWidth, standalone } = this.props;
|
||||
const { visible } = this.state;
|
||||
|
||||
const width = this.state.width || defaultWidth;
|
||||
@@ -305,7 +306,7 @@ class MediaGallery extends React.PureComponent {
|
||||
|
||||
const style = {};
|
||||
|
||||
if (this.isStandaloneEligible()) {
|
||||
if (this.isFullSizeEligible() && (standalone || !cropImages)) {
|
||||
if (width) {
|
||||
style.height = width / this.props.media.getIn([0, 'meta', 'small', 'aspect']);
|
||||
}
|
||||
@@ -318,7 +319,7 @@ class MediaGallery extends React.PureComponent {
|
||||
const size = media.take(4).size;
|
||||
const uncached = media.every(attachment => attachment.get('type') === 'unknown');
|
||||
|
||||
if (this.isStandaloneEligible()) {
|
||||
if (standalone && this.isFullSizeEligible()) {
|
||||
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 || uncached} />);
|
||||
|
||||
@@ -437,9 +437,9 @@ class Status extends ImmutablePureComponent {
|
||||
<div className={classNames('status', `status-${status.get('visibility')}`, { 'status-reply': !!status.get('in_reply_to_id'), muted: this.props.muted, read: unread === false })} data-id={status.get('id')}>
|
||||
<div className='status__expand' onClick={this.handleExpandClick} role='presentation' />
|
||||
<div className='status__info'>
|
||||
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a>
|
||||
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener noreferrer'><RelativeTimestamp timestamp={status.get('created_at')} /></a>
|
||||
|
||||
<a onClick={this.handleAccountClick} target='_blank' data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} title={status.getIn(['account', 'acct'])} className='status__display-name'>
|
||||
<a onClick={this.handleAccountClick} data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} title={status.getIn(['account', 'acct'])} className='status__display-name' target='_blank' rel='noopener noreferrer'>
|
||||
<div className='status__avatar'>
|
||||
{statusAvatar}
|
||||
</div>
|
||||
|
||||
@@ -59,7 +59,7 @@ export default class StatusContent extends React.PureComponent {
|
||||
}
|
||||
|
||||
link.setAttribute('target', '_blank');
|
||||
link.setAttribute('rel', 'noopener');
|
||||
link.setAttribute('rel', 'noopener noreferrer');
|
||||
}
|
||||
|
||||
if (
|
||||
|
||||
@@ -253,7 +253,7 @@ class Header extends ImmutablePureComponent {
|
||||
|
||||
<div className='account__header__bar'>
|
||||
<div className='account__header__tabs'>
|
||||
<a className='avatar' href={account.get('url')} rel='noopener' target='_blank'>
|
||||
<a className='avatar' href={account.get('url')} rel='noopener noreferrer' target='_blank'>
|
||||
<Avatar account={account} size={90} />
|
||||
</a>
|
||||
|
||||
@@ -282,10 +282,10 @@ class Header extends ImmutablePureComponent {
|
||||
<dt dangerouslySetInnerHTML={{ __html: proof.get('provider') }} />
|
||||
|
||||
<dd className='verified'>
|
||||
<a href={proof.get('proof_url')} target='_blank' rel='noopener'><span title={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(proof.get('updated_at'), dateFormatOptions) })}>
|
||||
<a href={proof.get('proof_url')} target='_blank' rel='noopener noreferrer'><span title={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(proof.get('updated_at'), dateFormatOptions) })}>
|
||||
<Icon id='check' className='verified__mark' />
|
||||
</span></a>
|
||||
<a href={proof.get('profile_url')} target='_blank' rel='noopener'><span dangerouslySetInnerHTML={{ __html: ' '+proof.get('provider_username') }} /></a>
|
||||
<a href={proof.get('profile_url')} target='_blank' rel='noopener noreferrer'><span dangerouslySetInnerHTML={{ __html: ' '+proof.get('provider_username') }} /></a>
|
||||
</dd>
|
||||
</dl>
|
||||
))}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { decode } from 'blurhash';
|
||||
import classNames from 'classnames';
|
||||
import Icon from 'mastodon/components/icon';
|
||||
import { autoPlayGif, displayMedia } from 'mastodon/initial_state';
|
||||
import classNames from 'classnames';
|
||||
import { decode } from 'blurhash';
|
||||
import { isIOS } from 'mastodon/is_mobile';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
|
||||
export default class MediaItem extends ImmutablePureComponent {
|
||||
|
||||
@@ -151,7 +151,7 @@ export default class MediaItem extends ImmutablePureComponent {
|
||||
|
||||
return (
|
||||
<div className='account-gallery__item' style={{ width, height }}>
|
||||
<a className='media-gallery__item-thumbnail' href={status.get('url')} target='_blank' onClick={this.handleClick} title={title}>
|
||||
<a className='media-gallery__item-thumbnail' href={status.get('url')} onClick={this.handleClick} title={title} target='_blank' rel='noopener noreferrer'>
|
||||
<canvas width={32} height={32} ref={this.setCanvasRef} className={classNames('media-gallery__preview', { 'media-gallery__preview--hidden': visible && loaded })} />
|
||||
{visible && thumbnail}
|
||||
{!visible && icon}
|
||||
|
||||
@@ -12,6 +12,7 @@ const messages = defineMessages({
|
||||
pause: { id: 'video.pause', defaultMessage: 'Pause' },
|
||||
mute: { id: 'video.mute', defaultMessage: 'Mute sound' },
|
||||
unmute: { id: 'video.unmute', defaultMessage: 'Unmute sound' },
|
||||
download: { id: 'video.download', defaultMessage: 'Download file' },
|
||||
});
|
||||
|
||||
export default @injectIntl
|
||||
@@ -202,6 +203,7 @@ class Audio extends React.PureComponent {
|
||||
<button type='button' aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button>
|
||||
|
||||
<div className='video-player__volume' onMouseDown={this.handleVolumeMouseDown} ref={this.setVolumeRef}>
|
||||
|
||||
<div className='video-player__volume__current' style={{ width: `${volumeWidth}px` }} />
|
||||
|
||||
<span
|
||||
@@ -217,6 +219,14 @@ class Audio extends React.PureComponent {
|
||||
<span className='video-player__time-total'>{formatTime(this.state.duration || Math.floor(this.props.duration))}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className='video-player__buttons right'>
|
||||
<button type='button' aria-label={intl.formatMessage(messages.download)}>
|
||||
<a className='video-player__download__icon' href={this.props.src} download>
|
||||
<Icon id={'download'} fixedWidth />
|
||||
</a>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import Toggle from 'react-toggle';
|
||||
import AsyncSelect from 'react-select/lib/Async';
|
||||
import AsyncSelect from 'react-select/async';
|
||||
|
||||
const messages = defineMessages({
|
||||
placeholder: { id: 'hashtag.column_settings.select.placeholder', defaultMessage: 'Enter hashtags…' },
|
||||
|
||||
@@ -148,7 +148,7 @@ export default class Card extends React.PureComponent {
|
||||
const horizontal = (!compact && card.get('width') > card.get('height') && (card.get('width') + 100 >= width)) || card.get('type') !== 'link' || embedded;
|
||||
const interactive = card.get('type') !== 'link';
|
||||
const className = classnames('status-card', { horizontal, compact, interactive });
|
||||
const title = interactive ? <a className='status-card__title' href={card.get('url')} title={card.get('title')} rel='noopener' target='_blank'><strong>{card.get('title')}</strong></a> : <strong className='status-card__title' title={card.get('title')}>{card.get('title')}</strong>;
|
||||
const title = interactive ? <a className='status-card__title' href={card.get('url')} title={card.get('title')} rel='noopener noreferrer' target='_blank'><strong>{card.get('title')}</strong></a> : <strong className='status-card__title' title={card.get('title')}>{card.get('title')}</strong>;
|
||||
const ratio = card.get('width') / card.get('height');
|
||||
const height = (compact && !embedded) ? (width / (16 / 9)) : (width / ratio);
|
||||
|
||||
@@ -180,7 +180,7 @@ export default class Card extends React.PureComponent {
|
||||
<div className='status-card__actions'>
|
||||
<div>
|
||||
<button onClick={this.handleEmbedClick}><Icon id={iconVariant} /></button>
|
||||
{horizontal && <a href={card.get('url')} target='_blank' rel='noopener'><Icon id='external-link' /></a>}
|
||||
{horizontal && <a href={card.get('url')} target='_blank' rel='noopener noreferrer'><Icon id='external-link' /></a>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -208,7 +208,7 @@ export default class Card extends React.PureComponent {
|
||||
}
|
||||
|
||||
return (
|
||||
<a href={card.get('url')} className={className} target='_blank' rel='noopener' ref={this.setRef}>
|
||||
<a href={card.get('url')} className={className} target='_blank' rel='noopener noreferrer' ref={this.setRef}>
|
||||
{embed}
|
||||
{description}
|
||||
</a>
|
||||
|
||||
@@ -156,7 +156,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
|
||||
}
|
||||
|
||||
if (status.get('application')) {
|
||||
applicationLink = <span> · <a className='detailed-status__application' href={status.getIn(['application', 'website'])} target='_blank' rel='noopener'>{status.getIn(['application', 'name'])}</a></span>;
|
||||
applicationLink = <span> · <a className='detailed-status__application' href={status.getIn(['application', 'website'])} target='_blank' rel='noopener noreferrer'>{status.getIn(['application', 'name'])}</a></span>;
|
||||
}
|
||||
|
||||
if (status.get('visibility') === 'direct') {
|
||||
@@ -220,7 +220,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
|
||||
{media}
|
||||
|
||||
<div className='detailed-status__meta'>
|
||||
<a className='detailed-status__datetime' href={status.get('url')} target='_blank' rel='noopener'>
|
||||
<a className='detailed-status__datetime' href={status.get('url')} target='_blank' rel='noopener noreferrer'>
|
||||
<FormattedDate value={new Date(status.get('created_at'))} hour12={false} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' />
|
||||
</a>{applicationLink} · {reblogLink} · {favouriteLink}
|
||||
</div>
|
||||
|
||||
@@ -26,7 +26,7 @@ export default class ActionsModal extends ImmutablePureComponent {
|
||||
|
||||
return (
|
||||
<li key={`${text}-${i}`}>
|
||||
<a href={href} target='_blank' rel='noopener' onClick={this.props.onClick} data-index={i} className={classNames({ active })}>
|
||||
<a href={href} target='_blank' rel='noopener noreferrer' onClick={this.props.onClick} data-index={i} className={classNames({ active })}>
|
||||
{icon && <IconButton title={text} icon={icon} role='presentation' tabIndex='-1' inverted />}
|
||||
<div>
|
||||
<div className={classNames({ 'actions-modal__item-label': !!meta })}>{text}</div>
|
||||
@@ -42,7 +42,7 @@ export default class ActionsModal extends ImmutablePureComponent {
|
||||
<div className='status light'>
|
||||
<div className='boost-modal__status-header'>
|
||||
<div className='boost-modal__status-time'>
|
||||
<a href={this.props.status.get('url')} className='status__relative-time' target='_blank' rel='noopener'>
|
||||
<a href={this.props.status.get('url')} className='status__relative-time' target='_blank' rel='noopener noreferrer'>
|
||||
<RelativeTimestamp timestamp={this.props.status.get('created_at')} />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -61,7 +61,7 @@ class BoostModal extends ImmutablePureComponent {
|
||||
<div className='status light'>
|
||||
<div className='boost-modal__status-header'>
|
||||
<div className='boost-modal__status-time'>
|
||||
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a>
|
||||
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener noreferrer'><RelativeTimestamp timestamp={status.get('created_at')} /></a>
|
||||
</div>
|
||||
|
||||
<a onClick={this.handleAccountClick} href={status.getIn(['account', 'url'])} className='status__display-name'>
|
||||
|
||||
@@ -182,7 +182,7 @@ class ColumnsArea extends ImmutablePureComponent {
|
||||
const floatingActionButton = shouldHideFAB(this.context.router.history.location.pathname) ? null : <Link key='floating-action-button' to='/statuses/new' className='floating-action-button' aria-label={intl.formatMessage(messages.publish)}><Icon id='pencil' /></Link>;
|
||||
|
||||
const content = columnIndex !== -1 ? (
|
||||
<ReactSwipeableViews key='content' index={columnIndex} onChangeIndex={this.handleSwipe} onTransitionEnd={this.handleAnimationEnd} animateTransitions={shouldAnimate} springConfig={{ duration: '400ms', delay: '0s', easeFunction: 'ease' }} style={{ height: '100%' }}>
|
||||
<ReactSwipeableViews key='content' hysteresis={0.2} threshold={15} index={columnIndex} onChangeIndex={this.handleSwipe} onTransitionEnd={this.handleAnimationEnd} animateTransitions={shouldAnimate} springConfig={{ duration: '400ms', delay: '0s', easeFunction: 'ease' }} style={{ height: '100%' }}>
|
||||
{links.map(this.renderView)}
|
||||
</ReactSwipeableViews>
|
||||
) : (
|
||||
|
||||
@@ -62,7 +62,7 @@ class LinkFooter extends React.PureComponent {
|
||||
<FormattedMessage
|
||||
id='getting_started.open_source_notice'
|
||||
defaultMessage='Mastodon is open source software. You can contribute or report issues on GitHub at {github}.'
|
||||
values={{ github: <span><a href={source_url} rel='noopener' target='_blank'>{repository}</a> (v{version})</span> }}
|
||||
values={{ github: <span><a href={source_url} rel='noopener noreferrer' target='_blank'>{repository}</a> (v{version})</span> }}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -19,6 +19,7 @@ const messages = defineMessages({
|
||||
close: { id: 'video.close', defaultMessage: 'Close video' },
|
||||
fullscreen: { id: 'video.fullscreen', defaultMessage: 'Full screen' },
|
||||
exit_fullscreen: { id: 'video.exit_fullscreen', defaultMessage: 'Exit full screen' },
|
||||
download: { id: 'video.download', defaultMessage: 'Download file' },
|
||||
});
|
||||
|
||||
export const formatTime = secondsNum => {
|
||||
@@ -470,6 +471,7 @@ class Video extends React.PureComponent {
|
||||
<button type='button' aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button>
|
||||
|
||||
<div className='video-player__volume' onMouseDown={this.handleVolumeMouseDown} ref={this.setVolumeRef}>
|
||||
|
||||
<div className='video-player__volume__current' style={{ width: `${volumeWidth}px` }} />
|
||||
<span
|
||||
className={classNames('video-player__volume__handle')}
|
||||
@@ -493,7 +495,13 @@ class Video extends React.PureComponent {
|
||||
{(!onCloseVideo && !editable) && <button type='button' aria-label={intl.formatMessage(messages.hide)} onClick={this.toggleReveal}><Icon id='eye-slash' fixedWidth /></button>}
|
||||
{(!fullscreen && onOpenVideo) && <button type='button' aria-label={intl.formatMessage(messages.expand)} onClick={this.handleOpenVideo}><Icon id='expand' fixedWidth /></button>}
|
||||
{onCloseVideo && <button type='button' aria-label={intl.formatMessage(messages.close)} onClick={this.handleCloseVideo}><Icon id='compress' fixedWidth /></button>}
|
||||
<button type='button' aria-label={intl.formatMessage(messages.download)}>
|
||||
<a className='video-player__download__icon' href={this.props.src} download>
|
||||
<Icon id={'download'} fixedWidth />
|
||||
</a>
|
||||
</button>
|
||||
<button type='button' aria-label={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)} onClick={this.toggleFullscreen}><Icon id={fullscreen ? 'compress' : 'arrows-alt'} fixedWidth /></button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -25,5 +25,6 @@ export const useBlurhash = getMeta('use_blurhash');
|
||||
export const usePendingItems = getMeta('use_pending_items');
|
||||
export const showTrends = getMeta('trends');
|
||||
export const title = getMeta('title');
|
||||
export const cropImages = getMeta('crop_images');
|
||||
|
||||
export default initialState;
|
||||
|
||||
@@ -776,6 +776,10 @@
|
||||
{
|
||||
"defaultMessage": "Unmute sound",
|
||||
"id": "video.unmute"
|
||||
},
|
||||
{
|
||||
"defaultMessage": "Download file",
|
||||
"id": "video.download"
|
||||
}
|
||||
],
|
||||
"path": "app/javascript/mastodon/features/audio/index.json"
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
REBLOG_FAIL,
|
||||
FAVOURITE_REQUEST,
|
||||
FAVOURITE_FAIL,
|
||||
UNFAVOURITE_SUCCESS,
|
||||
} from '../actions/interactions';
|
||||
import {
|
||||
STATUS_MUTE_SUCCESS,
|
||||
@@ -37,6 +38,9 @@ export default function statuses(state = initialState, action) {
|
||||
return importStatuses(state, action.statuses);
|
||||
case FAVOURITE_REQUEST:
|
||||
return state.setIn([action.status.get('id'), 'favourited'], true);
|
||||
case UNFAVOURITE_SUCCESS:
|
||||
const favouritesCount = action.status.get('favourites_count');
|
||||
return state.setIn([action.status.get('id'), 'favourites_count'], favouritesCount - 1);
|
||||
case FAVOURITE_FAIL:
|
||||
return state.get(action.status.get('id')) === undefined ? state : state.setIn([action.status.get('id'), 'favourited'], false);
|
||||
case REBLOG_REQUEST:
|
||||
|
||||
@@ -1591,6 +1591,20 @@ a.account__display-name {
|
||||
color: $gold-star;
|
||||
}
|
||||
|
||||
.no-reduce-motion .icon-button.star-icon {
|
||||
&.activate {
|
||||
& > .fa-star {
|
||||
animation: spring-rotate-in 1s linear;
|
||||
}
|
||||
}
|
||||
|
||||
&.deactivate {
|
||||
& > .fa-star {
|
||||
animation: spring-rotate-out 1s linear;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.notification__display-name {
|
||||
color: inherit;
|
||||
font-weight: 500;
|
||||
@@ -3373,6 +3387,50 @@ a.status-card.compact:hover {
|
||||
animation: loader-figure 1.15s infinite cubic-bezier(0.215, 0.61, 0.355, 1);
|
||||
}
|
||||
|
||||
@keyframes spring-rotate-in {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
30% {
|
||||
transform: rotate(-484.8deg);
|
||||
}
|
||||
|
||||
60% {
|
||||
transform: rotate(-316.7deg);
|
||||
}
|
||||
|
||||
90% {
|
||||
transform: rotate(-375deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(-360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spring-rotate-out {
|
||||
0% {
|
||||
transform: rotate(-360deg);
|
||||
}
|
||||
|
||||
30% {
|
||||
transform: rotate(124.8deg);
|
||||
}
|
||||
|
||||
60% {
|
||||
transform: rotate(-43.27deg);
|
||||
}
|
||||
|
||||
90% {
|
||||
transform: rotate(15deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes loader-figure {
|
||||
0% {
|
||||
width: 0;
|
||||
@@ -5194,6 +5252,7 @@ a.status-card.compact:hover {
|
||||
max-height: 100% !important;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
outline: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5271,6 +5330,10 @@ a.status-card.compact:hover {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding-bottom: 10px;
|
||||
|
||||
.video-player__download__icon {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
&__buttons {
|
||||
|
||||
@@ -149,10 +149,6 @@ a.table-action-link {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: $no-gap-breakpoint) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__actions,
|
||||
@@ -174,10 +170,6 @@ a.table-action-link {
|
||||
text-align: right;
|
||||
padding-right: 16px - 5px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: $no-gap-breakpoint) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__form {
|
||||
@@ -198,7 +190,7 @@ a.table-action-link {
|
||||
background: darken($ui-base-color, 4%);
|
||||
|
||||
@media screen and (max-width: $no-gap-breakpoint) {
|
||||
&:first-child {
|
||||
.optional &:first-child {
|
||||
border-top: 1px solid darken($ui-base-color, 8%);
|
||||
}
|
||||
}
|
||||
@@ -264,6 +256,13 @@ a.table-action-link {
|
||||
}
|
||||
}
|
||||
|
||||
&.optional .batch-table__toolbar,
|
||||
&.optional .batch-table__row__select {
|
||||
@media screen and (max-width: $no-gap-breakpoint) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.status__content {
|
||||
padding-top: 0;
|
||||
|
||||
|
||||
@@ -153,6 +153,14 @@ class ActivityPub::Activity
|
||||
fetch_remote_original_status
|
||||
end
|
||||
|
||||
def follow_request_from_object
|
||||
@follow_request ||= FollowRequest.find_by(target_account: @account, uri: object_uri) unless object_uri.nil?
|
||||
end
|
||||
|
||||
def follow_from_object
|
||||
@follow ||= Follow.find_by(target_account: @account, uri: object_uri) unless object_uri.nil?
|
||||
end
|
||||
|
||||
def fetch_remote_original_status
|
||||
if object_uri.start_with?('http')
|
||||
return if ActivityPub::TagManager.instance.local_uri?(object_uri)
|
||||
|
||||
@@ -2,17 +2,18 @@
|
||||
|
||||
class ActivityPub::Activity::Accept < ActivityPub::Activity
|
||||
def perform
|
||||
return accept_follow_for_relay if relay_follow?
|
||||
return follow_request_from_object.authorize! unless follow_request_from_object.nil?
|
||||
|
||||
case @object['type']
|
||||
when 'Follow'
|
||||
accept_follow
|
||||
accept_embedded_follow
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def accept_follow
|
||||
return accept_follow_for_relay if relay_follow?
|
||||
|
||||
def accept_embedded_follow
|
||||
target_account = account_from_uri(target_uri)
|
||||
|
||||
return if target_account.nil? || !target_account.local?
|
||||
|
||||
@@ -2,17 +2,19 @@
|
||||
|
||||
class ActivityPub::Activity::Reject < ActivityPub::Activity
|
||||
def perform
|
||||
return reject_follow_for_relay if relay_follow?
|
||||
return follow_request_from_object.reject! unless follow_request_from_object.nil?
|
||||
return UnfollowService.new.call(follow_from_object.target_account, @account) unless follow_from_object.nil?
|
||||
|
||||
case @object['type']
|
||||
when 'Follow'
|
||||
reject_follow
|
||||
reject_embedded_follow
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def reject_follow
|
||||
return reject_follow_for_relay if relay_follow?
|
||||
|
||||
def reject_embedded_follow
|
||||
target_account = account_from_uri(target_uri)
|
||||
|
||||
return if target_account.nil? || !target_account.local?
|
||||
|
||||
@@ -337,7 +337,7 @@ class Formatter
|
||||
|
||||
def link_to_url(entity, options = {})
|
||||
url = Addressable::URI.parse(entity[:url])
|
||||
html_attrs = { target: '_blank', rel: 'nofollow noopener' }
|
||||
html_attrs = { target: '_blank', rel: 'nofollow noopener noreferrer' }
|
||||
|
||||
html_attrs[:rel] = "me #{html_attrs[:rel]}" if options[:me]
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ class Sanitize
|
||||
|
||||
add_attributes: {
|
||||
'a' => {
|
||||
'rel' => 'nofollow noopener tag',
|
||||
'rel' => 'nofollow noopener tag noreferrer',
|
||||
'target' => '_blank',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -42,6 +42,7 @@ class UserSettingsDecorator
|
||||
user.settings['use_blurhash'] = use_blurhash_preference if change?('setting_use_blurhash')
|
||||
user.settings['use_pending_items'] = use_pending_items_preference if change?('setting_use_pending_items')
|
||||
user.settings['trends'] = trends_preference if change?('setting_trends')
|
||||
user.settings['crop_images'] = crop_images_preference if change?('setting_crop_images')
|
||||
end
|
||||
|
||||
def merged_notification_emails
|
||||
@@ -152,6 +153,10 @@ class UserSettingsDecorator
|
||||
boolean_cast_setting 'setting_trends'
|
||||
end
|
||||
|
||||
def crop_images_preference
|
||||
boolean_cast_setting 'setting_crop_images'
|
||||
end
|
||||
|
||||
def boolean_cast_setting(key)
|
||||
ActiveModel::Type::Boolean.new.cast(settings[key])
|
||||
end
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
class AdminMailer < ApplicationMailer
|
||||
layout 'plain_mailer'
|
||||
|
||||
helper :statuses
|
||||
helper :accounts
|
||||
|
||||
def new_report(recipient, report)
|
||||
@report = report
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class NotificationMailer < ApplicationMailer
|
||||
helper :accounts
|
||||
helper :statuses
|
||||
|
||||
add_template_helper RoutingHelper
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
class UserMailer < Devise::Mailer
|
||||
layout 'mailer'
|
||||
|
||||
helper :accounts
|
||||
helper :application
|
||||
helper :instance
|
||||
helper :statuses
|
||||
|
||||
add_template_helper RoutingHelper
|
||||
|
||||
|
||||
@@ -87,6 +87,7 @@ class MediaAttachment < ApplicationRecord
|
||||
convert_options: {
|
||||
output: {
|
||||
'loglevel' => 'fatal',
|
||||
'map_metadata' => '-1',
|
||||
'q:a' => 2,
|
||||
},
|
||||
},
|
||||
|
||||
+1
-1
@@ -108,7 +108,7 @@ class User < ApplicationRecord
|
||||
delegate :auto_play_gif, :default_sensitive, :unfollow_modal, :boost_modal, :favourite_modal, :delete_modal,
|
||||
:reduce_motion, :system_font_ui, :noindex, :flavour, :skin, :display_media, :hide_network, :hide_followers_count,
|
||||
:expand_spoilers, :default_language, :aggregate_reblogs, :show_application,
|
||||
:advanced_layout, :use_blurhash, :use_pending_items, :trends,
|
||||
:advanced_layout, :use_blurhash, :use_pending_items, :trends, :crop_images,
|
||||
:default_content_type, :system_emoji_font,
|
||||
to: :settings, prefix: :setting, allow_nil: false
|
||||
|
||||
|
||||
@@ -55,11 +55,13 @@ class InitialStateSerializer < ActiveModel::Serializer
|
||||
store[:trends] = Setting.trends && object.current_account.user.setting_trends
|
||||
store[:default_content_type] = object.current_account.user.setting_default_content_type
|
||||
store[:system_emoji_font] = object.current_account.user.setting_system_emoji_font
|
||||
store[:crop_images] = object.current_account.user.setting_crop_images
|
||||
else
|
||||
store[:auto_play_gif] = Setting.auto_play_gif
|
||||
store[:display_media] = Setting.display_media
|
||||
store[:reduce_motion] = Setting.reduce_motion
|
||||
store[:use_blurhash] = Setting.use_blurhash
|
||||
store[:crop_images] = Setting.crop_images
|
||||
end
|
||||
|
||||
store
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
class RSS::AccountSerializer
|
||||
include ActionView::Helpers::NumberHelper
|
||||
include StatusesHelper
|
||||
include AccountsHelper
|
||||
include RoutingHelper
|
||||
|
||||
def render(account, statuses, tag)
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
class RSS::TagSerializer
|
||||
include ActionView::Helpers::NumberHelper
|
||||
include ActionView::Helpers::SanitizeHelper
|
||||
include StatusesHelper
|
||||
include RoutingHelper
|
||||
|
||||
def render(tag, statuses)
|
||||
|
||||
@@ -84,7 +84,7 @@ class FetchLinkCardService < BaseService
|
||||
|
||||
def skip_link?(a)
|
||||
# Avoid links for hashtags and mentions (microformats)
|
||||
a['rel']&.include?('tag') || a['class']&.include?('u-url') || mention_link?(a)
|
||||
a['rel']&.include?('tag') || a['class']&.match?(/u-url|h-card/) || mention_link?(a)
|
||||
end
|
||||
|
||||
def attempt_oembed
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
%small= t('about.browse_public_posts')
|
||||
|
||||
.directory__tag
|
||||
= link_to 'https://joinmastodon.org/apps', target: '_blank', rel: 'noopener' do
|
||||
= link_to 'https://joinmastodon.org/apps', target: '_blank', rel: 'noopener noreferrer' do
|
||||
%h4
|
||||
= fa_icon 'tablet fw'
|
||||
= t('about.get_apps')
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
= t('accounts.moved_html', name: content_tag(:bdi, content_tag(:strong, display_name(account, custom_emojify: true), class: :emojify)), new_profile_link: link_to(content_tag(:strong, safe_join(['@', content_tag(:span, moved_to_account.acct)])), ActivityPub::TagManager.instance.url_for(moved_to_account), class: 'mention'))
|
||||
|
||||
.moved-account-widget__card
|
||||
= link_to ActivityPub::TagManager.instance.url_for(moved_to_account), class: 'detailed-status__display-name p-author h-card', target: '_blank', rel: 'me noopener' do
|
||||
= link_to ActivityPub::TagManager.instance.url_for(moved_to_account), class: 'detailed-status__display-name p-author h-card', target: '_blank', rel: 'me noopener noreferrer' do
|
||||
.detailed-status__display-avatar
|
||||
.account__avatar-overlay
|
||||
.account__avatar-overlay-base{ style: "background-image: url('#{moved_to_account.avatar.url(:original)}')" }
|
||||
|
||||
@@ -143,12 +143,15 @@
|
||||
%th= t('admin.accounts.most_recent_ip')
|
||||
%td= @account.user_current_sign_in_ip
|
||||
%td
|
||||
- if @account.user_current_sign_in_ip
|
||||
= table_link_to 'search', t('admin.accounts.search_same_ip'), admin_accounts_path(ip: @account.user_current_sign_in_ip)
|
||||
|
||||
%tr
|
||||
%th= t('admin.accounts.most_recent_activity')
|
||||
%td
|
||||
- if @account.user_current_sign_in_at
|
||||
%time.formatted{ datetime: @account.user_current_sign_in_at.iso8601, title: l(@account.user_current_sign_in_at) }= l @account.user_current_sign_in_at
|
||||
%td
|
||||
|
||||
- if @account.user&.invited?
|
||||
%tr
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
= react_component :media_gallery, height: 343, sensitive: !current_account&.user&.show_all_media? && status.proper.sensitive? || current_account&.user&.hide_all_media?, 'autoPlayGif': current_account&.user&.setting_auto_play_gif, media: status.proper.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json }
|
||||
|
||||
.detailed-status__meta
|
||||
= link_to ActivityPub::TagManager.instance.url_for(status), class: 'detailed-status__datetime', target: stream_link_target, rel: 'noopener' do
|
||||
= link_to ActivityPub::TagManager.instance.url_for(status), class: 'detailed-status__datetime', target: stream_link_target, rel: 'noopener noreferrer' do
|
||||
%time.formatted{ datetime: status.created_at.iso8601, title: l(status.created_at) }= l(status.created_at)
|
||||
- if status.discarded?
|
||||
·
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
- Admin::FilterHelper::TAGS_FILTERS.each do |key|
|
||||
= hidden_field_tag key, params[key] if params[key].present?
|
||||
|
||||
.batch-table
|
||||
.batch-table.optional
|
||||
.batch-table__toolbar
|
||||
%label.batch-table__toolbar__select.batch-checkbox-all
|
||||
= check_box_tag :batch_checkbox_all, nil, false
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
.dashboard__counters
|
||||
%div
|
||||
= link_to tag_url(@tag), target: '_blank', rel: 'noopener' do
|
||||
= link_to tag_url(@tag), target: '_blank', rel: 'noopener noreferrer' do
|
||||
.dashboard__counters__num= number_with_delimiter @accounts_today
|
||||
.dashboard__counters__label= t 'admin.tags.accounts_today'
|
||||
%div
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
- account_url = local_assigns[:admin] ? admin_account_path(account.id) : ActivityPub::TagManager.instance.url_for(account)
|
||||
|
||||
.card.h-card
|
||||
= link_to account_url, target: '_blank', rel: 'noopener' do
|
||||
= link_to account_url, target: '_blank', rel: 'noopener noreferrer' do
|
||||
.card__img
|
||||
= image_tag account.header.url, alt: ''
|
||||
.card__bar
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
- if application.website.blank?
|
||||
= application.name
|
||||
- else
|
||||
= link_to application.name, application.website, target: '_blank', rel: 'noopener'
|
||||
= link_to application.name, application.website, target: '_blank', rel: 'noopener noreferrer'
|
||||
%th!= application.scopes.map { |scope| t(scope, scope: [:doorkeeper, :scopes]) }.join(', ')
|
||||
%td= l application.created_at
|
||||
%td
|
||||
|
||||
@@ -23,6 +23,11 @@
|
||||
= f.input :setting_system_font_ui, as: :boolean, wrapper: :with_label
|
||||
= f.input :setting_system_emoji_font, as: :boolean, wrapper: :with_label
|
||||
|
||||
%h4= t 'appearance.toot_layout'
|
||||
|
||||
.fields-group
|
||||
= f.input :setting_crop_images, as: :boolean, wrapper: :with_label
|
||||
|
||||
%h4= t 'appearance.discovery'
|
||||
|
||||
.fields-group
|
||||
|
||||
@@ -44,14 +44,14 @@
|
||||
.detailed-status__meta
|
||||
%data.dt-published{ value: status.created_at.to_time.iso8601 }
|
||||
|
||||
= link_to ActivityPub::TagManager.instance.url_for(status), class: 'detailed-status__datetime u-url u-uid', target: stream_link_target, rel: 'noopener' do
|
||||
= link_to ActivityPub::TagManager.instance.url_for(status), class: 'detailed-status__datetime u-url u-uid', target: stream_link_target, rel: 'noopener noreferrer' do
|
||||
%time.formatted{ datetime: status.created_at.iso8601, title: l(status.created_at) }= l(status.created_at)
|
||||
·
|
||||
- if status.application && @account.user&.setting_show_application
|
||||
- if status.application.website.blank?
|
||||
%strong.detailed-status__application= status.application.name
|
||||
- else
|
||||
= link_to status.application.name, status.application.website, class: 'detailed-status__application', target: '_blank', rel: 'noopener'
|
||||
= link_to status.application.name, status.application.website, class: 'detailed-status__application', target: '_blank', rel: 'noopener noreferrer'
|
||||
·
|
||||
= link_to remote_interaction_path(status, type: :reply), class: 'modal-button detailed-status__link' do
|
||||
- if status.in_reply_to_id.nil?
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
.status
|
||||
.status__info
|
||||
= link_to ActivityPub::TagManager.instance.url_for(status), class: 'status__relative-time u-url u-uid', target: stream_link_target, rel: 'noopener' do
|
||||
= link_to ActivityPub::TagManager.instance.url_for(status), class: 'status__relative-time u-url u-uid', target: stream_link_target, rel: 'noopener noreferrer' do
|
||||
%time.time-ago{ datetime: status.created_at.iso8601, title: l(status.created_at) }= l(status.created_at)
|
||||
%data.dt-published{ value: status.created_at.to_time.iso8601 }
|
||||
|
||||
.p-author.h-card
|
||||
= link_to ActivityPub::TagManager.instance.url_for(status.account), class: 'status__display-name u-url', target: stream_link_target, rel: 'noopener' do
|
||||
= link_to ActivityPub::TagManager.instance.url_for(status.account), class: 'status__display-name u-url', target: stream_link_target, rel: 'noopener noreferrer' do
|
||||
.status__avatar
|
||||
%div
|
||||
- if current_account&.user&.setting_auto_play_gif || autoplay
|
||||
|
||||
Reference in New Issue
Block a user