Merge branch 'main' into glitch-soc/merge-upstream
Conflicts: - `package.json`: Not really a conflict, upstream updated a dependency textually adjacent to a glitch-soc-only one. Updated the dependency as upstream did.main
commit
5fd8780b14
2
Gemfile
2
Gemfile
|
@ -114,7 +114,7 @@ group :production, :test do
|
||||||
end
|
end
|
||||||
|
|
||||||
group :test do
|
group :test do
|
||||||
gem 'capybara', '~> 3.36'
|
gem 'capybara', '~> 3.37'
|
||||||
gem 'climate_control', '~> 0.2'
|
gem 'climate_control', '~> 0.2'
|
||||||
gem 'faker', '~> 2.20'
|
gem 'faker', '~> 2.20'
|
||||||
gem 'microformats', '~> 4.2'
|
gem 'microformats', '~> 4.2'
|
||||||
|
|
12
Gemfile.lock
12
Gemfile.lock
|
@ -144,7 +144,7 @@ GEM
|
||||||
sshkit (~> 1.3)
|
sshkit (~> 1.3)
|
||||||
capistrano-yarn (2.0.2)
|
capistrano-yarn (2.0.2)
|
||||||
capistrano (~> 3.0)
|
capistrano (~> 3.0)
|
||||||
capybara (3.36.0)
|
capybara (3.37.1)
|
||||||
addressable
|
addressable
|
||||||
matrix
|
matrix
|
||||||
mini_mime (>= 0.1.3)
|
mini_mime (>= 0.1.3)
|
||||||
|
@ -308,7 +308,7 @@ GEM
|
||||||
rainbow (>= 2.0.0)
|
rainbow (>= 2.0.0)
|
||||||
i18n (1.10.0)
|
i18n (1.10.0)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
i18n-tasks (1.0.9)
|
i18n-tasks (1.0.10)
|
||||||
activesupport (>= 4.0.2)
|
activesupport (>= 4.0.2)
|
||||||
ast (>= 2.1.0)
|
ast (>= 2.1.0)
|
||||||
better_html (~> 1.0)
|
better_html (~> 1.0)
|
||||||
|
@ -469,7 +469,7 @@ GEM
|
||||||
pry (~> 0.13.0)
|
pry (~> 0.13.0)
|
||||||
pry-rails (0.3.9)
|
pry-rails (0.3.9)
|
||||||
pry (>= 0.10.4)
|
pry (>= 0.10.4)
|
||||||
public_suffix (4.0.6)
|
public_suffix (4.0.7)
|
||||||
puma (5.6.4)
|
puma (5.6.4)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
pundit (2.2.0)
|
pundit (2.2.0)
|
||||||
|
@ -536,7 +536,7 @@ GEM
|
||||||
redis (4.5.1)
|
redis (4.5.1)
|
||||||
redis-namespace (1.8.2)
|
redis-namespace (1.8.2)
|
||||||
redis (>= 3.0.4)
|
redis (>= 3.0.4)
|
||||||
regexp_parser (2.3.1)
|
regexp_parser (2.4.0)
|
||||||
request_store (1.5.1)
|
request_store (1.5.1)
|
||||||
rack (>= 1.4)
|
rack (>= 1.4)
|
||||||
responders (3.0.1)
|
responders (3.0.1)
|
||||||
|
@ -614,7 +614,7 @@ GEM
|
||||||
rufus-scheduler (~> 3.2)
|
rufus-scheduler (~> 3.2)
|
||||||
sidekiq (>= 4)
|
sidekiq (>= 4)
|
||||||
tilt (>= 1.4.0)
|
tilt (>= 1.4.0)
|
||||||
sidekiq-unique-jobs (7.1.21)
|
sidekiq-unique-jobs (7.1.22)
|
||||||
brpoplpush-redis_script (> 0.1.1, <= 2.0.0)
|
brpoplpush-redis_script (> 0.1.1, <= 2.0.0)
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.5)
|
concurrent-ruby (~> 1.0, >= 1.0.5)
|
||||||
sidekiq (>= 5.0, < 8.0)
|
sidekiq (>= 5.0, < 8.0)
|
||||||
|
@ -745,7 +745,7 @@ DEPENDENCIES
|
||||||
capistrano-rails (~> 1.6)
|
capistrano-rails (~> 1.6)
|
||||||
capistrano-rbenv (~> 2.2)
|
capistrano-rbenv (~> 2.2)
|
||||||
capistrano-yarn (~> 2.0)
|
capistrano-yarn (~> 2.0)
|
||||||
capybara (~> 3.36)
|
capybara (~> 3.37)
|
||||||
charlock_holmes (~> 0.7.7)
|
charlock_holmes (~> 0.7.7)
|
||||||
chewy (~> 7.2)
|
chewy (~> 7.2)
|
||||||
climate_control (~> 0.2)
|
climate_control (~> 0.2)
|
||||||
|
|
|
@ -45,7 +45,6 @@ class AccountsController < ApplicationController
|
||||||
limit = params[:limit].present? ? [params[:limit].to_i, PAGE_SIZE_MAX].min : PAGE_SIZE
|
limit = params[:limit].present? ? [params[:limit].to_i, PAGE_SIZE_MAX].min : PAGE_SIZE
|
||||||
@statuses = filtered_statuses.without_reblogs.limit(limit)
|
@statuses = filtered_statuses.without_reblogs.limit(limit)
|
||||||
@statuses = cache_collection(@statuses, Status)
|
@statuses = cache_collection(@statuses, Status)
|
||||||
render xml: RSS::AccountSerializer.render(@account, @statuses, params[:tag])
|
|
||||||
end
|
end
|
||||||
|
|
||||||
format.json do
|
format.json do
|
||||||
|
|
|
@ -27,7 +27,6 @@ class TagsController < ApplicationController
|
||||||
|
|
||||||
format.rss do
|
format.rss do
|
||||||
expires_in 0, public: true
|
expires_in 0, public: true
|
||||||
render xml: RSS::TagSerializer.render(@tag, @statuses)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
format.json do
|
format.json do
|
||||||
|
|
|
@ -244,7 +244,7 @@ module ApplicationHelper
|
||||||
end.values
|
end.values
|
||||||
end
|
end
|
||||||
|
|
||||||
def prerender_custom_emojis(html, custom_emojis)
|
def prerender_custom_emojis(html, custom_emojis, other_options = {})
|
||||||
EmojiFormatter.new(html, custom_emojis, animate: prefers_autoplay?).to_s
|
EmojiFormatter.new(html, custom_emojis, other_options.merge(animate: prefers_autoplay?)).to_s
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -18,6 +18,32 @@ module FormattingHelper
|
||||||
html_aware_format(status.text, status.local?, preloaded_accounts: [status.account] + (status.respond_to?(:active_mentions) ? status.active_mentions.map(&:account) : []), content_type: status.content_type)
|
html_aware_format(status.text, status.local?, preloaded_accounts: [status.account] + (status.respond_to?(:active_mentions) ? status.active_mentions.map(&:account) : []), content_type: status.content_type)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def rss_status_content_format(status)
|
||||||
|
html = status_content_format(status)
|
||||||
|
|
||||||
|
before_html = begin
|
||||||
|
if status.spoiler_text?
|
||||||
|
"<p><strong>#{I18n.t('rss.content_warning', locale: valid_locale_or_nil(status.language))}</strong> #{h(status.spoiler_text)}</p><hr />"
|
||||||
|
else
|
||||||
|
''
|
||||||
|
end
|
||||||
|
end.html_safe # rubocop:disable Rails/OutputSafety
|
||||||
|
|
||||||
|
after_html = begin
|
||||||
|
if status.preloadable_poll
|
||||||
|
"<p>#{status.preloadable_poll.options.map { |o| "<input type=#{status.preloadable_poll.multiple? ? 'checkbox' : 'radio'} disabled /> #{h(o)}" }.join('<br />')}</p>"
|
||||||
|
else
|
||||||
|
''
|
||||||
|
end
|
||||||
|
end.html_safe # rubocop:disable Rails/OutputSafety
|
||||||
|
|
||||||
|
prerender_custom_emojis(
|
||||||
|
safe_join([before_html, html, after_html]),
|
||||||
|
status.emojis,
|
||||||
|
style: 'width: 1.1em; height: 1.1em; object-fit: contain; vertical-align: middle; margin: -.2ex .15em .2ex'
|
||||||
|
).to_str
|
||||||
|
end
|
||||||
|
|
||||||
def account_bio_format(account)
|
def account_bio_format(account)
|
||||||
html_aware_format(account.note, account.local?)
|
html_aware_format(account.note, account.local?)
|
||||||
end
|
end
|
||||||
|
|
|
@ -77,6 +77,8 @@ export const FOLLOW_REQUEST_REJECT_REQUEST = 'FOLLOW_REQUEST_REJECT_REQUEST';
|
||||||
export const FOLLOW_REQUEST_REJECT_SUCCESS = 'FOLLOW_REQUEST_REJECT_SUCCESS';
|
export const FOLLOW_REQUEST_REJECT_SUCCESS = 'FOLLOW_REQUEST_REJECT_SUCCESS';
|
||||||
export const FOLLOW_REQUEST_REJECT_FAIL = 'FOLLOW_REQUEST_REJECT_FAIL';
|
export const FOLLOW_REQUEST_REJECT_FAIL = 'FOLLOW_REQUEST_REJECT_FAIL';
|
||||||
|
|
||||||
|
export const ACCOUNT_REVEAL = 'ACCOUNT_REVEAL';
|
||||||
|
|
||||||
export function fetchAccount(id) {
|
export function fetchAccount(id) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
dispatch(fetchRelationships([id]));
|
dispatch(fetchRelationships([id]));
|
||||||
|
@ -780,3 +782,8 @@ export function unpinAccountFail(error) {
|
||||||
error,
|
error,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const revealAccount = id => ({
|
||||||
|
type: ACCOUNT_REVEAL,
|
||||||
|
id,
|
||||||
|
});
|
||||||
|
|
|
@ -18,6 +18,8 @@ const messages = defineMessages({
|
||||||
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
|
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
|
||||||
mute_notifications: { id: 'account.mute_notifications', defaultMessage: 'Mute notifications from @{name}' },
|
mute_notifications: { id: 'account.mute_notifications', defaultMessage: 'Mute notifications from @{name}' },
|
||||||
unmute_notifications: { id: 'account.unmute_notifications', defaultMessage: 'Unmute notifications from @{name}' },
|
unmute_notifications: { id: 'account.unmute_notifications', defaultMessage: 'Unmute notifications from @{name}' },
|
||||||
|
mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
|
||||||
|
block: { id: 'account.block', defaultMessage: 'Block @{name}' },
|
||||||
});
|
});
|
||||||
|
|
||||||
export default @injectIntl
|
export default @injectIntl
|
||||||
|
@ -33,6 +35,7 @@ class Account extends ImmutablePureComponent {
|
||||||
hidden: PropTypes.bool,
|
hidden: PropTypes.bool,
|
||||||
actionIcon: PropTypes.string,
|
actionIcon: PropTypes.string,
|
||||||
actionTitle: PropTypes.string,
|
actionTitle: PropTypes.string,
|
||||||
|
defaultAction: PropTypes.string,
|
||||||
onActionClick: PropTypes.func,
|
onActionClick: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -61,7 +64,7 @@ class Account extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { account, intl, hidden, onActionClick, actionIcon, actionTitle } = this.props;
|
const { account, intl, hidden, onActionClick, actionIcon, actionTitle, defaultAction } = this.props;
|
||||||
|
|
||||||
if (!account) {
|
if (!account) {
|
||||||
return <div />;
|
return <div />;
|
||||||
|
@ -105,6 +108,10 @@ class Account extends ImmutablePureComponent {
|
||||||
{hidingNotificationsButton}
|
{hidingNotificationsButton}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
|
} else if (defaultAction === 'mute') {
|
||||||
|
buttons = <IconButton icon='volume-off' title={intl.formatMessage(messages.mute, { name: account.get('username') })} onClick={this.handleMute} />;
|
||||||
|
} else if (defaultAction === 'block') {
|
||||||
|
buttons = <IconButton icon='lock' title={intl.formatMessage(messages.block, { name: account.get('username') })} onClick={this.handleBlock} />;
|
||||||
} else if (!account.get('moved') || following) {
|
} else if (!account.get('moved') || following) {
|
||||||
buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following} />;
|
buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following} />;
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,11 +2,12 @@ import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import { autoPlayGif } from '../initial_state';
|
import { autoPlayGif } from '../initial_state';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
export default class Avatar extends React.PureComponent {
|
export default class Avatar extends React.PureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
account: ImmutablePropTypes.map.isRequired,
|
account: ImmutablePropTypes.map,
|
||||||
size: PropTypes.number.isRequired,
|
size: PropTypes.number.isRequired,
|
||||||
style: PropTypes.object,
|
style: PropTypes.object,
|
||||||
inline: PropTypes.bool,
|
inline: PropTypes.bool,
|
||||||
|
@ -37,15 +38,6 @@ export default class Avatar extends React.PureComponent {
|
||||||
const { account, size, animate, inline } = this.props;
|
const { account, size, animate, inline } = this.props;
|
||||||
const { hovering } = this.state;
|
const { hovering } = this.state;
|
||||||
|
|
||||||
const src = account.get('avatar');
|
|
||||||
const staticSrc = account.get('avatar_static');
|
|
||||||
|
|
||||||
let className = 'account__avatar';
|
|
||||||
|
|
||||||
if (inline) {
|
|
||||||
className = className + ' account__avatar-inline';
|
|
||||||
}
|
|
||||||
|
|
||||||
const style = {
|
const style = {
|
||||||
...this.props.style,
|
...this.props.style,
|
||||||
width: `${size}px`,
|
width: `${size}px`,
|
||||||
|
@ -53,15 +45,21 @@ export default class Avatar extends React.PureComponent {
|
||||||
backgroundSize: `${size}px ${size}px`,
|
backgroundSize: `${size}px ${size}px`,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (hovering || animate) {
|
if (account) {
|
||||||
style.backgroundImage = `url(${src})`;
|
const src = account.get('avatar');
|
||||||
} else {
|
const staticSrc = account.get('avatar_static');
|
||||||
style.backgroundImage = `url(${staticSrc})`;
|
|
||||||
|
if (hovering || animate) {
|
||||||
|
style.backgroundImage = `url(${src})`;
|
||||||
|
} else {
|
||||||
|
style.backgroundImage = `url(${staticSrc})`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={className}
|
className={classNames('account__avatar', { 'account__avatar-inline': inline })}
|
||||||
onMouseEnter={this.handleMouseEnter}
|
onMouseEnter={this.handleMouseEnter}
|
||||||
onMouseLeave={this.handleMouseLeave}
|
onMouseLeave={this.handleMouseLeave}
|
||||||
style={style}
|
style={style}
|
||||||
|
|
|
@ -82,6 +82,7 @@ class Header extends ImmutablePureComponent {
|
||||||
onEditAccountNote: PropTypes.func.isRequired,
|
onEditAccountNote: PropTypes.func.isRequired,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
domain: PropTypes.string.isRequired,
|
domain: PropTypes.string.isRequired,
|
||||||
|
hidden: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
openEditProfile = () => {
|
openEditProfile = () => {
|
||||||
|
@ -123,7 +124,7 @@ class Header extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { account, intl, domain } = this.props;
|
const { account, hidden, intl, domain } = this.props;
|
||||||
|
|
||||||
if (!account) {
|
if (!account) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -267,21 +268,25 @@ class Header extends ImmutablePureComponent {
|
||||||
{!suspended && info}
|
{!suspended && info}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<img src={autoPlayGif ? account.get('header') : account.get('header_static')} alt='' className='parallax' />
|
{!(suspended || hidden) && <img src={autoPlayGif ? account.get('header') : account.get('header_static')} alt='' className='parallax' />}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='account__header__bar'>
|
<div className='account__header__bar'>
|
||||||
<div className='account__header__tabs'>
|
<div className='account__header__tabs'>
|
||||||
<a className='avatar' href={account.get('url')} rel='noopener noreferrer' target='_blank'>
|
<a className='avatar' href={account.get('url')} rel='noopener noreferrer' target='_blank'>
|
||||||
<Avatar account={account} size={90} />
|
<Avatar account={suspended || hidden ? undefined : account} size={90} />
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div className='spacer' />
|
<div className='spacer' />
|
||||||
|
|
||||||
{!suspended && (
|
{!suspended && (
|
||||||
<div className='account__header__tabs__buttons'>
|
<div className='account__header__tabs__buttons'>
|
||||||
{actionBtn}
|
{!hidden && (
|
||||||
{bellBtn}
|
<React.Fragment>
|
||||||
|
{actionBtn}
|
||||||
|
{bellBtn}
|
||||||
|
</React.Fragment>
|
||||||
|
)}
|
||||||
|
|
||||||
<DropdownMenuContainer items={menu} icon='ellipsis-v' size={24} direction='right' />
|
<DropdownMenuContainer items={menu} icon='ellipsis-v' size={24} direction='right' />
|
||||||
</div>
|
</div>
|
||||||
|
@ -295,30 +300,30 @@ class Header extends ImmutablePureComponent {
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='account__header__extra'>
|
{!(suspended || hidden) && (
|
||||||
<div className='account__header__bio'>
|
<div className='account__header__extra'>
|
||||||
{fields.size > 0 && (
|
<div className='account__header__bio'>
|
||||||
<div className='account__header__fields'>
|
{fields.size > 0 && (
|
||||||
{fields.map((pair, i) => (
|
<div className='account__header__fields'>
|
||||||
<dl key={i}>
|
{fields.map((pair, i) => (
|
||||||
<dt dangerouslySetInnerHTML={{ __html: pair.get('name_emojified') }} title={pair.get('name')} className='translate' />
|
<dl key={i}>
|
||||||
|
<dt dangerouslySetInnerHTML={{ __html: pair.get('name_emojified') }} title={pair.get('name')} className='translate' />
|
||||||
|
|
||||||
<dd className={`${pair.get('verified_at') ? 'verified' : ''} translate`} title={pair.get('value_plain')}>
|
<dd className={`${pair.get('verified_at') ? 'verified' : ''} translate`} title={pair.get('value_plain')}>
|
||||||
{pair.get('verified_at') && <span title={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(pair.get('verified_at'), dateFormatOptions) })}><Icon id='check' className='verified__mark' /></span>} <span dangerouslySetInnerHTML={{ __html: pair.get('value_emojified') }} />
|
{pair.get('verified_at') && <span title={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(pair.get('verified_at'), dateFormatOptions) })}><Icon id='check' className='verified__mark' /></span>} <span dangerouslySetInnerHTML={{ __html: pair.get('value_emojified') }} />
|
||||||
</dd>
|
</dd>
|
||||||
</dl>
|
</dl>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{account.get('id') !== me && !suspended && <AccountNoteContainer account={account} />}
|
{account.get('id') !== me && <AccountNoteContainer account={account} />}
|
||||||
|
|
||||||
{account.get('note').length > 0 && account.get('note') !== '<p></p>' && <div className='account__header__content translate' dangerouslySetInnerHTML={content} />}
|
{account.get('note').length > 0 && account.get('note') !== '<p></p>' && <div className='account__header__content translate' dangerouslySetInnerHTML={content} />}
|
||||||
|
|
||||||
<div className='account__header__joined'><FormattedMessage id='account.joined' defaultMessage='Joined {date}' values={{ date: intl.formatDate(account.get('created_at'), { year: 'numeric', month: 'short', day: '2-digit' }) }} /></div>
|
<div className='account__header__joined'><FormattedMessage id='account.joined' defaultMessage='Joined {date}' values={{ date: intl.formatDate(account.get('created_at'), { year: 'numeric', month: 'short', day: '2-digit' }) }} /></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!suspended && (
|
|
||||||
<div className='account__header__extra__links'>
|
<div className='account__header__extra__links'>
|
||||||
<NavLink isActive={this.isStatusesPageActive} activeClassName='active' to={`/@${account.get('acct')}`} title={intl.formatNumber(account.get('statuses_count'))}>
|
<NavLink isActive={this.isStatusesPageActive} activeClassName='active' to={`/@${account.get('acct')}`} title={intl.formatNumber(account.get('statuses_count'))}>
|
||||||
<ShortNumber
|
<ShortNumber
|
||||||
|
@ -341,8 +346,8 @@ class Header extends ImmutablePureComponent {
|
||||||
/>
|
/>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -24,6 +24,7 @@ export default class Header extends ImmutablePureComponent {
|
||||||
onAddToList: PropTypes.func.isRequired,
|
onAddToList: PropTypes.func.isRequired,
|
||||||
hideTabs: PropTypes.bool,
|
hideTabs: PropTypes.bool,
|
||||||
domain: PropTypes.string.isRequired,
|
domain: PropTypes.string.isRequired,
|
||||||
|
hidden: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
static contextTypes = {
|
static contextTypes = {
|
||||||
|
@ -91,7 +92,7 @@ export default class Header extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { account, hideTabs } = this.props;
|
const { account, hidden, hideTabs } = this.props;
|
||||||
|
|
||||||
if (account === null) {
|
if (account === null) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -99,7 +100,7 @@ export default class Header extends ImmutablePureComponent {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='account-timeline__header'>
|
<div className='account-timeline__header'>
|
||||||
{account.get('moved') && <MovedNote from={account} to={account.get('moved')} />}
|
{(!hidden && account.get('moved')) && <MovedNote from={account} to={account.get('moved')} />}
|
||||||
|
|
||||||
<InnerHeader
|
<InnerHeader
|
||||||
account={account}
|
account={account}
|
||||||
|
@ -117,9 +118,10 @@ export default class Header extends ImmutablePureComponent {
|
||||||
onAddToList={this.handleAddToList}
|
onAddToList={this.handleAddToList}
|
||||||
onEditAccountNote={this.handleEditAccountNote}
|
onEditAccountNote={this.handleEditAccountNote}
|
||||||
domain={this.props.domain}
|
domain={this.props.domain}
|
||||||
|
hidden={hidden}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{!hideTabs && (
|
{!(hideTabs || hidden) && (
|
||||||
<div className='account__section-headline'>
|
<div className='account__section-headline'>
|
||||||
<NavLink exact to={`/@${account.get('acct')}`}><FormattedMessage id='account.posts' defaultMessage='Posts' /></NavLink>
|
<NavLink exact to={`/@${account.get('acct')}`}><FormattedMessage id='account.posts' defaultMessage='Posts' /></NavLink>
|
||||||
<NavLink exact to={`/@${account.get('acct')}/with_replies`}><FormattedMessage id='account.posts_with_replies' defaultMessage='Posts and replies' /></NavLink>
|
<NavLink exact to={`/@${account.get('acct')}/with_replies`}><FormattedMessage id='account.posts_with_replies' defaultMessage='Posts and replies' /></NavLink>
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { revealAccount } from 'mastodon/actions/accounts';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
import Button from 'mastodon/components/button';
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch, { accountId }) => ({
|
||||||
|
|
||||||
|
reveal () {
|
||||||
|
dispatch(revealAccount(accountId));
|
||||||
|
},
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
export default @connect(() => {}, mapDispatchToProps)
|
||||||
|
class LimitedAccountHint extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
accountId: PropTypes.string.isRequired,
|
||||||
|
reveal: PropTypes.func,
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { reveal } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='limited-account-hint'>
|
||||||
|
<p><FormattedMessage id='limited_account_hint.title' defaultMessage='This profile has been hidden by the moderators of your server.' /></p>
|
||||||
|
<Button onClick={reveal}><FormattedMessage id='limited_account_hint.action' defaultMessage='Show profile anyway' /></Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { makeGetAccount } from '../../../selectors';
|
import { makeGetAccount, getAccountHidden } from '../../../selectors';
|
||||||
import Header from '../components/header';
|
import Header from '../components/header';
|
||||||
import {
|
import {
|
||||||
followAccount,
|
followAccount,
|
||||||
|
@ -33,6 +33,7 @@ const makeMapStateToProps = () => {
|
||||||
const mapStateToProps = (state, { accountId }) => ({
|
const mapStateToProps = (state, { accountId }) => ({
|
||||||
account: getAccount(state, accountId),
|
account: getAccount(state, accountId),
|
||||||
domain: state.getIn(['meta', 'domain']),
|
domain: state.getIn(['meta', 'domain']),
|
||||||
|
hidden: getAccountHidden(state, accountId),
|
||||||
});
|
});
|
||||||
|
|
||||||
return mapStateToProps;
|
return mapStateToProps;
|
||||||
|
|
|
@ -16,6 +16,8 @@ import MissingIndicator from 'mastodon/components/missing_indicator';
|
||||||
import TimelineHint from 'mastodon/components/timeline_hint';
|
import TimelineHint from 'mastodon/components/timeline_hint';
|
||||||
import { me } from 'mastodon/initial_state';
|
import { me } from 'mastodon/initial_state';
|
||||||
import { connectTimeline, disconnectTimeline } from 'mastodon/actions/timelines';
|
import { connectTimeline, disconnectTimeline } from 'mastodon/actions/timelines';
|
||||||
|
import LimitedAccountHint from './components/limited_account_hint';
|
||||||
|
import { getAccountHidden } from 'mastodon/selectors';
|
||||||
|
|
||||||
const emptyList = ImmutableList();
|
const emptyList = ImmutableList();
|
||||||
|
|
||||||
|
@ -40,6 +42,7 @@ const mapStateToProps = (state, { params: { acct, id }, withReplies = false }) =
|
||||||
isLoading: state.getIn(['timelines', `account:${path}`, 'isLoading']),
|
isLoading: state.getIn(['timelines', `account:${path}`, 'isLoading']),
|
||||||
hasMore: state.getIn(['timelines', `account:${path}`, 'hasMore']),
|
hasMore: state.getIn(['timelines', `account:${path}`, 'hasMore']),
|
||||||
suspended: state.getIn(['accounts', accountId, 'suspended'], false),
|
suspended: state.getIn(['accounts', accountId, 'suspended'], false),
|
||||||
|
hidden: getAccountHidden(state, accountId),
|
||||||
blockedBy: state.getIn(['relationships', accountId, 'blocked_by'], false),
|
blockedBy: state.getIn(['relationships', accountId, 'blocked_by'], false),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -70,6 +73,7 @@ class AccountTimeline extends ImmutablePureComponent {
|
||||||
blockedBy: PropTypes.bool,
|
blockedBy: PropTypes.bool,
|
||||||
isAccount: PropTypes.bool,
|
isAccount: PropTypes.bool,
|
||||||
suspended: PropTypes.bool,
|
suspended: PropTypes.bool,
|
||||||
|
hidden: PropTypes.bool,
|
||||||
remote: PropTypes.bool,
|
remote: PropTypes.bool,
|
||||||
remoteUrl: PropTypes.string,
|
remoteUrl: PropTypes.string,
|
||||||
multiColumn: PropTypes.bool,
|
multiColumn: PropTypes.bool,
|
||||||
|
@ -128,7 +132,7 @@ class AccountTimeline extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { statusIds, featuredStatusIds, isLoading, hasMore, blockedBy, suspended, isAccount, multiColumn, remote, remoteUrl } = this.props;
|
const { accountId, statusIds, featuredStatusIds, isLoading, hasMore, blockedBy, suspended, isAccount, hidden, multiColumn, remote, remoteUrl } = this.props;
|
||||||
|
|
||||||
if (!isAccount) {
|
if (!isAccount) {
|
||||||
return (
|
return (
|
||||||
|
@ -149,8 +153,12 @@ class AccountTimeline extends ImmutablePureComponent {
|
||||||
|
|
||||||
let emptyMessage;
|
let emptyMessage;
|
||||||
|
|
||||||
|
const forceEmptyState = suspended || blockedBy || hidden;
|
||||||
|
|
||||||
if (suspended) {
|
if (suspended) {
|
||||||
emptyMessage = <FormattedMessage id='empty_column.account_suspended' defaultMessage='Account suspended' />;
|
emptyMessage = <FormattedMessage id='empty_column.account_suspended' defaultMessage='Account suspended' />;
|
||||||
|
} else if (hidden) {
|
||||||
|
emptyMessage = <LimitedAccountHint accountId={accountId} />;
|
||||||
} else if (blockedBy) {
|
} else if (blockedBy) {
|
||||||
emptyMessage = <FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' />;
|
emptyMessage = <FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' />;
|
||||||
} else if (remote && statusIds.isEmpty()) {
|
} else if (remote && statusIds.isEmpty()) {
|
||||||
|
@ -166,14 +174,14 @@ class AccountTimeline extends ImmutablePureComponent {
|
||||||
<ColumnBackButton multiColumn={multiColumn} />
|
<ColumnBackButton multiColumn={multiColumn} />
|
||||||
|
|
||||||
<StatusList
|
<StatusList
|
||||||
prepend={<HeaderContainer accountId={this.props.accountId} />}
|
prepend={<HeaderContainer accountId={this.props.accountId} hideTabs={forceEmptyState} />}
|
||||||
alwaysPrepend
|
alwaysPrepend
|
||||||
append={remoteMessage}
|
append={remoteMessage}
|
||||||
scrollKey='account_timeline'
|
scrollKey='account_timeline'
|
||||||
statusIds={(suspended || blockedBy) ? emptyList : statusIds}
|
statusIds={forceEmptyState ? emptyList : statusIds}
|
||||||
featuredStatusIds={featuredStatusIds}
|
featuredStatusIds={featuredStatusIds}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
hasMore={hasMore}
|
hasMore={!forceEmptyState && hasMore}
|
||||||
onLoadMore={this.handleLoadMore}
|
onLoadMore={this.handleLoadMore}
|
||||||
emptyMessage={emptyMessage}
|
emptyMessage={emptyMessage}
|
||||||
bindToDocument={!multiColumn}
|
bindToDocument={!multiColumn}
|
||||||
|
|
|
@ -69,7 +69,7 @@ class Blocks extends ImmutablePureComponent {
|
||||||
bindToDocument={!multiColumn}
|
bindToDocument={!multiColumn}
|
||||||
>
|
>
|
||||||
{accountIds.map(id =>
|
{accountIds.map(id =>
|
||||||
<AccountContainer key={id} id={id} />,
|
<AccountContainer key={id} id={id} defaultAction='block' />,
|
||||||
)}
|
)}
|
||||||
</ScrollableList>
|
</ScrollableList>
|
||||||
</Column>
|
</Column>
|
||||||
|
|
|
@ -19,6 +19,8 @@ import ColumnBackButton from '../../components/column_back_button';
|
||||||
import ScrollableList from '../../components/scrollable_list';
|
import ScrollableList from '../../components/scrollable_list';
|
||||||
import MissingIndicator from 'mastodon/components/missing_indicator';
|
import MissingIndicator from 'mastodon/components/missing_indicator';
|
||||||
import TimelineHint from 'mastodon/components/timeline_hint';
|
import TimelineHint from 'mastodon/components/timeline_hint';
|
||||||
|
import LimitedAccountHint from '../account_timeline/components/limited_account_hint';
|
||||||
|
import { getAccountHidden } from 'mastodon/selectors';
|
||||||
|
|
||||||
const mapStateToProps = (state, { params: { acct, id } }) => {
|
const mapStateToProps = (state, { params: { acct, id } }) => {
|
||||||
const accountId = id || state.getIn(['accounts_map', acct]);
|
const accountId = id || state.getIn(['accounts_map', acct]);
|
||||||
|
@ -37,6 +39,8 @@ const mapStateToProps = (state, { params: { acct, id } }) => {
|
||||||
accountIds: state.getIn(['user_lists', 'followers', accountId, 'items']),
|
accountIds: state.getIn(['user_lists', 'followers', accountId, 'items']),
|
||||||
hasMore: !!state.getIn(['user_lists', 'followers', accountId, 'next']),
|
hasMore: !!state.getIn(['user_lists', 'followers', accountId, 'next']),
|
||||||
isLoading: state.getIn(['user_lists', 'followers', accountId, 'isLoading'], true),
|
isLoading: state.getIn(['user_lists', 'followers', accountId, 'isLoading'], true),
|
||||||
|
suspended: state.getIn(['accounts', accountId, 'suspended'], false),
|
||||||
|
hidden: getAccountHidden(state, accountId),
|
||||||
blockedBy: state.getIn(['relationships', accountId, 'blocked_by'], false),
|
blockedBy: state.getIn(['relationships', accountId, 'blocked_by'], false),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -64,6 +68,8 @@ class Followers extends ImmutablePureComponent {
|
||||||
isLoading: PropTypes.bool,
|
isLoading: PropTypes.bool,
|
||||||
blockedBy: PropTypes.bool,
|
blockedBy: PropTypes.bool,
|
||||||
isAccount: PropTypes.bool,
|
isAccount: PropTypes.bool,
|
||||||
|
suspended: PropTypes.bool,
|
||||||
|
hidden: PropTypes.bool,
|
||||||
remote: PropTypes.bool,
|
remote: PropTypes.bool,
|
||||||
remoteUrl: PropTypes.string,
|
remoteUrl: PropTypes.string,
|
||||||
multiColumn: PropTypes.bool,
|
multiColumn: PropTypes.bool,
|
||||||
|
@ -101,7 +107,7 @@ class Followers extends ImmutablePureComponent {
|
||||||
}, 300, { leading: true });
|
}, 300, { leading: true });
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { accountIds, hasMore, blockedBy, isAccount, multiColumn, isLoading, remote, remoteUrl } = this.props;
|
const { accountId, accountIds, hasMore, blockedBy, isAccount, multiColumn, isLoading, suspended, hidden, remote, remoteUrl } = this.props;
|
||||||
|
|
||||||
if (!isAccount) {
|
if (!isAccount) {
|
||||||
return (
|
return (
|
||||||
|
@ -121,7 +127,13 @@ class Followers extends ImmutablePureComponent {
|
||||||
|
|
||||||
let emptyMessage;
|
let emptyMessage;
|
||||||
|
|
||||||
if (blockedBy) {
|
const forceEmptyState = blockedBy || suspended || hidden;
|
||||||
|
|
||||||
|
if (suspended) {
|
||||||
|
emptyMessage = <FormattedMessage id='empty_column.account_suspended' defaultMessage='Account suspended' />;
|
||||||
|
} else if (hidden) {
|
||||||
|
emptyMessage = <LimitedAccountHint accountId={accountId} />;
|
||||||
|
} else if (blockedBy) {
|
||||||
emptyMessage = <FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' />;
|
emptyMessage = <FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' />;
|
||||||
} else if (remote && accountIds.isEmpty()) {
|
} else if (remote && accountIds.isEmpty()) {
|
||||||
emptyMessage = <RemoteHint url={remoteUrl} />;
|
emptyMessage = <RemoteHint url={remoteUrl} />;
|
||||||
|
@ -137,7 +149,7 @@ class Followers extends ImmutablePureComponent {
|
||||||
|
|
||||||
<ScrollableList
|
<ScrollableList
|
||||||
scrollKey='followers'
|
scrollKey='followers'
|
||||||
hasMore={hasMore}
|
hasMore={!forceEmptyState && hasMore}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
onLoadMore={this.handleLoadMore}
|
onLoadMore={this.handleLoadMore}
|
||||||
prepend={<HeaderContainer accountId={this.props.accountId} hideTabs />}
|
prepend={<HeaderContainer accountId={this.props.accountId} hideTabs />}
|
||||||
|
@ -146,7 +158,7 @@ class Followers extends ImmutablePureComponent {
|
||||||
emptyMessage={emptyMessage}
|
emptyMessage={emptyMessage}
|
||||||
bindToDocument={!multiColumn}
|
bindToDocument={!multiColumn}
|
||||||
>
|
>
|
||||||
{blockedBy ? [] : accountIds.map(id =>
|
{forceEmptyState ? [] : accountIds.map(id =>
|
||||||
<AccountContainer key={id} id={id} withNote={false} />,
|
<AccountContainer key={id} id={id} withNote={false} />,
|
||||||
)}
|
)}
|
||||||
</ScrollableList>
|
</ScrollableList>
|
||||||
|
|
|
@ -19,6 +19,8 @@ import ColumnBackButton from '../../components/column_back_button';
|
||||||
import ScrollableList from '../../components/scrollable_list';
|
import ScrollableList from '../../components/scrollable_list';
|
||||||
import MissingIndicator from 'mastodon/components/missing_indicator';
|
import MissingIndicator from 'mastodon/components/missing_indicator';
|
||||||
import TimelineHint from 'mastodon/components/timeline_hint';
|
import TimelineHint from 'mastodon/components/timeline_hint';
|
||||||
|
import LimitedAccountHint from '../account_timeline/components/limited_account_hint';
|
||||||
|
import { getAccountHidden } from 'mastodon/selectors';
|
||||||
|
|
||||||
const mapStateToProps = (state, { params: { acct, id } }) => {
|
const mapStateToProps = (state, { params: { acct, id } }) => {
|
||||||
const accountId = id || state.getIn(['accounts_map', acct]);
|
const accountId = id || state.getIn(['accounts_map', acct]);
|
||||||
|
@ -37,6 +39,8 @@ const mapStateToProps = (state, { params: { acct, id } }) => {
|
||||||
accountIds: state.getIn(['user_lists', 'following', accountId, 'items']),
|
accountIds: state.getIn(['user_lists', 'following', accountId, 'items']),
|
||||||
hasMore: !!state.getIn(['user_lists', 'following', accountId, 'next']),
|
hasMore: !!state.getIn(['user_lists', 'following', accountId, 'next']),
|
||||||
isLoading: state.getIn(['user_lists', 'following', accountId, 'isLoading'], true),
|
isLoading: state.getIn(['user_lists', 'following', accountId, 'isLoading'], true),
|
||||||
|
suspended: state.getIn(['accounts', accountId, 'suspended'], false),
|
||||||
|
hidden: getAccountHidden(state, accountId),
|
||||||
blockedBy: state.getIn(['relationships', accountId, 'blocked_by'], false),
|
blockedBy: state.getIn(['relationships', accountId, 'blocked_by'], false),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -64,6 +68,8 @@ class Following extends ImmutablePureComponent {
|
||||||
isLoading: PropTypes.bool,
|
isLoading: PropTypes.bool,
|
||||||
blockedBy: PropTypes.bool,
|
blockedBy: PropTypes.bool,
|
||||||
isAccount: PropTypes.bool,
|
isAccount: PropTypes.bool,
|
||||||
|
suspended: PropTypes.bool,
|
||||||
|
hidden: PropTypes.bool,
|
||||||
remote: PropTypes.bool,
|
remote: PropTypes.bool,
|
||||||
remoteUrl: PropTypes.string,
|
remoteUrl: PropTypes.string,
|
||||||
multiColumn: PropTypes.bool,
|
multiColumn: PropTypes.bool,
|
||||||
|
@ -101,7 +107,7 @@ class Following extends ImmutablePureComponent {
|
||||||
}, 300, { leading: true });
|
}, 300, { leading: true });
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { accountIds, hasMore, blockedBy, isAccount, multiColumn, isLoading, remote, remoteUrl } = this.props;
|
const { accountId, accountIds, hasMore, blockedBy, isAccount, multiColumn, isLoading, suspended, hidden, remote, remoteUrl } = this.props;
|
||||||
|
|
||||||
if (!isAccount) {
|
if (!isAccount) {
|
||||||
return (
|
return (
|
||||||
|
@ -121,7 +127,13 @@ class Following extends ImmutablePureComponent {
|
||||||
|
|
||||||
let emptyMessage;
|
let emptyMessage;
|
||||||
|
|
||||||
if (blockedBy) {
|
const forceEmptyState = blockedBy || suspended || hidden;
|
||||||
|
|
||||||
|
if (suspended) {
|
||||||
|
emptyMessage = <FormattedMessage id='empty_column.account_suspended' defaultMessage='Account suspended' />;
|
||||||
|
} else if (hidden) {
|
||||||
|
emptyMessage = <LimitedAccountHint accountId={accountId} />;
|
||||||
|
} else if (blockedBy) {
|
||||||
emptyMessage = <FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' />;
|
emptyMessage = <FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' />;
|
||||||
} else if (remote && accountIds.isEmpty()) {
|
} else if (remote && accountIds.isEmpty()) {
|
||||||
emptyMessage = <RemoteHint url={remoteUrl} />;
|
emptyMessage = <RemoteHint url={remoteUrl} />;
|
||||||
|
@ -137,7 +149,7 @@ class Following extends ImmutablePureComponent {
|
||||||
|
|
||||||
<ScrollableList
|
<ScrollableList
|
||||||
scrollKey='following'
|
scrollKey='following'
|
||||||
hasMore={hasMore}
|
hasMore={!forceEmptyState && hasMore}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
onLoadMore={this.handleLoadMore}
|
onLoadMore={this.handleLoadMore}
|
||||||
prepend={<HeaderContainer accountId={this.props.accountId} hideTabs />}
|
prepend={<HeaderContainer accountId={this.props.accountId} hideTabs />}
|
||||||
|
@ -146,7 +158,7 @@ class Following extends ImmutablePureComponent {
|
||||||
emptyMessage={emptyMessage}
|
emptyMessage={emptyMessage}
|
||||||
bindToDocument={!multiColumn}
|
bindToDocument={!multiColumn}
|
||||||
>
|
>
|
||||||
{blockedBy ? [] : accountIds.map(id =>
|
{forceEmptyState ? [] : accountIds.map(id =>
|
||||||
<AccountContainer key={id} id={id} withNote={false} />,
|
<AccountContainer key={id} id={id} withNote={false} />,
|
||||||
)}
|
)}
|
||||||
</ScrollableList>
|
</ScrollableList>
|
||||||
|
|
|
@ -69,7 +69,7 @@ class Mutes extends ImmutablePureComponent {
|
||||||
bindToDocument={!multiColumn}
|
bindToDocument={!multiColumn}
|
||||||
>
|
>
|
||||||
{accountIds.map(id =>
|
{accountIds.map(id =>
|
||||||
<AccountContainer key={id} id={id} />,
|
<AccountContainer key={id} id={id} defaultAction='mute' />,
|
||||||
)}
|
)}
|
||||||
</ScrollableList>
|
</ScrollableList>
|
||||||
</Column>
|
</Column>
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { ACCOUNT_IMPORT, ACCOUNTS_IMPORT } from '../actions/importer';
|
import { ACCOUNT_IMPORT, ACCOUNTS_IMPORT } from 'mastodon/actions/importer';
|
||||||
|
import { ACCOUNT_REVEAL } from 'mastodon/actions/accounts';
|
||||||
import { Map as ImmutableMap, fromJS } from 'immutable';
|
import { Map as ImmutableMap, fromJS } from 'immutable';
|
||||||
|
|
||||||
const initialState = ImmutableMap();
|
const initialState = ImmutableMap();
|
||||||
|
@ -10,6 +11,8 @@ const normalizeAccount = (state, account) => {
|
||||||
delete account.following_count;
|
delete account.following_count;
|
||||||
delete account.statuses_count;
|
delete account.statuses_count;
|
||||||
|
|
||||||
|
account.hidden = state.getIn([account.id, 'hidden']) === false ? false : account.limited;
|
||||||
|
|
||||||
return state.set(account.id, fromJS(account));
|
return state.set(account.id, fromJS(account));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -27,6 +30,8 @@ export default function accounts(state = initialState, action) {
|
||||||
return normalizeAccount(state, action.account);
|
return normalizeAccount(state, action.account);
|
||||||
case ACCOUNTS_IMPORT:
|
case ACCOUNTS_IMPORT:
|
||||||
return normalizeAccounts(state, action.accounts);
|
return normalizeAccounts(state, action.accounts);
|
||||||
|
case ACCOUNT_REVEAL:
|
||||||
|
return state.setIn([action.id, 'hidden'], false);
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
|
@ -175,3 +175,11 @@ export const getAccountGallery = createSelector([
|
||||||
|
|
||||||
return medias;
|
return medias;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const getAccountHidden = createSelector([
|
||||||
|
(state, id) => state.getIn(['accounts', id, 'hidden']),
|
||||||
|
(state, id) => state.getIn(['relationships', id, 'following']) || state.getIn(['relationships', id, 'requested']),
|
||||||
|
(state, id) => id === me,
|
||||||
|
], (hidden, followingOrRequested, isSelf) => {
|
||||||
|
return hidden && !(isSelf || followingOrRequested);
|
||||||
|
});
|
||||||
|
|
|
@ -4037,6 +4037,15 @@ a.status-card.compact:hover {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.limited-account-hint {
|
||||||
|
p {
|
||||||
|
color: $secondary-text-color;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.empty-column-indicator,
|
.empty-column-indicator,
|
||||||
.error-column,
|
.error-column,
|
||||||
.follow_requests-unlocked_explanation {
|
.follow_requests-unlocked_explanation {
|
||||||
|
|
|
@ -11,6 +11,7 @@ class EmojiFormatter
|
||||||
# @param [Array<CustomEmoji>] custom_emojis
|
# @param [Array<CustomEmoji>] custom_emojis
|
||||||
# @param [Hash] options
|
# @param [Hash] options
|
||||||
# @option options [Boolean] :animate
|
# @option options [Boolean] :animate
|
||||||
|
# @option options [String] :style
|
||||||
def initialize(html, custom_emojis, options = {})
|
def initialize(html, custom_emojis, options = {})
|
||||||
raise ArgumentError unless html.html_safe?
|
raise ArgumentError unless html.html_safe?
|
||||||
|
|
||||||
|
@ -85,14 +86,29 @@ class EmojiFormatter
|
||||||
def image_for_emoji(shortcode, emoji)
|
def image_for_emoji(shortcode, emoji)
|
||||||
original_url, static_url = emoji
|
original_url, static_url = emoji
|
||||||
|
|
||||||
if animate?
|
image_tag(
|
||||||
image_tag(original_url, draggable: false, class: 'emojione', alt: ":#{shortcode}:", title: ":#{shortcode}:")
|
animate? ? original_url : static_url,
|
||||||
else
|
image_attributes.merge(alt: ":#{shortcode}:", title: ":#{shortcode}:", data: image_data_attributes(original_url, static_url))
|
||||||
image_tag(original_url, draggable: false, class: 'emojione custom-emoji', alt: ":#{shortcode}:", title: ":#{shortcode}:", data: { original: original_url, static: static_url })
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def image_attributes
|
||||||
|
{ rel: 'emoji', draggable: false, width: 16, height: 16, class: image_class_names, style: image_style }
|
||||||
|
end
|
||||||
|
|
||||||
|
def image_data_attributes(original_url, static_url)
|
||||||
|
{ original: original_url, static: static_url } unless animate?
|
||||||
|
end
|
||||||
|
|
||||||
|
def image_class_names
|
||||||
|
animate? ? 'emojione' : 'emojione custom-emoji'
|
||||||
|
end
|
||||||
|
|
||||||
|
def image_style
|
||||||
|
@options[:style]
|
||||||
end
|
end
|
||||||
|
|
||||||
def animate?
|
def animate?
|
||||||
@options[:animate]
|
@options[:animate] || @options.key?(:style)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class RSS::Builder
|
||||||
|
attr_reader :dsl
|
||||||
|
|
||||||
|
def self.build
|
||||||
|
new.tap do |builder|
|
||||||
|
yield builder.dsl
|
||||||
|
end.to_xml
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize
|
||||||
|
@dsl = RSS::Channel.new
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_xml
|
||||||
|
('<?xml version="1.0" encoding="UTF-8"?>'.dup << Ox.dump(wrap_in_document, effort: :tolerant)).force_encoding('UTF-8')
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def wrap_in_document
|
||||||
|
Ox::Document.new(version: '1.0').tap do |document|
|
||||||
|
document << Ox::Element.new('rss').tap do |rss|
|
||||||
|
rss['version'] = '2.0'
|
||||||
|
rss['xmlns:webfeeds'] = 'http://webfeeds.org/rss/1.0'
|
||||||
|
rss['xmlns:media'] = 'http://search.yahoo.com/mrss/'
|
||||||
|
|
||||||
|
rss << @dsl.to_element
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,49 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class RSS::Channel < RSS::Element
|
||||||
|
def initialize
|
||||||
|
super()
|
||||||
|
|
||||||
|
@root = create_element('channel')
|
||||||
|
end
|
||||||
|
|
||||||
|
def title(str)
|
||||||
|
append_element('title', str)
|
||||||
|
end
|
||||||
|
|
||||||
|
def link(str)
|
||||||
|
append_element('link', str)
|
||||||
|
end
|
||||||
|
|
||||||
|
def last_build_date(date)
|
||||||
|
append_element('lastBuildDate', date.to_formatted_s(:rfc822))
|
||||||
|
end
|
||||||
|
|
||||||
|
def image(url, title, link)
|
||||||
|
append_element('image') do |image|
|
||||||
|
image << create_element('url', url)
|
||||||
|
image << create_element('title', title)
|
||||||
|
image << create_element('link', link)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def description(str)
|
||||||
|
append_element('description', str)
|
||||||
|
end
|
||||||
|
|
||||||
|
def generator(str)
|
||||||
|
append_element('generator', str)
|
||||||
|
end
|
||||||
|
|
||||||
|
def icon(str)
|
||||||
|
append_element('webfeeds:icon', str)
|
||||||
|
end
|
||||||
|
|
||||||
|
def logo(str)
|
||||||
|
append_element('webfeeds:logo', str)
|
||||||
|
end
|
||||||
|
|
||||||
|
def item(&block)
|
||||||
|
@root << RSS::Item.with(&block)
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,24 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class RSS::Element
|
||||||
|
def self.with(*args, &block)
|
||||||
|
new(*args).tap(&block).to_element
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_element(name, content = nil)
|
||||||
|
Ox::Element.new(name).tap do |element|
|
||||||
|
yield element if block_given?
|
||||||
|
element << content if content.present?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def append_element(name, content = nil)
|
||||||
|
@root << create_element(name, content).tap do |element|
|
||||||
|
yield element if block_given?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_element
|
||||||
|
@root
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,45 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class RSS::Item < RSS::Element
|
||||||
|
def initialize
|
||||||
|
super()
|
||||||
|
|
||||||
|
@root = create_element('item')
|
||||||
|
end
|
||||||
|
|
||||||
|
def title(str)
|
||||||
|
append_element('title', str)
|
||||||
|
end
|
||||||
|
|
||||||
|
def link(str)
|
||||||
|
append_element('guid', str) do |guid|
|
||||||
|
guid['isPermaLink'] = 'true'
|
||||||
|
end
|
||||||
|
|
||||||
|
append_element('link', str)
|
||||||
|
end
|
||||||
|
|
||||||
|
def pub_date(date)
|
||||||
|
append_element('pubDate', date.to_formatted_s(:rfc822))
|
||||||
|
end
|
||||||
|
|
||||||
|
def description(str)
|
||||||
|
append_element('description', str)
|
||||||
|
end
|
||||||
|
|
||||||
|
def category(str)
|
||||||
|
append_element('category', str)
|
||||||
|
end
|
||||||
|
|
||||||
|
def enclosure(url, type, size)
|
||||||
|
append_element('enclosure') do |enclosure|
|
||||||
|
enclosure['url'] = url
|
||||||
|
enclosure['length'] = size
|
||||||
|
enclosure['type'] = type
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def media_content(url, type, size, &block)
|
||||||
|
@root << RSS::MediaContent.with(url, type, size, &block)
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,29 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class RSS::MediaContent < RSS::Element
|
||||||
|
def initialize(url, type, size)
|
||||||
|
super()
|
||||||
|
|
||||||
|
@root = create_element('media:content') do |content|
|
||||||
|
content['url'] = url
|
||||||
|
content['type'] = type
|
||||||
|
content['fileSize'] = size
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def medium(str)
|
||||||
|
@root['medium'] = str
|
||||||
|
end
|
||||||
|
|
||||||
|
def rating(str)
|
||||||
|
append_element('media:rating', str) do |rating|
|
||||||
|
rating['scheme'] = 'urn:simple'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def description(str)
|
||||||
|
append_element('media:description', str) do |description|
|
||||||
|
description['type'] = 'plain'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,55 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
class RSS::Serializer
|
|
||||||
include FormattingHelper
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def render_statuses(builder, statuses)
|
|
||||||
statuses.each do |status|
|
|
||||||
builder.item do |item|
|
|
||||||
item.title(status_title(status))
|
|
||||||
.link(ActivityPub::TagManager.instance.url_for(status))
|
|
||||||
.pub_date(status.created_at)
|
|
||||||
.description(status_description(status))
|
|
||||||
|
|
||||||
status.ordered_media_attachments.each do |media|
|
|
||||||
item.enclosure(full_asset_url(media.file.url(:original, false)), media.file.content_type, media.file.size)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def status_title(status)
|
|
||||||
preview = status.proper.spoiler_text.presence || status.proper.text
|
|
||||||
|
|
||||||
if preview.length > 30 || preview[0, 30].include?("\n")
|
|
||||||
preview = preview[0, 30]
|
|
||||||
preview = preview[0, preview.index("\n").presence || 30] + '…'
|
|
||||||
end
|
|
||||||
|
|
||||||
preview = "#{status.proper.spoiler_text.present? ? 'CW ' : ''}“#{preview}”#{status.proper.sensitive? ? ' (sensitive)' : ''}"
|
|
||||||
|
|
||||||
if status.reblog?
|
|
||||||
"#{status.account.acct} boosted #{status.reblog.account.acct}: #{preview}"
|
|
||||||
else
|
|
||||||
"#{status.account.acct}: #{preview}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def status_description(status)
|
|
||||||
if status.proper.spoiler_text?
|
|
||||||
status.proper.spoiler_text
|
|
||||||
else
|
|
||||||
html = status_content_format(status.proper).to_str
|
|
||||||
after_html = ''
|
|
||||||
|
|
||||||
if status.proper.preloadable_poll
|
|
||||||
poll_options_html = status.proper.preloadable_poll.options.map { |o| "[ ] #{o}" }.join('<br />')
|
|
||||||
after_html = "<p>#{poll_options_html}</p>"
|
|
||||||
end
|
|
||||||
|
|
||||||
"#{html}#{after_html}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,130 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
class RSSBuilder
|
|
||||||
class ItemBuilder
|
|
||||||
def initialize
|
|
||||||
@item = Ox::Element.new('item')
|
|
||||||
end
|
|
||||||
|
|
||||||
def title(str)
|
|
||||||
@item << (Ox::Element.new('title') << str)
|
|
||||||
|
|
||||||
self
|
|
||||||
end
|
|
||||||
|
|
||||||
def link(str)
|
|
||||||
@item << Ox::Element.new('guid').tap do |guid|
|
|
||||||
guid['isPermalink'] = 'true'
|
|
||||||
guid << str
|
|
||||||
end
|
|
||||||
|
|
||||||
@item << (Ox::Element.new('link') << str)
|
|
||||||
|
|
||||||
self
|
|
||||||
end
|
|
||||||
|
|
||||||
def pub_date(date)
|
|
||||||
@item << (Ox::Element.new('pubDate') << date.to_formatted_s(:rfc822))
|
|
||||||
|
|
||||||
self
|
|
||||||
end
|
|
||||||
|
|
||||||
def description(str)
|
|
||||||
@item << (Ox::Element.new('description') << str)
|
|
||||||
|
|
||||||
self
|
|
||||||
end
|
|
||||||
|
|
||||||
def enclosure(url, type, size)
|
|
||||||
@item << Ox::Element.new('enclosure').tap do |enclosure|
|
|
||||||
enclosure['url'] = url
|
|
||||||
enclosure['length'] = size
|
|
||||||
enclosure['type'] = type
|
|
||||||
end
|
|
||||||
|
|
||||||
self
|
|
||||||
end
|
|
||||||
|
|
||||||
def to_element
|
|
||||||
@item
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def initialize
|
|
||||||
@document = Ox::Document.new(version: '1.0')
|
|
||||||
@channel = Ox::Element.new('channel')
|
|
||||||
|
|
||||||
@document << (rss << @channel)
|
|
||||||
end
|
|
||||||
|
|
||||||
def title(str)
|
|
||||||
@channel << (Ox::Element.new('title') << str)
|
|
||||||
|
|
||||||
self
|
|
||||||
end
|
|
||||||
|
|
||||||
def link(str)
|
|
||||||
@channel << (Ox::Element.new('link') << str)
|
|
||||||
|
|
||||||
self
|
|
||||||
end
|
|
||||||
|
|
||||||
def image(str)
|
|
||||||
@channel << Ox::Element.new('image').tap do |image|
|
|
||||||
image << (Ox::Element.new('url') << str)
|
|
||||||
image << (Ox::Element.new('title') << '')
|
|
||||||
image << (Ox::Element.new('link') << '')
|
|
||||||
end
|
|
||||||
|
|
||||||
@channel << (Ox::Element.new('webfeeds:icon') << str)
|
|
||||||
|
|
||||||
self
|
|
||||||
end
|
|
||||||
|
|
||||||
def cover(str)
|
|
||||||
@channel << Ox::Element.new('webfeeds:cover').tap do |cover|
|
|
||||||
cover['image'] = str
|
|
||||||
end
|
|
||||||
|
|
||||||
self
|
|
||||||
end
|
|
||||||
|
|
||||||
def logo(str)
|
|
||||||
@channel << (Ox::Element.new('webfeeds:logo') << str)
|
|
||||||
|
|
||||||
self
|
|
||||||
end
|
|
||||||
|
|
||||||
def accent_color(str)
|
|
||||||
@channel << (Ox::Element.new('webfeeds:accentColor') << str)
|
|
||||||
|
|
||||||
self
|
|
||||||
end
|
|
||||||
|
|
||||||
def description(str)
|
|
||||||
@channel << (Ox::Element.new('description') << str)
|
|
||||||
|
|
||||||
self
|
|
||||||
end
|
|
||||||
|
|
||||||
def item
|
|
||||||
@channel << ItemBuilder.new.tap do |item|
|
|
||||||
yield item
|
|
||||||
end.to_element
|
|
||||||
|
|
||||||
self
|
|
||||||
end
|
|
||||||
|
|
||||||
def to_xml
|
|
||||||
('<?xml version="1.0" encoding="UTF-8"?>' + Ox.dump(@document, effort: :tolerant)).force_encoding('UTF-8')
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def rss
|
|
||||||
Ox::Element.new('rss').tap do |rss|
|
|
||||||
rss['version'] = '2.0'
|
|
||||||
rss['xmlns:webfeeds'] = 'http://webfeeds.org/rss/1.0'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -13,6 +13,7 @@ class REST::AccountSerializer < ActiveModel::Serializer
|
||||||
has_many :emojis, serializer: REST::CustomEmojiSerializer
|
has_many :emojis, serializer: REST::CustomEmojiSerializer
|
||||||
|
|
||||||
attribute :suspended, if: :suspended?
|
attribute :suspended, if: :suspended?
|
||||||
|
attribute :silenced, key: :limited, if: :silenced?
|
||||||
|
|
||||||
class FieldSerializer < ActiveModel::Serializer
|
class FieldSerializer < ActiveModel::Serializer
|
||||||
include FormattingHelper
|
include FormattingHelper
|
||||||
|
@ -102,7 +103,11 @@ class REST::AccountSerializer < ActiveModel::Serializer
|
||||||
object.suspended?
|
object.suspended?
|
||||||
end
|
end
|
||||||
|
|
||||||
delegate :suspended?, to: :object
|
def silenced
|
||||||
|
object.silenced?
|
||||||
|
end
|
||||||
|
|
||||||
|
delegate :suspended?, :silenced?, to: :object
|
||||||
|
|
||||||
def moved_and_not_nested?
|
def moved_and_not_nested?
|
||||||
object.moved? && object.moved_to_account.moved_to_account_id.nil?
|
object.moved? && object.moved_to_account.moved_to_account_id.nil?
|
||||||
|
|
|
@ -1,28 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
class RSS::AccountSerializer < RSS::Serializer
|
|
||||||
include ActionView::Helpers::NumberHelper
|
|
||||||
include AccountsHelper
|
|
||||||
include RoutingHelper
|
|
||||||
|
|
||||||
def render(account, statuses, tag)
|
|
||||||
builder = RSSBuilder.new
|
|
||||||
|
|
||||||
builder.title("#{display_name(account)} (@#{account.local_username_and_domain})")
|
|
||||||
.description(account_description(account))
|
|
||||||
.link(tag.present? ? short_account_tag_url(account, tag) : short_account_url(account))
|
|
||||||
.logo(full_pack_url('media/images/logo.svg'))
|
|
||||||
.accent_color('2b90d9')
|
|
||||||
|
|
||||||
builder.image(full_asset_url(account.avatar.url(:original))) if account.avatar?
|
|
||||||
builder.cover(full_asset_url(account.header.url(:original))) if account.header?
|
|
||||||
|
|
||||||
render_statuses(builder, statuses)
|
|
||||||
|
|
||||||
builder.to_xml
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.render(account, statuses, tag)
|
|
||||||
new.render(account, statuses, tag)
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,25 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
class RSS::TagSerializer < RSS::Serializer
|
|
||||||
include ActionView::Helpers::NumberHelper
|
|
||||||
include ActionView::Helpers::SanitizeHelper
|
|
||||||
include RoutingHelper
|
|
||||||
|
|
||||||
def render(tag, statuses)
|
|
||||||
builder = RSSBuilder.new
|
|
||||||
|
|
||||||
builder.title("##{tag.name}")
|
|
||||||
.description(strip_tags(I18n.t('about.about_hashtag_html', hashtag: tag.name)))
|
|
||||||
.link(tag_url(tag))
|
|
||||||
.logo(full_pack_url('media/images/logo.svg'))
|
|
||||||
.accent_color('2b90d9')
|
|
||||||
|
|
||||||
render_statuses(builder, statuses)
|
|
||||||
|
|
||||||
builder.to_xml
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.render(tag, statuses)
|
|
||||||
new.render(tag, statuses)
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
RSS::Builder.build do |doc|
|
||||||
|
doc.title(display_name(@account))
|
||||||
|
doc.description(I18n.t('rss.descriptions.account', acct: @account.local_username_and_domain))
|
||||||
|
doc.link(params[:tag].present? ? short_account_tag_url(@account, params[:tag]) : short_account_url(@account))
|
||||||
|
doc.image(full_asset_url(@account.avatar.url(:original)), display_name(@account), params[:tag].present? ? short_account_tag_url(@account, params[:tag]) : short_account_url(@account))
|
||||||
|
doc.last_build_date(@statuses.first.created_at) if @statuses.any?
|
||||||
|
doc.icon(full_asset_url(@account.avatar.url(:original)))
|
||||||
|
doc.logo(full_pack_url('media/images/logo_transparent_white.svg'))
|
||||||
|
doc.generator("Mastodon v#{Mastodon::Version.to_s}")
|
||||||
|
|
||||||
|
@statuses.each do |status|
|
||||||
|
doc.item do |item|
|
||||||
|
item.title(l(status.created_at))
|
||||||
|
item.link(ActivityPub::TagManager.instance.url_for(status))
|
||||||
|
item.pub_date(status.created_at)
|
||||||
|
item.description(rss_status_content_format(status))
|
||||||
|
|
||||||
|
if status.ordered_media_attachments.first&.audio?
|
||||||
|
media = status.ordered_media_attachments.first
|
||||||
|
item.enclosure(full_asset_url(media.file.url(:original, false)), media.file.content_type, media.file.size)
|
||||||
|
end
|
||||||
|
|
||||||
|
status.ordered_media_attachments.each do |media|
|
||||||
|
item.media_content(full_asset_url(media.file.url(:original, false)), media.file.content_type, media.file.size) do |media_content|
|
||||||
|
media_content.medium(media.gifv? ? 'image' : media.type.to_s)
|
||||||
|
media_content.rating(status.sensitive? ? 'adult' : 'nonadult')
|
||||||
|
media_content.description(media.description) if media.description.present?
|
||||||
|
media_content.thumbnail(media.thumbnail.url(:original, false)) if media.thumbnail?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
status.tags.each do |tag|
|
||||||
|
item.category(tag.name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,36 @@
|
||||||
|
RSS::Builder.build do |doc|
|
||||||
|
doc.title("##{@tag.name}")
|
||||||
|
doc.description(I18n.t('rss.descriptions.tag', hashtag: @tag.name))
|
||||||
|
doc.link(tag_url(@tag))
|
||||||
|
doc.last_build_date(@statuses.first.created_at) if @statuses.any?
|
||||||
|
doc.icon(full_asset_url(@account.avatar.url(:original)))
|
||||||
|
doc.logo(full_pack_url('media/images/logo_transparent_white.svg'))
|
||||||
|
doc.generator("Mastodon v#{Mastodon::Version.to_s}")
|
||||||
|
|
||||||
|
@statuses.each do |status|
|
||||||
|
doc.item do |item|
|
||||||
|
item.title(l(status.created_at))
|
||||||
|
item.link(ActivityPub::TagManager.instance.url_for(status))
|
||||||
|
item.pub_date(status.created_at)
|
||||||
|
item.description(rss_status_content_format(status))
|
||||||
|
|
||||||
|
if status.ordered_media_attachments.first&.audio?
|
||||||
|
media = status.ordered_media_attachments.first
|
||||||
|
item.enclosure(full_asset_url(media.file.url(:original, false)), media.file.content_type, media.file.size)
|
||||||
|
end
|
||||||
|
|
||||||
|
status.ordered_media_attachments.each do |media|
|
||||||
|
item.media_content(full_asset_url(media.file.url(:original, false)), media.file.content_type, media.file.size) do |media_content|
|
||||||
|
media_content.medium(media.gifv? ? 'image' : media.type.to_s)
|
||||||
|
media_content.rating(status.sensitive? ? 'adult' : 'nonadult')
|
||||||
|
media_content.description(media.description) if media.description.present?
|
||||||
|
media_content.thumbnail(media.thumbnail.url(:original, false)) if media.thumbnail?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
status.tags.each do |tag|
|
||||||
|
item.category(tag.name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1357,6 +1357,11 @@ en:
|
||||||
reports:
|
reports:
|
||||||
errors:
|
errors:
|
||||||
invalid_rules: does not reference valid rules
|
invalid_rules: does not reference valid rules
|
||||||
|
rss:
|
||||||
|
content_warning: 'Content warning:'
|
||||||
|
descriptions:
|
||||||
|
account: Public posts from @%{acct}
|
||||||
|
tag: 'Public posts tagged #%{hashtag}'
|
||||||
scheduled_statuses:
|
scheduled_statuses:
|
||||||
over_daily_limit: You have exceeded the limit of %{limit} scheduled posts for today
|
over_daily_limit: You have exceeded the limit of %{limit} scheduled posts for today
|
||||||
over_total_limit: You have exceeded the limit of %{limit} scheduled posts
|
over_total_limit: You have exceeded the limit of %{limit} scheduled posts
|
||||||
|
|
|
@ -8,6 +8,14 @@ namespace :mastodon do
|
||||||
prompt = TTY::Prompt.new
|
prompt = TTY::Prompt.new
|
||||||
env = {}
|
env = {}
|
||||||
|
|
||||||
|
# When the application code gets loaded, it runs `lib/mastodon/redis_configuration.rb`.
|
||||||
|
# This happens before application environment configuration and sets REDIS_URL etc.
|
||||||
|
# These variables are then used even when REDIS_HOST etc. are changed, so clear them
|
||||||
|
# out so they don't interfer with our new configuration.
|
||||||
|
ENV.delete('REDIS_URL')
|
||||||
|
ENV.delete('CACHE_REDIS_URL')
|
||||||
|
ENV.delete('SIDEKIQ_REDIS_URL')
|
||||||
|
|
||||||
begin
|
begin
|
||||||
prompt.say('Your instance is identified by its domain name. Changing it afterward will break things.')
|
prompt.say('Your instance is identified by its domain name. Changing it afterward will break things.')
|
||||||
env['LOCAL_DOMAIN'] = prompt.ask('Domain name:') do |q|
|
env['LOCAL_DOMAIN'] = prompt.ask('Domain name:') do |q|
|
||||||
|
|
10
package.json
10
package.json
|
@ -40,7 +40,7 @@
|
||||||
"@gamestdio/websocket": "^0.3.2",
|
"@gamestdio/websocket": "^0.3.2",
|
||||||
"@github/webauthn-json": "^0.5.7",
|
"@github/webauthn-json": "^0.5.7",
|
||||||
"@rails/ujs": "^6.1.5",
|
"@rails/ujs": "^6.1.5",
|
||||||
"array-includes": "^3.1.4",
|
"array-includes": "^3.1.5",
|
||||||
"atrament": "0.2.4",
|
"atrament": "0.2.4",
|
||||||
"arrow-key-navigation": "^1.2.0",
|
"arrow-key-navigation": "^1.2.0",
|
||||||
"autoprefixer": "^9.8.8",
|
"autoprefixer": "^9.8.8",
|
||||||
|
@ -110,7 +110,7 @@
|
||||||
"react-redux-loading-bar": "^4.0.8",
|
"react-redux-loading-bar": "^4.0.8",
|
||||||
"react-router-dom": "^4.1.1",
|
"react-router-dom": "^4.1.1",
|
||||||
"react-router-scroll-4": "^1.0.0-beta.1",
|
"react-router-scroll-4": "^1.0.0-beta.1",
|
||||||
"react-select": "^5.3.1",
|
"react-select": "^5.3.2",
|
||||||
"react-sparklines": "^1.7.0",
|
"react-sparklines": "^1.7.0",
|
||||||
"react-swipeable-views": "^0.14.0",
|
"react-swipeable-views": "^0.14.0",
|
||||||
"react-textarea-autosize": "^8.3.3",
|
"react-textarea-autosize": "^8.3.3",
|
||||||
|
@ -147,14 +147,14 @@
|
||||||
"@testing-library/jest-dom": "^5.16.4",
|
"@testing-library/jest-dom": "^5.16.4",
|
||||||
"@testing-library/react": "^12.1.5",
|
"@testing-library/react": "^12.1.5",
|
||||||
"babel-eslint": "^10.1.0",
|
"babel-eslint": "^10.1.0",
|
||||||
"babel-jest": "^28.0.3",
|
"babel-jest": "^28.1.0",
|
||||||
"eslint": "^7.32.0",
|
"eslint": "^7.32.0",
|
||||||
"eslint-plugin-import": "~2.26.0",
|
"eslint-plugin-import": "~2.26.0",
|
||||||
"eslint-plugin-jsx-a11y": "~6.5.1",
|
"eslint-plugin-jsx-a11y": "~6.5.1",
|
||||||
"eslint-plugin-promise": "~6.0.0",
|
"eslint-plugin-promise": "~6.0.0",
|
||||||
"eslint-plugin-react": "~7.29.4",
|
"eslint-plugin-react": "~7.29.4",
|
||||||
"jest": "^28.0.3",
|
"jest": "^28.1.0",
|
||||||
"jest-environment-jsdom": "^28.0.2",
|
"jest-environment-jsdom": "^28.1.0",
|
||||||
"prettier": "^2.6.2",
|
"prettier": "^2.6.2",
|
||||||
"raf": "^3.4.1",
|
"raf": "^3.4.1",
|
||||||
"react-intl-translations-manager": "^5.0.3",
|
"react-intl-translations-manager": "^5.0.3",
|
||||||
|
|
|
@ -22,7 +22,7 @@ describe Settings::TwoFactorAuthentication::ConfirmationsController do
|
||||||
let(:user) { Fabricate(:user, email: 'local-part@domain', otp_secret: with_otp_secret ? 'oldotpsecret' : nil) }
|
let(:user) { Fabricate(:user, email: 'local-part@domain', otp_secret: with_otp_secret ? 'oldotpsecret' : nil) }
|
||||||
|
|
||||||
describe 'GET #new' do
|
describe 'GET #new' do
|
||||||
context 'when signed in and a new otp secret has been setted in the session' do
|
context 'when signed in and a new otp secret has been set in the session' do
|
||||||
subject do
|
subject do
|
||||||
sign_in user, scope: :user
|
sign_in user, scope: :user
|
||||||
get :new, session: { challenge_passed_at: Time.now.utc, new_otp_secret: 'thisisasecretforthespecofnewview' }
|
get :new, session: { challenge_passed_at: Time.now.utc, new_otp_secret: 'thisisasecretforthespecofnewview' }
|
||||||
|
@ -36,7 +36,7 @@ describe Settings::TwoFactorAuthentication::ConfirmationsController do
|
||||||
expect(response).to redirect_to('/auth/sign_in')
|
expect(response).to redirect_to('/auth/sign_in')
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'redirects if a new otp_secret has not been setted in the session' do
|
it 'redirects if a new otp_secret has not been set in the session' do
|
||||||
sign_in user, scope: :user
|
sign_in user, scope: :user
|
||||||
get :new, session: { challenge_passed_at: Time.now.utc }
|
get :new, session: { challenge_passed_at: Time.now.utc }
|
||||||
expect(response).to redirect_to('/settings/otp_authentication')
|
expect(response).to redirect_to('/settings/otp_authentication')
|
||||||
|
|
|
@ -24,7 +24,7 @@ RSpec.describe EmojiFormatter do
|
||||||
let(:text) { preformat_text(':coolcat: Beep boop') }
|
let(:text) { preformat_text(':coolcat: Beep boop') }
|
||||||
|
|
||||||
it 'converts the shortcode to an image tag' do
|
it 'converts the shortcode to an image tag' do
|
||||||
is_expected.to match(/<img draggable="false" class="emojione custom-emoji" alt=":coolcat:"/)
|
is_expected.to match(/<img rel="emoji" draggable="false" width="16" height="16" class="emojione custom-emoji" alt=":coolcat:"/)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -32,7 +32,7 @@ RSpec.describe EmojiFormatter do
|
||||||
let(:text) { preformat_text('Beep :coolcat: boop') }
|
let(:text) { preformat_text('Beep :coolcat: boop') }
|
||||||
|
|
||||||
it 'converts the shortcode to an image tag' do
|
it 'converts the shortcode to an image tag' do
|
||||||
is_expected.to match(/Beep <img draggable="false" class="emojione custom-emoji" alt=":coolcat:"/)
|
is_expected.to match(/Beep <img rel="emoji" draggable="false" width="16" height="16" class="emojione custom-emoji" alt=":coolcat:"/)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -48,7 +48,7 @@ RSpec.describe EmojiFormatter do
|
||||||
let(:text) { preformat_text('Beep boop :coolcat:') }
|
let(:text) { preformat_text('Beep boop :coolcat:') }
|
||||||
|
|
||||||
it 'converts the shortcode to an image tag' do
|
it 'converts the shortcode to an image tag' do
|
||||||
is_expected.to match(/boop <img draggable="false" class="emojione custom-emoji" alt=":coolcat:"/)
|
is_expected.to match(/boop <img rel="emoji" draggable="false" width="16" height="16" class="emojione custom-emoji" alt=":coolcat:"/)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,56 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require 'rails_helper'
|
|
||||||
|
|
||||||
describe RSS::Serializer do
|
|
||||||
describe '#status_title' do
|
|
||||||
let(:text) { 'This is a toot' }
|
|
||||||
let(:spoiler) { '' }
|
|
||||||
let(:sensitive) { false }
|
|
||||||
let(:reblog) { nil }
|
|
||||||
let(:account) { Fabricate(:account) }
|
|
||||||
let(:status) { Fabricate(:status, account: account, text: text, spoiler_text: spoiler, sensitive: sensitive, reblog: reblog) }
|
|
||||||
|
|
||||||
subject { RSS::Serializer.new.send(:status_title, status) }
|
|
||||||
|
|
||||||
context 'on a toot with long text' do
|
|
||||||
let(:text) { "This toot's text is longer than the allowed number of characters" }
|
|
||||||
|
|
||||||
it 'truncates toot text appropriately' do
|
|
||||||
expect(subject).to eq "#{account.acct}: “This toot's text is longer tha…”"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'on a toot with long text with a newline' do
|
|
||||||
let(:text) { "This toot's text is longer\nthan the allowed number of characters" }
|
|
||||||
|
|
||||||
it 'truncates toot text appropriately' do
|
|
||||||
expect(subject).to eq "#{account.acct}: “This toot's text is longer…”"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'on a toot with a content warning' do
|
|
||||||
let(:spoiler) { 'long toot' }
|
|
||||||
|
|
||||||
it 'displays spoiler text instead of toot content' do
|
|
||||||
expect(subject).to eq "#{account.acct}: CW “long toot”"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'on a toot with sensitive media' do
|
|
||||||
let(:sensitive) { true }
|
|
||||||
|
|
||||||
it 'displays that the media is sensitive' do
|
|
||||||
expect(subject).to eq "#{account.acct}: “This is a toot” (sensitive)"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'on a reblog' do
|
|
||||||
let(:reblog) { Fabricate(:status, text: 'This is a toot') }
|
|
||||||
|
|
||||||
it 'display that the toot is a reblog' do
|
|
||||||
expect(subject).to eq "#{account.acct} boosted #{reblog.account.acct}: “This is a toot”"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
Loading…
Reference in New Issue