Merge pull request #1861 from ClearlyClaire/glitch-soc/features/logged-out-webui
Port logged-out Web UI to glitch-soclolsob-rspec
commit
93efea5049
|
@ -20,7 +20,7 @@ class AboutController < ApplicationController
|
|||
def more
|
||||
flash.now[:notice] = I18n.t('about.instance_actor_flash') if params[:instance_actor]
|
||||
|
||||
toc_generator = TOCGenerator.new(@instance_presenter.site_extended_description)
|
||||
toc_generator = TOCGenerator.new(@instance_presenter.extended_description)
|
||||
|
||||
@rules = Rule.ordered
|
||||
@contents = toc_generator.html
|
||||
|
|
|
@ -6,6 +6,6 @@ class Api::V1::InstancesController < Api::BaseController
|
|||
|
||||
def show
|
||||
expires_in 3.minutes, public: true
|
||||
render_with_cache json: {}, serializer: REST::InstanceSerializer, root: 'instance'
|
||||
render_with_cache json: InstancePresenter.new, serializer: REST::V1::InstanceSerializer, root: 'instance'
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::V2::InstancesController < Api::V1::InstancesController
|
||||
def show
|
||||
expires_in 3.minutes, public: true
|
||||
render_with_cache json: InstancePresenter.new, serializer: REST::InstanceSerializer, root: 'instance'
|
||||
end
|
||||
end
|
|
@ -2,10 +2,10 @@
|
|||
|
||||
class HomeController < ApplicationController
|
||||
before_action :redirect_unauthenticated_to_permalinks!
|
||||
before_action :authenticate_user!
|
||||
|
||||
before_action :set_pack
|
||||
before_action :set_referrer_policy_header
|
||||
before_action :set_instance_presenter
|
||||
|
||||
def index
|
||||
@body_classes = 'app-body'
|
||||
|
@ -16,7 +16,10 @@ class HomeController < ApplicationController
|
|||
def redirect_unauthenticated_to_permalinks!
|
||||
return if user_signed_in?
|
||||
|
||||
redirect_to(PermalinkRedirector.new(request.path).redirect_path || default_redirect_path)
|
||||
redirect_path = PermalinkRedirector.new(request.path).redirect_path
|
||||
redirect_path ||= default_redirect_path
|
||||
|
||||
redirect_to(redirect_path) if redirect_path.present?
|
||||
end
|
||||
|
||||
def set_pack
|
||||
|
@ -24,8 +27,10 @@ class HomeController < ApplicationController
|
|||
end
|
||||
|
||||
def default_redirect_path
|
||||
if request.path.start_with?('/web') || whitelist_mode?
|
||||
if whitelist_mode?
|
||||
new_user_session_path
|
||||
elsif request.path.start_with?('/web')
|
||||
nil
|
||||
elsif single_user_mode?
|
||||
short_account_path(Account.local.without_suspended.where('id > 0').first)
|
||||
else
|
||||
|
@ -36,4 +41,8 @@ class HomeController < ApplicationController
|
|||
def set_referrer_policy_header
|
||||
response.headers['Referrer-Policy'] = 'origin'
|
||||
end
|
||||
|
||||
def set_instance_presenter
|
||||
@instance_presenter = InstancePresenter.new
|
||||
end
|
||||
end
|
||||
|
|
|
@ -553,10 +553,12 @@ export function expandFollowingFail(id, error) {
|
|||
|
||||
export function fetchRelationships(accountIds) {
|
||||
return (dispatch, getState) => {
|
||||
const loadedRelationships = getState().get('relationships');
|
||||
const state = getState();
|
||||
const loadedRelationships = state.get('relationships');
|
||||
const newAccountIds = accountIds.filter(id => loadedRelationships.get(id, null) === null);
|
||||
const signedIn = !!state.getIn(['meta', 'me']);
|
||||
|
||||
if (newAccountIds.length === 0) {
|
||||
if (!signedIn || newAccountIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import api from 'flavours/glitch/util/api';
|
||||
import { debounce } from 'lodash';
|
||||
import compareId from 'flavours/glitch/util/compare_id';
|
||||
import { List as ImmutableList } from 'immutable';
|
||||
|
||||
export const MARKERS_FETCH_REQUEST = 'MARKERS_FETCH_REQUEST';
|
||||
export const MARKERS_FETCH_SUCCESS = 'MARKERS_FETCH_SUCCESS';
|
||||
|
@ -11,7 +12,7 @@ export const synchronouslySubmitMarkers = () => (dispatch, getState) => {
|
|||
const accessToken = getState().getIn(['meta', 'access_token'], '');
|
||||
const params = _buildParams(getState());
|
||||
|
||||
if (Object.keys(params).length === 0) {
|
||||
if (Object.keys(params).length === 0 || accessToken === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -63,7 +64,7 @@ export const synchronouslySubmitMarkers = () => (dispatch, getState) => {
|
|||
const _buildParams = (state) => {
|
||||
const params = {};
|
||||
|
||||
const lastHomeId = state.getIn(['timelines', 'home', 'items']).find(item => item !== null);
|
||||
const lastHomeId = state.getIn(['timelines', 'home', 'items'], ImmutableList()).find(item => item !== null);
|
||||
const lastNotificationId = state.getIn(['notifications', 'lastReadId']);
|
||||
|
||||
if (lastHomeId && compareId(lastHomeId, state.getIn(['markers', 'home'])) > 0) {
|
||||
|
@ -82,9 +83,10 @@ const _buildParams = (state) => {
|
|||
};
|
||||
|
||||
const debouncedSubmitMarkers = debounce((dispatch, getState) => {
|
||||
const accessToken = getState().getIn(['meta', 'access_token'], '');
|
||||
const params = _buildParams(getState());
|
||||
|
||||
if (Object.keys(params).length === 0) {
|
||||
if (Object.keys(params).length === 0 || accessToken === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,19 +1,5 @@
|
|||
import {
|
||||
SET_BROWSER_SUPPORT,
|
||||
SET_SUBSCRIPTION,
|
||||
CLEAR_SUBSCRIPTION,
|
||||
SET_ALERTS,
|
||||
setAlerts,
|
||||
} from './setter';
|
||||
import { register, saveSettings } from './registerer';
|
||||
|
||||
export {
|
||||
SET_BROWSER_SUPPORT,
|
||||
SET_SUBSCRIPTION,
|
||||
CLEAR_SUBSCRIPTION,
|
||||
SET_ALERTS,
|
||||
register,
|
||||
};
|
||||
import { setAlerts } from './setter';
|
||||
import { saveSettings } from './registerer';
|
||||
|
||||
export function changeAlerts(path, value) {
|
||||
return dispatch => {
|
||||
|
@ -21,3 +7,11 @@ export function changeAlerts(path, value) {
|
|||
dispatch(saveSettings());
|
||||
};
|
||||
}
|
||||
|
||||
export {
|
||||
CLEAR_SUBSCRIPTION,
|
||||
SET_BROWSER_SUPPORT,
|
||||
SET_SUBSCRIPTION,
|
||||
SET_ALERTS,
|
||||
} from './setter';
|
||||
export { register } from './registerer';
|
||||
|
|
|
@ -1,27 +0,0 @@
|
|||
import api from 'flavours/glitch/util/api';
|
||||
|
||||
export const RULES_FETCH_REQUEST = 'RULES_FETCH_REQUEST';
|
||||
export const RULES_FETCH_SUCCESS = 'RULES_FETCH_SUCCESS';
|
||||
export const RULES_FETCH_FAIL = 'RULES_FETCH_FAIL';
|
||||
|
||||
export const fetchRules = () => (dispatch, getState) => {
|
||||
dispatch(fetchRulesRequest());
|
||||
|
||||
api(getState)
|
||||
.get('/api/v1/instance').then(({ data }) => dispatch(fetchRulesSuccess(data.rules)))
|
||||
.catch(err => dispatch(fetchRulesFail(err)));
|
||||
};
|
||||
|
||||
const fetchRulesRequest = () => ({
|
||||
type: RULES_FETCH_REQUEST,
|
||||
});
|
||||
|
||||
const fetchRulesSuccess = rules => ({
|
||||
type: RULES_FETCH_SUCCESS,
|
||||
rules,
|
||||
});
|
||||
|
||||
const fetchRulesFail = error => ({
|
||||
type: RULES_FETCH_FAIL,
|
||||
error,
|
||||
});
|
|
@ -0,0 +1,30 @@
|
|||
import api from 'flavours/glitch/util/api';
|
||||
import { importFetchedAccount } from './importer';
|
||||
|
||||
export const SERVER_FETCH_REQUEST = 'Server_FETCH_REQUEST';
|
||||
export const SERVER_FETCH_SUCCESS = 'Server_FETCH_SUCCESS';
|
||||
export const SERVER_FETCH_FAIL = 'Server_FETCH_FAIL';
|
||||
|
||||
export const fetchServer = () => (dispatch, getState) => {
|
||||
dispatch(fetchServerRequest());
|
||||
|
||||
api(getState)
|
||||
.get('/api/v2/instance').then(({ data }) => {
|
||||
if (data.contact.account) dispatch(importFetchedAccount(data.contact.account));
|
||||
dispatch(fetchServerSuccess(data));
|
||||
}).catch(err => dispatch(fetchServerFail(err)));
|
||||
};
|
||||
|
||||
const fetchServerRequest = () => ({
|
||||
type: SERVER_FETCH_REQUEST,
|
||||
});
|
||||
|
||||
const fetchServerSuccess = server => ({
|
||||
type: SERVER_FETCH_SUCCESS,
|
||||
server,
|
||||
});
|
||||
|
||||
const fetchServerFail = error => ({
|
||||
type: SERVER_FETCH_FAIL,
|
||||
error,
|
||||
});
|
|
@ -9,6 +9,7 @@ import { defineMessages, injectIntl } from 'react-intl';
|
|||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { me } from 'flavours/glitch/util/initial_state';
|
||||
import RelativeTimestamp from './relative_timestamp';
|
||||
import Skeleton from 'flavours/glitch/components/skeleton';
|
||||
|
||||
const messages = defineMessages({
|
||||
follow: { id: 'account.follow', defaultMessage: 'Follow' },
|
||||
|
@ -26,7 +27,7 @@ export default @injectIntl
|
|||
class Account extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
account: ImmutablePropTypes.map.isRequired,
|
||||
account: ImmutablePropTypes.map,
|
||||
onFollow: PropTypes.func.isRequired,
|
||||
onBlock: PropTypes.func.isRequired,
|
||||
onMute: PropTypes.func.isRequired,
|
||||
|
@ -77,7 +78,16 @@ class Account extends ImmutablePureComponent {
|
|||
} = this.props;
|
||||
|
||||
if (!account) {
|
||||
return <div />;
|
||||
return (
|
||||
<div className='account'>
|
||||
<div className='account__wrapper'>
|
||||
<div className='account__display-name'>
|
||||
<div className='account__avatar-wrapper'><Skeleton width={36} height={36} /></div>
|
||||
<DisplayName />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (hidden) {
|
||||
|
|
|
@ -17,6 +17,7 @@ class ColumnHeader extends React.PureComponent {
|
|||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object,
|
||||
identity: PropTypes.object,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
|
@ -150,7 +151,7 @@ class ColumnHeader extends React.PureComponent {
|
|||
collapsedContent.push(moveButtons);
|
||||
}
|
||||
|
||||
if (children || (multiColumn && this.props.onPin)) {
|
||||
if (this.context.identity.signedIn && (children || (multiColumn && this.props.onPin))) {
|
||||
collapseButton = (
|
||||
<button
|
||||
className={collapsibleButtonClassName}
|
||||
|
|
|
@ -3,6 +3,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
|||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { autoPlayGif } from 'flavours/glitch/util/initial_state';
|
||||
import Skeleton from 'flavours/glitch/components/skeleton';
|
||||
|
||||
export default class DisplayName extends React.PureComponent {
|
||||
|
||||
|
@ -46,15 +47,16 @@ export default class DisplayName extends React.PureComponent {
|
|||
|
||||
const computedClass = classNames('display-name', { inline }, className);
|
||||
|
||||
if (!account) return null;
|
||||
|
||||
let displayName, suffix;
|
||||
let acct;
|
||||
|
||||
let acct = account.get('acct');
|
||||
if (account) {
|
||||
acct = account.get('acct');
|
||||
|
||||
if (acct.indexOf('@') === -1 && localDomain) {
|
||||
acct = `${acct}@${localDomain}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (others && others.size > 0) {
|
||||
displayName = others.take(2).map(a => (
|
||||
|
@ -80,9 +82,12 @@ export default class DisplayName extends React.PureComponent {
|
|||
<span className='display-name__account'>@{acct}</span>
|
||||
</a>
|
||||
);
|
||||
} else {
|
||||
} else if (account) {
|
||||
displayName = <bdi><strong className='display-name__html' dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }} /></bdi>;
|
||||
suffix = <span className='display-name__account'>@{acct}</span>;
|
||||
} else {
|
||||
displayName = <bdi><strong className='display-name__html'><Skeleton width='10ch' /></strong></bdi>;
|
||||
suffix = <span className='display-name__account'><Skeleton width='7ch' /></span>;
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import React from 'react';
|
||||
|
||||
const Logo = () => (
|
||||
<svg viewBox='0 0 216.4144 232.00976' className='logo'>
|
||||
<use xlinkHref='#mastodon-svg-logo' />
|
||||
<svg viewBox='0 0 261 66' className='logo'>
|
||||
<use xlinkHref='#logo-symbol-wordmark' />
|
||||
</svg>
|
||||
);
|
||||
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
const NotSignedInIndicator = () => (
|
||||
<div className='scrollable scrollable--flex'>
|
||||
<div className='empty-column-indicator'>
|
||||
<FormattedMessage id='not_signed_in_indicator.not_signed_in' defaultMessage='You need to sign in to access this resource.' />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default NotSignedInIndicator;
|
|
@ -34,6 +34,10 @@ const makeEmojiMap = record => record.get('emojis').reduce((obj, emoji) => {
|
|||
export default @injectIntl
|
||||
class Poll extends ImmutablePureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
identity: PropTypes.object,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
poll: ImmutablePropTypes.map,
|
||||
intl: PropTypes.object.isRequired,
|
||||
|
@ -217,7 +221,7 @@ class Poll extends ImmutablePureComponent {
|
|||
</ul>
|
||||
|
||||
<div className='poll__footer'>
|
||||
{!showResults && <button className='button button-secondary' disabled={disabled} onClick={this.handleVote}><FormattedMessage id='poll.vote' defaultMessage='Vote' /></button>}
|
||||
{!showResults && <button className='button button-secondary' disabled={disabled || !this.context.identity.signedIn} onClick={this.handleVote}><FormattedMessage id='poll.vote' defaultMessage='Vote' /></button>}
|
||||
{showResults && !this.props.disabled && <span><button className='poll__link' onClick={this.handleRefresh}><FormattedMessage id='poll.refresh' defaultMessage='Refresh' /></button> · </span>}
|
||||
{votesCount}
|
||||
{poll.get('expires_at') && <span> · {timeRemaining}</span>}
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { domain } from 'flavours/glitch/util/initial_state';
|
||||
import { fetchServer } from 'flavours/glitch/actions/server';
|
||||
import { connect } from 'react-redux';
|
||||
import Account from 'flavours/glitch/containers/account_container';
|
||||
import ShortNumber from 'flavours/glitch/components/short_number';
|
||||
import Skeleton from 'flavours/glitch/components/skeleton';
|
||||
import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
|
||||
|
||||
const messages = defineMessages({
|
||||
aboutActiveUsers: { id: 'server_banner.about_active_users', defaultMessage: 'People using this server during the last 30 days (Monthly Active Users)' },
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
server: state.get('server'),
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
@injectIntl
|
||||
class ServerBanner extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
server: PropTypes.object,
|
||||
dispatch: PropTypes.func,
|
||||
intl: PropTypes.object,
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
const { dispatch } = this.props;
|
||||
dispatch(fetchServer());
|
||||
}
|
||||
|
||||
render () {
|
||||
const { server, intl } = this.props;
|
||||
const isLoading = server.get('isLoading');
|
||||
|
||||
return (
|
||||
<div className='server-banner'>
|
||||
<div className='server-banner__introduction'>
|
||||
<FormattedMessage id='server_banner.introduction' defaultMessage='{domain} is part of the decentralized social network powered by {mastodon}.' values={{ domain: <strong>{domain}</strong>, mastodon: <a href='https://joinmastodon.org' target='_blank'>Mastodon</a> }} />
|
||||
</div>
|
||||
|
||||
<img src={server.get('thumbnail')} alt={server.get('title')} className='server-banner__hero' />
|
||||
|
||||
<div className='server-banner__description'>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Skeleton width='100%' />
|
||||
<br />
|
||||
<Skeleton width='100%' />
|
||||
<br />
|
||||
<Skeleton width='70%' />
|
||||
</>
|
||||
) : server.get('description')}
|
||||
</div>
|
||||
|
||||
<div className='server-banner__meta'>
|
||||
<div className='server-banner__meta__column'>
|
||||
<h4><FormattedMessage id='server_banner.administered_by' defaultMessage='Administered by:' /></h4>
|
||||
|
||||
<Account id={server.getIn(['contact', 'account', 'id'])} />
|
||||
</div>
|
||||
|
||||
<div className='server-banner__meta__column'>
|
||||
<h4><FormattedMessage id='server_banner.server_stats' defaultMessage='Server stats:' /></h4>
|
||||
|
||||
{isLoading ? (
|
||||
<>
|
||||
<strong className='server-banner__number'><Skeleton width='10ch' /></strong>
|
||||
<br />
|
||||
<span className='server-banner__number-label'><Skeleton width='5ch' /></span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<strong className='server-banner__number'><ShortNumber value={server.getIn(['usage', 'users', 'active_month'])} /></strong>
|
||||
<br />
|
||||
<span className='server-banner__number-label' title={intl.formatMessage(messages.aboutActiveUsers)}><FormattedMessage id='server_banner.active_users' defaultMessage='active users' /></span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr className='spacer' />
|
||||
|
||||
<a className='button button--block button-secondary' href='/about/more' target='_blank'><FormattedMessage id='server_banner.learn_more' defaultMessage='Learn more' /></a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -83,6 +83,7 @@ class Status extends ImmutablePureComponent {
|
|||
onEmbed: PropTypes.func,
|
||||
onHeightChange: PropTypes.func,
|
||||
onToggleHidden: PropTypes.func,
|
||||
onInteractionModal: PropTypes.func,
|
||||
muted: PropTypes.bool,
|
||||
hidden: PropTypes.bool,
|
||||
unread: PropTypes.bool,
|
||||
|
|
|
@ -69,6 +69,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||
onBookmark: PropTypes.func,
|
||||
onFilter: PropTypes.func,
|
||||
onAddFilter: PropTypes.func,
|
||||
onInteractionModal: PropTypes.func,
|
||||
withDismiss: PropTypes.bool,
|
||||
withCounters: PropTypes.bool,
|
||||
showReplyCount: PropTypes.bool,
|
||||
|
@ -86,10 +87,12 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||
]
|
||||
|
||||
handleReplyClick = () => {
|
||||
if (me) {
|
||||
const { signedIn } = this.context.identity;
|
||||
|
||||
if (signedIn) {
|
||||
this.props.onReply(this.props.status, this.context.router.history);
|
||||
} else {
|
||||
this._openInteractionDialog('reply');
|
||||
this.props.onInteractionModal('reply', this.props.status);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -101,10 +104,22 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
handleFavouriteClick = (e) => {
|
||||
if (me) {
|
||||
const { signedIn } = this.context.identity;
|
||||
|
||||
if (signedIn) {
|
||||
this.props.onFavourite(this.props.status, e);
|
||||
} else {
|
||||
this._openInteractionDialog('favourite');
|
||||
this.props.onInteractionModal('favourite', this.props.status);
|
||||
}
|
||||
}
|
||||
|
||||
handleReblogClick = e => {
|
||||
const { signedIn } = this.context.identity;
|
||||
|
||||
if (signedIn) {
|
||||
this.props.onReblog(this.props.status, e);
|
||||
} else {
|
||||
this.props.onInteractionModal('reblog', this.props.status);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -112,18 +127,6 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||
this.props.onBookmark(this.props.status, e);
|
||||
}
|
||||
|
||||
handleReblogClick = e => {
|
||||
if (me) {
|
||||
this.props.onReblog(this.props.status, e);
|
||||
} else {
|
||||
this._openInteractionDialog('reblog');
|
||||
}
|
||||
}
|
||||
|
||||
_openInteractionDialog = type => {
|
||||
window.open(`/interact/${this.props.status.get('id')}?type=${type}`, 'mastodon-intent', 'width=445,height=600,resizable=no,menubar=no,status=no,scrollbars=yes');
|
||||
}
|
||||
|
||||
handleDeleteClick = () => {
|
||||
this.props.onDelete(this.props.status, this.context.router.history);
|
||||
}
|
||||
|
|
|
@ -31,7 +31,7 @@ const createIdentityContext = state => ({
|
|||
signedIn: !!state.meta.me,
|
||||
accountId: state.meta.me,
|
||||
accessToken: state.meta.access_token,
|
||||
permissions: state.role.permissions,
|
||||
permissions: state.role ? state.role.permissions : 0,
|
||||
});
|
||||
|
||||
export default class Mastodon extends React.PureComponent {
|
||||
|
|
|
@ -244,6 +244,14 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
|
|||
});
|
||||
},
|
||||
|
||||
onInteractionModal (type, status) {
|
||||
dispatch(openModal('INTERACTION', {
|
||||
type,
|
||||
accountId: status.getIn(['account', 'id']),
|
||||
url: status.get('url'),
|
||||
}));
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Status));
|
||||
|
|
|
@ -3,7 +3,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
|||
import PropTypes from 'prop-types';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { autoPlayGif, me } from 'flavours/glitch/util/initial_state';
|
||||
import { autoPlayGif, me, title, domain } from 'flavours/glitch/util/initial_state';
|
||||
import { preferencesLink, profileLink, accountAdminLink } from 'flavours/glitch/util/backend_links';
|
||||
import classNames from 'classnames';
|
||||
import Icon from 'flavours/glitch/components/icon';
|
||||
|
@ -14,6 +14,7 @@ import { NavLink } from 'react-router-dom';
|
|||
import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container';
|
||||
import AccountNoteContainer from '../containers/account_note_container';
|
||||
import { PERMISSION_MANAGE_USERS } from 'flavours/glitch/permissions';
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
const messages = defineMessages({
|
||||
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
|
||||
|
@ -54,6 +55,14 @@ const messages = defineMessages({
|
|||
languages: { id: 'account.languages', defaultMessage: 'Change subscribed languages' },
|
||||
});
|
||||
|
||||
const titleFromAccount = account => {
|
||||
const displayName = account.get('display_name');
|
||||
const acct = account.get('acct') === account.get('username') ? `${account.get('username')}@${domain}` : account.get('acct');
|
||||
const prefix = displayName.trim().length === 0 ? account.get('username') : displayName;
|
||||
|
||||
return `${prefix} (@${acct})`;
|
||||
};
|
||||
|
||||
const dateFormatOptions = {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
|
@ -87,6 +96,7 @@ class Header extends ImmutablePureComponent {
|
|||
onAddToList: PropTypes.func.isRequired,
|
||||
onEditAccountNote: PropTypes.func.isRequired,
|
||||
onChangeLanguages: PropTypes.func.isRequired,
|
||||
onInteractionModal: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
domain: PropTypes.string.isRequired,
|
||||
hidden: PropTypes.bool,
|
||||
|
@ -124,6 +134,7 @@ class Header extends ImmutablePureComponent {
|
|||
|
||||
render () {
|
||||
const { account, hidden, intl, domain } = this.props;
|
||||
const { signedIn } = this.context.identity;
|
||||
|
||||
if (!account) {
|
||||
return null;
|
||||
|
@ -157,12 +168,12 @@ class Header extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
if (me !== account.get('id')) {
|
||||
if (!account.get('relationship')) { // Wait until the relationship is loaded
|
||||
if (signedIn && !account.get('relationship')) { // Wait until the relationship is loaded
|
||||
actionBtn = '';
|
||||
} else if (account.getIn(['relationship', 'requested'])) {
|
||||
actionBtn = <Button className={classNames('logo-button', { 'button--with-bell': bellBtn !== '' })} text={intl.formatMessage(messages.cancel_follow_request)} title={intl.formatMessage(messages.requested)} onClick={this.props.onFollow} />;
|
||||
} else if (!account.getIn(['relationship', 'blocking'])) {
|
||||
actionBtn = <Button className={classNames('logo-button', { 'button--destructive': account.getIn(['relationship', 'following']), 'button--with-bell': bellBtn !== '' })} text={intl.formatMessage(account.getIn(['relationship', 'following']) ? messages.unfollow : messages.follow)} onClick={this.props.onFollow} />;
|
||||
actionBtn = <Button className={classNames('logo-button', { 'button--destructive': account.getIn(['relationship', 'following']), 'button--with-bell': bellBtn !== '' })} text={intl.formatMessage(account.getIn(['relationship', 'following']) ? messages.unfollow : messages.follow)} onClick={signedIn ? this.props.onFollow : this.props.onInteractionModal} />;
|
||||
} else if (account.getIn(['relationship', 'blocking'])) {
|
||||
actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.props.onBlock} />;
|
||||
}
|
||||
|
@ -182,7 +193,7 @@ class Header extends ImmutablePureComponent {
|
|||
lockedIcon = <Icon id='lock' title={intl.formatMessage(messages.account_locked)} />;
|
||||
}
|
||||
|
||||
if (account.get('id') !== me && !suspended) {
|
||||
if (signedIn && account.get('id') !== me && !suspended) {
|
||||
menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('username') }), action: this.props.onMention });
|
||||
menu.push({ text: intl.formatMessage(messages.direct, { name: account.get('username') }), action: this.props.onDirect });
|
||||
menu.push(null);
|
||||
|
@ -209,7 +220,7 @@ class Header extends ImmutablePureComponent {
|
|||
menu.push({ text: intl.formatMessage(messages.mutes), to: '/mutes' });
|
||||
menu.push({ text: intl.formatMessage(messages.blocks), to: '/blocks' });
|
||||
menu.push({ text: intl.formatMessage(messages.domain_blocks), to: '/domain_blocks' });
|
||||
} else {
|
||||
} else if (signedIn) {
|
||||
if (account.getIn(['relationship', 'following'])) {
|
||||
if (!account.getIn(['relationship', 'muting'])) {
|
||||
if (account.getIn(['relationship', 'showing_reblogs'])) {
|
||||
|
@ -242,7 +253,7 @@ class Header extends ImmutablePureComponent {
|
|||
menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.props.onReport });
|
||||
}
|
||||
|
||||
if (account.get('acct') !== account.get('username')) {
|
||||
if (signedIn && account.get('acct') !== account.get('username')) {
|
||||
const domain = account.get('acct').split('@')[1];
|
||||
|
||||
menu.push(null);
|
||||
|
@ -301,7 +312,7 @@ class Header extends ImmutablePureComponent {
|
|||
</React.Fragment>
|
||||
)}
|
||||
|
||||
<DropdownMenuContainer items={menu} icon='ellipsis-v' size={24} direction='right' />
|
||||
<DropdownMenuContainer disabled={menu.length === 0} items={menu} icon='ellipsis-v' size={24} direction='right' />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
@ -313,7 +324,7 @@ class Header extends ImmutablePureComponent {
|
|||
</h1>
|
||||
</div>
|
||||
|
||||
<AccountNoteContainer account={account} />
|
||||
{signedIn && <AccountNoteContainer account={account} />}
|
||||
|
||||
{!(suspended || hidden) && (
|
||||
<div className='account__header__extra'>
|
||||
|
@ -339,6 +350,10 @@ class Header extends ImmutablePureComponent {
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Helmet>
|
||||
<title>{titleFromAccount(account)} - {title}</title>
|
||||
</Helmet>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ export default class Header extends ImmutablePureComponent {
|
|||
onEndorseToggle: PropTypes.func.isRequired,
|
||||
onAddToList: PropTypes.func.isRequired,
|
||||
onChangeLanguages: PropTypes.func.isRequired,
|
||||
onInteractionModal: PropTypes.func.isRequired,
|
||||
hideTabs: PropTypes.bool,
|
||||
domain: PropTypes.string.isRequired,
|
||||
hidden: PropTypes.bool,
|
||||
|
@ -97,6 +98,10 @@ export default class Header extends ImmutablePureComponent {
|
|||
this.props.onChangeLanguages(this.props.account);
|
||||
}
|
||||
|
||||
handleInteractionModal = () => {
|
||||
this.props.onInteractionModal(this.props.account);
|
||||
}
|
||||
|
||||
render () {
|
||||
const { account, hidden, hideTabs } = this.props;
|
||||
|
||||
|
@ -124,6 +129,7 @@ export default class Header extends ImmutablePureComponent {
|
|||
onAddToList={this.handleAddToList}
|
||||
onEditAccountNote={this.handleEditAccountNote}
|
||||
onChangeLanguages={this.handleChangeLanguages}
|
||||
onInteractionModal={this.handleInteractionModal}
|
||||
domain={this.props.domain}
|
||||
hidden={hidden}
|
||||
/>
|
||||
|
|
|
@ -58,6 +58,14 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
|||
}
|
||||
},
|
||||
|
||||
onInteractionModal (account) {
|
||||
dispatch(openModal('INTERACTION', {
|
||||
type: 'follow',
|
||||
accountId: account.get('id'),
|
||||
url: account.get('url'),
|
||||
}));
|
||||
},
|
||||
|
||||
onBlock (account) {
|
||||
if (account.getIn(['relationship', 'blocking'])) {
|
||||
dispatch(unblockAccount(account.get('id')));
|
||||
|
|
|
@ -9,6 +9,8 @@ import { expandCommunityTimeline } from 'flavours/glitch/actions/timelines';
|
|||
import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns';
|
||||
import ColumnSettingsContainer from './containers/column_settings_container';
|
||||
import { connectCommunityStream } from 'flavours/glitch/actions/streaming';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { title } from 'flavours/glitch/util/initial_state';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'column.community', defaultMessage: 'Local timeline' },
|
||||
|
@ -39,6 +41,7 @@ class CommunityTimeline extends React.PureComponent {
|
|||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object,
|
||||
identity: PropTypes.object,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
|
@ -72,20 +75,32 @@ class CommunityTimeline extends React.PureComponent {
|
|||
|
||||
componentDidMount () {
|
||||
const { dispatch, onlyMedia } = this.props;
|
||||
const { signedIn } = this.context.identity;
|
||||
|
||||
dispatch(expandCommunityTimeline({ onlyMedia }));
|
||||
|
||||
if (signedIn) {
|
||||
this.disconnect = dispatch(connectCommunityStream({ onlyMedia }));
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
const { signedIn } = this.context.identity;
|
||||
|
||||
if (prevProps.onlyMedia !== this.props.onlyMedia) {
|
||||
const { dispatch, onlyMedia } = this.props;
|
||||
|
||||
if (this.disconnect) {
|
||||
this.disconnect();
|
||||
}
|
||||
|
||||
dispatch(expandCommunityTimeline({ onlyMedia }));
|
||||
|
||||
if (signedIn) {
|
||||
this.disconnect = dispatch(connectCommunityStream({ onlyMedia }));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
if (this.disconnect) {
|
||||
|
@ -132,6 +147,10 @@ class CommunityTimeline extends React.PureComponent {
|
|||
bindToDocument={!multiColumn}
|
||||
regex={this.props.regex}
|
||||
/>
|
||||
|
||||
<Helmet>
|
||||
<title>{intl.formatMessage(messages.title)} - {title}</title>
|
||||
</Helmet>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -13,6 +13,8 @@ import RadioButton from 'flavours/glitch/components/radio_button';
|
|||
import LoadMore from 'flavours/glitch/components/load_more';
|
||||
import ScrollContainer from 'flavours/glitch/containers/scroll_container';
|
||||
import LoadingIndicator from 'flavours/glitch/components/loading_indicator';
|
||||
import { title } from 'flavours/glitch/util/initial_state';
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'column.directory', defaultMessage: 'Browse profiles' },
|
||||
|
@ -165,6 +167,10 @@ class Directory extends React.PureComponent {
|
|||
/>
|
||||
|
||||
{multiColumn && !pinned ? <ScrollContainer scrollKey='directory'>{scrollableArea}</ScrollContainer> : scrollableArea}
|
||||
|
||||
<Helmet>
|
||||
<title>{intl.formatMessage(messages.title)} - {title}</title>
|
||||
</Helmet>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -12,6 +12,8 @@ import Suggestions from './suggestions';
|
|||
import Search from 'flavours/glitch/features/compose/containers/search_container';
|
||||
import SearchResults from './results';
|
||||
import { showTrends } from 'flavours/glitch/util/initial_state';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { title } from 'flavours/glitch/util/initial_state';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'explore.title', defaultMessage: 'Explore' },
|
||||
|
@ -29,13 +31,13 @@ class Explore extends React.PureComponent {
|
|||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object,
|
||||
identity: PropTypes.object,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
intl: PropTypes.object.isRequired,
|
||||
multiColumn: PropTypes.bool,
|
||||
isSearching: PropTypes.bool,
|
||||
layout: PropTypes.string,
|
||||
};
|
||||
|
||||
handleHeaderClick = () => {
|
||||
|
@ -47,22 +49,21 @@ class Explore extends React.PureComponent {
|
|||
}
|
||||
|
||||
render () {
|
||||
const { intl, multiColumn, isSearching, layout } = this.props;
|
||||
const { intl, multiColumn, isSearching } = this.props;
|
||||
const { signedIn } = this.context.identity;
|
||||
|
||||
return (
|
||||
<Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.title)}>
|
||||
{layout === 'mobile' ? (
|
||||
<div className='explore__search-header'>
|
||||
<Search />
|
||||
</div>
|
||||
) : (
|
||||
<ColumnHeader
|
||||
icon={isSearching ? 'search' : 'hashtag'}
|
||||
title={intl.formatMessage(isSearching ? messages.searchResults : messages.title)}
|
||||
onClick={this.handleHeaderClick}
|
||||
multiColumn={multiColumn}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className='explore__search-header'>
|
||||
<Search />
|
||||
</div>
|
||||
|
||||
<div className='scrollable scrollable--flex'>
|
||||
{isSearching ? (
|
||||
|
@ -73,7 +74,7 @@ class Explore extends React.PureComponent {
|
|||
<NavLink exact to='/explore'><FormattedMessage id='explore.trending_statuses' defaultMessage='Posts' /></NavLink>
|
||||
<NavLink exact to='/explore/tags'><FormattedMessage id='explore.trending_tags' defaultMessage='Hashtags' /></NavLink>
|
||||
<NavLink exact to='/explore/links'><FormattedMessage id='explore.trending_links' defaultMessage='News' /></NavLink>
|
||||
<NavLink exact to='/explore/suggestions'><FormattedMessage id='explore.suggested_follows' defaultMessage='For you' /></NavLink>
|
||||
{signedIn && <NavLink exact to='/explore/suggestions'><FormattedMessage id='explore.suggested_follows' defaultMessage='For you' /></NavLink>}
|
||||
</div>
|
||||
|
||||
<Switch>
|
||||
|
@ -82,6 +83,10 @@ class Explore extends React.PureComponent {
|
|||
<Route path='/explore/suggestions' component={Suggestions} />
|
||||
<Route exact path={['/explore', '/explore/posts', '/search']} component={Statuses} componentParams={{ multiColumn }} />
|
||||
</Switch>
|
||||
|
||||
<Helmet>
|
||||
<title>{intl.formatMessage(messages.title)} - {title}</title>
|
||||
</Helmet>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -5,6 +5,7 @@ import Story from './components/story';
|
|||
import LoadingIndicator from 'flavours/glitch/components/loading_indicator';
|
||||
import { connect } from 'react-redux';
|
||||
import { fetchTrendingLinks } from 'flavours/glitch/actions/trends';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
links: state.getIn(['trends', 'links', 'items']),
|
||||
|
@ -28,6 +29,16 @@ class Links extends React.PureComponent {
|
|||
render () {
|
||||
const { isLoading, links } = this.props;
|
||||
|
||||
if (!isLoading && links.isEmpty()) {
|
||||
return (
|
||||
<div className='explore__links scrollable scrollable--flex'>
|
||||
<div className='empty-column-indicator'>
|
||||
<FormattedMessage id='empty_column.explore_statuses' defaultMessage='Nothing is trending right now. Check back later!' />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='explore__links'>
|
||||
{isLoading ? (<LoadingIndicator />) : links.map(link => (
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { injectIntl, defineMessages, FormattedMessage } from 'react-intl';
|
||||
import { connect } from 'react-redux';
|
||||
import { expandSearch } from 'flavours/glitch/actions/search';
|
||||
import Account from 'flavours/glitch/containers/account_container';
|
||||
|
@ -10,10 +10,17 @@ import { ImmutableHashtag as Hashtag } from 'flavours/glitch/components/hashtag'
|
|||
import { List as ImmutableList } from 'immutable';
|
||||
import LoadMore from 'flavours/glitch/components/load_more';
|
||||
import LoadingIndicator from 'flavours/glitch/components/loading_indicator';
|
||||
import { title } from 'flavours/glitch/util/initial_state';
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'search_results.title', defaultMessage: 'Search for {q}' },
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
isLoading: state.getIn(['search', 'isLoading']),
|
||||
results: state.getIn(['search', 'results']),
|
||||
q: state.getIn(['search', 'searchTerm']),
|
||||
});
|
||||
|
||||
const appendLoadMore = (id, list, onLoadMore) => {
|
||||
|
@ -37,6 +44,7 @@ const renderStatuses = (results, onLoadMore) => appendLoadMore('statuses', resul
|
|||
)), onLoadMore);
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
@injectIntl
|
||||
class Results extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
|
@ -44,6 +52,8 @@ class Results extends React.PureComponent {
|
|||
isLoading: PropTypes.bool,
|
||||
multiColumn: PropTypes.bool,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
q: PropTypes.string,
|
||||
intl: PropTypes.object,
|
||||
};
|
||||
|
||||
state = {
|
||||
|
@ -64,7 +74,7 @@ class Results extends React.PureComponent {
|
|||
}
|
||||
|
||||
render () {
|
||||
const { isLoading, results } = this.props;
|
||||
const { intl, isLoading, q, results } = this.props;
|
||||
const { type } = this.state;
|
||||
|
||||
let filteredResults = ImmutableList();
|
||||
|
@ -106,6 +116,10 @@ class Results extends React.PureComponent {
|
|||
<div className='explore__search-results'>
|
||||
{isLoading ? <LoadingIndicator /> : filteredResults}
|
||||
</div>
|
||||
|
||||
<Helmet>
|
||||
<title>{intl.formatMessage(messages.title, { q })} - {title}</title>
|
||||
</Helmet>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import AccountCard from 'flavours/glitch/features/directory/components/account_c
|
|||
import LoadingIndicator from 'flavours/glitch/components/loading_indicator';
|
||||
import { connect } from 'react-redux';
|
||||
import { fetchSuggestions, dismissSuggestion } from 'flavours/glitch/actions/suggestions';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
suggestions: state.getIn(['suggestions', 'items']),
|
||||
|
@ -33,6 +34,16 @@ class Suggestions extends React.PureComponent {
|
|||
render () {
|
||||
const { isLoading, suggestions } = this.props;
|
||||
|
||||
if (!isLoading && suggestions.isEmpty()) {
|
||||
return (
|
||||
<div className='explore__suggestions scrollable scrollable--flex'>
|
||||
<div className='empty-column-indicator'>
|
||||
<FormattedMessage id='empty_column.explore_statuses' defaultMessage='Nothing is trending right now. Check back later!' />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='explore__suggestions'>
|
||||
{isLoading ? <LoadingIndicator /> : suggestions.map(suggestion => (
|
||||
|
|
|
@ -5,6 +5,7 @@ import { ImmutableHashtag as Hashtag } from 'flavours/glitch/components/hashtag'
|
|||
import LoadingIndicator from 'flavours/glitch/components/loading_indicator';
|
||||
import { connect } from 'react-redux';
|
||||
import { fetchTrendingHashtags } from 'flavours/glitch/actions/trends';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
hashtags: state.getIn(['trends', 'tags', 'items']),
|
||||
|
@ -28,6 +29,16 @@ class Tags extends React.PureComponent {
|
|||
render () {
|
||||
const { isLoading, hashtags } = this.props;
|
||||
|
||||
if (!isLoading && hashtags.isEmpty()) {
|
||||
return (
|
||||
<div className='explore__links scrollable scrollable--flex'>
|
||||
<div className='empty-column-indicator'>
|
||||
<FormattedMessage id='empty_column.explore_statuses' defaultMessage='Nothing is trending right now. Check back later!' />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='explore__links'>
|
||||
{isLoading ? (<LoadingIndicator />) : hashtags.map(hashtag => (
|
||||
|
|
|
@ -10,7 +10,6 @@ import { requestBrowserPermission } from 'flavours/glitch/actions/notifications'
|
|||
import { markAsPartial } from 'flavours/glitch/actions/timelines';
|
||||
import Column from 'flavours/glitch/features/ui/components/column';
|
||||
import Account from './components/account';
|
||||
import Logo from 'flavours/glitch/components/logo';
|
||||
import imageGreeting from 'mastodon/../images/elephant_ui_greeting.svg';
|
||||
import Button from 'flavours/glitch/components/button';
|
||||
|
||||
|
@ -78,7 +77,10 @@ class FollowRecommendations extends ImmutablePureComponent {
|
|||
<Column>
|
||||
<div className='scrollable follow-recommendations-container'>
|
||||
<div className='column-title'>
|
||||
<Logo />
|
||||
<svg viewBox='0 0 79 79' className='logo'>
|
||||
<use xlinkHref='#logo-symbol-icon' />
|
||||
</svg>
|
||||
|
||||
<h3><FormattedMessage id='follow_recommendations.heading' defaultMessage="Follow people you'd like to see posts from! Here are some suggestions." /></h3>
|
||||
<p><FormattedMessage id='follow_recommendations.lead' defaultMessage="Posts from people you follow will show up in chronological order on your home feed. Don't be afraid to make mistakes, you can unfollow people just as easily any time!" /></p>
|
||||
</div>
|
||||
|
|
|
@ -14,6 +14,8 @@ import { isEqual } from 'lodash';
|
|||
import { fetchHashtag, followHashtag, unfollowHashtag } from 'flavours/glitch/actions/tags';
|
||||
import Icon from 'flavours/glitch/components/icon';
|
||||
import classNames from 'classnames';
|
||||
import { title } from 'flavours/glitch/util/initial_state';
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
const messages = defineMessages({
|
||||
followHashtag: { id: 'hashtag.follow', defaultMessage: 'Follow hashtag' },
|
||||
|
@ -31,6 +33,10 @@ class HashtagTimeline extends React.PureComponent {
|
|||
|
||||
disconnects = [];
|
||||
|
||||
static contextTypes = {
|
||||
identity: PropTypes.object,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
columnId: PropTypes.string,
|
||||
|
@ -90,6 +96,12 @@ class HashtagTimeline extends React.PureComponent {
|
|||
}
|
||||
|
||||
_subscribe (dispatch, id, tags = {}, local) {
|
||||
const { signedIn } = this.context.identity;
|
||||
|
||||
if (!signedIn) {
|
||||
return;
|
||||
}
|
||||
|
||||
let any = (tags.any || []).map(tag => tag.value);
|
||||
let all = (tags.all || []).map(tag => tag.value);
|
||||
let none = (tags.none || []).map(tag => tag.value);
|
||||
|
@ -158,6 +170,11 @@ class HashtagTimeline extends React.PureComponent {
|
|||
handleFollow = () => {
|
||||
const { dispatch, params, tag } = this.props;
|
||||
const { id } = params;
|
||||
const { signedIn } = this.context.identity;
|
||||
|
||||
if (!signedIn) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (tag.get('following')) {
|
||||
dispatch(unfollowHashtag(id));
|
||||
|
@ -170,6 +187,7 @@ class HashtagTimeline extends React.PureComponent {
|
|||
const { hasUnread, columnId, multiColumn, tag, intl } = this.props;
|
||||
const { id, local } = this.props.params;
|
||||
const pinned = !!columnId;
|
||||
const { signedIn } = this.context.identity;
|
||||
|
||||
let followButton;
|
||||
|
||||
|
@ -177,7 +195,7 @@ class HashtagTimeline extends React.PureComponent {
|
|||
const following = tag.get('following');
|
||||
|
||||
followButton = (
|
||||
<button className={classNames('column-header__button')} onClick={this.handleFollow} title={intl.formatMessage(following ? messages.unfollowHashtag : messages.followHashtag)} aria-label={intl.formatMessage(following ? messages.unfollowHashtag : messages.followHashtag)} aria-pressed={following ? 'true' : 'false'}>
|
||||
<button className={classNames('column-header__button')} onClick={this.handleFollow} disabled={!signedIn} title={intl.formatMessage(following ? messages.unfollowHashtag : messages.followHashtag)} aria-label={intl.formatMessage(following ? messages.unfollowHashtag : messages.followHashtag)} aria-pressed={following ? 'true' : 'false'}>
|
||||
<Icon id={following ? 'user-times' : 'user-plus'} fixedWidth className='column-header__icon' />
|
||||
</button>
|
||||
);
|
||||
|
@ -208,6 +226,10 @@ class HashtagTimeline extends React.PureComponent {
|
|||
emptyMessage={<FormattedMessage id='empty_column.hashtag' defaultMessage='There is nothing in this hashtag yet.' />}
|
||||
bindToDocument={!multiColumn}
|
||||
/>
|
||||
|
||||
<Helmet>
|
||||
<title>{`#${id}`} - {title}</title>
|
||||
</Helmet>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -13,6 +13,9 @@ import { fetchAnnouncements, toggleShowAnnouncements } from 'flavours/glitch/act
|
|||
import AnnouncementsContainer from 'flavours/glitch/features/getting_started/containers/announcements_container';
|
||||
import classNames from 'classnames';
|
||||
import IconWithBadge from 'flavours/glitch/components/icon_with_badge';
|
||||
import NotSignedInIndicator from 'flavours/glitch/components/not_signed_in_indicator';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { title } from 'flavours/glitch/util/initial_state';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'column.home', defaultMessage: 'Home' },
|
||||
|
@ -33,6 +36,10 @@ export default @connect(mapStateToProps)
|
|||
@injectIntl
|
||||
class HomeTimeline extends React.PureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
identity: PropTypes.object,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
|
@ -115,6 +122,7 @@ class HomeTimeline extends React.PureComponent {
|
|||
render () {
|
||||
const { intl, hasUnread, columnId, multiColumn, hasAnnouncements, unreadAnnouncements, showAnnouncements } = this.props;
|
||||
const pinned = !!columnId;
|
||||
const { signedIn } = this.context.identity;
|
||||
|
||||
let announcementsButton = null;
|
||||
|
||||
|
@ -149,6 +157,7 @@ class HomeTimeline extends React.PureComponent {
|
|||
<ColumnSettingsContainer />
|
||||
</ColumnHeader>
|
||||
|
||||
{signedIn ? (
|
||||
<StatusListContainer
|
||||
trackScroll={!pinned}
|
||||
scrollKey={`home_timeline-${columnId}`}
|
||||
|
@ -158,6 +167,11 @@ class HomeTimeline extends React.PureComponent {
|
|||
bindToDocument={!multiColumn}
|
||||
regex={this.props.regex}
|
||||
/>
|
||||
) : <NotSignedInIndicator />}
|
||||
|
||||
<Helmet>
|
||||
<title>{intl.formatMessage(messages.title)} - {title}</title>
|
||||
</Helmet>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,132 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { registrationsOpen } from 'flavours/glitch/util/initial_state';
|
||||
import { connect } from 'react-redux';
|
||||
import Icon from 'flavours/glitch/components/icon';
|
||||
import classNames from 'classnames';
|
||||
|
||||
const mapStateToProps = (state, { accountId }) => ({
|
||||
displayNameHtml: state.getIn(['accounts', accountId, 'display_name_html']),
|
||||
});
|
||||
|
||||
class Copypaste extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
value: PropTypes.string,
|
||||
};
|
||||
|
||||
state = {
|
||||
copied: false,
|
||||
};
|
||||
|
||||
setRef = c => {
|
||||
this.input = c;
|
||||
}
|
||||
|
||||
handleInputClick = () => {
|
||||
this.setState({ copied: false });
|
||||
this.input.focus();
|
||||
this.input.select();
|
||||
this.input.setSelectionRange(0, this.input.value.length);
|
||||
}
|
||||
|
||||
handleButtonClick = () => {
|
||||
const { value } = this.props;
|
||||
navigator.clipboard.writeText(value);
|
||||
this.input.blur();
|
||||
this.setState({ copied: true });
|
||||
this.timeout = setTimeout(() => this.setState({ copied: false }), 700);
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
if (this.timeout) clearTimeout(this.timeout);
|
||||
}
|
||||
|
||||
render () {
|
||||
const { value } = this.props;
|
||||
const { copied } = this.state;
|
||||
|
||||
return (
|
||||
<div className={classNames('copypaste', { copied })}>
|
||||
<input
|
||||
type='text'
|
||||
ref={this.setRef}
|
||||
value={value}
|
||||
readOnly
|
||||
onClick={this.handleInputClick}
|
||||
/>
|
||||
|
||||
<button className='button' onClick={this.handleButtonClick}>
|
||||
{copied ? <FormattedMessage id='copypaste.copied' defaultMessage='Copied' /> : <FormattedMessage id='copypaste.copy' defaultMessage='Copy' />}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
class InteractionModal extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
displayNameHtml: PropTypes.string,
|
||||
url: PropTypes.string,
|
||||
type: PropTypes.oneOf(['reply', 'reblog', 'favourite', 'follow']),
|
||||
};
|
||||
|
||||
render () {
|
||||
const { url, type, displayNameHtml } = this.props;
|
||||
|
||||
const name = <bdi dangerouslySetInnerHTML={{ __html: displayNameHtml }} />;
|
||||
|
||||
let title, actionDescription, icon;
|
||||
|
||||
switch(type) {
|
||||
case 'reply':
|
||||
icon = <Icon id='reply' />;
|
||||
title = <FormattedMessage id='interaction_modal.title.reply' defaultMessage="Reply to {name}'s post" values={{ name }} />;
|
||||
actionDescription = <FormattedMessage id='interaction_modal.description.reply' defaultMessage='With an account on Mastodon, you can respond to this post.' />;
|
||||
break;
|
||||
case 'reblog':
|
||||
icon = <Icon id='retweet' />;
|
||||
title = <FormattedMessage id='interaction_modal.title.reblog' defaultMessage="Boost {name}'s post" values={{ name }} />;
|
||||
actionDescription = <FormattedMessage id='interaction_modal.description.reblog' defaultMessage='With an account on Mastodon, you can boost this post to share it with your own followers.' />;
|
||||
break;
|
||||
case 'favourite':
|
||||
icon = <Icon id='star' />;
|
||||
title = <FormattedMessage id='interaction_modal.title.favourite' defaultMessage="Favourite {name}'s post" values={{ name }} />;
|
||||
actionDescription = <FormattedMessage id='interaction_modal.description.favourite' defaultMessage='With an account on Mastodon, you can favourite this post to let the author know you appreciate it and save it for later.' />;
|
||||
break;
|
||||
case 'follow':
|
||||
icon = <Icon id='user-plus' />;
|
||||
title = <FormattedMessage id='interaction_modal.title.follow' defaultMessage='Follow {name}' values={{ name }} />;
|
||||
actionDescription = <FormattedMessage id='interaction_modal.description.follow' defaultMessage='With an account on Mastodon, you can follow {name} to receive their posts in your home feed.' values={{ name }} />;
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='modal-root__modal interaction-modal'>
|
||||
<div className='interaction-modal__lead'>
|
||||
<h3><span className='interaction-modal__icon'>{icon}</span> {title}</h3>
|
||||
<p>{actionDescription} <FormattedMessage id='interaction_modal.preamble' defaultMessage="Since Mastodon is decentralized, you can use your existing account hosted by another Mastodon server or compatible platform if you don't have an account on this one." /></p>
|
||||
</div>
|
||||
|
||||
<div className='interaction-modal__choices'>
|
||||
<div className='interaction-modal__choices__choice'>
|
||||
<h3><FormattedMessage id='interaction_modal.on_this_server' defaultMessage='On this server' /></h3>
|
||||
<a href='/auth/sign_in' className='button button--block'><FormattedMessage id='sign_in_banner.sign_in' defaultMessage='Sign in' /></a>
|
||||
<a href={registrationsOpen ? '/auth/sign_up' : 'https://joinmastodon.org/servers'} className='button button--block button-tertiary'><FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' /></a>
|
||||
</div>
|
||||
|
||||
<div className='interaction-modal__choices__choice'>
|
||||
<h3><FormattedMessage id='interaction_modal.on_another_server' defaultMessage='On a different server' /></h3>
|
||||
<p><FormattedMessage id='interaction_modal.other_server_instructions' defaultMessage='Simply copy and paste this URL into the search bar of your favourite app or the web interface where you are signed in.' /></p>
|
||||
<Copypaste value={url} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -28,6 +28,9 @@ import LoadGap from 'flavours/glitch/components/load_gap';
|
|||
import Icon from 'flavours/glitch/components/icon';
|
||||
import compareId from 'flavours/glitch/util/compare_id';
|
||||
import NotificationsPermissionBanner from './components/notifications_permission_banner';
|
||||
import NotSignedInIndicator from 'flavours/glitch/components/not_signed_in_indicator';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { title } from 'flavours/glitch/util/initial_state';
|
||||
|
||||
import NotificationPurgeButtonsContainer from 'flavours/glitch/containers/notification_purge_buttons_container';
|
||||
|
||||
|
@ -94,6 +97,10 @@ export default @connect(mapStateToProps, mapDispatchToProps)
|
|||
@injectIntl
|
||||
class Notifications extends React.PureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
identity: PropTypes.object,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
columnId: PropTypes.string,
|
||||
notifications: ImmutablePropTypes.list.isRequired,
|
||||
|
@ -224,10 +231,11 @@ class Notifications extends React.PureComponent {
|
|||
const { animatingNCD } = this.state;
|
||||
const pinned = !!columnId;
|
||||
const emptyMessage = <FormattedMessage id='empty_column.notifications' defaultMessage="You don't have any notifications yet. When other people interact with you, you will see it here." />;
|
||||
const { signedIn } = this.context.identity;
|
||||
|
||||
let scrollableContent = null;
|
||||
|
||||
const filterBarContainer = showFilterBar
|
||||
const filterBarContainer = (signedIn && showFilterBar)
|
||||
? (<FilterBarContainer />)
|
||||
: null;
|
||||
|
||||
|
@ -257,7 +265,10 @@ class Notifications extends React.PureComponent {
|
|||
|
||||
this.scrollableContent = scrollableContent;
|
||||
|
||||
const scrollContainer = (
|
||||
let scrollContainer;
|
||||
|
||||
if (signedIn) {
|
||||
scrollContainer = (
|
||||
<ScrollableList
|
||||
scrollKey={`notifications-${columnId}`}
|
||||
trackScroll={!pinned}
|
||||
|
@ -277,6 +288,9 @@ class Notifications extends React.PureComponent {
|
|||
{scrollableContent}
|
||||
</ScrollableList>
|
||||
);
|
||||
} else {
|
||||
scrollContainer = <NotSignedInIndicator />;
|
||||
}
|
||||
|
||||
const extraButtons = [];
|
||||
|
||||
|
@ -354,8 +368,13 @@ class Notifications extends React.PureComponent {
|
|||
>
|
||||
<ColumnSettingsContainer />
|
||||
</ColumnHeader>
|
||||
|
||||
{filterBarContainer}
|
||||
{scrollContainer}
|
||||
|
||||
<Helmet>
|
||||
<title>{intl.formatMessage(messages.title)} - {title}</title>
|
||||
</Helmet>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -44,6 +44,7 @@ class Footer extends ImmutablePureComponent {
|
|||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object,
|
||||
identity: PropTypes.object,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
|
@ -69,8 +70,10 @@ class Footer extends ImmutablePureComponent {
|
|||
};
|
||||
|
||||
handleReplyClick = () => {
|
||||
const { dispatch, askReplyConfirmation, intl } = this.props;
|
||||
const { dispatch, askReplyConfirmation, status, intl } = this.props;
|
||||
const { signedIn } = this.context.identity;
|
||||
|
||||
if (signedIn) {
|
||||
if (askReplyConfirmation) {
|
||||
dispatch(openModal('CONFIRM', {
|
||||
message: intl.formatMessage(messages.replyMessage),
|
||||
|
@ -80,16 +83,32 @@ class Footer extends ImmutablePureComponent {
|
|||
} else {
|
||||
this._performReply();
|
||||
}
|
||||
} else {
|
||||
dispatch(openModal('INTERACTION', {
|
||||
type: 'reply',
|
||||
accountId: status.getIn(['account', 'id']),
|
||||
url: status.get('url'),
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
handleFavouriteClick = () => {
|
||||
const { dispatch, status } = this.props;
|
||||
const { signedIn } = this.context.identity;
|
||||
|
||||
if (signedIn) {
|
||||
if (status.get('favourited')) {
|
||||
dispatch(unfavourite(status));
|
||||
} else {
|
||||
dispatch(favourite(status));
|
||||
}
|
||||
} else {
|
||||
dispatch(openModal('INTERACTION', {
|
||||
type: 'favourite',
|
||||
accountId: status.getIn(['account', 'id']),
|
||||
url: status.get('url'),
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
_performReblog = (privacy) => {
|
||||
|
@ -99,7 +118,9 @@ class Footer extends ImmutablePureComponent {
|
|||
|
||||
handleReblogClick = e => {
|
||||
const { dispatch, status } = this.props;
|
||||
const { signedIn } = this.context.identity;
|
||||
|
||||
if (signedIn) {
|
||||
if (status.get('reblogged')) {
|
||||
dispatch(unreblog(status));
|
||||
} else if ((e && e.shiftKey) || !boostModal) {
|
||||
|
@ -107,6 +128,13 @@ class Footer extends ImmutablePureComponent {
|
|||
} else {
|
||||
dispatch(initBoostModal({ status, onReblog: this._performReblog }));
|
||||
}
|
||||
} else {
|
||||
dispatch(openModal('INTERACTION', {
|
||||
type: 'reblog',
|
||||
accountId: status.getIn(['account', 'id']),
|
||||
url: status.get('url'),
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
handleOpenClick = e => {
|
||||
|
|
|
@ -9,6 +9,8 @@ import { expandPublicTimeline } from 'flavours/glitch/actions/timelines';
|
|||
import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns';
|
||||
import ColumnSettingsContainer from './containers/column_settings_container';
|
||||
import { connectPublicStream } from 'flavours/glitch/actions/streaming';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { title } from 'flavours/glitch/util/initial_state';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'column.public', defaultMessage: 'Federated timeline' },
|
||||
|
@ -43,6 +45,7 @@ class PublicTimeline extends React.PureComponent {
|
|||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object,
|
||||
identity: PropTypes.object,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
|
@ -78,20 +81,31 @@ class PublicTimeline extends React.PureComponent {
|
|||
|
||||
componentDidMount () {
|
||||
const { dispatch, onlyMedia, onlyRemote, allowLocalOnly } = this.props;
|
||||
const { signedIn } = this.context.identity;
|
||||
|
||||
dispatch(expandPublicTimeline({ onlyMedia, onlyRemote, allowLocalOnly }));
|
||||
if (signedIn) {
|
||||
this.disconnect = dispatch(connectPublicStream({ onlyMedia, onlyRemote, allowLocalOnly }));
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
const { signedIn } = this.context.identity;
|
||||
|
||||
if (prevProps.onlyMedia !== this.props.onlyMedia || prevProps.onlyRemote !== this.props.onlyRemote || prevProps.allowLocalOnly !== this.props.allowLocalOnly) {
|
||||
const { dispatch, onlyMedia, onlyRemote, allowLocalOnly } = this.props;
|
||||
|
||||
if (this.disconnect) {
|
||||
this.disconnect();
|
||||
}
|
||||
|
||||
dispatch(expandPublicTimeline({ onlyMedia, onlyRemote, allowLocalOnly }));
|
||||
|
||||
if (signedIn) {
|
||||
this.disconnect = dispatch(connectPublicStream({ onlyMedia, onlyRemote, allowLocalOnly }));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
if (this.disconnect) {
|
||||
|
@ -138,6 +152,10 @@ class PublicTimeline extends React.PureComponent {
|
|||
bindToDocument={!multiColumn}
|
||||
regex={this.props.regex}
|
||||
/>
|
||||
|
||||
<Helmet>
|
||||
<title>{intl.formatMessage(messages.title)} - {title}</title>
|
||||
</Helmet>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ const messages = defineMessages({
|
|||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
rules: state.get('rules'),
|
||||
rules: state.getIn(['server', 'rules']),
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
|
|
|
@ -7,7 +7,7 @@ import Button from 'flavours/glitch/components/button';
|
|||
import Option from './components/option';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
rules: state.get('rules'),
|
||||
rules: state.getIn(['server', 'rules']),
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
|
|
|
@ -152,6 +152,7 @@ class ActionBar extends React.PureComponent {
|
|||
|
||||
render () {
|
||||
const { status, intl } = this.props;
|
||||
const { signedIn, permissions } = this.context.identity;
|
||||
|
||||
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
|
||||
const pinnableStatus = ['public', 'unlisted', 'private'].includes(status.get('visibility'));
|
||||
|
@ -184,7 +185,7 @@ class ActionBar extends React.PureComponent {
|
|||
menu.push({ text: intl.formatMessage(messages.mute, { name: status.getIn(['account', 'username']) }), action: this.handleMuteClick });
|
||||
menu.push({ text: intl.formatMessage(messages.block, { name: status.getIn(['account', 'username']) }), action: this.handleBlockClick });
|
||||
menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport });
|
||||
if ((this.context.identity.permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS && (accountAdminLink || statusAdminLink)) {
|
||||
if ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS && (accountAdminLink || statusAdminLink)) {
|
||||
menu.push(null);
|
||||
if (accountAdminLink !== undefined) {
|
||||
menu.push({
|
||||
|
@ -224,7 +225,7 @@ class ActionBar extends React.PureComponent {
|
|||
<div className='detailed-status__button'><IconButton className={classNames({ reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon='retweet' onClick={this.handleReblogClick} /></div>
|
||||
<div className='detailed-status__button'><IconButton className='star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} /></div>
|
||||
{shareButton}
|
||||
<div className='detailed-status__button'><IconButton className='bookmark-icon' active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} /></div>
|
||||
<div className='detailed-status__button'><IconButton className='bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} /></div>
|
||||
|
||||
<div className='detailed-status__action-bar-dropdown'>
|
||||
<DropdownMenuContainer size={18} icon='ellipsis-h' items={menu} direction='left' title={intl.formatMessage(messages.more)} />
|
||||
|
|
|
@ -47,11 +47,12 @@ import { openModal } from 'flavours/glitch/actions/modal';
|
|||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { HotKeys } from 'react-hotkeys';
|
||||
import { boostModal, favouriteModal, deleteModal } from 'flavours/glitch/util/initial_state';
|
||||
import { boostModal, favouriteModal, deleteModal, title } from 'flavours/glitch/util/initial_state';
|
||||
import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from 'flavours/glitch/util/fullscreen';
|
||||
import { autoUnfoldCW } from 'flavours/glitch/util/content_warning';
|
||||
import { textForScreenReader, defaultMediaVisibility } from 'flavours/glitch/components/status';
|
||||
import Icon from 'flavours/glitch/components/icon';
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
const messages = defineMessages({
|
||||
deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
|
||||
|
@ -147,12 +148,30 @@ const makeMapStateToProps = () => {
|
|||
return mapStateToProps;
|
||||
};
|
||||
|
||||
const truncate = (str, num) => {
|
||||
if (str.length > num) {
|
||||
return str.slice(0, num) + '…';
|
||||
} else {
|
||||
return str;
|
||||
}
|
||||
};
|
||||
|
||||
const titleFromStatus = status => {
|
||||
const displayName = status.getIn(['account', 'display_name']);
|
||||
const username = status.getIn(['account', 'username']);
|
||||
const prefix = displayName.trim().length === 0 ? username : displayName;
|
||||
const text = status.get('search_index');
|
||||
|
||||
return `${prefix}: "${truncate(text, 30)}"`;
|
||||
};
|
||||
|
||||
export default @injectIntl
|
||||
@connect(makeMapStateToProps)
|
||||
class Status extends ImmutablePureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object,
|
||||
identity: PropTypes.object,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
|
@ -245,15 +264,26 @@ class Status extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
handleFavouriteClick = (status, e) => {
|
||||
const { dispatch } = this.props;
|
||||
const { signedIn } = this.context.identity;
|
||||
|
||||
if (signedIn) {
|
||||
if (status.get('favourited')) {
|
||||
this.props.dispatch(unfavourite(status));
|
||||
dispatch(unfavourite(status));
|
||||
} else {
|
||||
if ((e && e.shiftKey) || !favouriteModal) {
|
||||
this.handleModalFavourite(status);
|
||||
} else {
|
||||
this.props.dispatch(openModal('FAVOURITE', { status, onFavourite: this.handleModalFavourite }));
|
||||
dispatch(openModal('FAVOURITE', { status, onFavourite: this.handleModalFavourite }));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
dispatch(openModal('INTERACTION', {
|
||||
type: 'favourite',
|
||||
accountId: status.getIn(['account', 'id']),
|
||||
url: status.get('url'),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
handlePin = (status) => {
|
||||
|
@ -265,7 +295,10 @@ class Status extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
handleReplyClick = (status) => {
|
||||
let { askReplyConfirmation, dispatch, intl } = this.props;
|
||||
const { askReplyConfirmation, dispatch, intl } = this.props;
|
||||
const { signedIn } = this.context.identity;
|
||||
|
||||
if (signedIn) {
|
||||
if (askReplyConfirmation) {
|
||||
dispatch(openModal('CONFIRM', {
|
||||
message: intl.formatMessage(messages.replyMessage),
|
||||
|
@ -276,6 +309,13 @@ class Status extends ImmutablePureComponent {
|
|||
} else {
|
||||
dispatch(replyCompose(status, this.context.router.history));
|
||||
}
|
||||
} else {
|
||||
dispatch(openModal('INTERACTION', {
|
||||
type: 'reply',
|
||||
accountId: status.getIn(['account', 'id']),
|
||||
url: status.get('url'),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
handleModalReblog = (status, privacy) => {
|
||||
|
@ -290,7 +330,9 @@ class Status extends ImmutablePureComponent {
|
|||
|
||||
handleReblogClick = (status, e) => {
|
||||
const { settings, dispatch } = this.props;
|
||||
const { signedIn } = this.context.identity;
|
||||
|
||||
if (signedIn) {
|
||||
if (settings.get('confirm_boost_missing_media_description') && status.get('media_attachments').some(item => !item.get('description')) && !status.get('reblogged')) {
|
||||
dispatch(initBoostModal({ status, onReblog: this.handleModalReblog, missingMediaDescription: true }));
|
||||
} else if ((e && e.shiftKey) || !boostModal) {
|
||||
|
@ -298,6 +340,13 @@ class Status extends ImmutablePureComponent {
|
|||
} else {
|
||||
dispatch(initBoostModal({ status, onReblog: this.handleModalReblog }));
|
||||
}
|
||||
} else {
|
||||
dispatch(openModal('INTERACTION', {
|
||||
type: 'reblog',
|
||||
accountId: status.getIn(['account', 'id']),
|
||||
url: status.get('url'),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
handleBookmarkClick = (status) => {
|
||||
|
@ -633,6 +682,10 @@ class Status extends ImmutablePureComponent {
|
|||
{descendants}
|
||||
</div>
|
||||
</ScrollContainer>
|
||||
|
||||
<Helmet>
|
||||
<title>{titleFromStatus(status)} - {title}</title>
|
||||
</Helmet>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -60,6 +60,7 @@ class ColumnsArea extends ImmutablePureComponent {
|
|||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object.isRequired,
|
||||
identity: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
|
@ -213,11 +214,12 @@ class ColumnsArea extends ImmutablePureComponent {
|
|||
render () {
|
||||
const { columns, children, singleColumn, intl, navbarUnder, openSettings } = this.props;
|
||||
const { shouldAnimate, renderComposePanel } = this.state;
|
||||
const { signedIn } = this.context.identity;
|
||||
|
||||
const columnIndex = getIndex(this.context.router.history.location.pathname);
|
||||
|
||||
if (singleColumn) {
|
||||
const floatingActionButton = shouldHideFAB(this.context.router.history.location.pathname) ? null : <Link key='floating-action-button' to='/publish' className='floating-action-button' aria-label={intl.formatMessage(messages.publish)}><Icon id='pencil' /></Link>;
|
||||
const floatingActionButton = (!signedIn || shouldHideFAB(this.context.router.history.location.pathname)) ? null : <Link key='floating-action-button' to='/publish' className='floating-action-button' aria-label={intl.formatMessage(messages.publish)}><Icon id='pencil' /></Link>;
|
||||
|
||||
const content = columnIndex !== -1 ? (
|
||||
<ReactSwipeableViews key='content' hysteresis={0.2} threshold={15} index={columnIndex} onChangeIndex={this.handleSwipe} onTransitionEnd={this.handleAnimationEnd} animateTransitions={shouldAnimate} springConfig={{ duration: '400ms', delay: '0s', easeFunction: 'ease' }} style={{ height: '100%' }} disabled={disableSwiping}>
|
||||
|
|
|
@ -1,16 +1,42 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import SearchContainer from 'flavours/glitch/features/compose/containers/search_container';
|
||||
import ComposeFormContainer from 'flavours/glitch/features/compose/containers/compose_form_container';
|
||||
import NavigationContainer from 'flavours/glitch/features/compose/containers/navigation_container';
|
||||
import LinkFooter from './link_footer';
|
||||
import ServerBanner from 'flavours/glitch/components/server_banner';
|
||||
|
||||
const ComposePanel = () => (
|
||||
export default
|
||||
class ComposePanel extends React.PureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
identity: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
render() {
|
||||
const { signedIn } = this.context.identity;
|
||||
|
||||
return (
|
||||
<div className='compose-panel'>
|
||||
<SearchContainer openInRoute />
|
||||
|
||||
{!signedIn && (
|
||||
<React.Fragment>
|
||||
<ServerBanner />
|
||||
<div className='flex-spacer' />
|
||||
</React.Fragment>
|
||||
)}
|
||||
|
||||
{signedIn && (
|
||||
<React.Fragment>
|
||||
<NavigationContainer />
|
||||
<ComposeFormContainer singleColumn />
|
||||
</React.Fragment>
|
||||
)}
|
||||
|
||||
<LinkFooter withHotkeys />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ComposePanel;
|
||||
};
|
||||
|
|
|
@ -34,6 +34,7 @@ class LinkFooter extends React.PureComponent {
|
|||
};
|
||||
|
||||
static propTypes = {
|
||||
withHotkeys: PropTypes.bool,
|
||||
onLogout: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
@ -48,18 +49,53 @@ class LinkFooter extends React.PureComponent {
|
|||
}
|
||||
|
||||
render () {
|
||||
const { withHotkeys } = this.props;
|
||||
const { signedIn, permissions } = this.context.identity;
|
||||
|
||||
const items = [];
|
||||
|
||||
if ((this.context.identity.permissions & PERMISSION_INVITE_USERS) === PERMISSION_INVITE_USERS) {
|
||||
items.push(<a key='invites' href='/invites' target='_blank'><FormattedMessage id='getting_started.invite' defaultMessage='Invite people' /></a>);
|
||||
}
|
||||
|
||||
if (signedIn && withHotkeys) {
|
||||
items.push(<Link key='hotkeys' to='/keyboard-shortcuts'><FormattedMessage id='navigation_bar.keyboard_shortcuts' defaultMessage='Hotkeys' /></Link>);
|
||||
}
|
||||
|
||||
if (signedIn && securityLink) {
|
||||
items.push(<a key='security' href='/auth/edit'><FormattedMessage id='getting_started.security' defaultMessage='Security' /></a>);
|
||||
}
|
||||
|
||||
if (!limitedFederationMode) {
|
||||
items.push(<a key='about' href='/about/more' target='_blank'><FormattedMessage id='navigation_bar.info' defaultMessage='About this server' /></a>);
|
||||
}
|
||||
|
||||
if (profileDirectory) {
|
||||
items.push(<Link key='directory' to='/directory'><FormattedMessage id='getting_started.directory' defaultMessage='Profile directory' /></Link>);
|
||||
}
|
||||
|
||||
items.push(<a key='apps' href='https://joinmastodon.org/apps' target='_blank'><FormattedMessage id='navigation_bar.apps' defaultMessage='Mobile apps' /></a>);
|
||||
|
||||
if (privacyPolicyLink) {
|
||||
items.push(<a key='terms' href={privacyPolicyLink} target='_blank'><FormattedMessage id='getting_started.privacy_policy' defaultMessage='Privacy Policy' /></a>);
|
||||
}
|
||||
|
||||
if (signedIn) {
|
||||
items.push(<a key='developers' href='/settings/applications' target='_blank'><FormattedMessage id='getting_started.developers' defaultMessage='Developers' /></a>);
|
||||
}
|
||||
|
||||
items.push(<a key='docs' href='https://docs.joinmastodon.org' target='_blank'><FormattedMessage id='getting_started.documentation' defaultMessage='Documentation' /></a>);
|
||||
|
||||
if (signedIn) {
|
||||
items.push(<a key='logout' href='/auth/sign_out' onClick={this.handleLogoutClick}><FormattedMessage id='navigation_bar.logout' defaultMessage='Logout' /></a>);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='getting-started__footer'>
|
||||
<ul>
|
||||
{((this.context.identity.permissions & PERMISSION_INVITE_USERS) === PERMISSION_INVITE_USERS) && <li><a href='/invites' target='_blank'><FormattedMessage id='getting_started.invite' defaultMessage='Invite people' /></a> · </li>}
|
||||
{!!securityLink && <li><a href='/auth/edit'><FormattedMessage id='getting_started.security' defaultMessage='Security' /></a> · </li>}
|
||||
{!limitedFederationMode && <li><a href='/about/more' target='_blank'><FormattedMessage id='navigation_bar.info' defaultMessage='About this server' /></a> · </li>}
|
||||
{profileDirectory && <li><Link to='/directory'><FormattedMessage id='getting_started.directory' defaultMessage='Profile directory' /></Link> · </li>}
|
||||
<li><a href='https://joinmastodon.org/apps' target='_blank'><FormattedMessage id='navigation_bar.apps' defaultMessage='Mobile apps' /></a> · </li>
|
||||
<li><a href={privacyPolicyLink} target='_blank'><FormattedMessage id='getting_started.privacy_policy' defaultMessage='Privacy Policy' /></a> · </li>
|
||||
<li><a href='/settings/applications' target='_blank'><FormattedMessage id='getting_started.developers' defaultMessage='Developers' /></a> · </li>
|
||||
<li><a href='https://docs.joinmastodon.org' target='_blank'><FormattedMessage id='getting_started.documentation' defaultMessage='Documentation' /></a> · </li>
|
||||
<li><a href={signOutLink} onClick={this.handleLogoutClick}><FormattedMessage id='navigation_bar.logout' defaultMessage='Logout' /></a></li>
|
||||
{items.map((item, index, array) => (
|
||||
<li>{item} { index === array.length - 1 ? null : ' · ' }</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
|
|
|
@ -16,6 +16,7 @@ import ConfirmationModal from './confirmation_modal';
|
|||
import SubscribedLanguagesModal from 'flavours/glitch/features/subscribed_languages_modal';
|
||||
import FocalPointModal from './focal_point_modal';
|
||||
import DeprecatedSettingsModal from './deprecated_settings_modal';
|
||||
import InteractionModal from 'flavours/glitch/features/interaction_modal';
|
||||
import {
|
||||
OnboardingModal,
|
||||
MuteModal,
|
||||
|
@ -53,6 +54,7 @@ const MODAL_COMPONENTS = {
|
|||
'COMPARE_HISTORY': CompareHistoryModal,
|
||||
'FILTER': FilterModal,
|
||||
'SUBSCRIBED_LANGUAGES': () => Promise.resolve({ default: SubscribedLanguagesModal }),
|
||||
'INTERACTION': () => Promise.resolve({ default: InteractionModal }),
|
||||
};
|
||||
|
||||
export default class ModalRoot extends React.PureComponent {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import React from 'react';
|
||||
import { NavLink, withRouter } from 'react-router-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import { NavLink, Link } from 'react-router-dom';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import Icon from 'flavours/glitch/components/icon';
|
||||
import { showTrends } from 'flavours/glitch/util/initial_state';
|
||||
|
@ -8,15 +9,47 @@ import NotificationsCounterIcon from './notifications_counter_icon';
|
|||
import FollowRequestsNavLink from './follow_requests_nav_link';
|
||||
import ListPanel from './list_panel';
|
||||
import TrendsContainer from 'flavours/glitch/features/getting_started/containers/trends_container';
|
||||
import SignInBanner from './sign_in_banner';
|
||||
|
||||
const NavigationPanel = ({ onOpenSettings }) => (
|
||||
export default class NavigationPanel extends React.Component {
|
||||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object.isRequired,
|
||||
identity: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
onOpenSettings: PropTypes.func,
|
||||
};
|
||||
|
||||
render() {
|
||||
const { signedIn } = this.context.identity;
|
||||
const { onOpenSettings } = this.props;
|
||||
|
||||
return (
|
||||
<div className='navigation-panel'>
|
||||
{signedIn && (
|
||||
<React.Fragment>
|
||||
<NavLink className='column-link column-link--transparent' to='/home' data-preview-title-id='column.home' data-preview-icon='home' ><Icon className='column-link__icon' id='home' fixedWidth /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></NavLink>
|
||||
<NavLink className='column-link column-link--transparent' to='/notifications' data-preview-title-id='column.notifications' data-preview-icon='bell' ><NotificationsCounterIcon className='column-link__icon' /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></NavLink>
|
||||
<FollowRequestsNavLink />
|
||||
</React.Fragment>
|
||||
)}
|
||||
|
||||
{ showTrends && <NavLink className='column-link column-link--transparent' to='/explore' data-preview-title-id='explore.title' data-preview-icon='hashtag'><Icon className='column-link__icon' id='hashtag' fixedWidth /><FormattedMessage id='explore.title' defaultMessage='Explore' /></NavLink> }
|
||||
|
||||
<NavLink className='column-link column-link--transparent' to='/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><Icon className='column-link__icon' id='users' fixedWidth /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></NavLink>
|
||||
<NavLink className='column-link column-link--transparent' exact to='/public' data-preview-title-id='column.public' data-preview-icon='globe' ><Icon className='column-link__icon' id='globe' fixedWidth /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink>
|
||||
|
||||
{!signedIn && (
|
||||
<React.Fragment>
|
||||
<hr />
|
||||
<SignInBanner />
|
||||
</React.Fragment>
|
||||
)}
|
||||
|
||||
{signedIn && (
|
||||
<React.Fragment>
|
||||
<NavLink className='column-link column-link--transparent' to='/conversations'><Icon className='column-link__icon' id='envelope' fixedWidth /><FormattedMessage id='navigation_bar.direct' defaultMessage='Direct messages' /></NavLink>
|
||||
<NavLink className='column-link column-link--transparent' to='/bookmarks'><Icon className='column-link__icon' id='bookmark' fixedWidth /><FormattedMessage id='navigation_bar.bookmarks' defaultMessage='Bookmarks' /></NavLink>
|
||||
<NavLink className='column-link column-link--transparent' to='/lists'><Icon className='column-link__icon' id='list-ul' fixedWidth /><FormattedMessage id='navigation_bar.lists' defaultMessage='Lists' /></NavLink>
|
||||
|
@ -28,10 +61,18 @@ const NavigationPanel = ({ onOpenSettings }) => (
|
|||
{!!preferencesLink && <a className='column-link column-link--transparent' href={preferencesLink} target='_blank'><Icon className='column-link__icon' id='cog' fixedWidth /><FormattedMessage id='navigation_bar.preferences' defaultMessage='Preferences' /></a>}
|
||||
<a className='column-link column-link--transparent' href='#' onClick={onOpenSettings}><Icon className='column-link__icon' id='cogs' fixedWidth /><FormattedMessage id='navigation_bar.app_settings' defaultMessage='App settings' /></a>
|
||||
{!!relationshipsLink && <a className='column-link column-link--transparent' href={relationshipsLink} target='_blank'><Icon className='column-link__icon' id='users' fixedWidth /><FormattedMessage id='navigation_bar.follows_and_followers' defaultMessage='Follows and followers' /></a>}
|
||||
</React.Fragment>
|
||||
)}
|
||||
|
||||
{showTrends && (
|
||||
<React.Fragment>
|
||||
<div className='flex-spacer' />
|
||||
<TrendsContainer />
|
||||
</React.Fragment>
|
||||
)}
|
||||
|
||||
{showTrends && <div className='flex-spacer' />}
|
||||
{showTrends && <TrendsContainer />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default withRouter(NavigationPanel);
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
|||
import { connect } from 'react-redux';
|
||||
import { submitReport } from 'flavours/glitch/actions/reports';
|
||||
import { expandAccountTimeline } from 'flavours/glitch/actions/timelines';
|
||||
import { fetchRules } from 'flavours/glitch/actions/rules';
|
||||
import { fetchServer } from 'flavours/glitch/actions/server';
|
||||
import { fetchRelationships } from 'flavours/glitch/actions/accounts';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
|
@ -119,7 +119,7 @@ class ReportModal extends ImmutablePureComponent {
|
|||
|
||||
dispatch(fetchRelationships([accountId]));
|
||||
dispatch(expandAccountTimeline(accountId, { withReplies: true }));
|
||||
dispatch(fetchRules());
|
||||
dispatch(fetchServer());
|
||||
}
|
||||
|
||||
render () {
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { registrationsOpen } from 'flavours/glitch/util/initial_state';
|
||||
|
||||
const SignInBanner = () => (
|
||||
<div className='sign-in-banner'>
|
||||
<p><FormattedMessage id='sign_in_banner.text' defaultMessage='Sign in to follow profiles or hashtags, favourite, share and reply to posts, or interact from your account on a different server.' /></p>
|
||||
<a href='/auth/sign_in' className='button button--block'><FormattedMessage id='sign_in_banner.sign_in' defaultMessage='Sign in' /></a>
|
||||
<a href={registrationsOpen ? '/auth/sign_up' : 'https://joinmastodon.org/servers'} className='button button--block button-tertiary'><FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' /></a>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default SignInBanner;
|
|
@ -10,7 +10,7 @@ import { debounce } from 'lodash';
|
|||
import { uploadCompose, resetCompose, changeComposeSpoilerness } from 'flavours/glitch/actions/compose';
|
||||
import { expandHomeTimeline } from 'flavours/glitch/actions/timelines';
|
||||
import { expandNotifications, notificationsSetVisibility } from 'flavours/glitch/actions/notifications';
|
||||
import { fetchRules } from 'flavours/glitch/actions/rules';
|
||||
import { fetchServer } from 'flavours/glitch/actions/server';
|
||||
import { clearHeight } from 'flavours/glitch/actions/height_cache';
|
||||
import { changeLayout } from 'flavours/glitch/actions/app';
|
||||
import { synchronouslySubmitMarkers, submitMarkers, fetchMarkers } from 'flavours/glitch/actions/markers';
|
||||
|
@ -54,9 +54,10 @@ import {
|
|||
FollowRecommendations,
|
||||
} from 'flavours/glitch/util/async-components';
|
||||
import { HotKeys } from 'react-hotkeys';
|
||||
import { me } from 'flavours/glitch/util/initial_state';
|
||||
import { me, title } from 'flavours/glitch/util/initial_state';
|
||||
import { closeOnboarding, INTRODUCTION_VERSION } from 'flavours/glitch/actions/onboarding';
|
||||
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
// Dummy import, to make sure that <Status /> ends up in the application bundle.
|
||||
// Without this it ends up in ~8 very commonly used bundles.
|
||||
|
@ -121,6 +122,10 @@ const keyMap = {
|
|||
|
||||
class SwitchingColumnsArea extends React.PureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
identity: PropTypes.object,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
children: PropTypes.node,
|
||||
location: PropTypes.object,
|
||||
|
@ -157,12 +162,25 @@ class SwitchingColumnsArea extends React.PureComponent {
|
|||
|
||||
render () {
|
||||
const { children, mobile, navbarUnder } = this.props;
|
||||
const redirect = mobile ? <Redirect from='/' to='/home' exact /> : <Redirect from='/' to='/getting-started' exact />;
|
||||
const { signedIn } = this.context.identity;
|
||||
|
||||
let redirect;
|
||||
|
||||
if (signedIn) {
|
||||
if (mobile) {
|
||||
redirect = <Redirect from='/' to='/home' exact />;
|
||||
} else {
|
||||
redirect = <Redirect from='/' to='/getting-started' exact />;
|
||||
}
|
||||
} else {
|
||||
redirect = <Redirect from='/' to='/explore' exact />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ColumnsAreaContainer ref={this.setRef} singleColumn={mobile} navbarUnder={navbarUnder}>
|
||||
<WrappedSwitch>
|
||||
{redirect}
|
||||
|
||||
<WrappedRoute path='/getting-started' component={GettingStarted} content={children} />
|
||||
<WrappedRoute path='/keyboard-shortcuts' component={KeyboardShortcuts} content={children} />
|
||||
|
||||
|
@ -219,6 +237,10 @@ export default @connect(mapStateToProps)
|
|||
@withRouter
|
||||
class UI extends React.Component {
|
||||
|
||||
static contextTypes = {
|
||||
identity: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
children: PropTypes.node,
|
||||
|
@ -358,6 +380,8 @@ class UI extends React.Component {
|
|||
}
|
||||
|
||||
componentDidMount () {
|
||||
const { signedIn } = this.context.identity;
|
||||
|
||||
window.addEventListener('beforeunload', this.handleBeforeUnload, false);
|
||||
window.addEventListener('resize', this.handleResize, { passive: true });
|
||||
|
||||
|
@ -374,16 +398,18 @@ class UI extends React.Component {
|
|||
this.favicon = new Favico({ animation:"none" });
|
||||
|
||||
// On first launch, redirect to the follow recommendations page
|
||||
if (this.props.firstLaunch) {
|
||||
if (signedIn && this.props.firstLaunch) {
|
||||
this.context.router.history.replace('/start');
|
||||
this.props.dispatch(closeOnboarding());
|
||||
}
|
||||
|
||||
if (signedIn) {
|
||||
this.props.dispatch(fetchMarkers());
|
||||
this.props.dispatch(expandHomeTimeline());
|
||||
this.props.dispatch(expandNotifications());
|
||||
|
||||
setTimeout(() => this.props.dispatch(fetchRules()), 3000);
|
||||
setTimeout(() => this.props.dispatch(fetchServer()), 3000);
|
||||
}
|
||||
|
||||
this.hotkeys.__mousetrap__.stopCallback = (e, element) => {
|
||||
return ['TEXTAREA', 'SELECT', 'INPUT'].includes(element.tagName);
|
||||
|
@ -635,6 +661,10 @@ class UI extends React.Component {
|
|||
<LoadingBarContainer className='loading-bar' />
|
||||
<ModalContainer />
|
||||
<UploadArea active={draggingOver} onClose={this.closeUploadModal} />
|
||||
|
||||
<Helmet>
|
||||
<title>{title}</title>
|
||||
</Helmet>
|
||||
</div>
|
||||
</HotKeys>
|
||||
);
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import 'packs/public-path';
|
||||
import loadPolyfills from 'flavours/glitch/util/load_polyfills';
|
||||
|
||||
loadPolyfills().then(() => {
|
||||
require('flavours/glitch/util/main').default();
|
||||
loadPolyfills().then(async () => {
|
||||
const { default: main } = await import('flavours/glitch/util/main');
|
||||
|
||||
return main();
|
||||
}).catch(e => {
|
||||
console.error(e);
|
||||
});
|
||||
|
|
|
@ -17,7 +17,7 @@ import push_notifications from './push_notifications';
|
|||
import status_lists from './status_lists';
|
||||
import mutes from './mutes';
|
||||
import blocks from './blocks';
|
||||
import rules from './rules';
|
||||
import server from './server';
|
||||
import boosts from './boosts';
|
||||
import contexts from './contexts';
|
||||
import compose from './compose';
|
||||
|
@ -64,7 +64,7 @@ const reducers = {
|
|||
push_notifications,
|
||||
mutes,
|
||||
blocks,
|
||||
rules,
|
||||
server,
|
||||
boosts,
|
||||
contexts,
|
||||
compose,
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
import { RULES_FETCH_SUCCESS } from 'flavours/glitch/actions/rules';
|
||||
import { List as ImmutableList, fromJS } from 'immutable';
|
||||
|
||||
const initialState = ImmutableList();
|
||||
|
||||
export default function rules(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case RULES_FETCH_SUCCESS:
|
||||
return fromJS(action.rules);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
import { SERVER_FETCH_REQUEST, SERVER_FETCH_SUCCESS, SERVER_FETCH_FAIL } from 'flavours/glitch/actions/server';
|
||||
import { Map as ImmutableMap, fromJS } from 'immutable';
|
||||
|
||||
const initialState = ImmutableMap({
|
||||
isLoading: true,
|
||||
});
|
||||
|
||||
export default function server(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case SERVER_FETCH_REQUEST:
|
||||
return state.set('isLoading', true);
|
||||
case SERVER_FETCH_SUCCESS:
|
||||
return fromJS(action.server).set('isLoading', false);
|
||||
case SERVER_FETCH_FAIL:
|
||||
return state.set('isLoading', false);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
|
@ -60,6 +60,7 @@
|
|||
font-family: inherit;
|
||||
background: $ui-base-color;
|
||||
color: $darker-text-color;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
}
|
||||
|
|
|
@ -117,6 +117,7 @@
|
|||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
background: lighten($ui-base-color, 4%);
|
||||
border-radius: 4px 4px 0 0;
|
||||
color: $highlight-text-color;
|
||||
cursor: pointer;
|
||||
flex: 0 0 auto;
|
||||
|
@ -204,6 +205,17 @@
|
|||
color: $highlight-text-color;
|
||||
}
|
||||
}
|
||||
|
||||
&--logo {
|
||||
background: transparent;
|
||||
padding: 10px;
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.column-link__icon {
|
||||
|
@ -255,6 +267,7 @@
|
|||
display: flex;
|
||||
font-size: 16px;
|
||||
background: lighten($ui-base-color, 4%);
|
||||
border-radius: 4px 4px 0 0;
|
||||
flex: 0 0 auto;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
|
@ -309,6 +322,7 @@
|
|||
|
||||
> .scrollable {
|
||||
background: $ui-base-color;
|
||||
border-radius: 0 0 4px 4px;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -352,6 +366,11 @@
|
|||
&:focus {
|
||||
text-shadow: 0 0 4px darken($ui-highlight-color, 5%);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
color: $dark-text-color;
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
.column-header__notif-cleaning-buttons {
|
||||
|
|
|
@ -110,6 +110,27 @@
|
|||
&:hover {
|
||||
border-color: lighten($ui-primary-color, 4%);
|
||||
color: lighten($darker-text-color, 4%);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
&.button-tertiary {
|
||||
background: transparent;
|
||||
padding: 6px 17px;
|
||||
color: $highlight-text-color;
|
||||
border: 1px solid $highlight-text-color;
|
||||
|
||||
&:active,
|
||||
&:focus,
|
||||
&:hover {
|
||||
background: $ui-highlight-color;
|
||||
color: $primary-text-color;
|
||||
border: 0;
|
||||
padding: 7px 18px;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
|
@ -1756,3 +1777,4 @@ noscript {
|
|||
@import 'single_column';
|
||||
@import 'announcements';
|
||||
@import 'explore';
|
||||
@import 'signed_out';
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
@ -1304,3 +1305,123 @@ img.modal-warning {
|
|||
margin-bottom: 15px;
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
.interaction-modal {
|
||||
max-width: 90vw;
|
||||
width: 600px;
|
||||
background: $ui-base-color;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
display: block;
|
||||
padding: 20px;
|
||||
|
||||
h3 {
|
||||
font-size: 22px;
|
||||
line-height: 33px;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
color: $highlight-text-color;
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
&__lead {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
|
||||
h3 {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 17px;
|
||||
line-height: 22px;
|
||||
color: $darker-text-color;
|
||||
}
|
||||
}
|
||||
|
||||
&__choices {
|
||||
display: flex;
|
||||
|
||||
&__choice {
|
||||
flex: 0 0 auto;
|
||||
width: 50%;
|
||||
box-sizing: border-box;
|
||||
padding: 20px;
|
||||
|
||||
h3 {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
p {
|
||||
color: $darker-text-color;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.button {
|
||||
margin-bottom: 10px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: $no-gap-breakpoint - 1px) {
|
||||
&__choices {
|
||||
display: block;
|
||||
|
||||
&__choice {
|
||||
width: auto;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.copypaste {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
|
||||
input {
|
||||
display: block;
|
||||
font-family: inherit;
|
||||
background: darken($ui-base-color, 8%);
|
||||
border: 1px solid $highlight-text-color;
|
||||
color: $darker-text-color;
|
||||
border-radius: 4px;
|
||||
padding: 6px 9px;
|
||||
line-height: 22px;
|
||||
font-size: 14px;
|
||||
transition: border-color 300ms linear;
|
||||
flex: 1 1 auto;
|
||||
overflow: hidden;
|
||||
|
||||
&:focus {
|
||||
outline: 0;
|
||||
background: darken($ui-base-color, 4%);
|
||||
}
|
||||
}
|
||||
|
||||
.button {
|
||||
flex: 0 0 auto;
|
||||
transition: background 300ms linear;
|
||||
}
|
||||
|
||||
&.copied {
|
||||
input {
|
||||
border: 1px solid $valid-value-color;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.button {
|
||||
background: $valid-value-color;
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,96 @@
|
|||
.sign-in-banner {
|
||||
padding: 10px;
|
||||
|
||||
p {
|
||||
color: $darker-text-color;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.button {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.server-banner {
|
||||
padding: 20px 0;
|
||||
|
||||
&__introduction {
|
||||
color: $darker-text-color;
|
||||
margin-bottom: 20px;
|
||||
|
||||
strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: underline;
|
||||
|
||||
&:hover,
|
||||
&:active,
|
||||
&:focus {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__hero {
|
||||
display: block;
|
||||
border-radius: 4px;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
margin-bottom: 20px;
|
||||
aspect-ratio: 1.9;
|
||||
border: 0;
|
||||
background: $ui-base-color;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
&__description {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
&__meta {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
max-width: 100%;
|
||||
|
||||
&__column {
|
||||
flex: 0 0 auto;
|
||||
width: calc(50% - 5px);
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
&__number {
|
||||
font-weight: 600;
|
||||
color: $primary-text-color;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
&__number-label {
|
||||
color: $darker-text-color;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
h4 {
|
||||
text-transform: uppercase;
|
||||
color: $darker-text-color;
|
||||
margin-bottom: 10px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.account {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.account__avatar-wrapper {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
margin: 10px 0;
|
||||
}
|
||||
}
|
|
@ -6,6 +6,26 @@
|
|||
height: calc(100% - 10px);
|
||||
overflow-y: hidden;
|
||||
|
||||
.hero-widget {
|
||||
box-shadow: none;
|
||||
|
||||
&__text,
|
||||
&__img,
|
||||
&__img img {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
&__text {
|
||||
padding: 15px;
|
||||
color: $secondary-text-color;
|
||||
|
||||
strong {
|
||||
font-weight: 700;
|
||||
color: $primary-text-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search__input {
|
||||
line-height: 18px;
|
||||
font-size: 16px;
|
||||
|
@ -21,10 +41,6 @@
|
|||
flex: 0 1 48px;
|
||||
}
|
||||
|
||||
.flex-spacer {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.composer {
|
||||
flex: 1;
|
||||
overflow-y: hidden;
|
||||
|
@ -61,6 +77,14 @@
|
|||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 30px;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.navigation-panel,
|
||||
.compose-panel {
|
||||
hr {
|
||||
flex: 0 0 auto;
|
||||
border: 0;
|
||||
|
|
|
@ -11,6 +11,7 @@ const initialState = element && function () {
|
|||
|
||||
const getMeta = (prop) => initialState && initialState.meta && initialState.meta[prop];
|
||||
|
||||
export const domain = getMeta('domain');
|
||||
export const reduceMotion = getMeta('reduce_motion');
|
||||
export const autoPlayGif = getMeta('auto_play_gif');
|
||||
export const displayMedia = getMeta('display_media') || (getMeta('display_sensitive_media') ? 'show_all' : 'default');
|
||||
|
@ -24,17 +25,19 @@ export const searchEnabled = getMeta('search_enabled');
|
|||
export const maxChars = (initialState && initialState.max_toot_chars) || 500;
|
||||
export const pollLimits = (initialState && initialState.poll_limits);
|
||||
export const limitedFederationMode = getMeta('limited_federation_mode');
|
||||
export const registrationsOpen = getMeta('registrations_open');
|
||||
export const repository = getMeta('repository');
|
||||
export const source_url = getMeta('source_url');
|
||||
export const version = getMeta('version');
|
||||
export const mascot = getMeta('mascot');
|
||||
export const profile_directory = getMeta('profile_directory');
|
||||
export const defaultContentType = getMeta('default_content_type');
|
||||
export const forceSingleColumn = getMeta('advanced_layout') === false;
|
||||
export const forceSingleColumn = !getMeta('advanced_layout');
|
||||
export const useBlurhash = getMeta('use_blurhash');
|
||||
export const usePendingItems = getMeta('use_pending_items');
|
||||
export const useSystemEmojiFont = getMeta('system_emoji_font');
|
||||
export const showTrends = getMeta('trends');
|
||||
export const title = getMeta('title');
|
||||
export const disableSwiping = getMeta('disable_swiping');
|
||||
export const languages = initialState && initialState.languages;
|
||||
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import * as registerPushNotifications from 'flavours/glitch/actions/push_notifications';
|
||||
import { setupBrowserNotifications } from 'flavours/glitch/actions/notifications';
|
||||
import Mastodon, { store } from 'flavours/glitch/containers/mastodon';
|
||||
import ready from 'flavours/glitch/util/ready';
|
||||
|
||||
const perf = require('./performance');
|
||||
const perf = require('flavours/glitch/util/performance');
|
||||
|
||||
/**
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
function main() {
|
||||
perf.start('main()');
|
||||
|
||||
|
@ -18,7 +20,7 @@ function main() {
|
|||
}
|
||||
}
|
||||
|
||||
ready(() => {
|
||||
return ready(async () => {
|
||||
const mountNode = document.getElementById('mastodon');
|
||||
const props = JSON.parse(mountNode.getAttribute('data-props'));
|
||||
|
||||
|
@ -26,19 +28,28 @@ function main() {
|
|||
store.dispatch(setupBrowserNotifications());
|
||||
|
||||
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
|
||||
import('workbox-window')
|
||||
.then(({ Workbox }) => {
|
||||
const [{ Workbox }, { me }] = await Promise.all([
|
||||
import('workbox-window'),
|
||||
import('mastodon/initial_state'),
|
||||
]);
|
||||
|
||||
const wb = new Workbox('/sw.js');
|
||||
|
||||
return wb.register();
|
||||
})
|
||||
.then(() => {
|
||||
store.dispatch(registerPushNotifications.register());
|
||||
})
|
||||
.catch(err => {
|
||||
try {
|
||||
await wb.register();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (me) {
|
||||
const registerPushNotifications = await import('flavours/glitch/actions/push_notifications');
|
||||
|
||||
store.dispatch(registerPushNotifications.register());
|
||||
}
|
||||
}
|
||||
|
||||
perf.stop('main()');
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,7 +1,32 @@
|
|||
export default function ready(loaded) {
|
||||
// @ts-check
|
||||
|
||||
/**
|
||||
* @param {(() => void) | (() => Promise<void>)} callback
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export default function ready(callback) {
|
||||
return new Promise((resolve, reject) => {
|
||||
function loaded() {
|
||||
let result;
|
||||
try {
|
||||
result = callback();
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof result?.then === 'function') {
|
||||
result.then(resolve).catch(reject);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
|
||||
if (['interactive', 'complete'].includes(document.readyState)) {
|
||||
loaded();
|
||||
} else {
|
||||
document.addEventListener('DOMContentLoaded', loaded);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,27 +0,0 @@
|
|||
import api from '../api';
|
||||
|
||||
export const RULES_FETCH_REQUEST = 'RULES_FETCH_REQUEST';
|
||||
export const RULES_FETCH_SUCCESS = 'RULES_FETCH_SUCCESS';
|
||||
export const RULES_FETCH_FAIL = 'RULES_FETCH_FAIL';
|
||||
|
||||
export const fetchRules = () => (dispatch, getState) => {
|
||||
dispatch(fetchRulesRequest());
|
||||
|
||||
api(getState)
|
||||
.get('/api/v1/instance').then(({ data }) => dispatch(fetchRulesSuccess(data.rules)))
|
||||
.catch(err => dispatch(fetchRulesFail(err)));
|
||||
};
|
||||
|
||||
const fetchRulesRequest = () => ({
|
||||
type: RULES_FETCH_REQUEST,
|
||||
});
|
||||
|
||||
const fetchRulesSuccess = rules => ({
|
||||
type: RULES_FETCH_SUCCESS,
|
||||
rules,
|
||||
});
|
||||
|
||||
const fetchRulesFail = error => ({
|
||||
type: RULES_FETCH_FAIL,
|
||||
error,
|
||||
});
|
|
@ -0,0 +1,30 @@
|
|||
import api from '../api';
|
||||
import { importFetchedAccount } from './importer';
|
||||
|
||||
export const SERVER_FETCH_REQUEST = 'Server_FETCH_REQUEST';
|
||||
export const SERVER_FETCH_SUCCESS = 'Server_FETCH_SUCCESS';
|
||||
export const SERVER_FETCH_FAIL = 'Server_FETCH_FAIL';
|
||||
|
||||
export const fetchServer = () => (dispatch, getState) => {
|
||||
dispatch(fetchServerRequest());
|
||||
|
||||
api(getState)
|
||||
.get('/api/v2/instance').then(({ data }) => {
|
||||
if (data.contact.account) dispatch(importFetchedAccount(data.contact.account));
|
||||
dispatch(fetchServerSuccess(data));
|
||||
}).catch(err => dispatch(fetchServerFail(err)));
|
||||
};
|
||||
|
||||
const fetchServerRequest = () => ({
|
||||
type: SERVER_FETCH_REQUEST,
|
||||
});
|
||||
|
||||
const fetchServerSuccess = server => ({
|
||||
type: SERVER_FETCH_SUCCESS,
|
||||
server,
|
||||
});
|
||||
|
||||
const fetchServerFail = error => ({
|
||||
type: SERVER_FETCH_FAIL,
|
||||
error,
|
||||
});
|
|
@ -9,6 +9,7 @@ import { defineMessages, injectIntl } from 'react-intl';
|
|||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { me } from '../initial_state';
|
||||
import RelativeTimestamp from './relative_timestamp';
|
||||
import Skeleton from 'mastodon/components/skeleton';
|
||||
|
||||
const messages = defineMessages({
|
||||
follow: { id: 'account.follow', defaultMessage: 'Follow' },
|
||||
|
@ -26,7 +27,7 @@ export default @injectIntl
|
|||
class Account extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
account: ImmutablePropTypes.map.isRequired,
|
||||
account: ImmutablePropTypes.map,
|
||||
onFollow: PropTypes.func.isRequired,
|
||||
onBlock: PropTypes.func.isRequired,
|
||||
onMute: PropTypes.func.isRequired,
|
||||
|
@ -67,7 +68,16 @@ class Account extends ImmutablePureComponent {
|
|||
const { account, intl, hidden, onActionClick, actionIcon, actionTitle, defaultAction } = this.props;
|
||||
|
||||
if (!account) {
|
||||
return <div />;
|
||||
return (
|
||||
<div className='account'>
|
||||
<div className='account__wrapper'>
|
||||
<div className='account__display-name'>
|
||||
<div className='account__avatar-wrapper'><Skeleton width={36} height={36} /></div>
|
||||
<DisplayName />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (hidden) {
|
||||
|
|
|
@ -2,11 +2,12 @@ import React from 'react';
|
|||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import PropTypes from 'prop-types';
|
||||
import { autoPlayGif } from 'mastodon/initial_state';
|
||||
import Skeleton from 'mastodon/components/skeleton';
|
||||
|
||||
export default class DisplayName extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
account: ImmutablePropTypes.map.isRequired,
|
||||
account: ImmutablePropTypes.map,
|
||||
others: ImmutablePropTypes.list,
|
||||
localDomain: PropTypes.string,
|
||||
};
|
||||
|
@ -48,7 +49,7 @@ export default class DisplayName extends React.PureComponent {
|
|||
if (others.size - 2 > 0) {
|
||||
suffix = `+${others.size - 2}`;
|
||||
}
|
||||
} else {
|
||||
} else if ((others && others.size > 0) || this.props.account) {
|
||||
if (others && others.size > 0) {
|
||||
account = others.first();
|
||||
} else {
|
||||
|
@ -63,6 +64,9 @@ export default class DisplayName extends React.PureComponent {
|
|||
|
||||
displayName = <bdi><strong className='display-name__html' dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }} /></bdi>;
|
||||
suffix = <span className='display-name__account'>@{acct}</span>;
|
||||
} else {
|
||||
displayName = <bdi><strong className='display-name__html'><Skeleton width='10ch' /></strong></bdi>;
|
||||
suffix = <span className='display-name__account'><Skeleton width='7ch' /></span>;
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { domain } from 'mastodon/initial_state';
|
||||
import { fetchServer } from 'mastodon/actions/server';
|
||||
import { connect } from 'react-redux';
|
||||
import Account from 'mastodon/containers/account_container';
|
||||
import ShortNumber from 'mastodon/components/short_number';
|
||||
import Skeleton from 'mastodon/components/skeleton';
|
||||
import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
|
||||
|
||||
const messages = defineMessages({
|
||||
aboutActiveUsers: { id: 'server_banner.about_active_users', defaultMessage: 'People using this server during the last 30 days (Monthly Active Users)' },
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
server: state.get('server'),
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
@injectIntl
|
||||
class ServerBanner extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
server: PropTypes.object,
|
||||
dispatch: PropTypes.func,
|
||||
intl: PropTypes.object,
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
const { dispatch } = this.props;
|
||||
dispatch(fetchServer());
|
||||
}
|
||||
|
||||
render () {
|
||||
const { server, intl } = this.props;
|
||||
const isLoading = server.get('isLoading');
|
||||
|
||||
return (
|
||||
<div className='server-banner'>
|
||||
<div className='server-banner__introduction'>
|
||||
<FormattedMessage id='server_banner.introduction' defaultMessage='{domain} is part of the decentralized social network powered by {mastodon}.' values={{ domain: <strong>{domain}</strong>, mastodon: <a href='https://joinmastodon.org' target='_blank'>Mastodon</a> }} />
|
||||
</div>
|
||||
|
||||
<img src={server.get('thumbnail')} alt={server.get('title')} className='server-banner__hero' />
|
||||
|
||||
<div className='server-banner__description'>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Skeleton width='100%' />
|
||||
<br />
|
||||
<Skeleton width='100%' />
|
||||
<br />
|
||||
<Skeleton width='70%' />
|
||||
</>
|
||||
) : server.get('description')}
|
||||
</div>
|
||||
|
||||
<div className='server-banner__meta'>
|
||||
<div className='server-banner__meta__column'>
|
||||
<h4><FormattedMessage id='server_banner.administered_by' defaultMessage='Administered by:' /></h4>
|
||||
|
||||
<Account id={server.getIn(['contact', 'account', 'id'])} />
|
||||
</div>
|
||||
|
||||
<div className='server-banner__meta__column'>
|
||||
<h4><FormattedMessage id='server_banner.server_stats' defaultMessage='Server stats:' /></h4>
|
||||
|
||||
{isLoading ? (
|
||||
<>
|
||||
<strong className='server-banner__number'><Skeleton width='10ch' /></strong>
|
||||
<br />
|
||||
<span className='server-banner__number-label'><Skeleton width='5ch' /></span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<strong className='server-banner__number'><ShortNumber value={server.getIn(['usage', 'users', 'active_month'])} /></strong>
|
||||
<br />
|
||||
<span className='server-banner__number-label' title={intl.formatMessage(messages.aboutActiveUsers)}><FormattedMessage id='server_banner.active_users' defaultMessage='active users' /></span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr className='spacer' />
|
||||
|
||||
<a className='button button--block button-secondary' href='/about/more' target='_blank'><FormattedMessage id='server_banner.learn_more' defaultMessage='Learn more' /></a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -7,7 +7,7 @@ import Button from 'mastodon/components/button';
|
|||
import Option from './components/option';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
rules: state.get('rules'),
|
||||
rules: state.getIn(['server', 'rules']),
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
|
|
|
@ -5,6 +5,7 @@ import SearchContainer from 'mastodon/features/compose/containers/search_contain
|
|||
import ComposeFormContainer from 'mastodon/features/compose/containers/compose_form_container';
|
||||
import NavigationContainer from 'mastodon/features/compose/containers/navigation_container';
|
||||
import LinkFooter from './link_footer';
|
||||
import ServerBanner from 'mastodon/components/server_banner';
|
||||
import { changeComposing } from 'mastodon/actions/compose';
|
||||
|
||||
export default @connect()
|
||||
|
@ -35,6 +36,7 @@ class ComposePanel extends React.PureComponent {
|
|||
|
||||
{!signedIn && (
|
||||
<React.Fragment>
|
||||
<ServerBanner />
|
||||
<div className='flex-spacer' />
|
||||
</React.Fragment>
|
||||
)}
|
||||
|
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
|||
import { connect } from 'react-redux';
|
||||
import { submitReport } from 'mastodon/actions/reports';
|
||||
import { expandAccountTimeline } from 'mastodon/actions/timelines';
|
||||
import { fetchRules } from 'mastodon/actions/rules';
|
||||
import { fetchServer } from 'mastodon/actions/server';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { makeGetAccount } from 'mastodon/selectors';
|
||||
|
@ -117,7 +117,7 @@ class ReportModal extends ImmutablePureComponent {
|
|||
const { dispatch, accountId } = this.props;
|
||||
|
||||
dispatch(expandAccountTimeline(accountId, { withReplies: true }));
|
||||
dispatch(fetchRules());
|
||||
dispatch(fetchServer());
|
||||
}
|
||||
|
||||
render () {
|
||||
|
|
|
@ -13,7 +13,7 @@ import { debounce } from 'lodash';
|
|||
import { uploadCompose, resetCompose, changeComposeSpoilerness } from '../../actions/compose';
|
||||
import { expandHomeTimeline } from '../../actions/timelines';
|
||||
import { expandNotifications } from '../../actions/notifications';
|
||||
import { fetchRules } from '../../actions/rules';
|
||||
import { fetchServer } from '../../actions/server';
|
||||
import { clearHeight } from '../../actions/height_cache';
|
||||
import { focusApp, unfocusApp, changeLayout } from 'mastodon/actions/app';
|
||||
import { synchronouslySubmitMarkers, submitMarkers, fetchMarkers } from 'mastodon/actions/markers';
|
||||
|
@ -389,7 +389,7 @@ class UI extends React.PureComponent {
|
|||
this.props.dispatch(expandHomeTimeline());
|
||||
this.props.dispatch(expandNotifications());
|
||||
|
||||
setTimeout(() => this.props.dispatch(fetchRules()), 3000);
|
||||
setTimeout(() => this.props.dispatch(fetchServer()), 3000);
|
||||
}
|
||||
|
||||
this.hotkeys.__mousetrap__.stopCallback = (e, element) => {
|
||||
|
|
|
@ -29,6 +29,5 @@ export const title = getMeta('title');
|
|||
export const cropImages = getMeta('crop_images');
|
||||
export const disableSwiping = getMeta('disable_swiping');
|
||||
export const languages = initialState && initialState.languages;
|
||||
export const server = initialState && initialState.server;
|
||||
|
||||
export default initialState;
|
||||
|
|
|
@ -17,7 +17,7 @@ import status_lists from './status_lists';
|
|||
import mutes from './mutes';
|
||||
import blocks from './blocks';
|
||||
import boosts from './boosts';
|
||||
import rules from './rules';
|
||||
import server from './server';
|
||||
import contexts from './contexts';
|
||||
import compose from './compose';
|
||||
import search from './search';
|
||||
|
@ -62,7 +62,7 @@ const reducers = {
|
|||
mutes,
|
||||
blocks,
|
||||
boosts,
|
||||
rules,
|
||||
server,
|
||||
contexts,
|
||||
compose,
|
||||
search,
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
import { RULES_FETCH_SUCCESS } from 'mastodon/actions/rules';
|
||||
import { List as ImmutableList, fromJS } from 'immutable';
|
||||
|
||||
const initialState = ImmutableList();
|
||||
|
||||
export default function rules(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case RULES_FETCH_SUCCESS:
|
||||
return fromJS(action.rules);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
import { SERVER_FETCH_REQUEST, SERVER_FETCH_SUCCESS, SERVER_FETCH_FAIL } from 'mastodon/actions/server';
|
||||
import { Map as ImmutableMap, fromJS } from 'immutable';
|
||||
|
||||
const initialState = ImmutableMap({
|
||||
isLoading: true,
|
||||
});
|
||||
|
||||
export default function server(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case SERVER_FETCH_REQUEST:
|
||||
return state.set('isLoading', true);
|
||||
case SERVER_FETCH_SUCCESS:
|
||||
return fromJS(action.server).set('isLoading', false);
|
||||
case SERVER_FETCH_FAIL:
|
||||
return state.set('isLoading', false);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
|
@ -7949,3 +7949,85 @@ noscript {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.server-banner {
|
||||
padding: 20px 0;
|
||||
|
||||
&__introduction {
|
||||
color: $darker-text-color;
|
||||
margin-bottom: 20px;
|
||||
|
||||
strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: underline;
|
||||
|
||||
&:hover,
|
||||
&:active,
|
||||
&:focus {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__hero {
|
||||
display: block;
|
||||
border-radius: 4px;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
margin-bottom: 20px;
|
||||
aspect-ratio: 1.9;
|
||||
border: 0;
|
||||
background: $ui-base-color;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
&__description {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
&__meta {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
max-width: 100%;
|
||||
|
||||
&__column {
|
||||
flex: 0 0 auto;
|
||||
width: calc(50% - 5px);
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
&__number {
|
||||
font-weight: 600;
|
||||
color: $primary-text-color;
|
||||
}
|
||||
|
||||
&__number-label {
|
||||
color: $darker-text-color;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
h4 {
|
||||
text-transform: uppercase;
|
||||
color: $darker-text-color;
|
||||
margin-bottom: 10px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.account {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.account__avatar-wrapper {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
margin: 10px 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,10 +17,6 @@ class PermalinkRedirector
|
|||
find_status_url_by_id(path_segments[2])
|
||||
elsif path_segments[1] == 'accounts' && path_segments[2] =~ /\d/
|
||||
find_account_url_by_id(path_segments[2])
|
||||
elsif path_segments[1] == 'timelines' && path_segments[2] == 'tag' && path_segments[3].present?
|
||||
find_tag_url_by_name(path_segments[3])
|
||||
elsif path_segments[1] == 'tags' && path_segments[2].present?
|
||||
find_tag_url_by_name(path_segments[2])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,20 +1,52 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class InstancePresenter
|
||||
delegate(
|
||||
:site_contact_email,
|
||||
:site_title,
|
||||
:site_short_description,
|
||||
:site_description,
|
||||
:site_extended_description,
|
||||
:site_terms,
|
||||
:closed_registrations_message,
|
||||
to: Setting
|
||||
)
|
||||
class InstancePresenter < ActiveModelSerializers::Model
|
||||
attributes :domain, :title, :version, :source_url,
|
||||
:description, :languages, :rules, :contact
|
||||
|
||||
def contact_account
|
||||
class ContactPresenter < ActiveModelSerializers::Model
|
||||
attributes :email, :account
|
||||
|
||||
def email
|
||||
Setting.site_contact_email
|
||||
end
|
||||
|
||||
def account
|
||||
Account.find_local(Setting.site_contact_username.strip.gsub(/\A@/, ''))
|
||||
end
|
||||
end
|
||||
|
||||
def contact
|
||||
ContactPresenter.new
|
||||
end
|
||||
|
||||
def closed_registrations_message
|
||||
Setting.closed_registrations_message
|
||||
end
|
||||
|
||||
def description
|
||||
Setting.site_short_description
|
||||
end
|
||||
|
||||
def extended_description
|
||||
Setting.site_extended_description
|
||||
end
|
||||
|
||||
def privacy_policy
|
||||
Setting.site_terms
|
||||
end
|
||||
|
||||
def domain
|
||||
Rails.configuration.x.local_domain
|
||||
end
|
||||
|
||||
def title
|
||||
Setting.site_title
|
||||
end
|
||||
|
||||
def languages
|
||||
[I18n.default_locale]
|
||||
end
|
||||
|
||||
def rules
|
||||
Rule.ordered
|
||||
|
@ -40,8 +72,8 @@ class InstancePresenter
|
|||
Rails.cache.fetch('sample_accounts', expires_in: 12.hours) { Account.local.discoverable.popular.limit(3) }
|
||||
end
|
||||
|
||||
def version_number
|
||||
Mastodon::Version
|
||||
def version
|
||||
Mastodon::Version.to_s
|
||||
end
|
||||
|
||||
def source_url
|
||||
|
|
|
@ -6,7 +6,7 @@ class InitialStateSerializer < ActiveModel::Serializer
|
|||
attributes :meta, :compose, :accounts,
|
||||
:media_attachments, :settings,
|
||||
:max_toot_chars, :poll_limits,
|
||||
:languages, :server
|
||||
:languages
|
||||
|
||||
has_one :push_subscription, serializer: REST::WebPushSubscriptionSerializer
|
||||
has_one :role, serializer: REST::RoleSerializer
|
||||
|
@ -24,18 +24,19 @@ class InitialStateSerializer < ActiveModel::Serializer
|
|||
}
|
||||
end
|
||||
|
||||
# rubocop:disable Metrics/AbcSize
|
||||
def meta
|
||||
store = {
|
||||
streaming_api_base_url: Rails.configuration.x.streaming_api_base_url,
|
||||
access_token: object.token,
|
||||
locale: I18n.locale,
|
||||
domain: Rails.configuration.x.local_domain,
|
||||
title: instance_presenter.site_title,
|
||||
domain: instance_presenter.domain,
|
||||
title: instance_presenter.title,
|
||||
admin: object.admin&.id&.to_s,
|
||||
search_enabled: Chewy.enabled?,
|
||||
repository: Mastodon::Version.repository,
|
||||
source_url: Mastodon::Version.source_url,
|
||||
version: Mastodon::Version.to_s,
|
||||
source_url: instance_presenter.source_url,
|
||||
version: instance_presenter.version,
|
||||
limited_federation_mode: Rails.configuration.x.whitelist_mode,
|
||||
mascot: instance_presenter.mascot&.file&.url,
|
||||
profile_directory: Setting.profile_directory,
|
||||
|
@ -71,6 +72,7 @@ class InitialStateSerializer < ActiveModel::Serializer
|
|||
|
||||
store
|
||||
end
|
||||
# rubocop:enable Metrics/AbcSize
|
||||
|
||||
def compose
|
||||
store = {}
|
||||
|
@ -102,13 +104,6 @@ class InitialStateSerializer < ActiveModel::Serializer
|
|||
LanguagesHelper::SUPPORTED_LOCALES.map { |(key, value)| [key, value[0], value[1]] }
|
||||
end
|
||||
|
||||
def server
|
||||
{
|
||||
hero: instance_presenter.hero&.file&.url || instance_presenter.thumbnail&.file&.url || asset_pack_path('media/images/preview.png'),
|
||||
description: instance_presenter.site_short_description.presence || I18n.t('about.about_mastodon_html'),
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def instance_presenter
|
||||
|
|
|
@ -22,11 +22,11 @@ class ManifestSerializer < ActiveModel::Serializer
|
|||
:share_target, :shortcuts
|
||||
|
||||
def name
|
||||
object.site_title
|
||||
object.title
|
||||
end
|
||||
|
||||
def short_name
|
||||
object.site_title
|
||||
object.title
|
||||
end
|
||||
|
||||
def icons
|
||||
|
|
|
@ -1,74 +1,39 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class REST::InstanceSerializer < ActiveModel::Serializer
|
||||
class ContactSerializer < ActiveModel::Serializer
|
||||
attributes :email
|
||||
|
||||
has_one :account, serializer: REST::AccountSerializer
|
||||
end
|
||||
|
||||
include RoutingHelper
|
||||
|
||||
attributes :uri, :title, :short_description, :description, :email,
|
||||
:version, :urls, :stats, :thumbnail, :max_toot_chars, :poll_limits,
|
||||
:languages, :registrations, :approval_required, :invites_enabled,
|
||||
:configuration
|
||||
|
||||
has_one :contact_account, serializer: REST::AccountSerializer
|
||||
attributes :domain, :title, :version, :source_url, :description,
|
||||
:usage, :thumbnail, :languages, :configuration,
|
||||
:registrations
|
||||
|
||||
has_one :contact, serializer: ContactSerializer
|
||||
has_many :rules, serializer: REST::RuleSerializer
|
||||
|
||||
delegate :contact_account, :rules, to: :instance_presenter
|
||||
|
||||
def uri
|
||||
Rails.configuration.x.local_domain
|
||||
end
|
||||
|
||||
def title
|
||||
Setting.site_title
|
||||
end
|
||||
|
||||
def short_description
|
||||
Setting.site_short_description
|
||||
end
|
||||
|
||||
def description
|
||||
Setting.site_description
|
||||
end
|
||||
|
||||
def email
|
||||
Setting.site_contact_email
|
||||
end
|
||||
|
||||
def version
|
||||
Mastodon::Version.to_s
|
||||
end
|
||||
|
||||
def thumbnail
|
||||
instance_presenter.thumbnail ? full_asset_url(instance_presenter.thumbnail.file.url) : full_pack_url('media/images/preview.png')
|
||||
object.thumbnail ? full_asset_url(object.thumbnail.file.url) : full_pack_url('media/images/preview.png')
|
||||
end
|
||||
|
||||
def max_toot_chars
|
||||
StatusLengthValidator::MAX_CHARS
|
||||
end
|
||||
|
||||
def poll_limits
|
||||
def usage
|
||||
{
|
||||
max_options: PollValidator::MAX_OPTIONS,
|
||||
max_option_chars: PollValidator::MAX_OPTION_CHARS,
|
||||
min_expiration: PollValidator::MIN_EXPIRATION,
|
||||
max_expiration: PollValidator::MAX_EXPIRATION,
|
||||
users: {
|
||||
active_month: object.active_user_count(4),
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
def stats
|
||||
{
|
||||
user_count: instance_presenter.user_count,
|
||||
status_count: instance_presenter.status_count,
|
||||
domain_count: instance_presenter.domain_count,
|
||||
}
|
||||
end
|
||||
|
||||
def urls
|
||||
{ streaming_api: Rails.configuration.x.streaming_api_base_url }
|
||||
end
|
||||
|
||||
def configuration
|
||||
{
|
||||
urls: {
|
||||
streaming: Rails.configuration.x.streaming_api_base_url,
|
||||
},
|
||||
|
||||
statuses: {
|
||||
max_characters: StatusLengthValidator::MAX_CHARS,
|
||||
max_media_attachments: 4,
|
||||
|
@ -93,25 +58,10 @@ class REST::InstanceSerializer < ActiveModel::Serializer
|
|||
}
|
||||
end
|
||||
|
||||
def languages
|
||||
[I18n.default_locale]
|
||||
end
|
||||
|
||||
def registrations
|
||||
Setting.registrations_mode != 'none' && !Rails.configuration.x.single_user_mode
|
||||
end
|
||||
|
||||
def approval_required
|
||||
Setting.registrations_mode == 'approved'
|
||||
end
|
||||
|
||||
def invites_enabled
|
||||
UserRole.everyone.can?(:invite_users)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def instance_presenter
|
||||
@instance_presenter ||= InstancePresenter.new
|
||||
{
|
||||
enabled: Setting.registrations_mode != 'none' && !Rails.configuration.x.single_user_mode,
|
||||
approval_required: Setting.registrations_mode == 'approved',
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,115 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class REST::V1::InstanceSerializer < ActiveModel::Serializer
|
||||
include RoutingHelper
|
||||
|
||||
attributes :uri, :title, :short_description, :description, :email,
|
||||
:version, :urls, :stats, :thumbnail, :max_toot_chars, :poll_limits,
|
||||
:languages, :registrations, :approval_required, :invites_enabled,
|
||||
:configuration
|
||||
|
||||
has_one :contact_account, serializer: REST::AccountSerializer
|
||||
|
||||
has_many :rules, serializer: REST::RuleSerializer
|
||||
|
||||
def uri
|
||||
object.domain
|
||||
end
|
||||
|
||||
def short_description
|
||||
object.description
|
||||
end
|
||||
|
||||
def description
|
||||
Setting.site_description # Legacy
|
||||
end
|
||||
|
||||
def email
|
||||
object.contact.email
|
||||
end
|
||||
|
||||
def contact_account
|
||||
object.contact.account
|
||||
end
|
||||
|
||||
def thumbnail
|
||||
instance_presenter.thumbnail ? full_asset_url(instance_presenter.thumbnail.file.url) : full_pack_url('media/images/preview.png')
|
||||
end
|
||||
|
||||
def max_toot_chars
|
||||
StatusLengthValidator::MAX_CHARS
|
||||
end
|
||||
|
||||
def poll_limits
|
||||
{
|
||||
max_options: PollValidator::MAX_OPTIONS,
|
||||
max_option_chars: PollValidator::MAX_OPTION_CHARS,
|
||||
min_expiration: PollValidator::MIN_EXPIRATION,
|
||||
max_expiration: PollValidator::MAX_EXPIRATION,
|
||||
}
|
||||
end
|
||||
|
||||
def stats
|
||||
{
|
||||
user_count: instance_presenter.user_count,
|
||||
status_count: instance_presenter.status_count,
|
||||
domain_count: instance_presenter.domain_count,
|
||||
}
|
||||
end
|
||||
|
||||
def urls
|
||||
{ streaming_api: Rails.configuration.x.streaming_api_base_url }
|
||||
end
|
||||
|
||||
def usage
|
||||
{
|
||||
users: {
|
||||
active_month: instance_presenter.active_user_count(4),
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
def configuration
|
||||
{
|
||||
statuses: {
|
||||
max_characters: StatusLengthValidator::MAX_CHARS,
|
||||
max_media_attachments: 4,
|
||||
characters_reserved_per_url: StatusLengthValidator::URL_PLACEHOLDER_CHARS,
|
||||
},
|
||||
|
||||
media_attachments: {
|
||||
supported_mime_types: MediaAttachment::IMAGE_MIME_TYPES + MediaAttachment::VIDEO_MIME_TYPES + MediaAttachment::AUDIO_MIME_TYPES,
|
||||
image_size_limit: MediaAttachment::IMAGE_LIMIT,
|
||||
image_matrix_limit: Attachmentable::MAX_MATRIX_LIMIT,
|
||||
video_size_limit: MediaAttachment::VIDEO_LIMIT,
|
||||
video_frame_rate_limit: MediaAttachment::MAX_VIDEO_FRAME_RATE,
|
||||
video_matrix_limit: MediaAttachment::MAX_VIDEO_MATRIX_LIMIT,
|
||||
},
|
||||
|
||||
polls: {
|
||||
max_options: PollValidator::MAX_OPTIONS,
|
||||
max_characters_per_option: PollValidator::MAX_OPTION_CHARS,
|
||||
min_expiration: PollValidator::MIN_EXPIRATION,
|
||||
max_expiration: PollValidator::MAX_EXPIRATION,
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
def registrations
|
||||
Setting.registrations_mode != 'none' && !Rails.configuration.x.single_user_mode
|
||||
end
|
||||
|
||||
def approval_required
|
||||
Setting.registrations_mode == 'approved'
|
||||
end
|
||||
|
||||
def invites_enabled
|
||||
UserRole.everyone.can?(:invite_users)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def instance_presenter
|
||||
@instance_presenter ||= InstancePresenter.new
|
||||
end
|
||||
end
|
|
@ -8,7 +8,7 @@
|
|||
.column-0
|
||||
.public-account-header.public-account-header--no-bar
|
||||
.public-account-header__image
|
||||
= image_tag @instance_presenter.hero&.file&.url || @instance_presenter.thumbnail&.file&.url || asset_pack_path('media/images/preview.png'), alt: @instance_presenter.site_title, class: 'parallax'
|
||||
= image_tag @instance_presenter.hero&.file&.url || @instance_presenter.thumbnail&.file&.url || asset_pack_path('media/images/preview.png'), alt: @instance_presenter.title, class: 'parallax'
|
||||
|
||||
.column-1
|
||||
.landing-page__call-to-action{ dir: 'ltr' }
|
||||
|
@ -30,14 +30,14 @@
|
|||
.contact-widget
|
||||
%h4= t 'about.administered_by'
|
||||
|
||||
= account_link_to(@instance_presenter.contact_account)
|
||||
= account_link_to(@instance_presenter.contact.account)
|
||||
|
||||
- if @instance_presenter.site_contact_email.present?
|
||||
- if @instance_presenter.contact.email.present?
|
||||
%h4
|
||||
= succeed ':' do
|
||||
= t 'about.contact'
|
||||
|
||||
= mail_to @instance_presenter.site_contact_email, nil, title: @instance_presenter.site_contact_email
|
||||
= mail_to @instance_presenter.contact.email, nil, title: @instance_presenter.contact.email
|
||||
|
||||
.column-3
|
||||
= render 'application/flashes'
|
||||
|
|
|
@ -53,11 +53,11 @@
|
|||
|
||||
.hero-widget
|
||||
.hero-widget__img
|
||||
= image_tag @instance_presenter.hero&.file&.url || @instance_presenter.thumbnail&.file&.url || asset_pack_path('media/images/preview.png'), alt: @instance_presenter.site_title
|
||||
= image_tag @instance_presenter.hero&.file&.url || @instance_presenter.thumbnail&.file&.url || asset_pack_path('media/images/preview.png'), alt: @instance_presenter.title
|
||||
|
||||
.hero-widget__text
|
||||
%p
|
||||
= @instance_presenter.site_short_description.html_safe.presence || t('about.about_mastodon_html')
|
||||
= @instance_presenter.description.html_safe.presence || t('about.about_mastodon_html')
|
||||
= link_to about_more_path do
|
||||
= t('about.learn_more')
|
||||
= fa_icon 'angle-double-right'
|
||||
|
@ -66,7 +66,7 @@
|
|||
.hero-widget__footer__column
|
||||
%h4= t 'about.administered_by'
|
||||
|
||||
= account_link_to @instance_presenter.contact_account
|
||||
= account_link_to @instance_presenter.contact.account
|
||||
|
||||
.hero-widget__footer__column
|
||||
%h4= t 'about.server_stats'
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
.hero-widget
|
||||
.hero-widget__img
|
||||
= image_tag @instance_presenter.hero&.file&.url || @instance_presenter.thumbnail&.file&.url || asset_pack_path('media/images/preview.png'), alt: @instance_presenter.site_title
|
||||
= image_tag @instance_presenter.hero&.file&.url || @instance_presenter.thumbnail&.file&.url || asset_pack_path('media/images/preview.png'), alt: @instance_presenter.title
|
||||
|
||||
.hero-widget__text
|
||||
%p= @instance_presenter.site_short_description.html_safe.presence || t('about.about_mastodon_html')
|
||||
%p= @instance_presenter.description.html_safe.presence || t('about.about_mastodon_html')
|
||||
|
||||
- if Setting.trends && !(user_signed_in? && !current_user.setting_trends)
|
||||
- trends = Trends.tags.query.allowed.limit(3)
|
||||
|
|
|
@ -1,10 +1,14 @@
|
|||
- content_for :header_tags do
|
||||
- if user_signed_in?
|
||||
= preload_pack_asset 'features/getting_started.js', crossorigin: 'anonymous'
|
||||
= preload_pack_asset 'features/compose.js', crossorigin: 'anonymous'
|
||||
= preload_pack_asset 'features/home_timeline.js', crossorigin: 'anonymous'
|
||||
= preload_pack_asset 'features/notifications.js', crossorigin: 'anonymous'
|
||||
|
||||
= render partial: 'shared/og'
|
||||
|
||||
%meta{name: 'applicationServerKey', content: Rails.configuration.x.vapid_public_key}
|
||||
|
||||
= render_initial_state
|
||||
|
||||
.notranslate.app-holder#mastodon{ data: { props: Oj.dump(default_props) } }
|
||||
|
|
|
@ -4,6 +4,6 @@
|
|||
.grid
|
||||
.column-0
|
||||
.box-widget
|
||||
.rich-formatting= @instance_presenter.site_terms.html_safe.presence || t('terms.body_html')
|
||||
.rich-formatting= @instance_presenter.privacy_policy.html_safe.presence || t('terms.body_html')
|
||||
.column-1
|
||||
= render 'application/sidebar'
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
- thumbnail = @instance_presenter.thumbnail
|
||||
- description ||= strip_tags(@instance_presenter.site_short_description.presence || t('about.about_mastodon_html'))
|
||||
- description ||= strip_tags(@instance_presenter.description.presence || t('about.about_mastodon_html'))
|
||||
|
||||
%meta{ name: 'description', content: description }/
|
||||
|
||||
= opengraph 'og:site_name', t('about.hosted_on', domain: site_hostname)
|
||||
= opengraph 'og:url', url_for(only_path: false)
|
||||
= opengraph 'og:type', 'website'
|
||||
= opengraph 'og:title', @instance_presenter.site_title
|
||||
= opengraph 'og:title', @instance_presenter.title
|
||||
= opengraph 'og:description', description
|
||||
= opengraph 'og:image', full_asset_url(thumbnail&.file&.url || asset_pack_path('media/images/preview.png', protocol: :request))
|
||||
= opengraph 'og:image:width', thumbnail ? thumbnail.meta['width'] : '1200'
|
||||
|
|
|
@ -639,10 +639,12 @@ Rails.application.routes.draw do
|
|||
end
|
||||
|
||||
namespace :v2 do
|
||||
resources :media, only: [:create]
|
||||
get '/search', to: 'search#index', as: :search
|
||||
|
||||
resources :media, only: [:create]
|
||||
resources :suggestions, only: [:index]
|
||||
resources :filters, only: [:index, :create, :show, :update, :destroy]
|
||||
resource :instance, only: [:show]
|
||||
|
||||
namespace :admin do
|
||||
resources :accounts, only: [:index]
|
||||
|
|
|
@ -8,9 +8,9 @@ RSpec.describe HomeController, type: :controller do
|
|||
|
||||
context 'when not signed in' do
|
||||
context 'when requested path is tag timeline' do
|
||||
it 'redirects to the tag\'s permalink' do
|
||||
it 'returns http success' do
|
||||
@request.path = '/web/timelines/tag/name'
|
||||
is_expected.to redirect_to '/tags/name'
|
||||
is_expected.to have_http_status(:success)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -23,11 +23,12 @@ RSpec.describe HomeController, type: :controller do
|
|||
context 'when signed in' do
|
||||
let(:user) { Fabricate(:user) }
|
||||
|
||||
before { sign_in(user) }
|
||||
before do
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
it 'assigns @body_classes' do
|
||||
subject
|
||||
expect(assigns(:body_classes)).to eq 'app-body'
|
||||
it 'returns http success' do
|
||||
is_expected.to have_http_status(:success)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -21,7 +21,7 @@ describe PermalinkRedirector do
|
|||
|
||||
it 'returns path for legacy tag links' do
|
||||
redirector = described_class.new('web/timelines/tag/hoge')
|
||||
expect(redirector.redirect_path).to eq '/tags/hoge'
|
||||
expect(redirector.redirect_path).to be_nil
|
||||
end
|
||||
|
||||
it 'returns path for pretty account links' do
|
||||
|
@ -36,7 +36,7 @@ describe PermalinkRedirector do
|
|||
|
||||
it 'returns path for pretty tag links' do
|
||||
redirector = described_class.new('web/tags/hoge')
|
||||
expect(redirector.redirect_path).to eq '/tags/hoge'
|
||||
expect(redirector.redirect_path).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,21 +3,20 @@ require 'rails_helper'
|
|||
describe InstancePresenter do
|
||||
let(:instance_presenter) { InstancePresenter.new }
|
||||
|
||||
context do
|
||||
describe '#description' do
|
||||
around do |example|
|
||||
site_description = Setting.site_description
|
||||
site_description = Setting.site_short_description
|
||||
example.run
|
||||
Setting.site_description = site_description
|
||||
Setting.site_short_description = site_description
|
||||
end
|
||||
|
||||
it "delegates site_description to Setting" do
|
||||
Setting.site_description = "Site desc"
|
||||
|
||||
expect(instance_presenter.site_description).to eq "Site desc"
|
||||
Setting.site_short_description = "Site desc"
|
||||
expect(instance_presenter.description).to eq "Site desc"
|
||||
end
|
||||
end
|
||||
|
||||
context do
|
||||
describe '#extended_description' do
|
||||
around do |example|
|
||||
site_extended_description = Setting.site_extended_description
|
||||
example.run
|
||||
|
@ -26,12 +25,11 @@ describe InstancePresenter do
|
|||
|
||||
it "delegates site_extended_description to Setting" do
|
||||
Setting.site_extended_description = "Extended desc"
|
||||
|
||||
expect(instance_presenter.site_extended_description).to eq "Extended desc"
|
||||
expect(instance_presenter.extended_description).to eq "Extended desc"
|
||||
end
|
||||
end
|
||||
|
||||
context do
|
||||
describe '#email' do
|
||||
around do |example|
|
||||
site_contact_email = Setting.site_contact_email
|
||||
example.run
|
||||
|
@ -40,12 +38,11 @@ describe InstancePresenter do
|
|||
|
||||
it "delegates contact_email to Setting" do
|
||||
Setting.site_contact_email = "admin@example.com"
|
||||
|
||||
expect(instance_presenter.site_contact_email).to eq "admin@example.com"
|
||||
expect(instance_presenter.contact.email).to eq "admin@example.com"
|
||||
end
|
||||
end
|
||||
|
||||
describe "contact_account" do
|
||||
describe '#account' do
|
||||
around do |example|
|
||||
site_contact_username = Setting.site_contact_username
|
||||
example.run
|
||||
|
@ -55,12 +52,11 @@ describe InstancePresenter do
|
|||
it "returns the account for the site contact username" do
|
||||
Setting.site_contact_username = "aaa"
|
||||
account = Fabricate(:account, username: "aaa")
|
||||
|
||||
expect(instance_presenter.contact_account).to eq(account)
|
||||
expect(instance_presenter.contact.account).to eq(account)
|
||||
end
|
||||
end
|
||||
|
||||
describe "user_count" do
|
||||
describe '#user_count' do
|
||||
it "returns the number of site users" do
|
||||
Rails.cache.write 'user_count', 123
|
||||
|
||||
|
@ -68,7 +64,7 @@ describe InstancePresenter do
|
|||
end
|
||||
end
|
||||
|
||||
describe "status_count" do
|
||||
describe '#status_count' do
|
||||
it "returns the number of local statuses" do
|
||||
Rails.cache.write 'local_status_count', 234
|
||||
|
||||
|
@ -76,7 +72,7 @@ describe InstancePresenter do
|
|||
end
|
||||
end
|
||||
|
||||
describe "domain_count" do
|
||||
describe '#domain_count' do
|
||||
it "returns the number of known domains" do
|
||||
Rails.cache.write 'distinct_domain_count', 345
|
||||
|
||||
|
@ -84,9 +80,9 @@ describe InstancePresenter do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#version_number' do
|
||||
it 'returns Mastodon::Version' do
|
||||
expect(instance_presenter.version_number).to be(Mastodon::Version)
|
||||
describe '#version' do
|
||||
it 'returns string' do
|
||||
expect(instance_presenter.version).to be_a String
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -14,26 +14,7 @@ describe 'about/show.html.haml', without_verify_partial_doubles: true do
|
|||
end
|
||||
|
||||
it 'has valid open graph tags' do
|
||||
instance_presenter = double(
|
||||
:instance_presenter,
|
||||
site_title: 'something',
|
||||
site_short_description: 'something',
|
||||
site_description: 'something',
|
||||
version_number: '1.0',
|
||||
source_url: 'https://github.com/mastodon/mastodon',
|
||||
open_registrations: false,
|
||||
thumbnail: nil,
|
||||
hero: nil,
|
||||
mascot: nil,
|
||||
user_count: 420,
|
||||
status_count: 69,
|
||||
active_user_count: 420,
|
||||
commit_hash: commit_hash,
|
||||
contact_account: nil,
|
||||
sample_accounts: []
|
||||
)
|
||||
|
||||
assign(:instance_presenter, instance_presenter)
|
||||
assign(:instance_presenter, InstancePresenter.new)
|
||||
render
|
||||
|
||||
header_tags = view.content_for(:header_tags)
|
||||
|
|
Loading…
Reference in New Issue