mirror of
https://git.kescher.at/CatCatNya/catstodon.git
synced 2024-11-30 00:39:02 +01:00
Merge pull request #1861 from ClearlyClaire/glitch-soc/features/logged-out-webui
Port logged-out Web UI to glitch-soc
This commit is contained in:
commit
94713940c7
93 changed files with 1802 additions and 505 deletions
|
@ -20,7 +20,7 @@ class AboutController < ApplicationController
|
||||||
def more
|
def more
|
||||||
flash.now[:notice] = I18n.t('about.instance_actor_flash') if params[:instance_actor]
|
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
|
@rules = Rule.ordered
|
||||||
@contents = toc_generator.html
|
@contents = toc_generator.html
|
||||||
|
|
|
@ -6,6 +6,6 @@ class Api::V1::InstancesController < Api::BaseController
|
||||||
|
|
||||||
def show
|
def show
|
||||||
expires_in 3.minutes, public: true
|
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
|
||||||
end
|
end
|
||||||
|
|
8
app/controllers/api/v2/instances_controller.rb
Normal file
8
app/controllers/api/v2/instances_controller.rb
Normal file
|
@ -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
|
class HomeController < ApplicationController
|
||||||
before_action :redirect_unauthenticated_to_permalinks!
|
before_action :redirect_unauthenticated_to_permalinks!
|
||||||
before_action :authenticate_user!
|
|
||||||
|
|
||||||
before_action :set_pack
|
before_action :set_pack
|
||||||
before_action :set_referrer_policy_header
|
before_action :set_referrer_policy_header
|
||||||
|
before_action :set_instance_presenter
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@body_classes = 'app-body'
|
@body_classes = 'app-body'
|
||||||
|
@ -16,7 +16,10 @@ class HomeController < ApplicationController
|
||||||
def redirect_unauthenticated_to_permalinks!
|
def redirect_unauthenticated_to_permalinks!
|
||||||
return if user_signed_in?
|
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
|
end
|
||||||
|
|
||||||
def set_pack
|
def set_pack
|
||||||
|
@ -24,8 +27,10 @@ class HomeController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
def default_redirect_path
|
def default_redirect_path
|
||||||
if request.path.start_with?('/web') || whitelist_mode?
|
if whitelist_mode?
|
||||||
new_user_session_path
|
new_user_session_path
|
||||||
|
elsif request.path.start_with?('/web')
|
||||||
|
nil
|
||||||
elsif single_user_mode?
|
elsif single_user_mode?
|
||||||
short_account_path(Account.local.without_suspended.where('id > 0').first)
|
short_account_path(Account.local.without_suspended.where('id > 0').first)
|
||||||
else
|
else
|
||||||
|
@ -36,4 +41,8 @@ class HomeController < ApplicationController
|
||||||
def set_referrer_policy_header
|
def set_referrer_policy_header
|
||||||
response.headers['Referrer-Policy'] = 'origin'
|
response.headers['Referrer-Policy'] = 'origin'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def set_instance_presenter
|
||||||
|
@instance_presenter = InstancePresenter.new
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -553,10 +553,12 @@ export function expandFollowingFail(id, error) {
|
||||||
|
|
||||||
export function fetchRelationships(accountIds) {
|
export function fetchRelationships(accountIds) {
|
||||||
return (dispatch, getState) => {
|
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 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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import api from 'flavours/glitch/util/api';
|
import api from 'flavours/glitch/util/api';
|
||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
import compareId from 'flavours/glitch/util/compare_id';
|
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_REQUEST = 'MARKERS_FETCH_REQUEST';
|
||||||
export const MARKERS_FETCH_SUCCESS = 'MARKERS_FETCH_SUCCESS';
|
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 accessToken = getState().getIn(['meta', 'access_token'], '');
|
||||||
const params = _buildParams(getState());
|
const params = _buildParams(getState());
|
||||||
|
|
||||||
if (Object.keys(params).length === 0) {
|
if (Object.keys(params).length === 0 || accessToken === '') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,7 +64,7 @@ export const synchronouslySubmitMarkers = () => (dispatch, getState) => {
|
||||||
const _buildParams = (state) => {
|
const _buildParams = (state) => {
|
||||||
const params = {};
|
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']);
|
const lastNotificationId = state.getIn(['notifications', 'lastReadId']);
|
||||||
|
|
||||||
if (lastHomeId && compareId(lastHomeId, state.getIn(['markers', 'home'])) > 0) {
|
if (lastHomeId && compareId(lastHomeId, state.getIn(['markers', 'home'])) > 0) {
|
||||||
|
@ -82,9 +83,10 @@ const _buildParams = (state) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const debouncedSubmitMarkers = debounce((dispatch, getState) => {
|
const debouncedSubmitMarkers = debounce((dispatch, getState) => {
|
||||||
|
const accessToken = getState().getIn(['meta', 'access_token'], '');
|
||||||
const params = _buildParams(getState());
|
const params = _buildParams(getState());
|
||||||
|
|
||||||
if (Object.keys(params).length === 0) {
|
if (Object.keys(params).length === 0 || accessToken === '') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,19 +1,5 @@
|
||||||
import {
|
import { setAlerts } from './setter';
|
||||||
SET_BROWSER_SUPPORT,
|
import { saveSettings } from './registerer';
|
||||||
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,
|
|
||||||
};
|
|
||||||
|
|
||||||
export function changeAlerts(path, value) {
|
export function changeAlerts(path, value) {
|
||||||
return dispatch => {
|
return dispatch => {
|
||||||
|
@ -21,3 +7,11 @@ export function changeAlerts(path, value) {
|
||||||
dispatch(saveSettings());
|
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,
|
|
||||||
});
|
|
30
app/javascript/flavours/glitch/actions/server.js
Normal file
30
app/javascript/flavours/glitch/actions/server.js
Normal file
|
@ -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 ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import { me } from 'flavours/glitch/util/initial_state';
|
import { me } from 'flavours/glitch/util/initial_state';
|
||||||
import RelativeTimestamp from './relative_timestamp';
|
import RelativeTimestamp from './relative_timestamp';
|
||||||
|
import Skeleton from 'flavours/glitch/components/skeleton';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
follow: { id: 'account.follow', defaultMessage: 'Follow' },
|
follow: { id: 'account.follow', defaultMessage: 'Follow' },
|
||||||
|
@ -26,7 +27,7 @@ export default @injectIntl
|
||||||
class Account extends ImmutablePureComponent {
|
class Account extends ImmutablePureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
account: ImmutablePropTypes.map.isRequired,
|
account: ImmutablePropTypes.map,
|
||||||
onFollow: PropTypes.func.isRequired,
|
onFollow: PropTypes.func.isRequired,
|
||||||
onBlock: PropTypes.func.isRequired,
|
onBlock: PropTypes.func.isRequired,
|
||||||
onMute: PropTypes.func.isRequired,
|
onMute: PropTypes.func.isRequired,
|
||||||
|
@ -77,7 +78,16 @@ class Account extends ImmutablePureComponent {
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
if (!account) {
|
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) {
|
if (hidden) {
|
||||||
|
|
|
@ -17,6 +17,7 @@ class ColumnHeader extends React.PureComponent {
|
||||||
|
|
||||||
static contextTypes = {
|
static contextTypes = {
|
||||||
router: PropTypes.object,
|
router: PropTypes.object,
|
||||||
|
identity: PropTypes.object,
|
||||||
};
|
};
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
@ -150,7 +151,7 @@ class ColumnHeader extends React.PureComponent {
|
||||||
collapsedContent.push(moveButtons);
|
collapsedContent.push(moveButtons);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (children || (multiColumn && this.props.onPin)) {
|
if (this.context.identity.signedIn && (children || (multiColumn && this.props.onPin))) {
|
||||||
collapseButton = (
|
collapseButton = (
|
||||||
<button
|
<button
|
||||||
className={collapsibleButtonClassName}
|
className={collapsibleButtonClassName}
|
||||||
|
|
|
@ -3,6 +3,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { autoPlayGif } from 'flavours/glitch/util/initial_state';
|
import { autoPlayGif } from 'flavours/glitch/util/initial_state';
|
||||||
|
import Skeleton from 'flavours/glitch/components/skeleton';
|
||||||
|
|
||||||
export default class DisplayName extends React.PureComponent {
|
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);
|
const computedClass = classNames('display-name', { inline }, className);
|
||||||
|
|
||||||
if (!account) return null;
|
|
||||||
|
|
||||||
let displayName, suffix;
|
let displayName, suffix;
|
||||||
|
let acct;
|
||||||
|
|
||||||
let acct = account.get('acct');
|
if (account) {
|
||||||
|
acct = account.get('acct');
|
||||||
|
|
||||||
if (acct.indexOf('@') === -1 && localDomain) {
|
if (acct.indexOf('@') === -1 && localDomain) {
|
||||||
acct = `${acct}@${localDomain}`;
|
acct = `${acct}@${localDomain}`;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (others && others.size > 0) {
|
if (others && others.size > 0) {
|
||||||
displayName = others.take(2).map(a => (
|
displayName = others.take(2).map(a => (
|
||||||
|
@ -80,9 +82,12 @@ export default class DisplayName extends React.PureComponent {
|
||||||
<span className='display-name__account'>@{acct}</span>
|
<span className='display-name__account'>@{acct}</span>
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
} else {
|
} else if (account) {
|
||||||
displayName = <bdi><strong className='display-name__html' dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }} /></bdi>;
|
displayName = <bdi><strong className='display-name__html' dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }} /></bdi>;
|
||||||
suffix = <span className='display-name__account'>@{acct}</span>;
|
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 (
|
return (
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
const Logo = () => (
|
const Logo = () => (
|
||||||
<svg viewBox='0 0 216.4144 232.00976' className='logo'>
|
<svg viewBox='0 0 261 66' className='logo'>
|
||||||
<use xlinkHref='#mastodon-svg-logo' />
|
<use xlinkHref='#logo-symbol-wordmark' />
|
||||||
</svg>
|
</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
|
export default @injectIntl
|
||||||
class Poll extends ImmutablePureComponent {
|
class Poll extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
static contextTypes = {
|
||||||
|
identity: PropTypes.object,
|
||||||
|
};
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
poll: ImmutablePropTypes.map,
|
poll: ImmutablePropTypes.map,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
|
@ -217,7 +221,7 @@ class Poll extends ImmutablePureComponent {
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div className='poll__footer'>
|
<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>}
|
{showResults && !this.props.disabled && <span><button className='poll__link' onClick={this.handleRefresh}><FormattedMessage id='poll.refresh' defaultMessage='Refresh' /></button> · </span>}
|
||||||
{votesCount}
|
{votesCount}
|
||||||
{poll.get('expires_at') && <span> · {timeRemaining}</span>}
|
{poll.get('expires_at') && <span> · {timeRemaining}</span>}
|
||||||
|
|
91
app/javascript/flavours/glitch/components/server_banner.js
Normal file
91
app/javascript/flavours/glitch/components/server_banner.js
Normal file
|
@ -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,
|
onEmbed: PropTypes.func,
|
||||||
onHeightChange: PropTypes.func,
|
onHeightChange: PropTypes.func,
|
||||||
onToggleHidden: PropTypes.func,
|
onToggleHidden: PropTypes.func,
|
||||||
|
onInteractionModal: PropTypes.func,
|
||||||
muted: PropTypes.bool,
|
muted: PropTypes.bool,
|
||||||
hidden: PropTypes.bool,
|
hidden: PropTypes.bool,
|
||||||
unread: PropTypes.bool,
|
unread: PropTypes.bool,
|
||||||
|
|
|
@ -69,6 +69,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
onBookmark: PropTypes.func,
|
onBookmark: PropTypes.func,
|
||||||
onFilter: PropTypes.func,
|
onFilter: PropTypes.func,
|
||||||
onAddFilter: PropTypes.func,
|
onAddFilter: PropTypes.func,
|
||||||
|
onInteractionModal: PropTypes.func,
|
||||||
withDismiss: PropTypes.bool,
|
withDismiss: PropTypes.bool,
|
||||||
withCounters: PropTypes.bool,
|
withCounters: PropTypes.bool,
|
||||||
showReplyCount: PropTypes.bool,
|
showReplyCount: PropTypes.bool,
|
||||||
|
@ -86,10 +87,12 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
]
|
]
|
||||||
|
|
||||||
handleReplyClick = () => {
|
handleReplyClick = () => {
|
||||||
if (me) {
|
const { signedIn } = this.context.identity;
|
||||||
|
|
||||||
|
if (signedIn) {
|
||||||
this.props.onReply(this.props.status, this.context.router.history);
|
this.props.onReply(this.props.status, this.context.router.history);
|
||||||
} else {
|
} else {
|
||||||
this._openInteractionDialog('reply');
|
this.props.onInteractionModal('reply', this.props.status);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -101,10 +104,22 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleFavouriteClick = (e) => {
|
handleFavouriteClick = (e) => {
|
||||||
if (me) {
|
const { signedIn } = this.context.identity;
|
||||||
|
|
||||||
|
if (signedIn) {
|
||||||
this.props.onFavourite(this.props.status, e);
|
this.props.onFavourite(this.props.status, e);
|
||||||
} else {
|
} 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);
|
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 = () => {
|
handleDeleteClick = () => {
|
||||||
this.props.onDelete(this.props.status, this.context.router.history);
|
this.props.onDelete(this.props.status, this.context.router.history);
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,7 +31,7 @@ const createIdentityContext = state => ({
|
||||||
signedIn: !!state.meta.me,
|
signedIn: !!state.meta.me,
|
||||||
accountId: state.meta.me,
|
accountId: state.meta.me,
|
||||||
accessToken: state.meta.access_token,
|
accessToken: state.meta.access_token,
|
||||||
permissions: state.role.permissions,
|
permissions: state.role ? state.role.permissions : 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default class Mastodon extends React.PureComponent {
|
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));
|
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Status));
|
||||||
|
|
|
@ -3,7 +3,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
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 { preferencesLink, profileLink, accountAdminLink } from 'flavours/glitch/util/backend_links';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import Icon from 'flavours/glitch/components/icon';
|
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 DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container';
|
||||||
import AccountNoteContainer from '../containers/account_note_container';
|
import AccountNoteContainer from '../containers/account_note_container';
|
||||||
import { PERMISSION_MANAGE_USERS } from 'flavours/glitch/permissions';
|
import { PERMISSION_MANAGE_USERS } from 'flavours/glitch/permissions';
|
||||||
|
import { Helmet } from 'react-helmet';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
|
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
|
||||||
|
@ -54,6 +55,14 @@ const messages = defineMessages({
|
||||||
languages: { id: 'account.languages', defaultMessage: 'Change subscribed languages' },
|
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 = {
|
const dateFormatOptions = {
|
||||||
month: 'short',
|
month: 'short',
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
|
@ -87,6 +96,7 @@ class Header extends ImmutablePureComponent {
|
||||||
onAddToList: PropTypes.func.isRequired,
|
onAddToList: PropTypes.func.isRequired,
|
||||||
onEditAccountNote: PropTypes.func.isRequired,
|
onEditAccountNote: PropTypes.func.isRequired,
|
||||||
onChangeLanguages: PropTypes.func.isRequired,
|
onChangeLanguages: PropTypes.func.isRequired,
|
||||||
|
onInteractionModal: PropTypes.func.isRequired,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
domain: PropTypes.string.isRequired,
|
domain: PropTypes.string.isRequired,
|
||||||
hidden: PropTypes.bool,
|
hidden: PropTypes.bool,
|
||||||
|
@ -124,6 +134,7 @@ class Header extends ImmutablePureComponent {
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { account, hidden, intl, domain } = this.props;
|
const { account, hidden, intl, domain } = this.props;
|
||||||
|
const { signedIn } = this.context.identity;
|
||||||
|
|
||||||
if (!account) {
|
if (!account) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -157,12 +168,12 @@ class Header extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (me !== account.get('id')) {
|
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 = '';
|
actionBtn = '';
|
||||||
} else if (account.getIn(['relationship', 'requested'])) {
|
} 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} />;
|
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'])) {
|
} 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'])) {
|
} else if (account.getIn(['relationship', 'blocking'])) {
|
||||||
actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.props.onBlock} />;
|
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)} />;
|
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.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({ text: intl.formatMessage(messages.direct, { name: account.get('username') }), action: this.props.onDirect });
|
||||||
menu.push(null);
|
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.mutes), to: '/mutes' });
|
||||||
menu.push({ text: intl.formatMessage(messages.blocks), to: '/blocks' });
|
menu.push({ text: intl.formatMessage(messages.blocks), to: '/blocks' });
|
||||||
menu.push({ text: intl.formatMessage(messages.domain_blocks), to: '/domain_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', 'following'])) {
|
||||||
if (!account.getIn(['relationship', 'muting'])) {
|
if (!account.getIn(['relationship', 'muting'])) {
|
||||||
if (account.getIn(['relationship', 'showing_reblogs'])) {
|
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 });
|
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];
|
const domain = account.get('acct').split('@')[1];
|
||||||
|
|
||||||
menu.push(null);
|
menu.push(null);
|
||||||
|
@ -301,7 +312,7 @@ class Header extends ImmutablePureComponent {
|
||||||
</React.Fragment>
|
</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>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -313,7 +324,7 @@ class Header extends ImmutablePureComponent {
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AccountNoteContainer account={account} />
|
{signedIn && <AccountNoteContainer account={account} />}
|
||||||
|
|
||||||
{!(suspended || hidden) && (
|
{!(suspended || hidden) && (
|
||||||
<div className='account__header__extra'>
|
<div className='account__header__extra'>
|
||||||
|
@ -339,6 +350,10 @@ class Header extends ImmutablePureComponent {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Helmet>
|
||||||
|
<title>{titleFromAccount(account)} - {title}</title>
|
||||||
|
</Helmet>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,7 @@ export default class Header extends ImmutablePureComponent {
|
||||||
onEndorseToggle: PropTypes.func.isRequired,
|
onEndorseToggle: PropTypes.func.isRequired,
|
||||||
onAddToList: PropTypes.func.isRequired,
|
onAddToList: PropTypes.func.isRequired,
|
||||||
onChangeLanguages: PropTypes.func.isRequired,
|
onChangeLanguages: PropTypes.func.isRequired,
|
||||||
|
onInteractionModal: PropTypes.func.isRequired,
|
||||||
hideTabs: PropTypes.bool,
|
hideTabs: PropTypes.bool,
|
||||||
domain: PropTypes.string.isRequired,
|
domain: PropTypes.string.isRequired,
|
||||||
hidden: PropTypes.bool,
|
hidden: PropTypes.bool,
|
||||||
|
@ -97,6 +98,10 @@ export default class Header extends ImmutablePureComponent {
|
||||||
this.props.onChangeLanguages(this.props.account);
|
this.props.onChangeLanguages(this.props.account);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleInteractionModal = () => {
|
||||||
|
this.props.onInteractionModal(this.props.account);
|
||||||
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { account, hidden, hideTabs } = this.props;
|
const { account, hidden, hideTabs } = this.props;
|
||||||
|
|
||||||
|
@ -124,6 +129,7 @@ export default class Header extends ImmutablePureComponent {
|
||||||
onAddToList={this.handleAddToList}
|
onAddToList={this.handleAddToList}
|
||||||
onEditAccountNote={this.handleEditAccountNote}
|
onEditAccountNote={this.handleEditAccountNote}
|
||||||
onChangeLanguages={this.handleChangeLanguages}
|
onChangeLanguages={this.handleChangeLanguages}
|
||||||
|
onInteractionModal={this.handleInteractionModal}
|
||||||
domain={this.props.domain}
|
domain={this.props.domain}
|
||||||
hidden={hidden}
|
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) {
|
onBlock (account) {
|
||||||
if (account.getIn(['relationship', 'blocking'])) {
|
if (account.getIn(['relationship', 'blocking'])) {
|
||||||
dispatch(unblockAccount(account.get('id')));
|
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 { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns';
|
||||||
import ColumnSettingsContainer from './containers/column_settings_container';
|
import ColumnSettingsContainer from './containers/column_settings_container';
|
||||||
import { connectCommunityStream } from 'flavours/glitch/actions/streaming';
|
import { connectCommunityStream } from 'flavours/glitch/actions/streaming';
|
||||||
|
import { Helmet } from 'react-helmet';
|
||||||
|
import { title } from 'flavours/glitch/util/initial_state';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
title: { id: 'column.community', defaultMessage: 'Local timeline' },
|
title: { id: 'column.community', defaultMessage: 'Local timeline' },
|
||||||
|
@ -39,6 +41,7 @@ class CommunityTimeline extends React.PureComponent {
|
||||||
|
|
||||||
static contextTypes = {
|
static contextTypes = {
|
||||||
router: PropTypes.object,
|
router: PropTypes.object,
|
||||||
|
identity: PropTypes.object,
|
||||||
};
|
};
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
@ -72,20 +75,32 @@ class CommunityTimeline extends React.PureComponent {
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
const { dispatch, onlyMedia } = this.props;
|
const { dispatch, onlyMedia } = this.props;
|
||||||
|
const { signedIn } = this.context.identity;
|
||||||
|
|
||||||
dispatch(expandCommunityTimeline({ onlyMedia }));
|
dispatch(expandCommunityTimeline({ onlyMedia }));
|
||||||
|
|
||||||
|
if (signedIn) {
|
||||||
this.disconnect = dispatch(connectCommunityStream({ onlyMedia }));
|
this.disconnect = dispatch(connectCommunityStream({ onlyMedia }));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
componentDidUpdate (prevProps) {
|
componentDidUpdate (prevProps) {
|
||||||
|
const { signedIn } = this.context.identity;
|
||||||
|
|
||||||
if (prevProps.onlyMedia !== this.props.onlyMedia) {
|
if (prevProps.onlyMedia !== this.props.onlyMedia) {
|
||||||
const { dispatch, onlyMedia } = this.props;
|
const { dispatch, onlyMedia } = this.props;
|
||||||
|
|
||||||
|
if (this.disconnect) {
|
||||||
this.disconnect();
|
this.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
dispatch(expandCommunityTimeline({ onlyMedia }));
|
dispatch(expandCommunityTimeline({ onlyMedia }));
|
||||||
|
|
||||||
|
if (signedIn) {
|
||||||
this.disconnect = dispatch(connectCommunityStream({ onlyMedia }));
|
this.disconnect = dispatch(connectCommunityStream({ onlyMedia }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
componentWillUnmount () {
|
componentWillUnmount () {
|
||||||
if (this.disconnect) {
|
if (this.disconnect) {
|
||||||
|
@ -132,6 +147,10 @@ class CommunityTimeline extends React.PureComponent {
|
||||||
bindToDocument={!multiColumn}
|
bindToDocument={!multiColumn}
|
||||||
regex={this.props.regex}
|
regex={this.props.regex}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Helmet>
|
||||||
|
<title>{intl.formatMessage(messages.title)} - {title}</title>
|
||||||
|
</Helmet>
|
||||||
</Column>
|
</Column>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,8 @@ import RadioButton from 'flavours/glitch/components/radio_button';
|
||||||
import LoadMore from 'flavours/glitch/components/load_more';
|
import LoadMore from 'flavours/glitch/components/load_more';
|
||||||
import ScrollContainer from 'flavours/glitch/containers/scroll_container';
|
import ScrollContainer from 'flavours/glitch/containers/scroll_container';
|
||||||
import LoadingIndicator from 'flavours/glitch/components/loading_indicator';
|
import LoadingIndicator from 'flavours/glitch/components/loading_indicator';
|
||||||
|
import { title } from 'flavours/glitch/util/initial_state';
|
||||||
|
import { Helmet } from 'react-helmet';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
title: { id: 'column.directory', defaultMessage: 'Browse profiles' },
|
title: { id: 'column.directory', defaultMessage: 'Browse profiles' },
|
||||||
|
@ -165,6 +167,10 @@ class Directory extends React.PureComponent {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{multiColumn && !pinned ? <ScrollContainer scrollKey='directory'>{scrollableArea}</ScrollContainer> : scrollableArea}
|
{multiColumn && !pinned ? <ScrollContainer scrollKey='directory'>{scrollableArea}</ScrollContainer> : scrollableArea}
|
||||||
|
|
||||||
|
<Helmet>
|
||||||
|
<title>{intl.formatMessage(messages.title)} - {title}</title>
|
||||||
|
</Helmet>
|
||||||
</Column>
|
</Column>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,8 @@ import Suggestions from './suggestions';
|
||||||
import Search from 'flavours/glitch/features/compose/containers/search_container';
|
import Search from 'flavours/glitch/features/compose/containers/search_container';
|
||||||
import SearchResults from './results';
|
import SearchResults from './results';
|
||||||
import { showTrends } from 'flavours/glitch/util/initial_state';
|
import { showTrends } from 'flavours/glitch/util/initial_state';
|
||||||
|
import { Helmet } from 'react-helmet';
|
||||||
|
import { title } from 'flavours/glitch/util/initial_state';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
title: { id: 'explore.title', defaultMessage: 'Explore' },
|
title: { id: 'explore.title', defaultMessage: 'Explore' },
|
||||||
|
@ -29,13 +31,13 @@ class Explore extends React.PureComponent {
|
||||||
|
|
||||||
static contextTypes = {
|
static contextTypes = {
|
||||||
router: PropTypes.object,
|
router: PropTypes.object,
|
||||||
|
identity: PropTypes.object,
|
||||||
};
|
};
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
multiColumn: PropTypes.bool,
|
multiColumn: PropTypes.bool,
|
||||||
isSearching: PropTypes.bool,
|
isSearching: PropTypes.bool,
|
||||||
layout: PropTypes.string,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
handleHeaderClick = () => {
|
handleHeaderClick = () => {
|
||||||
|
@ -47,22 +49,21 @@ class Explore extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { intl, multiColumn, isSearching, layout } = this.props;
|
const { intl, multiColumn, isSearching } = this.props;
|
||||||
|
const { signedIn } = this.context.identity;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.title)}>
|
<Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.title)}>
|
||||||
{layout === 'mobile' ? (
|
|
||||||
<div className='explore__search-header'>
|
|
||||||
<Search />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<ColumnHeader
|
<ColumnHeader
|
||||||
icon={isSearching ? 'search' : 'hashtag'}
|
icon={isSearching ? 'search' : 'hashtag'}
|
||||||
title={intl.formatMessage(isSearching ? messages.searchResults : messages.title)}
|
title={intl.formatMessage(isSearching ? messages.searchResults : messages.title)}
|
||||||
onClick={this.handleHeaderClick}
|
onClick={this.handleHeaderClick}
|
||||||
multiColumn={multiColumn}
|
multiColumn={multiColumn}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
|
<div className='explore__search-header'>
|
||||||
|
<Search />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className='scrollable scrollable--flex'>
|
<div className='scrollable scrollable--flex'>
|
||||||
{isSearching ? (
|
{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'><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/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/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>
|
</div>
|
||||||
|
|
||||||
<Switch>
|
<Switch>
|
||||||
|
@ -82,6 +83,10 @@ class Explore extends React.PureComponent {
|
||||||
<Route path='/explore/suggestions' component={Suggestions} />
|
<Route path='/explore/suggestions' component={Suggestions} />
|
||||||
<Route exact path={['/explore', '/explore/posts', '/search']} component={Statuses} componentParams={{ multiColumn }} />
|
<Route exact path={['/explore', '/explore/posts', '/search']} component={Statuses} componentParams={{ multiColumn }} />
|
||||||
</Switch>
|
</Switch>
|
||||||
|
|
||||||
|
<Helmet>
|
||||||
|
<title>{intl.formatMessage(messages.title)} - {title}</title>
|
||||||
|
</Helmet>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -5,6 +5,7 @@ import Story from './components/story';
|
||||||
import LoadingIndicator from 'flavours/glitch/components/loading_indicator';
|
import LoadingIndicator from 'flavours/glitch/components/loading_indicator';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { fetchTrendingLinks } from 'flavours/glitch/actions/trends';
|
import { fetchTrendingLinks } from 'flavours/glitch/actions/trends';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
links: state.getIn(['trends', 'links', 'items']),
|
links: state.getIn(['trends', 'links', 'items']),
|
||||||
|
@ -28,6 +29,16 @@ class Links extends React.PureComponent {
|
||||||
render () {
|
render () {
|
||||||
const { isLoading, links } = this.props;
|
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 (
|
return (
|
||||||
<div className='explore__links'>
|
<div className='explore__links'>
|
||||||
{isLoading ? (<LoadingIndicator />) : links.map(link => (
|
{isLoading ? (<LoadingIndicator />) : links.map(link => (
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { injectIntl, defineMessages, FormattedMessage } from 'react-intl';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { expandSearch } from 'flavours/glitch/actions/search';
|
import { expandSearch } from 'flavours/glitch/actions/search';
|
||||||
import Account from 'flavours/glitch/containers/account_container';
|
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 { List as ImmutableList } from 'immutable';
|
||||||
import LoadMore from 'flavours/glitch/components/load_more';
|
import LoadMore from 'flavours/glitch/components/load_more';
|
||||||
import LoadingIndicator from 'flavours/glitch/components/loading_indicator';
|
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 => ({
|
const mapStateToProps = state => ({
|
||||||
isLoading: state.getIn(['search', 'isLoading']),
|
isLoading: state.getIn(['search', 'isLoading']),
|
||||||
results: state.getIn(['search', 'results']),
|
results: state.getIn(['search', 'results']),
|
||||||
|
q: state.getIn(['search', 'searchTerm']),
|
||||||
});
|
});
|
||||||
|
|
||||||
const appendLoadMore = (id, list, onLoadMore) => {
|
const appendLoadMore = (id, list, onLoadMore) => {
|
||||||
|
@ -37,6 +44,7 @@ const renderStatuses = (results, onLoadMore) => appendLoadMore('statuses', resul
|
||||||
)), onLoadMore);
|
)), onLoadMore);
|
||||||
|
|
||||||
export default @connect(mapStateToProps)
|
export default @connect(mapStateToProps)
|
||||||
|
@injectIntl
|
||||||
class Results extends React.PureComponent {
|
class Results extends React.PureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
@ -44,6 +52,8 @@ class Results extends React.PureComponent {
|
||||||
isLoading: PropTypes.bool,
|
isLoading: PropTypes.bool,
|
||||||
multiColumn: PropTypes.bool,
|
multiColumn: PropTypes.bool,
|
||||||
dispatch: PropTypes.func.isRequired,
|
dispatch: PropTypes.func.isRequired,
|
||||||
|
q: PropTypes.string,
|
||||||
|
intl: PropTypes.object,
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
|
@ -64,7 +74,7 @@ class Results extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { isLoading, results } = this.props;
|
const { intl, isLoading, q, results } = this.props;
|
||||||
const { type } = this.state;
|
const { type } = this.state;
|
||||||
|
|
||||||
let filteredResults = ImmutableList();
|
let filteredResults = ImmutableList();
|
||||||
|
@ -106,6 +116,10 @@ class Results extends React.PureComponent {
|
||||||
<div className='explore__search-results'>
|
<div className='explore__search-results'>
|
||||||
{isLoading ? <LoadingIndicator /> : filteredResults}
|
{isLoading ? <LoadingIndicator /> : filteredResults}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Helmet>
|
||||||
|
<title>{intl.formatMessage(messages.title, { q })} - {title}</title>
|
||||||
|
</Helmet>
|
||||||
</React.Fragment>
|
</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 LoadingIndicator from 'flavours/glitch/components/loading_indicator';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { fetchSuggestions, dismissSuggestion } from 'flavours/glitch/actions/suggestions';
|
import { fetchSuggestions, dismissSuggestion } from 'flavours/glitch/actions/suggestions';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
suggestions: state.getIn(['suggestions', 'items']),
|
suggestions: state.getIn(['suggestions', 'items']),
|
||||||
|
@ -33,6 +34,16 @@ class Suggestions extends React.PureComponent {
|
||||||
render () {
|
render () {
|
||||||
const { isLoading, suggestions } = this.props;
|
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 (
|
return (
|
||||||
<div className='explore__suggestions'>
|
<div className='explore__suggestions'>
|
||||||
{isLoading ? <LoadingIndicator /> : suggestions.map(suggestion => (
|
{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 LoadingIndicator from 'flavours/glitch/components/loading_indicator';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { fetchTrendingHashtags } from 'flavours/glitch/actions/trends';
|
import { fetchTrendingHashtags } from 'flavours/glitch/actions/trends';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
hashtags: state.getIn(['trends', 'tags', 'items']),
|
hashtags: state.getIn(['trends', 'tags', 'items']),
|
||||||
|
@ -28,6 +29,16 @@ class Tags extends React.PureComponent {
|
||||||
render () {
|
render () {
|
||||||
const { isLoading, hashtags } = this.props;
|
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 (
|
return (
|
||||||
<div className='explore__links'>
|
<div className='explore__links'>
|
||||||
{isLoading ? (<LoadingIndicator />) : hashtags.map(hashtag => (
|
{isLoading ? (<LoadingIndicator />) : hashtags.map(hashtag => (
|
||||||
|
|
|
@ -10,7 +10,6 @@ import { requestBrowserPermission } from 'flavours/glitch/actions/notifications'
|
||||||
import { markAsPartial } from 'flavours/glitch/actions/timelines';
|
import { markAsPartial } from 'flavours/glitch/actions/timelines';
|
||||||
import Column from 'flavours/glitch/features/ui/components/column';
|
import Column from 'flavours/glitch/features/ui/components/column';
|
||||||
import Account from './components/account';
|
import Account from './components/account';
|
||||||
import Logo from 'flavours/glitch/components/logo';
|
|
||||||
import imageGreeting from 'mastodon/../images/elephant_ui_greeting.svg';
|
import imageGreeting from 'mastodon/../images/elephant_ui_greeting.svg';
|
||||||
import Button from 'flavours/glitch/components/button';
|
import Button from 'flavours/glitch/components/button';
|
||||||
|
|
||||||
|
@ -78,7 +77,10 @@ class FollowRecommendations extends ImmutablePureComponent {
|
||||||
<Column>
|
<Column>
|
||||||
<div className='scrollable follow-recommendations-container'>
|
<div className='scrollable follow-recommendations-container'>
|
||||||
<div className='column-title'>
|
<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>
|
<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>
|
<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>
|
</div>
|
||||||
|
|
|
@ -14,6 +14,8 @@ import { isEqual } from 'lodash';
|
||||||
import { fetchHashtag, followHashtag, unfollowHashtag } from 'flavours/glitch/actions/tags';
|
import { fetchHashtag, followHashtag, unfollowHashtag } from 'flavours/glitch/actions/tags';
|
||||||
import Icon from 'flavours/glitch/components/icon';
|
import Icon from 'flavours/glitch/components/icon';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import { title } from 'flavours/glitch/util/initial_state';
|
||||||
|
import { Helmet } from 'react-helmet';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
followHashtag: { id: 'hashtag.follow', defaultMessage: 'Follow hashtag' },
|
followHashtag: { id: 'hashtag.follow', defaultMessage: 'Follow hashtag' },
|
||||||
|
@ -31,6 +33,10 @@ class HashtagTimeline extends React.PureComponent {
|
||||||
|
|
||||||
disconnects = [];
|
disconnects = [];
|
||||||
|
|
||||||
|
static contextTypes = {
|
||||||
|
identity: PropTypes.object,
|
||||||
|
};
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
params: PropTypes.object.isRequired,
|
params: PropTypes.object.isRequired,
|
||||||
columnId: PropTypes.string,
|
columnId: PropTypes.string,
|
||||||
|
@ -90,6 +96,12 @@ class HashtagTimeline extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
_subscribe (dispatch, id, tags = {}, local) {
|
_subscribe (dispatch, id, tags = {}, local) {
|
||||||
|
const { signedIn } = this.context.identity;
|
||||||
|
|
||||||
|
if (!signedIn) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let any = (tags.any || []).map(tag => tag.value);
|
let any = (tags.any || []).map(tag => tag.value);
|
||||||
let all = (tags.all || []).map(tag => tag.value);
|
let all = (tags.all || []).map(tag => tag.value);
|
||||||
let none = (tags.none || []).map(tag => tag.value);
|
let none = (tags.none || []).map(tag => tag.value);
|
||||||
|
@ -158,6 +170,11 @@ class HashtagTimeline extends React.PureComponent {
|
||||||
handleFollow = () => {
|
handleFollow = () => {
|
||||||
const { dispatch, params, tag } = this.props;
|
const { dispatch, params, tag } = this.props;
|
||||||
const { id } = params;
|
const { id } = params;
|
||||||
|
const { signedIn } = this.context.identity;
|
||||||
|
|
||||||
|
if (!signedIn) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (tag.get('following')) {
|
if (tag.get('following')) {
|
||||||
dispatch(unfollowHashtag(id));
|
dispatch(unfollowHashtag(id));
|
||||||
|
@ -170,6 +187,7 @@ class HashtagTimeline extends React.PureComponent {
|
||||||
const { hasUnread, columnId, multiColumn, tag, intl } = this.props;
|
const { hasUnread, columnId, multiColumn, tag, intl } = this.props;
|
||||||
const { id, local } = this.props.params;
|
const { id, local } = this.props.params;
|
||||||
const pinned = !!columnId;
|
const pinned = !!columnId;
|
||||||
|
const { signedIn } = this.context.identity;
|
||||||
|
|
||||||
let followButton;
|
let followButton;
|
||||||
|
|
||||||
|
@ -177,7 +195,7 @@ class HashtagTimeline extends React.PureComponent {
|
||||||
const following = tag.get('following');
|
const following = tag.get('following');
|
||||||
|
|
||||||
followButton = (
|
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' />
|
<Icon id={following ? 'user-times' : 'user-plus'} fixedWidth className='column-header__icon' />
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
@ -208,6 +226,10 @@ class HashtagTimeline extends React.PureComponent {
|
||||||
emptyMessage={<FormattedMessage id='empty_column.hashtag' defaultMessage='There is nothing in this hashtag yet.' />}
|
emptyMessage={<FormattedMessage id='empty_column.hashtag' defaultMessage='There is nothing in this hashtag yet.' />}
|
||||||
bindToDocument={!multiColumn}
|
bindToDocument={!multiColumn}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Helmet>
|
||||||
|
<title>{`#${id}`} - {title}</title>
|
||||||
|
</Helmet>
|
||||||
</Column>
|
</Column>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,9 @@ import { fetchAnnouncements, toggleShowAnnouncements } from 'flavours/glitch/act
|
||||||
import AnnouncementsContainer from 'flavours/glitch/features/getting_started/containers/announcements_container';
|
import AnnouncementsContainer from 'flavours/glitch/features/getting_started/containers/announcements_container';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import IconWithBadge from 'flavours/glitch/components/icon_with_badge';
|
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({
|
const messages = defineMessages({
|
||||||
title: { id: 'column.home', defaultMessage: 'Home' },
|
title: { id: 'column.home', defaultMessage: 'Home' },
|
||||||
|
@ -33,6 +36,10 @@ export default @connect(mapStateToProps)
|
||||||
@injectIntl
|
@injectIntl
|
||||||
class HomeTimeline extends React.PureComponent {
|
class HomeTimeline extends React.PureComponent {
|
||||||
|
|
||||||
|
static contextTypes = {
|
||||||
|
identity: PropTypes.object,
|
||||||
|
};
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
dispatch: PropTypes.func.isRequired,
|
dispatch: PropTypes.func.isRequired,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
|
@ -115,6 +122,7 @@ class HomeTimeline extends React.PureComponent {
|
||||||
render () {
|
render () {
|
||||||
const { intl, hasUnread, columnId, multiColumn, hasAnnouncements, unreadAnnouncements, showAnnouncements } = this.props;
|
const { intl, hasUnread, columnId, multiColumn, hasAnnouncements, unreadAnnouncements, showAnnouncements } = this.props;
|
||||||
const pinned = !!columnId;
|
const pinned = !!columnId;
|
||||||
|
const { signedIn } = this.context.identity;
|
||||||
|
|
||||||
let announcementsButton = null;
|
let announcementsButton = null;
|
||||||
|
|
||||||
|
@ -149,6 +157,7 @@ class HomeTimeline extends React.PureComponent {
|
||||||
<ColumnSettingsContainer />
|
<ColumnSettingsContainer />
|
||||||
</ColumnHeader>
|
</ColumnHeader>
|
||||||
|
|
||||||
|
{signedIn ? (
|
||||||
<StatusListContainer
|
<StatusListContainer
|
||||||
trackScroll={!pinned}
|
trackScroll={!pinned}
|
||||||
scrollKey={`home_timeline-${columnId}`}
|
scrollKey={`home_timeline-${columnId}`}
|
||||||
|
@ -158,6 +167,11 @@ class HomeTimeline extends React.PureComponent {
|
||||||
bindToDocument={!multiColumn}
|
bindToDocument={!multiColumn}
|
||||||
regex={this.props.regex}
|
regex={this.props.regex}
|
||||||
/>
|
/>
|
||||||
|
) : <NotSignedInIndicator />}
|
||||||
|
|
||||||
|
<Helmet>
|
||||||
|
<title>{intl.formatMessage(messages.title)} - {title}</title>
|
||||||
|
</Helmet>
|
||||||
</Column>
|
</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 Icon from 'flavours/glitch/components/icon';
|
||||||
import compareId from 'flavours/glitch/util/compare_id';
|
import compareId from 'flavours/glitch/util/compare_id';
|
||||||
import NotificationsPermissionBanner from './components/notifications_permission_banner';
|
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';
|
import NotificationPurgeButtonsContainer from 'flavours/glitch/containers/notification_purge_buttons_container';
|
||||||
|
|
||||||
|
@ -94,6 +97,10 @@ export default @connect(mapStateToProps, mapDispatchToProps)
|
||||||
@injectIntl
|
@injectIntl
|
||||||
class Notifications extends React.PureComponent {
|
class Notifications extends React.PureComponent {
|
||||||
|
|
||||||
|
static contextTypes = {
|
||||||
|
identity: PropTypes.object,
|
||||||
|
};
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
columnId: PropTypes.string,
|
columnId: PropTypes.string,
|
||||||
notifications: ImmutablePropTypes.list.isRequired,
|
notifications: ImmutablePropTypes.list.isRequired,
|
||||||
|
@ -224,10 +231,11 @@ class Notifications extends React.PureComponent {
|
||||||
const { animatingNCD } = this.state;
|
const { animatingNCD } = this.state;
|
||||||
const pinned = !!columnId;
|
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 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;
|
let scrollableContent = null;
|
||||||
|
|
||||||
const filterBarContainer = showFilterBar
|
const filterBarContainer = (signedIn && showFilterBar)
|
||||||
? (<FilterBarContainer />)
|
? (<FilterBarContainer />)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
@ -257,7 +265,10 @@ class Notifications extends React.PureComponent {
|
||||||
|
|
||||||
this.scrollableContent = scrollableContent;
|
this.scrollableContent = scrollableContent;
|
||||||
|
|
||||||
const scrollContainer = (
|
let scrollContainer;
|
||||||
|
|
||||||
|
if (signedIn) {
|
||||||
|
scrollContainer = (
|
||||||
<ScrollableList
|
<ScrollableList
|
||||||
scrollKey={`notifications-${columnId}`}
|
scrollKey={`notifications-${columnId}`}
|
||||||
trackScroll={!pinned}
|
trackScroll={!pinned}
|
||||||
|
@ -277,6 +288,9 @@ class Notifications extends React.PureComponent {
|
||||||
{scrollableContent}
|
{scrollableContent}
|
||||||
</ScrollableList>
|
</ScrollableList>
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
scrollContainer = <NotSignedInIndicator />;
|
||||||
|
}
|
||||||
|
|
||||||
const extraButtons = [];
|
const extraButtons = [];
|
||||||
|
|
||||||
|
@ -354,8 +368,13 @@ class Notifications extends React.PureComponent {
|
||||||
>
|
>
|
||||||
<ColumnSettingsContainer />
|
<ColumnSettingsContainer />
|
||||||
</ColumnHeader>
|
</ColumnHeader>
|
||||||
|
|
||||||
{filterBarContainer}
|
{filterBarContainer}
|
||||||
{scrollContainer}
|
{scrollContainer}
|
||||||
|
|
||||||
|
<Helmet>
|
||||||
|
<title>{intl.formatMessage(messages.title)} - {title}</title>
|
||||||
|
</Helmet>
|
||||||
</Column>
|
</Column>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,6 +44,7 @@ class Footer extends ImmutablePureComponent {
|
||||||
|
|
||||||
static contextTypes = {
|
static contextTypes = {
|
||||||
router: PropTypes.object,
|
router: PropTypes.object,
|
||||||
|
identity: PropTypes.object,
|
||||||
};
|
};
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
@ -69,8 +70,10 @@ class Footer extends ImmutablePureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
handleReplyClick = () => {
|
handleReplyClick = () => {
|
||||||
const { dispatch, askReplyConfirmation, intl } = this.props;
|
const { dispatch, askReplyConfirmation, status, intl } = this.props;
|
||||||
|
const { signedIn } = this.context.identity;
|
||||||
|
|
||||||
|
if (signedIn) {
|
||||||
if (askReplyConfirmation) {
|
if (askReplyConfirmation) {
|
||||||
dispatch(openModal('CONFIRM', {
|
dispatch(openModal('CONFIRM', {
|
||||||
message: intl.formatMessage(messages.replyMessage),
|
message: intl.formatMessage(messages.replyMessage),
|
||||||
|
@ -80,16 +83,32 @@ class Footer extends ImmutablePureComponent {
|
||||||
} else {
|
} else {
|
||||||
this._performReply();
|
this._performReply();
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
dispatch(openModal('INTERACTION', {
|
||||||
|
type: 'reply',
|
||||||
|
accountId: status.getIn(['account', 'id']),
|
||||||
|
url: status.get('url'),
|
||||||
|
}));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
handleFavouriteClick = () => {
|
handleFavouriteClick = () => {
|
||||||
const { dispatch, status } = this.props;
|
const { dispatch, status } = this.props;
|
||||||
|
const { signedIn } = this.context.identity;
|
||||||
|
|
||||||
|
if (signedIn) {
|
||||||
if (status.get('favourited')) {
|
if (status.get('favourited')) {
|
||||||
dispatch(unfavourite(status));
|
dispatch(unfavourite(status));
|
||||||
} else {
|
} else {
|
||||||
dispatch(favourite(status));
|
dispatch(favourite(status));
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
dispatch(openModal('INTERACTION', {
|
||||||
|
type: 'favourite',
|
||||||
|
accountId: status.getIn(['account', 'id']),
|
||||||
|
url: status.get('url'),
|
||||||
|
}));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_performReblog = (privacy) => {
|
_performReblog = (privacy) => {
|
||||||
|
@ -99,7 +118,9 @@ class Footer extends ImmutablePureComponent {
|
||||||
|
|
||||||
handleReblogClick = e => {
|
handleReblogClick = e => {
|
||||||
const { dispatch, status } = this.props;
|
const { dispatch, status } = this.props;
|
||||||
|
const { signedIn } = this.context.identity;
|
||||||
|
|
||||||
|
if (signedIn) {
|
||||||
if (status.get('reblogged')) {
|
if (status.get('reblogged')) {
|
||||||
dispatch(unreblog(status));
|
dispatch(unreblog(status));
|
||||||
} else if ((e && e.shiftKey) || !boostModal) {
|
} else if ((e && e.shiftKey) || !boostModal) {
|
||||||
|
@ -107,6 +128,13 @@ class Footer extends ImmutablePureComponent {
|
||||||
} else {
|
} else {
|
||||||
dispatch(initBoostModal({ status, onReblog: this._performReblog }));
|
dispatch(initBoostModal({ status, onReblog: this._performReblog }));
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
dispatch(openModal('INTERACTION', {
|
||||||
|
type: 'reblog',
|
||||||
|
accountId: status.getIn(['account', 'id']),
|
||||||
|
url: status.get('url'),
|
||||||
|
}));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
handleOpenClick = e => {
|
handleOpenClick = e => {
|
||||||
|
|
|
@ -9,6 +9,8 @@ import { expandPublicTimeline } from 'flavours/glitch/actions/timelines';
|
||||||
import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns';
|
import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns';
|
||||||
import ColumnSettingsContainer from './containers/column_settings_container';
|
import ColumnSettingsContainer from './containers/column_settings_container';
|
||||||
import { connectPublicStream } from 'flavours/glitch/actions/streaming';
|
import { connectPublicStream } from 'flavours/glitch/actions/streaming';
|
||||||
|
import { Helmet } from 'react-helmet';
|
||||||
|
import { title } from 'flavours/glitch/util/initial_state';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
title: { id: 'column.public', defaultMessage: 'Federated timeline' },
|
title: { id: 'column.public', defaultMessage: 'Federated timeline' },
|
||||||
|
@ -43,6 +45,7 @@ class PublicTimeline extends React.PureComponent {
|
||||||
|
|
||||||
static contextTypes = {
|
static contextTypes = {
|
||||||
router: PropTypes.object,
|
router: PropTypes.object,
|
||||||
|
identity: PropTypes.object,
|
||||||
};
|
};
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
@ -78,20 +81,31 @@ class PublicTimeline extends React.PureComponent {
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
const { dispatch, onlyMedia, onlyRemote, allowLocalOnly } = this.props;
|
const { dispatch, onlyMedia, onlyRemote, allowLocalOnly } = this.props;
|
||||||
|
const { signedIn } = this.context.identity;
|
||||||
|
|
||||||
dispatch(expandPublicTimeline({ onlyMedia, onlyRemote, allowLocalOnly }));
|
dispatch(expandPublicTimeline({ onlyMedia, onlyRemote, allowLocalOnly }));
|
||||||
|
if (signedIn) {
|
||||||
this.disconnect = dispatch(connectPublicStream({ onlyMedia, onlyRemote, allowLocalOnly }));
|
this.disconnect = dispatch(connectPublicStream({ onlyMedia, onlyRemote, allowLocalOnly }));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
componentDidUpdate (prevProps) {
|
componentDidUpdate (prevProps) {
|
||||||
|
const { signedIn } = this.context.identity;
|
||||||
|
|
||||||
if (prevProps.onlyMedia !== this.props.onlyMedia || prevProps.onlyRemote !== this.props.onlyRemote || prevProps.allowLocalOnly !== this.props.allowLocalOnly) {
|
if (prevProps.onlyMedia !== this.props.onlyMedia || prevProps.onlyRemote !== this.props.onlyRemote || prevProps.allowLocalOnly !== this.props.allowLocalOnly) {
|
||||||
const { dispatch, onlyMedia, onlyRemote, allowLocalOnly } = this.props;
|
const { dispatch, onlyMedia, onlyRemote, allowLocalOnly } = this.props;
|
||||||
|
|
||||||
|
if (this.disconnect) {
|
||||||
this.disconnect();
|
this.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
dispatch(expandPublicTimeline({ onlyMedia, onlyRemote, allowLocalOnly }));
|
dispatch(expandPublicTimeline({ onlyMedia, onlyRemote, allowLocalOnly }));
|
||||||
|
|
||||||
|
if (signedIn) {
|
||||||
this.disconnect = dispatch(connectPublicStream({ onlyMedia, onlyRemote, allowLocalOnly }));
|
this.disconnect = dispatch(connectPublicStream({ onlyMedia, onlyRemote, allowLocalOnly }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
componentWillUnmount () {
|
componentWillUnmount () {
|
||||||
if (this.disconnect) {
|
if (this.disconnect) {
|
||||||
|
@ -138,6 +152,10 @@ class PublicTimeline extends React.PureComponent {
|
||||||
bindToDocument={!multiColumn}
|
bindToDocument={!multiColumn}
|
||||||
regex={this.props.regex}
|
regex={this.props.regex}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Helmet>
|
||||||
|
<title>{intl.formatMessage(messages.title)} - {title}</title>
|
||||||
|
</Helmet>
|
||||||
</Column>
|
</Column>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@ const messages = defineMessages({
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
rules: state.get('rules'),
|
rules: state.getIn(['server', 'rules']),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default @connect(mapStateToProps)
|
export default @connect(mapStateToProps)
|
||||||
|
|
|
@ -7,7 +7,7 @@ import Button from 'flavours/glitch/components/button';
|
||||||
import Option from './components/option';
|
import Option from './components/option';
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
rules: state.get('rules'),
|
rules: state.getIn(['server', 'rules']),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default @connect(mapStateToProps)
|
export default @connect(mapStateToProps)
|
||||||
|
|
|
@ -152,6 +152,7 @@ class ActionBar extends React.PureComponent {
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { status, intl } = this.props;
|
const { status, intl } = this.props;
|
||||||
|
const { signedIn, permissions } = this.context.identity;
|
||||||
|
|
||||||
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
|
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
|
||||||
const pinnableStatus = ['public', 'unlisted', 'private'].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.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.block, { name: status.getIn(['account', 'username']) }), action: this.handleBlockClick });
|
||||||
menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport });
|
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);
|
menu.push(null);
|
||||||
if (accountAdminLink !== undefined) {
|
if (accountAdminLink !== undefined) {
|
||||||
menu.push({
|
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={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>
|
<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}
|
{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'>
|
<div className='detailed-status__action-bar-dropdown'>
|
||||||
<DropdownMenuContainer size={18} icon='ellipsis-h' items={menu} direction='left' title={intl.formatMessage(messages.more)} />
|
<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 { defineMessages, injectIntl } from 'react-intl';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import { HotKeys } from 'react-hotkeys';
|
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 { attachFullscreenListener, detachFullscreenListener, isFullscreen } from 'flavours/glitch/util/fullscreen';
|
||||||
import { autoUnfoldCW } from 'flavours/glitch/util/content_warning';
|
import { autoUnfoldCW } from 'flavours/glitch/util/content_warning';
|
||||||
import { textForScreenReader, defaultMediaVisibility } from 'flavours/glitch/components/status';
|
import { textForScreenReader, defaultMediaVisibility } from 'flavours/glitch/components/status';
|
||||||
import Icon from 'flavours/glitch/components/icon';
|
import Icon from 'flavours/glitch/components/icon';
|
||||||
|
import { Helmet } from 'react-helmet';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
|
deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
|
||||||
|
@ -147,12 +148,30 @@ const makeMapStateToProps = () => {
|
||||||
return mapStateToProps;
|
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
|
export default @injectIntl
|
||||||
@connect(makeMapStateToProps)
|
@connect(makeMapStateToProps)
|
||||||
class Status extends ImmutablePureComponent {
|
class Status extends ImmutablePureComponent {
|
||||||
|
|
||||||
static contextTypes = {
|
static contextTypes = {
|
||||||
router: PropTypes.object,
|
router: PropTypes.object,
|
||||||
|
identity: PropTypes.object,
|
||||||
};
|
};
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
@ -245,15 +264,26 @@ class Status extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleFavouriteClick = (status, e) => {
|
handleFavouriteClick = (status, e) => {
|
||||||
|
const { dispatch } = this.props;
|
||||||
|
const { signedIn } = this.context.identity;
|
||||||
|
|
||||||
|
if (signedIn) {
|
||||||
if (status.get('favourited')) {
|
if (status.get('favourited')) {
|
||||||
this.props.dispatch(unfavourite(status));
|
dispatch(unfavourite(status));
|
||||||
} else {
|
} else {
|
||||||
if ((e && e.shiftKey) || !favouriteModal) {
|
if ((e && e.shiftKey) || !favouriteModal) {
|
||||||
this.handleModalFavourite(status);
|
this.handleModalFavourite(status);
|
||||||
} else {
|
} 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) => {
|
handlePin = (status) => {
|
||||||
|
@ -265,7 +295,10 @@ class Status extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleReplyClick = (status) => {
|
handleReplyClick = (status) => {
|
||||||
let { askReplyConfirmation, dispatch, intl } = this.props;
|
const { askReplyConfirmation, dispatch, intl } = this.props;
|
||||||
|
const { signedIn } = this.context.identity;
|
||||||
|
|
||||||
|
if (signedIn) {
|
||||||
if (askReplyConfirmation) {
|
if (askReplyConfirmation) {
|
||||||
dispatch(openModal('CONFIRM', {
|
dispatch(openModal('CONFIRM', {
|
||||||
message: intl.formatMessage(messages.replyMessage),
|
message: intl.formatMessage(messages.replyMessage),
|
||||||
|
@ -276,6 +309,13 @@ class Status extends ImmutablePureComponent {
|
||||||
} else {
|
} else {
|
||||||
dispatch(replyCompose(status, this.context.router.history));
|
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) => {
|
handleModalReblog = (status, privacy) => {
|
||||||
|
@ -290,7 +330,9 @@ class Status extends ImmutablePureComponent {
|
||||||
|
|
||||||
handleReblogClick = (status, e) => {
|
handleReblogClick = (status, e) => {
|
||||||
const { settings, dispatch } = this.props;
|
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')) {
|
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 }));
|
dispatch(initBoostModal({ status, onReblog: this.handleModalReblog, missingMediaDescription: true }));
|
||||||
} else if ((e && e.shiftKey) || !boostModal) {
|
} else if ((e && e.shiftKey) || !boostModal) {
|
||||||
|
@ -298,6 +340,13 @@ class Status extends ImmutablePureComponent {
|
||||||
} else {
|
} else {
|
||||||
dispatch(initBoostModal({ status, onReblog: this.handleModalReblog }));
|
dispatch(initBoostModal({ status, onReblog: this.handleModalReblog }));
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
dispatch(openModal('INTERACTION', {
|
||||||
|
type: 'reblog',
|
||||||
|
accountId: status.getIn(['account', 'id']),
|
||||||
|
url: status.get('url'),
|
||||||
|
}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleBookmarkClick = (status) => {
|
handleBookmarkClick = (status) => {
|
||||||
|
@ -633,6 +682,10 @@ class Status extends ImmutablePureComponent {
|
||||||
{descendants}
|
{descendants}
|
||||||
</div>
|
</div>
|
||||||
</ScrollContainer>
|
</ScrollContainer>
|
||||||
|
|
||||||
|
<Helmet>
|
||||||
|
<title>{titleFromStatus(status)} - {title}</title>
|
||||||
|
</Helmet>
|
||||||
</Column>
|
</Column>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,6 +60,7 @@ class ColumnsArea extends ImmutablePureComponent {
|
||||||
|
|
||||||
static contextTypes = {
|
static contextTypes = {
|
||||||
router: PropTypes.object.isRequired,
|
router: PropTypes.object.isRequired,
|
||||||
|
identity: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
@ -213,11 +214,12 @@ class ColumnsArea extends ImmutablePureComponent {
|
||||||
render () {
|
render () {
|
||||||
const { columns, children, singleColumn, intl, navbarUnder, openSettings } = this.props;
|
const { columns, children, singleColumn, intl, navbarUnder, openSettings } = this.props;
|
||||||
const { shouldAnimate, renderComposePanel } = this.state;
|
const { shouldAnimate, renderComposePanel } = this.state;
|
||||||
|
const { signedIn } = this.context.identity;
|
||||||
|
|
||||||
const columnIndex = getIndex(this.context.router.history.location.pathname);
|
const columnIndex = getIndex(this.context.router.history.location.pathname);
|
||||||
|
|
||||||
if (singleColumn) {
|
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 ? (
|
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}>
|
<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 React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import SearchContainer from 'flavours/glitch/features/compose/containers/search_container';
|
import SearchContainer from 'flavours/glitch/features/compose/containers/search_container';
|
||||||
import ComposeFormContainer from 'flavours/glitch/features/compose/containers/compose_form_container';
|
import ComposeFormContainer from 'flavours/glitch/features/compose/containers/compose_form_container';
|
||||||
import NavigationContainer from 'flavours/glitch/features/compose/containers/navigation_container';
|
import NavigationContainer from 'flavours/glitch/features/compose/containers/navigation_container';
|
||||||
import LinkFooter from './link_footer';
|
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'>
|
<div className='compose-panel'>
|
||||||
<SearchContainer openInRoute />
|
<SearchContainer openInRoute />
|
||||||
|
|
||||||
|
{!signedIn && (
|
||||||
|
<React.Fragment>
|
||||||
|
<ServerBanner />
|
||||||
|
<div className='flex-spacer' />
|
||||||
|
</React.Fragment>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{signedIn && (
|
||||||
|
<React.Fragment>
|
||||||
<NavigationContainer />
|
<NavigationContainer />
|
||||||
<ComposeFormContainer singleColumn />
|
<ComposeFormContainer singleColumn />
|
||||||
|
</React.Fragment>
|
||||||
|
)}
|
||||||
|
|
||||||
<LinkFooter withHotkeys />
|
<LinkFooter withHotkeys />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default ComposePanel;
|
};
|
||||||
|
|
|
@ -34,6 +34,7 @@ class LinkFooter extends React.PureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
withHotkeys: PropTypes.bool,
|
||||||
onLogout: PropTypes.func.isRequired,
|
onLogout: PropTypes.func.isRequired,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
@ -48,18 +49,53 @@ class LinkFooter extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
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 (
|
return (
|
||||||
<div className='getting-started__footer'>
|
<div className='getting-started__footer'>
|
||||||
<ul>
|
<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>}
|
{items.map((item, index, array) => (
|
||||||
{!!securityLink && <li><a href='/auth/edit'><FormattedMessage id='getting_started.security' defaultMessage='Security' /></a> · </li>}
|
<li>{item} { index === array.length - 1 ? null : ' · ' }</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>
|
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
|
|
|
@ -16,6 +16,7 @@ import ConfirmationModal from './confirmation_modal';
|
||||||
import SubscribedLanguagesModal from 'flavours/glitch/features/subscribed_languages_modal';
|
import SubscribedLanguagesModal from 'flavours/glitch/features/subscribed_languages_modal';
|
||||||
import FocalPointModal from './focal_point_modal';
|
import FocalPointModal from './focal_point_modal';
|
||||||
import DeprecatedSettingsModal from './deprecated_settings_modal';
|
import DeprecatedSettingsModal from './deprecated_settings_modal';
|
||||||
|
import InteractionModal from 'flavours/glitch/features/interaction_modal';
|
||||||
import {
|
import {
|
||||||
OnboardingModal,
|
OnboardingModal,
|
||||||
MuteModal,
|
MuteModal,
|
||||||
|
@ -53,6 +54,7 @@ const MODAL_COMPONENTS = {
|
||||||
'COMPARE_HISTORY': CompareHistoryModal,
|
'COMPARE_HISTORY': CompareHistoryModal,
|
||||||
'FILTER': FilterModal,
|
'FILTER': FilterModal,
|
||||||
'SUBSCRIBED_LANGUAGES': () => Promise.resolve({ default: SubscribedLanguagesModal }),
|
'SUBSCRIBED_LANGUAGES': () => Promise.resolve({ default: SubscribedLanguagesModal }),
|
||||||
|
'INTERACTION': () => Promise.resolve({ default: InteractionModal }),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class ModalRoot extends React.PureComponent {
|
export default class ModalRoot extends React.PureComponent {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import React from 'react';
|
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 { FormattedMessage } from 'react-intl';
|
||||||
import Icon from 'flavours/glitch/components/icon';
|
import Icon from 'flavours/glitch/components/icon';
|
||||||
import { showTrends } from 'flavours/glitch/util/initial_state';
|
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 FollowRequestsNavLink from './follow_requests_nav_link';
|
||||||
import ListPanel from './list_panel';
|
import ListPanel from './list_panel';
|
||||||
import TrendsContainer from 'flavours/glitch/features/getting_started/containers/trends_container';
|
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'>
|
<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='/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>
|
<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 />
|
<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> }
|
{ 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' 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>
|
<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='/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='/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>
|
<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>}
|
{!!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>
|
<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>}
|
{!!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>
|
</div>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default withRouter(NavigationPanel);
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { submitReport } from 'flavours/glitch/actions/reports';
|
import { submitReport } from 'flavours/glitch/actions/reports';
|
||||||
import { expandAccountTimeline } from 'flavours/glitch/actions/timelines';
|
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 { fetchRelationships } from 'flavours/glitch/actions/accounts';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
@ -119,7 +119,7 @@ class ReportModal extends ImmutablePureComponent {
|
||||||
|
|
||||||
dispatch(fetchRelationships([accountId]));
|
dispatch(fetchRelationships([accountId]));
|
||||||
dispatch(expandAccountTimeline(accountId, { withReplies: true }));
|
dispatch(expandAccountTimeline(accountId, { withReplies: true }));
|
||||||
dispatch(fetchRules());
|
dispatch(fetchServer());
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
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 { uploadCompose, resetCompose, changeComposeSpoilerness } from 'flavours/glitch/actions/compose';
|
||||||
import { expandHomeTimeline } from 'flavours/glitch/actions/timelines';
|
import { expandHomeTimeline } from 'flavours/glitch/actions/timelines';
|
||||||
import { expandNotifications, notificationsSetVisibility } from 'flavours/glitch/actions/notifications';
|
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 { clearHeight } from 'flavours/glitch/actions/height_cache';
|
||||||
import { changeLayout } from 'flavours/glitch/actions/app';
|
import { changeLayout } from 'flavours/glitch/actions/app';
|
||||||
import { synchronouslySubmitMarkers, submitMarkers, fetchMarkers } from 'flavours/glitch/actions/markers';
|
import { synchronouslySubmitMarkers, submitMarkers, fetchMarkers } from 'flavours/glitch/actions/markers';
|
||||||
|
@ -54,9 +54,10 @@ import {
|
||||||
FollowRecommendations,
|
FollowRecommendations,
|
||||||
} from 'flavours/glitch/util/async-components';
|
} from 'flavours/glitch/util/async-components';
|
||||||
import { HotKeys } from 'react-hotkeys';
|
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 { closeOnboarding, INTRODUCTION_VERSION } from 'flavours/glitch/actions/onboarding';
|
||||||
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
|
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.
|
// Dummy import, to make sure that <Status /> ends up in the application bundle.
|
||||||
// Without this it ends up in ~8 very commonly used bundles.
|
// Without this it ends up in ~8 very commonly used bundles.
|
||||||
|
@ -121,6 +122,10 @@ const keyMap = {
|
||||||
|
|
||||||
class SwitchingColumnsArea extends React.PureComponent {
|
class SwitchingColumnsArea extends React.PureComponent {
|
||||||
|
|
||||||
|
static contextTypes = {
|
||||||
|
identity: PropTypes.object,
|
||||||
|
};
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
children: PropTypes.node,
|
children: PropTypes.node,
|
||||||
location: PropTypes.object,
|
location: PropTypes.object,
|
||||||
|
@ -157,12 +162,25 @@ class SwitchingColumnsArea extends React.PureComponent {
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { children, mobile, navbarUnder } = this.props;
|
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 (
|
return (
|
||||||
<ColumnsAreaContainer ref={this.setRef} singleColumn={mobile} navbarUnder={navbarUnder}>
|
<ColumnsAreaContainer ref={this.setRef} singleColumn={mobile} navbarUnder={navbarUnder}>
|
||||||
<WrappedSwitch>
|
<WrappedSwitch>
|
||||||
{redirect}
|
{redirect}
|
||||||
|
|
||||||
<WrappedRoute path='/getting-started' component={GettingStarted} content={children} />
|
<WrappedRoute path='/getting-started' component={GettingStarted} content={children} />
|
||||||
<WrappedRoute path='/keyboard-shortcuts' component={KeyboardShortcuts} content={children} />
|
<WrappedRoute path='/keyboard-shortcuts' component={KeyboardShortcuts} content={children} />
|
||||||
|
|
||||||
|
@ -219,6 +237,10 @@ export default @connect(mapStateToProps)
|
||||||
@withRouter
|
@withRouter
|
||||||
class UI extends React.Component {
|
class UI extends React.Component {
|
||||||
|
|
||||||
|
static contextTypes = {
|
||||||
|
identity: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
dispatch: PropTypes.func.isRequired,
|
dispatch: PropTypes.func.isRequired,
|
||||||
children: PropTypes.node,
|
children: PropTypes.node,
|
||||||
|
@ -358,6 +380,8 @@ class UI extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
|
const { signedIn } = this.context.identity;
|
||||||
|
|
||||||
window.addEventListener('beforeunload', this.handleBeforeUnload, false);
|
window.addEventListener('beforeunload', this.handleBeforeUnload, false);
|
||||||
window.addEventListener('resize', this.handleResize, { passive: true });
|
window.addEventListener('resize', this.handleResize, { passive: true });
|
||||||
|
|
||||||
|
@ -374,16 +398,18 @@ class UI extends React.Component {
|
||||||
this.favicon = new Favico({ animation:"none" });
|
this.favicon = new Favico({ animation:"none" });
|
||||||
|
|
||||||
// On first launch, redirect to the follow recommendations page
|
// 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.context.router.history.replace('/start');
|
||||||
this.props.dispatch(closeOnboarding());
|
this.props.dispatch(closeOnboarding());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (signedIn) {
|
||||||
this.props.dispatch(fetchMarkers());
|
this.props.dispatch(fetchMarkers());
|
||||||
this.props.dispatch(expandHomeTimeline());
|
this.props.dispatch(expandHomeTimeline());
|
||||||
this.props.dispatch(expandNotifications());
|
this.props.dispatch(expandNotifications());
|
||||||
|
|
||||||
setTimeout(() => this.props.dispatch(fetchRules()), 3000);
|
setTimeout(() => this.props.dispatch(fetchServer()), 3000);
|
||||||
|
}
|
||||||
|
|
||||||
this.hotkeys.__mousetrap__.stopCallback = (e, element) => {
|
this.hotkeys.__mousetrap__.stopCallback = (e, element) => {
|
||||||
return ['TEXTAREA', 'SELECT', 'INPUT'].includes(element.tagName);
|
return ['TEXTAREA', 'SELECT', 'INPUT'].includes(element.tagName);
|
||||||
|
@ -635,6 +661,10 @@ class UI extends React.Component {
|
||||||
<LoadingBarContainer className='loading-bar' />
|
<LoadingBarContainer className='loading-bar' />
|
||||||
<ModalContainer />
|
<ModalContainer />
|
||||||
<UploadArea active={draggingOver} onClose={this.closeUploadModal} />
|
<UploadArea active={draggingOver} onClose={this.closeUploadModal} />
|
||||||
|
|
||||||
|
<Helmet>
|
||||||
|
<title>{title}</title>
|
||||||
|
</Helmet>
|
||||||
</div>
|
</div>
|
||||||
</HotKeys>
|
</HotKeys>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
import 'packs/public-path';
|
import 'packs/public-path';
|
||||||
import loadPolyfills from 'flavours/glitch/util/load_polyfills';
|
import loadPolyfills from 'flavours/glitch/util/load_polyfills';
|
||||||
|
|
||||||
loadPolyfills().then(() => {
|
loadPolyfills().then(async () => {
|
||||||
require('flavours/glitch/util/main').default();
|
const { default: main } = await import('flavours/glitch/util/main');
|
||||||
|
|
||||||
|
return main();
|
||||||
}).catch(e => {
|
}).catch(e => {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
});
|
});
|
||||||
|
|
|
@ -17,7 +17,7 @@ import push_notifications from './push_notifications';
|
||||||
import status_lists from './status_lists';
|
import status_lists from './status_lists';
|
||||||
import mutes from './mutes';
|
import mutes from './mutes';
|
||||||
import blocks from './blocks';
|
import blocks from './blocks';
|
||||||
import rules from './rules';
|
import server from './server';
|
||||||
import boosts from './boosts';
|
import boosts from './boosts';
|
||||||
import contexts from './contexts';
|
import contexts from './contexts';
|
||||||
import compose from './compose';
|
import compose from './compose';
|
||||||
|
@ -64,7 +64,7 @@ const reducers = {
|
||||||
push_notifications,
|
push_notifications,
|
||||||
mutes,
|
mutes,
|
||||||
blocks,
|
blocks,
|
||||||
rules,
|
server,
|
||||||
boosts,
|
boosts,
|
||||||
contexts,
|
contexts,
|
||||||
compose,
|
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;
|
|
||||||
}
|
|
||||||
}
|
|
19
app/javascript/flavours/glitch/reducers/server.js
Normal file
19
app/javascript/flavours/glitch/reducers/server.js
Normal file
|
@ -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;
|
font-family: inherit;
|
||||||
background: $ui-base-color;
|
background: $ui-base-color;
|
||||||
color: $darker-text-color;
|
color: $darker-text-color;
|
||||||
|
border-radius: 4px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -117,6 +117,7 @@
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background: lighten($ui-base-color, 4%);
|
background: lighten($ui-base-color, 4%);
|
||||||
|
border-radius: 4px 4px 0 0;
|
||||||
color: $highlight-text-color;
|
color: $highlight-text-color;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
|
@ -204,6 +205,17 @@
|
||||||
color: $highlight-text-color;
|
color: $highlight-text-color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--logo {
|
||||||
|
background: transparent;
|
||||||
|
padding: 10px;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus,
|
||||||
|
&:active {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.column-link__icon {
|
.column-link__icon {
|
||||||
|
@ -255,6 +267,7 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
background: lighten($ui-base-color, 4%);
|
background: lighten($ui-base-color, 4%);
|
||||||
|
border-radius: 4px 4px 0 0;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -309,6 +322,7 @@
|
||||||
|
|
||||||
> .scrollable {
|
> .scrollable {
|
||||||
background: $ui-base-color;
|
background: $ui-base-color;
|
||||||
|
border-radius: 0 0 4px 4px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -352,6 +366,11 @@
|
||||||
&:focus {
|
&:focus {
|
||||||
text-shadow: 0 0 4px darken($ui-highlight-color, 5%);
|
text-shadow: 0 0 4px darken($ui-highlight-color, 5%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
color: $dark-text-color;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.column-header__notif-cleaning-buttons {
|
.column-header__notif-cleaning-buttons {
|
||||||
|
|
|
@ -110,6 +110,27 @@
|
||||||
&:hover {
|
&:hover {
|
||||||
border-color: lighten($ui-primary-color, 4%);
|
border-color: lighten($ui-primary-color, 4%);
|
||||||
color: lighten($darker-text-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 {
|
&:disabled {
|
||||||
|
@ -1756,3 +1777,4 @@ noscript {
|
||||||
@import 'single_column';
|
@import 'single_column';
|
||||||
@import 'announcements';
|
@import 'announcements';
|
||||||
@import 'explore';
|
@import 'explore';
|
||||||
|
@import 'signed_out';
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -1304,3 +1305,123 @@ img.modal-warning {
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
width: 60px;
|
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);
|
height: calc(100% - 10px);
|
||||||
overflow-y: hidden;
|
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 {
|
.search__input {
|
||||||
line-height: 18px;
|
line-height: 18px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
@ -21,10 +41,6 @@
|
||||||
flex: 0 1 48px;
|
flex: 0 1 48px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.flex-spacer {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.composer {
|
.composer {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
|
@ -61,6 +77,14 @@
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
height: 30px;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.navigation-panel,
|
||||||
|
.compose-panel {
|
||||||
hr {
|
hr {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
border: 0;
|
border: 0;
|
||||||
|
|
|
@ -11,6 +11,7 @@ const initialState = element && function () {
|
||||||
|
|
||||||
const getMeta = (prop) => initialState && initialState.meta && initialState.meta[prop];
|
const getMeta = (prop) => initialState && initialState.meta && initialState.meta[prop];
|
||||||
|
|
||||||
|
export const domain = getMeta('domain');
|
||||||
export const reduceMotion = getMeta('reduce_motion');
|
export const reduceMotion = getMeta('reduce_motion');
|
||||||
export const autoPlayGif = getMeta('auto_play_gif');
|
export const autoPlayGif = getMeta('auto_play_gif');
|
||||||
export const displayMedia = getMeta('display_media') || (getMeta('display_sensitive_media') ? 'show_all' : 'default');
|
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 maxChars = (initialState && initialState.max_toot_chars) || 500;
|
||||||
export const pollLimits = (initialState && initialState.poll_limits);
|
export const pollLimits = (initialState && initialState.poll_limits);
|
||||||
export const limitedFederationMode = getMeta('limited_federation_mode');
|
export const limitedFederationMode = getMeta('limited_federation_mode');
|
||||||
|
export const registrationsOpen = getMeta('registrations_open');
|
||||||
export const repository = getMeta('repository');
|
export const repository = getMeta('repository');
|
||||||
export const source_url = getMeta('source_url');
|
export const source_url = getMeta('source_url');
|
||||||
export const version = getMeta('version');
|
export const version = getMeta('version');
|
||||||
export const mascot = getMeta('mascot');
|
export const mascot = getMeta('mascot');
|
||||||
export const profile_directory = getMeta('profile_directory');
|
export const profile_directory = getMeta('profile_directory');
|
||||||
export const defaultContentType = getMeta('default_content_type');
|
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 useBlurhash = getMeta('use_blurhash');
|
||||||
export const usePendingItems = getMeta('use_pending_items');
|
export const usePendingItems = getMeta('use_pending_items');
|
||||||
export const useSystemEmojiFont = getMeta('system_emoji_font');
|
export const useSystemEmojiFont = getMeta('system_emoji_font');
|
||||||
export const showTrends = getMeta('trends');
|
export const showTrends = getMeta('trends');
|
||||||
|
export const title = getMeta('title');
|
||||||
export const disableSwiping = getMeta('disable_swiping');
|
export const disableSwiping = getMeta('disable_swiping');
|
||||||
export const languages = initialState && initialState.languages;
|
export const languages = initialState && initialState.languages;
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import * as registerPushNotifications from 'flavours/glitch/actions/push_notifications';
|
|
||||||
import { setupBrowserNotifications } from 'flavours/glitch/actions/notifications';
|
import { setupBrowserNotifications } from 'flavours/glitch/actions/notifications';
|
||||||
import Mastodon, { store } from 'flavours/glitch/containers/mastodon';
|
import Mastodon, { store } from 'flavours/glitch/containers/mastodon';
|
||||||
import ready from 'flavours/glitch/util/ready';
|
import ready from 'flavours/glitch/util/ready';
|
||||||
|
|
||||||
const perf = require('./performance');
|
const perf = require('flavours/glitch/util/performance');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
function main() {
|
function main() {
|
||||||
perf.start('main()');
|
perf.start('main()');
|
||||||
|
|
||||||
|
@ -18,7 +20,7 @@ function main() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ready(() => {
|
return ready(async () => {
|
||||||
const mountNode = document.getElementById('mastodon');
|
const mountNode = document.getElementById('mastodon');
|
||||||
const props = JSON.parse(mountNode.getAttribute('data-props'));
|
const props = JSON.parse(mountNode.getAttribute('data-props'));
|
||||||
|
|
||||||
|
@ -26,19 +28,28 @@ function main() {
|
||||||
store.dispatch(setupBrowserNotifications());
|
store.dispatch(setupBrowserNotifications());
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
|
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
|
||||||
import('workbox-window')
|
const [{ Workbox }, { me }] = await Promise.all([
|
||||||
.then(({ Workbox }) => {
|
import('workbox-window'),
|
||||||
|
import('mastodon/initial_state'),
|
||||||
|
]);
|
||||||
|
|
||||||
const wb = new Workbox('/sw.js');
|
const wb = new Workbox('/sw.js');
|
||||||
|
|
||||||
return wb.register();
|
try {
|
||||||
})
|
await wb.register();
|
||||||
.then(() => {
|
} catch (err) {
|
||||||
store.dispatch(registerPushNotifications.register());
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
console.error(err);
|
console.error(err);
|
||||||
});
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (me) {
|
||||||
|
const registerPushNotifications = await import('flavours/glitch/actions/push_notifications');
|
||||||
|
|
||||||
|
store.dispatch(registerPushNotifications.register());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
perf.stop('main()');
|
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)) {
|
if (['interactive', 'complete'].includes(document.readyState)) {
|
||||||
loaded();
|
loaded();
|
||||||
} else {
|
} else {
|
||||||
document.addEventListener('DOMContentLoaded', loaded);
|
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,
|
|
||||||
});
|
|
30
app/javascript/mastodon/actions/server.js
Normal file
30
app/javascript/mastodon/actions/server.js
Normal file
|
@ -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 ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import { me } from '../initial_state';
|
import { me } from '../initial_state';
|
||||||
import RelativeTimestamp from './relative_timestamp';
|
import RelativeTimestamp from './relative_timestamp';
|
||||||
|
import Skeleton from 'mastodon/components/skeleton';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
follow: { id: 'account.follow', defaultMessage: 'Follow' },
|
follow: { id: 'account.follow', defaultMessage: 'Follow' },
|
||||||
|
@ -26,7 +27,7 @@ export default @injectIntl
|
||||||
class Account extends ImmutablePureComponent {
|
class Account extends ImmutablePureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
account: ImmutablePropTypes.map.isRequired,
|
account: ImmutablePropTypes.map,
|
||||||
onFollow: PropTypes.func.isRequired,
|
onFollow: PropTypes.func.isRequired,
|
||||||
onBlock: PropTypes.func.isRequired,
|
onBlock: PropTypes.func.isRequired,
|
||||||
onMute: 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;
|
const { account, intl, hidden, onActionClick, actionIcon, actionTitle, defaultAction } = this.props;
|
||||||
|
|
||||||
if (!account) {
|
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) {
|
if (hidden) {
|
||||||
|
|
|
@ -2,11 +2,12 @@ import React from 'react';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { autoPlayGif } from 'mastodon/initial_state';
|
import { autoPlayGif } from 'mastodon/initial_state';
|
||||||
|
import Skeleton from 'mastodon/components/skeleton';
|
||||||
|
|
||||||
export default class DisplayName extends React.PureComponent {
|
export default class DisplayName extends React.PureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
account: ImmutablePropTypes.map.isRequired,
|
account: ImmutablePropTypes.map,
|
||||||
others: ImmutablePropTypes.list,
|
others: ImmutablePropTypes.list,
|
||||||
localDomain: PropTypes.string,
|
localDomain: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
@ -48,7 +49,7 @@ export default class DisplayName extends React.PureComponent {
|
||||||
if (others.size - 2 > 0) {
|
if (others.size - 2 > 0) {
|
||||||
suffix = `+${others.size - 2}`;
|
suffix = `+${others.size - 2}`;
|
||||||
}
|
}
|
||||||
} else {
|
} else if ((others && others.size > 0) || this.props.account) {
|
||||||
if (others && others.size > 0) {
|
if (others && others.size > 0) {
|
||||||
account = others.first();
|
account = others.first();
|
||||||
} else {
|
} 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>;
|
displayName = <bdi><strong className='display-name__html' dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }} /></bdi>;
|
||||||
suffix = <span className='display-name__account'>@{acct}</span>;
|
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 (
|
return (
|
||||||
|
|
91
app/javascript/mastodon/components/server_banner.js
Normal file
91
app/javascript/mastodon/components/server_banner.js
Normal file
|
@ -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';
|
import Option from './components/option';
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
rules: state.get('rules'),
|
rules: state.getIn(['server', 'rules']),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default @connect(mapStateToProps)
|
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 ComposeFormContainer from 'mastodon/features/compose/containers/compose_form_container';
|
||||||
import NavigationContainer from 'mastodon/features/compose/containers/navigation_container';
|
import NavigationContainer from 'mastodon/features/compose/containers/navigation_container';
|
||||||
import LinkFooter from './link_footer';
|
import LinkFooter from './link_footer';
|
||||||
|
import ServerBanner from 'mastodon/components/server_banner';
|
||||||
import { changeComposing } from 'mastodon/actions/compose';
|
import { changeComposing } from 'mastodon/actions/compose';
|
||||||
|
|
||||||
export default @connect()
|
export default @connect()
|
||||||
|
@ -35,6 +36,7 @@ class ComposePanel extends React.PureComponent {
|
||||||
|
|
||||||
{!signedIn && (
|
{!signedIn && (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
|
<ServerBanner />
|
||||||
<div className='flex-spacer' />
|
<div className='flex-spacer' />
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { submitReport } from 'mastodon/actions/reports';
|
import { submitReport } from 'mastodon/actions/reports';
|
||||||
import { expandAccountTimeline } from 'mastodon/actions/timelines';
|
import { expandAccountTimeline } from 'mastodon/actions/timelines';
|
||||||
import { fetchRules } from 'mastodon/actions/rules';
|
import { fetchServer } from 'mastodon/actions/server';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import { makeGetAccount } from 'mastodon/selectors';
|
import { makeGetAccount } from 'mastodon/selectors';
|
||||||
|
@ -117,7 +117,7 @@ class ReportModal extends ImmutablePureComponent {
|
||||||
const { dispatch, accountId } = this.props;
|
const { dispatch, accountId } = this.props;
|
||||||
|
|
||||||
dispatch(expandAccountTimeline(accountId, { withReplies: true }));
|
dispatch(expandAccountTimeline(accountId, { withReplies: true }));
|
||||||
dispatch(fetchRules());
|
dispatch(fetchServer());
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
|
|
|
@ -13,7 +13,7 @@ import { debounce } from 'lodash';
|
||||||
import { uploadCompose, resetCompose, changeComposeSpoilerness } from '../../actions/compose';
|
import { uploadCompose, resetCompose, changeComposeSpoilerness } from '../../actions/compose';
|
||||||
import { expandHomeTimeline } from '../../actions/timelines';
|
import { expandHomeTimeline } from '../../actions/timelines';
|
||||||
import { expandNotifications } from '../../actions/notifications';
|
import { expandNotifications } from '../../actions/notifications';
|
||||||
import { fetchRules } from '../../actions/rules';
|
import { fetchServer } from '../../actions/server';
|
||||||
import { clearHeight } from '../../actions/height_cache';
|
import { clearHeight } from '../../actions/height_cache';
|
||||||
import { focusApp, unfocusApp, changeLayout } from 'mastodon/actions/app';
|
import { focusApp, unfocusApp, changeLayout } from 'mastodon/actions/app';
|
||||||
import { synchronouslySubmitMarkers, submitMarkers, fetchMarkers } from 'mastodon/actions/markers';
|
import { synchronouslySubmitMarkers, submitMarkers, fetchMarkers } from 'mastodon/actions/markers';
|
||||||
|
@ -389,7 +389,7 @@ class UI extends React.PureComponent {
|
||||||
this.props.dispatch(expandHomeTimeline());
|
this.props.dispatch(expandHomeTimeline());
|
||||||
this.props.dispatch(expandNotifications());
|
this.props.dispatch(expandNotifications());
|
||||||
|
|
||||||
setTimeout(() => this.props.dispatch(fetchRules()), 3000);
|
setTimeout(() => this.props.dispatch(fetchServer()), 3000);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.hotkeys.__mousetrap__.stopCallback = (e, element) => {
|
this.hotkeys.__mousetrap__.stopCallback = (e, element) => {
|
||||||
|
|
|
@ -29,6 +29,5 @@ export const title = getMeta('title');
|
||||||
export const cropImages = getMeta('crop_images');
|
export const cropImages = getMeta('crop_images');
|
||||||
export const disableSwiping = getMeta('disable_swiping');
|
export const disableSwiping = getMeta('disable_swiping');
|
||||||
export const languages = initialState && initialState.languages;
|
export const languages = initialState && initialState.languages;
|
||||||
export const server = initialState && initialState.server;
|
|
||||||
|
|
||||||
export default initialState;
|
export default initialState;
|
||||||
|
|
|
@ -17,7 +17,7 @@ import status_lists from './status_lists';
|
||||||
import mutes from './mutes';
|
import mutes from './mutes';
|
||||||
import blocks from './blocks';
|
import blocks from './blocks';
|
||||||
import boosts from './boosts';
|
import boosts from './boosts';
|
||||||
import rules from './rules';
|
import server from './server';
|
||||||
import contexts from './contexts';
|
import contexts from './contexts';
|
||||||
import compose from './compose';
|
import compose from './compose';
|
||||||
import search from './search';
|
import search from './search';
|
||||||
|
@ -62,7 +62,7 @@ const reducers = {
|
||||||
mutes,
|
mutes,
|
||||||
blocks,
|
blocks,
|
||||||
boosts,
|
boosts,
|
||||||
rules,
|
server,
|
||||||
contexts,
|
contexts,
|
||||||
compose,
|
compose,
|
||||||
search,
|
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;
|
|
||||||
}
|
|
||||||
}
|
|
19
app/javascript/mastodon/reducers/server.js
Normal file
19
app/javascript/mastodon/reducers/server.js
Normal file
|
@ -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])
|
find_status_url_by_id(path_segments[2])
|
||||||
elsif path_segments[1] == 'accounts' && path_segments[2] =~ /\d/
|
elsif path_segments[1] == 'accounts' && path_segments[2] =~ /\d/
|
||||||
find_account_url_by_id(path_segments[2])
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,20 +1,52 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class InstancePresenter
|
class InstancePresenter < ActiveModelSerializers::Model
|
||||||
delegate(
|
attributes :domain, :title, :version, :source_url,
|
||||||
:site_contact_email,
|
:description, :languages, :rules, :contact
|
||||||
:site_title,
|
|
||||||
:site_short_description,
|
|
||||||
:site_description,
|
|
||||||
:site_extended_description,
|
|
||||||
:site_terms,
|
|
||||||
:closed_registrations_message,
|
|
||||||
to: Setting
|
|
||||||
)
|
|
||||||
|
|
||||||
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@/, ''))
|
Account.find_local(Setting.site_contact_username.strip.gsub(/\A@/, ''))
|
||||||
end
|
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
|
def rules
|
||||||
Rule.ordered
|
Rule.ordered
|
||||||
|
@ -40,8 +72,8 @@ class InstancePresenter
|
||||||
Rails.cache.fetch('sample_accounts', expires_in: 12.hours) { Account.local.discoverable.popular.limit(3) }
|
Rails.cache.fetch('sample_accounts', expires_in: 12.hours) { Account.local.discoverable.popular.limit(3) }
|
||||||
end
|
end
|
||||||
|
|
||||||
def version_number
|
def version
|
||||||
Mastodon::Version
|
Mastodon::Version.to_s
|
||||||
end
|
end
|
||||||
|
|
||||||
def source_url
|
def source_url
|
||||||
|
|
|
@ -6,7 +6,7 @@ class InitialStateSerializer < ActiveModel::Serializer
|
||||||
attributes :meta, :compose, :accounts,
|
attributes :meta, :compose, :accounts,
|
||||||
:media_attachments, :settings,
|
:media_attachments, :settings,
|
||||||
:max_toot_chars, :poll_limits,
|
:max_toot_chars, :poll_limits,
|
||||||
:languages, :server
|
:languages
|
||||||
|
|
||||||
has_one :push_subscription, serializer: REST::WebPushSubscriptionSerializer
|
has_one :push_subscription, serializer: REST::WebPushSubscriptionSerializer
|
||||||
has_one :role, serializer: REST::RoleSerializer
|
has_one :role, serializer: REST::RoleSerializer
|
||||||
|
@ -24,18 +24,19 @@ class InitialStateSerializer < ActiveModel::Serializer
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# rubocop:disable Metrics/AbcSize
|
||||||
def meta
|
def meta
|
||||||
store = {
|
store = {
|
||||||
streaming_api_base_url: Rails.configuration.x.streaming_api_base_url,
|
streaming_api_base_url: Rails.configuration.x.streaming_api_base_url,
|
||||||
access_token: object.token,
|
access_token: object.token,
|
||||||
locale: I18n.locale,
|
locale: I18n.locale,
|
||||||
domain: Rails.configuration.x.local_domain,
|
domain: instance_presenter.domain,
|
||||||
title: instance_presenter.site_title,
|
title: instance_presenter.title,
|
||||||
admin: object.admin&.id&.to_s,
|
admin: object.admin&.id&.to_s,
|
||||||
search_enabled: Chewy.enabled?,
|
search_enabled: Chewy.enabled?,
|
||||||
repository: Mastodon::Version.repository,
|
repository: Mastodon::Version.repository,
|
||||||
source_url: Mastodon::Version.source_url,
|
source_url: instance_presenter.source_url,
|
||||||
version: Mastodon::Version.to_s,
|
version: instance_presenter.version,
|
||||||
limited_federation_mode: Rails.configuration.x.whitelist_mode,
|
limited_federation_mode: Rails.configuration.x.whitelist_mode,
|
||||||
mascot: instance_presenter.mascot&.file&.url,
|
mascot: instance_presenter.mascot&.file&.url,
|
||||||
profile_directory: Setting.profile_directory,
|
profile_directory: Setting.profile_directory,
|
||||||
|
@ -71,6 +72,7 @@ class InitialStateSerializer < ActiveModel::Serializer
|
||||||
|
|
||||||
store
|
store
|
||||||
end
|
end
|
||||||
|
# rubocop:enable Metrics/AbcSize
|
||||||
|
|
||||||
def compose
|
def compose
|
||||||
store = {}
|
store = {}
|
||||||
|
@ -102,13 +104,6 @@ class InitialStateSerializer < ActiveModel::Serializer
|
||||||
LanguagesHelper::SUPPORTED_LOCALES.map { |(key, value)| [key, value[0], value[1]] }
|
LanguagesHelper::SUPPORTED_LOCALES.map { |(key, value)| [key, value[0], value[1]] }
|
||||||
end
|
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
|
private
|
||||||
|
|
||||||
def instance_presenter
|
def instance_presenter
|
||||||
|
|
|
@ -22,11 +22,11 @@ class ManifestSerializer < ActiveModel::Serializer
|
||||||
:share_target, :shortcuts
|
:share_target, :shortcuts
|
||||||
|
|
||||||
def name
|
def name
|
||||||
object.site_title
|
object.title
|
||||||
end
|
end
|
||||||
|
|
||||||
def short_name
|
def short_name
|
||||||
object.site_title
|
object.title
|
||||||
end
|
end
|
||||||
|
|
||||||
def icons
|
def icons
|
||||||
|
|
|
@ -1,74 +1,39 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class REST::InstanceSerializer < ActiveModel::Serializer
|
class REST::InstanceSerializer < ActiveModel::Serializer
|
||||||
|
class ContactSerializer < ActiveModel::Serializer
|
||||||
|
attributes :email
|
||||||
|
|
||||||
|
has_one :account, serializer: REST::AccountSerializer
|
||||||
|
end
|
||||||
|
|
||||||
include RoutingHelper
|
include RoutingHelper
|
||||||
|
|
||||||
attributes :uri, :title, :short_description, :description, :email,
|
attributes :domain, :title, :version, :source_url, :description,
|
||||||
:version, :urls, :stats, :thumbnail, :max_toot_chars, :poll_limits,
|
:usage, :thumbnail, :languages, :configuration,
|
||||||
:languages, :registrations, :approval_required, :invites_enabled,
|
:registrations
|
||||||
:configuration
|
|
||||||
|
|
||||||
has_one :contact_account, serializer: REST::AccountSerializer
|
|
||||||
|
|
||||||
|
has_one :contact, serializer: ContactSerializer
|
||||||
has_many :rules, serializer: REST::RuleSerializer
|
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
|
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
|
end
|
||||||
|
|
||||||
def max_toot_chars
|
def usage
|
||||||
StatusLengthValidator::MAX_CHARS
|
|
||||||
end
|
|
||||||
|
|
||||||
def poll_limits
|
|
||||||
{
|
{
|
||||||
max_options: PollValidator::MAX_OPTIONS,
|
users: {
|
||||||
max_option_chars: PollValidator::MAX_OPTION_CHARS,
|
active_month: object.active_user_count(4),
|
||||||
min_expiration: PollValidator::MIN_EXPIRATION,
|
},
|
||||||
max_expiration: PollValidator::MAX_EXPIRATION,
|
|
||||||
}
|
}
|
||||||
end
|
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
|
def configuration
|
||||||
{
|
{
|
||||||
|
urls: {
|
||||||
|
streaming: Rails.configuration.x.streaming_api_base_url,
|
||||||
|
},
|
||||||
|
|
||||||
statuses: {
|
statuses: {
|
||||||
max_characters: StatusLengthValidator::MAX_CHARS,
|
max_characters: StatusLengthValidator::MAX_CHARS,
|
||||||
max_media_attachments: 4,
|
max_media_attachments: 4,
|
||||||
|
@ -93,25 +58,10 @@ class REST::InstanceSerializer < ActiveModel::Serializer
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
def languages
|
|
||||||
[I18n.default_locale]
|
|
||||||
end
|
|
||||||
|
|
||||||
def registrations
|
def registrations
|
||||||
Setting.registrations_mode != 'none' && !Rails.configuration.x.single_user_mode
|
{
|
||||||
end
|
enabled: Setting.registrations_mode != 'none' && !Rails.configuration.x.single_user_mode,
|
||||||
|
approval_required: Setting.registrations_mode == 'approved',
|
||||||
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
|
||||||
end
|
end
|
||||||
|
|
115
app/serializers/rest/v1/instance_serializer.rb
Normal file
115
app/serializers/rest/v1/instance_serializer.rb
Normal file
|
@ -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
|
.column-0
|
||||||
.public-account-header.public-account-header--no-bar
|
.public-account-header.public-account-header--no-bar
|
||||||
.public-account-header__image
|
.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
|
.column-1
|
||||||
.landing-page__call-to-action{ dir: 'ltr' }
|
.landing-page__call-to-action{ dir: 'ltr' }
|
||||||
|
@ -30,14 +30,14 @@
|
||||||
.contact-widget
|
.contact-widget
|
||||||
%h4= t 'about.administered_by'
|
%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
|
%h4
|
||||||
= succeed ':' do
|
= succeed ':' do
|
||||||
= t 'about.contact'
|
= 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
|
.column-3
|
||||||
= render 'application/flashes'
|
= render 'application/flashes'
|
||||||
|
|
|
@ -53,11 +53,11 @@
|
||||||
|
|
||||||
.hero-widget
|
.hero-widget
|
||||||
.hero-widget__img
|
.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
|
.hero-widget__text
|
||||||
%p
|
%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
|
= link_to about_more_path do
|
||||||
= t('about.learn_more')
|
= t('about.learn_more')
|
||||||
= fa_icon 'angle-double-right'
|
= fa_icon 'angle-double-right'
|
||||||
|
@ -66,7 +66,7 @@
|
||||||
.hero-widget__footer__column
|
.hero-widget__footer__column
|
||||||
%h4= t 'about.administered_by'
|
%h4= t 'about.administered_by'
|
||||||
|
|
||||||
= account_link_to @instance_presenter.contact_account
|
= account_link_to @instance_presenter.contact.account
|
||||||
|
|
||||||
.hero-widget__footer__column
|
.hero-widget__footer__column
|
||||||
%h4= t 'about.server_stats'
|
%h4= t 'about.server_stats'
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
.hero-widget
|
.hero-widget
|
||||||
.hero-widget__img
|
.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
|
.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)
|
- if Setting.trends && !(user_signed_in? && !current_user.setting_trends)
|
||||||
- trends = Trends.tags.query.allowed.limit(3)
|
- trends = Trends.tags.query.allowed.limit(3)
|
||||||
|
|
|
@ -1,10 +1,14 @@
|
||||||
- content_for :header_tags do
|
- content_for :header_tags do
|
||||||
|
- if user_signed_in?
|
||||||
= preload_pack_asset 'features/getting_started.js', crossorigin: 'anonymous'
|
= preload_pack_asset 'features/getting_started.js', crossorigin: 'anonymous'
|
||||||
= preload_pack_asset 'features/compose.js', crossorigin: 'anonymous'
|
= preload_pack_asset 'features/compose.js', crossorigin: 'anonymous'
|
||||||
= preload_pack_asset 'features/home_timeline.js', crossorigin: 'anonymous'
|
= preload_pack_asset 'features/home_timeline.js', crossorigin: 'anonymous'
|
||||||
= preload_pack_asset 'features/notifications.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}
|
%meta{name: 'applicationServerKey', content: Rails.configuration.x.vapid_public_key}
|
||||||
|
|
||||||
= render_initial_state
|
= render_initial_state
|
||||||
|
|
||||||
.notranslate.app-holder#mastodon{ data: { props: Oj.dump(default_props) } }
|
.notranslate.app-holder#mastodon{ data: { props: Oj.dump(default_props) } }
|
||||||
|
|
|
@ -4,6 +4,6 @@
|
||||||
.grid
|
.grid
|
||||||
.column-0
|
.column-0
|
||||||
.box-widget
|
.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
|
.column-1
|
||||||
= render 'application/sidebar'
|
= render 'application/sidebar'
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
- thumbnail = @instance_presenter.thumbnail
|
- 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 }/
|
%meta{ name: 'description', content: description }/
|
||||||
|
|
||||||
= opengraph 'og:site_name', t('about.hosted_on', domain: site_hostname)
|
= opengraph 'og:site_name', t('about.hosted_on', domain: site_hostname)
|
||||||
= opengraph 'og:url', url_for(only_path: false)
|
= opengraph 'og:url', url_for(only_path: false)
|
||||||
= opengraph 'og:type', 'website'
|
= opengraph 'og:type', 'website'
|
||||||
= opengraph 'og:title', @instance_presenter.site_title
|
= opengraph 'og:title', @instance_presenter.title
|
||||||
= opengraph 'og:description', description
|
= 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', full_asset_url(thumbnail&.file&.url || asset_pack_path('media/images/preview.png', protocol: :request))
|
||||||
= opengraph 'og:image:width', thumbnail ? thumbnail.meta['width'] : '1200'
|
= opengraph 'og:image:width', thumbnail ? thumbnail.meta['width'] : '1200'
|
||||||
|
|
|
@ -639,10 +639,12 @@ Rails.application.routes.draw do
|
||||||
end
|
end
|
||||||
|
|
||||||
namespace :v2 do
|
namespace :v2 do
|
||||||
resources :media, only: [:create]
|
|
||||||
get '/search', to: 'search#index', as: :search
|
get '/search', to: 'search#index', as: :search
|
||||||
|
|
||||||
|
resources :media, only: [:create]
|
||||||
resources :suggestions, only: [:index]
|
resources :suggestions, only: [:index]
|
||||||
resources :filters, only: [:index, :create, :show, :update, :destroy]
|
resources :filters, only: [:index, :create, :show, :update, :destroy]
|
||||||
|
resource :instance, only: [:show]
|
||||||
|
|
||||||
namespace :admin do
|
namespace :admin do
|
||||||
resources :accounts, only: [:index]
|
resources :accounts, only: [:index]
|
||||||
|
|
|
@ -8,9 +8,9 @@ RSpec.describe HomeController, type: :controller do
|
||||||
|
|
||||||
context 'when not signed in' do
|
context 'when not signed in' do
|
||||||
context 'when requested path is tag timeline' 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'
|
@request.path = '/web/timelines/tag/name'
|
||||||
is_expected.to redirect_to '/tags/name'
|
is_expected.to have_http_status(:success)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -23,11 +23,12 @@ RSpec.describe HomeController, type: :controller do
|
||||||
context 'when signed in' do
|
context 'when signed in' do
|
||||||
let(:user) { Fabricate(:user) }
|
let(:user) { Fabricate(:user) }
|
||||||
|
|
||||||
before { sign_in(user) }
|
before do
|
||||||
|
sign_in(user)
|
||||||
|
end
|
||||||
|
|
||||||
it 'assigns @body_classes' do
|
it 'returns http success' do
|
||||||
subject
|
is_expected.to have_http_status(:success)
|
||||||
expect(assigns(:body_classes)).to eq 'app-body'
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -21,7 +21,7 @@ describe PermalinkRedirector do
|
||||||
|
|
||||||
it 'returns path for legacy tag links' do
|
it 'returns path for legacy tag links' do
|
||||||
redirector = described_class.new('web/timelines/tag/hoge')
|
redirector = described_class.new('web/timelines/tag/hoge')
|
||||||
expect(redirector.redirect_path).to eq '/tags/hoge'
|
expect(redirector.redirect_path).to be_nil
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns path for pretty account links' do
|
it 'returns path for pretty account links' do
|
||||||
|
@ -36,7 +36,7 @@ describe PermalinkRedirector do
|
||||||
|
|
||||||
it 'returns path for pretty tag links' do
|
it 'returns path for pretty tag links' do
|
||||||
redirector = described_class.new('web/tags/hoge')
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,21 +3,20 @@ require 'rails_helper'
|
||||||
describe InstancePresenter do
|
describe InstancePresenter do
|
||||||
let(:instance_presenter) { InstancePresenter.new }
|
let(:instance_presenter) { InstancePresenter.new }
|
||||||
|
|
||||||
context do
|
describe '#description' do
|
||||||
around do |example|
|
around do |example|
|
||||||
site_description = Setting.site_description
|
site_description = Setting.site_short_description
|
||||||
example.run
|
example.run
|
||||||
Setting.site_description = site_description
|
Setting.site_short_description = site_description
|
||||||
end
|
end
|
||||||
|
|
||||||
it "delegates site_description to Setting" do
|
it "delegates site_description to Setting" do
|
||||||
Setting.site_description = "Site desc"
|
Setting.site_short_description = "Site desc"
|
||||||
|
expect(instance_presenter.description).to eq "Site desc"
|
||||||
expect(instance_presenter.site_description).to eq "Site desc"
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context do
|
describe '#extended_description' do
|
||||||
around do |example|
|
around do |example|
|
||||||
site_extended_description = Setting.site_extended_description
|
site_extended_description = Setting.site_extended_description
|
||||||
example.run
|
example.run
|
||||||
|
@ -26,12 +25,11 @@ describe InstancePresenter do
|
||||||
|
|
||||||
it "delegates site_extended_description to Setting" do
|
it "delegates site_extended_description to Setting" do
|
||||||
Setting.site_extended_description = "Extended desc"
|
Setting.site_extended_description = "Extended desc"
|
||||||
|
expect(instance_presenter.extended_description).to eq "Extended desc"
|
||||||
expect(instance_presenter.site_extended_description).to eq "Extended desc"
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context do
|
describe '#email' do
|
||||||
around do |example|
|
around do |example|
|
||||||
site_contact_email = Setting.site_contact_email
|
site_contact_email = Setting.site_contact_email
|
||||||
example.run
|
example.run
|
||||||
|
@ -40,12 +38,11 @@ describe InstancePresenter do
|
||||||
|
|
||||||
it "delegates contact_email to Setting" do
|
it "delegates contact_email to Setting" do
|
||||||
Setting.site_contact_email = "admin@example.com"
|
Setting.site_contact_email = "admin@example.com"
|
||||||
|
expect(instance_presenter.contact.email).to eq "admin@example.com"
|
||||||
expect(instance_presenter.site_contact_email).to eq "admin@example.com"
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "contact_account" do
|
describe '#account' do
|
||||||
around do |example|
|
around do |example|
|
||||||
site_contact_username = Setting.site_contact_username
|
site_contact_username = Setting.site_contact_username
|
||||||
example.run
|
example.run
|
||||||
|
@ -55,12 +52,11 @@ describe InstancePresenter do
|
||||||
it "returns the account for the site contact username" do
|
it "returns the account for the site contact username" do
|
||||||
Setting.site_contact_username = "aaa"
|
Setting.site_contact_username = "aaa"
|
||||||
account = Fabricate(:account, 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
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "user_count" do
|
describe '#user_count' do
|
||||||
it "returns the number of site users" do
|
it "returns the number of site users" do
|
||||||
Rails.cache.write 'user_count', 123
|
Rails.cache.write 'user_count', 123
|
||||||
|
|
||||||
|
@ -68,7 +64,7 @@ describe InstancePresenter do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "status_count" do
|
describe '#status_count' do
|
||||||
it "returns the number of local statuses" do
|
it "returns the number of local statuses" do
|
||||||
Rails.cache.write 'local_status_count', 234
|
Rails.cache.write 'local_status_count', 234
|
||||||
|
|
||||||
|
@ -76,7 +72,7 @@ describe InstancePresenter do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "domain_count" do
|
describe '#domain_count' do
|
||||||
it "returns the number of known domains" do
|
it "returns the number of known domains" do
|
||||||
Rails.cache.write 'distinct_domain_count', 345
|
Rails.cache.write 'distinct_domain_count', 345
|
||||||
|
|
||||||
|
@ -84,9 +80,9 @@ describe InstancePresenter do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#version_number' do
|
describe '#version' do
|
||||||
it 'returns Mastodon::Version' do
|
it 'returns string' do
|
||||||
expect(instance_presenter.version_number).to be(Mastodon::Version)
|
expect(instance_presenter.version).to be_a String
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -14,26 +14,7 @@ describe 'about/show.html.haml', without_verify_partial_doubles: true do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'has valid open graph tags' do
|
it 'has valid open graph tags' do
|
||||||
instance_presenter = double(
|
assign(:instance_presenter, InstancePresenter.new)
|
||||||
: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)
|
|
||||||
render
|
render
|
||||||
|
|
||||||
header_tags = view.content_for(:header_tags)
|
header_tags = view.content_for(:header_tags)
|
||||||
|
|
Loading…
Reference in a new issue