Compare commits
20 Commits
7b3070de05
...
cea84abb03
Author | SHA1 | Date |
---|---|---|
nachtjasmin | cea84abb03 | |
nachtjasmin | bf6d451ff7 | |
nachtjasmin | 9abc807591 | |
nachtjasmin | 94794cd4cf | |
nachtjasmin | a99a5fecd2 | |
nachtjasmin | b200e4ad1e | |
nachtjasmin | 6947207a30 | |
nachtjasmin | fb6547ea7c | |
Claire | 7efc85c2f7 | |
nachtjasmin | 35742cdf3d | |
nachtjasmin | ecb892afbc | |
nachtjasmin | 66ff566453 | |
nachtjasmin | a23ca40a44 | |
nachtjasmin | 104981bbba | |
nachtjasmin | 76d94f3850 | |
nachtjasmin | 13bd1cca81 | |
nachtjasmin | 967aa653d3 | |
nachtjasmin | 2bf1233a7e | |
nachtjasmin | 38d112cc6f | |
nachtjasmin | f5747f4b88 |
|
@ -1,58 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AboutController < ApplicationController
|
||||
include RegistrationSpamConcern
|
||||
include WebAppControllerConcern
|
||||
|
||||
layout 'public'
|
||||
skip_before_action :require_functional!
|
||||
|
||||
before_action :set_body_classes, only: :show
|
||||
before_action :set_instance_presenter
|
||||
before_action :set_expires_in, only: [:more, :terms]
|
||||
before_action :set_registration_form_time, only: :show
|
||||
|
||||
skip_before_action :require_functional!, only: [:more, :terms]
|
||||
|
||||
def show
|
||||
expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless user_signed_in?
|
||||
end
|
||||
|
||||
def terms; end
|
||||
|
||||
helper_method :display_blocks?
|
||||
helper_method :display_blocks_rationale?
|
||||
helper_method :public_fetch_mode?
|
||||
helper_method :new_user
|
||||
|
||||
private
|
||||
|
||||
def markdown
|
||||
@markdown ||= Redcarpet::Markdown.new(Redcarpet::Render::HTML, escape_html: true, no_images: true)
|
||||
end
|
||||
|
||||
def display_blocks?
|
||||
Setting.show_domain_blocks == 'all' || (Setting.show_domain_blocks == 'users' && user_signed_in?)
|
||||
end
|
||||
|
||||
def display_blocks_rationale?
|
||||
Setting.show_domain_blocks_rationale == 'all' || (Setting.show_domain_blocks_rationale == 'users' && user_signed_in?)
|
||||
end
|
||||
|
||||
def new_user
|
||||
User.new.tap do |user|
|
||||
user.build_account
|
||||
user.build_invite_request
|
||||
end
|
||||
end
|
||||
|
||||
def set_instance_presenter
|
||||
@instance_presenter = InstancePresenter.new
|
||||
end
|
||||
|
||||
def set_body_classes
|
||||
@hide_navbar = true
|
||||
end
|
||||
|
||||
def set_expires_in
|
||||
expires_in 0, public: true
|
||||
end
|
||||
end
|
||||
|
|
|
@ -25,6 +25,12 @@ class AccountsController < ApplicationController
|
|||
format.rss do
|
||||
expires_in 1.minute, public: true
|
||||
|
||||
# Hometown: Do not display any entries in the RSS feed if it's disabled
|
||||
if @account&.user&.setting_norss == true
|
||||
@statuses = []
|
||||
next
|
||||
end
|
||||
|
||||
limit = params[:limit].present? ? [params[:limit].to_i, PAGE_SIZE_MAX].min : PAGE_SIZE
|
||||
@statuses = filtered_statuses.without_reblogs.limit(limit)
|
||||
@statuses = cache_collection(@statuses, Status)
|
||||
|
@ -48,7 +54,12 @@ class AccountsController < ApplicationController
|
|||
end
|
||||
|
||||
def default_statuses
|
||||
@account.statuses.where(visibility: [:public, :unlisted])
|
||||
# Hometown: local-only posts are not meant for RSS feeds, even if they are public.
|
||||
if current_user.nil?
|
||||
@account.statuses.without_local_only.where(visibility: [:public, :unlisted])
|
||||
else
|
||||
@account.statuses.where(visibility: [:public, :unlisted])
|
||||
end
|
||||
end
|
||||
|
||||
def only_media_scope
|
||||
|
|
|
@ -11,8 +11,6 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio
|
|||
|
||||
before_action :set_last_used_at_by_app, only: :index, unless: -> { request.format == :json }
|
||||
|
||||
before_action :set_last_used_at_by_app, only: :index, unless: -> { request.format == :json }
|
||||
|
||||
skip_before_action :require_functional!
|
||||
|
||||
include Localized
|
||||
|
|
|
@ -19,6 +19,7 @@ module WellKnown
|
|||
|
||||
def set_account
|
||||
username = username_from_resource
|
||||
|
||||
@account = begin
|
||||
if username == Rails.configuration.x.local_domain
|
||||
Account.representative
|
||||
|
|
|
@ -3,12 +3,12 @@ import PropTypes from 'prop-types';
|
|||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
|
||||
import { EmptyAccount } from 'mastodon/components/empty_account';
|
||||
import { Permalink } from 'mastodon/components/permalink';
|
||||
import { ShortNumber } from 'mastodon/components/short_number';
|
||||
import { VerifiedBadge } from 'mastodon/components/verified_badge';
|
||||
|
||||
|
@ -151,7 +151,7 @@ class Account extends ImmutablePureComponent {
|
|||
return (
|
||||
<div className={classNames('account', { 'account--minimal': minimal })}>
|
||||
<div className='account__wrapper'>
|
||||
<Link key={account.get('id')} className='account__display-name' title={account.get('acct')} to={`/@${account.get('acct')}`}>
|
||||
<Permalink key={account.get('id')} className='account__display-name' title={account.get('acct')} to={`/@${account.get('acct')}`}>
|
||||
<div className='account__avatar-wrapper'>
|
||||
<Avatar account={account} size={size} />
|
||||
</div>
|
||||
|
@ -164,7 +164,7 @@ class Account extends ImmutablePureComponent {
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
</Permalink>
|
||||
|
||||
{!minimal && (
|
||||
<div className='account__relationship'>
|
||||
|
|
|
@ -5,12 +5,12 @@ import { Component } from 'react';
|
|||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
|
||||
import { Sparklines, SparklinesCurve } from 'react-sparklines';
|
||||
|
||||
import { Permalink } from 'mastodon/components/permalink';
|
||||
import { ShortNumber } from 'mastodon/components/short_number';
|
||||
import { Skeleton } from 'mastodon/components/skeleton';
|
||||
|
||||
|
@ -74,9 +74,9 @@ ImmutableHashtag.propTypes = {
|
|||
const Hashtag = ({ name, href, to, people, uses, history, className, description, withGraph }) => (
|
||||
<div className={classNames('trends__item', className)}>
|
||||
<div className='trends__item__name'>
|
||||
<Link to={to} href={href}>
|
||||
<Permalink to={to} href={href}>
|
||||
{name ? <>#<span>{name}</span></> : <Skeleton width={50} />}
|
||||
</Link>
|
||||
</Permalink>
|
||||
|
||||
{description ? (
|
||||
<span>{description}</span>
|
||||
|
|
|
@ -18,7 +18,6 @@ import { IconButton } from './icon_button';
|
|||
|
||||
const messages = defineMessages({
|
||||
toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: '{number, plural, one {Hide image} other {Hide images}}' },
|
||||
no_descriptive_text: { id: 'media.no_descriptive_text', defaultMessage: 'No descriptive text was provided for this media.' },
|
||||
});
|
||||
|
||||
class Item extends PureComponent {
|
||||
|
@ -33,7 +32,6 @@ class Item extends PureComponent {
|
|||
displayWidth: PropTypes.number,
|
||||
visible: PropTypes.bool.isRequired,
|
||||
autoplay: PropTypes.bool,
|
||||
noDescriptionTitle: PropTypes.string,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
|
@ -63,7 +61,7 @@ class Item extends PureComponent {
|
|||
return this.props.autoplay || autoPlayGif;
|
||||
}
|
||||
|
||||
hoverToPlay () {
|
||||
hoverToPlay() {
|
||||
const { attachment } = this.props;
|
||||
return !this.getAutoPlay() && attachment.get('type') === 'gifv';
|
||||
}
|
||||
|
@ -87,12 +85,12 @@ class Item extends PureComponent {
|
|||
this.setState({ loaded: true });
|
||||
};
|
||||
|
||||
render () {
|
||||
render() {
|
||||
const { attachment, lang, index, size, standalone, displayWidth, visible } = this.props;
|
||||
|
||||
let badges = [], thumbnail;
|
||||
|
||||
let width = 50;
|
||||
let width = 50;
|
||||
let height = 100;
|
||||
|
||||
if (size === 1) {
|
||||
|
@ -103,7 +101,8 @@ class Item extends PureComponent {
|
|||
height = 50;
|
||||
}
|
||||
|
||||
if (attachment.get('description')?.length > 0) {
|
||||
const hasMediaDescription = !attachment.get('description')?.length > 0;
|
||||
if (hasMediaDescription) {
|
||||
badges.push(<span key='alt' className='media-gallery__gifv__label'>ALT</span>);
|
||||
}
|
||||
|
||||
|
@ -111,8 +110,8 @@ class Item extends PureComponent {
|
|||
|
||||
if (attachment.get('type') === 'unknown') {
|
||||
return (
|
||||
<div className={classNames('media-gallery__item', { standalone, 'media-gallery__item--tall': height === 100, 'media-gallery__item--wide': width === 100 })} key={attachment.get('id')}>
|
||||
<a className={`media-gallery__item-thumbnail ${!attachment.get('description') && 'media-missing-description'}`} href={attachment.get('remote_url') || attachment.get('url')} style={{ cursor: 'pointer' }} title={description} lang={lang} target='_blank' rel='noopener noreferrer'>
|
||||
<div className={classNames('media-gallery__item', { standalone, 'media-gallery__item--tall': height === 100, 'media-gallery__item--wide': width === 100, 'media-missing-description': !hasMediaDescription })} key={attachment.get('id')}>
|
||||
<a className='media-gallery__item-thumbnail' href={attachment.get('remote_url') || attachment.get('url')} style={{ cursor: 'pointer' }} title={description} lang={lang} target='_blank' rel='noopener noreferrer'>
|
||||
<Blurhash
|
||||
hash={attachment.get('blurhash')}
|
||||
className='media-gallery__preview'
|
||||
|
@ -122,31 +121,30 @@ class Item extends PureComponent {
|
|||
</div>
|
||||
);
|
||||
} else if (attachment.get('type') === 'image') {
|
||||
const previewUrl = attachment.get('preview_url');
|
||||
const previewUrl = attachment.get('preview_url');
|
||||
const previewWidth = attachment.getIn(['meta', 'small', 'width']);
|
||||
|
||||
const originalUrl = attachment.get('url');
|
||||
const originalUrl = attachment.get('url');
|
||||
const originalWidth = attachment.getIn(['meta', 'original', 'width']);
|
||||
|
||||
const hasSize = typeof originalWidth === 'number' && typeof previewWidth === 'number';
|
||||
|
||||
const srcSet = hasSize ? `${originalUrl} ${originalWidth}w, ${previewUrl} ${previewWidth}w` : null;
|
||||
const sizes = hasSize && (displayWidth > 0) ? `${displayWidth * (width / 100)}px` : null;
|
||||
const sizes = hasSize && (displayWidth > 0) ? `${displayWidth * (width / 100)}px` : null;
|
||||
|
||||
const focusX = attachment.getIn(['meta', 'focus', 'x']) || 0;
|
||||
const focusY = attachment.getIn(['meta', 'focus', 'y']) || 0;
|
||||
const x = ((focusX / 2) + .5) * 100;
|
||||
const y = ((focusY / -2) + .5) * 100;
|
||||
const x = ((focusX / 2) + .5) * 100;
|
||||
const y = ((focusY / -2) + .5) * 100;
|
||||
|
||||
thumbnail = (
|
||||
<a
|
||||
className={`media-gallery__item-thumbnail ${!attachment.get('description') && 'media-missing-description'}`}
|
||||
className={classNames("media-gallery__item-thumbnail", { "media-missing-description": !hasMediaDescription })}
|
||||
href={attachment.get('remote_url') || originalUrl}
|
||||
onClick={this.handleClick}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
>
|
||||
{ !attachment.get('description') && <IconButton className='media-gallery__item-no-description media__no-description-icon' title={this.props.noDescriptionTitle} icon='exclamation-triangle' style={{ right: `calc(4px + ${(left === 'auto' ? '0px' : left)})`, bottom: `calc(4px + ${(top === 'auto' ? '0px' : top)})` }} overlay /> }
|
||||
<img
|
||||
src={previewUrl}
|
||||
srcSet={srcSet}
|
||||
|
@ -167,7 +165,7 @@ class Item extends PureComponent {
|
|||
thumbnail = (
|
||||
<div className={classNames('media-gallery__gifv', { autoplay: autoPlay })}>
|
||||
<video
|
||||
className={`media-gallery__item-gifv-thumbnail ${!attachment.get('description') && 'media-missing-description'}`}
|
||||
className={classNames("media-gallery__item-gifv-thumbnail", { "media-missing-description": !hasMediaDescription })}
|
||||
aria-label={description}
|
||||
title={description}
|
||||
lang={lang}
|
||||
|
@ -181,10 +179,6 @@ class Item extends PureComponent {
|
|||
loop
|
||||
muted
|
||||
/>
|
||||
<div className='media-gallery__gifv__label__container'>
|
||||
<span className='media-gallery__gifv__label'>GIF</span>
|
||||
{ !attachment.get('description') && <span className='media-gallery__gifv__label__no-description'><IconButton title={this.props.noDescriptionTitle} icon='exclamation-triangle' overlay /></span> }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -234,15 +228,15 @@ class MediaGallery extends PureComponent {
|
|||
width: this.props.defaultWidth,
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
componentDidMount() {
|
||||
window.addEventListener('resize', this.handleResize, { passive: true });
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('resize', this.handleResize);
|
||||
}
|
||||
|
||||
UNSAFE_componentWillReceiveProps (nextProps) {
|
||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||
if (!is(nextProps.media, this.props.media) && nextProps.visible === undefined) {
|
||||
this.setState({ visible: displayMedia !== 'hide_all' && !nextProps.sensitive || displayMedia === 'show_all' });
|
||||
} else if (!is(nextProps.visible, this.props.visible) && nextProps.visible !== undefined) {
|
||||
|
@ -278,7 +272,7 @@ class MediaGallery extends PureComponent {
|
|||
}
|
||||
};
|
||||
|
||||
_setDimensions () {
|
||||
_setDimensions() {
|
||||
const width = this.node.offsetWidth;
|
||||
|
||||
// offsetWidth triggers a layout, so only calculate when we need to
|
||||
|
@ -296,7 +290,7 @@ class MediaGallery extends PureComponent {
|
|||
return media.size === 1 && media.getIn([0, 'meta', 'small', 'aspect']);
|
||||
}
|
||||
|
||||
render () {
|
||||
render() {
|
||||
const { media, lang, intl, sensitive, defaultWidth, autoplay } = this.props;
|
||||
const { visible } = this.state;
|
||||
const width = this.state.width || defaultWidth;
|
||||
|
@ -311,14 +305,13 @@ class MediaGallery extends PureComponent {
|
|||
style.aspectRatio = '3 / 2';
|
||||
}
|
||||
|
||||
const size = media.take(4).size;
|
||||
const uncached = media.every(attachment => attachment.get('type') === 'unknown');
|
||||
const noDescriptionTitle = intl.formatMessage(messages.no_descriptive_text);
|
||||
const size = media.take(4).size;
|
||||
const uncached = media.every(attachment => attachment.get('type') === 'unknown');
|
||||
|
||||
if (standalone && this.isFullSizeEligible()) {
|
||||
children = <Item standalone autoplay={autoplay} onClick={this.handleClick} attachment={media.get(0)} lang={lang} displayWidth={width} visible={visible} noDescriptionTitle={noDescriptionTitle} />;
|
||||
if (this.isFullSizeEligible()) {
|
||||
children = <Item standalone autoplay={autoplay} onClick={this.handleClick} attachment={media.get(0)} lang={lang} displayWidth={width} visible={visible} />;
|
||||
} else {
|
||||
children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} autoplay={autoplay} onClick={this.handleClick} attachment={attachment} index={i} lang={lang} size={size} displayWidth={width} visible={visible || uncached} noDescriptionTitle={noDescriptionTitle} />);
|
||||
children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} autoplay={autoplay} onClick={this.handleClick} attachment={attachment} index={i} lang={lang} size={size} displayWidth={width} visible={visible || uncached} />);
|
||||
}
|
||||
|
||||
if (uncached) {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
export default class Permalink extends React.PureComponent {
|
||||
export class Permalink extends PureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object,
|
|
@ -4,12 +4,12 @@ import { PureComponent } from 'react';
|
|||
import { FormattedMessage, injectIntl } from 'react-intl';
|
||||
|
||||
import classnames from 'classnames';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { Permalink } from 'mastodon/components/permalink';
|
||||
import PollContainer from 'mastodon/containers/poll_container';
|
||||
import { autoPlayGif, languages as preloadedLanguages, expandUsernames } from 'mastodon/initial_state';
|
||||
|
||||
|
@ -284,9 +284,9 @@ class StatusContent extends PureComponent {
|
|||
let mentionsPlaceholder = '';
|
||||
|
||||
const mentionLinks = status.get('mentions').map(item => (
|
||||
<Link to={`/@${item.get('acct')}`} href={item.get('url')} key={item.get('id')} className='status-link mention'>
|
||||
<Permalink to={`/@${item.get('acct')}`} href={item.get('url')} key={item.get('id')} className='status-link mention'>
|
||||
@<span>{expandUsernames ? item.get('acct') : item.get('username')}</span>
|
||||
</Link>
|
||||
</Permalink>
|
||||
)).reduce((aggregate, item) => [...aggregate, item, ' '], []);
|
||||
|
||||
const toggleText = hidden ? <FormattedMessage id='status.show_more' defaultMessage='Show more' /> : <FormattedMessage id='status.show_less' defaultMessage='Show less' />;
|
||||
|
|
|
@ -419,8 +419,8 @@ class Header extends ImmutablePureComponent {
|
|||
|
||||
<div className='account__header__tabs__name'>
|
||||
<h1>
|
||||
<a href={account.get('url')} target='_blank' rel='noopener noreferrer'><span dangerouslySetInnerHTML={displayNameHtml} /></a>{isRemote ? <span> <IconButton icon='external-link' size={14} onClick={this.handleDisplayNameClick} /></span> : null}
|
||||
<span dangerouslySetInnerHTML={displayNameHtml} />
|
||||
<a href={account.get('url')} target='_blank' rel='noopener noreferrer'><span dangerouslySetInnerHTML={displayNameHtml} /></a>{isRemote ? <span> <IconButton icon='external-link' size={14} onClick={this.handleDisplayNameClick} /></span> : null}
|
||||
<small>
|
||||
<span>@{acct}</span> {lockedIcon}
|
||||
</small>
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
|
||||
import { Permalink } from 'mastodon/components/permalink';
|
||||
|
||||
import { AvatarOverlay } from '../../../components/avatar_overlay';
|
||||
import { DisplayName } from '../../../components/display_name';
|
||||
|
||||
|
@ -25,12 +25,12 @@ export default class MovedNote extends ImmutablePureComponent {
|
|||
</div>
|
||||
|
||||
<div className='moved-account-banner__action'>
|
||||
<Link href={to.get('url')} to={`/@${to.get('acct')}`} className='detailed-status__display-name'>
|
||||
<Permalink href={to.get('url')} to={`/@${to.get('acct')}`} className='detailed-status__display-name'>
|
||||
<div className='detailed-status__display-avatar'><AvatarOverlay account={to} friend={from} /></div>
|
||||
<DisplayName account={to} />
|
||||
</Link>
|
||||
</Permalink>
|
||||
|
||||
<Link href={to.get('url')} to={`/@${to.get('acct')}`} className='button'><FormattedMessage id='account.go_to_profile' defaultMessage='Go to profile' /></Link>
|
||||
<Permalink href={to.get('url')} to={`/@${to.get('acct')}`} className='button'><FormattedMessage id='account.go_to_profile' defaultMessage='Go to profile' /></Permalink>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,252 +0,0 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { injectIntl, defineMessages } from 'react-intl';
|
||||
import IconButton from '../../../components/icon_button';
|
||||
import Overlay from 'react-overlays/lib/Overlay';
|
||||
import Motion from '../../ui/util/optional_motion';
|
||||
import spring from 'react-motion/lib/spring';
|
||||
import { supportsPassiveEvents } from 'detect-passive-events';
|
||||
import classNames from 'classnames';
|
||||
|
||||
const messages = defineMessages({
|
||||
federate_short: { id: 'federation.federated.short', defaultMessage: 'Federated' },
|
||||
federate_long: { id: 'federation.federated.long', defaultMessage: 'Allow post to reach other instances' },
|
||||
local_only_short: { id: 'federation.local_only.short', defaultMessage: 'Local-only' },
|
||||
local_only_long: { id: 'federation.local_only.long', defaultMessage: 'Restrict this post only to my instance' },
|
||||
change_federation: { id: 'federation.change', defaultMessage: 'Adjust status federation' },
|
||||
});
|
||||
|
||||
const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
|
||||
|
||||
const getValue = element => element.dataset.index === 'true';
|
||||
|
||||
class FederationDropdownMenu extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
style: PropTypes.object,
|
||||
items: PropTypes.array.isRequired,
|
||||
value: PropTypes.bool.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
mounted: false,
|
||||
};
|
||||
|
||||
handleDocumentClick = e => {
|
||||
if (this.node && !this.node.contains(e.target)) {
|
||||
this.props.onClose();
|
||||
}
|
||||
}
|
||||
|
||||
handleKeyDown = e => {
|
||||
const { items } = this.props;
|
||||
const value = getValue(e.currentTarget);
|
||||
const index = items.findIndex(item => {
|
||||
return (item.value === value);
|
||||
});
|
||||
let element;
|
||||
|
||||
switch(e.key) {
|
||||
case 'Escape':
|
||||
this.props.onClose();
|
||||
break;
|
||||
case 'Enter':
|
||||
this.handleClick(e);
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
element = this.node.childNodes[index + 1];
|
||||
if (element) {
|
||||
element.focus();
|
||||
this.props.onChange(getValue(element));
|
||||
}
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
element = this.node.childNodes[index - 1];
|
||||
if (element) {
|
||||
element.focus();
|
||||
this.props.onChange(getValue(element));
|
||||
}
|
||||
break;
|
||||
case 'Home':
|
||||
element = this.node.firstChild;
|
||||
if (element) {
|
||||
element.focus();
|
||||
this.props.onChange(getValue(element));
|
||||
}
|
||||
break;
|
||||
case 'End':
|
||||
element = this.node.lastChild;
|
||||
if (element) {
|
||||
element.focus();
|
||||
this.props.onChange(getValue(element));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
handleClick = e => {
|
||||
e.preventDefault();
|
||||
|
||||
this.props.onClose();
|
||||
this.props.onChange(getValue(e.currentTarget));
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
document.addEventListener('click', this.handleDocumentClick, false);
|
||||
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
||||
if (this.focusedItem) this.focusedItem.focus();
|
||||
this.setState({ mounted: true });
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
document.removeEventListener('click', this.handleDocumentClick, false);
|
||||
document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
||||
}
|
||||
|
||||
setRef = c => {
|
||||
this.node = c;
|
||||
}
|
||||
|
||||
setFocusRef = c => {
|
||||
this.focusedItem = c;
|
||||
}
|
||||
|
||||
render () {
|
||||
const { mounted } = this.state;
|
||||
const { style, items, value } = this.props;
|
||||
|
||||
return (
|
||||
<Motion defaultStyle={{ opacity: 0, scaleX: 0.85, scaleY: 0.75 }} style={{ opacity: spring(1, { damping: 35, stiffness: 400 }), scaleX: spring(1, { damping: 35, stiffness: 400 }), scaleY: spring(1, { damping: 35, stiffness: 400 }) }}>
|
||||
{({ opacity, scaleX, scaleY }) => (
|
||||
// It should not be transformed when mounting because the resulting
|
||||
// size will be used to determine the coordinate of the menu by
|
||||
// react-overlays
|
||||
<div className='privacy-dropdown__dropdown' style={{ ...style, opacity: opacity, transform: mounted ? `scale(${scaleX}, ${scaleY})` : null }} role='listbox' ref={this.setRef}>
|
||||
{items.map(item => (
|
||||
<div role='option' tabIndex='0' key={item.value} data-index={item.value} onKeyDown={this.handleKeyDown} onClick={this.handleClick} className={classNames('privacy-dropdown__option', { active: item.value === value })} aria-selected={item.value === value} ref={item.value === value ? this.setFocusRef : null}>
|
||||
<div className='privacy-dropdown__option__icon'>
|
||||
<i className={`fa fa-fw fa-${item.icon}`} />
|
||||
</div>
|
||||
|
||||
<div className='privacy-dropdown__option__content'>
|
||||
<strong>{item.text}</strong>
|
||||
{item.meta}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Motion>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@injectIntl
|
||||
export default class FederationDropdown extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
isUserTouching: PropTypes.func,
|
||||
isModalOpen: PropTypes.bool.isRequired,
|
||||
onModalOpen: PropTypes.func,
|
||||
onModalClose: PropTypes.func,
|
||||
value: PropTypes.bool.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
disabled: PropTypes.bool,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
open: false,
|
||||
placement: null,
|
||||
};
|
||||
|
||||
handleToggle = ({ target }) => {
|
||||
if (this.props.isUserTouching()) {
|
||||
if (this.state.open) {
|
||||
this.props.onModalClose();
|
||||
} else {
|
||||
this.props.onModalOpen({
|
||||
actions: this.options.map(option => ({ ...option, active: option.value === this.props.value })),
|
||||
onClick: this.handleModalActionClick,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const { top } = target.getBoundingClientRect();
|
||||
this.setState({ placement: top * 2 < innerHeight ? 'bottom' : 'top' });
|
||||
this.setState({ open: !this.state.open });
|
||||
}
|
||||
}
|
||||
|
||||
handleModalActionClick = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const { value } = this.options[e.currentTarget.getAttribute('data-index')];
|
||||
|
||||
this.props.onModalClose();
|
||||
this.props.onChange(value);
|
||||
}
|
||||
|
||||
handleKeyDown = e => {
|
||||
switch(e.key) {
|
||||
case 'Escape':
|
||||
this.handleClose();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
handleClose = () => {
|
||||
this.setState({ open: false });
|
||||
}
|
||||
|
||||
handleChange = value => {
|
||||
this.props.onChange(value);
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
const { intl: { formatMessage } } = this.props;
|
||||
|
||||
this.options = [
|
||||
{ icon: 'link', value: true, text: formatMessage(messages.federate_short), meta: formatMessage(messages.federate_long) },
|
||||
{ icon: 'chain-broken', value: false, text: formatMessage(messages.local_only_short), meta: formatMessage(messages.local_only_long) },
|
||||
];
|
||||
}
|
||||
|
||||
render () {
|
||||
const { value, intl, disabled } = this.props;
|
||||
const { open, placement } = this.state;
|
||||
|
||||
const valueOption = this.options.find(item => item.value === value);
|
||||
|
||||
return (
|
||||
<div className={classNames('privacy-dropdown', { active: open })} onKeyDown={this.handleKeyDown}>
|
||||
<div className={classNames('privacy-dropdown__value', { active: this.options.indexOf(valueOption) === 0 })}>
|
||||
<IconButton
|
||||
className='privacy-dropdown__value-icon'
|
||||
icon={valueOption.icon}
|
||||
title={intl.formatMessage(messages.change_federation)}
|
||||
size={18}
|
||||
expanded={open}
|
||||
active={open}
|
||||
inverted
|
||||
onClick={this.handleToggle}
|
||||
style={{ height: null, lineHeight: '27px' }}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Overlay show={open} placement={placement} target={this}>
|
||||
<FederationDropdownMenu
|
||||
items={this.options}
|
||||
value={value}
|
||||
onClose={this.handleClose}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
</Overlay>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,286 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
import { injectIntl, defineMessages } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { supportsPassiveEvents } from 'detect-passive-events';
|
||||
import Overlay from 'react-overlays/Overlay';
|
||||
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
|
||||
import { IconButton } from '../../../components/icon_button';
|
||||
|
||||
const messages = defineMessages({
|
||||
federate_short: { id: 'federation.federated.short', defaultMessage: 'Federated' },
|
||||
federate_long: { id: 'federation.federated.long', defaultMessage: 'Allow post to reach other instances' },
|
||||
local_only_short: { id: 'federation.local_only.short', defaultMessage: 'Local-only' },
|
||||
local_only_long: { id: 'federation.local_only.long', defaultMessage: 'Restrict this post only to my instance' },
|
||||
change_federation: { id: 'federation.change', defaultMessage: 'Adjust status federation' },
|
||||
});
|
||||
|
||||
const listenerOptions = supportsPassiveEvents ? { passive: true, capture: true } : true;
|
||||
|
||||
class FederationDropdownMenu extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
style: PropTypes.object,
|
||||
items: PropTypes.array.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
handleDocumentClick = e => {
|
||||
if (this.node && !this.node.contains(e.target)) {
|
||||
this.props.onClose();
|
||||
e.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
handleKeyDown = e => {
|
||||
const { items } = this.props;
|
||||
const value = e.currentTarget.getAttribute('data-index');
|
||||
const index = items.findIndex(item => {
|
||||
return (item.value === value);
|
||||
});
|
||||
let element = null;
|
||||
|
||||
switch (e.key) {
|
||||
case 'Escape':
|
||||
this.props.onClose();
|
||||
break;
|
||||
case 'Enter':
|
||||
this.handleClick(e);
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
element = this.node.childNodes[index + 1] || this.node.firstChild;
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
element = this.node.childNodes[index - 1] || this.node.lastChild;
|
||||
break;
|
||||
case 'Tab':
|
||||
if (e.shiftKey) {
|
||||
element = this.node.childNodes[index - 1] || this.node.lastChild;
|
||||
} else {
|
||||
element = this.node.childNodes[index + 1] || this.node.firstChild;
|
||||
}
|
||||
break;
|
||||
case 'Home':
|
||||
element = this.node.firstChild;
|
||||
break;
|
||||
case 'End':
|
||||
element = this.node.lastChild;
|
||||
break;
|
||||
}
|
||||
|
||||
if (element) {
|
||||
element.focus();
|
||||
this.props.onChange(element.getAttribute('data-index'));
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
handleClick = e => {
|
||||
const value = e.currentTarget.getAttribute('data-index');
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
this.props.onClose();
|
||||
this.props.onChange(value);
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
document.addEventListener('click', this.handleDocumentClick, { capture: true });
|
||||
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
||||
if (this.focusedItem) this.focusedItem.focus({ preventScroll: true });
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener('click', this.handleDocumentClick, { capture: true });
|
||||
document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
||||
}
|
||||
|
||||
setRef = c => {
|
||||
this.node = c;
|
||||
};
|
||||
|
||||
setFocusRef = c => {
|
||||
this.focusedItem = c;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { style, items, value } = this.props;
|
||||
|
||||
return (
|
||||
<div style={{ ...style }} role='listbox' ref={this.setRef}>
|
||||
{items.map(item => (
|
||||
<div role='option' tabIndex={0} key={item.value} data-index={item.value} onKeyDown={this.handleKeyDown} onClick={this.handleClick} className={classNames('privacy-dropdown__option', { active: item.value === value })} aria-selected={item.value === value} ref={item.value === value ? this.setFocusRef : null}>
|
||||
<div className='privacy-dropdown__option__icon'>
|
||||
<Icon id={item.icon} fixedWidth />
|
||||
</div>
|
||||
|
||||
<div className='privacy-dropdown__option__content'>
|
||||
<strong>{item.text}</strong>
|
||||
{item.meta}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class FederationDropdown extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
isUserTouching: PropTypes.func,
|
||||
onModalOpen: PropTypes.func,
|
||||
onModalClose: PropTypes.func,
|
||||
value: PropTypes.bool.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
noDirect: PropTypes.bool,
|
||||
container: PropTypes.func,
|
||||
disabled: PropTypes.bool,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
open: false,
|
||||
placement: 'bottom',
|
||||
};
|
||||
|
||||
handleToggle = () => {
|
||||
if (this.props.isUserTouching && this.props.isUserTouching()) {
|
||||
if (this.state.open) {
|
||||
this.props.onModalClose();
|
||||
} else {
|
||||
this.props.onModalOpen({
|
||||
actions: this.options.map(option => ({ ...option, active: option.value === this.props.value })),
|
||||
onClick: this.handleModalActionClick,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (this.state.open && this.activeElement) {
|
||||
this.activeElement.focus({ preventScroll: true });
|
||||
}
|
||||
this.setState({ open: !this.state.open });
|
||||
}
|
||||
};
|
||||
|
||||
handleModalActionClick = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const { value } = this.options[e.currentTarget.getAttribute('data-index')];
|
||||
|
||||
this.props.onModalClose();
|
||||
this.props.onChange(value);
|
||||
};
|
||||
|
||||
handleKeyDown = e => {
|
||||
switch (e.key) {
|
||||
case 'Escape':
|
||||
this.handleClose();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
handleMouseDown = () => {
|
||||
if (!this.state.open) {
|
||||
this.activeElement = document.activeElement;
|
||||
}
|
||||
};
|
||||
|
||||
handleButtonKeyDown = (e) => {
|
||||
switch (e.key) {
|
||||
case ' ':
|
||||
case 'Enter':
|
||||
this.handleMouseDown();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
handleClose = () => {
|
||||
if (this.state.open && this.activeElement) {
|
||||
this.activeElement.focus({ preventScroll: true });
|
||||
}
|
||||
this.setState({ open: false });
|
||||
};
|
||||
|
||||
handleChange = value => {
|
||||
// handleChange receives the values as string, therefore we need to convert them
|
||||
// to proper JS booleans.
|
||||
value = value === "true";
|
||||
this.props.onChange(value);
|
||||
};
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
const { intl: { formatMessage } } = this.props;
|
||||
|
||||
this.options = [
|
||||
{ icon: 'link', value: true, text: formatMessage(messages.federate_short), meta: formatMessage(messages.federate_long) },
|
||||
{ icon: 'chain-broken', value: false, text: formatMessage(messages.local_only_short), meta: formatMessage(messages.local_only_long) },
|
||||
];
|
||||
}
|
||||
|
||||
setTargetRef = c => {
|
||||
this.target = c;
|
||||
};
|
||||
|
||||
findTarget = () => {
|
||||
return this.target;
|
||||
};
|
||||
|
||||
handleOverlayEnter = (state) => {
|
||||
this.setState({ placement: state.placement });
|
||||
};
|
||||
|
||||
render() {
|
||||
const { value, container, disabled, intl } = this.props;
|
||||
const { open, placement } = this.state;
|
||||
|
||||
const valueOption = this.options.find(item => item.value === value);
|
||||
|
||||
return (
|
||||
<div className={classNames('privacy-dropdown', placement, { active: open })} onKeyDown={this.handleKeyDown}>
|
||||
<div className={classNames('privacy-dropdown__value', { active: this.options.indexOf(valueOption) === (placement === 'bottom' ? 0 : (this.options.length - 1)) })} ref={this.setTargetRef}>
|
||||
<IconButton
|
||||
className='privacy-dropdown__value-icon'
|
||||
icon={valueOption.icon}
|
||||
title={intl.formatMessage(messages.change_federation)}
|
||||
size={18}
|
||||
expanded={open}
|
||||
active={open}
|
||||
inverted
|
||||
onClick={this.handleToggle}
|
||||
onMouseDown={this.handleMouseDown}
|
||||
onKeyDown={this.handleButtonKeyDown}
|
||||
style={{ height: null, lineHeight: '27px' }}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Overlay show={open} placement={'bottom'} flip target={this.findTarget} container={container} popperConfig={{ strategy: 'fixed', onFirstUpdate: this.handleOverlayEnter }}>
|
||||
{({ props, placement }) => (
|
||||
<div {...props}>
|
||||
<div className={`dropdown-animation privacy-dropdown__dropdown ${placement}`}>
|
||||
<FederationDropdownMenu
|
||||
items={this.options}
|
||||
value={value}
|
||||
onClose={this.handleClose}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Overlay>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default injectIntl(FederationDropdown);
|
|
@ -2,11 +2,11 @@ import PropTypes from 'prop-types';
|
|||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
|
||||
import { Permalink } from 'mastodon/components/permalink';
|
||||
|
||||
import { Avatar } from '../../../components/avatar';
|
||||
|
||||
import ActionBar from './action_bar';
|
||||
|
@ -23,17 +23,17 @@ export default class NavigationBar extends ImmutablePureComponent {
|
|||
const username = this.props.account.get('acct')
|
||||
const url = this.props.account.get('url')
|
||||
return (
|
||||
<div classNamesName='navigation-bar'>
|
||||
<Link to={`/@${username}`} href={url}>
|
||||
<div className='navigation-bar'>
|
||||
<Permalink to={`/@${username}`} href={url}>
|
||||
<span style={{ display: 'none' }}>{username}</span>
|
||||
<Avatar account={this.props.account} size={46} />
|
||||
</Link>
|
||||
</Permalink>
|
||||
|
||||
<div className='navigation-bar__profile'>
|
||||
<span>
|
||||
<Link to={`/@${username}`} href={url}>
|
||||
<Permalink to={`/@${username}`} href={url}>
|
||||
<strong className='navigation-bar__profile-account'>@{username}</strong>
|
||||
</Link>
|
||||
</Permalink>
|
||||
</span>
|
||||
|
||||
<span>
|
||||
|
|
|
@ -6,7 +6,6 @@ import { isUserTouching } from '../../../is_mobile';
|
|||
import FederationDropdown from '../components/federation_dropdown';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
isModalOpen: state.get('modal').modalType === 'ACTIONS',
|
||||
value: state.getIn(['compose', 'federation']),
|
||||
});
|
||||
|
||||
|
@ -17,9 +16,14 @@ const mapDispatchToProps = dispatch => ({
|
|||
},
|
||||
|
||||
isUserTouching,
|
||||
onModalOpen: props => dispatch(openModal('ACTIONS', props)),
|
||||
onModalClose: () => dispatch(closeModal()),
|
||||
|
||||
onModalOpen: props => dispatch(openModal({
|
||||
modalType: 'ACTIONS',
|
||||
modalProps: props,
|
||||
})),
|
||||
onModalClose: () => dispatch(closeModal({
|
||||
modalType: undefined,
|
||||
ignoreFocus: false,
|
||||
})),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(FederationDropdown);
|
||||
|
|
|
@ -3,7 +3,6 @@ import PropTypes from 'prop-types';
|
|||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
|
@ -13,6 +12,7 @@ import { HotKeys } from 'react-hotkeys';
|
|||
import AttachmentList from 'mastodon/components/attachment_list';
|
||||
import AvatarComposite from 'mastodon/components/avatar_composite';
|
||||
import { IconButton } from 'mastodon/components/icon_button';
|
||||
import { Permalink } from 'mastodon/components/permalink';
|
||||
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
|
||||
import StatusContent from 'mastodon/components/status_content';
|
||||
import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
|
||||
|
@ -136,7 +136,7 @@ class Conversation extends ImmutablePureComponent {
|
|||
|
||||
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDelete });
|
||||
|
||||
const names = accounts.map(a => <Link to={`/@${a.get('acct')}`} href={a.get('url')} key={a.get('id')} title={a.get('acct')}><bdi><strong className='display-name__html' dangerouslySetInnerHTML={{ __html: a.get('display_name_html') }} /></bdi></Link>).reduce((prev, cur) => [prev, ', ', cur]);
|
||||
const names = accounts.map(a => <Permalink to={`/@${a.get('acct')}`} href={a.get('url')} key={a.get('id')} title={a.get('acct')}><bdi><strong className='display-name__html' dangerouslySetInnerHTML={{ __html: a.get('display_name_html') }} /></bdi></Permalink>).reduce((prev, cur) => [prev, ', ', cur]);
|
||||
|
||||
const handlers = {
|
||||
reply: this.handleReply,
|
||||
|
|
|
@ -3,7 +3,6 @@ import PropTypes from 'prop-types';
|
|||
import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
|
@ -19,6 +18,7 @@ import { openModal } from 'mastodon/actions/modal';
|
|||
import { Avatar } from 'mastodon/components/avatar';
|
||||
import Button from 'mastodon/components/button';
|
||||
import { DisplayName } from 'mastodon/components/display_name';
|
||||
import { Permalink } from 'mastodon/components/permalink';
|
||||
import { ShortNumber } from 'mastodon/components/short_number';
|
||||
import { autoPlayGif, me, unfollowModal } from 'mastodon/initial_state';
|
||||
import { makeGetAccount } from 'mastodon/selectors';
|
||||
|
@ -174,7 +174,7 @@ class AccountCard extends ImmutablePureComponent {
|
|||
|
||||
return (
|
||||
<div className='account-card'>
|
||||
<Link href={account.get('url')} to={`/@${account.get('acct')}`} className='account-card__permalink'>
|
||||
<Permalink href={account.get('url')} to={`/@${account.get('acct')}`} className='account-card__permalink'>
|
||||
<div className='account-card__header'>
|
||||
<img
|
||||
src={
|
||||
|
@ -188,7 +188,7 @@ class AccountCard extends ImmutablePureComponent {
|
|||
<div className='account-card__title__avatar'><Avatar account={account} size={56} /></div>
|
||||
<DisplayName account={account} />
|
||||
</div>
|
||||
</Link>
|
||||
</Permalink>
|
||||
|
||||
{account.get('note').length > 0 && (
|
||||
<div
|
||||
|
|
|
@ -2,11 +2,11 @@ import PropTypes from 'prop-types';
|
|||
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
|
||||
import { Permalink } from 'mastodon/components/permalink';
|
||||
|
||||
import { Avatar } from '../../../components/avatar';
|
||||
import { DisplayName } from '../../../components/display_name';
|
||||
import { IconButton } from '../../../components/icon_button';
|
||||
|
@ -32,10 +32,10 @@ class AccountAuthorize extends ImmutablePureComponent {
|
|||
return (
|
||||
<div className='account-authorize__wrapper'>
|
||||
<div className='account-authorize'>
|
||||
<Link href={account.get('url')} to={`/@${account.get('acct')}`} className='detailed-status__display-name'>
|
||||
<Permalink href={account.get('url')} to={`/@${account.get('acct')}`} className='detailed-status__display-name'>
|
||||
<div className='account-authorize__avatar'><Avatar account={account} size={48} /></div>
|
||||
<DisplayName account={account} />
|
||||
</Link>
|
||||
</Permalink>
|
||||
|
||||
<div className='account__header__content translate' dangerouslySetInnerHTML={content} />
|
||||
</div>
|
||||
|
|
|
@ -30,7 +30,6 @@ class ListEditor extends ImmutablePureComponent {
|
|||
|
||||
static propTypes = {
|
||||
listId: PropTypes.string.isRequired,
|
||||
isExclusive: PropTypes.bool.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
onInitialize: PropTypes.func.isRequired,
|
||||
|
@ -56,7 +55,7 @@ class ListEditor extends ImmutablePureComponent {
|
|||
|
||||
return (
|
||||
<div className='modal-root__modal list-editor'>
|
||||
<EditListForm isExclusive={this.props.isExclusive} />
|
||||
<EditListForm />
|
||||
|
||||
<Search />
|
||||
|
||||
|
|
|
@ -2,14 +2,13 @@ import PropTypes from 'prop-types';
|
|||
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
|
||||
import { Avatar } from 'mastodon/components/avatar';
|
||||
import { DisplayName } from 'mastodon/components/display_name';
|
||||
import { IconButton } from 'mastodon/components/icon_button';
|
||||
import { Permalink } from 'mastodon/components/permalink';
|
||||
|
||||
const messages = defineMessages({
|
||||
authorize: { id: 'follow_request.authorize', defaultMessage: 'Authorize' },
|
||||
|
@ -44,10 +43,10 @@ class FollowRequest extends ImmutablePureComponent {
|
|||
return (
|
||||
<div className='account'>
|
||||
<div className='account__wrapper'>
|
||||
<Link key={account.get('id')} className='account__display-name' title={account.get('acct')} href={account.get('url')} to={`/@${account.get('acct')}`}>
|
||||
<Permalink key={account.get('id')} className='account__display-name' title={account.get('acct')} href={account.get('url')} to={`/@${account.get('acct')}`}>
|
||||
<div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div>
|
||||
<DisplayName account={account} />
|
||||
</Link>
|
||||
</Permalink>
|
||||
|
||||
<div className='account__relationship'>
|
||||
<IconButton title={intl.formatMessage(messages.authorize)} icon='check' onClick={onAuthorize} />
|
||||
|
|
|
@ -3,7 +3,6 @@ import PropTypes from 'prop-types';
|
|||
import { injectIntl, FormattedMessage, defineMessages } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
|
@ -11,6 +10,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
|
|||
import { HotKeys } from 'react-hotkeys';
|
||||
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { Permalink } from 'mastodon/components/permalink';
|
||||
import AccountContainer from 'mastodon/containers/account_container';
|
||||
import StatusContainer from 'mastodon/containers/status_container';
|
||||
import { me } from 'mastodon/initial_state';
|
||||
|
@ -398,7 +398,7 @@ class Notification extends ImmutablePureComponent {
|
|||
|
||||
const targetAccount = report.get('target_account');
|
||||
const targetDisplayNameHtml = { __html: targetAccount.get('display_name_html') };
|
||||
const targetLink = <bdi><Link className='notification__display-name' title={targetAccount.get('acct')} href={targetAccount.get('url')} to={`/@${targetAccount.get('acct')}`} dangerouslySetInnerHTML={targetDisplayNameHtml} /></bdi>;
|
||||
const targetLink = <bdi><Permalink className='notification__display-name' title={targetAccount.get('acct')} href={targetAccount.get('url')} to={`/@${targetAccount.get('acct')}`} dangerouslySetInnerHTML={targetDisplayNameHtml} /></bdi>;
|
||||
|
||||
return (
|
||||
<HotKeys handlers={this.getHandlers()}>
|
||||
|
@ -423,7 +423,7 @@ class Notification extends ImmutablePureComponent {
|
|||
const { notification } = this.props;
|
||||
const account = notification.get('account');
|
||||
const displayNameHtml = { __html: account.get('display_name_html') };
|
||||
const link = <bdi><Link className='notification__display-name' title={account.get('acct')} href={account.get('url')} to={`/@${account.get('acct')}`} dangerouslySetInnerHTML={displayNameHtml} /></bdi>;
|
||||
const link = <bdi><Permalink className='notification__display-name' title={account.get('acct')} href={account.get('url')} to={`/@${account.get('acct')}`} dangerouslySetInnerHTML={displayNameHtml} /></bdi>;
|
||||
|
||||
switch(notification.get('type')) {
|
||||
case 'follow':
|
||||
|
|
|
@ -15,6 +15,7 @@ const messages = defineMessages({
|
|||
spam: { id: 'report_notification.categories.spam', defaultMessage: 'Spam' },
|
||||
legal: { id: 'report_notification.categories.legal', defaultMessage: 'Legal' },
|
||||
violation: { id: 'report_notification.categories.violation', defaultMessage: 'Rule violation' },
|
||||
dislike: { id: 'report_notification.categories.dislike', defaultMessage: 'Dislike' },
|
||||
});
|
||||
|
||||
class Report extends ImmutablePureComponent {
|
||||
|
|
|
@ -45,6 +45,7 @@ class Category extends PureComponent {
|
|||
const { onNextStep, category } = this.props;
|
||||
|
||||
switch(category) {
|
||||
// Hometown: also send reports for "disliked" toots, it's quite unfair to do nothing in this case.
|
||||
case 'dislike':
|
||||
onNextStep('statuses');
|
||||
break;
|
||||
|
|
|
@ -190,7 +190,7 @@ class ActionBar extends PureComponent {
|
|||
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
|
||||
const pinnableStatus = ['public', 'unlisted', 'private'].includes(status.get('visibility'));
|
||||
const mutingConversation = status.get('muted');
|
||||
const federated = !status.get('local_only');
|
||||
const federated = !status.get('local_only');
|
||||
const account = status.get('account');
|
||||
const writtenByMe = status.getIn(['account', 'id']) === me;
|
||||
const isRemote = status.getIn(['account', 'username']) !== status.getIn(['account', 'acct']);
|
||||
|
@ -203,7 +203,7 @@ class ActionBar extends PureComponent {
|
|||
|
||||
menu.push({ text: intl.formatMessage(messages.copy), action: this.handleCopy });
|
||||
|
||||
if (publicStatus && 'share' in navigator) {
|
||||
if (publicStatus && 'share' in navigator && federated) {
|
||||
menu.push({ text: intl.formatMessage(messages.share), action: this.handleShare });
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
|||
import classNames from 'classnames';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
|
||||
const ColumnLink = ({ icon, text, to, href, method, badge, transparent, button, onClick, ...other }) => {
|
||||
const className = classNames('column-link', { 'column-link--transparent': transparent, 'column-link--button': button });
|
||||
|
@ -46,7 +46,7 @@ ColumnLink.propTypes = {
|
|||
badge: PropTypes.node,
|
||||
transparent: PropTypes.bool,
|
||||
button: PropTypes.bool,
|
||||
onClick: PropTypes.function,
|
||||
onClick: PropTypes.func,
|
||||
};
|
||||
|
||||
export default ColumnLink;
|
||||
|
|
|
@ -155,8 +155,10 @@ export default class ColumnsArea extends ImmutablePureComponent {
|
|||
<div className='columns-area columns-area--mobile'>{children}</div>
|
||||
</div>
|
||||
|
||||
<div className='columns-area__panels__pane columns-area__panels__pane--start columns-area__panels__pane--navigational columns-area__panels__pane__inner'>
|
||||
<NavigationPanel />
|
||||
<div className='columns-area__panels__pane columns-area__panels__pane--start columns-area__panels__pane--navigational'>
|
||||
<div className='columns-area__panels__pane__inner'>
|
||||
<NavigationPanel />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -18,7 +18,7 @@ const messages = defineMessages({
|
|||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||
onLogout () {
|
||||
onLogout() {
|
||||
dispatch(openModal({
|
||||
modalType: 'CONFIRM',
|
||||
modalProps: {
|
||||
|
@ -52,7 +52,7 @@ class LinkFooter extends PureComponent {
|
|||
return false;
|
||||
};
|
||||
|
||||
render () {
|
||||
render() {
|
||||
const { signedIn, permissions } = this.context.identity;
|
||||
const { multiColumn } = this.props;
|
||||
|
||||
|
@ -110,7 +110,7 @@ class LinkFooter extends PureComponent {
|
|||
{DividingCircle}
|
||||
<a href={source_url} rel='noopener noreferrer' target='_blank'><FormattedMessage id='footer.source_code' defaultMessage='View source code' /></a>
|
||||
{DividingCircle}
|
||||
<span class='version'>v{version}</span>
|
||||
<span className='version'>v{version}</span>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -57,6 +57,7 @@
|
|||
* @property {string} display_media
|
||||
* @property {string} domain
|
||||
* @property {boolean=} expand_spoilers
|
||||
* @property {boolean=} expand_usernames
|
||||
* @property {boolean} limited_federation_mode
|
||||
* @property {string} locale
|
||||
* @property {string | null} mascot
|
||||
|
@ -89,6 +90,7 @@
|
|||
* @property {InitialStateLanguage[]} languages
|
||||
* @property {boolean=} critical_updates_pending
|
||||
* @property {InitialStateMeta} meta
|
||||
* @property {number} max_toot_chars
|
||||
*/
|
||||
|
||||
const element = document.getElementById('initial-state');
|
||||
|
|
|
@ -10,7 +10,6 @@ import {
|
|||
LIST_EDITOR_RESET,
|
||||
LIST_EDITOR_SETUP,
|
||||
LIST_EDITOR_TITLE_CHANGE,
|
||||
LIST_EDITOR_IS_EXCLUSIVE_CHANGE,
|
||||
LIST_ACCOUNTS_FETCH_REQUEST,
|
||||
LIST_ACCOUNTS_FETCH_SUCCESS,
|
||||
LIST_ACCOUNTS_FETCH_FAIL,
|
||||
|
@ -56,11 +55,6 @@ export default function listEditorReducer(state = initialState, action) {
|
|||
map.set('title', action.value);
|
||||
map.set('isChanged', true);
|
||||
});
|
||||
case LIST_EDITOR_IS_EXCLUSIVE_CHANGE:
|
||||
return state.withMutations(map => {
|
||||
map.set('isExclusive', action.value);
|
||||
map.set('isChanged', true);
|
||||
});
|
||||
case LIST_CREATE_REQUEST:
|
||||
case LIST_UPDATE_REQUEST:
|
||||
return state.withMutations(map => {
|
||||
|
|
|
@ -40,8 +40,4 @@ class ActivityPub::Activity::Flag < ActivityPub::Activity
|
|||
def report_comment
|
||||
(@json['content'] || '')[0...5000]
|
||||
end
|
||||
|
||||
def report_comment
|
||||
(@json['content'] || '')[0...5000]
|
||||
end
|
||||
end
|
||||
|
|
|
@ -53,7 +53,8 @@ class ActivityPub::Parser::StatusParser
|
|||
end
|
||||
|
||||
def created_at
|
||||
@object['published']&.to_datetime
|
||||
datetime = @object['published']&.to_datetime
|
||||
datetime if datetime.present? && (0..9999).cover?(datetime.year)
|
||||
rescue ArgumentError
|
||||
nil
|
||||
end
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class PlainTextFormatter
|
||||
include ActionView::Helpers::TextHelper
|
||||
|
||||
NEWLINE_TAGS_RE = %r{(<br />|<br>|</p>)+}
|
||||
|
||||
attr_reader :text, :local
|
||||
|
|
|
@ -20,10 +20,4 @@ class ApplicationMailer < ActionMailer::Base
|
|||
headers['X-Auto-Response-Suppress'] = 'All'
|
||||
headers['Auto-Submitted'] = 'auto-generated'
|
||||
end
|
||||
|
||||
def set_autoreply_headers!
|
||||
headers['Precedence'] = 'list'
|
||||
headers['X-Auto-Response-Suppress'] = 'All'
|
||||
headers['Auto-Submitted'] = 'auto-generated'
|
||||
end
|
||||
end
|
||||
|
|
|
@ -66,7 +66,7 @@ class UserMailer < Devise::Mailer
|
|||
return unless @resource.active_for_authentication?
|
||||
|
||||
I18n.with_locale(locale) do
|
||||
mail to: @resource.email, subject: I18n.t('devise.mailer.two_factor_enabled.subject')
|
||||
mail to: @resource.email, subject: I18n.t('devise.mailer.two_factor_enabled.subject', title: Setting.site_title)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -77,7 +77,7 @@ class UserMailer < Devise::Mailer
|
|||
return unless @resource.active_for_authentication?
|
||||
|
||||
I18n.with_locale(locale) do
|
||||
mail to: @resource.email, subject: I18n.t('devise.mailer.two_factor_disabled.subject')
|
||||
mail to: @resource.email, subject: I18n.t('devise.mailer.two_factor_disabled.subject', title: Setting.site_title)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -88,7 +88,7 @@ class UserMailer < Devise::Mailer
|
|||
return unless @resource.active_for_authentication?
|
||||
|
||||
I18n.with_locale(locale) do
|
||||
mail to: @resource.email, subject: I18n.t('devise.mailer.two_factor_recovery_codes_changed.subject')
|
||||
mail to: @resource.email, subject: I18n.t('devise.mailer.two_factor_recovery_codes_changed.subject', title: Setting.site_title)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -99,7 +99,7 @@ class UserMailer < Devise::Mailer
|
|||
return unless @resource.active_for_authentication?
|
||||
|
||||
I18n.with_locale(locale) do
|
||||
mail to: @resource.email, subject: I18n.t('devise.mailer.webauthn_enabled.subject')
|
||||
mail to: @resource.email, subject: I18n.t('devise.mailer.webauthn_enabled.subject', title: Setting.site_title)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -110,7 +110,7 @@ class UserMailer < Devise::Mailer
|
|||
return unless @resource.active_for_authentication?
|
||||
|
||||
I18n.with_locale(locale) do
|
||||
mail to: @resource.email, subject: I18n.t('devise.mailer.webauthn_disabled.subject')
|
||||
mail to: @resource.email, subject: I18n.t('devise.mailer.webauthn_disabled.subject', title: Setting.site_title)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -122,7 +122,7 @@ class UserMailer < Devise::Mailer
|
|||
return unless @resource.active_for_authentication?
|
||||
|
||||
I18n.with_locale(locale) do
|
||||
mail to: @resource.email, subject: I18n.t('devise.mailer.webauthn_credential.added.subject')
|
||||
mail to: @resource.email, subject: I18n.t('devise.mailer.webauthn_credential.added.subject', title: Setting.site_title)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -134,7 +134,7 @@ class UserMailer < Devise::Mailer
|
|||
return unless @resource.active_for_authentication?
|
||||
|
||||
I18n.with_locale(locale) do
|
||||
mail to: @resource.email, subject: I18n.t('devise.mailer.webauthn_credential.deleted.subject')
|
||||
mail to: @resource.email, subject: I18n.t('devise.mailer.webauthn_credential.deleted.subject', title: Setting.site_title)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -145,7 +145,6 @@ class UserMailer < Devise::Mailer
|
|||
return unless @resource.active_for_authentication?
|
||||
|
||||
I18n.with_locale(locale) do
|
||||
mail to: @resource.email, subject: I18n.t('user_mailer.welcome.subject')
|
||||
mail to: @resource.email, subject: I18n.t('user_mailer.welcome.subject', title: Setting.site_title)
|
||||
end
|
||||
end
|
||||
|
@ -169,7 +168,7 @@ class UserMailer < Devise::Mailer
|
|||
@statuses = @warning.statuses.includes(:account, :preloadable_poll, :media_attachments, active_mentions: [:account])
|
||||
|
||||
I18n.with_locale(locale) do
|
||||
mail to: @resource.email, subject: I18n.t("user_mailer.warning.subject.#{@warning.action}", acct: "@#{user.account.local_username_and_domain}")
|
||||
mail to: @resource.email, subject: I18n.t("user_mailer.warning.subject.#{@warning.action}", acct: "@#{user.account.local_username_and_domain}", title: Setting.site_title)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -179,7 +178,7 @@ class UserMailer < Devise::Mailer
|
|||
@appeal = appeal
|
||||
|
||||
I18n.with_locale(locale) do
|
||||
mail to: @resource.email, subject: I18n.t('user_mailer.appeal_approved.subject', date: l(@appeal.created_at))
|
||||
mail to: @resource.email, subject: I18n.t('user_mailer.appeal_approved.subject', date: l(@appeal.created_at), title: Setting.site_title)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -189,7 +188,7 @@ class UserMailer < Devise::Mailer
|
|||
@appeal = appeal
|
||||
|
||||
I18n.with_locale(locale) do
|
||||
mail to: @resource.email, subject: I18n.t('user_mailer.appeal_rejected.subject', date: l(@appeal.created_at))
|
||||
mail to: @resource.email, subject: I18n.t('user_mailer.appeal_rejected.subject', date: l(@appeal.created_at), title: Setting.site_title)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -202,7 +201,7 @@ class UserMailer < Devise::Mailer
|
|||
@timestamp = timestamp.to_time.utc
|
||||
|
||||
I18n.with_locale(locale) do
|
||||
mail to: @resource.email, subject: I18n.t('user_mailer.suspicious_sign_in.subject')
|
||||
mail to: @resource.email, subject: I18n.t('user_mailer.suspicious_sign_in.subject', title: Setting.site_title)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -134,4 +134,17 @@ module HasUserSettings
|
|||
def hide_all_media?
|
||||
settings['web.display_media'] == 'hide_all'
|
||||
end
|
||||
|
||||
## --- Hometown-specific settings from here on ---
|
||||
def setting_expand_usernames
|
||||
settings['web.expand_usernames']
|
||||
end
|
||||
|
||||
def setting_default_federation
|
||||
settings['default_federation']
|
||||
end
|
||||
|
||||
def setting_norss
|
||||
settings['norss']
|
||||
end
|
||||
end
|
||||
|
|
|
@ -57,6 +57,7 @@ class Report < ApplicationRecord
|
|||
spam: 1_000,
|
||||
legal: 1_500,
|
||||
violation: 2_000,
|
||||
dislike: 3_000,
|
||||
}
|
||||
|
||||
before_validation :set_uri, only: :create
|
||||
|
|
|
@ -16,6 +16,11 @@ class UserSettings
|
|||
setting :default_sensitive, default: false
|
||||
setting :default_privacy, default: nil, in: %w(public unlisted private)
|
||||
|
||||
# Hometown-specific: Opt-out of RSS feeds for public posts
|
||||
setting :norss, default: false
|
||||
# Hometown: New posts should federate by default
|
||||
setting :default_federation, default: true
|
||||
|
||||
setting_inverse_alias :indexable, :noindex
|
||||
|
||||
namespace :web do
|
||||
|
@ -32,6 +37,9 @@ class UserSettings
|
|||
setting :expand_content_warnings, default: false
|
||||
setting :display_media, default: 'default', in: %w(default show_all hide_all)
|
||||
setting :auto_play, default: false
|
||||
|
||||
# Hometown: Show full username (including domain) for remote users
|
||||
setting :expand_usernames, default: true
|
||||
end
|
||||
|
||||
namespace :notification_emails do
|
||||
|
|
|
@ -88,10 +88,6 @@ class Webhook < ApplicationRecord
|
|||
end
|
||||
end
|
||||
|
||||
def validate_permissions
|
||||
errors.add(:events, :invalid_permissions) if defined?(@current_account) && required_permissions.any? { |permission| !@current_account.user_role.can?(permission) }
|
||||
end
|
||||
|
||||
def strip_events
|
||||
self.events = events.filter_map { |str| str.strip.presence } if events.present?
|
||||
end
|
||||
|
|
|
@ -1,119 +1,7 @@
|
|||
- content_for :page_title do
|
||||
= site_hostname
|
||||
= t('about.title')
|
||||
|
||||
- content_for :header_tags do
|
||||
= javascript_pack_tag 'public', crossorigin: 'anonymous'
|
||||
= render partial: 'shared/og'
|
||||
|
||||
.grid-4
|
||||
.column-0
|
||||
.public-account-header.public-account-header--no-bar
|
||||
.public-account-header__image
|
||||
%div{:class => ("originalheader")}
|
||||
%h1
|
||||
= link_to root_url, class: 'brand' do
|
||||
= site_title
|
||||
.box-widget
|
||||
%p <a href="/auth/sign_in" id="login" class="btn button button-primary">Sign in</a>
|
||||
- if !closed_registrations?
|
||||
%p <a href="/auth/sign_up" id="register" class="btn button button-primary">Create account</a>
|
||||
- if closed_registrations? && @instance_presenter.closed_registrations_message.present?
|
||||
.closed
|
||||
.rich-formatting
|
||||
%h3= 'Registrations closed.'
|
||||
%p= @instance_presenter.closed_registrations_message.html_safe
|
||||
- if closed_registrations? && !@instance_presenter.closed_registrations_message.present?
|
||||
%hr
|
||||
.closed
|
||||
.rich-formatting
|
||||
%h3= 'Registrations closed.'
|
||||
%p= 'This server is closed to registrations.'
|
||||
.column-1
|
||||
.landing-page__call-to-action{ dir: 'ltr' }
|
||||
.row
|
||||
.row__information-board
|
||||
.information-board__section
|
||||
%span= t 'about.user_count_before'
|
||||
%strong= friendly_number_to_human @instance_presenter.user_count
|
||||
%span= t 'about.user_count_after', count: @instance_presenter.user_count
|
||||
.information-board__section
|
||||
%span= t 'about.status_count_before'
|
||||
%strong= friendly_number_to_human @instance_presenter.status_count
|
||||
%span= t 'about.status_count_after', count: @instance_presenter.status_count
|
||||
.row__mascot
|
||||
- if @instance_presenter.mascot&.file&.url
|
||||
.landing-page__mascot
|
||||
= image_tag @instance_presenter.mascot&.file&.url
|
||||
- else
|
||||
.landing-page__mascot{:class => ("originalmascot")}
|
||||
%div{:class => ("originalmascotimg")}
|
||||
= logo_as_symbol
|
||||
|
||||
.column-2
|
||||
.contact-widget
|
||||
%h4= t 'about.administered_by'
|
||||
|
||||
= account_link_to(@instance_presenter.contact.account)
|
||||
|
||||
- if @instance_presenter.contact.email.present?
|
||||
%h4
|
||||
= succeed ':' do
|
||||
= t 'about.contact'
|
||||
|
||||
= mail_to @instance_presenter.contact.email, nil, title: @instance_presenter.contact.email
|
||||
|
||||
.column-3
|
||||
= render 'application/flashes'
|
||||
|
||||
- if @contents.blank? && @rules.empty? && (!display_blocks? || @blocks&.empty?)
|
||||
= nothing_here
|
||||
- else
|
||||
.box-widget
|
||||
.rich-formatting
|
||||
- unless @rules.empty?
|
||||
%h2#rules= t('about.rules')
|
||||
|
||||
%p= t('about.rules_html')
|
||||
|
||||
%ol.rules-list
|
||||
- @rules.each do |rule|
|
||||
%li
|
||||
.rules-list__text= rule.text
|
||||
|
||||
= @contents.html_safe
|
||||
|
||||
- if display_blocks? && !@blocks.empty?
|
||||
%h2#unavailable-content= t('about.unavailable_content')
|
||||
|
||||
%p= t('about.unavailable_content_html')
|
||||
|
||||
- if (blocks = @blocks.select(&:reject_media?)) && !blocks.empty?
|
||||
%h3= t('about.unavailable_content_description.rejecting_media_title')
|
||||
%p= t('about.unavailable_content_description.rejecting_media')
|
||||
= render partial: 'domain_blocks', locals: { domain_blocks: blocks }
|
||||
- if (blocks = @blocks.select(&:silence?)) && !blocks.empty?
|
||||
%h3= t('about.unavailable_content_description.silenced_title')
|
||||
%p= t('about.unavailable_content_description.silenced')
|
||||
= render partial: 'domain_blocks', locals: { domain_blocks: blocks }
|
||||
- if (blocks = @blocks.select(&:suspend?)) && !blocks.empty?
|
||||
%h3= t('about.unavailable_content_description.suspended_title')
|
||||
%p= t('about.unavailable_content_description.suspended')
|
||||
= render partial: 'domain_blocks', locals: { domain_blocks: blocks }
|
||||
|
||||
.column-4
|
||||
%ul.table-of-contents
|
||||
- unless @rules.empty?
|
||||
%li= link_to t('about.rules'), '#rules'
|
||||
|
||||
- @table_of_contents.each do |item|
|
||||
%li
|
||||
= link_to item.title, "##{item.anchor}"
|
||||
|
||||
- unless item.children.empty?
|
||||
%ul
|
||||
- item.children.each do |sub_item|
|
||||
%li= link_to sub_item.title, "##{sub_item.anchor}"
|
||||
|
||||
- if display_blocks? && !@blocks.empty?
|
||||
%li= link_to t('about.unavailable_content'), '#unavailable-content'
|
||||
|
||||
= render partial: 'shared/web_app'
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
- if controller_name != 'sessions'
|
||||
%li= link_to_login t('auth.login')
|
||||
|
||||
- if controller_name != 'registrations' && !whitelist_mode?
|
||||
- if controller_name != 'registrations'
|
||||
%li= link_to t('auth.register'), available_sign_up_path
|
||||
|
||||
- if controller_name != 'passwords' && controller_name != 'registrations'
|
||||
|
|
|
@ -11,12 +11,6 @@
|
|||
.fields-group
|
||||
= ff.input :noindex, wrapper: :with_label, label: I18n.t('simple_form.labels.defaults.setting_noindex'), hint: I18n.t('simple_form.hints.defaults.setting_noindex')
|
||||
|
||||
.fields-group
|
||||
= ff.input :norss, wrapper: :with_label, label: I18n.t('simple_form.labels.defaults.setting_norss'), hint: I18n.t('simple_form.hints.defaults.setting_norss')
|
||||
|
||||
.fields-group
|
||||
= ff.input :noindex, wrapper: :with_label, label: I18n.t('simple_form.labels.defaults.setting_noindex'), hint: I18n.t('simple_form.hints.defaults.setting_noindex')
|
||||
|
||||
.fields-group
|
||||
= ff.input :aggregate_reblogs, wrapper: :with_label, recommended: true, label: I18n.t('simple_form.labels.defaults.setting_aggregate_reblogs'), hint: I18n.t('simple_form.hints.defaults.setting_aggregate_reblogs')
|
||||
|
||||
|
@ -35,9 +29,6 @@
|
|||
.fields-group
|
||||
= ff.input :default_federation, wrapper: :with_label, label: I18n.t('simple_form.labels.defaults.setting_default_federation'), hint: I18n.t('simple_form.hints.defaults.setting_default_federation')
|
||||
|
||||
.fields-group
|
||||
= ff.input :show_application, wrapper: :with_label, recommended: true, label: I18n.t('simple_form.labels.defaults.setting_show_application'), hint: I18n.t('simple_form.hints.defaults.setting_show_application')
|
||||
|
||||
%h4= t 'preferences.public_timelines'
|
||||
|
||||
.fields-group
|
||||
|
|
|
@ -20,6 +20,10 @@
|
|||
.fields-group
|
||||
= f.input :unlocked, as: :boolean, wrapper: :with_label
|
||||
|
||||
= f.simple_fields_for :settings, current_user.settings do |ff|
|
||||
.fields-group
|
||||
= ff.input :norss, wrapper: :with_label, label: I18n.t('simple_form.labels.defaults.setting_norss')
|
||||
|
||||
%h4= t('privacy.search')
|
||||
|
||||
%p.lead= t('privacy.search_hint_html')
|
||||
|
|
|
@ -44,6 +44,11 @@ class MoveUserSettings < ActiveRecord::Migration[6.1]
|
|||
must_be_following: 'interactions.must_be_following',
|
||||
must_be_following_dm: 'interactions.must_be_following_dm',
|
||||
}.freeze,
|
||||
|
||||
# Hometown-specific fields
|
||||
norss: 'norss',
|
||||
default_federation: 'default_federation',
|
||||
expand_usernames: 'web.expand_usernames',
|
||||
}.freeze
|
||||
|
||||
class LegacySetting < ApplicationRecord
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class MigrateHometownExclusiveListsToMastodon < ActiveRecord::Migration[7.0]
|
||||
def up
|
||||
List.where(is_exclusive: true).in_batches.update_all(exclusive: :is_exclusive) # rubocop:disable Rails/SkipsModelValidations
|
||||
safety_assured { remove_column :lists, :is_exclusive }
|
||||
end
|
||||
end
|
|
@ -10,7 +10,7 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[7.0].define(version: 2023_09_07_150100) do
|
||||
ActiveRecord::Schema[7.0].define(version: 2023_11_23_195926) do
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "plpgsql"
|
||||
|
||||
|
@ -116,6 +116,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_09_07_150100) do
|
|||
t.integer "min_reblogs"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.boolean "keep_local", default: false
|
||||
t.index ["account_id"], name: "index_account_statuses_cleanup_policies_on_account_id"
|
||||
end
|
||||
|
||||
|
@ -977,7 +978,9 @@ ActiveRecord::Schema[7.0].define(version: 2023_09_07_150100) do
|
|||
t.bigint "account_id", null: false
|
||||
t.bigint "application_id"
|
||||
t.bigint "in_reply_to_account_id"
|
||||
t.boolean "local_only"
|
||||
t.bigint "poll_id"
|
||||
t.string "activity_pub_type"
|
||||
t.datetime "deleted_at", precision: nil
|
||||
t.datetime "edited_at", precision: nil
|
||||
t.boolean "trendable"
|
||||
|
|
|
@ -47,6 +47,10 @@ module Paperclip
|
|||
maximum_bitrate = (size_limit_in_bits / duration).floor - 192_000 # Leave some space for the audio stream
|
||||
bitrate = [desired_bitrate, maximum_bitrate].min
|
||||
|
||||
@output_options['b:v'] = bitrate
|
||||
@output_options['maxrate'] = bitrate + 192_000
|
||||
@output_options['bufsize'] = bitrate * 5
|
||||
|
||||
if high_vfr?(metadata)
|
||||
@output_options['vsync'] = 'vfr'
|
||||
@output_options['r'] = @vfr_threshold
|
||||
|
|
|
@ -31,6 +31,46 @@ RSpec.describe ActivityPub::Activity::Create do
|
|||
subject.perform
|
||||
end
|
||||
|
||||
context 'when object publication date is below ISO8601 range' do
|
||||
let(:object_json) do
|
||||
{
|
||||
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||
type: 'Note',
|
||||
content: 'Lorem ipsum',
|
||||
published: '-0977-11-03T08:31:22Z',
|
||||
}
|
||||
end
|
||||
|
||||
it 'creates status with a valid creation date', :aggregate_failures do
|
||||
status = sender.statuses.first
|
||||
|
||||
expect(status).to_not be_nil
|
||||
expect(status.text).to eq 'Lorem ipsum'
|
||||
|
||||
expect(status.created_at).to be_within(30).of(Time.now.utc)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when object publication date is above ISO8601 range' do
|
||||
let(:object_json) do
|
||||
{
|
||||
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||
type: 'Note',
|
||||
content: 'Lorem ipsum',
|
||||
published: '10000-11-03T08:31:22Z',
|
||||
}
|
||||
end
|
||||
|
||||
it 'creates status with a valid creation date', :aggregate_failures do
|
||||
status = sender.statuses.first
|
||||
|
||||
expect(status).to_not be_nil
|
||||
expect(status.text).to eq 'Lorem ipsum'
|
||||
|
||||
expect(status.created_at).to be_within(30).of(Time.now.utc)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when object has been edited' do
|
||||
let(:object_json) do
|
||||
{
|
||||
|
@ -42,18 +82,16 @@ RSpec.describe ActivityPub::Activity::Create do
|
|||
}
|
||||
end
|
||||
|
||||
it 'creates status' do
|
||||
it 'creates status with appropriate creation and edition dates', :aggregate_failures do
|
||||
status = sender.statuses.first
|
||||
|
||||
expect(status).to_not be_nil
|
||||
expect(status.text).to eq 'Lorem ipsum'
|
||||
end
|
||||
|
||||
it 'marks status as edited' do
|
||||
status = sender.statuses.first
|
||||
expect(status.created_at).to eq '2022-01-22T15:00:00Z'.to_datetime
|
||||
|
||||
expect(status).to_not be_nil
|
||||
expect(status.edited?).to be true
|
||||
expect(status.edited_at).to eq '2022-01-22T16:00:00Z'.to_datetime
|
||||
end
|
||||
end
|
||||
|
||||
|
|
Loading…
Reference in New Issue